diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx index ac0dc9a875..955fde47d9 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx @@ -175,10 +175,10 @@ export const TemplateVersionEditor: FC = ({ typeof editorValue === "string" ? isBinaryData(editorValue) : false; // Auto scroll - const buildLogsRef = useRef(null); + const logsContentRef = useRef(null); useEffect(() => { - if (buildLogsRef.current) { - buildLogsRef.current.scrollTop = buildLogsRef.current.scrollHeight; + if (logsContentRef.current) { + logsContentRef.current.scrollTop = logsContentRef.current.scrollHeight; } }, [buildLogs]); @@ -237,9 +237,7 @@ export const TemplateVersionEditor: FC = ({ paddingRight: 16, }} > - {buildLogs && ( - - )} + = ({ )} -
- {templateVersion.job.error && ( -
- - Error during the build - {templateVersion.job.error} - -
- )} + {selectedTab === "logs" && ( +
+ {templateVersion.job.error && ( +
+ + Error during the build + {templateVersion.job.error} + +
+ )} - {buildLogs && buildLogs.length === 0 && ( - - )} + {buildLogs && buildLogs.length > 0 ? ( + + ) : ( + + )} +
+ )} - {buildLogs && buildLogs.length > 0 && ( - - )} -
- -
- {resources && ( - r.workspace_transition === "start", - )} - /> - )} -
+ {selectedTab === "resources" && ( +
+ {resources && ( + r.workspace_transition === "start", + )} + /> + )} +
+ )} @@ -751,6 +738,17 @@ const styles = { }, }), + tabContent: { + height: 280, + overflowY: "auto", + }, + + logs: { + display: "flex", + height: "100%", + flexDirection: "column", + }, + buildLogs: { borderRadius: 0, border: 0, @@ -780,8 +778,6 @@ const styles = { }, resources: { - overflowY: "auto", - // Hack to access customize resource-card from here "& .resource-card": { borderLeft: 0, diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx index 1558cb5c27..6f54255fbe 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx @@ -1,5 +1,6 @@ import { render, screen, waitFor, within } from "@testing-library/react"; import userEvent, { type UserEvent } from "@testing-library/user-event"; +import WS from "jest-websocket-mock"; import { HttpResponse, http } from "msw"; import { QueryClient } from "react-query"; import { RouterProvider, createMemoryRouter } from "react-router-dom"; @@ -16,6 +17,7 @@ import { MockWorkspaceBuildLogs, } from "testHelpers/entities"; import { + createTestQueryClient, renderWithAuth, waitForLoaderToBeRemoved, } from "testHelpers/renderHelpers"; @@ -291,30 +293,7 @@ describe.each([ ); } - render( - - , - children: [ - { - element: , - path: "/templates/:template/versions/:version/edit", - }, - ], - }, - ], - { - initialEntries: [ - `/templates/${MockTemplate.name}/versions/${MockTemplateVersion.name}/edit`, - ], - }, - )} - /> - , - ); + renderEditorPage(queryClient); await waitForLoaderToBeRemoved(); const dialogSelector = /template variables/i; @@ -326,3 +305,80 @@ describe.each([ }); }, ); + +test("display pending badge and update it to running when status changes", async () => { + const MockPendingTemplateVersion = { + ...MockTemplateVersion, + job: { + ...MockTemplateVersion.job, + status: "pending", + }, + }; + const MockRunningTemplateVersion = { + ...MockTemplateVersion, + job: { + ...MockTemplateVersion.job, + status: "running", + }, + }; + + let calls = 0; + server.use( + http.get( + "/api/v2/organizations/:org/templates/:template/versions/:version", + () => { + calls += 1; + return HttpResponse.json( + calls > 1 ? MockRunningTemplateVersion : MockPendingTemplateVersion, + ); + }, + ), + ); + + // Mock the logs when the status is running. This prevents connection errors + // from being thrown in the console during the test. + new WS( + `ws://localhost/api/v2/templateversions/${MockTemplateVersion.name}/logs?follow=true`, + ); + + renderEditorPage(createTestQueryClient()); + + const status = await screen.findByRole("status"); + expect(status).toHaveTextContent("Pending"); + + await waitFor( + () => { + expect(status).toHaveTextContent("Running"); + }, + // Increase the timeout due to the page fetching results every second, which + // may cause delays. + { timeout: 5_000 }, + ); +}); + +function renderEditorPage(queryClient: QueryClient) { + return render( + + , + children: [ + { + element: , + path: "/templates/:template/versions/:version/edit", + }, + ], + }, + ], + { + initialEntries: [ + `/templates/${MockTemplate.name}/versions/${MockTemplateVersion.name}/edit`, + ], + }, + )} + /> + , + ); +} diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx index 2df3712aa6..10412bd616 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx @@ -43,27 +43,31 @@ export const TemplateVersionEditorPage: FC = () => { templateName, versionName, ); - const templateVersionQuery = useQuery({ + const activeTemplateVersionQuery = useQuery({ ...templateVersionOptions, keepPreviousData: true, + refetchInterval(data) { + return data?.job.status === "pending" ? 1_000 : false; + }, }); + const { data: activeTemplateVersion } = activeTemplateVersionQuery; const uploadFileMutation = useMutation(uploadFile()); const createTemplateVersionMutation = useMutation( createTemplateVersion(organizationId), ); const resourcesQuery = useQuery({ - ...resources(templateVersionQuery.data?.id ?? ""), - enabled: templateVersionQuery.data?.job.status === "succeeded", + ...resources(activeTemplateVersion?.id ?? ""), + enabled: activeTemplateVersion?.job.status === "succeeded", }); - const logs = useWatchVersionLogs(templateVersionQuery.data, { - onDone: templateVersionQuery.refetch, + const logs = useWatchVersionLogs(activeTemplateVersion, { + onDone: activeTemplateVersionQuery.refetch, }); - const { fileTree, tarFile } = useFileTree(templateVersionQuery.data); + const { fileTree, tarFile } = useFileTree(activeTemplateVersion); const { missingVariables, setIsMissingVariablesDialogOpen, isMissingVariablesDialogOpen, - } = useMissingVariables(templateVersionQuery.data); + } = useMissingVariables(activeTemplateVersion); // Handle template publishing const [isPublishingDialogOpen, setIsPublishingDialogOpen] = useState(false); @@ -109,10 +113,10 @@ export const TemplateVersionEditorPage: FC = () => { Record >({}); useEffect(() => { - if (templateVersionQuery.data?.job.tags) { - setProvisionerTags(templateVersionQuery.data.job.tags); + if (activeTemplateVersion?.job.tags) { + setProvisionerTags(activeTemplateVersion.job.tags); } - }, [templateVersionQuery.data?.job.tags]); + }, [activeTemplateVersion?.job.tags]); return ( <> @@ -120,14 +124,14 @@ export const TemplateVersionEditorPage: FC = () => { {pageTitle(`${templateName} ยท Template Editor`)} - {!(templateQuery.data && templateVersionQuery.data && fileTree) ? ( + {!(templateQuery.data && activeTemplateVersion && fileTree) ? ( ) : ( { if (!tarFile) { @@ -159,10 +163,10 @@ export const TemplateVersionEditorPage: FC = () => { await publishVersionMutation.mutateAsync({ isActiveVersion, data, - version: templateVersionQuery.data, + version: activeTemplateVersion, }); const publishedVersion = { - ...templateVersionQuery.data, + ...activeTemplateVersion, ...data, }; setIsPublishingDialogOpen(false); @@ -190,13 +194,12 @@ export const TemplateVersionEditorPage: FC = () => { isBuilding={ createTemplateVersionMutation.isLoading || uploadFileMutation.isLoading || - templateVersionQuery.data.job.status === "running" || - templateVersionQuery.data.job.status === "pending" + activeTemplateVersion.job.status === "running" || + activeTemplateVersion.job.status === "pending" } canPublish={ - templateVersionQuery.data.job.status === "succeeded" && - templateQuery.data.active_version_id !== - templateVersionQuery.data.id + activeTemplateVersion.job.status === "succeeded" && + templateQuery.data.active_version_id !== activeTemplateVersion.id } resources={resourcesQuery.data} buildLogs={logs} diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionStatusBadge.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionStatusBadge.tsx index 625927020d..6da9ce01e2 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionStatusBadge.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionStatusBadge.tsx @@ -1,9 +1,11 @@ import CheckIcon from "@mui/icons-material/CheckOutlined"; import ErrorIcon from "@mui/icons-material/ErrorOutline"; +import QueuedIcon from "@mui/icons-material/HourglassEmpty"; import type { FC, ReactNode } from "react"; import type { TemplateVersion } from "api/typesGenerated"; import { Pill, PillSpinner } from "components/Pill/Pill"; import type { ThemeRole } from "theme/roles"; +import { getPendingStatusLabel } from "utils/provisionerJob"; interface TemplateVersionStatusBadgeProps { version: TemplateVersion; @@ -14,7 +16,12 @@ export const TemplateVersionStatusBadge: FC< > = ({ version }) => { const { text, icon, type } = getStatus(version); return ( - + {text} ); @@ -37,8 +44,8 @@ export const getStatus = ( case "pending": return { type: "info", - text: "Pending", - icon: , + text: getPendingStatusLabel(version.job), + icon: , }; case "canceling": return { diff --git a/site/src/utils/provisionerJob.ts b/site/src/utils/provisionerJob.ts new file mode 100644 index 0000000000..00d618f433 --- /dev/null +++ b/site/src/utils/provisionerJob.ts @@ -0,0 +1,10 @@ +import type { ProvisionerJob } from "api/typesGenerated"; + +export const getPendingStatusLabel = ( + provisionerJob?: ProvisionerJob, +): string => { + if (!provisionerJob || provisionerJob.queue_size === 0) { + return "Pending"; + } + return "Position in queue: " + provisionerJob.queue_position; +}; diff --git a/site/src/utils/workspace.tsx b/site/src/utils/workspace.tsx index 2bc789e0d0..59bb670508 100644 --- a/site/src/utils/workspace.tsx +++ b/site/src/utils/workspace.tsx @@ -10,6 +10,7 @@ import utc from "dayjs/plugin/utc"; import semver from "semver"; import type * as TypesGen from "api/typesGenerated"; import { PillSpinner } from "components/Pill/Pill"; +import { getPendingStatusLabel } from "./provisionerJob"; dayjs.extend(duration); dayjs.extend(utc); @@ -234,21 +235,12 @@ export const getDisplayWorkspaceStatus = ( case "pending": return { type: "active", - text: getPendingWorkspaceStatusText(provisionerJob), + text: getPendingStatusLabel(provisionerJob), icon: , } as const; } }; -const getPendingWorkspaceStatusText = ( - provisionerJob?: TypesGen.ProvisionerJob, -): string => { - if (!provisionerJob || provisionerJob.queue_size === 0) { - return "Pending"; - } - return "Position in queue: " + provisionerJob.queue_position; -}; - export const hasJobError = (workspace: TypesGen.Workspace) => { return workspace.latest_build.job.error !== undefined; };