import SwiftUI import AppKit // MARK: - Classic Workbench Colours private enum WB { static let toolbar = Color(nsColor: NSColor( calibratedWhite: 0.88, alpha: 1 )) static let sidebar = Color(nsColor: NSColor( calibratedWhite: 0.94, alpha: 1 )) static let border = Color(nsColor: NSColor( calibratedWhite: 0.76, alpha: 1 )) static let header = Color(nsColor: NSColor( calibratedWhite: 0.91, alpha: 1 )) } // MARK: - Main Window struct MainView: View { @EnvironmentObject private var model: DatabaseModel @State private var showingEditor = false @State private var editing = ConnectionProfile() @State private var editingPassword = "" @State private var expandedSchemas: Set = [] @State private var expandedTableFolders: Set = [] @State private var selectedTableName: String? = nil @State private var outputExpanded = true @State private var navigatorMode = 0 @State private var administrationPage: WorkbenchAdminPage? = nil var body: some View { VStack(spacing: 0) { mainToolbar Rectangle() .fill(WB.border) .frame(height: 1) HSplitView { navigator .frame( minWidth: 180, idealWidth: 250, maxWidth: 420 ) workspace .frame(minWidth: 480) } statusBar } .frame(minWidth: 800, minHeight: 520) .preferredColorScheme( UserDefaults.standard.string( forKey: "preferences.appearance" ) == "dark" ? .dark : .light ) .background(Color(nsColor: .windowBackgroundColor)) .sheet(isPresented: $showingEditor) { ConnectionEditor( profile: $editing, password: $editingPassword ) { model.save(editing, password: editingPassword) showingEditor = false } } } // MARK: - Main Toolbar private var mainToolbar: some View { HStack(spacing: 16) { Image(systemName: "externaldrive.connected.to.line.below") .font(.system(size: 23)) .foregroundStyle(.blue) Text("MySQL Workbench") .font(.system(size: 15, weight: .semibold)) .foregroundStyle(.primary) Rectangle() .fill(WB.border) .frame(width: 1, height: 30) toolbarButton( "New Connection", icon: "plus.rectangle.on.rectangle" ) { editing = ConnectionProfile(name: "New Connection") editingPassword = "" showingEditor = true } toolbarButton( "Refresh", icon: "arrow.clockwise" ) { model.loadDatabases() } toolbarButton( "Execute", icon: "bolt.fill" ) { model.runSQL() } .keyboardShortcut(.return, modifiers: [.command]) toolbarButton( "Format SQL", icon: "text.alignleft" ) { model.formatSQL() } Spacer() VStack(alignment: .trailing, spacing: 2) { Text(model.selected?.name ?? "No Connection") .font(.system(size: 12, weight: .semibold)) Text(model.selectedDatabase.isEmpty ? "No default schema" : model.selectedDatabase) .font(.system(size: 11)) .foregroundStyle(.secondary) } } .padding(.horizontal, 10) .frame(height: 46) .background(WB.toolbar) } private func toolbarButton( _ title: String, icon: String, action: @escaping () -> Void ) -> some View { Button(action: action) { VStack(spacing: 3) { Image(systemName: icon) .font(.system(size: 19)) .frame(height: 23) Text(title) .font(.system(size: 10)) } .foregroundStyle(.primary) .frame(minWidth: 65) } .buttonStyle(.plain) .help(title) } // MARK: - Navigator private var navigator: some View { VStack(spacing: 0) { Picker("Navigator", selection: $navigatorMode) { Text("Administration").tag(0) Text("Schemas").tag(1) } .pickerStyle(.segmented) .labelsHidden() .padding(8) if navigatorMode == 0 { administrationNavigator } else { schemasNavigator } } .background(WB.sidebar) } private var schemasNavigator: some View { VStack(spacing: 0) { HStack { Text("SCHEMAS") .font(.system(size: 11, weight: .bold)) Spacer() Button { model.loadDatabases() } label: { Image(systemName: "arrow.clockwise") } .buttonStyle(.borderless) } .padding(.horizontal, 12) .frame(height: 34) Divider() ScrollView { LazyVStack(alignment: .leading, spacing: 2) { ForEach(model.databases, id: \.self) { db in schemaNode(db) } } .padding(.vertical, 8) } Divider() sectionHeader("MYSQL CONNECTIONS") ScrollView { VStack(alignment: .leading, spacing: 4) { ForEach(model.profiles) { profile in Button { model.selectedID = profile.id model.loadDatabases() } label: { Label( profile.name, systemImage: "server.rack" ) .frame( maxWidth: .infinity, alignment: .leading ) .padding(6) .background( model.selectedID == profile.id ? Color.blue.opacity(0.14) : Color.clear ) } .buttonStyle(.plain) .contextMenu { Button("Edit Connection") { edit(profile) } } } } .padding(8) } .frame(height: 125) } } private var administrationNavigator: some View { ScrollView { VStack(alignment: .leading, spacing: 4) { administrationSection( "MANAGEMENT", pages: [ .serverStatus, .clientConnections, .users, .variables ] ) administrationSection( "PERFORMANCE", pages: [ .dashboard, .performanceReports, .performanceSchema ] ) } .padding(8) } .frame(maxWidth: .infinity, maxHeight: .infinity) } private func administrationSection( _ title: String, pages: [WorkbenchAdminPage] ) -> some View { VStack(alignment: .leading, spacing: 4) { Text(title) .font(.system(size: 11, weight: .bold)) .foregroundStyle(.secondary) .padding(.top, 12) .padding(.bottom, 4) ForEach(pages) { page in Button { administrationPage = page } label: { Label(page.rawValue, systemImage: page.icon) .frame( maxWidth: .infinity, alignment: .leading ) .padding(.horizontal, 8) .padding(.vertical, 7) .background( administrationPage == page ? Color.accentColor.opacity(0.18) : Color.clear ) .clipShape(RoundedRectangle(cornerRadius: 5)) } .buttonStyle(.plain) } } } // MARK: - Compact Schema Tree private func schemaNode(_ db: String) -> some View { VStack(alignment: .leading, spacing: 0) { HStack(spacing: 5) { Image(systemName: expandedSchemas.contains(db) ? "chevron.down" : "chevron.right" ) .font(.system(size: 10)) .foregroundStyle(.secondary) .frame(width: 13) Image(systemName: "cylinder.split.1x2") .foregroundStyle(.blue) .frame(width: 17) Text(db) .font(.system(size: 12, weight: model.selectedDatabase == db ? .semibold : .regular)) .lineLimit(1) Spacer(minLength: 0) } .frame(height: 25) .padding(.leading, 10) .contentShape(Rectangle()) .onTapGesture { toggleSchema(db) } .onTapGesture(count: 2) { model.selectedDatabase = db expandedSchemas.insert(db) expandedTableFolders.insert(db) if model.schemaTables[db] == nil { model.loadTables(database: db) } } .contextMenu { Button("Set as Default Schema") { model.selectedDatabase = db } Button("Refresh Tables") { model.loadTables(database: db) } } if expandedSchemas.contains(db) { schemaChildren(db) } } .frame(maxWidth: .infinity, alignment: .leading) } private func toggleSchema(_ db: String) { if expandedSchemas.contains(db) { expandedSchemas.remove(db) } else { expandedSchemas.insert(db) expandedTableFolders.insert(db) if model.schemaTables[db] == nil { model.loadTables(database: db) } } } private func schemaChildren(_ db: String) -> some View { VStack(alignment: .leading, spacing: 0) { // Tables folder HStack(spacing: 5) { Image(systemName: expandedTableFolders.contains(db) ? "chevron.down" : "chevron.right" ) .font(.system(size: 10)) .foregroundStyle(.secondary) .frame(width: 13) Image(systemName: "folder") .foregroundStyle(.secondary) .frame(width: 17) Text("Tables") .font(.system(size: 12)) Spacer(minLength: 0) } .frame(height: 24) .padding(.leading, 28) .contentShape(Rectangle()) .onTapGesture { if expandedTableFolders.contains(db) { expandedTableFolders.remove(db) } else { expandedTableFolders.insert(db) if model.schemaTables[db] == nil { model.loadTables(database: db) } } } // Tables if expandedTableFolders.contains(db) { if let tables = model.schemaTables[db] { ForEach(tables, id: \.self) { table in tableNode(database: db, table: table) } } else { Button("Load Tables") { model.loadTables(database: db) } .font(.system(size: 11)) .buttonStyle(.borderless) .padding(.leading, 66) .frame(height: 24) } } folderNode("Views", indent: 28) folderNode("Stored Procedures", indent: 28) folderNode("Functions", indent: 28) } .frame(maxWidth: .infinity, alignment: .leading) } private func tableNode(database: String, table: String) -> some View { let identifier = database + "." + table let isSelected = selectedTableName == identifier return HStack(spacing: 5) { Image(systemName: "tablecells") .font(.system(size: 11)) .foregroundStyle( isSelected ? Color.blue : Color.gray ) .frame(width: 17) Text(table) .font(.system(size: 12)) .lineLimit(1) .truncationMode(.middle) Spacer(minLength: 0) } .frame(height: 23) .padding(.leading, 49) .frame(maxWidth: .infinity, alignment: .leading) .background( isSelected ? Color.blue.opacity(0.14) : Color.clear ) .contentShape(Rectangle()) .onTapGesture { selectedTableName = identifier } .onTapGesture(count: 2) { selectedTableName = identifier model.selectTable( database: database, table: table ) } .contextMenu { Button("Select First 100 Rows") { selectedTableName = identifier model.selectTable( database: database, table: table ) } } } private func folderNode( _ title: String, indent: CGFloat ) -> some View { HStack(spacing: 5) { Image(systemName: "chevron.right") .font(.system(size: 10)) .foregroundStyle(.secondary) .frame(width: 13) Image(systemName: "folder") .foregroundStyle(.secondary) .frame(width: 17) Text(title) .font(.system(size: 12)) Spacer(minLength: 0) } .frame(height: 24) .padding(.leading, indent) .frame(maxWidth: .infinity, alignment: .leading) } // MARK: - Workspace private var workspace: some View { Group { if let page = administrationPage { WorkbenchAdministrationView( page: page, profile: model.selected ) { administrationPage = nil navigatorMode = 1 } } else { sqlWorkspace } } } private var sqlWorkspace: some View { VStack(spacing: 0) { queryTabBar Divider() queryToolbar Divider() VSplitView { sqlEditor .frame(minHeight: 150) bottomWorkspace .frame(minHeight: 150) } } } private var queryTabBar: some View { HStack(spacing: 0) { ScrollView(.horizontal) { HStack(spacing: 2) { ForEach(model.queryTabs) { tab in HStack(spacing: 8) { Button { model.activateQuery(tab.id) } label: { Label(tab.title, systemImage: "doc.text") .lineLimit(1) } .buttonStyle(.plain) Button { model.closeQuery(tab.id) } label: { Image(systemName: "xmark") .font(.system(size: 9)) } .buttonStyle(.plain) } .font(.system(size: 12)) .padding(.horizontal, 10) .frame(height: 30) .background( model.activeQueryID == tab.id ? Color(nsColor: .textBackgroundColor) : Color.clear ) .clipShape( UnevenRoundedRectangle( topLeadingRadius: 5, topTrailingRadius: 5 ) ) } } } .scrollIndicators(.hidden) Button { model.newQueryTab() } label: { Image(systemName: "plus") } .buttonStyle(.borderless) .padding(.horizontal, 10) .help("New Query Tab") } .background(WB.header) } private var queryToolbar: some View { HStack(spacing: 14) { Button { model.runSQL() } label: { Image(systemName: "bolt.fill") .foregroundStyle(.orange) } .help("Execute SQL") Button { model.formatSQL() } label: { Image(systemName: "text.alignleft") } .help("Format SQL") Divider() .frame(height: 17) Text("Default Schema:") .font(.system(size: 11)) Text(model.selectedDatabase.isEmpty ? "None" : model.selectedDatabase) .font(.system(size: 11, weight: .semibold)) Spacer() Text("SQL Editor") .font(.system(size: 11)) .foregroundStyle(.secondary) } .buttonStyle(.borderless) .padding(.horizontal, 12) .frame(height: 30) .background(WB.header) } private var sqlEditor: some View { HStack(spacing: 0) { if UserDefaults.standard.object( forKey: "preferences.showLineNumbers" ) as? Bool ?? true { lineNumbers Rectangle() .fill(WB.border) .frame(width: 1) } Rectangle() .fill(WB.border) .frame(width: 1) SQLTextView(text: $model.sql) } .background(Color(nsColor: .textBackgroundColor)) } private var lineNumbers: some View { ScrollView { VStack(alignment: .trailing, spacing: 0) { ForEach( 1...max(1, model.sql.components( separatedBy: "\n" ).count), id: \.self ) { number in Text("\(number)") .frame(height: 19) } } .padding(.horizontal, 9) .padding(.top, 8) } .font(.system(size: 12, design: .monospaced)) .foregroundStyle(.secondary) .frame(width: 48) .background(WB.header) .allowsHitTesting(false) } // MARK: - Results and Action Output private var bottomWorkspace: some View { VSplitView { resultsPanel .frame(minHeight: 230) if outputExpanded { actionOutput .frame(minHeight: 90) } } } private var resultsPanel: some View { VStack(spacing: 0) { HStack { Image(systemName: "tablecells") Text("Result Grid") .fontWeight(.semibold) Spacer() Text(model.result.message) .foregroundStyle(.secondary) } .font(.system(size: 12)) .padding(.horizontal, 12) .frame(height: 28) .background(WB.header) Divider() if model.result.columns.isEmpty { VStack { Spacer() Text("No query results") .foregroundStyle(.secondary) Spacer() } } else { resultGrid } } } private var resultGrid: some View { NativeResultGrid( columns: model.result.columns, rows: model.result.rows ) } private var actionOutput: some View { VStack(spacing: 0) { HStack { Image(systemName: "list.bullet.rectangle") Text("Action Output") .fontWeight(.semibold) Spacer() Button { outputExpanded = false } label: { Image(systemName: "chevron.down") } .buttonStyle(.borderless) } .font(.system(size: 12)) .padding(.horizontal, 12) .frame(height: 26) .background(WB.header) Divider() HStack(alignment: .top, spacing: 12) { Image(systemName: model.status == "Error" ? "xmark.circle.fill" : "checkmark.circle.fill" ) .foregroundStyle( model.status == "Error" ? .red : .green ) VStack(alignment: .leading, spacing: 5) { Text(model.status) .fontWeight(.semibold) Text(model.result.message) .foregroundStyle(.secondary) } Spacer() } .font(.system(size: 12)) .padding(12) Spacer() } } // MARK: - Status Bar private var statusBar: some View { HStack(spacing: 10) { Circle() .fill( model.status == "Error" ? Color.red : Color.green ) .frame(width: 8, height: 8) Text(model.status) .lineLimit(1) Spacer() Text(model.selected?.name ?? "Disconnected") Divider() .frame(height: 13) Text(model.selectedDatabase.isEmpty ? "No Schema" : model.selectedDatabase) Divider() .frame(height: 13) Text("UTF-8") } .font(.system(size: 11)) .padding(.horizontal, 12) .frame(height: 22) .background(WB.header) } private func sectionHeader(_ title: String) -> some View { HStack { Text(title) .font(.system(size: 11, weight: .bold)) .foregroundStyle(.secondary) Spacer() } .padding(.horizontal, 12) .frame(height: 30) .background(WB.header) } private func edit(_ profile: ConnectionProfile) { editing = profile editingPassword = PasswordStore.read(profile.id) showingEditor = true } } // MARK: - Native SQL Text Editor struct SQLTextView: NSViewRepresentable { @Binding var text: String func makeCoordinator() -> Coordinator { Coordinator(self) } func makeNSView(context: Context) -> NSScrollView { let scroll = NSTextView.scrollableTextView() let editor = scroll.documentView as! NSTextView scroll.appearance = NSAppearance(named: .aqua) editor.appearance = NSAppearance(named: .aqua) editor.backgroundColor = .white editor.textColor = .black scroll.backgroundColor = .white editor.isEditable = true editor.isRichText = false editor.allowsUndo = true editor.isAutomaticQuoteSubstitutionEnabled = false editor.isAutomaticDashSubstitutionEnabled = false editor.font = NSFont.monospacedSystemFont( ofSize: CGFloat( UserDefaults.standard.object( forKey: "preferences.editorFontSize" ) as? Double ?? 13 ), weight: .regular ) editor.textContainerInset = NSSize( width: 10, height: 8 ) editor.delegate = context.coordinator editor.string = text context.coordinator.highlight(editor) return scroll } func updateNSView( _ scroll: NSScrollView, context: Context ) { guard let editor = scroll.documentView as? NSTextView else { return } if editor.string != text { let selection = editor.selectedRange() editor.string = text let safeLocation = min( selection.location, (text as NSString).length ) editor.setSelectedRange( NSRange(location: safeLocation, length: 0) ) context.coordinator.highlight(editor) } } final class Coordinator: NSObject, NSTextViewDelegate { var parent: SQLTextView init(_ parent: SQLTextView) { self.parent = parent } func textDidChange(_ notification: Notification) { guard let editor = notification.object as? NSTextView else { return } parent.text = editor.string highlight(editor) } func highlight(_ editor: NSTextView) { guard let storage = editor.textStorage else { return } let text = editor.string as NSString let range = NSRange( location: 0, length: text.length ) storage.beginEditing() storage.setAttributes([ .foregroundColor: NSColor.textColor, .font: NSFont.monospacedSystemFont( ofSize: CGFloat( UserDefaults.standard.object( forKey: "preferences.editorFontSize" ) as? Double ?? 13 ), weight: .regular ) ], range: range) let pattern = """ \\b(SELECT|FROM|WHERE|INSERT|UPDATE|DELETE|CREATE|TABLE|DATABASE|SHOW|USE|JOIN|LEFT|RIGHT|INNER|OUTER|ON|AS|AND|OR|ORDER|GROUP|BY|LIMIT|VALUES|SET|NULL|NOT|IN|LIKE|DISTINCT|ALTER|DROP|DESCRIBE|EXPLAIN)\\b """ if let regex = try? NSRegularExpression( pattern: pattern, options: [.caseInsensitive] ) { for match in regex.matches( in: editor.string, range: range ) { storage.addAttribute( .foregroundColor, value: NSColor.systemBlue, range: match.range ) } } storage.endEditing() } } } // MARK: - Connection Editor struct ConnectionEditor: View { @Binding var profile: ConnectionProfile @Binding var password: String let save: () -> Void @Environment(\.dismiss) private var dismiss var body: some View { VStack(alignment: .leading, spacing: 14) { Text("Connection Settings") .font(.title2.bold()) Form { TextField( "Connection Name", text: $profile.name ) TextField( "Hostname", text: $profile.host ) TextField( "Port", value: $profile.port, format: .number ) TextField( "Username", text: $profile.username ) SecureField( "Password (macOS Keychain)", text: $password ) TextField( "Default Schema", text: $profile.database ) } HStack { Spacer() Button("Cancel") { dismiss() } Button("Save") { save() } .buttonStyle(.borderedProminent) } } .padding(24) .frame(width: 520) } } // MARK: - High Performance Native Result Grid struct NativeResultGrid: NSViewRepresentable { let columns: [String] let rows: [[String]] func makeCoordinator() -> Coordinator { Coordinator() } func makeNSView(context: Context) -> NSScrollView { let scroll = NSScrollView() scroll.hasVerticalScroller = true scroll.hasHorizontalScroller = true scroll.autohidesScrollers = true scroll.drawsBackground = true scroll.backgroundColor = .white let table = NSTableView() table.headerView = NSTableHeaderView() table.usesAlternatingRowBackgroundColors = UserDefaults.standard.object( forKey: "preferences.alternatingRows" ) as? Bool ?? true table.rowHeight = CGFloat( UserDefaults.standard.object( forKey: "preferences.resultRowHeight" ) as? Double ?? 24 ) table.intercellSpacing = NSSize(width: 1, height: 1) table.allowsMultipleSelection = true table.allowsColumnResizing = true table.allowsColumnReordering = true table.columnAutoresizingStyle = .noColumnAutoresizing table.backgroundColor = .white context.coordinator.columns = columns context.coordinator.rows = rows table.delegate = context.coordinator table.dataSource = context.coordinator configureColumns(table) scroll.documentView = table table.reloadData() return scroll } func updateNSView( _ scroll: NSScrollView, context: Context ) { guard let table = scroll.documentView as? NSTableView else { return } let oldColumns = context.coordinator.columns context.coordinator.columns = columns context.coordinator.rows = rows if oldColumns != columns { configureColumns(table) } table.reloadData() } private func configureColumns(_ table: NSTableView) { for column in Array(table.tableColumns) { table.removeTableColumn(column) } for (index, name) in columns.enumerated() { let identifier = NSUserInterfaceItemIdentifier( "column_\(index)" ) let column = NSTableColumn(identifier: identifier) column.title = name column.width = CGFloat( UserDefaults.standard.object( forKey: "preferences.resultColumnWidth" ) as? Double ?? 170 ) column.minWidth = 70 column.maxWidth = 700 column.resizingMask = .userResizingMask table.addTableColumn(column) } } final class Coordinator: NSObject, NSTableViewDataSource, NSTableViewDelegate { var columns: [String] = [] var rows: [[String]] = [] func numberOfRows(in tableView: NSTableView) -> Int { rows.count } func tableView( _ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int ) -> NSView? { guard let tableColumn, row >= 0, row < rows.count else { return nil } let identifier = tableColumn.identifier.rawValue guard identifier.hasPrefix("column_"), let columnIndex = Int(identifier.dropFirst(7)), columnIndex < columns.count else { return nil } let cellIdentifier = NSUserInterfaceItemIdentifier("ResultCell") let cell: NSTableCellView if let reused = tableView.makeView( withIdentifier: cellIdentifier, owner: nil ) as? NSTableCellView { cell = reused } else { cell = NSTableCellView() cell.identifier = cellIdentifier let field = NSTextField(labelWithString: "") field.font = NSFont.monospacedSystemFont( ofSize: 12, weight: .regular ) field.lineBreakMode = .byTruncatingTail field.translatesAutoresizingMaskIntoConstraints = false cell.addSubview(field) cell.textField = field NSLayoutConstraint.activate([ field.leadingAnchor.constraint( equalTo: cell.leadingAnchor, constant: 7 ), field.trailingAnchor.constraint( equalTo: cell.trailingAnchor, constant: -7 ), field.centerYAnchor.constraint( equalTo: cell.centerYAnchor ) ]) } let values = rows[row] cell.textField?.stringValue = columnIndex < values.count ? values[columnIndex] : "" return cell } } } // MARK: - MySQL Administration enum WorkbenchAdminPage: String, CaseIterable, Identifiable { case serverStatus = "Server Status" case clientConnections = "Client Connections" case users = "Users and Privileges" case variables = "Status and System Variables" case dashboard = "Dashboard" case performanceReports = "Performance Reports" case performanceSchema = "Performance Schema Setup" var id: String { rawValue } var icon: String { switch self { case .serverStatus: return "server.rack" case .clientConnections: return "person.2" case .users: return "person.crop.circle.badge.checkmark" case .variables: return "slider.horizontal.3" case .dashboard: return "chart.xyaxis.line" case .performanceReports: return "chart.bar.xaxis" case .performanceSchema: return "gauge.with.dots.needle.67percent" } } var statement: String { switch self { case .serverStatus: return """ SHOW GLOBAL STATUS WHERE Variable_name IN ( 'Uptime', 'Threads_connected', 'Threads_running', 'Questions', 'Queries', 'Connections', 'Bytes_received', 'Bytes_sent' ) """ case .clientConnections: return "SHOW FULL PROCESSLIST" case .users: return """ SELECT User, Host, plugin, account_locked FROM mysql.user ORDER BY User, Host """ case .variables: return "SHOW GLOBAL VARIABLES" case .dashboard: return """ SHOW GLOBAL STATUS WHERE Variable_name IN ( 'Uptime', 'Connections', 'Threads_connected', 'Threads_running', 'Questions', 'Queries', 'Bytes_received', 'Bytes_sent', 'Innodb_buffer_pool_reads', 'Innodb_buffer_pool_read_requests', 'Slow_queries' ) """ case .performanceReports: return """ SELECT * FROM sys.statement_analysis LIMIT 100 """ case .performanceSchema: return """ SELECT NAME, ENABLED, TIMED FROM performance_schema.setup_instruments LIMIT 250 """ } } } struct WorkbenchAdministrationView: View { let page: WorkbenchAdminPage let profile: ConnectionProfile? let close: () -> Void @State private var result = QueryResult() @State private var loading = false @State private var errorMessage: String? @State private var lastUpdated: Date? var body: some View { VStack(spacing: 0) { header Divider() if loading && result.columns.isEmpty { Spacer() ProgressView("Loading \(page.rawValue)…") Spacer() } else if let errorMessage { ContentUnavailableView { Label( "Unable to Load \(page.rawValue)", systemImage: "exclamationmark.triangle" ) } description: { Text(errorMessage) } actions: { Button("Retry") { Task { await refresh() } } } .frame(maxWidth: .infinity, maxHeight: .infinity) } else if result.columns.isEmpty { ContentUnavailableView( "No Data", systemImage: "tablecells", description: Text( "The server did not return any columns." ) ) .frame(maxWidth: .infinity, maxHeight: .infinity) } else { resultContent } Divider() footer } .frame(maxWidth: .infinity, maxHeight: .infinity) .task(id: "\(page.id)-\(profile?.id.uuidString ?? "")") { await refresh() } } private var header: some View { HStack(spacing: 14) { Image(systemName: page.icon) .font(.system(size: 27)) .foregroundStyle(.blue) .frame(width: 40) VStack(alignment: .leading, spacing: 3) { Text(profile?.name ?? "No Connection") .font(.caption) .foregroundStyle(.secondary) Text(page.rawValue) .font(.system(size: 22, weight: .semibold)) } Spacer() Button { Task { await refresh() } } label: { Label("Refresh", systemImage: "arrow.clockwise") } .disabled(loading) Button { close() } label: { Label("SQL Editor", systemImage: "chevron.left.forwardslash.chevron.right") } } .padding(18) } @ViewBuilder private var resultContent: some View { if page == .dashboard || page == .serverStatus { ScrollView { LazyVGrid( columns: [ GridItem(.adaptive(minimum: 180), spacing: 12) ], spacing: 12 ) { ForEach(result.rows.indices, id: \.self) { index in let row = result.rows[index] if row.count >= 2 { VStack(alignment: .leading, spacing: 10) { Text( row[0] .replacingOccurrences( of: "_", with: " " ) ) .font(.caption) .foregroundStyle(.secondary) Text(row[1]) .font( .system( size: 23, weight: .semibold, design: .rounded ) ) .lineLimit(1) .minimumScaleFactor(0.65) } .frame( maxWidth: .infinity, minHeight: 90, alignment: .leading ) .padding(14) .background( Color(nsColor: .controlBackgroundColor) ) .clipShape( RoundedRectangle(cornerRadius: 10) ) } } } .padding(18) } } else { NativeResultGrid( columns: result.columns, rows: result.rows ) .frame( maxWidth: .infinity, maxHeight: .infinity ) } } private var footer: some View { HStack { if let lastUpdated { Text( "Updated \(lastUpdated.formatted(date: .omitted, time: .standard))" ) } else { Text("Not loaded") } Spacer() Text("\(result.rows.count) rows") } .font(.caption) .foregroundStyle(.secondary) .padding(.horizontal, 16) .frame(height: 30) } @MainActor private func refresh() async { guard let profile else { errorMessage = "Select a MySQL connection first." return } loading = true errorMessage = nil defer { loading = false } do { let response = try await NativeMySQLRunner.run( profile: profile, database: "", password: PasswordStore.read(profile.id), sql: page.statement ) result = response lastUpdated = Date() } catch { errorMessage = error.localizedDescription } } }