import Foundation import Observation /// Whether the signed-in account is an instance admin, from `whoami`. A /// failed read counts as not: the account menu then leaves Admin out. enum AdminCheck { nonisolated private struct Who: Decodable, Sendable { let admin: Bool? } static func isAdmin(_ client: GitbayClient) async -> Bool { ((try? await client.read(["whoami"], as: Who.self))?.admin) ?? false } } /// The admin screen: `dashboard`'s server build and worker queues, the /// read the web's /admin page makes. @Observable @MainActor final class AdminOverviewViewModel { nonisolated struct Overview: Sendable, Hashable { let commit: String? let sections: [QueueSection] } private(set) var state: LoadState = .loading private let client: GitbayClient init(client: GitbayClient) { self.client = client } func load() async { do { let dashboard = try await client.read(["dashboard"], as: AdminDashboard.self) guard let queues = dashboard.queues else { state = .empty("Only instance admins can see the worker queues.") return } state = .loaded(Overview(commit: dashboard.server?.commit, sections: queues.sections())) } catch { state = .from(error) } } } /// `admin user list`, narrowed by the web's five filters, and the four /// writes /admin/users makes per row. @Observable @MainActor final class AdminUsersViewModel { let list: PagedListModel var filter = AdminUserFilter.all { didSet { guard filter != oldValue else { return } list.argv = ["admin", "user", "list"] + filter.flags() reloadTask?.cancel() reloadTask = Task { await list.reload() } } } private var reloadTask: Task? private(set) var actionError: String? /// A refused invite, kept apart from the list's banner: the invite /// sheet shows it. var inviteError: String? private(set) var working = false let client: GitbayClient init(client: GitbayClient) { self.client = client list = PagedListModel( client: client, argv: ["admin", "user", "list"], emptyMessage: "No account matches" ) } func load() async { reloadTask?.cancel() await list.reload() } /// The list's `.task`: reads only while nothing is shown yet. func loadIfNeeded() async { guard list.needsLoad else { return } await load() } /// A write from a sheet (create): its refusal stays on the sheet, and /// the list reloads after one that went through. func write(_ argv: [String], stdin: String?) async -> Bool { working = true actionError = nil defer { working = false } do { try await client.run(argv, stdin: stdin) } catch let error as GitbayError { actionError = error.messageForTypedInput return false } catch { actionError = GitbayError.transport(error).userFacingMessage return false } await load() return true } func clearError() { actionError = nil inviteError = nil } func perform(_ action: AdminAction, on username: String) async { working = true actionError = nil defer { working = false } do { try await client.run(action.argv(username)) } catch let error as GitbayError { actionError = error.userFacingMessage } catch { actionError = GitbayError.transport(error).userFacingMessage } reloadTask?.cancel() reloadTask = Task { await list.reload() } await reloadTask?.value } }