blob: d1d60a4c5362ded7c97f1dad7962e386dcae0467 (
plain) (
blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
|
import SwiftUI
import UIKit
struct BuildTaskLogView: View {
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 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)
}
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.")
)
}
} 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)
}
}
}
} else {
SRHTLoadingStateView(message: "Loading log…")
}
} else {
SRHTLoadingStateView(message: "Loading log…")
}
}
.navigationTitle(taskName)
.navigationBarTitleDisplayMode(.inline)
// 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
}
}
|