gitbay/Admin/AdminViewModels.swift

main
gitbay-ios/gitbay/Admin/AdminViewModels.swift history · blame · raw

133 lines · 3892 bytes

10 symbols in this file
  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}