OXYGENHUB
# cccf06468345 oxygenhub-android / app / src / main / java / io / oxygenhub / android / ui / flow / FlowViewModels.kt
4.9 KiB · last changed Approve, reject and expedite from the phone: a job waiting for a person shows Approve and Reject, a live run has Expedite with its reason (refs #5) 8 days ago source blame history raw
1
package io.oxygenhub.android.ui.flow
2
3
import androidx.lifecycle.ViewModel
4
import androidx.lifecycle.viewModelScope
5
import io.oxygenhub.android.api.FlowRun
6
import io.oxygenhub.android.api.HubApi
7
import io.oxygenhub.android.api.LogLine
8
import io.oxygenhub.android.api.userMessage
9
import io.oxygenhub.android.ui.common.Load
10
import io.oxygenhub.android.ui.common.load
11
import kotlinx.coroutines.CancellationException
12
import kotlinx.coroutines.Job
13
import kotlinx.coroutines.delay
14
import kotlinx.coroutines.flow.MutableStateFlow
15
import kotlinx.coroutines.flow.StateFlow
16
import kotlinx.coroutines.flow.asStateFlow
17
import kotlinx.coroutines.isActive
18
import kotlinx.coroutines.launch
19
20
/** A repository's runs, newest first (#5). */
21
class RunsViewModel(private val api: HubApi, val owner: String, val name: String) : ViewModel() {
22
    private val _state = MutableStateFlow<Load<List<FlowRun>>>(Load.Loading)
23
    val state: StateFlow<Load<List<FlowRun>>> = _state.asStateFlow()
24
    private var job: Job? = null
25
26
    init {
27
        refresh()
28
    }
29
30
    fun refresh() {
31
        job = load(_state, job) { api.runs(owner, name) }
32
    }
33
}
34
35
/**
36
 * One run and its jobs, fetched again every few seconds while it is queued
37
 * or running, so its jobs' states follow; rerun and cancel it.
38
 */
39
class RunViewModel(
40
    private val api: HubApi,
41
    val owner: String,
42
    val name: String,
43
    val number: Long,
44
    /** How often a live run is fetched again; 0 not at all (tests). */
45
    private val pollMs: Long = 3000,
46
) : ViewModel() {
47
    private val _state = MutableStateFlow<Load<FlowRun>>(Load.Loading)
48
    val state: StateFlow<Load<FlowRun>> = _state.asStateFlow()
49
50
    private val _error = MutableStateFlow<String?>(null)
51
    val error: StateFlow<String?> = _error.asStateFlow()
52
53
    private var job: Job? = null
54
55
    init {
56
        refresh()
57
        if (pollMs > 0) {
58
            viewModelScope.launch {
59
                while (isActive) {
60
                    delay(pollMs)
61
                    if ((_state.value as? Load.Ready)?.value?.live == true) poll()
62
                }
63
            }
64
        }
65
    }
66
67
    fun refresh() {
68
        job = load(_state, job) { api.run(owner, name, number) }
69
    }
70
71
    /** Fetch it again without a spinner. */
72
    suspend fun poll() {
73
        try {
74
            _state.value = Load.Ready(api.run(owner, name, number))
75
        } catch (e: CancellationException) {
76
            throw e
77
        } catch (_: Exception) {
78
            // The next tick tries again; the last state stays shown.
79
        }
80
    }
81
82
    fun rerun() = act { api.rerunRun(owner, name, number) }
83
84
    fun rerunJob(job: String) = act { api.rerunJob(owner, name, number, job) }
85
86
    fun cancel() = act { api.cancelRun(owner, name, number) }
87
88
    /** Approve, or reject, a job that waits for a person (#5). */
89
    fun decide(job: String, approve: Boolean) = act { api.decideJob(owner, name, number, job, approve) }
90
91
    /** Make the run urgent, saying why (#5). */
92
    fun expedite(reason: String) = act { api.expediteRun(owner, name, number, reason.trim()) }
93
94
    private fun act(step: suspend () -> Unit) {
95
        _error.value = null
96
        viewModelScope.launch {
97
            try {
98
                step()
99
                poll()
100
            } catch (e: CancellationException) {
101
                throw e
102
            } catch (e: Exception) {
103
                _error.value = e.userMessage()
104
            }
105
        }
106
    }
107
}
108
109
/**
110
 * A job's log, followed while the job runs: new lines are asked for after
111
 * the last one held, every couple of seconds, until the job has finished.
112
 */
113
class JobLogViewModel(
114
    private val api: HubApi,
115
    val owner: String,
116
    val name: String,
117
    val number: Long,
118
    val job: String,
119
    private val pollMs: Long = 2000,
120
) : ViewModel() {
121
    data class Log(val lines: List<LogLine>, val status: String, val live: Boolean)
122
123
    private val _state = MutableStateFlow<Load<Log>>(Load.Loading)
124
    val state: StateFlow<Load<Log>> = _state.asStateFlow()
125
126
    init {
127
        viewModelScope.launch {
128
            next()
129
            while (pollMs > 0 && isActive && (_state.value as? Load.Ready)?.value?.live != false) {
130
                delay(pollMs)
131
                next()
132
            }
133
        }
134
    }
135
136
    /** The lines after the last one held, and whether the job still runs. */
137
    suspend fun next() {
138
        val held = (_state.value as? Load.Ready)?.value
139
        try {
140
            val after = held?.lines?.lastOrNull()?.seq ?: 0
141
            val more = api.jobLog(owner, name, number, job, after)
142
            val j = api.run(owner, name, number).jobs.firstOrNull { it.name == job }
143
            _state.value = Load.Ready(Log((held?.lines.orEmpty()) + more, j?.status ?: held?.status.orEmpty(), j?.live ?: false))
144
        } catch (e: CancellationException) {
145
            throw e
146
        } catch (e: Exception) {
147
            if (held == null) _state.value = Load.failed(e)
148
        }
149
    }
150
151
    fun retry() {
152
        _state.value = Load.Loading
153
        viewModelScope.launch { next() }
154
    }
155
}