gitbay/Views/Builds/BuildListView.swift
91 lines · 2981 bytes
1import SwiftUI
2
3struct BuildListView: View {
4
5 @State private var model: BuildListViewModel
6
7 init(client: GitbayClient, repo: String) {
8 _model = State(initialValue: BuildListViewModel(client: client, repoPath: repo))
9 }
10
11 var body: some View {
12 List {
13 if let error = model.actionError {
14 Section {
15 GBNotice(error, .gbWarn)
16 }
17 }
18 ForEach(model.state.value ?? []) { build in
19 NavigationLink(value: BuildRoute.detail(repo: model.repoPath, number: build.number)) {
20 BuildRow(build: build)
21 }
22 }
23 }
24 .overlay { LoadStateOverlay(state: model.state) }
25 .navigationTitle("Builds")
26 .navigationBarTitleDisplayMode(.inline)
27 .toolbar {
28 ToolbarItem(placement: .topBarTrailing) {
29 // A menu of jobs, like the web's picker — not a name to
30 // type. Nothing to trigger without a job file.
31 Menu {
32 ForEach(model.jobs) { job in
33 Button("\(job.name) — \(job.trigger)") {
34 Task { await model.trigger(job: job.name) }
35 }
36 }
37 } label: {
38 Image(systemName: "play.circle")
39 }
40 .disabled(model.working || model.jobs.isEmpty)
41 .accessibilityIdentifier("build-trigger-button")
42 }
43 }
44 .task {
45 await model.load()
46 await model.loadJobs()
47 }
48 .refreshable {
49 await model.load()
50 await model.loadJobs()
51 }
52 }
53}
54
55private struct BuildRow: View {
56 let build: Build
57
58 var body: some View {
59 HStack(spacing: 10) {
60 Image(systemName: build.statusIcon)
61 .foregroundStyle(build.statusColor)
62 VStack(alignment: .leading, spacing: 2) {
63 Text("#\(build.number) \(build.job)")
64 .font(.gbSans(.subheadline).weight(.medium))
65 HStack(spacing: 6) {
66 Text(build.ref)
67 Text(build.shortSHA)
68 .font(.gbMono(.caption))
69 }
70 .font(.gbSans(.caption))
71 .foregroundStyle(.secondary)
72 }
73 Spacer()
74 VStack(alignment: .trailing, spacing: 2) {
75 Text(build.status)
76 .font(.gbSans(.caption).weight(.medium))
77 .foregroundStyle(build.statusColor)
78 Text(build.createdAt, format: .relative(presentation: .named))
79 .font(.gbSans(.caption))
80 .foregroundStyle(.secondary)
81 }
82 }
83 .padding(.vertical, 2)
84 }
85
86}
87
88nonisolated enum BuildRoute: Hashable {
89 case list(repo: String)
90 case detail(repo: String, number: Int64)
91}