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}