summaryrefslogtreecommitdiff
path: root/Hutch/Views/Builds/BuildRowView.swift
diff options
context:
space:
mode:
Diffstat (limited to 'Hutch/Views/Builds/BuildRowView.swift')
-rw-r--r--Hutch/Views/Builds/BuildRowView.swift106
1 files changed, 106 insertions, 0 deletions
diff --git a/Hutch/Views/Builds/BuildRowView.swift b/Hutch/Views/Builds/BuildRowView.swift
new file mode 100644
index 0000000..e2afd27
--- /dev/null
+++ b/Hutch/Views/Builds/BuildRowView.swift
@@ -0,0 +1,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
+ }
+}