connection updates

This commit is contained in:
Mark Gregory
2026-10-19 11:31:55 +01:00
parent 5aba461451
commit c6504b3d1b
+125 -39
View File
@@ -3155,36 +3155,6 @@ struct WorkbenchAdministrationView: View {
processControls
}
if page == .users && !result.rows.isEmpty {
HStack {
Picker("Account", selection: $selectedAccount) {
Text("Select account").tag(String?.none)
ForEach(result.rows.indices, id: \.self) { i in
let row = result.rows[i]
if row.count >= 2 {
Text("\(row[0])@\(row[1])")
.tag(Optional("\(row[0])\u{001f}\(row[1])"))
}
}
}
.frame(maxWidth: 320)
Button("Show Grants") {
Task { await showGrants() }
}
.disabled(selectedAccount == nil)
Button("Manage Accounts…") {
autoRefresh = false
showingAccountManager = true
}
.disabled(profile == nil)
Spacer()
Text("Changes require confirmation")
.foregroundStyle(.secondary)
}
.padding(.horizontal, 16)
.padding(.vertical, 8)
}
Divider()
footer
@@ -3466,6 +3436,8 @@ struct WorkbenchAdministrationView: View {
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.accessibilityIdentifier("scrollable-dashboard-phase51")
case .users:
accountAdministration
case .serverLogs:
VStack(alignment: .leading, spacing: 0) {
HStack {
@@ -3516,6 +3488,108 @@ struct WorkbenchAdministrationView: View {
}
}
// Workbench-style account administration: persistent account list and inspector.
private var accountAdministration: some View {
HStack(spacing: 0) {
VStack(spacing: 0) {
HStack {
Label("User Accounts", systemImage: "person.2")
.font(.headline)
Spacer()
Text("\(filteredRows.count)")
.foregroundStyle(.secondary)
}
.padding(12)
Divider()
List(selection: $selectedAccount) {
ForEach(filteredRows.indices, id: \.self) { index in
let row = filteredRows[index]
if row.count >= 2 {
let key = "\(row[0])\u{001f}\(row[1])"
VStack(alignment: .leading, spacing: 4) {
Label(row[0].isEmpty ? "(anonymous)" : row[0], systemImage: "person.crop.circle")
.font(.body.weight(.medium))
Text("Host: \(row[1])")
.font(.caption)
.foregroundStyle(.secondary)
}
.padding(.vertical, 4)
.tag(key)
}
}
}
.listStyle(.sidebar)
Divider()
HStack {
Button {
selectedAccount = nil
showingAccountManager = true
} label: { Image(systemName: "plus") }
.help("Create MySQL account")
Spacer()
Button { Task { await refresh() } } label: {
Image(systemName: "arrow.clockwise")
}
.help("Refresh accounts")
}
.buttonStyle(.borderless)
.padding(12)
}
.frame(minWidth: 205, idealWidth: 245, maxWidth: 290)
Divider()
VStack(alignment: .leading, spacing: 0) {
if let selectedAccount,
let row = result.rows.first(where: {
$0.count >= 2 && "\($0[0])\u{001f}\($0[1])" == selectedAccount
}) {
HStack(alignment: .top) {
VStack(alignment: .leading, spacing: 6) {
Text(row[0].isEmpty ? "Anonymous account" : row[0])
.font(.title2.bold())
Text("'\(row[0])'@'\(row[1])'")
.font(.system(.subheadline, design: .monospaced))
.foregroundStyle(.secondary)
.textSelection(.enabled)
}
Spacer()
Button("Edit Account…") {
showingAccountManager = true
}
.disabled(profile == nil)
}
.padding(18)
Divider()
Form {
Section("Login") {
LabeledContent("User", value: row[0])
LabeledContent("Host", value: row[1])
if row.count > 2 { LabeledContent("Authentication plugin", value: row[2]) }
if row.count > 3 {
LabeledContent("Account status", value: row[3] == "Y" ? "Locked" : "Active")
}
}
Section("Administrative Actions") {
HStack {
Button("View Grants…") { Task { await showGrants() } }
Button("Manage Privileges…") { showingAccountManager = true }
Button("Change Password…") { showingAccountManager = true }
}
Text("Changes require MySQL administrative privileges and explicit confirmation.")
.font(.caption).foregroundStyle(.secondary)
}
}
.formStyle(.grouped)
Spacer(minLength: 0)
} else {
ContentUnavailableView("Select a MySQL Account", systemImage: "person.crop.circle.badge.checkmark", description: Text("Choose an account on the left to inspect its login, authentication and privileges."))
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
private func logValue(_ name: String, in row: [String]) -> String {
guard let index = result.columns.firstIndex(where: { $0.caseInsensitiveCompare(name) == .orderedSame }),
index < row.count else { return "" }
@@ -4963,18 +5037,30 @@ private struct WorkbenchAccountManagerView: View {
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("Manage MySQL Accounts").font(.title2.bold())
Text("Account changes run on \(profile.name). Review the SQL before applying it.")
HStack {
Label("Users and Privileges", systemImage: "person.crop.circle.badge.checkmark")
.font(.title2.bold())
Spacer()
Text(profile.name).foregroundStyle(.secondary)
}
Text("Manage login credentials, account access and schema privileges. Every change requires SQL review and confirmation.")
.foregroundStyle(.secondary)
Form {
Picker("Operation", selection: $action) {
ForEach(WorkbenchAccountAction.allCases) { option in
Text(option.rawValue).tag(option)
}
Section("Account") {
TextField("Username", text: $username)
.textContentType(.username)
TextField("Host", text: $hostname)
Text("MySQL accounts are identified by both username and host.")
.font(.caption).foregroundStyle(.secondary)
}
Section("Account Management") {
Picker("Operation", selection: $action) {
ForEach(WorkbenchAccountAction.allCases) { option in
Text(option.rawValue).tag(option)
}
}
.pickerStyle(.menu)
}
TextField("Username", text: $username)
.textContentType(.username)
TextField("Host", text: $hostname)
if action == .create || action == .password {
SecureField("Password", text: $password)
}