krz/domain-dig
an ios app for DNS & SSL analysis
clone: git clone https://gitbay.org/krz/domain-dig.git
v4.8.2: DomainDig/BatchResultsView.swift · raw
1import SwiftUI
2
3struct BatchResultsView: View {
4 @Environment(\.appDensity) private var appDensity
5 @Bindable var viewModel: DomainViewModel
6 let title: String
7
8 var body: some View {
9 VStack(alignment: .leading, spacing: appDensity.metrics.cardSpacing) {
10 HStack(alignment: .top) {
11 SectionTitleView(title: title)
12 Spacer()
13 if viewModel.batchLookupRunning {
14 VStack(alignment: .trailing, spacing: 4) {
15 ProgressView(value: Double(viewModel.batchCompletedCount), total: Double(max(viewModel.batchTotalCount, 1)))
16 .tint(.cyan)
17 .frame(width: 120)
18 Text(viewModel.batchProgressLabel)
19 .font(appDensity.font(.caption2))
20 .foregroundStyle(.secondary)
21 }
22 } else if !viewModel.batchResults.isEmpty {
23 Text("\(viewModel.batchResults.count) domains")
24 .font(appDensity.font(.caption2))
25 .foregroundStyle(.secondary)
26 }
27 }
28
29 if viewModel.batchResults.isEmpty {
30 EmptyStateCardView(
31 title: "No Batch Results Yet",
32 message: "Batch runs collect availability, IP, and change status for multiple domains in one pass.",
33 suggestion: "Switch to Bulk mode, paste a list of domains, then run a batch lookup.",
34 systemImage: "square.stack.3d.up"
35 )
36 } else {
37 CardView(allowsHorizontalScroll: false) {
38 ForEach(viewModel.batchResults) { result in
39 if let entry = viewModel.historyEntry(for: result) {
40 NavigationLink {
41 HistoryDetailView(viewModel: viewModel, entry: entry)
42 } label: {
43 BatchResultRowView(result: result)
44 }
45 .buttonStyle(.plain)
46 } else {
47 BatchResultRowView(result: result)
48 }
49 }
50 }
51 }
52 }
53 }
54}
55
56struct BatchResultRowView: View {
57 @Environment(\.appDensity) private var appDensity
58 let result: BatchLookupResult
59
60 var body: some View {
61 VStack(alignment: .leading, spacing: appDensity.metrics.rowSpacing + 1) {
62 HStack(alignment: .firstTextBaseline, spacing: 8) {
63 Text(result.domain)
64 .font(appDensity.font(.callout))
65 .foregroundStyle(.primary)
66 .lineLimit(1)
67 Spacer(minLength: 8)
68 Text(result.resultSource.label.lowercased())
69 .font(appDensity.font(.caption2))
70 .foregroundStyle(.secondary)
71 AppStatusBadgeView(model: quickStatusBadge)
72 }
73
74 HStack(spacing: 10) {
75 AppStatusBadgeView(model: availabilityBadgeModel)
76 if let riskScore = result.riskScore, let riskLevel = result.riskLevel {
77 Text("Risk \(riskScore) \(riskLevel.title)")
78 .lineLimit(1)
79 .minimumScaleFactor(0.85)
80 }
81 }
82 .font(appDensity.font(.caption2))
83 .foregroundStyle(.secondary)
84
85 HStack(spacing: 10) {
86 Text(result.primaryIP ?? "No IP")
87 .lineLimit(1)
88 .truncationMode(.middle)
89
90 Spacer(minLength: 8)
91
92 Text(result.timestamp.formatted(date: .abbreviated, time: .shortened))
93 .lineLimit(1)
94 }
95 .font(appDensity.font(.caption2))
96 .foregroundStyle(.secondary)
97
98 if let summaryMessage = result.summaryMessage {
99 Text(summaryMessage)
100 .font(appDensity.font(.caption2))
101 .foregroundStyle(.secondary)
102 }
103
104 if let changeClassification = result.changeClassification {
105 Text("Impact: \(changeClassification.title)")
106 .font(appDensity.font(.caption2))
107 .foregroundStyle(changeClassification == .critical ? .red : (changeClassification == .warning ? .yellow : .secondary))
108 }
109
110 if let errorMessage = result.errorMessage {
111 Text(errorMessage)
112 .font(appDensity.font(.caption2))
113 .foregroundStyle(result.status == .failed ? .red : .secondary)
114 }
115 }
116 .frame(maxWidth: .infinity, alignment: .leading)
117 .padding(.vertical, 4)
118 .frame(minHeight: appDensity.metrics.rowMinHeight + 12, alignment: .topLeading)
119 }
120
121 private var availabilityText: String {
122 switch result.availability {
123 case .available:
124 return "Available"
125 case .registered:
126 return "Registered"
127 case .unknown, .none:
128 return "Unknown"
129 }
130 }
131
132 private var availabilityBadgeModel: AppStatusBadgeModel {
133 guard result.status == .failed else {
134 return AppStatusFactory.availability(result.availability)
135 }
136
137 return .init(
138 title: availabilityText,
139 systemImage: "exclamationmark.circle",
140 foregroundColor: .secondary,
141 backgroundColor: Color(.systemGray5).opacity(0.55)
142 )
143 }
144
145 private var quickStatusBadge: AppStatusBadgeModel {
146 switch result.status {
147 case .pending:
148 return .init(title: "Pending", systemImage: "clock", foregroundColor: .secondary, backgroundColor: Color(.systemGray5).opacity(0.55))
149 case .running:
150 return .init(title: "Running", systemImage: "arrow.clockwise", foregroundColor: .cyan, backgroundColor: .cyan.opacity(0.16))
151 case .completed:
152 if result.changeClassification == .critical || result.certificateWarningLevel == .critical || result.riskLevel == .high {
153 return .init(title: "Critical", systemImage: "exclamationmark.octagon.fill", foregroundColor: .red, backgroundColor: .red.opacity(0.16))
154 }
155 if result.changeClassification == .warning || result.changeSeverity == .medium || result.certificateWarningLevel == .warning {
156 return .init(title: "Warning", systemImage: "exclamationmark.triangle.fill", foregroundColor: .yellow, backgroundColor: .yellow.opacity(0.16))
157 }
158 if result.quickStatus == "Changed" {
159 return .init(title: "Changed", systemImage: "arrow.triangle.2.circlepath", foregroundColor: .cyan, backgroundColor: .cyan.opacity(0.16))
160 }
161 return .init(title: "Stable", systemImage: "checkmark.circle.fill", foregroundColor: .green, backgroundColor: .green.opacity(0.16))
162 case .failed:
163 return .init(title: "Failed", systemImage: "xmark.circle.fill", foregroundColor: .red, backgroundColor: .red.opacity(0.16))
164 }
165 }
166}