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:
@@ -282,7 +282,8 @@ struct MainView: View {
|
||||
.serverStatus,
|
||||
.clientConnections,
|
||||
.users,
|
||||
.variables
|
||||
.variables,
|
||||
.serverLogs
|
||||
]
|
||||
)
|
||||
|
||||
@@ -1272,11 +1273,13 @@ enum WorkbenchAdminPage: String, CaseIterable, Identifiable {
|
||||
case dashboard = "Dashboard"
|
||||
case performanceReports = "Performance Reports"
|
||||
case performanceSchema = "Performance Schema Setup"
|
||||
case serverLogs = "Server Error Log"
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
var icon: String {
|
||||
switch self {
|
||||
case .serverLogs: return "text.alignleft"
|
||||
case .serverStatus: return "server.rack"
|
||||
case .clientConnections: return "person.2"
|
||||
case .users: return "person.crop.circle.badge.checkmark"
|
||||
@@ -1289,6 +1292,8 @@ enum WorkbenchAdminPage: String, CaseIterable, Identifiable {
|
||||
|
||||
var statement: String {
|
||||
switch self {
|
||||
case .serverLogs:
|
||||
return "SELECT LOGGED, PRIO, ERROR_CODE, SUBSYSTEM, DATA FROM performance_schema.error_log ORDER BY LOGGED DESC LIMIT 500"
|
||||
case .serverStatus:
|
||||
return """
|
||||
SHOW GLOBAL STATUS WHERE Variable_name IN (
|
||||
@@ -1359,6 +1364,19 @@ struct WorkbenchAdministrationView: View {
|
||||
@State private var loading = false
|
||||
@State private var errorMessage: String?
|
||||
@State private var lastUpdated: Date?
|
||||
@State private var searchText = ""
|
||||
@State private var autoRefresh = false
|
||||
@State private var selectedAccount: String? = nil
|
||||
@State private var grants: [String] = []
|
||||
@State private var grantsError: String? = nil
|
||||
@State private var showingGrants = false
|
||||
|
||||
private var filteredRows: [[String]] {
|
||||
guard !searchText.isEmpty else { return result.rows }
|
||||
return result.rows.filter { row in
|
||||
row.contains { $0.localizedCaseInsensitiveContains(searchText) }
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
@@ -1399,6 +1417,31 @@ struct WorkbenchAdministrationView: View {
|
||||
resultContent
|
||||
}
|
||||
|
||||
if page == .users && !result.rows.isEmpty {
|
||||
HStack {
|
||||
Picker("Account", selection: $selectedAccount) {
|
||||
Text("Select account").tag(String?.none)
|
||||
ForEach(result.rows.indices, id: \.self) { i in
|
||||
let row = result.rows[i]
|
||||
if row.count >= 2 {
|
||||
Text("\(row[0])@\(row[1])")
|
||||
.tag(Optional("\(row[0])\u{001f}\(row[1])"))
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: 320)
|
||||
Button("Show Grants") {
|
||||
Task { await showGrants() }
|
||||
}
|
||||
.disabled(selectedAccount == nil)
|
||||
Spacer()
|
||||
Text("Read-only account inspection")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
footer
|
||||
@@ -1407,6 +1450,32 @@ struct WorkbenchAdministrationView: View {
|
||||
.task(id: "\(page.id)-\(profile?.id.uuidString ?? "")") {
|
||||
await refresh()
|
||||
}
|
||||
.task(id: "\(page.id)-\(autoRefresh)-\(profile?.id.uuidString ?? "")") {
|
||||
while autoRefresh && !Task.isCancelled {
|
||||
try? await Task.sleep(for: .seconds(5))
|
||||
if Task.isCancelled || !autoRefresh { break }
|
||||
await refresh()
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingGrants) {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Grants: \(selectedAccount ?? "")")
|
||||
.font(.title2.bold())
|
||||
if let grantsError {
|
||||
Text(grantsError).foregroundStyle(.red)
|
||||
} else {
|
||||
ScrollView {
|
||||
Text(grants.joined(separator: "\n\n"))
|
||||
.font(.system(.body, design: .monospaced))
|
||||
.textSelection(.enabled)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
Button("Close") { showingGrants = false }
|
||||
}
|
||||
.padding(20)
|
||||
.frame(minWidth: 580, minHeight: 300)
|
||||
}
|
||||
}
|
||||
|
||||
private var header: some View {
|
||||
@@ -1427,6 +1496,16 @@ struct WorkbenchAdministrationView: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
if page != .dashboard && page != .serverStatus {
|
||||
TextField("Filter results", text: $searchText)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.frame(width: 175)
|
||||
}
|
||||
|
||||
Toggle("Auto-refresh", isOn: $autoRefresh)
|
||||
.toggleStyle(.checkbox)
|
||||
.help("Refresh every five seconds")
|
||||
|
||||
Button {
|
||||
Task { await refresh() }
|
||||
} label: {
|
||||
@@ -1499,7 +1578,7 @@ struct WorkbenchAdministrationView: View {
|
||||
} else {
|
||||
NativeResultGrid(
|
||||
columns: result.columns,
|
||||
rows: result.rows
|
||||
rows: filteredRows
|
||||
)
|
||||
.frame(
|
||||
maxWidth: .infinity,
|
||||
@@ -1528,6 +1607,29 @@ struct WorkbenchAdministrationView: View {
|
||||
.frame(height: 30)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func showGrants() async {
|
||||
guard let profile, let selectedAccount else { return }
|
||||
let components = selectedAccount.split(separator: "\u{001f}", omittingEmptySubsequences: false)
|
||||
guard components.count == 2 else { return }
|
||||
func quoted(_ text: String) -> String {
|
||||
"'" + text.replacingOccurrences(of: "'", with: "''") + "'"
|
||||
}
|
||||
grants = []
|
||||
grantsError = nil
|
||||
showingGrants = true
|
||||
do {
|
||||
let sql = "SHOW GRANTS FOR \(quoted(String(components[0])))@\(quoted(String(components[1])))"
|
||||
let response = try await NativeMySQLRunner.run(
|
||||
profile: profile, database: "",
|
||||
password: PasswordStore.read(profile.id), sql: sql
|
||||
)
|
||||
grants = response.rows.compactMap { $0.first }
|
||||
} catch {
|
||||
grantsError = error.localizedDescription
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func refresh() async {
|
||||
guard let profile else {
|
||||
|
||||
Reference in New Issue
Block a user