1
1
import LinearProgress from "@material-ui/core/LinearProgress"
2
2
import makeStyles from "@material-ui/core/styles/makeStyles"
3
- import { TransitionStats , Template , Workspace , WorkspaceTransition } from "api/typesGenerated"
3
+ import {
4
+ TransitionStats ,
5
+ Template ,
6
+ Workspace ,
7
+ WorkspaceTransition ,
8
+ WorkspaceStatus ,
9
+ } from "api/typesGenerated"
4
10
import dayjs , { Dayjs } from "dayjs"
5
11
import { FC , useEffect , useState } from "react"
6
12
import { MONOSPACE_FONT_FAMILY } from "theme/constants"
@@ -9,43 +15,67 @@ import duration from "dayjs/plugin/duration"
9
15
10
16
dayjs . extend ( duration )
11
17
18
+ // ActiveTransition gets the build estimate for the workspace,
19
+ // if it is in a transition state.
20
+ export const ActiveTransition = (
21
+ template : Template ,
22
+ workspace : Workspace ,
23
+ ) : TransitionStats | undefined => {
24
+ const status = workspace . latest_build . status
25
+
26
+ switch ( status ) {
27
+ case "starting" :
28
+ return template . build_time_stats . start
29
+ case "stopping" :
30
+ return template . build_time_stats . stop
31
+ case "deleting" :
32
+ return template . build_time_stats . delete
33
+ default :
34
+ return undefined
35
+ }
36
+ }
37
+
12
38
const estimateFinish = (
13
39
startedAt : Dayjs ,
14
- buildEstimate : number ,
15
- ) : [ number , string ] => {
16
- const realPercentage = dayjs ( ) . diff ( startedAt ) / buildEstimate
40
+ median : number ,
41
+ stddev : number ,
42
+ ) : [ number | undefined , string ] => {
43
+ const sinceStart = dayjs ( ) . diff ( startedAt )
44
+ const secondsLeft = ( est : number ) =>
45
+ Math . max (
46
+ Math . ceil ( dayjs . duration ( ( 1 - sinceStart / est ) * est ) . asSeconds ( ) ) ,
47
+ 0 ,
48
+ )
17
49
18
- const maxPercentage = 1
19
- if ( realPercentage > maxPercentage ) {
20
- return [ maxPercentage * 100 , "Any moment now..." ]
21
- }
50
+ const lowGuess = secondsLeft ( median )
51
+ const highGuess = secondsLeft ( median + stddev )
52
+
53
+ // If variation is too high (and greater than second), don't show
54
+ // progress bar and give range.
55
+ const highVariation = stddev / median > 0.1 && highGuess - lowGuess > 1
22
56
23
- return [
24
- realPercentage * 100 ,
25
- `~${ Math . ceil (
26
- dayjs . duration ( ( 1 - realPercentage ) * buildEstimate ) . asSeconds ( ) ,
27
- ) } seconds remaining...`,
57
+ const anyMomentNow : [ number | undefined , string ] = [
58
+ undefined ,
59
+ "Any moment now..." ,
28
60
]
61
+
62
+ if ( highVariation ) {
63
+ if ( highGuess <= 0 ) {
64
+ return anyMomentNow
65
+ }
66
+ return [ undefined , `${ lowGuess } to ${ highGuess } seconds remaining...` ]
67
+ } else {
68
+ const realPercentage = sinceStart / median
69
+ if ( realPercentage > 1 ) {
70
+ return anyMomentNow
71
+ }
72
+ return [ realPercentage * 100 , `${ highGuess } seconds remaining...` ]
73
+ }
29
74
}
30
75
31
76
export interface WorkspaceBuildProgressProps {
32
77
workspace : Workspace
33
- transitionStats ?: TransitionStats
34
- }
35
-
36
- // EstimateTransitionTime gets the build estimate for the workspace,
37
- // if it is in a transition state.
38
- export const EstimateTransitionTime = (
39
- template : Template ,
40
- workspace : Workspace ,
41
- ) : [ TransitionStats | undefined , boolean ] => {
42
- const transition = workspace . latest_build . status
43
-
44
- if ( ! [ "starting" , "stopping" , "deleting" ] . includes ( transition ) ) {
45
- return [ undefined , false ]
46
- }
47
-
48
- return [ template . build_time_stats [ transition as WorkspaceTransition ] , true ]
78
+ transitionStats : TransitionStats
49
79
}
50
80
51
81
export const WorkspaceBuildProgress : FC < WorkspaceBuildProgressProps > = ( {
@@ -55,18 +85,32 @@ export const WorkspaceBuildProgress: FC<WorkspaceBuildProgressProps> = ({
55
85
const styles = useStyles ( )
56
86
const job = workspace . latest_build . job
57
87
const [ progressValue , setProgressValue ] = useState < number | undefined > ( 0 )
88
+ const [ progressText , setProgressText ] = useState < string | undefined > (
89
+ "Finding ETA..." ,
90
+ )
58
91
59
92
// By default workspace is updated every second, which can cause visual stutter
60
93
// when the build estimate is a few seconds. The timer ensures no observable
61
94
// stutter in all cases.
62
95
useEffect ( ( ) => {
63
96
const updateProgress = ( ) => {
64
- if ( job . status !== "running" || transitionStats === undefined ) {
97
+ if (
98
+ job . status !== "running" ||
99
+ transitionStats . Median === undefined ||
100
+ transitionStats . Stddev === undefined
101
+ ) {
65
102
setProgressValue ( undefined )
103
+ setProgressText ( undefined )
66
104
return
67
105
}
68
- const est = estimateFinish ( dayjs ( job . started_at ) , transitionStats ) [ 0 ]
106
+
107
+ const [ est , text ] = estimateFinish (
108
+ dayjs ( job . started_at ) ,
109
+ transitionStats . Median ,
110
+ transitionStats . Stddev ,
111
+ )
69
112
setProgressValue ( est )
113
+ setProgressText ( text )
70
114
}
71
115
setTimeout ( updateProgress , 5 )
72
116
} , [ progressValue , job , transitionStats ] )
@@ -93,17 +137,7 @@ export const WorkspaceBuildProgress: FC<WorkspaceBuildProgressProps> = ({
93
137
/>
94
138
< div className = { styles . barHelpers } >
95
139
< div className = { styles . label } > { `Build ${ job . status } ` } </ div >
96
- < div className = { styles . label } >
97
- { ( ( ) => {
98
- if ( job . status !== "running" ) {
99
- return ""
100
- } else if ( transitionStats !== undefined ) {
101
- return estimateFinish ( dayjs ( job . started_at ) , transitionStats ) [ 1 ]
102
- } else {
103
- return "Unknown ETA"
104
- }
105
- } ) ( ) }
106
- </ div >
140
+ < div className = { styles . label } > { progressText } </ div >
107
141
</ div >
108
142
</ div >
109
143
)
0 commit comments