190 lines
6.3 KiB
Plaintext
190 lines
6.3 KiB
Plaintext
import SwiftUI
|
|
|
|
struct MainView: View {
|
|
@EnvironmentObject private var model: DatabaseModel
|
|
@State private var showingEditor = false
|
|
@State private var editing = ConnectionProfile()
|
|
@State private var editingPassword = ""
|
|
|
|
var body: some View {
|
|
NavigationSplitView {
|
|
navigator
|
|
} detail: {
|
|
editorPanel
|
|
}
|
|
.sheet(isPresented: $showingEditor) {
|
|
ConnectionEditor(profile: $editing, password: $editingPassword) {
|
|
model.save(editing, password: editingPassword)
|
|
showingEditor = false
|
|
}
|
|
}
|
|
}
|
|
|
|
private var navigator: some View {
|
|
List(selection: $model.selectedID) {
|
|
Section("MYSQL CONNECTIONS") {
|
|
ForEach(model.profiles) { profile in
|
|
Label(profile.name, systemImage: "server.rack")
|
|
.tag(profile.id)
|
|
.contextMenu {
|
|
Button("Edit Connection") { edit(profile) }
|
|
}
|
|
}
|
|
}
|
|
Section("SCHEMAS") {
|
|
ForEach(model.databases, id: \.self) { db in
|
|
Button {
|
|
model.selectedDatabase = db
|
|
} label: {
|
|
Label(db, systemImage: "cylinder.split.1x2")
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
}
|
|
.listStyle(.sidebar)
|
|
.navigationTitle("Navigator")
|
|
.toolbar {
|
|
Button {
|
|
editing = ConnectionProfile(name: "New Connection")
|
|
editingPassword = ""
|
|
showingEditor = true
|
|
} label: {
|
|
Image(systemName: "plus")
|
|
}
|
|
.help("New connection")
|
|
Button {
|
|
model.loadDatabases()
|
|
} label: {
|
|
Image(systemName: "arrow.clockwise")
|
|
}
|
|
.help("Connect / refresh schemas")
|
|
}
|
|
}
|
|
|
|
private var editorPanel: some View {
|
|
VStack(spacing: 0) {
|
|
editorToolbar
|
|
Divider()
|
|
TextEditor(text: $model.sql)
|
|
.font(.system(size: 14, design: .monospaced))
|
|
.frame(minHeight: 190)
|
|
.padding(8)
|
|
Divider()
|
|
resultsPanel
|
|
Divider()
|
|
statusBar
|
|
}
|
|
.navigationTitle("Workbench Replacement")
|
|
}
|
|
|
|
private var editorToolbar: some View {
|
|
HStack {
|
|
Label("SQL Editor", systemImage: "chevron.left.forwardslash.chevron.right")
|
|
.font(.headline)
|
|
Spacer()
|
|
Button("Edit Connection") {
|
|
if let profile = model.selected { edit(profile) }
|
|
}
|
|
Button("Execute") { model.runSQL() }
|
|
.keyboardShortcut(.return, modifiers: [.command])
|
|
.disabled(model.isRunning)
|
|
}
|
|
.padding(12)
|
|
}
|
|
|
|
private var resultsPanel: some View {
|
|
VStack(spacing: 0) {
|
|
HStack {
|
|
Text("Result Grid").font(.headline)
|
|
Spacer()
|
|
Text(model.result.message).foregroundStyle(.secondary)
|
|
}
|
|
.padding(12)
|
|
if model.result.columns.isEmpty {
|
|
ContentUnavailableView("No Results", systemImage: "tablecells",
|
|
description: Text("Run a query to display results."))
|
|
} else {
|
|
resultsGrid
|
|
}
|
|
}
|
|
}
|
|
|
|
private var resultsGrid: some View {
|
|
ScrollView([.horizontal, .vertical]) {
|
|
Grid(alignment: .leading, horizontalSpacing: 14, verticalSpacing: 8) {
|
|
GridRow {
|
|
ForEach(model.result.columns, id: \.self) { name in
|
|
Text(name)
|
|
.fontWeight(.semibold)
|
|
.frame(minWidth: 130, alignment: .leading)
|
|
}
|
|
}
|
|
ForEach(model.result.rows.indices, id: \.self) { rowIndex in
|
|
resultRow(model.result.rows[rowIndex])
|
|
}
|
|
}
|
|
.padding(12)
|
|
}
|
|
}
|
|
|
|
private func resultRow(_ row: [String]) -> some View {
|
|
GridRow {
|
|
ForEach(model.result.columns.indices, id: \.self) { index in
|
|
Text(index < row.count ? row[index] : "")
|
|
.font(.system(.body, design: .monospaced))
|
|
.frame(minWidth: 130, alignment: .leading)
|
|
}
|
|
}
|
|
}
|
|
|
|
private var statusBar: some View {
|
|
HStack {
|
|
Circle()
|
|
.fill(model.status == "Error" ? Color.red : Color.green)
|
|
.frame(width: 8, height: 8)
|
|
Text(model.status).lineLimit(1)
|
|
Spacer()
|
|
Text(model.selected?.name ?? "No connection")
|
|
}
|
|
.font(.caption)
|
|
.padding(9)
|
|
}
|
|
|
|
private func edit(_ profile: ConnectionProfile) {
|
|
editing = profile
|
|
editingPassword = PasswordStore.read(profile.id)
|
|
showingEditor = true
|
|
}
|
|
}
|
|
|
|
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 (stored in macOS Keychain)", text: $password)
|
|
TextField("Default Schema", text: $profile.database)
|
|
}
|
|
Text("Built-in MySQL protocol client. This first version supports loopback TCP connections and local SSH tunnels. Remote TLS and full caching_sha2 authentication are not implemented yet.")
|
|
.font(.caption).foregroundStyle(.secondary)
|
|
HStack {
|
|
Spacer()
|
|
Button("Cancel") { dismiss() }
|
|
Button("Save") { save() }.buttonStyle(.borderedProminent)
|
|
}
|
|
}
|
|
.padding(24)
|
|
.frame(width: 520)
|
|
}
|
|
}
|