Skip to content

Commit 4bf636e

Browse files
committed
Improve ConsoleTaskCell default design
1 parent 00503d8 commit 4bf636e

File tree

4 files changed

+201
-23
lines changed

4 files changed

+201
-23
lines changed

Sources/PulseUI/Features/Console/Views/ConsoleMessageCell.swift

+9-12
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,13 @@ struct ConsoleMessageCell: View {
4444
Spacer()
4545
Components.makePinView(for: message)
4646
HStack(spacing: 3) {
47-
Text(ConsoleMessageCell.timeFormatter.string(from: message.createdAt))
48-
.lineLimit(1)
49-
.font(detailsFont)
50-
.monospacedDigit()
51-
.foregroundColor(.secondary)
52-
if isDisclosureNeeded {
53-
ListDisclosureIndicator()
54-
}
47+
ConsoleTimestampView(date: message.createdAt)
48+
.overlay(alignment: .trailing) {
49+
if isDisclosureNeeded {
50+
ListDisclosureIndicator()
51+
.offset(x: 11, y: 0)
52+
}
53+
}
5554
}
5655
}
5756
}
@@ -91,11 +90,9 @@ struct ConsoleMessageCell: View {
9190
struct ListDisclosureIndicator: View {
9291
var body: some View {
9392
Image(systemName: "chevron.right")
94-
.foregroundColor(Color.separator)
9593
.lineLimit(1)
96-
.font(ConsoleConstants.fontTitle)
97-
.foregroundColor(.secondary)
98-
.padding(.trailing, -12)
94+
.font(.caption2.weight(.bold))
95+
.foregroundColor(.secondary.opacity(0.33))
9996
}
10097
}
10198

Sources/PulseUI/Features/Console/Views/ConsoleTaskCell.swift

+182-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,184 @@ import Pulse
77
import Combine
88
import CoreData
99

10+
#if os(iOS)
11+
12+
@available(iOS 15, visionOS 1.0, *)
13+
struct ConsoleTaskCell: View {
14+
@ObservedObject var task: NetworkTaskEntity
15+
var isDisclosureNeeded = false
16+
17+
@ScaledMetric(relativeTo: .body) private var fontMultiplier = 1.0
18+
@ObservedObject private var settings: UserSettings = .shared
19+
@Environment(\.store) private var store: LoggerStore
20+
21+
var body: some View {
22+
VStack(alignment: .leading, spacing: 6) {
23+
header
24+
content
25+
footer
26+
}
27+
}
28+
29+
// MARK: – Header
30+
31+
// TODO: add missing "Mock" badge
32+
private var header: some View {
33+
HStack {
34+
StatusIndicatorView(state: task.state(in: store))
35+
info
36+
Spacer()
37+
ConsoleTimestampView(date: task.createdAt)
38+
.overlay(alignment: .trailing) {
39+
if isDisclosureNeeded {
40+
ListDisclosureIndicator()
41+
.offset(x: 11)
42+
}
43+
}
44+
}
45+
}
46+
47+
private var info: some View {
48+
var text: Text {
49+
let status = Text(ConsoleFormatter.status(for: task, store: store))
50+
.font(.footnote.weight(.medium))
51+
52+
guard settings.displayOptions.isShowingDetails else {
53+
return status
54+
}
55+
let details = settings.displayOptions.detailsFields
56+
.compactMap(makeInfoText)
57+
.joined(separator: " · ")
58+
guard !details.isEmpty else {
59+
return status
60+
}
61+
return status + Text(" · \(details)").font(.footnote)
62+
}
63+
return text
64+
.tracking(-0.3)
65+
.lineLimit(1)
66+
.foregroundStyle(.secondary)
67+
}
68+
69+
70+
private func makeInfoText(for detail: DisplayOptions.Field) -> String? {
71+
switch detail {
72+
case .method:
73+
task.httpMethod
74+
case .requestSize:
75+
byteCount(for: task.requestBodySize)
76+
case .responseSize:
77+
byteCount(for: task.responseBodySize)
78+
case .responseContentType:
79+
task.responseContentType.map(NetworkLogger.ContentType.init)?.lastComponent.uppercased()
80+
case .duration:
81+
ConsoleFormatter.duration(for: task)
82+
case .host:
83+
task.host
84+
case .statusCode:
85+
task.statusCode != 0 ? task.statusCode.description : nil
86+
case .taskType:
87+
NetworkLogger.TaskType(rawValue: task.taskType)?.urlSessionTaskClassName
88+
case .taskDescription:
89+
task.taskDescription
90+
}
91+
}
92+
93+
// MARK: – Content
94+
95+
private var content: some View {
96+
var method: Text? {
97+
guard let method = task.httpMethod else {
98+
return nil
99+
}
100+
return Text(method.appending(" "))
101+
.font(contentFont.weight(.medium).smallCaps())
102+
.tracking(-0.3)
103+
}
104+
105+
var main: Text {
106+
Text(task.getFormattedContent(options: settings.displayOptions) ?? "")
107+
.font(contentFont)
108+
}
109+
110+
var text: Text {
111+
if let method {
112+
method + main
113+
} else {
114+
main
115+
}
116+
}
117+
118+
return text
119+
.lineLimit(settings.displayOptions.contentLineLimit)
120+
}
121+
122+
// MARK: – Footer
123+
124+
@ViewBuilder
125+
private var footer: some View {
126+
if let host = task.host, !host.isEmpty {
127+
Text(host)
128+
.lineLimit(1)
129+
.font(.footnote)
130+
.foregroundStyle(.secondary)
131+
}
132+
}
133+
134+
// MARK: - Helpers
135+
136+
private var contentFont: Font {
137+
let baseSize = CGFloat(settings.displayOptions.contentFontSize)
138+
return Font.system(size: baseSize * fontMultiplier)
139+
}
140+
141+
private var detailsFont: Font {
142+
let baseSize = CGFloat(settings.displayOptions.detailsFontSize)
143+
return Font.system(size: baseSize * fontMultiplier).monospacedDigit()
144+
}
145+
146+
private func byteCount(for size: Int64) -> String {
147+
guard size > 0 else { return "0 KB" }
148+
return ByteCountFormatter.string(fromByteCount: size)
149+
}
150+
}
151+
152+
private struct StatusIndicatorView: View {
153+
let state: NetworkTaskEntity.State?
154+
155+
var body: some View {
156+
Image(systemName: "circle.fill")
157+
.foregroundStyle(color)
158+
.font(.system(size: 9))
159+
.clipShape(RoundedRectangle(cornerRadius: 3))
160+
}
161+
162+
private var color: Color {
163+
guard let state else {
164+
return .secondary
165+
}
166+
switch state {
167+
case .pending: return .orange
168+
case .success: return .green
169+
case .failure: return .red
170+
}
171+
}
172+
}
173+
174+
struct ConsoleTimestampView: View {
175+
let date: Date
176+
177+
var body: some View {
178+
Text(ConsoleMessageCell.timeFormatter.string(from: date))
179+
.font(.caption)
180+
.monospacedDigit()
181+
.tracking(-0.5)
182+
.foregroundStyle(.secondary)
183+
}
184+
}
185+
186+
#else
187+
10188
@available(iOS 15, visionOS 1.0, *)
11189
struct ConsoleTaskCell: View {
12190
@ObservedObject var task: NetworkTaskEntity
@@ -135,7 +313,7 @@ struct ConsoleTaskCell: View {
135313
Text(task.httpMethod ?? "GET")
136314
case .requestSize:
137315
makeInfoText("arrow.up", byteCount(for: task.requestBodySize))
138-
case .responseSize:
316+
case .responseSize:
139317
makeInfoText("arrow.down", byteCount(for: task.responseBodySize))
140318
case .responseContentType:
141319
task.responseContentType.map(NetworkLogger.ContentType.init).map {
@@ -211,6 +389,8 @@ private let titleSpacing: CGFloat = 20
211389
private let titleSpacing: CGFloat? = nil
212390
#endif
213391

392+
#endif
393+
214394
@available(iOS 15, visionOS 1.0, *)
215395
struct MockBadgeView: View {
216396
var body: some View {
@@ -248,3 +428,4 @@ struct ConsoleTaskCell_Previews: PreviewProvider {
248428
}
249429
}
250430
#endif
431+

Sources/PulseUI/Helpers/UserSettings.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ public final class UserSettings: ObservableObject {
150150
}
151151

152152
public init(
153-
detailsFields: [Field] = [.method, .requestSize, .responseSize, .duration]
153+
detailsFields: [Field] = [.responseSize, .duration]
154154
) {
155155
self.detailsFields = detailsFields
156156
}

Sources/PulseUI/Mocks/MockTask.swift

+9-9
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ private let mockReposBody = Bundle.main.url(forResource: "repos", withExtension:
302302

303303
// MARK: - /CreateAPI (GET, redirect)
304304

305-
private let mockCreateAPIOriginalRequest = URLRequest(url: "https://github.com/CreateAPI/Get")
305+
private let mockCreateAPIOriginalRequest = URLRequest(url: "https://github.com/createapi/get")
306306

307307
private let mockCreateAPICurrentRequest = mockCreateAPIOriginalRequest.adding(headers: [
308308
"User-Agent": "Pulse Demo/2.0",
@@ -311,22 +311,22 @@ private let mockCreateAPICurrentRequest = mockCreateAPIOriginalRequest.adding(he
311311
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
312312
])
313313

314-
private let mockCreateAPIRedirectRequest = URLRequest(url: "https://github.com/kean/Get").adding(headers: [
314+
private let mockCreateAPIRedirectRequest = URLRequest(url: "https://github.com/kean/get").adding(headers: [
315315
"User-Agent": "Pulse Demo/2.0",
316316
"Accept-Encoding": "gzip",
317317
"Accept-Language": "en-us",
318318
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
319319
])
320320

321-
private let mockCreateaAPIRedirectResponse = HTTPURLResponse(url: "https://github.com/CreateAPI/Get", statusCode: 301, headers: [
321+
private let mockCreateaAPIRedirectResponse = HTTPURLResponse(url: "https://github.com/createapi/get", statusCode: 301, headers: [
322322
"Content-Type": "text/html; charset=utf-8",
323323
"Location": "https://github.com/kean/Get",
324324
"Cache-Control": "no-cache",
325325
"Content-Length": "0",
326326
"Server": "GitHub.com"
327327
])
328328

329-
private let mockCreateaAPIResponse = HTTPURLResponse(url: "https://github.com/kean/Get", statusCode: 200, headers: [
329+
private let mockCreateaAPIResponse = HTTPURLResponse(url: "https://github.com/kean/get", statusCode: 200, headers: [
330330
"Content-Type": "text/html; charset=utf-8",
331331
"Content-Length": "90",
332332
"Cache-Control": "no-store"
@@ -349,7 +349,7 @@ private let mockCreateaAPIBody = """
349349
// MARK: - PATCH
350350

351351
private let mockPatchRepoOriginalRequest: URLRequest = {
352-
var request = URLRequest(url: "https://github.com/repos/kean/Nuke", method: "PATCH")
352+
var request = URLRequest(url: "https://github.com/repos/kean/nuke", method: "PATCH")
353353
request.httpBody = """
354354
name=ImageKit&description=Image%20Loading%Framework&private=false
355355
""".data(using: .utf8)
@@ -364,7 +364,7 @@ private let mockPatchRepoCurrentRequest = mockPatchRepoOriginalRequest.adding(he
364364
"Accept": "application/vnd.github+json"
365365
])
366366

367-
private let mockPatchRepoResponse = HTTPURLResponse(url: "https://github.com/repos/kean/Nuke", statusCode: 200, headers: [
367+
private let mockPatchRepoResponse = HTTPURLResponse(url: "https://github.com/repos/kean/nuke", statusCode: 200, headers: [
368368
"Content-Length": "165061",
369369
"Content-Type": "application/json; charset=utf-8",
370370
"Cache-Control": "no-store",
@@ -447,7 +447,7 @@ private let mockPatchRepoDecodingError: Error = {
447447

448448
// MARK: - Download (GET)
449449

450-
private let mockDownloadNukeOriginalRequest = URLRequest(url: "https://github.com/kean/Nuke/archive/tags/11.0.0.zip")
450+
private let mockDownloadNukeOriginalRequest = URLRequest(url: "https://github.com/kean/nuke/archive/tags/11.0.0.zip")
451451

452452
private let mockDownloadNukeCurrentRequest = mockDownloadNukeOriginalRequest.adding(headers: [
453453
"User-Agent": "Pulse Demo/2.0",
@@ -456,7 +456,7 @@ private let mockDownloadNukeCurrentRequest = mockDownloadNukeOriginalRequest.add
456456
"Accept": "*/*"
457457
])
458458

459-
private let mockDownloadNukeRedirectResponse = HTTPURLResponse(url: "https://codeload.github.com/kean/Nuke/zip/tags/11.0.0", statusCode: 302, headers: [
459+
private let mockDownloadNukeRedirectResponse = HTTPURLResponse(url: "https://codeload.github.com/kean/nuke/zip/tags/11.0.0", statusCode: 302, headers: [
460460
"Server": "GitHub.com",
461461
"Content-Type": "text/html; charset=utf-8",
462462
"Location": "https://codeload.github.com/kean/Nuke/zip/tags/11.0.0",
@@ -465,7 +465,7 @@ private let mockDownloadNukeRedirectResponse = HTTPURLResponse(url: "https://cod
465465
"Content-Security-Policy": "default-src 'none'; base-uri 'self'; block-all-mixed-content; child-src github.com/assets-cdn/worker/ gist.github.com/assets-cdn/worker/; connect-src 'self' uploads.github.com objects-origin.githubusercontent.com www.githubstatus.com collector.github.com raw.githubusercontent.com api.github.com github-cloud.s3.amazonaws.com github-production-repository-file-5c1aeb.s3.amazonaws.com github-production-upload-manifest-file-7fdce7.s3.amazonaws.com github-production-user-asset-6210df.s3.amazonaws.com cdn.optimizely.com logx.optimizely.com/v1/events *.actions.githubusercontent.com wss://*.actions.githubusercontent.com online.visualstudio.com/api/v1/locations github-production-repository-image-32fea6.s3.amazonaws.com github-production-release-asset-2e65be.s3.amazonaws.com"
466466
])
467467

468-
private let mockDownloadNukeResponse = HTTPURLResponse(url: "https://codeload.github.com/kean/Nuke/zip/tags/11.0.0", statusCode: 200, headers: [
468+
private let mockDownloadNukeResponse = HTTPURLResponse(url: "https://codeload.github.com/kean/nuke/zip/tags/11.0.0", statusCode: 200, headers: [
469469
"Content-Type": "application/zip",
470470
"Content-Disposition": "attachment; filename=Nuke-11.0.0.zip",
471471
"Etag": "W/\\\"4358c3c3d9bd5a22f6d86b47cbe567417fa1efc8df6beaa54c1730caf6ad86da\\\"",

0 commit comments

Comments
 (0)