connection updates
This commit is contained in:
+125
-39
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user