gitbay/Admin/AdminViewModels.swift
133 lines · 3892 bytes
1import Foundation
2import Observation
3
4/// Whether the signed-in account is an instance admin, from `whoami`. A
5/// failed read counts as not: the account menu then leaves Admin out.
6enum AdminCheck {
7
8 nonisolated private struct Who: Decodable, Sendable {
9 let admin: Bool?
10 }
11
12 static func isAdmin(_ client: GitbayClient) async -> Bool {
13 ((try? await client.read(["whoami"], as: Who.self))?.admin) ?? false
14 }
15}
16
17/// The admin screen: `dashboard`'s server build and worker queues, the
18/// read the web's /admin page makes.
19@Observable
20@MainActor
21final class AdminOverviewViewModel {
22
23 nonisolated struct Overview: Sendable, Hashable {
24 let commit: String?
25 let sections: [QueueSection]
26 }
27
28 private(set) var state: LoadState<Overview> = .loading
29
30 private let client: GitbayClient
31
32 init(client: GitbayClient) {
33 self.client = client
34 }
35
36 func load() async {
37 do {
38 let dashboard = try await client.read(["dashboard"], as: AdminDashboard.self)
39 guard let queues = dashboard.queues else {
40 state = .empty("Only instance admins can see the worker queues.")
41 return
42 }
43 state = .loaded(Overview(commit: dashboard.server?.commit, sections: queues.sections()))
44 } catch {
45 state = .from(error)
46 }
47 }
48}
49
50/// `admin user list`, narrowed by the web's five filters, and the four
51/// writes /admin/users makes per row.
52@Observable
53@MainActor
54final class AdminUsersViewModel {
55
56 let list: PagedListModel<AdminUser>
57 var filter = AdminUserFilter.all {
58 didSet {
59 guard filter != oldValue else { return }
60 list.argv = ["admin", "user", "list"] + filter.flags()
61 reloadTask?.cancel()
62 reloadTask = Task { await list.reload() }
63 }
64 }
65 private var reloadTask: Task<Void, Never>?
66 private(set) var actionError: String?
67 /// A refused invite, kept apart from the list's banner: the invite
68 /// sheet shows it.
69 var inviteError: String?
70 private(set) var working = false
71
72 let client: GitbayClient
73
74 init(client: GitbayClient) {
75 self.client = client
76 list = PagedListModel(
77 client: client,
78 argv: ["admin", "user", "list"],
79 emptyMessage: "No account matches"
80 )
81 }
82
83 func load() async {
84 reloadTask?.cancel()
85 await list.reload()
86 }
87
88 /// The list's `.task`: reads only while nothing is shown yet.
89 func loadIfNeeded() async {
90 guard list.needsLoad else { return }
91 await load()
92 }
93
94 /// A write from a sheet (create): its refusal stays on the sheet, and
95 /// the list reloads after one that went through.
96 func write(_ argv: [String], stdin: String?) async -> Bool {
97 working = true
98 actionError = nil
99 defer { working = false }
100 do {
101 try await client.run(argv, stdin: stdin)
102 } catch let error as GitbayError {
103 actionError = error.messageForTypedInput
104 return false
105 } catch {
106 actionError = GitbayError.transport(error).userFacingMessage
107 return false
108 }
109 await load()
110 return true
111 }
112
113 func clearError() {
114 actionError = nil
115 inviteError = nil
116 }
117
118 func perform(_ action: AdminAction, on username: String) async {
119 working = true
120 actionError = nil
121 defer { working = false }
122 do {
123 try await client.run(action.argv(username))
124 } catch let error as GitbayError {
125 actionError = error.userFacingMessage
126 } catch {
127 actionError = GitbayError.transport(error).userFacingMessage
128 }
129 reloadTask?.cancel()
130 reloadTask = Task { await list.reload() }
131 await reloadTask?.value
132 }
133}