diff --git a/site/src/pages/TaskPage/TaskPage.tsx b/site/src/pages/TaskPage/TaskPage.tsx index 7017986c7b686..4a65c6f1be993 100644 --- a/site/src/pages/TaskPage/TaskPage.tsx +++ b/site/src/pages/TaskPage/TaskPage.tsx @@ -2,25 +2,28 @@ 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 isChromatic from "chromatic/isChromatic"; import { Button } from "components/Button/Button"; import { Loader } from "components/Loader/Loader"; import { Margins } from "components/Margins/Margins"; +import { ScrollArea } from "components/ScrollArea/ScrollArea"; 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"; +import { WorkspaceBuildLogs } from "modules/workspaces/WorkspaceBuildLogs/WorkspaceBuildLogs"; +import { type FC, type ReactNode, useEffect, useRef } from "react"; import { Helmet } from "react-helmet-async"; import { useQuery } from "react-query"; import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; import { Link as RouterLink, useParams } from "react-router"; -import { ellipsizeText } from "utils/ellipsizeText"; import { pageTitle } from "utils/page"; import { - ActiveTransition, + getActiveTransitionStats, WorkspaceBuildProgress, } from "../WorkspacePage/WorkspaceBuildProgress"; import { TaskApps } from "./TaskApps"; import { TaskSidebar } from "./TaskSidebar"; +import { TaskTopbar } from "./TaskTopbar"; const TaskPage = () => { const { workspace: workspaceName, username } = useParams() as { @@ -37,18 +40,7 @@ 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 ( @@ -95,38 +87,9 @@ const TaskPage = () => { } let content: ReactNode = null; - const _terminatedStatuses: WorkspaceStatus[] = [ - "canceled", - "canceling", - "deleted", - "deleting", - "stopped", - "stopping", - ]; 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 -

-
{lastStage}
-
-
- -
-
- ); + content = ; } else if (task.workspace.latest_build.status === "failed") { content = (
@@ -170,14 +133,7 @@ const TaskPage = () => { ); } else { - content = ; - } - - return ( - <> - - {pageTitle(ellipsizeText(task.prompt, 64) ?? "Task")} - + content = ( @@ -185,14 +141,95 @@ const TaskPage = () => {
- {content} + + + + ); + } + + return ( + <> + + {pageTitle(ellipsizeText(task.prompt, 64))} + + +
+ + {content} +
); }; export default TaskPage; +type TaskBuildingWorkspaceProps = { task: Task }; + +const TaskBuildingWorkspace: FC = ({ task }) => { + const { data: template } = useQuery( + templateQueryOptions(task.workspace.template_id), + ); + + const buildLogs = useWorkspaceBuildLogs(task?.workspace.latest_build.id); + + // If no template yet, use an indeterminate progress bar. + const transitionStats = (template && + getActiveTransitionStats(template, task.workspace)) || { + P50: 0, + P95: null, + }; + + const scrollAreaRef = useRef(null); + // biome-ignore lint/correctness/useExhaustiveDependencies: this effect should run when build logs change + useEffect(() => { + if (isChromatic()) { + return; + } + const scrollAreaEl = scrollAreaRef.current; + const scrollAreaViewportEl = scrollAreaEl?.querySelector( + "[data-radix-scroll-area-viewport]", + ); + if (scrollAreaViewportEl) { + scrollAreaViewportEl.scrollTop = scrollAreaViewportEl.scrollHeight; + } + }, [buildLogs]); + + return ( +
+
+
+

+ Starting your workspace +

+
+ Your task will be running in a few moments +
+
+ +
+ + + + + +
+
+
+ ); +}; + export class WorkspaceDoesNotHaveAITaskError extends Error { constructor(workspace: Workspace) { super( @@ -228,3 +265,7 @@ export const data = { } satisfies Task; }, }; + +const ellipsizeText = (text: string, maxLength = 80): string => { + return text.length <= maxLength ? text : `${text.slice(0, maxLength - 3)}...`; +}; diff --git a/site/src/pages/TaskPage/TaskSidebar.tsx b/site/src/pages/TaskPage/TaskSidebar.tsx index 2309884d166b8..eb1aeb6d59375 100644 --- a/site/src/pages/TaskPage/TaskSidebar.tsx +++ b/site/src/pages/TaskPage/TaskSidebar.tsx @@ -1,24 +1,8 @@ import type { WorkspaceApp } from "api/typesGenerated"; -import { Button } from "components/Button/Button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "components/DropdownMenu/DropdownMenu"; import { Spinner } from "components/Spinner/Spinner"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "components/Tooltip/Tooltip"; -import { ArrowLeftIcon, EllipsisVerticalIcon } from "lucide-react"; import type { Task } from "modules/tasks/tasks"; import type { FC } from "react"; -import { Link as RouterLink } from "react-router"; import { TaskAppIFrame } from "./TaskAppIframe"; -import { TaskStatusLink } from "./TaskStatusLink"; type TaskSidebarProps = { task: Task; @@ -84,60 +68,6 @@ export const TaskSidebar: FC = ({ task }) => { return (