new tests
This commit is contained in:
+137
-14
@@ -29,6 +29,7 @@ struct MainView: View {
|
||||
@State private var editResultRow: [String]? = nil
|
||||
private let resultPageSize = 1_000
|
||||
@State private var showingEditor = false
|
||||
@State private var showingConnectionManager = false
|
||||
@State private var showingDatabaseTransfer = false
|
||||
@State private var tableSearchTarget: WorkbenchTableTarget? = nil
|
||||
@State private var tableImportTarget: WorkbenchTableTarget? = nil
|
||||
@@ -169,6 +170,17 @@ struct MainView: View {
|
||||
.sheet(item: $schemaManagement, onDismiss: { model.loadDatabases() }) { target in
|
||||
WorkbenchSchemaManagementSheet(model: model, target: target)
|
||||
}
|
||||
.sheet(isPresented: $showingConnectionManager) {
|
||||
DBSynConnectionManager(model: model) { profile in
|
||||
showingConnectionManager = false
|
||||
edit(profile)
|
||||
} onAdd: {
|
||||
showingConnectionManager = false
|
||||
editing = ConnectionProfile(name: "New Connection")
|
||||
editingPassword = ""
|
||||
showingEditor = true
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingEditor) {
|
||||
ConnectionEditor(
|
||||
profile: $editing,
|
||||
@@ -258,8 +270,29 @@ struct MainView: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
Menu {
|
||||
ForEach(model.profiles) { profile in
|
||||
Button {
|
||||
model.selectConnection(profile.id)
|
||||
} label: {
|
||||
if model.selectedID == profile.id {
|
||||
Label(profile.name, systemImage: "checkmark.circle.fill")
|
||||
} else {
|
||||
Label(profile.name, systemImage: "server.rack")
|
||||
}
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
Button("Manage Connections…") { showingConnectionManager = true }
|
||||
} label: {
|
||||
Label(model.selected?.name ?? "Select Connection", systemImage: "server.rack")
|
||||
.lineLimit(1)
|
||||
.frame(maxWidth: 185)
|
||||
}
|
||||
.help("Switch active MySQL connection")
|
||||
|
||||
VStack(alignment: .trailing, spacing: 2) {
|
||||
Text(model.selected?.name ?? "No Connection")
|
||||
Text(model.selected?.host ?? "Not connected")
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
|
||||
Text(model.selectedDatabase.isEmpty
|
||||
@@ -366,8 +399,7 @@ struct MainView: View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
ForEach(model.profiles) { profile in
|
||||
Button {
|
||||
model.selectedID = profile.id
|
||||
model.loadDatabases()
|
||||
model.selectConnection(profile.id)
|
||||
} label: {
|
||||
Label(
|
||||
profile.name,
|
||||
@@ -2455,11 +2487,17 @@ struct ConnectionEditor: View {
|
||||
let save: () -> Void
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State private var testMessage = ""
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
Text("Connection Settings")
|
||||
.font(.title2.bold())
|
||||
HStack {
|
||||
Text("Connection Settings").font(.title2.bold())
|
||||
Spacer()
|
||||
Button { dismiss() } label: { Image(systemName: "xmark.circle.fill") }
|
||||
.buttonStyle(.borderless)
|
||||
.help("Close")
|
||||
}
|
||||
|
||||
Form {
|
||||
TextField(
|
||||
@@ -2472,11 +2510,7 @@ struct ConnectionEditor: View {
|
||||
text: $profile.host
|
||||
)
|
||||
|
||||
TextField(
|
||||
"Port",
|
||||
value: $profile.port,
|
||||
format: .number
|
||||
)
|
||||
TextField("Port", value: $profile.port, format: .number.grouping(.never))
|
||||
|
||||
TextField(
|
||||
"Username",
|
||||
@@ -2488,13 +2522,28 @@ struct ConnectionEditor: View {
|
||||
text: $password
|
||||
)
|
||||
|
||||
TextField(
|
||||
"Default Schema",
|
||||
text: $profile.database
|
||||
)
|
||||
TextField("Default Schema", text: $profile.database)
|
||||
Text("Remote MySQL requires a trusted SSH tunnel until native TLS support is available.")
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
if !testMessage.isEmpty {
|
||||
Text(testMessage).font(.caption).foregroundStyle(.secondary)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
HStack {
|
||||
Button("Test Connection") {
|
||||
let candidate = profile
|
||||
let secret = password
|
||||
Task {
|
||||
do {
|
||||
let result = try await NativeMySQLRunner.run(
|
||||
profile: candidate, database: candidate.database,
|
||||
password: secret, sql: "SELECT VERSION()")
|
||||
testMessage = "Connection successful: " + (result.rows.first?.first ?? "MySQL")
|
||||
} catch { testMessage = error.localizedDescription }
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
|
||||
Button("Cancel") {
|
||||
@@ -7102,3 +7151,77 @@ private struct DBSynInlineRowEditor: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Connection Manager
|
||||
private struct DBSynConnectionManager: View {
|
||||
@ObservedObject var model: DatabaseModel
|
||||
let onEdit: (ConnectionProfile) -> Void
|
||||
let onAdd: () -> Void
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State private var search = ""
|
||||
@State private var confirmDelete: UUID?
|
||||
|
||||
private var filtered: [ConnectionProfile] {
|
||||
model.profiles.filter { search.isEmpty || $0.name.localizedCaseInsensitiveContains(search)
|
||||
|| $0.host.localizedCaseInsensitiveContains(search) }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
HStack {
|
||||
Label("Connections", systemImage: "server.rack").font(.title2.bold())
|
||||
Spacer()
|
||||
Button { dismiss() } label: { Image(systemName: "xmark.circle.fill") }
|
||||
.buttonStyle(.borderless)
|
||||
}
|
||||
TextField("Search connections", text: $search)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
List(filtered) { profile in
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: model.selectedID == profile.id ? "checkmark.circle.fill" : "server.rack")
|
||||
.foregroundStyle(model.selectedID == profile.id ? Color.blue : Color.secondary)
|
||||
VStack(alignment: .leading) {
|
||||
Text(profile.name).font(.headline)
|
||||
Text("\(profile.username)@\(profile.host):\(profile.port)")
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Button("Connect") { model.selectConnection(profile.id); dismiss() }
|
||||
.disabled(model.selectedID == profile.id)
|
||||
Button("Edit") { onEdit(profile) }
|
||||
Button {
|
||||
var copy = profile
|
||||
copy.id = UUID()
|
||||
copy.name += " Copy"
|
||||
model.save(copy, password: PasswordStore.read(profile.id))
|
||||
} label: { Image(systemName: "plus.square.on.square") }
|
||||
.help("Duplicate")
|
||||
Button(role: .destructive) { confirmDelete = profile.id } label: {
|
||||
Image(systemName: "trash")
|
||||
}
|
||||
.disabled(model.profiles.count <= 1)
|
||||
}
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
HStack {
|
||||
Button("Add Connection") { onAdd() }
|
||||
Spacer()
|
||||
Button("Done") { dismiss() }.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}
|
||||
.padding(24)
|
||||
.frame(minWidth: 750, minHeight: 430)
|
||||
.alert("Delete connection?", isPresented: Binding(
|
||||
get: { confirmDelete != nil }, set: { if !$0 { confirmDelete = nil } }
|
||||
)) {
|
||||
Button("Delete", role: .destructive) {
|
||||
if let id = confirmDelete { model.deleteConnection(id) }
|
||||
confirmDelete = nil
|
||||
}
|
||||
Button("Cancel", role: .cancel) { confirmDelete = nil }
|
||||
} message: {
|
||||
Text("This removes the saved connection and its DBSyn Keychain credential.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user