diff options
| -rw-r--r-- | Hutch/Views/Builds/BuildDetailView.swift | 82 | ||||
| -rw-r--r-- | Hutch/Views/Builds/BuildTaskLogView.swift | 55 |
2 files changed, 82 insertions, 55 deletions
diff --git a/Hutch/Views/Builds/BuildDetailView.swift b/Hutch/Views/Builds/BuildDetailView.swift index 0479f76..cd2a972 100644 --- a/Hutch/Views/Builds/BuildDetailView.swift +++ b/Hutch/Views/Builds/BuildDetailView.swift @@ -6,6 +6,7 @@ struct BuildDetailView: View { @Environment(AppState.self) private var appState @State private var viewModel: BuildDetailViewModel? @State private var rebuiltJobId: Int? + @State private var selectedTask: BuildTask? @State private var showEditResubmitSheet = false @State private var showCancelConfirmation = false @@ -41,6 +42,18 @@ struct BuildDetailView: View { BuildDetailView(jobId: rebuiltJobId) } } + .navigationDestination(isPresented: Binding( + get: { selectedTask != nil }, + set: { isPresented in + if !isPresented { + selectedTask = nil + } + } + )) { + if let selectedTask, let viewModel { + BuildTaskLogView(task: selectedTask, viewModel: viewModel) + } + } .sheet(isPresented: $showEditResubmitSheet) { if let viewModel, let job = viewModel.job { EditResubmitBuildSheet(viewModel: viewModel, job: job) { jobId in @@ -115,7 +128,20 @@ struct BuildDetailView: View { if !job.tasks.isEmpty { ForEach(job.tasks) { task in Section { - TaskLogSection(task: task, viewModel: viewModel) + Button { + selectedTask = task + } label: { + HStack { + Text(task.status.rawValue.capitalized) + .font(.subheadline) + .foregroundStyle(.secondary) + Spacer() + Image(systemName: "chevron.right") + .font(.caption) + .foregroundStyle(.tertiary) + } + } + .foregroundStyle(.primary) } header: { HStack(spacing: 6) { TaskStatusIcon(status: task.status) @@ -296,60 +322,6 @@ private struct EditResubmitBuildSheet: View { } } -// MARK: - Task Log Section - -private struct TaskLogSection: View { - let task: BuildTask - let viewModel: BuildDetailViewModel - - @State private var isExpanded: Bool - - init(task: BuildTask, viewModel: BuildDetailViewModel) { - self.task = task - self.viewModel = viewModel - self._isExpanded = State(initialValue: task.status == .failed) - } - - var body: some View { - DisclosureGroup(isExpanded: $isExpanded) { - if viewModel.loadingTaskLogs.contains(task.logCacheKey) { - HStack { - Spacer() - ProgressView("Loading log…") - Spacer() - } - } else if let logText = viewModel.taskLogs[task.logCacheKey] { - ScrollView(.horizontal, showsIndicators: false) { - Text(logText) - .font(.caption2.monospaced()) - .foregroundStyle(.primary) - .textSelection(.enabled) - .frame(maxWidth: .infinity, alignment: .leading) - } - } else if task.log == nil { - Text("No log available.") - .foregroundStyle(.secondary) - } - } label: { - HStack { - Text(task.status.rawValue) - .font(.caption) - .foregroundStyle(.secondary) - } - } - .task { - if isExpanded { - await viewModel.loadTaskLog(task: task) - } - } - .onChange(of: isExpanded) { _, expanded in - if expanded { - Task { await viewModel.loadTaskLog(task: task) } - } - } - } -} - // MARK: - Task Status Icon private struct TaskStatusIcon: View { diff --git a/Hutch/Views/Builds/BuildTaskLogView.swift b/Hutch/Views/Builds/BuildTaskLogView.swift new file mode 100644 index 0000000..9ed0bdd --- /dev/null +++ b/Hutch/Views/Builds/BuildTaskLogView.swift @@ -0,0 +1,55 @@ +import SwiftUI +import UIKit + +struct BuildTaskLogView: View { + let task: BuildTask + let viewModel: BuildDetailViewModel + + var body: some View { + Group { + if viewModel.loadingTaskLogs.contains(task.logCacheKey) { + SRHTLoadingStateView(message: "Loading log…") + } else if let logText = viewModel.taskLogs[task.logCacheKey] { + GeometryReader { geometry in + ScrollView([.vertical, .horizontal]) { + Text(logText) + .font(.caption2.monospaced()) + .textSelection(.enabled) + .padding() + .frame( + minWidth: geometry.size.width, + minHeight: geometry.size.height, + alignment: .topLeading + ) + } + } + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + ShareLink(item: logText) + } + ToolbarItem(placement: .topBarTrailing) { + Button { + UIPasteboard.general.string = logText + } label: { + Image(systemName: "doc.on.doc") + } + .accessibilityLabel("Copy log to clipboard") + } + } + } else if task.log == nil { + ContentUnavailableView( + "No Log", + systemImage: "doc.text", + description: Text("This task has no log output.") + ) + } else { + SRHTLoadingStateView(message: "Loading log…") + } + } + .navigationTitle(task.name) + .navigationBarTitleDisplayMode(.inline) + .task { + await viewModel.loadTaskLog(task: task) + } + } +} |
