gitbay/Views/Dashboard/DashboardView.swift

5411753023a5e2958f17b523903abce380941107
gitbay-ios/gitbay/Views/Dashboard/DashboardView.swift history · blame · raw

162 lines · 6238 bytes

  1import SwiftUI
  2
  3/// What needs me, in one read.
  4struct DashboardView: View {
  5
  6    @State private var model: DashboardViewModel
  7
  8    init(client: GitbayClient) {
  9        _model = State(initialValue: DashboardViewModel(client: client))
 10    }
 11
 12    var body: some View {
 13        List {
 14            if let data = model.state.value {
 15                if !data.pinned.isEmpty {
 16                    pinnedSection(data.pinned)
 17                }
 18                itemSection(
 19                    "Needs review", items: data.openMRs,
 20                    empty: "No open merge requests.",
 21                    marker: "!"
 22                ) { MRRoute.mr(repo: $0.repo, number: $0.number) }
 23                itemSection(
 24                    "Assigned to me", items: data.assignedIssues,
 25                    empty: "No assigned issues.",
 26                    marker: "#"
 27                ) { IssueRoute.issue(repo: $0.repo, number: $0.number) }
 28                buildsSection(data.builds)
 29            }
 30        }
 31        .overlay { LoadStateOverlay(state: model.state) }
 32        .navigationTitle("Dashboard")
 33        .toolbar {
 34            ToolbarItem(placement: .topBarTrailing) {
 35                NavigationLink(value: RepoRoute.account) {
 36                    Image(systemName: "key")
 37                }
 38                .accessibilityIdentifier("account-screen-link")
 39            }
 40            AccountMenu()
 41        }
 42        .task { await model.load() }
 43        .refreshable { await model.load() }
 44    }
 45
 46    private func pinnedSection(_ pinned: [RepoSummary]) -> some View {
 47        Section("Pinned") {
 48            ForEach(pinned) { repo in
 49                NavigationLink(value: RepoRoute.repo(repo.path)) {
 50                    VStack(alignment: .leading, spacing: 2) {
 51                        HStack(spacing: 6) {
 52                            Text(repo.path)
 53                                .font(.subheadline.weight(.medium))
 54                                .lineLimit(1)
 55                            if repo.isPrivate {
 56                                Image(systemName: "lock.fill")
 57                                    .font(.caption2)
 58                                    .foregroundStyle(.secondary)
 59                            }
 60                        }
 61                        if let description = repo.description, !description.isEmpty {
 62                            Text(description)
 63                                .font(.caption)
 64                                .foregroundStyle(.secondary)
 65                                .lineLimit(1)
 66                        }
 67                    }
 68                }
 69            }
 70        }
 71    }
 72
 73    private func itemSection(
 74        _ title: String,
 75        items: [DashboardItem],
 76        empty: String,
 77        marker: String,
 78        route: @escaping (DashboardItem) -> some Hashable
 79    ) -> some View {
 80        Section(title) {
 81            if items.isEmpty {
 82                Text(empty)
 83                    .font(.subheadline)
 84                    .foregroundStyle(.secondary)
 85            } else {
 86                ForEach(items) { item in
 87                    NavigationLink(value: route(item)) {
 88                        VStack(alignment: .leading, spacing: 2) {
 89                            Text(item.repo)
 90                                .font(.caption)
 91                                .foregroundStyle(.secondary)
 92                            HStack(alignment: .firstTextBaseline, spacing: 6) {
 93                                Text(marker + String(item.number))
 94                                    .font(.caption.monospaced())
 95                                    .foregroundStyle(.secondary)
 96                                Text(item.title)
 97                                    .font(.subheadline.weight(.medium))
 98                                    .lineLimit(2)
 99                            }
100                            HStack(spacing: 6) {
101                                Text("by \(item.author)")
102                                Text(item.updatedAt, format: .relative(presentation: .named))
103                                    .foregroundStyle(.tertiary)
104                            }
105                            .font(.caption)
106                            .foregroundStyle(.secondary)
107                        }
108                    }
109                }
110            }
111        }
112    }
113
114    private func buildsSection(_ builds: [DashboardBuild]) -> some View {
115        Section("Recent builds") {
116            if builds.isEmpty {
117                Text("No recent builds.")
118                    .font(.subheadline)
119                    .foregroundStyle(.secondary)
120            } else {
121                ForEach(builds) { build in
122                    NavigationLink(value: BuildRoute.log(repo: build.repo, number: build.number)) {
123                        HStack(spacing: 8) {
124                            Image(systemName: buildIcon(build.status))
125                                .foregroundStyle(buildColor(build.status))
126                            VStack(alignment: .leading, spacing: 2) {
127                                Text(build.repo)
128                                    .font(.caption)
129                                    .foregroundStyle(.secondary)
130                                Text("#\(build.number) \(build.job)")
131                                    .font(.subheadline)
132                            }
133                            Spacer()
134                            Text(build.createdAt, format: .relative(presentation: .named))
135                                .font(.caption)
136                                .foregroundStyle(.tertiary)
137                        }
138                    }
139                }
140            }
141        }
142    }
143
144    private func buildIcon(_ status: String) -> String {
145        switch status {
146        case "success": "checkmark.circle.fill"
147        case "failure", "error": "xmark.circle.fill"
148        case "running": "circle.dotted"
149        case "queued", "pending": "clock"
150        default: "questionmark.circle"
151        }
152    }
153
154    private func buildColor(_ status: String) -> Color {
155        switch status {
156        case "success": .green
157        case "failure", "error": .red
158        case "running", "queued", "pending": .orange
159        default: .secondary
160        }
161    }
162}