new ui
This commit is contained in:
@@ -33,6 +33,10 @@ struct MainView: View {
|
||||
@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
|
||||
@@ -163,8 +167,27 @@ struct MainView: View {
|
||||
|
||||
private var navigator: some View {
|
||||
VStack(spacing: 0) {
|
||||
sectionHeader("NAVIGATOR")
|
||||
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))
|
||||
@@ -230,7 +253,67 @@ struct MainView: View {
|
||||
}
|
||||
.frame(height: 125)
|
||||
}
|
||||
.background(WB.sidebar)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -444,6 +527,22 @@ struct MainView: View {
|
||||
// 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
|
||||
|
||||
@@ -1143,3 +1242,300 @@ struct NativeResultGrid: NSViewRepresentable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user