feat: add workspace build status to task page (#18520)

While a workspace is starting, display the build status and a progress bar.
This commit is contained in:
Asher
2025-06-24 13:17:12 -08:00
committed by GitHub
parent a8e2c75239
commit 288ec7709d
3 changed files with 81 additions and 23 deletions
@@ -1,5 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react";
import { expect, spyOn, within } from "@storybook/test";
import { API } from "api/api";
import type {
Workspace,
WorkspaceApp,
@@ -9,6 +10,7 @@ import {
MockFailedWorkspace,
MockStartingWorkspace,
MockStoppedWorkspace,
MockTemplate,
MockWorkspace,
MockWorkspaceAgent,
MockWorkspaceApp,
@@ -59,6 +61,16 @@ export const WaitingOnBuild: Story = {
},
};
export const WaitingOnBuildWithTemplate: Story = {
beforeEach: () => {
spyOn(API, "getTemplate").mockResolvedValue(MockTemplate);
spyOn(data, "fetchTask").mockResolvedValue({
prompt: "Create competitors page",
workspace: MockStartingWorkspace,
});
},
};
export const WaitingOnStatus: Story = {
beforeEach: () => {
spyOn(data, "fetchTask").mockResolvedValue({
+34 -7
View File
@@ -1,10 +1,12 @@
import { API } from "api/api";
import { getErrorDetail, getErrorMessage } from "api/errors";
import { template as templateQueryOptions } from "api/queries/templates";
import type { Workspace, WorkspaceStatus } from "api/typesGenerated";
import { Button } from "components/Button/Button";
import { Loader } from "components/Loader/Loader";
import { Margins } from "components/Margins/Margins";
import { Spinner } from "components/Spinner/Spinner";
import { useWorkspaceBuildLogs } from "hooks/useWorkspaceBuildLogs";
import { ArrowLeftIcon, RotateCcwIcon } from "lucide-react";
import { AI_PROMPT_PARAMETER_NAME, type Task } from "modules/tasks/tasks";
import type { ReactNode } from "react";
@@ -14,6 +16,10 @@ import { useParams } from "react-router-dom";
import { Link as RouterLink } from "react-router-dom";
import { ellipsizeText } from "utils/ellipsizeText";
import { pageTitle } from "utils/page";
import {
ActiveTransition,
WorkspaceBuildProgress,
} from "../WorkspacePage/WorkspaceBuildProgress";
import { TaskApps } from "./TaskApps";
import { TaskSidebar } from "./TaskSidebar";
@@ -32,6 +38,19 @@ const TaskPage = () => {
refetchInterval: 5_000,
});
const { data: template } = useQuery({
...templateQueryOptions(task?.workspace.template_id ?? ""),
enabled: Boolean(task),
});
const waitingStatuses: WorkspaceStatus[] = ["starting", "pending"];
const shouldStreamBuildLogs =
task && waitingStatuses.includes(task.workspace.latest_build.status);
const buildLogs = useWorkspaceBuildLogs(
task?.workspace.latest_build.id ?? "",
shouldStreamBuildLogs,
);
if (error) {
return (
<>
@@ -77,7 +96,6 @@ const TaskPage = () => {
}
let content: ReactNode = null;
const waitingStatuses: WorkspaceStatus[] = ["starting", "pending"];
const terminatedStatuses: WorkspaceStatus[] = [
"canceled",
"canceling",
@@ -88,16 +106,25 @@ const TaskPage = () => {
];
if (waitingStatuses.includes(task.workspace.latest_build.status)) {
// If no template yet, use an indeterminate progress bar.
const transition = (template &&
ActiveTransition(template, task.workspace)) || { P50: 0, P95: null };
const lastStage =
buildLogs?.[buildLogs.length - 1]?.stage || "Waiting for build status";
content = (
<div className="w-full min-h-80 flex items-center justify-center">
<div className="flex flex-col items-center">
<Spinner loading className="mb-4" />
<div className="w-full min-h-80 flex flex-col">
<div className="flex flex-col items-center grow justify-center">
<h3 className="m-0 font-medium text-content-primary text-base">
Starting your workspace
</h3>
<span className="text-content-secondary text-sm">
This should take a few minutes
</span>
<div className="text-content-secondary text-sm">{lastStage}</div>
</div>
<div className="w-full">
<WorkspaceBuildProgress
workspace={task.workspace}
transitionStats={transition}
variant="task"
/>
</div>
</div>
);
@@ -62,11 +62,18 @@ const estimateFinish = (
interface WorkspaceBuildProgressProps {
workspace: Workspace;
transitionStats: TransitionStats;
// variant changes how the progress bar is displayed: with the workspace
// variant the workspace transition and time remaining are displayed under the
// bar aligned to the left and right respectively. With the task variant the
// workspace transition is not displayed and the time remaining is displayed
// centered above the bar, and the bar's border radius is removed.
variant?: "workspace" | "task";
}
export const WorkspaceBuildProgress: FC<WorkspaceBuildProgressProps> = ({
workspace,
transitionStats,
variant,
}) => {
const job = workspace.latest_build.job;
const [progressValue, setProgressValue] = useState<number | undefined>(0);
@@ -114,6 +121,13 @@ export const WorkspaceBuildProgress: FC<WorkspaceBuildProgressProps> = ({
}
return (
<div css={styles.stack}>
{variant === "task" && (
<div className="mb-1 text-center">
<div css={styles.label} data-chromatic="ignore">
{progressText}
</div>
</div>
)}
<LinearProgress
data-chromatic="ignore"
value={progressValue !== undefined ? progressValue : 0}
@@ -126,19 +140,26 @@ export const WorkspaceBuildProgress: FC<WorkspaceBuildProgressProps> = ({
? "determinate"
: "indeterminate"
}
// If a transition is set, there is a moment on new load where the
// bar accelerates to progressValue and then rapidly decelerates, which
// is not indicative of true progress.
classes={{ bar: classNames.bar }}
classes={{
// If a transition is set, there is a moment on new load where the bar
// accelerates to progressValue and then rapidly decelerates, which is
// not indicative of true progress.
bar: classNames.bar,
// With the "task" variant, the progress bar is fullscreen, so remove
// the border radius.
root: variant === "task" ? classNames.root : undefined,
}}
/>
<div css={styles.barHelpers}>
<div css={styles.label}>
{capitalize(workspace.latest_build.status)} workspace...
{variant !== "task" && (
<div className="flex mt-1 justify-between">
<div css={styles.label}>
{capitalize(workspace.latest_build.status)} workspace...
</div>
<div css={styles.label} data-chromatic="ignore">
{progressText}
</div>
</div>
<div css={styles.label} data-chromatic="ignore">
{progressText}
</div>
</div>
)}
</div>
);
};
@@ -146,6 +167,9 @@ export const WorkspaceBuildProgress: FC<WorkspaceBuildProgressProps> = ({
const classNames = {
bar: css`
transition: none;
`,
root: css`
border-radius: 0;
`,
};
@@ -154,11 +178,6 @@ const styles = {
paddingLeft: 2,
paddingRight: 2,
},
barHelpers: {
display: "flex",
justifyContent: "space-between",
marginTop: 4,
},
label: (theme) => ({
fontSize: 12,
display: "block",