summaryrefslogtreecommitdiff
path: root/Hutch/Views/Builds/BuildDetailViewModel.swift
diff options
context:
space:
mode:
Diffstat (limited to 'Hutch/Views/Builds/BuildDetailViewModel.swift')
-rw-r--r--Hutch/Views/Builds/BuildDetailViewModel.swift235
1 files changed, 235 insertions, 0 deletions
diff --git a/Hutch/Views/Builds/BuildDetailViewModel.swift b/Hutch/Views/Builds/BuildDetailViewModel.swift
new file mode 100644
index 0000000..c14e47b
--- /dev/null
+++ b/Hutch/Views/Builds/BuildDetailViewModel.swift
@@ -0,0 +1,235 @@
+import Foundation
+
+// MARK: - Response types
+
+private struct JobDetailResponse: Decodable, Sendable {
+ let job: JobDetail
+}
+
+private struct CancelResponse: Decodable, Sendable {
+ let cancel: CancelResult
+}
+
+private struct CancelResult: Decodable, Sendable {
+ let id: Int
+}
+
+private struct SubmitJobResponse: Decodable, Sendable {
+ let submit: SubmittedJob
+}
+
+private struct SubmittedJob: Decodable, Sendable {
+ let id: Int
+}
+
+// MARK: - View Model
+
+@Observable
+@MainActor
+final class BuildDetailViewModel {
+
+ let jobId: Int
+ private let client: SRHTClient
+
+ private(set) var job: JobDetail?
+ private(set) var isLoading = false
+ private(set) var taskLogs: [String: String] = [:]
+ private(set) var loadingTaskLogs: Set<String> = []
+ private(set) var isCancelling = false
+ private(set) var isRebuilding = false
+ private(set) var isSubmittingEditedBuild = false
+ var error: String?
+
+ init(jobId: Int, client: SRHTClient) {
+ self.jobId = jobId
+ self.client = client
+ }
+
+ // MARK: - Queries
+
+ private static let detailQuery = """
+ query job($id: Int!) {
+ job(id: $id) {
+ id
+ created
+ updated
+ status
+ note
+ tags
+ visibility
+ image
+ manifest
+ tasks { name status log { fullURL } }
+ log { fullURL }
+ owner { canonicalName }
+ }
+ }
+ """
+
+ private static let cancelMutation = """
+ mutation cancel($id: Int!) {
+ cancel(jobId: $id) {
+ id
+ }
+ }
+ """
+
+ private static let submitMutation = """
+ mutation submit($manifest: String!, $tags: [String!], $note: String, $visibility: Visibility) {
+ submit(manifest: $manifest, tags: $tags, note: $note, visibility: $visibility) {
+ id
+ }
+ }
+ """
+
+ private static let editableSubmitMutation = """
+ mutation submit($manifest: String!, $tags: [String!], $note: String, $secrets: Boolean, $execute: Boolean, $visibility: Visibility) {
+ submit(manifest: $manifest, tags: $tags, note: $note, secrets: $secrets, execute: $execute, visibility: $visibility) {
+ id
+ }
+ }
+ """
+
+ // MARK: - Public API
+
+ func loadJob() async {
+ guard !isLoading else { return }
+ isLoading = true
+ error = nil
+
+ do {
+ let result = try await client.execute(
+ service: .builds,
+ query: Self.detailQuery,
+ variables: ["id": jobId],
+ responseType: JobDetailResponse.self
+ )
+ job = result.job
+ } catch {
+ self.error = error.localizedDescription
+ }
+
+ isLoading = false
+ }
+
+ func loadTaskLog(task: BuildTask) async {
+ guard let log = task.log,
+ let logURL = URL(string: log.fullURL),
+ !loadingTaskLogs.contains(task.name),
+ taskLogs[task.name] == nil else { return }
+ loadingTaskLogs.insert(task.name)
+
+ do {
+ taskLogs[task.name] = try await client.fetchText(url: logURL)
+ } catch {
+ self.error = error.localizedDescription
+ }
+
+ loadingTaskLogs.remove(task.name)
+ }
+
+ func cancelJob() async {
+ guard let job, job.status.isCancellable, !isCancelling else { return }
+ isCancelling = true
+ error = nil
+
+ do {
+ _ = try await client.execute(
+ service: .builds,
+ query: Self.cancelMutation,
+ variables: ["id": jobId],
+ responseType: CancelResponse.self
+ )
+ // Reload job to get updated status.
+ await loadJob()
+ } catch {
+ self.error = error.localizedDescription
+ }
+
+ isCancelling = false
+ }
+
+ func rebuildJob() async -> Int? {
+ guard let job, let manifest = job.manifest, !manifest.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty, !isRebuilding else {
+ return nil
+ }
+
+ isRebuilding = true
+ error = nil
+ defer { isRebuilding = false }
+
+ var variables: [String: any Sendable] = [
+ "manifest": manifest.trimmingCharacters(in: .whitespacesAndNewlines)
+ ]
+ if !job.tags.isEmpty {
+ variables["tags"] = job.tags
+ }
+ if let note = job.note, !note.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
+ variables["note"] = note.trimmingCharacters(in: .whitespacesAndNewlines)
+ }
+ if let visibility = job.visibility {
+ variables["visibility"] = visibility.rawValue
+ }
+
+ do {
+ let result = try await client.execute(
+ service: .builds,
+ query: Self.submitMutation,
+ variables: variables,
+ responseType: SubmitJobResponse.self
+ )
+ return result.submit.id
+ } catch {
+ self.error = error.localizedDescription
+ return nil
+ }
+ }
+
+ func submitBuild(
+ manifest: String,
+ tags: [String],
+ note: String,
+ secrets: Bool,
+ execute: Bool,
+ visibility: Visibility
+ ) async -> Int? {
+ guard !isSubmittingEditedBuild else { return nil }
+
+ let trimmedManifest = manifest.trimmingCharacters(in: .whitespacesAndNewlines)
+ guard !trimmedManifest.isEmpty else {
+ error = "Paste a build manifest."
+ return nil
+ }
+
+ isSubmittingEditedBuild = true
+ error = nil
+ defer { isSubmittingEditedBuild = false }
+
+ var variables: [String: any Sendable] = [
+ "manifest": trimmedManifest,
+ "secrets": secrets,
+ "execute": execute,
+ "visibility": visibility.rawValue
+ ]
+ if !tags.isEmpty {
+ variables["tags"] = tags
+ }
+ let trimmedNote = note.trimmingCharacters(in: .whitespacesAndNewlines)
+ if !trimmedNote.isEmpty {
+ variables["note"] = trimmedNote
+ }
+
+ do {
+ let result = try await client.execute(
+ service: .builds,
+ query: Self.editableSubmitMutation,
+ variables: variables,
+ responseType: SubmitJobResponse.self
+ )
+ return result.submit.id
+ } catch {
+ self.error = "Couldn’t submit the build. \(error.localizedDescription)"
+ return nil
+ }
+ }
+}