Skip to content

Commit 288ec77

Browse files
authored
feat: add workspace build status to task page (#18520)
While a workspace is starting, display the build status and a progress bar.
1 parent a8e2c75 commit 288ec77

File tree

3 files changed

+81
-23
lines changed

3 files changed

+81
-23
lines changed

site/src/pages/TaskPage/TaskPage.stories.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Meta, StoryObj } from "@storybook/react";
22
import { expect, spyOn, within } from "@storybook/test";
3+
import { API } from "api/api";
34
import type {
45
Workspace,
56
WorkspaceApp,
@@ -9,6 +10,7 @@ import {
910
MockFailedWorkspace,
1011
MockStartingWorkspace,
1112
MockStoppedWorkspace,
13+
MockTemplate,
1214
MockWorkspace,
1315
MockWorkspaceAgent,
1416
MockWorkspaceApp,
@@ -59,6 +61,16 @@ export const WaitingOnBuild: Story = {
5961
},
6062
};
6163

64+
export const WaitingOnBuildWithTemplate: Story = {
65+
beforeEach: () => {
66+
spyOn(API, "getTemplate").mockResolvedValue(MockTemplate);
67+
spyOn(data, "fetchTask").mockResolvedValue({
68+
prompt: "Create competitors page",
69+
workspace: MockStartingWorkspace,
70+
});
71+
},
72+
};
73+
6274
export const WaitingOnStatus: Story = {
6375
beforeEach: () => {
6476
spyOn(data, "fetchTask").mockResolvedValue({

site/src/pages/TaskPage/TaskPage.tsx

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { API } from "api/api";
22
import { getErrorDetail, getErrorMessage } from "api/errors";
3+
import { template as templateQueryOptions } from "api/queries/templates";
34
import type { Workspace, WorkspaceStatus } from "api/typesGenerated";
45
import { Button } from "components/Button/Button";
56
import { Loader } from "components/Loader/Loader";
67
import { Margins } from "components/Margins/Margins";
78
import { Spinner } from "components/Spinner/Spinner";
9+
import { useWorkspaceBuildLogs } from "hooks/useWorkspaceBuildLogs";
810
import { ArrowLeftIcon, RotateCcwIcon } from "lucide-react";
911
import { AI_PROMPT_PARAMETER_NAME, type Task } from "modules/tasks/tasks";
1012
import type { ReactNode } from "react";
@@ -14,6 +16,10 @@ import { useParams } from "react-router-dom";
1416
import { Link as RouterLink } from "react-router-dom";
1517
import { ellipsizeText } from "utils/ellipsizeText";
1618
import { pageTitle } from "utils/page";
19+
import {
20+
ActiveTransition,
21+
WorkspaceBuildProgress,
22+
} from "../WorkspacePage/WorkspaceBuildProgress";
1723
import { TaskApps } from "./TaskApps";
1824
import { TaskSidebar } from "./TaskSidebar";
1925

@@ -32,6 +38,19 @@ const TaskPage = () => {
3238
refetchInterval: 5_000,
3339
});
3440

41+
const { data: template } = useQuery({
42+
...templateQueryOptions(task?.workspace.template_id ?? ""),
43+
enabled: Boolean(task),
44+
});
45+
46+
const waitingStatuses: WorkspaceStatus[] = ["starting", "pending"];
47+
const shouldStreamBuildLogs =
48+
task && waitingStatuses.includes(task.workspace.latest_build.status);
49+
const buildLogs = useWorkspaceBuildLogs(
50+
task?.workspace.latest_build.id ?? "",
51+
shouldStreamBuildLogs,
52+
);
53+
3554
if (error) {
3655
return (
3756
<>
@@ -77,7 +96,6 @@ const TaskPage = () => {
7796
}
7897

7998
let content: ReactNode = null;
80-
const waitingStatuses: WorkspaceStatus[] = ["starting", "pending"];
8199
const terminatedStatuses: WorkspaceStatus[] = [
82100
"canceled",
83101
"canceling",
@@ -88,16 +106,25 @@ const TaskPage = () => {
88106
];
89107

90108
if (waitingStatuses.includes(task.workspace.latest_build.status)) {
109+
// If no template yet, use an indeterminate progress bar.
110+
const transition = (template &&
111+
ActiveTransition(template, task.workspace)) || { P50: 0, P95: null };
112+
const lastStage =
113+
buildLogs?.[buildLogs.length - 1]?.stage || "Waiting for build status";
91114
content = (
92-
<div className="w-full min-h-80 flex items-center justify-center">
93-
<div className="flex flex-col items-center">
94-
<Spinner loading className="mb-4" />
115+
<div className="w-full min-h-80 flex flex-col">
116+
<div className="flex flex-col items-center grow justify-center">
95117
<h3 className="m-0 font-medium text-content-primary text-base">
96118
Starting your workspace
97119
</h3>
98-
<span className="text-content-secondary text-sm">
99-
This should take a few minutes
100-
</span>
120+
<div className="text-content-secondary text-sm">{lastStage}</div>
121+
</div>
122+
<div className="w-full">
123+
<WorkspaceBuildProgress
124+
workspace={task.workspace}
125+
transitionStats={transition}
126+
variant="task"
127+
/>
101128
</div>
102129
</div>
103130
);

site/src/pages/WorkspacePage/WorkspaceBuildProgress.tsx

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,18 @@ const estimateFinish = (
6262
interface WorkspaceBuildProgressProps {
6363
workspace: Workspace;
6464
transitionStats: TransitionStats;
65+
// variant changes how the progress bar is displayed: with the workspace
66+
// variant the workspace transition and time remaining are displayed under the
67+
// bar aligned to the left and right respectively. With the task variant the
68+
// workspace transition is not displayed and the time remaining is displayed
69+
// centered above the bar, and the bar's border radius is removed.
70+
variant?: "workspace" | "task";
6571
}
6672

6773
export const WorkspaceBuildProgress: FC<WorkspaceBuildProgressProps> = ({
6874
workspace,
6975
transitionStats,
76+
variant,
7077
}) => {
7178
const job = workspace.latest_build.job;
7279
const [progressValue, setProgressValue] = useState<number | undefined>(0);
@@ -114,6 +121,13 @@ export const WorkspaceBuildProgress: FC<WorkspaceBuildProgressProps> = ({
114121
}
115122
return (
116123
<div css={styles.stack}>
124+
{variant === "task" && (
125+
<div className="mb-1 text-center">
126+
<div css={styles.label} data-chromatic="ignore">
127+
{progressText}
128+
</div>
129+
</div>
130+
)}
117131
<LinearProgress
118132
data-chromatic="ignore"
119133
value={progressValue !== undefined ? progressValue : 0}
@@ -126,26 +140,36 @@ export const WorkspaceBuildProgress: FC<WorkspaceBuildProgressProps> = ({
126140
? "determinate"
127141
: "indeterminate"
128142
}
129-
// If a transition is set, there is a moment on new load where the
130-
// bar accelerates to progressValue and then rapidly decelerates, which
131-
// is not indicative of true progress.
132-
classes={{ bar: classNames.bar }}
143+
classes={{
144+
// If a transition is set, there is a moment on new load where the bar
145+
// accelerates to progressValue and then rapidly decelerates, which is
146+
// not indicative of true progress.
147+
bar: classNames.bar,
148+
// With the "task" variant, the progress bar is fullscreen, so remove
149+
// the border radius.
150+
root: variant === "task" ? classNames.root : undefined,
151+
}}
133152
/>
134-
<div css={styles.barHelpers}>
135-
<div css={styles.label}>
136-
{capitalize(workspace.latest_build.status)} workspace...
137-
</div>
138-
<div css={styles.label} data-chromatic="ignore">
139-
{progressText}
153+
{variant !== "task" && (
154+
<div className="flex mt-1 justify-between">
155+
<div css={styles.label}>
156+
{capitalize(workspace.latest_build.status)} workspace...
157+
</div>
158+
<div css={styles.label} data-chromatic="ignore">
159+
{progressText}
160+
</div>
140161
</div>
141-
</div>
162+
)}
142163
</div>
143164
);
144165
};
145166

146167
const classNames = {
147168
bar: css`
148169
transition: none;
170+
`,
171+
root: css`
172+
border-radius: 0;
149173
`,
150174
};
151175

@@ -154,11 +178,6 @@ const styles = {
154178
paddingLeft: 2,
155179
paddingRight: 2,
156180
},
157-
barHelpers: {
158-
display: "flex",
159-
justifyContent: "space-between",
160-
marginTop: 4,
161-
},
162181
label: (theme) => ({
163182
fontSize: 12,
164183
display: "block",

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy