This commit is contained in:
Mark Gregory
2026-09-29 12:15:51 +01:00
parent 835cd28349
commit e27813e86f
7 changed files with 204 additions and 23 deletions
Vendored
BIN
View File
Binary file not shown.
+22 -1
View File
@@ -218,4 +218,25 @@ sonar-scanner.log
# *.xcodeproj
# *.xcworkspace
# *.xcassets
# *.entitlements
# *.entitlements
.DS_Store
**/.DS_Store
# Xcode per-user state
xcuserdata/
*.xcuserstate
UserInterfaceState.xcuserstate
# Xcode build output
DerivedData/
build/
.build/
+14
View File
@@ -1,6 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="BackendCodeEditorSettings">
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CommentTypo/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDFAArrayIndexOutOfBounds/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDFAConstantFunctionResult/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDFAConstantParameter/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDFAEndlessLoop/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDFAInfiniteRecursion/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDFATimeOver/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDFAUnreachableCode/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDFAUnreachableFunctionCall/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDFAUnreadVariable/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDFAUnusedValue/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppInconsistentNaming/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=IdentifierTypo/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=StringLiteralTypo/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
<option name="/Default/CodeStyle/CodeFormatting/CppClangFormat/EnableClangFormatSupport/@EntryValue" value="false" type="bool" />
<option name="/Default/CodeStyle/CodeFormatting/CppFormatting/ALIGN_MULTILINE_ARGUMENT/@EntryValue" value="true" type="bool" />
<option name="/Default/CodeStyle/CodeFormatting/CppFormatting/ALIGN_MULTILINE_BINARY_EXPRESSIONS_CHAIN/@EntryValue" value="true" type="bool" />
@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>SchemeUserState</key>
<dict>
<key>SonarDesk.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
</dict>
</dict>
</plist>
BIN
View File
Binary file not shown.
+168 -8
View File
@@ -42,6 +42,7 @@ struct IssuesView: View {
@State private var filter: IssueFilter
@State private var page = 1
@State private var pageSize = 25
@State private var selectedIssue: Issue?
init(projectKey: String?, initialFilter: IssueFilter = .all) {
self.projectKey = projectKey
@@ -64,6 +65,23 @@ struct IssuesView: View {
private var requestID: String { "\(projectKey ?? "all")|\(filter.rawValue)|\(page)|\(pageSize)" }
var body: some View {
Group {
if let selectedIssue {
IssueDetailView(issue: selectedIssue, tint: filter.tint) {
self.selectedIssue = nil
}
} else {
issueList
}
}
.background(SonarTheme.canvas)
.navigationTitle(selectedIssue == nil ? filter.rawValue : "Issue Details")
.task(id: requestID) { await app.loadIssues(projectKey: projectKey, filter: filter, page: page, pageSize: pageSize) }
.onChange(of: filter) { _, _ in page = 1; search = ""; selectedIssue = nil }
.onChange(of: pageSize) { _, _ in page = 1 }
}
private var issueList: some View {
VStack(spacing: 0) {
issueHeader
Divider().opacity(0.55)
@@ -93,11 +111,6 @@ struct IssuesView: View {
Divider().opacity(0.55)
pagination
}
.background(SonarTheme.canvas)
.navigationTitle(filter.rawValue)
.task(id: requestID) { await app.loadIssues(projectKey: projectKey, filter: filter, page: page, pageSize: pageSize) }
.onChange(of: filter) { _, _ in page = 1; search = "" }
.onChange(of: pageSize) { _, _ in page = 1 }
}
private var issueHeader: some View {
@@ -156,6 +169,8 @@ struct IssuesView: View {
}
.padding(.horizontal, 20).padding(.vertical, 12)
.contentShape(Rectangle())
.onTapGesture { selectedIssue = issue }
.help("Open issue details")
}
private var pagination: some View {
@@ -164,12 +179,34 @@ struct IssuesView: View {
Text(app.issueTotal == 0 ? "No issues" : "Showing \(start)–\(min(page * pageSize, app.issueTotal)) of \(app.issueTotal)")
.font(.caption).foregroundStyle(.secondary)
Spacer()
Button { page = max(1, page - 1) } label: { Label("Previous", systemImage: "chevron.left") }.disabled(page <= 1 || app.isLoading)
Text("Page \(page) of \(pageCount)").font(.caption.monospacedDigit()).frame(minWidth: 92)
Button { page = min(pageCount, page + 1) } label: { Label("Next", systemImage: "chevron.right") }.disabled(page >= pageCount || app.isLoading)
pagingButton("Previous", systemImage: "chevron.left", disabled: page <= 1 || app.isLoading) {
page = max(1, page - 1)
}
Text("Page \(page) of \(pageCount)").font(.caption.monospacedDigit().weight(.semibold)).frame(minWidth: 92)
pagingButton("Next", systemImage: "chevron.right", disabled: page >= pageCount || app.isLoading) {
page = min(pageCount, page + 1)
}
}.padding(.horizontal, 18).padding(.vertical, 12).background(SonarTheme.panel.opacity(0.45))
}
private func pagingButton(_ title: String, systemImage: String, disabled: Bool, action: @escaping () -> Void) -> some View {
Button(action: action) {
Label(title, systemImage: systemImage)
.font(.subheadline.weight(.semibold))
.foregroundStyle(disabled ? Color.primary.opacity(0.55) : Color.white)
.padding(.horizontal, 14)
.padding(.vertical, 7)
.background(disabled ? Color.primary.opacity(0.08) : SonarTheme.accent,
in: RoundedRectangle(cornerRadius: 8, style: .continuous))
.overlay {
RoundedRectangle(cornerRadius: 8, style: .continuous)
.stroke(disabled ? Color.primary.opacity(0.14) : SonarTheme.accent.opacity(0.8))
}
}
.buttonStyle(.plain)
.allowsHitTesting(!disabled)
}
private func severityBadge(_ severity: String) -> some View {
let color: Color = switch severity.uppercased() {
case "BLOCKER", "CRITICAL": SonarTheme.danger
@@ -184,3 +221,126 @@ struct IssuesView: View {
private func displayType(_ value: String?) -> String { (value ?? "—").replacingOccurrences(of: "_", with: " ").capitalized }
private func shortComponent(_ value: String?) -> String { guard let value else { return "—" }; return value.split(separator: ":").last.map(String.init) ?? value }
}
private struct IssueDetailView: View {
let issue: Issue
let tint: Color
let onBack: () -> Void
var body: some View {
VStack(spacing: 0) {
HStack(spacing: 12) {
Button(action: onBack) {
Label("Back to Issues", systemImage: "chevron.left")
.font(.subheadline.weight(.semibold))
.foregroundStyle(Color.white)
.padding(.horizontal, 13)
.padding(.vertical, 7)
.background(SonarTheme.accent, in: RoundedRectangle(cornerRadius: 8, style: .continuous))
}
.buttonStyle(.plain)
Divider().frame(height: 24)
Image(systemName: icon)
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(tint)
Text("Issue Details").font(.title2.bold())
Spacer()
badge(issue.severity ?? "UNKNOWN", color: severityColor)
badge(displayType(issue.type), color: tint)
}
.padding(.horizontal, 22).padding(.vertical, 16)
.background(SonarTheme.panel.opacity(0.45))
ScrollView {
VStack(alignment: .leading, spacing: 18) {
SonarPanel {
VStack(alignment: .leading, spacing: 10) {
Text("MESSAGE").font(.caption2.bold()).foregroundStyle(.secondary)
Text(issue.message ?? "No issue message supplied by SonarQube.")
.font(.title3.weight(.semibold))
.textSelection(.enabled)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(18)
}
SonarPanel {
VStack(spacing: 0) {
detailRow("Rule", issue.rule ?? "—")
Divider().opacity(0.45)
detailRow("Status", issue.status ?? "—")
Divider().opacity(0.45)
detailRow("Type", displayType(issue.type))
Divider().opacity(0.45)
detailRow("Severity", issue.severity?.capitalized ?? "—")
Divider().opacity(0.45)
detailRow("Project", issue.project ?? "—")
Divider().opacity(0.45)
detailRow("Component / File", issue.component ?? "—")
Divider().opacity(0.45)
detailRow("Line", issue.line.map(String.init) ?? "—")
Divider().opacity(0.45)
detailRow("Effort", issue.effort ?? "—")
Divider().opacity(0.45)
detailRow("Created", formattedDate(issue.creationDate))
Divider().opacity(0.45)
detailRow("Issue Key", issue.key)
}
.padding(.horizontal, 18)
}
}
.padding(22)
.frame(maxWidth: 980)
.frame(maxWidth: .infinity)
}
}
}
private var icon: String {
switch issue.type?.uppercased() {
case "BUG": "ladybug.fill"
case "VULNERABILITY": "lock.trianglebadge.exclamationmark.fill"
case "CODE_SMELL": "exclamationmark.bubble.fill"
default: "exclamationmark.triangle.fill"
}
}
private var severityColor: Color {
switch issue.severity?.uppercased() {
case "BLOCKER", "CRITICAL": SonarTheme.danger
case "MAJOR": SonarTheme.warning
case "MINOR": SonarTheme.info
default: .secondary
}
}
private func detailRow(_ label: String, _ value: String) -> some View {
HStack(alignment: .top, spacing: 18) {
Text(label).font(.caption.weight(.semibold)).foregroundStyle(.secondary).frame(width: 130, alignment: .leading)
Text(value).font(.subheadline).textSelection(.enabled).frame(maxWidth: .infinity, alignment: .leading)
}
.padding(.vertical, 11)
}
private func badge(_ value: String, color: Color) -> some View {
Text(value.replacingOccurrences(of: "_", with: " ").capitalized)
.font(.caption.weight(.bold))
.foregroundStyle(color)
.padding(.horizontal, 9).padding(.vertical, 5)
.background(color.opacity(0.13), in: Capsule())
}
private func displayType(_ value: String?) -> String {
(value ?? "—").replacingOccurrences(of: "_", with: " ").capitalized
}
private func formattedDate(_ value: String?) -> String {
guard let value else { return "—" }
let formatter = ISO8601DateFormatter()
guard let date = formatter.date(from: value) else { return value }
return date.formatted(date: .abbreviated, time: .shortened)
}
}