Files
SonarDesk/SonarDesk-iOS/Views/IOSSettingsView.swift
2026-10-06 09:13:11 +01:00

155 lines
8.0 KiB
Swift

import SwiftUI
struct IOSSettingsView: View {
@EnvironmentObject private var appState: AppState
@EnvironmentObject private var workspaceStore: WorkspaceStore
@State private var editor: ServerEditorItem?
@State private var serverToDelete: SonarServer?
var body: some View {
NavigationStack {
Form {
Section("Workspace") {
LabeledContent("Name", value: workspaceStore.selectedWorkspace.name)
LabeledContent("Type", value: workspaceStore.selectedWorkspace.kind == .team ? "Team" : "Personal")
LabeledContent("Role", value: workspaceStore.selectedWorkspace.role.rawValue.capitalized)
}
Section {
if appState.servers.isEmpty {
ContentUnavailableView("No SonarQube Servers", systemImage: "server.rack", description: Text("Add a server to load your SonarQube projects."))
} else {
ForEach(appState.servers) { server in
Button {
appState.selectServer(server.id)
} label: {
HStack(spacing: 12) {
Image(systemName: appState.selectedServerID == server.id ? "checkmark.circle.fill" : "circle")
.foregroundStyle(appState.selectedServerID == server.id ? Color.accentColor : Color.secondary)
VStack(alignment: .leading, spacing: 3) {
Text(server.name).foregroundStyle(.primary)
Text(server.baseURL).font(.caption).foregroundStyle(.secondary).lineLimit(1)
}
Spacer()
Button { editor = ServerEditorItem(server: server) } label: { Image(systemName: "pencil") }
.buttonStyle(.borderless)
}
}
.swipeActions(edge: .trailing) {
Button(role: .destructive) { serverToDelete = server } label: { Label("Delete", systemImage: "trash") }
Button { editor = ServerEditorItem(server: server) } label: { Label("Edit", systemImage: "pencil") }.tint(.blue)
}
}
}
Button { editor = ServerEditorItem(server: nil) } label: {
Label("Add SonarQube Server", systemImage: "plus.circle.fill")
}
} header: { Text("SonarQube Servers") } footer: {
Text("Access tokens are stored only in this device's Keychain and are not written to iCloud.")
}
if let server = appState.selectedServer {
Section("Active Server") {
LabeledContent("Server", value: server.name)
LabeledContent("Address", value: server.baseURL)
if let version = appState.serverVersion { LabeledContent("Version", value: version) }
Button("Refresh Projects") { Task { await appState.refresh() } }
}
}
}
.navigationTitle("Settings")
.sheet(item: $editor) { item in
IOSServerEditorView(server: item.server)
.environmentObject(appState)
}
.confirmationDialog("Delete this server?", isPresented: Binding(get: { serverToDelete != nil }, set: { if !$0 { serverToDelete = nil } }), titleVisibility: .visible) {
if let server = serverToDelete {
Button("Delete \(server.name)", role: .destructive) { appState.removeServer(server); serverToDelete = nil }
}
Button("Cancel", role: .cancel) { serverToDelete = nil }
} message: { Text("The server configuration and its token will be removed from this device.") }
}
}
}
private struct ServerEditorItem: Identifiable {
let id = UUID()
let server: SonarServer?
}
private struct IOSServerEditorView: View {
@EnvironmentObject private var appState: AppState
@Environment(\.dismiss) private var dismiss
let server: SonarServer?
@State private var name: String
@State private var url: String
@State private var token = ""
@State private var isTesting = false
@State private var isSaving = false
@State private var testResult: String?
@State private var errorMessage: String?
init(server: SonarServer?) {
self.server = server
_name = State(initialValue: server?.name ?? "")
_url = State(initialValue: server?.baseURL ?? "https://")
}
var body: some View {
NavigationStack {
Form {
Section("Server") {
TextField("Name", text: $name).textContentType(.organizationName)
TextField("https://sonarqube.example.com", text: $url)
.textInputAutocapitalization(.never).autocorrectionDisabled()
.keyboardType(.URL).textContentType(.URL)
}
Section("Authentication") {
SecureField(server == nil ? "SonarQube API token" : "New token (leave blank to keep current)", text: $token)
.textInputAutocapitalization(.never).autocorrectionDisabled()
Text(server == nil ? "Create a SonarQube user token and paste it here." : "For security the existing token cannot be displayed. Leave this blank unless you want to replace it.")
.font(.footnote).foregroundStyle(.secondary)
}
Section {
Button { Task { await testConnection() } } label: {
HStack { if isTesting { ProgressView() }; Label("Test Connection", systemImage: "network") }
}.disabled(isTesting || url.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || (server == nil && token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty))
if let testResult { Label(testResult, systemImage: "checkmark.circle.fill").foregroundStyle(.green) }
if let errorMessage { Text(errorMessage).foregroundStyle(.red).font(.footnote) }
}
}
.navigationTitle(server == nil ? "Add Server" : "Edit Server")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } }
ToolbarItem(placement: .confirmationAction) { Button("Save") { save() }.disabled(isSaving || url.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || (server == nil && token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)) }
}
}
}
@MainActor private func testConnection() async {
isTesting = true; errorMessage = nil; testResult = nil
defer { isTesting = false }
do {
let result: String
if let server, token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
result = try await appState.testServer(id: server.id)
} else {
result = try await appState.testServerConfiguration(name: name, url: url, token: token)
}
testResult = "Connected — \(result)"
} catch { errorMessage = error.localizedDescription }
}
private func save() {
isSaving = true; errorMessage = nil
do {
if let server { try appState.updateServer(id: server.id, name: name, url: url, token: token.isEmpty ? nil : token) }
else { try appState.addServer(name: name, url: url, token: token) }
dismiss()
} catch { errorMessage = error.localizedDescription; isSaving = false }
}
}