summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-03-22 20:40:56 -0500
committerChristian Cleberg <[email protected]>2026-03-22 20:40:56 -0500
commite03791d6c15ee0caac033d1b1b390c5d65a69d57 (patch)
tree406af72d2592e7ed3ced7a8302bf587b43a632ec
parent3ed2bb4da64d7cc9b67ef86a3892ab41b14fcc51 (diff)
downloadhutch-e03791d6c15ee0caac033d1b1b390c5d65a69d57.tar.gz
hutch-e03791d6c15ee0caac033d1b1b390c5d65a69d57.tar.bz2
hutch-e03791d6c15ee0caac033d1b1b390c5d65a69d57.zip
feat: replace inline task log expansion with a full-screen log view
-rw-r--r--Hutch/Views/Builds/BuildDetailView.swift82
-rw-r--r--Hutch/Views/Builds/BuildTaskLogView.swift55
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)
+ }
+ }
+}