diff options
| author | Christian Cleberg <[email protected]> | 2026-07-17 16:06:47 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-07-17 16:06:47 -0500 |
| commit | d8862a9b78b0d6cf4e11cf537be7678794e08811 (patch) | |
| tree | 4af615370bb122e6706bbefc6deaa044e02b477b /octosentry/SecurityEventListView.swift | |
| download | octosentry-0.1.0.tar.gz octosentry-0.1.0.tar.bz2 octosentry-0.1.0.zip | |
Scaffold octosentry MVP: unified GitHub security alert feed0.1.0
MenuBarExtra popover aggregating Dependabot, code scanning, and secret
scanning alerts for a single repo into one severity-ranked stream. Swift 6
strict concurrency, zero third-party dependencies, PAT-via-env-var auth as
a dev-only shortcut ahead of the device authorization flow.
Diffstat (limited to 'octosentry/SecurityEventListView.swift')
| -rw-r--r-- | octosentry/SecurityEventListView.swift | 143 |
1 files changed, 143 insertions, 0 deletions
diff --git a/octosentry/SecurityEventListView.swift b/octosentry/SecurityEventListView.swift new file mode 100644 index 0000000..4542275 --- /dev/null +++ b/octosentry/SecurityEventListView.swift @@ -0,0 +1,143 @@ +// +// SecurityEventListView.swift +// octosentry +// + +import AppKit +import SwiftUI + +struct SecurityEventListView: View { + var store: SecurityEventStore + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + header + Divider() + content + } + .frame(width: 380, height: 420) + .task { + await store.refresh() + } + } + + private var header: some View { + HStack { + Text("Security Events") + .font(.headline) + + if store.isLoading { + ProgressView() + .controlSize(.small) + } + + Spacer() + + Button { + Task { await store.refresh() } + } label: { + Image(systemName: "arrow.clockwise") + } + .buttonStyle(.plain) + .disabled(store.isLoading) + + Button("Quit") { + NSApplication.shared.terminate(nil) + } + .buttonStyle(.plain) + .foregroundStyle(.secondary) + } + .padding(12) + } + + @ViewBuilder + private var content: some View { + if store.events.isEmpty && !store.errorMessages.isEmpty { + StatusView(systemImage: "exclamationmark.triangle", tint: .orange, message: store.errorMessages.joined(separator: "\n\n")) + } else if store.events.isEmpty && !store.isLoading { + VStack(spacing: 8) { + StatusView(systemImage: "checkmark.shield", tint: .green, message: "No open security alerts") + if !store.unavailableNotices.isEmpty { + NoticeBanner(messages: store.unavailableNotices) + .padding(.horizontal) + .padding(.bottom) + } + } + } else { + ScrollView { + LazyVStack(alignment: .leading, spacing: 0) { + if !store.errorMessages.isEmpty { + ErrorBanner(messages: store.errorMessages) + Divider() + } + if !store.unavailableNotices.isEmpty { + NoticeBanner(messages: store.unavailableNotices) + Divider() + } + ForEach(store.events) { event in + SecurityEventRow(event: event) + Divider() + } + } + } + } + } +} + +private struct ErrorBanner: View { + let messages: [String] + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + ForEach(messages, id: \.self) { message in + Label(message, systemImage: "exclamationmark.triangle") + .font(.caption) + .foregroundStyle(.orange) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(10) + .background(.orange.opacity(0.1)) + } +} + +private struct NoticeBanner: View { + let messages: [String] + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + ForEach(messages, id: \.self) { message in + Label(message, systemImage: "info.circle") + .font(.caption2) + .foregroundStyle(.secondary) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(10) + .background(.secondary.opacity(0.08)) + } +} + +private struct StatusView: View { + let systemImage: String + let tint: Color + let message: String + + var body: some View { + VStack(spacing: 8) { + Image(systemName: systemImage) + .font(.title2) + .foregroundStyle(tint) + Text(message) + .font(.callout) + .multilineTextAlignment(.center) + .foregroundStyle(.secondary) + } + .padding() + .frame(maxWidth: .infinity, maxHeight: .infinity) + } +} + +#Preview { + SecurityEventListView(store: SecurityEventStore()) +} |
