87 lines
4.0 KiB
Swift
87 lines
4.0 KiB
Swift
import SwiftUI
|
|
|
|
struct IOSDashboardView: View {
|
|
@EnvironmentObject private var appState: AppState
|
|
|
|
private var failedProjects: [ProjectSnapshot] {
|
|
appState.projects.filter { $0.qualityGate.uppercased() != "OK" }
|
|
}
|
|
|
|
private var bugs: Int { total("bugs") }
|
|
private var vulnerabilities: Int { total("vulnerabilities") }
|
|
private var codeSmells: Int { total("code_smells") }
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
ScrollView {
|
|
LazyVGrid(columns: [GridItem(.adaptive(minimum: 155), spacing: 12)], spacing: 12) {
|
|
metricCard("Projects", value: appState.projects.count, icon: "square.stack.3d.up")
|
|
metricCard("Failed", value: failedProjects.count, icon: "xmark.octagon.fill", isFailure: !failedProjects.isEmpty)
|
|
metricCard("Bugs", value: bugs, icon: "ladybug")
|
|
metricCard("Vulnerabilities", value: vulnerabilities, icon: "lock.trianglebadge.exclamationmark")
|
|
metricCard("Code Smells", value: codeSmells, icon: "exclamationmark.bubble")
|
|
}
|
|
.padding()
|
|
|
|
if !failedProjects.isEmpty {
|
|
VStack(alignment: .leading, spacing: 10) {
|
|
Text("Needs attention")
|
|
.font(.headline)
|
|
ForEach(failedProjects) { snapshot in
|
|
NavigationLink {
|
|
IOSProjectDetailView(snapshot: snapshot)
|
|
} label: {
|
|
HStack {
|
|
Image(systemName: "xmark.octagon.fill")
|
|
.foregroundStyle(.red)
|
|
VStack(alignment: .leading) {
|
|
Text(snapshot.project.name).fontWeight(.semibold)
|
|
Text(snapshot.qualityGate).font(.caption)
|
|
}
|
|
Spacer()
|
|
Image(systemName: "chevron.right")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.padding()
|
|
.background(.red.opacity(0.10), in: RoundedRectangle(cornerRadius: 14))
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
.padding(.horizontal)
|
|
}
|
|
}
|
|
.navigationTitle("Dashboard")
|
|
.toolbar {
|
|
ToolbarItem(placement: .topBarTrailing) {
|
|
Button { Task { await appState.refresh() } } label: {
|
|
if appState.isLoading { ProgressView() } else { Image(systemName: "arrow.clockwise") }
|
|
}
|
|
.disabled(appState.isLoading || appState.selectedServer == nil)
|
|
}
|
|
}
|
|
.overlay {
|
|
if appState.selectedServer == nil {
|
|
ContentUnavailableView("No SonarQube Server", systemImage: "server.rack", description: Text("Add or sync a server to start using SonarDesk."))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func total(_ key: String) -> Int {
|
|
appState.projects.reduce(0) { $0 + (Int($1.measures[key] ?? "0") ?? 0) }
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func metricCard(_ title: String, value: Int, icon: String, isFailure: Bool = false) -> some View {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
Image(systemName: icon).font(.title2).foregroundStyle(isFailure ? .red : .primary)
|
|
Text("\(value)").font(.system(.title, design: .rounded, weight: .bold))
|
|
Text(title).font(.subheadline).foregroundStyle(.secondary)
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding()
|
|
.background(isFailure ? .red.opacity(0.10) : Color.secondary.opacity(0.08), in: RoundedRectangle(cornerRadius: 16))
|
|
}
|
|
}
|