Skip to content

Commit dd32287

Browse files
committed
Build out frontend
1 parent 0e0bff6 commit dd32287

File tree

3 files changed

+90
-51
lines changed

3 files changed

+90
-51
lines changed

site/src/components/Workspace/Workspace.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { WorkspaceStats } from "../WorkspaceStats/WorkspaceStats"
2020
import { AlertBanner } from "../AlertBanner/AlertBanner"
2121
import { useTranslation } from "react-i18next"
2222
import {
23-
EstimateTransitionTime,
23+
ActiveTransition,
2424
WorkspaceBuildProgress,
2525
} from "components/WorkspaceBuildProgress/WorkspaceBuildProgress"
2626
import { AgentRow } from "components/Resources/AgentRow"
@@ -120,12 +120,8 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
120120
)
121121

122122
let transitionStats: TypesGen.TransitionStats | undefined = undefined
123-
let isTransitioning: boolean | undefined = undefined
124123
if (template !== undefined) {
125-
;[transitionStats, isTransitioning] = EstimateTransitionTime(
126-
template,
127-
workspace,
128-
)
124+
transitionStats = ActiveTransition(template, workspace)
129125
}
130126

131127
return (
@@ -201,7 +197,7 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
201197

202198
<WorkspaceStats workspace={workspace} handleUpdate={handleUpdate} />
203199

204-
{isTransitioning !== undefined && isTransitioning && (
200+
{transitionStats !== undefined && (
205201
<WorkspaceBuildProgress
206202
workspace={workspace}
207203
transitionStats={transitionStats}

site/src/components/WorkspaceBuildProgress/WorkspaceBuildProgress.stories.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ const Template: Story<WorkspaceBuildProgressProps> = (args) => (
2121

2222
export const Starting = Template.bind({})
2323
Starting.args = {
24-
transitionStats: 10000,
24+
transitionStats: {
25+
Median: 10000,
26+
Stddev: 10,
27+
},
2528
workspace: {
2629
...MockStartingWorkspace,
2730
latest_build: {
@@ -45,5 +48,11 @@ StartingUnknown.args = {
4548
export const StartingPassedEstimate = Template.bind({})
4649
StartingPassedEstimate.args = {
4750
...Starting.args,
48-
transitionStats: 1000,
51+
transitionStats: { Median: 1000, Stddev: 10 },
52+
}
53+
54+
export const StartingHighStddev = Template.bind({})
55+
StartingHighStddev.args = {
56+
...Starting.args,
57+
transitionStats: { Median: 10000, Stddev: 3000 },
4958
}

site/src/components/WorkspaceBuildProgress/WorkspaceBuildProgress.tsx

Lines changed: 76 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import LinearProgress from "@material-ui/core/LinearProgress"
22
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"
410
import dayjs, { Dayjs } from "dayjs"
511
import { FC, useEffect, useState } from "react"
612
import { MONOSPACE_FONT_FAMILY } from "theme/constants"
@@ -9,43 +15,67 @@ import duration from "dayjs/plugin/duration"
915

1016
dayjs.extend(duration)
1117

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+
1238
const estimateFinish = (
1339
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+
)
1749

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
2256

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...",
2860
]
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+
}
2974
}
3075

3176
export interface WorkspaceBuildProgressProps {
3277
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
4979
}
5080

5181
export const WorkspaceBuildProgress: FC<WorkspaceBuildProgressProps> = ({
@@ -55,18 +85,32 @@ export const WorkspaceBuildProgress: FC<WorkspaceBuildProgressProps> = ({
5585
const styles = useStyles()
5686
const job = workspace.latest_build.job
5787
const [progressValue, setProgressValue] = useState<number | undefined>(0)
88+
const [progressText, setProgressText] = useState<string | undefined>(
89+
"Finding ETA...",
90+
)
5891

5992
// By default workspace is updated every second, which can cause visual stutter
6093
// when the build estimate is a few seconds. The timer ensures no observable
6194
// stutter in all cases.
6295
useEffect(() => {
6396
const updateProgress = () => {
64-
if (job.status !== "running" || transitionStats === undefined) {
97+
if (
98+
job.status !== "running" ||
99+
transitionStats.Median === undefined ||
100+
transitionStats.Stddev === undefined
101+
) {
65102
setProgressValue(undefined)
103+
setProgressText(undefined)
66104
return
67105
}
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+
)
69112
setProgressValue(est)
113+
setProgressText(text)
70114
}
71115
setTimeout(updateProgress, 5)
72116
}, [progressValue, job, transitionStats])
@@ -93,17 +137,7 @@ export const WorkspaceBuildProgress: FC<WorkspaceBuildProgressProps> = ({
93137
/>
94138
<div className={styles.barHelpers}>
95139
<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>
107141
</div>
108142
</div>
109143
)

0 commit comments

Comments
 (0)