diff options
| author | Christian Cleberg <[email protected]> | 2026-03-23 15:17:49 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-03-23 15:17:49 -0500 |
| commit | 7fd0b33e4c6d39a40947e83d6a1c55a4d26f9476 (patch) | |
| tree | d6b7d674da3217dfed4434f9352197c60cf8564a /Hutch/Views/Builds | |
| parent | cf87c44f674e020c32fea70903f02c317b57da3b (diff) | |
| download | hutch-7fd0b33e4c6d39a40947e83d6a1c55a4d26f9476.tar.gz hutch-7fd0b33e4c6d39a40947e83d6a1c55a4d26f9476.tar.bz2 hutch-7fd0b33e4c6d39a40947e83d6a1c55a4d26f9476.zip | |
Present build logs in a live-updating sheet
Diffstat (limited to 'Hutch/Views/Builds')
| -rw-r--r-- | Hutch/Views/Builds/BuildTaskLogView.swift | 128 |
1 files changed, 95 insertions, 33 deletions
diff --git a/Hutch/Views/Builds/BuildTaskLogView.swift b/Hutch/Views/Builds/BuildTaskLogView.swift index 9ed0bdd..85c9e21 100644 --- a/Hutch/Views/Builds/BuildTaskLogView.swift +++ b/Hutch/Views/Builds/BuildTaskLogView.swift @@ -2,54 +2,116 @@ import SwiftUI import UIKit struct BuildTaskLogView: View { - let task: BuildTask + let taskName: String let viewModel: BuildDetailViewModel + /// Always reflects the latest version of the task from the live job data. + private var task: BuildTask? { + viewModel.job?.tasks.first(where: { $0.name == taskName }) + } + 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 - ) + if let task { + if let logText = viewModel.displayedLogText(for: task) { + BuildLogTextView(text: logText) + .safeAreaInset(edge: .bottom) { + if viewModel.isShowingBuildLogFallback(for: task) { + Text("Showing the live build log until a task-specific log is available.") + .font(.footnote) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal) + .padding(.vertical, 8) + .background(.thinMaterial) + } } - } - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - ShareLink(item: logText) + .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 viewModel.loadingTaskLogs.contains(task.logCacheKey) { + SRHTLoadingStateView(message: "Loading log…") + } else if task.log == nil { + if task.status == .running || task.status == .pending { + if viewModel.isLoadingBuildLog { + SRHTLoadingStateView(message: "Loading build log…") + } else { + SRHTLoadingStateView(message: "Waiting for log…") + } + } else { + ContentUnavailableView( + "No Log", + systemImage: "doc.text", + description: Text("This task has no log output.") + ) } - ToolbarItem(placement: .topBarTrailing) { - Button { - UIPasteboard.general.string = logText - } label: { - Image(systemName: "doc.on.doc") + } else if viewModel.failedTaskLogs.contains(task.logCacheKey) { + ContentUnavailableView { + Label("Couldn't Load Log", systemImage: "exclamationmark.triangle") + } description: { + Text("The log is temporarily unavailable. Retry to fetch the latest output.") + } actions: { + Button("Retry") { + Task { + await viewModel.retryTaskLog(task: task) + } } - .accessibilityLabel("Copy log to clipboard") } + } else { + SRHTLoadingStateView(message: "Loading log…") } - } 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) + .navigationTitle(taskName) .navigationBarTitleDisplayMode(.inline) - .task { + // Re-runs whenever the log URL changes or a retry is requested. + .task(id: viewModel.taskLogTrigger(for: task)) { + guard let task else { return } await viewModel.loadTaskLog(task: task) } + .task(id: viewModel.job?.log?.fullURL) { + guard let task else { return } + guard task.status == .running || task.status == .pending else { return } + await viewModel.loadBuildLog() + } + } +} + +private struct BuildLogTextView: UIViewRepresentable { + let text: String + + func makeUIView(context: Context) -> UITextView { + let textView = UITextView() + textView.isEditable = false + textView.isSelectable = true + textView.isScrollEnabled = true + textView.alwaysBounceVertical = true + textView.alwaysBounceHorizontal = true + textView.showsHorizontalScrollIndicator = true + textView.showsVerticalScrollIndicator = true + textView.backgroundColor = .clear + textView.textContainerInset = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12) + textView.textContainer.lineFragmentPadding = 0 + textView.textContainer.widthTracksTextView = false + textView.font = UIFont.monospacedSystemFont(ofSize: UIFont.preferredFont(forTextStyle: .caption2).pointSize, weight: .regular) + textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + return textView + } + + func updateUIView(_ uiView: UITextView, context: Context) { + guard uiView.text != text else { return } + uiView.text = text } } |
