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