gitbay/Views/Builds/BuildListView.swift
111 lines · 3638 bytes
1import SwiftUI
2
3struct BuildListView: View {
4
5 @State private var model: BuildListViewModel
6 @State private var triggering = false
7 @State private var jobName = ""
8
9 init(client: GitbayClient, repo: String) {
10 _model = State(initialValue: BuildListViewModel(client: client, repoPath: repo))
11 }
12
13 var body: some View {
14 List {
15 if let error = model.actionError {
16 Section {
17 GBNotice(error, .gbWarn)
18 }
19 }
20 ForEach(model.state.value ?? []) { build in
21 NavigationLink(value: BuildRoute.log(repo: model.repoPath, number: build.number)) {
22 BuildRow(build: build)
23 }
24 }
25 }
26 .overlay { LoadStateOverlay(state: model.state) }
27 .navigationTitle("Builds")
28 .navigationBarTitleDisplayMode(.inline)
29 .toolbar {
30 ToolbarItem(placement: .topBarTrailing) {
31 Button {
32 jobName = model.latestJob ?? ""
33 triggering = true
34 } label: {
35 Image(systemName: "play.circle")
36 }
37 .disabled(model.working)
38 .accessibilityIdentifier("build-trigger-button")
39 }
40 }
41 .alert("Trigger a build", isPresented: $triggering) {
42 TextField("Job name", text: $jobName)
43 .autocorrectionDisabled()
44 .textInputAutocapitalization(.never)
45 Button("Trigger") {
46 Task { await model.trigger(job: jobName.trimmingCharacters(in: .whitespaces)) }
47 }
48 Button("Cancel", role: .cancel) {}
49 } message: {
50 Text("Runs the named job from .gitbay/ at the default branch head.")
51 }
52 .task { await model.load() }
53 .refreshable { await model.load() }
54 }
55}
56
57private struct BuildRow: View {
58 let build: Build
59
60 var body: some View {
61 HStack(spacing: 10) {
62 Image(systemName: icon)
63 .foregroundStyle(color)
64 VStack(alignment: .leading, spacing: 2) {
65 Text("#\(build.number) \(build.job)")
66 .font(.gbSans(.subheadline).weight(.medium))
67 HStack(spacing: 6) {
68 Text(build.ref)
69 Text(build.shortSHA)
70 .font(.gbMono(.caption))
71 }
72 .font(.gbSans(.caption))
73 .foregroundStyle(.secondary)
74 }
75 Spacer()
76 VStack(alignment: .trailing, spacing: 2) {
77 Text(build.status)
78 .font(.gbSans(.caption).weight(.medium))
79 .foregroundStyle(color)
80 Text(build.createdAt, format: .relative(presentation: .named))
81 .font(.gbSans(.caption))
82 .foregroundStyle(.secondary)
83 }
84 }
85 .padding(.vertical, 2)
86 }
87
88 private var icon: String {
89 switch build.status {
90 case "success": "checkmark.circle.fill"
91 case "failure", "error": "xmark.circle.fill"
92 case "running": "circle.dotted"
93 case "queued", "pending": "clock"
94 default: "questionmark.circle"
95 }
96 }
97
98 private var color: Color {
99 switch build.status {
100 case "success": .gbOK
101 case "failure", "error": .gbBad
102 case "running", "queued", "pending": .gbWarn
103 default: .secondary
104 }
105 }
106}
107
108nonisolated enum BuildRoute: Hashable {
109 case list(repo: String)
110 case log(repo: String, number: Int64)
111}