Skip to content

Commit

Permalink
Improve ConsoleTaskCell default design
Browse files Browse the repository at this point in the history
  • Loading branch information
kean committed Sep 10, 2024
1 parent 00503d8 commit bd8954e
Show file tree
Hide file tree
Showing 4 changed files with 222 additions and 28 deletions.
27 changes: 10 additions & 17 deletions Sources/PulseUI/Features/Console/Views/ConsoleMessageCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,23 +35,18 @@ struct ConsoleMessageCell: View {
HStack {
Text(title)
.lineLimit(1)
#if os(iOS) || os(visionOS)
.font(detailsFont.weight(.medium))
#else
.font(ConsoleConstants.fontTitle.weight(.medium))
#endif
.font(.footnote)
.foregroundColor(titleColor)
Spacer()
Components.makePinView(for: message)
HStack(spacing: 3) {
Text(ConsoleMessageCell.timeFormatter.string(from: message.createdAt))
.lineLimit(1)
.font(detailsFont)
.monospacedDigit()
.foregroundColor(.secondary)
if isDisclosureNeeded {
ListDisclosureIndicator()
}
ConsoleTimestampView(date: message.createdAt)
.overlay(alignment: .trailing) {
if isDisclosureNeeded {
ListDisclosureIndicator()
.offset(x: 11, y: 0)
}
}
}
}
}
Expand Down Expand Up @@ -91,11 +86,9 @@ struct ConsoleMessageCell: View {
struct ListDisclosureIndicator: View {
var body: some View {
Image(systemName: "chevron.right")
.foregroundColor(Color.separator)
.lineLimit(1)
.font(ConsoleConstants.fontTitle)
.foregroundColor(.secondary)
.padding(.trailing, -12)
.font(.caption2.weight(.bold))
.foregroundColor(.secondary.opacity(0.33))
}
}

Expand Down
203 changes: 202 additions & 1 deletion Sources/PulseUI/Features/Console/Views/ConsoleTaskCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,190 @@ import Pulse
import Combine
import CoreData

#if os(iOS)

@available(iOS 15, visionOS 1.0, *)
struct ConsoleTaskCell: View {
@ObservedObject var task: NetworkTaskEntity
var isDisclosureNeeded = false

@ScaledMetric(relativeTo: .body) private var fontMultiplier = 1.0
@ObservedObject private var settings: UserSettings = .shared
@Environment(\.store) private var store: LoggerStore

var body: some View {
VStack(alignment: .leading, spacing: 3) {
header
details
content.padding(.top, 3)
}
}

// MARK: – Header

private var header: some View {
HStack(spacing: 6) {
if task.isMocked {
MockBadgeView()
}
info
Spacer()
ConsoleTimestampView(date: task.createdAt)
}
.overlay(alignment: .leading) {
StatusIndicatorView(state: task.state(in: store))
.offset(x: -14)
}
.overlay(alignment: .trailing) {
if isDisclosureNeeded {
ListDisclosureIndicator()
.offset(x: 11)
}
}
}

private var info: some View {
var text: Text {
let status: Text = Text(ConsoleFormatter.status(for: task, store: store))
.font(.footnote.weight(.medium))
.foregroundColor(task.state == .failure ? .red : .primary)

guard settings.displayOptions.isShowingDetails else {
return status
}
let details = settings.displayOptions.detailsFields
.compactMap(makeInfoText)
.joined(separator: " · ")
guard !details.isEmpty else {
return status
}
return status + Text(" · \(details)").font(.footnote)
}
return text
.tracking(-0.1)
.lineLimit(1)
.foregroundStyle(.secondary)
}


private func makeInfoText(for detail: DisplayOptions.Field) -> String? {
switch detail {
case .method:
task.httpMethod
case .requestSize:
byteCount(for: task.requestBodySize)
case .responseSize:
byteCount(for: task.responseBodySize)
case .responseContentType:
task.responseContentType.map(NetworkLogger.ContentType.init)?.lastComponent.uppercased()
case .duration:
ConsoleFormatter.duration(for: task)
case .host:
task.host
case .statusCode:
task.statusCode != 0 ? task.statusCode.description : nil
case .taskType:
NetworkLogger.TaskType(rawValue: task.taskType)?.urlSessionTaskClassName
case .taskDescription:
task.taskDescription
}
}

// MARK: – Details

@ViewBuilder
private var details: some View {
if let host = task.host, !host.isEmpty {
Text(host)
.lineLimit(1)
.font(.footnote)
.foregroundStyle(.secondary)
}
}

// MARK: – Content

private var content: some View {
var method: Text? {
guard let method = task.httpMethod else {
return nil
}
return Text(method.appending(" "))
.font(contentFont.weight(.medium).smallCaps())
.tracking(-0.3)
}

var main: Text {
Text(task.getFormattedContent(options: settings.displayOptions) ?? "")
.font(contentFont)
}

var text: Text {
if let method {
method + main
} else {
main
}
}

return text
.lineLimit(settings.displayOptions.contentLineLimit)
}

// MARK: - Helpers

private var contentFont: Font {
let baseSize = CGFloat(settings.displayOptions.contentFontSize)
return Font.system(size: baseSize * fontMultiplier)
}

private var detailsFont: Font {
let baseSize = CGFloat(settings.displayOptions.detailsFontSize)
return Font.system(size: baseSize * fontMultiplier).monospacedDigit()
}

private func byteCount(for size: Int64) -> String {
guard size > 0 else { return "0 KB" }
return ByteCountFormatter.string(fromByteCount: size)
}
}

private struct StatusIndicatorView: View {
let state: NetworkTaskEntity.State?

var body: some View {
Image(systemName: "circle.fill")
.foregroundStyle(color)
.font(.system(size: 8))
.clipShape(RoundedRectangle(cornerRadius: 3))
}

private var color: Color {
guard let state else {
return .secondary
}
switch state {
case .pending: return .orange
case .success: return .green
case .failure: return .red
}
}
}

struct ConsoleTimestampView: View {
let date: Date

var body: some View {
Text(ConsoleMessageCell.timeFormatter.string(from: date))
.font(.caption)
.monospacedDigit()
.tracking(-0.5)
.foregroundStyle(.secondary)
}
}

#else

@available(iOS 15, visionOS 1.0, *)
struct ConsoleTaskCell: View {
@ObservedObject var task: NetworkTaskEntity
Expand Down Expand Up @@ -135,7 +319,7 @@ struct ConsoleTaskCell: View {
Text(task.httpMethod ?? "GET")
case .requestSize:
makeInfoText("arrow.up", byteCount(for: task.requestBodySize))
case .responseSize:
case .responseSize:
makeInfoText("arrow.down", byteCount(for: task.responseBodySize))
case .responseContentType:
task.responseContentType.map(NetworkLogger.ContentType.init).map {
Expand Down Expand Up @@ -211,6 +395,21 @@ private let titleSpacing: CGFloat = 20
private let titleSpacing: CGFloat? = nil
#endif

#endif

#if os(iOS)
@available(iOS 15, visionOS 1.0, *)
struct MockBadgeView: View {
var body: some View {
Text("MOCK")
.foregroundStyle(.background)
.font(.caption2.weight(.semibold))
.padding(EdgeInsets(top: 2, leading: 5, bottom: 1, trailing: 5))
.background(Color.secondary.opacity(0.66))
.clipShape(Capsule())
}
}
#else
@available(iOS 15, visionOS 1.0, *)
struct MockBadgeView: View {
var body: some View {
Expand All @@ -237,6 +436,7 @@ struct MockBadgeView: View {
#endif
}
}
#endif

#if DEBUG
@available(iOS 15, visionOS 1.0, *)
Expand All @@ -248,3 +448,4 @@ struct ConsoleTaskCell_Previews: PreviewProvider {
}
}
#endif

2 changes: 1 addition & 1 deletion Sources/PulseUI/Helpers/UserSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ public final class UserSettings: ObservableObject {
}

public init(
detailsFields: [Field] = [.method, .requestSize, .responseSize, .duration]
detailsFields: [Field] = [.responseSize, .duration]
) {
self.detailsFields = detailsFields
}
Expand Down
18 changes: 9 additions & 9 deletions Sources/PulseUI/Mocks/MockTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ private let mockReposBody = Bundle.main.url(forResource: "repos", withExtension:

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

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

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

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

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

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

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

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

// MARK: - Download (GET)

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

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

private let mockDownloadNukeRedirectResponse = HTTPURLResponse(url: "https://codeload.github.com/kean/Nuke/zip/tags/11.0.0", statusCode: 302, headers: [
private let mockDownloadNukeRedirectResponse = HTTPURLResponse(url: "https://codeload.github.com/kean/nuke/zip/tags/11.0.0", statusCode: 302, headers: [
"Server": "GitHub.com",
"Content-Type": "text/html; charset=utf-8",
"Location": "https://codeload.github.com/kean/Nuke/zip/tags/11.0.0",
Expand All @@ -465,7 +465,7 @@ private let mockDownloadNukeRedirectResponse = HTTPURLResponse(url: "https://cod
"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"
])

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

0 comments on commit bd8954e

Please sign in to comment.