aboutsummaryrefslogtreecommitdiff
path: root/Hutch/Views/Builds/BuildRowView.swift
blob: e2afd2707504b8f58da3ad3ce733f413060210a1 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import SwiftUI

struct BuildRowView: View {
    let job: JobSummary

    var body: some View {
        HStack(spacing: 12) {
            JobStatusIcon(status: job.status)
                .frame(width: 28)

            VStack(alignment: .leading, spacing: 4) {
                Text(job.displayLabel)
                    .font(.subheadline)
                    .lineLimit(1)

                HStack(spacing: 8) {
                    if let image = job.image {
                        Text(image)
                            .font(.caption)
                            .foregroundStyle(.secondary)
                    }

                    Spacer()

                    Text(job.created.relativeDescription)
                        .font(.caption)
                        .foregroundStyle(.tertiary)
                }

                if !job.tasks.isEmpty {
                    TaskProgressView(tasks: job.tasks)
                }
            }
        }
        .padding(.vertical, 2)
    }
}

// MARK: - Job Status Icon

struct JobStatusIcon: View {
    let status: JobStatus

    var body: some View {
        Image(systemName: iconName)
            .foregroundStyle(color)
            .symbolEffect(.pulse, isActive: status == .running)
    }

    private var iconName: String {
        switch status {
        case .success:              "checkmark.circle.fill"
        case .failed, .timeout:     "xmark.circle.fill"
        case .running:              "arrow.trianglehead.2.clockwise.rotate.90"
        case .queued:               "clock.fill"
        case .pending:              "circle.dashed"
        case .cancelled:            "minus.circle.fill"
        }
    }

    private var color: Color {
        switch status {
        case .success:              .green
        case .failed, .timeout:     .red
        case .running:              .yellow
        case .queued:               .orange
        case .pending, .cancelled:  .gray
        }
    }
}

// MARK: - Task Progress

struct TaskProgressView: View {
    let tasks: [JobTaskSummary]

    var body: some View {
        HStack(spacing: 6) {
            ProgressView(value: progress, total: 1.0)
                .tint(progressColor)
                .frame(maxWidth: 80)

            Text("\(completedCount)/\(tasks.count) tasks")
                .font(.caption2)
                .foregroundStyle(.secondary)
        }
    }

    private var completedCount: Int {
        tasks.filter { $0.status == .success }.count
    }

    private var progress: Double {
        tasks.isEmpty ? 0 : Double(completedCount) / Double(tasks.count)
    }

    private var progressColor: Color {
        if tasks.contains(where: { $0.status == .failed }) {
            return .red
        }
        if completedCount == tasks.count {
            return .green
        }
        return .blue
    }
}