diff --git a/site/src/pages/TaskPage/TaskPage.stories.tsx b/site/src/pages/TaskPage/TaskPage.stories.tsx index a24968d483e38..03f8cfe739d89 100644 --- a/site/src/pages/TaskPage/TaskPage.stories.tsx +++ b/site/src/pages/TaskPage/TaskPage.stories.tsx @@ -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({ diff --git a/site/src/pages/TaskPage/TaskPage.tsx b/site/src/pages/TaskPage/TaskPage.tsx index a46e0f09c7cc9..c340a96cfef11 100644 --- a/site/src/pages/TaskPage/TaskPage.tsx +++ b/site/src/pages/TaskPage/TaskPage.tsx @@ -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 = ( -
-
- +
+

Starting your workspace

- - This should take a few minutes - +
{lastStage}
+
+
+
); diff --git a/site/src/pages/WorkspacePage/WorkspaceBuildProgress.tsx b/site/src/pages/WorkspacePage/WorkspaceBuildProgress.tsx index 715ceb136c262..306da719be0ca 100644 --- a/site/src/pages/WorkspacePage/WorkspaceBuildProgress.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceBuildProgress.tsx @@ -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 = ({ workspace, transitionStats, + variant, }) => { const job = workspace.latest_build.job; const [progressValue, setProgressValue] = useState(0); @@ -114,6 +121,13 @@ export const WorkspaceBuildProgress: FC = ({ } return (
+ {variant === "task" && ( +
+
+ {progressText} +
+
+ )} = ({ ? "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, + }} /> -
-
- {capitalize(workspace.latest_build.status)} workspace... -
-
- {progressText} + {variant !== "task" && ( +
+
+ {capitalize(workspace.latest_build.status)} workspace... +
+
+ {progressText} +
-
+ )}
); }; @@ -146,6 +167,9 @@ export const WorkspaceBuildProgress: FC = ({ 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",