Improved progress display

Previously, the progress bar updated only after completing an entire table.

For large tables such as LIBARC, this could make the application appear to have stopped responding.

Phase 10 updates the interface while rows are being exported.
This commit is contained in:
Mark Gregory
2026-10-16 22:24:53 +01:00
parent fb1430ad1a
commit ff40ddd1a9
3 changed files with 134 additions and 0 deletions
+106
View File
@@ -1355,6 +1355,11 @@ enum WorkbenchAdminPage: String, CaseIterable, Identifiable {
}
}
private enum WorkbenchProcessAction: String {
case query = "Stop query for"
case connection = "Disconnect"
}
struct WorkbenchAdministrationView: View {
let page: WorkbenchAdminPage
let profile: ConnectionProfile?
@@ -1371,6 +1376,11 @@ struct WorkbenchAdministrationView: View {
@State private var grantsError: String? = nil
@State private var showingGrants = false
@State private var showingAccountManager = false
@State private var selectedProcessID: String? = nil
@State private var pendingProcessAction: WorkbenchProcessAction? = nil
@State private var processActionError: String? = nil
@State private var processActionStatus: String? = nil
@State private var terminatingProcess = false
private var filteredRows: [[String]] {
guard !searchText.isEmpty else { return result.rows }
@@ -1418,6 +1428,10 @@ struct WorkbenchAdministrationView: View {
resultContent
}
if page == .clientConnections {
processControls
}
if page == .users && !result.rows.isEmpty {
HStack {
Picker("Account", selection: $selectedAccount) {
@@ -1463,6 +1477,24 @@ struct WorkbenchAdministrationView: View {
await refresh()
}
}
.confirmationDialog(
"Terminate MySQL session?",
isPresented: Binding(
get: { pendingProcessAction != nil },
set: { if !$0 { pendingProcessAction = nil } }
),
titleVisibility: .visible
) {
if let action = pendingProcessAction, let id = selectedProcessID {
Button("\(action.rawValue) session \(id)", role: .destructive) {
pendingProcessAction = nil
Task { await terminateProcess(id: id, action: action) }
}
}
Button("Cancel", role: .cancel) { pendingProcessAction = nil }
} message: {
Text("This operation affects a live MySQL session. Terminating a connection can interrupt transactions and roll back uncommitted work. Verify the session ID before proceeding.")
}
.sheet(isPresented: $showingAccountManager, onDismiss: {
Task { await refresh() }
}) {
@@ -1494,6 +1526,75 @@ struct WorkbenchAdministrationView: View {
}
}
private var processControls: some View {
VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 12) {
Picker("Session", selection: $selectedProcessID) {
Text("Select a session").tag(String?.none)
ForEach(processIDs, id: \.self) { id in
Text("ID \(id)").tag(Optional(id))
}
}
.frame(maxWidth: 250)
Button("Stop Query") { pendingProcessAction = .query }
.disabled(selectedProcessID == nil || terminatingProcess)
Button("Disconnect Session") { pendingProcessAction = .connection }
.disabled(selectedProcessID == nil || terminatingProcess)
Spacer()
if terminatingProcess { ProgressView().controlSize(.small) }
}
if let processActionError {
Text(processActionError).foregroundStyle(.red).textSelection(.enabled)
}
if let processActionStatus {
Text(processActionStatus).foregroundStyle(.secondary)
}
Text("Requires PROCESS privilege to see other users' sessions and appropriate privileges to terminate them.")
.font(.caption).foregroundStyle(.secondary)
}
.padding(.horizontal, 16)
.padding(.vertical, 8)
}
private var processIDs: [String] {
guard let index = result.columns.firstIndex(where: { $0.caseInsensitiveCompare("Id") == .orderedSame }) else {
return []
}
return result.rows.compactMap { row in
guard row.indices.contains(index), let id = UInt64(row[index]), id > 0 else { return nil }
return String(id)
}
}
@MainActor
private func terminateProcess(id: String, action: WorkbenchProcessAction) async {
guard let profile, let numericID = UInt64(id), numericID > 0 else { return }
terminatingProcess = true
processActionError = nil
processActionStatus = nil
defer { terminatingProcess = false }
do {
let own = try await NativeMySQLRunner.run(
profile: profile, database: "",
password: PasswordStore.read(profile.id), sql: "SELECT CONNECTION_ID()"
)
if own.rows.first?.first == id {
processActionError = "Cannot terminate this application's own MySQL session."
return
}
let sql = "KILL \(action == .query ? "QUERY" : "CONNECTION") \(numericID)"
_ = try await NativeMySQLRunner.run(
profile: profile, database: "",
password: PasswordStore.read(profile.id), sql: sql
)
processActionStatus = "\(action.rawValue) requested for session \(id)."
selectedProcessID = nil
await refresh()
} catch {
processActionError = error.localizedDescription
}
}
private var header: some View {
HStack(spacing: 14) {
Image(systemName: page.icon)
@@ -1669,6 +1770,11 @@ struct WorkbenchAdministrationView: View {
)
result = response
if page == .clientConnections,
let selectedProcessID,
!processIDs.contains(selectedProcessID) {
self.selectedProcessID = nil
}
lastUpdated = Date()
} catch {
errorMessage = error.localizedDescription