Commit bc351b2fcd
Unsigned
Layout: unified · split
Hutch/Views/Builds/BuildDetailView.swift +27 −55
| @@ -6,6 +6,7 @@ struct BuildDetailView: View { | ||
| 6 | 6 | @Environment(AppState.self) private var appState |
| 7 | 7 | @State private var viewModel: BuildDetailViewModel? |
| 8 | 8 | @State private var rebuiltJobId: Int? |
| 9 | @State private var selectedTask: BuildTask? | |
| 9 | 10 | @State private var showEditResubmitSheet = false |
| 10 | 11 | @State private var showCancelConfirmation = false |
| 11 | 12 | |
| @@ -41,6 +42,18 @@ struct BuildDetailView: View { | ||
| 41 | 42 | BuildDetailView(jobId: rebuiltJobId) |
| 42 | 43 | } |
| 43 | 44 | } |
| 45 | .navigationDestination(isPresented: Binding( | |
| 46 | get: { selectedTask != nil }, | |
| 47 | set: { isPresented in | |
| 48 | if !isPresented { | |
| 49 | selectedTask = nil | |
| 50 | } | |
| 51 | } | |
| 52 | )) { | |
| 53 | if let selectedTask, let viewModel { | |
| 54 | BuildTaskLogView(task: selectedTask, viewModel: viewModel) | |
| 55 | } | |
| 56 | } | |
| 44 | 57 | .sheet(isPresented: $showEditResubmitSheet) { |
| 45 | 58 | if let viewModel, let job = viewModel.job { |
| 46 | 59 | EditResubmitBuildSheet(viewModel: viewModel, job: job) { jobId in |
| @@ -115,7 +128,20 @@ struct BuildDetailView: View { | ||
| 115 | 128 | if !job.tasks.isEmpty { |
| 116 | 129 | ForEach(job.tasks) { task in |
| 117 | 130 | Section { |
| 118 | TaskLogSection(task: task, viewModel: viewModel) | |
| 131 | Button { | |
| 132 | selectedTask = task | |
| 133 | } label: { | |
| 134 | HStack { | |
| 135 | Text(task.status.rawValue.capitalized) | |
| 136 | .font(.subheadline) | |
| 137 | .foregroundStyle(.secondary) | |
| 138 | Spacer() | |
| 139 | Image(systemName: "chevron.right") | |
| 140 | .font(.caption) | |
| 141 | .foregroundStyle(.tertiary) | |
| 142 | } | |
| 143 | } | |
| 144 | .foregroundStyle(.primary) | |
| 119 | 145 | } header: { |
| 120 | 146 | HStack(spacing: 6) { |
| 121 | 147 | TaskStatusIcon(status: task.status) |
| @@ -296,60 +322,6 @@ private struct EditResubmitBuildSheet: View { | ||
| 296 | 322 | } |
| 297 | 323 | } |
| 298 | 324 | |
| 299 | // MARK: - Task Log Section | |
| 300 | ||
| 301 | private struct TaskLogSection: View { | |
| 302 | let task: BuildTask | |
| 303 | let viewModel: BuildDetailViewModel | |
| 304 | ||
| 305 | @State private var isExpanded: Bool | |
| 306 | ||
| 307 | init(task: BuildTask, viewModel: BuildDetailViewModel) { | |
| 308 | self.task = task | |
| 309 | self.viewModel = viewModel | |
| 310 | self._isExpanded = State(initialValue: task.status == .failed) | |
| 311 | } | |
| 312 | ||
| 313 | var body: some View { | |
| 314 | DisclosureGroup(isExpanded: $isExpanded) { | |
| 315 | if viewModel.loadingTaskLogs.contains(task.logCacheKey) { | |
| 316 | HStack { | |
| 317 | Spacer() | |
| 318 | ProgressView("Loading log…") | |
| 319 | Spacer() | |
| 320 | } | |
| 321 | } else if let logText = viewModel.taskLogs[task.logCacheKey] { | |
| 322 | ScrollView(.horizontal, showsIndicators: false) { | |
| 323 | Text(logText) | |
| 324 | .font(.caption2.monospaced()) | |
| 325 | .foregroundStyle(.primary) | |
| 326 | .textSelection(.enabled) | |
| 327 | .frame(maxWidth: .infinity, alignment: .leading) | |
| 328 | } | |
| 329 | } else if task.log == nil { | |
| 330 | Text("No log available.") | |
| 331 | .foregroundStyle(.secondary) | |
| 332 | } | |
| 333 | } label: { | |
| 334 | HStack { | |
| 335 | Text(task.status.rawValue) | |
| 336 | .font(.caption) | |
| 337 | .foregroundStyle(.secondary) | |
| 338 | } | |
| 339 | } | |
| 340 | .task { | |
| 341 | if isExpanded { | |
| 342 | await viewModel.loadTaskLog(task: task) | |
| 343 | } | |
| 344 | } | |
| 345 | .onChange(of: isExpanded) { _, expanded in | |
| 346 | if expanded { | |
| 347 | Task { await viewModel.loadTaskLog(task: task) } | |
| 348 | } | |
| 349 | } | |
| 350 | } | |
| 351 | } | |
| 352 | ||
| 353 | 325 | // MARK: - Task Status Icon |
| 354 | 326 | |
| 355 | 327 | private struct TaskStatusIcon: View { |
Hutch/Views/Builds/BuildTaskLogView.swift added +55
| @@ -0,0 +1,55 @@ | ||
| 1 | import SwiftUI | |
| 2 | import UIKit | |
| 3 | ||
| 4 | struct BuildTaskLogView: View { | |
| 5 | let task: BuildTask | |
| 6 | let viewModel: BuildDetailViewModel | |
| 7 | ||
| 8 | var body: some View { | |
| 9 | Group { | |
| 10 | if viewModel.loadingTaskLogs.contains(task.logCacheKey) { | |
| 11 | SRHTLoadingStateView(message: "Loading log…") | |
| 12 | } else if let logText = viewModel.taskLogs[task.logCacheKey] { | |
| 13 | GeometryReader { geometry in | |
| 14 | ScrollView([.vertical, .horizontal]) { | |
| 15 | Text(logText) | |
| 16 | .font(.caption2.monospaced()) | |
| 17 | .textSelection(.enabled) | |
| 18 | .padding() | |
| 19 | .frame( | |
| 20 | minWidth: geometry.size.width, | |
| 21 | minHeight: geometry.size.height, | |
| 22 | alignment: .topLeading | |
| 23 | ) | |
| 24 | } | |
| 25 | } | |
| 26 | .toolbar { | |
| 27 | ToolbarItem(placement: .topBarTrailing) { | |
| 28 | ShareLink(item: logText) | |
| 29 | } | |
| 30 | ToolbarItem(placement: .topBarTrailing) { | |
| 31 | Button { | |
| 32 | UIPasteboard.general.string = logText | |
| 33 | } label: { | |
| 34 | Image(systemName: "doc.on.doc") | |
| 35 | } | |
| 36 | .accessibilityLabel("Copy log to clipboard") | |
| 37 | } | |
| 38 | } | |
| 39 | } else if task.log == nil { | |
| 40 | ContentUnavailableView( | |
| 41 | "No Log", | |
| 42 | systemImage: "doc.text", | |
| 43 | description: Text("This task has no log output.") | |
| 44 | ) | |
| 45 | } else { | |
| 46 | SRHTLoadingStateView(message: "Loading log…") | |
| 47 | } | |
| 48 | } | |
| 49 | .navigationTitle(task.name) | |
| 50 | .navigationBarTitleDisplayMode(.inline) | |
| 51 | .task { | |
| 52 | await viewModel.loadTaskLog(task: task) | |
| 53 | } | |
| 54 | } | |
| 55 | } | |