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:
@@ -50,6 +50,8 @@ struct MainView: View {
|
||||
@State private var expandedSchemas: Set<String> = []
|
||||
@State private var expandedTableFolders: Set<String> = []
|
||||
@State private var expandedObjectFolders: Set<String> = []
|
||||
@State private var schemaFilter = ""
|
||||
@State private var schemaManagement: WorkbenchSchemaManagementTarget? = nil
|
||||
@State private var selectedTableName: String? = nil
|
||||
@State private var outputExpanded = true
|
||||
|
||||
@@ -123,6 +125,9 @@ struct MainView: View {
|
||||
}) { target in
|
||||
WorkbenchObjectEditorView(profile: model.selected, target: target)
|
||||
}
|
||||
.sheet(item: $schemaManagement, onDismiss: { model.loadDatabases() }) { target in
|
||||
WorkbenchSchemaManagementSheet(model: model, target: target)
|
||||
}
|
||||
.sheet(isPresented: $showingEditor) {
|
||||
ConnectionEditor(
|
||||
profile: $editing,
|
||||
@@ -296,11 +301,16 @@ struct MainView: View {
|
||||
.padding(.horizontal, 12)
|
||||
.frame(height: 34)
|
||||
|
||||
TextField("Filter schemas", text: $schemaFilter)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding(.horizontal, 9)
|
||||
.padding(.bottom, 5)
|
||||
|
||||
Divider()
|
||||
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading, spacing: 2) {
|
||||
ForEach(model.databases, id: \.self) { db in
|
||||
ForEach(model.databases.filter { schemaFilter.isEmpty || $0.localizedCaseInsensitiveContains(schemaFilter) }, id: \.self) { db in
|
||||
schemaNode(db)
|
||||
}
|
||||
}
|
||||
@@ -459,16 +469,35 @@ struct MainView: View {
|
||||
}
|
||||
}
|
||||
.contextMenu {
|
||||
Button("Set as Default Schema") {
|
||||
model.selectedDatabase = db
|
||||
Button("Set as Default Schema") { model.selectedDatabase = db }
|
||||
Button("Filter to This Schema") { schemaFilter = db }
|
||||
Button("Clear Schema Filter") { schemaFilter = "" }
|
||||
.disabled(schemaFilter.isEmpty)
|
||||
Button("Schema Inspector…") {
|
||||
schemaManagement = .init(database: db, action: .inspect)
|
||||
}
|
||||
|
||||
Button("Refresh Tables") {
|
||||
model.loadTables(database: db)
|
||||
Divider()
|
||||
Button("Copy Schema Name") {
|
||||
NSPasteboard.general.clearContents()
|
||||
NSPasteboard.general.setString(db, forType: .string)
|
||||
}
|
||||
Button("Send CREATE Statement to SQL Editor") {
|
||||
revealSchemaSQL("SHOW CREATE DATABASE `\(db.replacingOccurrences(of: "`", with: "``"))`;")
|
||||
}
|
||||
Divider()
|
||||
Button("Create Schema…") { schemaManagement = .init(database: nil, action: .create) }
|
||||
Button("Alter Schema…") { schemaManagement = .init(database: db, action: .alter) }
|
||||
Button("Drop Schema…", role: .destructive) {
|
||||
schemaManagement = .init(database: db, action: .drop)
|
||||
}
|
||||
Divider()
|
||||
Button("Create Table…") {
|
||||
schemaEditorTarget = SchemaEditorTarget(database: db, table: nil)
|
||||
}
|
||||
Button("Refresh All") {
|
||||
model.loadDatabases()
|
||||
model.loadTables(database: db)
|
||||
}
|
||||
}
|
||||
|
||||
if expandedSchemas.contains(db) {
|
||||
@@ -478,6 +507,13 @@ struct MainView: View {
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
private func revealSchemaSQL(_ statement: String) {
|
||||
model.sql = statement
|
||||
administrationPage = nil
|
||||
navigatorMode = 1
|
||||
DispatchQueue.main.async { sqlFocusRequest += 1 }
|
||||
}
|
||||
|
||||
private func toggleSchema(_ db: String) {
|
||||
if expandedSchemas.contains(db) {
|
||||
expandedSchemas.remove(db)
|
||||
@@ -5366,3 +5402,98 @@ private struct WorkbenchObjectEditorView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Phase 57: Schema management
|
||||
private struct WorkbenchSchemaManagementTarget: Identifiable {
|
||||
enum Action { case create, alter, drop, inspect }
|
||||
let id = UUID()
|
||||
let database: String?
|
||||
let action: Action
|
||||
}
|
||||
|
||||
private struct WorkbenchSchemaManagementSheet: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@ObservedObject var model: DatabaseModel
|
||||
let target: WorkbenchSchemaManagementTarget
|
||||
@State private var name = ""
|
||||
@State private var charset = "utf8mb4"
|
||||
@State private var collation = "utf8mb4_0900_ai_ci"
|
||||
@State private var confirmation = ""
|
||||
@State private var submitted = false
|
||||
|
||||
private var escapedName: String { name.replacingOccurrences(of: "`", with: "``") }
|
||||
private var title: String {
|
||||
switch target.action {
|
||||
case .create: return "Create Schema"
|
||||
case .alter: return "Alter Schema"
|
||||
case .drop: return "Drop Schema"
|
||||
case .inspect: return "Schema Inspector"
|
||||
}
|
||||
}
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text(title).font(.title2.bold())
|
||||
if target.action == .inspect {
|
||||
LabeledContent("Schema", value: name)
|
||||
Text("Inspect the schema definition and available metadata using the SQL editor.")
|
||||
.foregroundStyle(.secondary)
|
||||
Button("Show CREATE DATABASE") {
|
||||
model.sql = "SHOW CREATE DATABASE `\(escapedName)`;"
|
||||
dismiss()
|
||||
}
|
||||
} else {
|
||||
TextField("Schema name", text: $name)
|
||||
.disabled(target.action != .create)
|
||||
if target.action != .drop {
|
||||
TextField("Character set", text: $charset)
|
||||
TextField("Collation", text: $collation)
|
||||
Text("Character set and collation must be valid identifiers supported by the server.")
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
} else {
|
||||
Text("This permanently deletes the schema and ALL its tables, views, routines and data. This cannot be undone.")
|
||||
.foregroundStyle(.red)
|
||||
TextField("Type the schema name to confirm", text: $confirmation)
|
||||
}
|
||||
if submitted {
|
||||
Text("Statement submitted. Check the SQL result/status before continuing.")
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
Spacer()
|
||||
Button("Close") { dismiss() }
|
||||
if target.action != .inspect {
|
||||
Button(target.action == .drop ? "Drop Schema" : "Apply") { apply() }
|
||||
.disabled(!valid || submitted)
|
||||
.keyboardShortcut(.defaultAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(24)
|
||||
.frame(width: 480)
|
||||
.onAppear { name = target.database ?? "" }
|
||||
}
|
||||
private var valid: Bool {
|
||||
guard !name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return false }
|
||||
if target.action == .drop { return confirmation == name }
|
||||
if target.action == .inspect { return true }
|
||||
let allowed = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "_"))
|
||||
return !charset.isEmpty && !collation.isEmpty && charset.unicodeScalars.allSatisfy(allowed.contains) && collation.unicodeScalars.allSatisfy(allowed.contains)
|
||||
}
|
||||
private func apply() {
|
||||
guard valid else { return }
|
||||
let sql: String
|
||||
switch target.action {
|
||||
case .create:
|
||||
sql = "CREATE DATABASE `\(escapedName)` CHARACTER SET \(charset) COLLATE \(collation);"
|
||||
case .alter:
|
||||
sql = "ALTER DATABASE `\(escapedName)` CHARACTER SET \(charset) COLLATE \(collation);"
|
||||
case .drop:
|
||||
sql = "DROP DATABASE `\(escapedName)`;"
|
||||
case .inspect: return
|
||||
}
|
||||
model.runSQL(statement: sql)
|
||||
submitted = true
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user