gitbay/Views/Builds/BuildListView.swift

5411753023a5e2958f17b523903abce380941107
gitbay-ios/gitbay/Views/Builds/BuildListView.swift history · blame · raw

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}