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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user