From 2309bc45f8acc1f29d250b61b65dfd7278eb06e4 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 9 Feb 2024 17:02:12 +0000 Subject: [PATCH 01/15] Show build logs during template creation --- site/src/api/api.ts | 4 +- site/src/api/queries/templates.ts | 8 +- site/src/modules/templates/useVersionLogs.ts | 47 ++++++++++++ .../CreateTemplatePage/BuildLogsDrawer.tsx | 75 +++++++++++++++++++ .../CreateTemplatePage/CreateTemplatePage.tsx | 40 +++++++--- .../DuplicateTemplateView.tsx | 30 +++----- .../ImportStarterTemplateView.tsx | 35 ++++----- .../CreateTemplatePage/UploadTemplateView.tsx | 33 ++++---- site/src/pages/CreateTemplatePage/types.ts | 7 ++ .../TemplateVersionEditorPage.test.tsx | 6 +- .../TemplateVersionEditorPage.tsx | 47 +----------- .../WorkspacePage/WorkspacePage.test.tsx | 2 +- 12 files changed, 213 insertions(+), 121 deletions(-) create mode 100644 site/src/modules/templates/useVersionLogs.ts create mode 100644 site/src/pages/CreateTemplatePage/BuildLogsDrawer.tsx create mode 100644 site/src/pages/CreateTemplatePage/types.ts diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 0a2e421ecc891..2610f66654250 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1502,7 +1502,7 @@ export const watchAgentMetadata = (agentId: string): EventSource => { type WatchBuildLogsByTemplateVersionIdOptions = { after?: number; onMessage: (log: TypesGen.ProvisionerJobLog) => void; - onDone: () => void; + onDone?: () => void; onError: (error: Error) => void; }; export const watchBuildLogsByTemplateVersionId = ( @@ -1534,7 +1534,7 @@ export const watchBuildLogsByTemplateVersionId = ( }); socket.addEventListener("close", () => { // When the socket closes, logs have finished streaming! - onDone(); + onDone && onDone(); }); return socket; }; diff --git a/site/src/api/queries/templates.ts b/site/src/api/queries/templates.ts index c0ba39e7cb9e2..c5c12396a8a66 100644 --- a/site/src/api/queries/templates.ts +++ b/site/src/api/queries/templates.ts @@ -206,15 +206,19 @@ export const createTemplate = () => { }; }; -const createTemplateFn = async (options: { +export type CreateTemplateOptions = { organizationId: string; version: CreateTemplateVersionRequest; template: Omit; -}) => { + onCreateVersion?: (version: TemplateVersion) => void; +}; + +const createTemplateFn = async (options: CreateTemplateOptions) => { const version = await API.createTemplateVersion( options.organizationId, options.version, ); + options.onCreateVersion?.(version); await waitBuildToBeFinished(version); return API.createTemplate(options.organizationId, { ...options.template, diff --git a/site/src/modules/templates/useVersionLogs.ts b/site/src/modules/templates/useVersionLogs.ts new file mode 100644 index 0000000000000..5dd9ff4158dc0 --- /dev/null +++ b/site/src/modules/templates/useVersionLogs.ts @@ -0,0 +1,47 @@ +import { watchBuildLogsByTemplateVersionId } from "api/api"; +import { + ProvisionerJobLog, + ProvisionerJobStatus, + TemplateVersion, +} from "api/typesGenerated"; +import { useState, useEffect } from "react"; + +export const useVersionLogs = ( + templateVersion: TemplateVersion | undefined, + options?: { onDone: () => Promise }, +) => { + const [logs, setLogs] = useState(); + const templateVersionId = templateVersion?.id; + const templateVersionStatus = templateVersion?.job.status; + + useEffect(() => { + const enabledStatuses: ProvisionerJobStatus[] = ["running", "pending"]; + + if (!templateVersionId || !templateVersionStatus) { + return; + } + + if (!enabledStatuses.includes(templateVersionStatus)) { + return; + } + + const socket = watchBuildLogsByTemplateVersionId(templateVersionId, { + onMessage: (log) => { + setLogs((logs) => (logs ? [...logs, log] : [log])); + }, + onDone: options?.onDone, + onError: (error) => { + console.error(error); + }, + }); + + return () => { + socket.close(); + }; + }, [options?.onDone, templateVersionId, templateVersionStatus]); + + return { + logs, + setLogs, + }; +}; diff --git a/site/src/pages/CreateTemplatePage/BuildLogsDrawer.tsx b/site/src/pages/CreateTemplatePage/BuildLogsDrawer.tsx new file mode 100644 index 0000000000000..d96e51939247e --- /dev/null +++ b/site/src/pages/CreateTemplatePage/BuildLogsDrawer.tsx @@ -0,0 +1,75 @@ +import Drawer from "@mui/material/Drawer"; +import Close from "@mui/icons-material/Close"; +import IconButton from "@mui/material/IconButton"; +import { visuallyHidden } from "@mui/utils"; +import { FC, useEffect, useRef } from "react"; +import { TemplateVersion } from "api/typesGenerated"; +import { WorkspaceBuildLogs } from "modules/workspaces/WorkspaceBuildLogs/WorkspaceBuildLogs"; +import { useTheme } from "@emotion/react"; +import { navHeight } from "theme/constants"; +import { useVersionLogs } from "modules/templates/useVersionLogs"; + +type BuildLogsDrawerProps = { + open: boolean; + onClose: () => void; + templateVersion: TemplateVersion | undefined; +}; + +export const BuildLogsDrawer: FC = ({ + templateVersion, + ...drawerProps +}) => { + const theme = useTheme(); + const { logs } = useVersionLogs(templateVersion); + + // Auto scroll + const logsContainer = useRef(null); + useEffect(() => { + if (logsContainer.current) { + logsContainer.current.scrollTop = logsContainer.current.scrollHeight; + } + }, [logs]); + + return ( + +
+
+

+ Creating template... +

+ + + Close build logs + +
+ +
+ +
+
+
+ ); +}; diff --git a/site/src/pages/CreateTemplatePage/CreateTemplatePage.tsx b/site/src/pages/CreateTemplatePage/CreateTemplatePage.tsx index c5440e0929701..3722c75b1b416 100644 --- a/site/src/pages/CreateTemplatePage/CreateTemplatePage.tsx +++ b/site/src/pages/CreateTemplatePage/CreateTemplatePage.tsx @@ -1,4 +1,4 @@ -import { type FC } from "react"; +import { useState, type FC } from "react"; import { Helmet } from "react-helmet-async"; import { useNavigate, useSearchParams } from "react-router-dom"; import { pageTitle } from "utils/page"; @@ -6,20 +6,36 @@ import { FullPageHorizontalForm } from "components/FullPageForm/FullPageHorizont import { DuplicateTemplateView } from "./DuplicateTemplateView"; import { ImportStarterTemplateView } from "./ImportStarterTemplateView"; import { UploadTemplateView } from "./UploadTemplateView"; -import { Template } from "api/typesGenerated"; +import { BuildLogsDrawer } from "./BuildLogsDrawer"; +import { useMutation } from "react-query"; +import { createTemplate } from "api/queries/templates"; +import { CreateTemplatePageViewProps } from "./types"; +import { TemplateVersion } from "api/typesGenerated"; const CreateTemplatePage: FC = () => { const navigate = useNavigate(); const [searchParams] = useSearchParams(); - - const onSuccess = (template: Template) => { - navigate(`/templates/${template.name}/files`); - }; + const [isBuildLogsOpen, setIsBuildLogsOpen] = useState(false); + const [templateVersion, setTemplateVersion] = useState(); + const createTemplateMutation = useMutation(createTemplate()); const onCancel = () => { navigate(-1); }; + const pageViewProps: CreateTemplatePageViewProps = { + onCreateTemplate: async (options) => { + setIsBuildLogsOpen(true); + const template = await createTemplateMutation.mutateAsync({ + ...options, + onCreateVersion: setTemplateVersion, + }); + navigate(`/templates/${template.name}/files`); + }, + error: createTemplateMutation.error, + isCreating: createTemplateMutation.isLoading, + }; + return ( <> @@ -28,13 +44,19 @@ const CreateTemplatePage: FC = () => { {searchParams.has("fromTemplate") ? ( - + ) : searchParams.has("exampleId") ? ( - + ) : ( - + )} + + setIsBuildLogsOpen(false)} + templateVersion={templateVersion} + /> ); }; diff --git a/site/src/pages/CreateTemplatePage/DuplicateTemplateView.tsx b/site/src/pages/CreateTemplatePage/DuplicateTemplateView.tsx index 62540343c9c1d..3a3f26e4f2978 100644 --- a/site/src/pages/CreateTemplatePage/DuplicateTemplateView.tsx +++ b/site/src/pages/CreateTemplatePage/DuplicateTemplateView.tsx @@ -1,5 +1,5 @@ import { type FC } from "react"; -import { useQuery, useMutation } from "react-query"; +import { useQuery } from "react-query"; import { useNavigate, useSearchParams } from "react-router-dom"; import { templateVersionLogs, @@ -7,7 +7,6 @@ import { templateVersion, templateVersionVariables, JobError, - createTemplate, } from "api/queries/templates"; import { useOrganizationId } from "contexts/auth/useOrganizationId"; import { useDashboard } from "modules/dashboard/useDashboard"; @@ -15,14 +14,12 @@ import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Loader } from "components/Loader/Loader"; import { CreateTemplateForm } from "./CreateTemplateForm"; import { firstVersionFromFile, getFormPermissions, newTemplate } from "./utils"; -import { Template } from "api/typesGenerated"; +import { CreateTemplatePageViewProps } from "./types"; -type DuplicateTemplateViewProps = { - onSuccess: (template: Template) => void; -}; - -export const DuplicateTemplateView: FC = ({ - onSuccess, +export const DuplicateTemplateView: FC = ({ + onCreateTemplate, + error, + isCreating, }) => { const navigate = useNavigate(); const organizationId = useOrganizationId(); @@ -51,11 +48,9 @@ export const DuplicateTemplateView: FC = ({ const dashboard = useDashboard(); const formPermissions = getFormPermissions(dashboard.entitlements); - const createTemplateMutation = useMutation(createTemplate()); - const createError = createTemplateMutation.error; - const isJobError = createError instanceof JobError; + const isJobError = error instanceof JobError; const templateVersionLogsQuery = useQuery({ - ...templateVersionLogs(isJobError ? createError.version.id : ""), + ...templateVersionLogs(isJobError ? error.version.id : ""), enabled: isJobError, }); @@ -71,14 +66,14 @@ export const DuplicateTemplateView: FC = ({ navigate(-1)} - jobError={isJobError ? createError.job.error : undefined} + jobError={isJobError ? error.job.error : undefined} logs={templateVersionLogsQuery.data} onSubmit={async (formData) => { - const template = await createTemplateMutation.mutateAsync({ + await onCreateTemplate({ organizationId, version: firstVersionFromFile( templateVersionQuery.data!.job.file_id, @@ -86,7 +81,6 @@ export const DuplicateTemplateView: FC = ({ ), template: newTemplate(formData), }); - onSuccess(template); }} /> ); diff --git a/site/src/pages/CreateTemplatePage/ImportStarterTemplateView.tsx b/site/src/pages/CreateTemplatePage/ImportStarterTemplateView.tsx index 5075b0ecae915..ab0aff6462cf7 100644 --- a/site/src/pages/CreateTemplatePage/ImportStarterTemplateView.tsx +++ b/site/src/pages/CreateTemplatePage/ImportStarterTemplateView.tsx @@ -1,10 +1,9 @@ import { type FC } from "react"; -import { useQuery, useMutation } from "react-query"; +import { useQuery } from "react-query"; import { useNavigate, useSearchParams } from "react-router-dom"; import { templateVersionLogs, JobError, - createTemplate, templateExamples, templateVersionVariables, } from "api/queries/templates"; @@ -18,14 +17,12 @@ import { getFormPermissions, newTemplate, } from "./utils"; -import { Template } from "api/typesGenerated"; +import { CreateTemplatePageViewProps } from "./types"; -type ImportStarterTemplateViewProps = { - onSuccess: (template: Template) => void; -}; - -export const ImportStarterTemplateView: FC = ({ - onSuccess, +export const ImportStarterTemplateView: FC = ({ + onCreateTemplate, + error, + isCreating, }) => { const navigate = useNavigate(); const organizationId = useOrganizationId(); @@ -41,19 +38,16 @@ export const ImportStarterTemplateView: FC = ({ const dashboard = useDashboard(); const formPermissions = getFormPermissions(dashboard.entitlements); - const createTemplateMutation = useMutation(createTemplate()); - const createError = createTemplateMutation.error; - const isJobError = createError instanceof JobError; + const isJobError = error instanceof JobError; const templateVersionLogsQuery = useQuery({ - ...templateVersionLogs(isJobError ? createError.version.id : ""), + ...templateVersionLogs(isJobError ? error.version.id : ""), enabled: isJobError, }); const missedVariables = useQuery({ - ...templateVersionVariables(isJobError ? createError.version.id : ""), + ...templateVersionVariables(isJobError ? error.version.id : ""), enabled: - isJobError && - createError.job.error_code === "REQUIRED_TEMPLATE_VARIABLES", + isJobError && error.job.error_code === "REQUIRED_TEMPLATE_VARIABLES", }); if (isLoading) { @@ -69,13 +63,13 @@ export const ImportStarterTemplateView: FC = ({ {...formPermissions} starterTemplate={templateExample!} variables={missedVariables.data} - error={createTemplateMutation.error} - isSubmitting={createTemplateMutation.isLoading} + error={error} + isSubmitting={isCreating} onCancel={() => navigate(-1)} - jobError={isJobError ? createError.job.error : undefined} + jobError={isJobError ? error.job.error : undefined} logs={templateVersionLogsQuery.data} onSubmit={async (formData) => { - const template = await createTemplateMutation.mutateAsync({ + await onCreateTemplate({ organizationId, version: firstVersionFromExample( templateExample!, @@ -83,7 +77,6 @@ export const ImportStarterTemplateView: FC = ({ ), template: newTemplate(formData), }); - onSuccess(template); }} /> ); diff --git a/site/src/pages/CreateTemplatePage/UploadTemplateView.tsx b/site/src/pages/CreateTemplatePage/UploadTemplateView.tsx index d228e484d0e4d..c20a237716cff 100644 --- a/site/src/pages/CreateTemplatePage/UploadTemplateView.tsx +++ b/site/src/pages/CreateTemplatePage/UploadTemplateView.tsx @@ -3,7 +3,6 @@ import { useNavigate } from "react-router-dom"; import { templateVersionLogs, JobError, - createTemplate, templateVersionVariables, } from "api/queries/templates"; import { uploadFile } from "api/queries/files"; @@ -11,15 +10,13 @@ import { useOrganizationId } from "contexts/auth/useOrganizationId"; import { useDashboard } from "modules/dashboard/useDashboard"; import { CreateTemplateForm } from "./CreateTemplateForm"; import { firstVersionFromFile, getFormPermissions, newTemplate } from "./utils"; -import { Template } from "api/typesGenerated"; import { FC } from "react"; +import { CreateTemplatePageViewProps } from "./types"; -type UploadTemplateViewProps = { - onSuccess: (template: Template) => void; -}; - -export const UploadTemplateView: FC = ({ - onSuccess, +export const UploadTemplateView: FC = ({ + onCreateTemplate, + isCreating, + error, }) => { const navigate = useNavigate(); const organizationId = useOrganizationId(); @@ -30,29 +27,26 @@ export const UploadTemplateView: FC = ({ const uploadFileMutation = useMutation(uploadFile()); const uploadedFile = uploadFileMutation.data; - const createTemplateMutation = useMutation(createTemplate()); - const createError = createTemplateMutation.error; - const isJobError = createError instanceof JobError; + const isJobError = error instanceof JobError; const templateVersionLogsQuery = useQuery({ - ...templateVersionLogs(isJobError ? createError.version.id : ""), + ...templateVersionLogs(isJobError ? error.version.id : ""), enabled: isJobError, }); const missedVariables = useQuery({ - ...templateVersionVariables(isJobError ? createError.version.id : ""), + ...templateVersionVariables(isJobError ? error.version.id : ""), enabled: - isJobError && - createError.job.error_code === "REQUIRED_TEMPLATE_VARIABLES", + isJobError && error.job.error_code === "REQUIRED_TEMPLATE_VARIABLES", }); return ( navigate(-1)} - jobError={isJobError ? createError.job.error : undefined} + jobError={isJobError ? error.job.error : undefined} logs={templateVersionLogsQuery.data} upload={{ onUpload: uploadFileMutation.mutateAsync, @@ -61,7 +55,7 @@ export const UploadTemplateView: FC = ({ file: uploadFileMutation.variables, }} onSubmit={async (formData) => { - const template = await createTemplateMutation.mutateAsync({ + await onCreateTemplate({ organizationId, version: firstVersionFromFile( uploadedFile!.hash, @@ -69,7 +63,6 @@ export const UploadTemplateView: FC = ({ ), template: newTemplate(formData), }); - onSuccess(template); }} /> ); diff --git a/site/src/pages/CreateTemplatePage/types.ts b/site/src/pages/CreateTemplatePage/types.ts new file mode 100644 index 0000000000000..5216d5a975632 --- /dev/null +++ b/site/src/pages/CreateTemplatePage/types.ts @@ -0,0 +1,7 @@ +import { CreateTemplateOptions } from "api/queries/templates"; + +export type CreateTemplatePageViewProps = { + onCreateTemplate: (options: CreateTemplateOptions) => Promise; + error: unknown; + isCreating: boolean; +}; diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx index 491c004bf971b..f6792478760f6 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx @@ -72,7 +72,7 @@ test("Use custom name, message and set it as active when publishing", async () = .spyOn(api, "watchBuildLogsByTemplateVersionId") .mockImplementation((_, options) => { options.onMessage(MockWorkspaceBuildLogs[0]); - options.onDone(); + options.onDone?.(); return jest.fn() as never; }); const buildButton = within(topbar).getByRole("button", { @@ -135,7 +135,7 @@ test("Do not mark as active if promote is not checked", async () => { .spyOn(api, "watchBuildLogsByTemplateVersionId") .mockImplementation((_, options) => { options.onMessage(MockWorkspaceBuildLogs[0]); - options.onDone(); + options.onDone?.(); return jest.fn() as never; }); const buildButton = within(topbar).getByRole("button", { @@ -200,7 +200,7 @@ test("Patch request is not send when there are no changes", async () => { .spyOn(api, "watchBuildLogsByTemplateVersionId") .mockImplementation((_, options) => { options.onMessage(MockWorkspaceBuildLogs[0]); - options.onDone(); + options.onDone?.(); return jest.fn() as never; }); const buildButton = within(topbar).getByRole("button", { diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx index 25afb46179b58..14a5b9468d6a0 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx @@ -5,14 +5,9 @@ import { useNavigate, useParams, useSearchParams } from "react-router-dom"; import { TemplateVersionEditor } from "./TemplateVersionEditor"; import { useOrganizationId } from "contexts/auth/useOrganizationId"; import { pageTitle } from "utils/page"; -import { - patchTemplateVersion, - updateActiveTemplateVersion, - watchBuildLogsByTemplateVersionId, -} from "api/api"; +import { patchTemplateVersion, updateActiveTemplateVersion } from "api/api"; import type { PatchTemplateVersionRequest, - ProvisionerJobLog, TemplateVersion, } from "api/typesGenerated"; import { @@ -28,6 +23,7 @@ import { FileTree, traverse } from "utils/filetree"; import { createTemplateVersionFileTree } from "utils/templateVersion"; import { displayError } from "components/GlobalSnackbar/utils"; import { FullScreenLoader } from "components/Loader/FullScreenLoader"; +import { useVersionLogs } from "modules/templates/useVersionLogs"; type Params = { version: string; @@ -276,45 +272,6 @@ const useFileTree = (templateVersion: TemplateVersion | undefined) => { return state; }; -const useVersionLogs = ( - templateVersion: TemplateVersion | undefined, - options: { onDone: () => Promise }, -) => { - const [logs, setLogs] = useState(); - const templateVersionId = templateVersion?.id; - const refetchTemplateVersion = options.onDone; - const templateVersionStatus = templateVersion?.job.status; - - useEffect(() => { - if (!templateVersionId || !templateVersionStatus) { - return; - } - - if (templateVersionStatus !== "running") { - return; - } - - const socket = watchBuildLogsByTemplateVersionId(templateVersionId, { - onMessage: (log) => { - setLogs((logs) => (logs ? [...logs, log] : [log])); - }, - onDone: refetchTemplateVersion, - onError: (error) => { - console.error(error); - }, - }); - - return () => { - socket.close(); - }; - }, [refetchTemplateVersion, templateVersionId, templateVersionStatus]); - - return { - logs, - setLogs, - }; -}; - const useMissingVariables = (templateVersion: TemplateVersion | undefined) => { const isRequiringVariables = templateVersion?.job.error_code === "REQUIRED_TEMPLATE_VARIABLES"; diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index f613eaf028575..d2482b8494cb4 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -33,7 +33,7 @@ const renderWorkspacePage = async (workspace: Workspace) => { jest .spyOn(api, "watchWorkspaceAgentLogs") .mockImplementation((_, options) => { - options.onDone && options.onDone(); + options.onDone?.(); return new WebSocket(""); }); From 34f97e895ad42f8b61c65d369a11dfcfbd5f239f Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 22 Feb 2024 16:41:11 +0000 Subject: [PATCH 02/15] Add minor adjustments --- site/src/components/Form/Form.tsx | 101 ++++++++------- site/src/components/FormFooter/FormFooter.tsx | 3 + site/src/modules/templates/useVersionLogs.ts | 1 + .../WorkspaceBuildLogs/WorkspaceBuildLogs.tsx | 3 + .../CreateTemplatePage/BuildLogsDrawer.tsx | 69 ++++++++++- .../CreateTemplatePage/CreateTemplateForm.tsx | 115 ++++++------------ .../CreateTemplatePage/CreateTemplatePage.tsx | 7 +- .../DuplicateTemplateView.tsx | 4 + .../ImportStarterTemplateView.tsx | 5 + .../CreateTemplatePage/UploadTemplateView.tsx | 4 + site/src/pages/CreateTemplatePage/types.ts | 2 + 11 files changed, 182 insertions(+), 132 deletions(-) diff --git a/site/src/components/Form/Form.tsx b/site/src/components/Form/Form.tsx index c9fff001b0814..add434c84e044 100644 --- a/site/src/components/Form/Form.tsx +++ b/site/src/components/Form/Form.tsx @@ -6,6 +6,7 @@ import { useContext, ReactNode, ComponentProps, + forwardRef, } from "react"; import { AlphaBadge, DeprecatedBadge } from "components/Badges/Badges"; import { Stack } from "components/Stack/Stack"; @@ -81,59 +82,65 @@ interface FormSectionProps { deprecated?: boolean; } -export const FormSection: FC = ({ - children, - title, - description, - classes = {}, - alpha = false, - deprecated = false, -}) => { - const { direction } = useContext(FormContext); - const theme = useTheme(); - - return ( -
-
( + ( + { + children, + title, + description, + classes = {}, + alpha = false, + deprecated = false, + }, + ref, + ) => { + const { direction } = useContext(FormContext); + const theme = useTheme(); + + return ( +
-

- {title} - {alpha && } - {deprecated && } -

-
{description}
-
- - {children} -
- ); -}; +
+

+ {title} + {alpha && } + {deprecated && } +

+
{description}
+
+ + {children} + + ); + }, +); export const FormFields: FC> = (props) => { return ( diff --git a/site/src/components/FormFooter/FormFooter.tsx b/site/src/components/FormFooter/FormFooter.tsx index fe669ca5efe78..9d7496e083b6f 100644 --- a/site/src/components/FormFooter/FormFooter.tsx +++ b/site/src/components/FormFooter/FormFooter.tsx @@ -19,10 +19,12 @@ export interface FormFooterProps { styles?: FormFooterStyles; submitLabel?: string; submitDisabled?: boolean; + extraActions?: React.ReactNode; } export const FormFooter: FC = ({ onCancel, + extraActions, isLoading, submitDisabled, submitLabel = Language.defaultSubmitLabel, @@ -52,6 +54,7 @@ export const FormFooter: FC = ({ > {Language.cancelLabel} + {extraActions} ); }; diff --git a/site/src/modules/templates/useVersionLogs.ts b/site/src/modules/templates/useVersionLogs.ts index 5dd9ff4158dc0..64d45525e5d3b 100644 --- a/site/src/modules/templates/useVersionLogs.ts +++ b/site/src/modules/templates/useVersionLogs.ts @@ -15,6 +15,7 @@ export const useVersionLogs = ( const templateVersionStatus = templateVersion?.job.status; useEffect(() => { + setLogs([]); const enabledStatuses: ProvisionerJobStatus[] = ["running", "pending"]; if (!templateVersionId || !templateVersionStatus) { diff --git a/site/src/modules/workspaces/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx b/site/src/modules/workspaces/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx index 43a85dccfcd3f..f3aabf737fa68 100644 --- a/site/src/modules/workspaces/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx +++ b/site/src/modules/workspaces/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx @@ -62,6 +62,9 @@ export const WorkspaceBuildLogs: FC = ({ }} {...attrs} > + {stages.length === 0 && ( +
Waiting for logs...
+ )} {stages.map((stage) => { const logs = groupedLogsByStage[stage]; const isEmpty = logs.every((log) => log.output === ""); diff --git a/site/src/pages/CreateTemplatePage/BuildLogsDrawer.tsx b/site/src/pages/CreateTemplatePage/BuildLogsDrawer.tsx index d96e51939247e..0105f6c8eeac5 100644 --- a/site/src/pages/CreateTemplatePage/BuildLogsDrawer.tsx +++ b/site/src/pages/CreateTemplatePage/BuildLogsDrawer.tsx @@ -8,28 +8,52 @@ import { WorkspaceBuildLogs } from "modules/workspaces/WorkspaceBuildLogs/Worksp import { useTheme } from "@emotion/react"; import { navHeight } from "theme/constants"; import { useVersionLogs } from "modules/templates/useVersionLogs"; +import { JobError } from "api/queries/templates"; +import AlertTitle from "@mui/material/AlertTitle"; +import { Alert, AlertDetail } from "components/Alert/Alert"; +import Button from "@mui/material/Button"; +import Collapse from "@mui/material/Collapse"; type BuildLogsDrawerProps = { + error: unknown; open: boolean; onClose: () => void; templateVersion: TemplateVersion | undefined; + variablesSectionRef: React.RefObject; }; export const BuildLogsDrawer: FC = ({ templateVersion, + error, + variablesSectionRef, ...drawerProps }) => { const theme = useTheme(); const { logs } = useVersionLogs(templateVersion); - - // Auto scroll const logsContainer = useRef(null); + + const scrollToBottom = () => { + setTimeout(() => { + if (logsContainer.current) { + logsContainer.current.scrollTop = logsContainer.current.scrollHeight; + } + }, 0); + }; + useEffect(() => { - if (logsContainer.current) { - logsContainer.current.scrollTop = logsContainer.current.scrollHeight; - } + scrollToBottom(); }, [logs]); + useEffect(() => { + if (drawerProps.open) { + scrollToBottom(); + } + }, [drawerProps.open]); + + const isMissingVariables = + error instanceof JobError && + error.job.error_code === "REQUIRED_TEMPLATE_VARIABLES"; + return (
= ({
= ({
+ + { + variablesSectionRef.current?.scrollIntoView({ + behavior: "smooth", + }); + const firstVariableInput = + variablesSectionRef.current?.querySelector("input"); + setTimeout(() => firstVariableInput?.focus(), 0); + drawerProps.onClose(); + }} + > + Add variables + + } + > + Failed to create template + {isMissingVariables && error.message} + +
void; onSubmit: (data: CreateTemplateData) => void; + onOpenBuildLogsDrawer: () => void; isSubmitting: boolean; variables?: TemplateVersionVariable[]; error?: unknown; jobError?: string; logs?: ProvisionerJobLog[]; allowAdvancedScheduling: boolean; + variablesSectionRef: React.RefObject; }; export const CreateTemplateForm: FC = (props) => { const { onCancel, onSubmit, + onOpenBuildLogsDrawer, variables, isSubmitting, error, jobError, logs, allowAdvancedScheduling, + variablesSectionRef, } = props; const form = useFormik({ initialValues: getInitialValues({ @@ -198,18 +199,6 @@ export const CreateTemplateForm: FC = (props) => { }); const getFieldHelpers = getFormHelpers(form, error); - useEffect(() => { - if (error) { - window.scrollTo(0, 0); - } - }, [error]); - - useEffect(() => { - if (jobError) { - window.scrollTo(0, document.body.scrollHeight); - } - }, [logs, jobError]); - return ( {/* General info */} @@ -283,6 +272,7 @@ export const CreateTemplateForm: FC = (props) => { {/* Variables */} {variables && variables.length > 0 && ( @@ -305,27 +295,37 @@ export const CreateTemplateForm: FC = (props) => { )} - {jobError && ( - -
-
Error during provisioning
-

- Looks like we found an error during the template provisioning. You - can see the logs bellow. -

- - {jobError} -
- - -
- )} - - +
+ ({ + backgroundColor: "transparent", + border: 0, + fontWeight: 500, + fontSize: 14, + cursor: "pointer", + color: theme.palette.text.secondary, + + "&:hover": { + textDecoration: "underline", + textUnderlineOffset: 4, + color: theme.palette.text.primary, + }, + })} + > + Show build logs + + ) + } + onCancel={onCancel} + isLoading={isSubmitting} + submitLabel={jobError ? "Retry" : "Create template"} + /> +
); }; @@ -344,44 +344,3 @@ const fillNameAndDisplayWithFilename = async ( form.setFieldValue("display_name", capitalize(name)), ]); }; - -const styles = { - ttlFields: { - width: "100%", - }, - - optionText: (theme) => ({ - fontSize: 16, - color: theme.palette.text.primary, - }), - - optionHelperText: (theme) => ({ - fontSize: 12, - color: theme.palette.text.secondary, - }), - - error: (theme) => ({ - padding: 24, - borderRadius: 8, - background: theme.palette.background.paper, - border: `1px solid ${theme.palette.error.main}`, - }), - - errorTitle: { - fontSize: 16, - margin: 0, - }, - - errorDescription: (theme) => ({ - margin: 0, - color: theme.palette.text.secondary, - marginTop: 4, - }), - - errorDetails: (theme) => ({ - display: "block", - marginTop: 8, - color: theme.palette.error.light, - fontSize: 16, - }), -} satisfies Record>; diff --git a/site/src/pages/CreateTemplatePage/CreateTemplatePage.tsx b/site/src/pages/CreateTemplatePage/CreateTemplatePage.tsx index 3722c75b1b416..712216a4f623f 100644 --- a/site/src/pages/CreateTemplatePage/CreateTemplatePage.tsx +++ b/site/src/pages/CreateTemplatePage/CreateTemplatePage.tsx @@ -1,4 +1,4 @@ -import { useState, type FC } from "react"; +import { useState, type FC, useRef } from "react"; import { Helmet } from "react-helmet-async"; import { useNavigate, useSearchParams } from "react-router-dom"; import { pageTitle } from "utils/page"; @@ -18,6 +18,7 @@ const CreateTemplatePage: FC = () => { const [isBuildLogsOpen, setIsBuildLogsOpen] = useState(false); const [templateVersion, setTemplateVersion] = useState(); const createTemplateMutation = useMutation(createTemplate()); + const variablesSectionRef = useRef(null); const onCancel = () => { navigate(-1); @@ -32,8 +33,10 @@ const CreateTemplatePage: FC = () => { }); navigate(`/templates/${template.name}/files`); }, + onOpenBuildLogsDrawer: () => setIsBuildLogsOpen(true), error: createTemplateMutation.error, isCreating: createTemplateMutation.isLoading, + variablesSectionRef, }; return ( @@ -53,9 +56,11 @@ const CreateTemplatePage: FC = () => { setIsBuildLogsOpen(false)} templateVersion={templateVersion} + variablesSectionRef={variablesSectionRef} /> ); diff --git a/site/src/pages/CreateTemplatePage/DuplicateTemplateView.tsx b/site/src/pages/CreateTemplatePage/DuplicateTemplateView.tsx index 3a3f26e4f2978..933b2ef36df39 100644 --- a/site/src/pages/CreateTemplatePage/DuplicateTemplateView.tsx +++ b/site/src/pages/CreateTemplatePage/DuplicateTemplateView.tsx @@ -18,6 +18,8 @@ import { CreateTemplatePageViewProps } from "./types"; export const DuplicateTemplateView: FC = ({ onCreateTemplate, + onOpenBuildLogsDrawer, + variablesSectionRef, error, isCreating, }) => { @@ -65,6 +67,8 @@ export const DuplicateTemplateView: FC = ({ return ( = ({ onCreateTemplate, + onOpenBuildLogsDrawer, + variablesSectionRef, error, isCreating, }) => { @@ -46,6 +48,7 @@ export const ImportStarterTemplateView: FC = ({ const missedVariables = useQuery({ ...templateVersionVariables(isJobError ? error.version.id : ""), + keepPreviousData: true, enabled: isJobError && error.job.error_code === "REQUIRED_TEMPLATE_VARIABLES", }); @@ -61,6 +64,8 @@ export const ImportStarterTemplateView: FC = ({ return ( = ({ onCreateTemplate, + onOpenBuildLogsDrawer, + variablesSectionRef, isCreating, error, }) => { @@ -42,6 +44,8 @@ export const UploadTemplateView: FC = ({ return ( Promise; + onOpenBuildLogsDrawer: () => void; + variablesSectionRef: React.RefObject; error: unknown; isCreating: boolean; }; From 8ba9ed15a39c2b9fde3fc3b678589ec6e147dc8f Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 22 Feb 2024 18:23:06 +0000 Subject: [PATCH 03/15] Refactor how version logs were consumed --- site/src/api/queries/templates.ts | 9 +- ...eVersionLogs.ts => useWatchVersionLogs.ts} | 20 ++-- .../WorkspaceBuildLogs/WorkspaceBuildLogs.tsx | 3 - .../CreateTemplatePage/BuildLogsDrawer.tsx | 96 ++++++++++++------- .../CreateTemplatePage/CreateTemplatePage.tsx | 1 + .../TemplateVersionEditor.stories.tsx | 1 - .../TemplateVersionEditor.tsx | 16 ++-- .../TemplateVersionEditorPage.tsx | 22 +---- 8 files changed, 89 insertions(+), 79 deletions(-) rename site/src/modules/templates/{useVersionLogs.ts => useWatchVersionLogs.ts} (73%) diff --git a/site/src/api/queries/templates.ts b/site/src/api/queries/templates.ts index c5c12396a8a66..8b7df37c481c1 100644 --- a/site/src/api/queries/templates.ts +++ b/site/src/api/queries/templates.ts @@ -211,6 +211,7 @@ export type CreateTemplateOptions = { version: CreateTemplateVersionRequest; template: Omit; onCreateVersion?: (version: TemplateVersion) => void; + onTemplateVersionChanges?: (version: TemplateVersion) => void; }; const createTemplateFn = async (options: CreateTemplateOptions) => { @@ -219,7 +220,7 @@ const createTemplateFn = async (options: CreateTemplateOptions) => { options.version, ); options.onCreateVersion?.(version); - await waitBuildToBeFinished(version); + await waitBuildToBeFinished(version, options.onTemplateVersionChanges); return API.createTemplate(options.organizationId, { ...options.template, template_version_id: version.id, @@ -282,12 +283,16 @@ export const previousTemplateVersion = ( }; }; -const waitBuildToBeFinished = async (version: TemplateVersion) => { +const waitBuildToBeFinished = async ( + version: TemplateVersion, + onRequest?: (data: TemplateVersion) => void, +) => { let data: TemplateVersion; let jobStatus: ProvisionerJobStatus; do { await delay(1000); data = await API.getTemplateVersion(version.id); + onRequest?.(data); jobStatus = data.job.status; if (jobStatus === "succeeded") { diff --git a/site/src/modules/templates/useVersionLogs.ts b/site/src/modules/templates/useWatchVersionLogs.ts similarity index 73% rename from site/src/modules/templates/useVersionLogs.ts rename to site/src/modules/templates/useWatchVersionLogs.ts index 64d45525e5d3b..1ae6be6143764 100644 --- a/site/src/modules/templates/useVersionLogs.ts +++ b/site/src/modules/templates/useWatchVersionLogs.ts @@ -1,12 +1,8 @@ import { watchBuildLogsByTemplateVersionId } from "api/api"; -import { - ProvisionerJobLog, - ProvisionerJobStatus, - TemplateVersion, -} from "api/typesGenerated"; +import { ProvisionerJobLog, TemplateVersion } from "api/typesGenerated"; import { useState, useEffect } from "react"; -export const useVersionLogs = ( +export const useWatchVersionLogs = ( templateVersion: TemplateVersion | undefined, options?: { onDone: () => Promise }, ) => { @@ -15,14 +11,15 @@ export const useVersionLogs = ( const templateVersionStatus = templateVersion?.job.status; useEffect(() => { - setLogs([]); - const enabledStatuses: ProvisionerJobStatus[] = ["running", "pending"]; + setLogs(undefined); + }, [templateVersionId]); + useEffect(() => { if (!templateVersionId || !templateVersionStatus) { return; } - if (!enabledStatuses.includes(templateVersionStatus)) { + if (templateVersionStatus !== "running") { return; } @@ -41,8 +38,5 @@ export const useVersionLogs = ( }; }, [options?.onDone, templateVersionId, templateVersionStatus]); - return { - logs, - setLogs, - }; + return logs; }; diff --git a/site/src/modules/workspaces/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx b/site/src/modules/workspaces/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx index f3aabf737fa68..43a85dccfcd3f 100644 --- a/site/src/modules/workspaces/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx +++ b/site/src/modules/workspaces/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx @@ -62,9 +62,6 @@ export const WorkspaceBuildLogs: FC = ({ }} {...attrs} > - {stages.length === 0 && ( -
Waiting for logs...
- )} {stages.map((stage) => { const logs = groupedLogsByStage[stage]; const isEmpty = logs.every((log) => log.output === ""); diff --git a/site/src/pages/CreateTemplatePage/BuildLogsDrawer.tsx b/site/src/pages/CreateTemplatePage/BuildLogsDrawer.tsx index 0105f6c8eeac5..5042a18709570 100644 --- a/site/src/pages/CreateTemplatePage/BuildLogsDrawer.tsx +++ b/site/src/pages/CreateTemplatePage/BuildLogsDrawer.tsx @@ -7,12 +7,11 @@ import { TemplateVersion } from "api/typesGenerated"; import { WorkspaceBuildLogs } from "modules/workspaces/WorkspaceBuildLogs/WorkspaceBuildLogs"; import { useTheme } from "@emotion/react"; import { navHeight } from "theme/constants"; -import { useVersionLogs } from "modules/templates/useVersionLogs"; +import { useWatchVersionLogs } from "modules/templates/useWatchVersionLogs"; import { JobError } from "api/queries/templates"; -import AlertTitle from "@mui/material/AlertTitle"; -import { Alert, AlertDetail } from "components/Alert/Alert"; import Button from "@mui/material/Button"; -import Collapse from "@mui/material/Collapse"; +import { Loader } from "components/Loader/Loader"; +import WarningOutlined from "@mui/icons-material/WarningOutlined"; type BuildLogsDrawerProps = { error: unknown; @@ -29,7 +28,7 @@ export const BuildLogsDrawer: FC = ({ ...drawerProps }) => { const theme = useTheme(); - const { logs } = useVersionLogs(templateVersion); + const logs = useWatchVersionLogs(templateVersion); const logsContainer = useRef(null); const scrollToBottom = () => { @@ -83,21 +82,54 @@ export const BuildLogsDrawer: FC = ({ - - +
+ +

+ Missing variables +

+

+ During the build process, we identified some missing variables. + Rest assured, we have automatically added them to the form for + you. +

- } +
+
+ ) : logs ? ( +
- Failed to create template - {isMissingVariables && error.message} - - -
- -
+ +
+ ) : ( + + )}
); diff --git a/site/src/pages/CreateTemplatePage/CreateTemplatePage.tsx b/site/src/pages/CreateTemplatePage/CreateTemplatePage.tsx index 712216a4f623f..6f260933c1505 100644 --- a/site/src/pages/CreateTemplatePage/CreateTemplatePage.tsx +++ b/site/src/pages/CreateTemplatePage/CreateTemplatePage.tsx @@ -30,6 +30,7 @@ const CreateTemplatePage: FC = () => { const template = await createTemplateMutation.mutateAsync({ ...options, onCreateVersion: setTemplateVersion, + onTemplateVersionChanges: setTemplateVersion, }); navigate(`/templates/${template.name}/files`); }, diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.stories.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.stories.tsx index 265b058e10881..853f41d2ed360 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.stories.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.stories.tsx @@ -30,7 +30,6 @@ const meta: Meta = { template: MockTemplate, templateVersion: MockTemplateVersion, defaultFileTree: MockTemplateVersionFileTree, - onPreview: action("onPreview"), onPublish: action("onPublish"), onConfirmPublish: action("onConfirmPublish"), onCancelPublish: action("onCancelPublish"), diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx index bbf605a6eda7a..879ddc3abbc76 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx @@ -67,7 +67,7 @@ export interface TemplateVersionEditorProps { resources?: WorkspaceResource[]; disablePreview?: boolean; disableUpdate?: boolean; - onPreview: (files: FileTree) => void; + onPreview: (files: FileTree) => Promise; onPublish: () => void; onConfirmPublish: (data: PublishVersionData) => void; onCancelPublish: () => void; @@ -122,14 +122,14 @@ export const TemplateVersionEditor: FC = ({ const [renameFileOpen, setRenameFileOpen] = useState(); const [dirty, setDirty] = useState(false); - const triggerPreview = useCallback(() => { - onPreview(fileTree); + const triggerPreview = useCallback(async () => { + await onPreview(fileTree); setSelectedTab("logs"); }, [fileTree, onPreview]); // Stop ctrl+s from saving files and make ctrl+enter trigger a preview. useEffect(() => { - const keyListener = (event: KeyboardEvent) => { + const keyListener = async (event: KeyboardEvent) => { if (!(navigator.platform.match("Mac") ? event.metaKey : event.ctrlKey)) { return; } @@ -140,7 +140,7 @@ export const TemplateVersionEditor: FC = ({ break; case "Enter": event.preventDefault(); - triggerPreview(); + await triggerPreview(); break; } }; @@ -252,8 +252,8 @@ export const TemplateVersionEditor: FC = ({ } title="Build template (Ctrl + Enter)" disabled={disablePreview} - onClick={() => { - triggerPreview(); + onClick={async () => { + await triggerPreview(); }} > Build @@ -719,7 +719,7 @@ const styles = { // Hack to update logs header and lines "& .logs-header": { border: 0, - padding: "0 16px", + padding: "8px 16px", fontFamily: MONOSPACE_FONT_FAMILY, "&:first-of-type": { diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx index 14a5b9468d6a0..c52d30b731ad8 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx @@ -23,7 +23,7 @@ import { FileTree, traverse } from "utils/filetree"; import { createTemplateVersionFileTree } from "utils/templateVersion"; import { displayError } from "components/GlobalSnackbar/utils"; import { FullScreenLoader } from "components/Loader/FullScreenLoader"; -import { useVersionLogs } from "modules/templates/useVersionLogs"; +import { useWatchVersionLogs } from "modules/templates/useWatchVersionLogs"; type Params = { version: string; @@ -54,7 +54,7 @@ export const TemplateVersionEditorPage: FC = () => { ...resources(templateVersionQuery.data?.id ?? ""), enabled: templateVersionQuery.data?.job.status === "succeeded", }); - const { logs, setLogs } = useVersionLogs(templateVersionQuery.data, { + const logs = useWatchVersionLogs(templateVersionQuery.data, { onDone: templateVersionQuery.refetch, }); const { fileTree, tarFile } = useFileTree(templateVersionQuery.data); @@ -93,22 +93,6 @@ export const TemplateVersionEditorPage: FC = () => { ); }; - // Optimistically update the template version data job status to make the - // build action feels faster - const onBuildStart = () => { - setLogs([]); - - queryClient.setQueryData(templateVersionOptions.queryKey, () => { - return { - ...templateVersionQuery.data, - job: { - ...templateVersionQuery.data?.job, - status: "pending", - }, - }; - }); - }; - const onBuildEnds = (newVersion: TemplateVersion) => { queryClient.setQueryData(templateVersionOptions.queryKey, newVersion); navigateToVersion(newVersion); @@ -141,7 +125,6 @@ export const TemplateVersionEditorPage: FC = () => { if (!tarFile) { return; } - onBuildStart(); const newVersionFile = await generateVersionFiles( tarFile, newFileTree, @@ -214,7 +197,6 @@ export const TemplateVersionEditorPage: FC = () => { if (!uploadFileMutation.data) { return; } - onBuildStart(); const newVersion = await createTemplateVersionMutation.mutateAsync({ provisioner: "terraform", storage_method: "file", From 2de86acf0cf8f139d74e8b99e7af8a06f7b4e6e7 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 22 Feb 2024 18:29:46 +0000 Subject: [PATCH 04/15] Decrease poll time when pending --- site/src/api/queries/templates.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/site/src/api/queries/templates.ts b/site/src/api/queries/templates.ts index 8b7df37c481c1..532ba0c9f7f22 100644 --- a/site/src/api/queries/templates.ts +++ b/site/src/api/queries/templates.ts @@ -288,9 +288,10 @@ const waitBuildToBeFinished = async ( onRequest?: (data: TemplateVersion) => void, ) => { let data: TemplateVersion; - let jobStatus: ProvisionerJobStatus; + let jobStatus: ProvisionerJobStatus | undefined = undefined; do { - await delay(1000); + // When pending we want to poll more frequently + await delay(jobStatus === "pending" ? 250 : 1000); data = await API.getTemplateVersion(version.id); onRequest?.(data); jobStatus = data.job.status; From 5eb9301353b4cc7c526fb9680566f1da46f2a0ec Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 22 Feb 2024 18:37:28 +0000 Subject: [PATCH 05/15] Fix enable update --- .../TemplateVersionEditorPage/TemplateVersionEditorPage.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx index c52d30b731ad8..051ef50f439ed 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx @@ -186,8 +186,9 @@ export const TemplateVersionEditorPage: FC = () => { } disableUpdate={ templateVersionQuery.data.job.status !== "succeeded" || + !lastSuccessfulPublishedVersion || templateVersionQuery.data.name === - lastSuccessfulPublishedVersion?.name + lastSuccessfulPublishedVersion.name } resources={resourcesQuery.data} buildLogs={logs} From 485ae40894cebcce5962a904dc8ad307c816cbbc Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Fri, 23 Feb 2024 08:02:41 -0300 Subject: [PATCH 06/15] Apply suggestions from code review Co-authored-by: Kayla Washburn-Love --- site/src/api/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 256ff9d1313e7..4acd2b3f155a9 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1575,7 +1575,7 @@ export const watchBuildLogsByTemplateVersionId = ( }); socket.addEventListener("close", () => { // When the socket closes, logs have finished streaming! - onDone && onDone(); + onDone?.(); }); return socket; }; From 9ffe1a3b2619fe469524ae64236af3572ea1ee04 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 23 Feb 2024 11:38:24 +0000 Subject: [PATCH 07/15] Apply minor improvements --- .../workspaces/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx | 2 +- .../TemplateVersionEditorPage/TemplateVersionEditorPage.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/site/src/modules/workspaces/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx b/site/src/modules/workspaces/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx index 43a85dccfcd3f..3e9490ba1cbd1 100644 --- a/site/src/modules/workspaces/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx +++ b/site/src/modules/workspaces/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx @@ -112,7 +112,7 @@ const styles = { borderRadius: "0 0 8px 8px", }, - "&:first-child": { + "&:first-of-type": { borderRadius: "8px 8px 0 0", }, }), diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx index 051ef50f439ed..9a96c1ae1798b 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx @@ -138,6 +138,7 @@ export const TemplateVersionEditorPage: FC = () => { template_id: templateQuery.data.id, file_id: serverFile.hash, }); + onBuildEnds(newVersion); }} onPublish={() => { @@ -186,9 +187,8 @@ export const TemplateVersionEditorPage: FC = () => { } disableUpdate={ templateVersionQuery.data.job.status !== "succeeded" || - !lastSuccessfulPublishedVersion || templateVersionQuery.data.name === - lastSuccessfulPublishedVersion.name + lastSuccessfulPublishedVersion?.name } resources={resourcesQuery.data} buildLogs={logs} From 76b52875cd2c01f6c3b981def7a211be6123649f Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 23 Feb 2024 11:47:32 +0000 Subject: [PATCH 08/15] Add upload file handler --- site/src/testHelpers/handlers.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/site/src/testHelpers/handlers.ts b/site/src/testHelpers/handlers.ts index 61f33d5cc1ac2..604695977d7b0 100644 --- a/site/src/testHelpers/handlers.ts +++ b/site/src/testHelpers/handlers.ts @@ -373,6 +373,15 @@ export const handlers = [ }, ), + rest.post("/api/v2/files", (_, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + hash: "some-file-hash", + }), + ); + }), + rest.get("/api/v2/files/:fileId", (_, res, ctx) => { const fileBuffer = fs.readFileSync( path.resolve(__dirname, "./templateFiles.tar"), From c4f4a6f9a48d6143a8aaf0f5d23b7cd7052b3c18 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 23 Feb 2024 12:08:34 +0000 Subject: [PATCH 09/15] Fix template version editor page tests --- .../TemplateVersionEditorPage.test.tsx | 108 +++++++----------- 1 file changed, 41 insertions(+), 67 deletions(-) diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx index f6792478760f6..6fbd90ccf3ac5 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx @@ -4,7 +4,7 @@ import { } from "testHelpers/renderHelpers"; import TemplateVersionEditorPage from "./TemplateVersionEditorPage"; import { render, screen, waitFor, within } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; +import userEvent, { UserEvent } from "@testing-library/user-event"; import * as api from "api/api"; import { MockTemplate, @@ -21,6 +21,7 @@ import { RequireAuth } from "contexts/auth/RequireAuth"; import { server } from "testHelpers/server"; import { rest } from "msw"; import { AppProviders } from "App"; +import { TemplateVersion } from "api/typesGenerated"; // For some reason this component in Jest is throwing a MUI style warning so, // since we don't need it for this test, we can mock it out @@ -50,35 +51,50 @@ const renderTemplateEditorPage = () => { }); }; -test("Use custom name, message and set it as active when publishing", async () => { - const user = userEvent.setup(); - renderTemplateEditorPage(); - const topbar = await screen.findByTestId("topbar"); - - // Build Template +const buildTemplateVersion = async ( + templateVersion: TemplateVersion, + user: UserEvent, + topbar: HTMLElement, +) => { jest.spyOn(api, "uploadFile").mockResolvedValueOnce({ hash: "hash" }); - const newTemplateVersion = { - ...MockTemplateVersion, - id: "new-version-id", - name: "new-version", - }; - jest - .spyOn(api, "createTemplateVersion") - .mockResolvedValue(newTemplateVersion); + jest.spyOn(api, "createTemplateVersion").mockResolvedValue({ + ...templateVersion, + job: { + ...templateVersion.job, + status: "running", + }, + }); jest .spyOn(api, "getTemplateVersionByName") - .mockResolvedValue(newTemplateVersion); + .mockResolvedValue(templateVersion); jest .spyOn(api, "watchBuildLogsByTemplateVersionId") .mockImplementation((_, options) => { options.onMessage(MockWorkspaceBuildLogs[0]); options.onDone?.(); - return jest.fn() as never; + const wsMock = { + close: jest.fn(), + } as unknown; + return wsMock as WebSocket; }); const buildButton = within(topbar).getByRole("button", { name: "Build", }); await user.click(buildButton); + await within(topbar).findByText("Success"); +}; + +test("Use custom name, message and set it as active when publishing", async () => { + const user = userEvent.setup(); + renderTemplateEditorPage(); + const topbar = await screen.findByTestId("topbar"); + + const newTemplateVersion: TemplateVersion = { + ...MockTemplateVersion, + id: "new-version-id", + name: "new-version", + }; + await buildTemplateVersion(newTemplateVersion, user, topbar); // Publish const patchTemplateVersion = jest @@ -87,7 +103,6 @@ test("Use custom name, message and set it as active when publishing", async () = const updateActiveTemplateVersion = jest .spyOn(api, "updateActiveTemplateVersion") .mockResolvedValue({ message: "" }); - await within(topbar).findByText("Success"); const publishButton = within(topbar).getByRole("button", { name: "Publish", }); @@ -118,30 +133,12 @@ test("Do not mark as active if promote is not checked", async () => { renderTemplateEditorPage(); const topbar = await screen.findByTestId("topbar"); - // Build Template - jest.spyOn(api, "uploadFile").mockResolvedValueOnce({ hash: "hash" }); const newTemplateVersion = { ...MockTemplateVersion, id: "new-version-id", name: "new-version", }; - jest - .spyOn(api, "createTemplateVersion") - .mockResolvedValue(newTemplateVersion); - jest - .spyOn(api, "getTemplateVersionByName") - .mockResolvedValue(newTemplateVersion); - jest - .spyOn(api, "watchBuildLogsByTemplateVersionId") - .mockImplementation((_, options) => { - options.onMessage(MockWorkspaceBuildLogs[0]); - options.onDone?.(); - return jest.fn() as never; - }); - const buildButton = within(topbar).getByRole("button", { - name: "Build", - }); - await user.click(buildButton); + await buildTemplateVersion(newTemplateVersion, user, topbar); // Publish const patchTemplateVersion = jest @@ -150,7 +147,6 @@ test("Do not mark as active if promote is not checked", async () => { const updateActiveTemplateVersion = jest .spyOn(api, "updateActiveTemplateVersion") .mockResolvedValue({ message: "" }); - await within(topbar).findByText("Success"); const publishButton = within(topbar).getByRole("button", { name: "Publish", }); @@ -175,44 +171,22 @@ test("Do not mark as active if promote is not checked", async () => { }); test("Patch request is not send when there are no changes", async () => { + const user = userEvent.setup(); + renderTemplateEditorPage(); + const topbar = await screen.findByTestId("topbar"); + const newTemplateVersion = { ...MockTemplateVersion, id: "new-version-id", name: "new-version", - }; - const MockTemplateVersionWithEmptyMessage = { - ...newTemplateVersion, message: "", }; - const user = userEvent.setup(); - renderTemplateEditorPage(); - const topbar = await screen.findByTestId("topbar"); - - // Build Template - jest.spyOn(api, "uploadFile").mockResolvedValueOnce({ hash: "hash" }); - jest - .spyOn(api, "createTemplateVersion") - .mockResolvedValue(MockTemplateVersionWithEmptyMessage); - jest - .spyOn(api, "getTemplateVersionByName") - .mockResolvedValue(MockTemplateVersionWithEmptyMessage); - jest - .spyOn(api, "watchBuildLogsByTemplateVersionId") - .mockImplementation((_, options) => { - options.onMessage(MockWorkspaceBuildLogs[0]); - options.onDone?.(); - return jest.fn() as never; - }); - const buildButton = within(topbar).getByRole("button", { - name: "Build", - }); - await user.click(buildButton); + await buildTemplateVersion(newTemplateVersion, user, topbar); // Publish const patchTemplateVersion = jest .spyOn(api, "patchTemplateVersion") - .mockResolvedValue(MockTemplateVersionWithEmptyMessage); - await within(topbar).findByText("Success"); + .mockResolvedValue(newTemplateVersion); const publishButton = within(topbar).getByRole("button", { name: "Publish", }); @@ -220,7 +194,7 @@ test("Patch request is not send when there are no changes", async () => { const publishDialog = await screen.findByTestId("dialog"); // It is using the name from the template const nameField = within(publishDialog).getByLabelText("Version name"); - expect(nameField).toHaveValue(MockTemplateVersionWithEmptyMessage.name); + expect(nameField).toHaveValue(newTemplateVersion.name); // Publish await user.click( within(publishDialog).getByRole("button", { name: "Publish" }), From 09e1be585451c1cf5ca35449838a64bd7c2bc7c6 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 23 Feb 2024 12:22:01 +0000 Subject: [PATCH 10/15] Fix create template page test --- site/jest.setup.ts | 2 ++ .../pages/CreateTemplatePage/CreateTemplatePage.test.tsx | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/site/jest.setup.ts b/site/jest.setup.ts index 2a17c9dc9a62b..c7c0e4f814ce8 100644 --- a/site/jest.setup.ts +++ b/site/jest.setup.ts @@ -49,6 +49,8 @@ global.TextDecoder = TextDecoder as any; global.Blob = Blob as any; global.scrollTo = jest.fn(); +window.HTMLElement.prototype.scrollIntoView = function () {}; + // Polyfill the getRandomValues that is used on utils/random.ts Object.defineProperty(global.self, "crypto", { value: { diff --git a/site/src/pages/CreateTemplatePage/CreateTemplatePage.test.tsx b/site/src/pages/CreateTemplatePage/CreateTemplatePage.test.tsx index 9301701365ec0..19883db6487a5 100644 --- a/site/src/pages/CreateTemplatePage/CreateTemplatePage.test.tsx +++ b/site/src/pages/CreateTemplatePage/CreateTemplatePage.test.tsx @@ -19,7 +19,7 @@ const renderPage = async (searchParams: URLSearchParams) => { route: `/templates/new?${searchParams.toString()}`, path: "/templates/new", // We need this because after creation, the user will be redirected to here - extraRoutes: [{ path: "templates/:template", element: <> }], + extraRoutes: [{ path: "templates/:template/files", element: <> }], }); // It is lazy loaded, so we have to wait for it to be rendered to not get an // act error @@ -62,6 +62,12 @@ test("Create template from starter template", async () => { within(form).getByRole("button", { name: /create template/i }), ); + // Wait for the drawer error to be rendered + await screen.findByRole("heading", { name: /missing variables/i }); + await userEvent.click( + screen.getByRole("button", { name: /fill variables/i }), + ); + // Wait for the variables form to be rendered and fill it await screen.findByText(/Variables/, undefined, { timeout: 5_000 }); From c118fd21e552b02fefa1acae233ffd58e2bc85f1 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 23 Feb 2024 13:28:11 +0000 Subject: [PATCH 11/15] Apply review suggestions --- site/src/components/Form/Form.tsx | 61 ++++-- .../modules/templates/useWatchVersionLogs.ts | 2 +- .../CreateTemplatePage/BuildLogsDrawer.tsx | 205 ++++++++++-------- 3 files changed, 148 insertions(+), 120 deletions(-) diff --git a/site/src/components/Form/Form.tsx b/site/src/components/Form/Form.tsx index add434c84e044..fcbc27a2511f3 100644 --- a/site/src/components/Form/Form.tsx +++ b/site/src/components/Form/Form.tsx @@ -95,37 +95,21 @@ export const FormSection = forwardRef( ref, ) => { const { direction } = useContext(FormContext); - const theme = useTheme(); return (

@@ -154,6 +138,35 @@ export const FormFields: FC> = (props) => { }; const styles = { + formSection: (theme) => ({ + display: "flex", + alignItems: "flex-start", + flexDirection: "column", + gap: 24, + + [theme.breakpoints.down("md")]: { + flexDirection: "column", + gap: 16, + }, + }), + formSectionHorizontal: { + flexDirection: "row", + gap: 120, + }, + formSectionInfo: (theme) => ({ + width: "100%", + flexShrink: 0, + top: 24, + + [theme.breakpoints.down("md")]: { + width: "100%", + position: "initial" as const, + }, + }), + formSectionInfoHorizontal: { + maxWidth: 312, + position: "sticky", + }, formSectionInfoTitle: (theme) => ({ fontSize: 20, color: theme.palette.text.primary, diff --git a/site/src/modules/templates/useWatchVersionLogs.ts b/site/src/modules/templates/useWatchVersionLogs.ts index 1ae6be6143764..62ac4a3a16412 100644 --- a/site/src/modules/templates/useWatchVersionLogs.ts +++ b/site/src/modules/templates/useWatchVersionLogs.ts @@ -6,7 +6,7 @@ export const useWatchVersionLogs = ( templateVersion: TemplateVersion | undefined, options?: { onDone: () => Promise }, ) => { - const [logs, setLogs] = useState(); + const [logs, setLogs] = useState(); const templateVersionId = templateVersion?.id; const templateVersionStatus = templateVersion?.job.status; diff --git a/site/src/pages/CreateTemplatePage/BuildLogsDrawer.tsx b/site/src/pages/CreateTemplatePage/BuildLogsDrawer.tsx index 5042a18709570..65283b640bae0 100644 --- a/site/src/pages/CreateTemplatePage/BuildLogsDrawer.tsx +++ b/site/src/pages/CreateTemplatePage/BuildLogsDrawer.tsx @@ -2,10 +2,10 @@ import Drawer from "@mui/material/Drawer"; import Close from "@mui/icons-material/Close"; import IconButton from "@mui/material/IconButton"; import { visuallyHidden } from "@mui/utils"; -import { FC, useEffect, useRef } from "react"; +import { FC, useLayoutEffect, useRef } from "react"; import { TemplateVersion } from "api/typesGenerated"; import { WorkspaceBuildLogs } from "modules/workspaces/WorkspaceBuildLogs/WorkspaceBuildLogs"; -import { useTheme } from "@emotion/react"; +import { Interpolation, Theme } from "@emotion/react"; import { navHeight } from "theme/constants"; import { useWatchVersionLogs } from "modules/templates/useWatchVersionLogs"; import { JobError } from "api/queries/templates"; @@ -27,7 +27,6 @@ export const BuildLogsDrawer: FC = ({ variablesSectionRef, ...drawerProps }) => { - const theme = useTheme(); const logs = useWatchVersionLogs(templateVersion); const logsContainer = useRef(null); @@ -39,11 +38,11 @@ export const BuildLogsDrawer: FC = ({ }, 0); }; - useEffect(() => { + useLayoutEffect(() => { scrollToBottom(); }, [logs]); - useEffect(() => { + useLayoutEffect(() => { if (drawerProps.open) { scrollToBottom(); } @@ -55,104 +54,29 @@ export const BuildLogsDrawer: FC = ({ return ( -
-
-

- Creating template... -

+
+
+

Creating template...

- + Close build logs
{isMissingVariables ? ( -
{ + variablesSectionRef.current?.scrollIntoView({ + behavior: "smooth", + }); + const firstVariableInput = + variablesSectionRef.current?.querySelector("input"); + setTimeout(() => firstVariableInput?.focus(), 0); + drawerProps.onClose(); }} - > -
- -

- Missing variables -

-

- During the build process, we identified some missing variables. - Rest assured, we have automatically added them to the form for - you. -

- -
-
+ /> ) : logs ? ( -
+
) : ( @@ -162,3 +86,94 @@ export const BuildLogsDrawer: FC = ({ ); }; + +const MissingVariablesBanner: FC<{ onFillVariables: () => void }> = ({ + onFillVariables, +}) => { + return ( +
+
+ +

Missing variables

+

+ During the build process, we identified some missing variables. Rest + assured, we have automatically added them to the form for you. +

+ +
+
+ ); +}; + +const styles = { + root: { + width: 800, + height: "100%", + display: "flex", + flexDirection: "column", + }, + header: (theme) => ({ + height: navHeight, + padding: "0 24px", + display: "flex", + alignItems: "center", + justifyContent: "space-between", + borderBottom: `1px solid ${theme.palette.divider}`, + }), + title: { + margin: 0, + fontWeight: 500, + fontSize: 16, + }, + closeIcon: { + fontSize: 20, + }, + logs: (theme) => ({ + flex: 1, + overflow: "auto", + backgroundColor: theme.palette.background.default, + }), +} satisfies Record>; + +const bannerStyles = { + root: { + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: 40, + }, + content: { + display: "flex", + flexDirection: "column", + alignItems: "center", + textAlign: "center", + maxWidth: 360, + }, + icon: (theme) => ({ + fontSize: 32, + color: theme.roles.warning.fill.outline, + }), + title: { + fontWeight: 500, + lineHeight: "1", + margin: 0, + marginTop: 16, + }, + description: (theme) => ({ + color: theme.palette.text.secondary, + fontSize: 14, + margin: 0, + marginTop: 8, + lineHeight: "1.5", + }), + button: { + marginTop: 16, + }, +} satisfies Record>; From 245101f05a7d909f4d6df8b61dbd1801a9cec22b Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 23 Feb 2024 14:21:03 +0000 Subject: [PATCH 12/15] Add storybook --- site/src/@types/storybook.d.ts | 1 + .../modules/templates/useWatchVersionLogs.ts | 2 + .../BuildLogsDrawer.stories.tsx | 51 +++++++++++++++++++ site/src/testHelpers/storybook.tsx | 25 +++++++++ 4 files changed, 79 insertions(+) create mode 100644 site/src/pages/CreateTemplatePage/BuildLogsDrawer.stories.tsx diff --git a/site/src/@types/storybook.d.ts b/site/src/@types/storybook.d.ts index 8a5b490987860..be93262081f7f 100644 --- a/site/src/@types/storybook.d.ts +++ b/site/src/@types/storybook.d.ts @@ -7,5 +7,6 @@ declare module "@storybook/react" { features?: FeatureName[]; experiments?: Experiments; queries?: { key: QueryKey; data: unknown }[]; + messages?: string[]; } } diff --git a/site/src/modules/templates/useWatchVersionLogs.ts b/site/src/modules/templates/useWatchVersionLogs.ts index 62ac4a3a16412..f2929c2e59dbc 100644 --- a/site/src/modules/templates/useWatchVersionLogs.ts +++ b/site/src/modules/templates/useWatchVersionLogs.ts @@ -33,6 +33,8 @@ export const useWatchVersionLogs = ( }, }); + console.log("Get logs", socket); + return () => { socket.close(); }; diff --git a/site/src/pages/CreateTemplatePage/BuildLogsDrawer.stories.tsx b/site/src/pages/CreateTemplatePage/BuildLogsDrawer.stories.tsx new file mode 100644 index 0000000000000..c1bd283588023 --- /dev/null +++ b/site/src/pages/CreateTemplatePage/BuildLogsDrawer.stories.tsx @@ -0,0 +1,51 @@ +import { JobError } from "api/queries/templates"; +import { BuildLogsDrawer } from "./BuildLogsDrawer"; +import type { Meta, StoryObj } from "@storybook/react"; +import { + MockProvisionerJob, + MockTemplateVersion, + MockWorkspaceBuildLogs, +} from "testHelpers/entities"; +import { withWSMessages } from "testHelpers/storybook"; + +const meta: Meta = { + title: "pages/CreateTemplatePage/BuildLogsDrawer", + component: BuildLogsDrawer, + args: { + open: true, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Loading: Story = {}; + +export const MissingVariables: Story = { + args: { + templateVersion: MockTemplateVersion, + error: new JobError( + { + ...MockProvisionerJob, + error_code: "REQUIRED_TEMPLATE_VARIABLES", + }, + MockTemplateVersion, + ), + }, +}; + +export const Logs: Story = { + args: { + templateVersion: { + ...MockTemplateVersion, + job: { + ...MockTemplateVersion.job, + status: "running", + }, + }, + }, + decorators: [withWSMessages], + parameters: { + messages: MockWorkspaceBuildLogs.map((log) => JSON.stringify(log)), + }, +}; diff --git a/site/src/testHelpers/storybook.tsx b/site/src/testHelpers/storybook.tsx index 3f8539d6d513b..08198c0099257 100644 --- a/site/src/testHelpers/storybook.tsx +++ b/site/src/testHelpers/storybook.tsx @@ -44,3 +44,28 @@ export const withDashboardProvider = ( ); }; + +export const withWSMessages = (Story: FC, { parameters }: StoryContext) => { + if (!parameters.messages) { + console.warn("Looks like you forgot to add messages to the story"); + } + + // @ts-expect-error -- TS doesn't know about the global WebSocket + window.WebSocket = function () { + return { + addEventListener: ( + type: string, + callback: (ev: Record<"data", string>) => void, + ) => { + if (type === "message") { + parameters.messages?.forEach((message) => { + callback({ data: message }); + }); + } + }, + close: () => {}, + }; + }; + + return ; +}; From c56f2dd94fc9f7b22975b368e187f90559a95071 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 23 Feb 2024 14:24:31 +0000 Subject: [PATCH 13/15] Improve ws decorator --- site/src/@types/storybook.d.ts | 4 +++- .../CreateTemplatePage/BuildLogsDrawer.stories.tsx | 8 +++++--- site/src/testHelpers/storybook.tsx | 10 ++++++---- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/site/src/@types/storybook.d.ts b/site/src/@types/storybook.d.ts index be93262081f7f..8aada49bf9e3f 100644 --- a/site/src/@types/storybook.d.ts +++ b/site/src/@types/storybook.d.ts @@ -7,6 +7,8 @@ declare module "@storybook/react" { features?: FeatureName[]; experiments?: Experiments; queries?: { key: QueryKey; data: unknown }[]; - messages?: string[]; + webSocket?: { + messages: string[]; + }; } } diff --git a/site/src/pages/CreateTemplatePage/BuildLogsDrawer.stories.tsx b/site/src/pages/CreateTemplatePage/BuildLogsDrawer.stories.tsx index c1bd283588023..8ecd2fe712b6a 100644 --- a/site/src/pages/CreateTemplatePage/BuildLogsDrawer.stories.tsx +++ b/site/src/pages/CreateTemplatePage/BuildLogsDrawer.stories.tsx @@ -6,7 +6,7 @@ import { MockTemplateVersion, MockWorkspaceBuildLogs, } from "testHelpers/entities"; -import { withWSMessages } from "testHelpers/storybook"; +import { withWebSocket } from "testHelpers/storybook"; const meta: Meta = { title: "pages/CreateTemplatePage/BuildLogsDrawer", @@ -44,8 +44,10 @@ export const Logs: Story = { }, }, }, - decorators: [withWSMessages], + decorators: [withWebSocket], parameters: { - messages: MockWorkspaceBuildLogs.map((log) => JSON.stringify(log)), + webSocket: { + messages: MockWorkspaceBuildLogs.map((log) => JSON.stringify(log)), + }, }, }; diff --git a/site/src/testHelpers/storybook.tsx b/site/src/testHelpers/storybook.tsx index 08198c0099257..47bea51065cf6 100644 --- a/site/src/testHelpers/storybook.tsx +++ b/site/src/testHelpers/storybook.tsx @@ -45,9 +45,11 @@ export const withDashboardProvider = ( ); }; -export const withWSMessages = (Story: FC, { parameters }: StoryContext) => { - if (!parameters.messages) { - console.warn("Looks like you forgot to add messages to the story"); +export const withWebSocket = (Story: FC, { parameters }: StoryContext) => { + if (!parameters.webSocket) { + console.warn( + "Looks like you forgot to add websocket messages to the story", + ); } // @ts-expect-error -- TS doesn't know about the global WebSocket @@ -58,7 +60,7 @@ export const withWSMessages = (Story: FC, { parameters }: StoryContext) => { callback: (ev: Record<"data", string>) => void, ) => { if (type === "message") { - parameters.messages?.forEach((message) => { + parameters.webSocket?.messages.forEach((message) => { callback({ data: message }); }); } From b2bc71b5905e788942e51391bf1caa96815e2260 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 23 Feb 2024 14:29:41 +0000 Subject: [PATCH 14/15] Remove console.log --- site/src/modules/templates/useWatchVersionLogs.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/site/src/modules/templates/useWatchVersionLogs.ts b/site/src/modules/templates/useWatchVersionLogs.ts index f2929c2e59dbc..62ac4a3a16412 100644 --- a/site/src/modules/templates/useWatchVersionLogs.ts +++ b/site/src/modules/templates/useWatchVersionLogs.ts @@ -33,8 +33,6 @@ export const useWatchVersionLogs = ( }, }); - console.log("Get logs", socket); - return () => { socket.close(); }; From d7aa79a03e759502d496de8a36285d4f89a350f0 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 23 Feb 2024 14:36:35 +0000 Subject: [PATCH 15/15] Remove old story --- .../CreateTemplateForm.stories.tsx | 319 +----------------- 1 file changed, 1 insertion(+), 318 deletions(-) diff --git a/site/src/pages/CreateTemplatePage/CreateTemplateForm.stories.tsx b/site/src/pages/CreateTemplatePage/CreateTemplateForm.stories.tsx index eb0c8d9a5c847..583a3e8cc4172 100644 --- a/site/src/pages/CreateTemplatePage/CreateTemplateForm.stories.tsx +++ b/site/src/pages/CreateTemplatePage/CreateTemplateForm.stories.tsx @@ -11,7 +11,7 @@ import { CreateTemplateForm } from "./CreateTemplateForm"; import type { Meta, StoryObj } from "@storybook/react"; const meta: Meta = { - title: "pages/CreateTemplatePage", + title: "pages/CreateTemplatePage/CreateTemplateForm", component: CreateTemplateForm, args: { isSubmitting: false, @@ -50,320 +50,3 @@ export const DuplicateTemplateWithVariables: Story = { ], }, }; - -export const WithJobError: Story = { - args: { - copiedTemplate: MockTemplate, - jobError: - "template import provision for start: recv import provision: plan terraform: terraform plan: exit status 1", - logs: [ - { - id: 461061, - created_at: "2023-03-06T14:47:32.501Z", - log_source: "provisioner_daemon", - log_level: "info", - stage: "Adding README.md...", - output: "", - }, - { - id: 461062, - created_at: "2023-03-06T14:47:32.501Z", - log_source: "provisioner_daemon", - log_level: "info", - stage: "Setting up", - output: "", - }, - { - id: 461063, - created_at: "2023-03-06T14:47:32.528Z", - log_source: "provisioner_daemon", - log_level: "info", - stage: "Parsing template parameters", - output: "", - }, - { - id: 461064, - created_at: "2023-03-06T14:47:32.552Z", - log_source: "provisioner_daemon", - log_level: "info", - stage: "Detecting persistent resources", - output: "", - }, - { - id: 461065, - created_at: "2023-03-06T14:47:32.633Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: "", - }, - { - id: 461066, - created_at: "2023-03-06T14:47:32.633Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: "Initializing the backend...", - }, - { - id: 461067, - created_at: "2023-03-06T14:47:32.71Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: "", - }, - { - id: 461068, - created_at: "2023-03-06T14:47:32.711Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: "Initializing provider plugins...", - }, - { - id: 461069, - created_at: "2023-03-06T14:47:32.712Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: '- Finding coder/coder versions matching "~\u003e 0.6.12"...', - }, - { - id: 461070, - created_at: "2023-03-06T14:47:32.922Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: '- Finding hashicorp/aws versions matching "~\u003e 4.55"...', - }, - { - id: 461071, - created_at: "2023-03-06T14:47:33.132Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: "- Installing hashicorp/aws v4.57.0...", - }, - { - id: 461072, - created_at: "2023-03-06T14:47:37.364Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: "- Installed hashicorp/aws v4.57.0 (signed by HashiCorp)", - }, - { - id: 461073, - created_at: "2023-03-06T14:47:38.142Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: "- Installing coder/coder v0.6.15...", - }, - { - id: 461074, - created_at: "2023-03-06T14:47:39.083Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: - "- Installed coder/coder v0.6.15 (signed by a HashiCorp partner, key ID 93C75807601AA0EC)", - }, - { - id: 461075, - created_at: "2023-03-06T14:47:39.394Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: "", - }, - { - id: 461076, - created_at: "2023-03-06T14:47:39.394Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: - "Partner and community providers are signed by their developers.", - }, - { - id: 461077, - created_at: "2023-03-06T14:47:39.394Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: - "If you'd like to know more about provider signing, you can read about it here:", - }, - { - id: 461078, - created_at: "2023-03-06T14:47:39.394Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: "https://www.terraform.io/docs/cli/plugins/signing.html", - }, - { - id: 461079, - created_at: "2023-03-06T14:47:39.394Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: "", - }, - { - id: 461080, - created_at: "2023-03-06T14:47:39.394Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: - "Terraform has created a lock file .terraform.lock.hcl to record the provider", - }, - { - id: 461081, - created_at: "2023-03-06T14:47:39.394Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: - "selections it made above. Include this file in your version control repository", - }, - { - id: 461082, - created_at: "2023-03-06T14:47:39.394Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: - "so that Terraform can guarantee to make the same selections by default when", - }, - { - id: 461083, - created_at: "2023-03-06T14:47:39.395Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: 'you run "terraform init" in the future.', - }, - { - id: 461084, - created_at: "2023-03-06T14:47:39.395Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: "", - }, - { - id: 461085, - created_at: "2023-03-06T14:47:39.395Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: "Terraform has been successfully initialized!", - }, - { - id: 461086, - created_at: "2023-03-06T14:47:39.395Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: "", - }, - { - id: 461087, - created_at: "2023-03-06T14:47:39.395Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: - 'You may now begin working with Terraform. Try running "terraform plan" to see', - }, - { - id: 461088, - created_at: "2023-03-06T14:47:39.395Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: - "any changes that are required for your infrastructure. All Terraform commands", - }, - { - id: 461089, - created_at: "2023-03-06T14:47:39.395Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: "should now work.", - }, - { - id: 461090, - created_at: "2023-03-06T14:47:39.397Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: "", - }, - { - id: 461091, - created_at: "2023-03-06T14:47:39.397Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: - "If you ever set or change modules or backend configuration for Terraform,", - }, - { - id: 461092, - created_at: "2023-03-06T14:47:39.397Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: - "rerun this command to reinitialize your working directory. If you forget, other", - }, - { - id: 461093, - created_at: "2023-03-06T14:47:39.397Z", - log_source: "provisioner", - log_level: "debug", - stage: "Detecting persistent resources", - output: "commands will detect it and remind you to do so if necessary.", - }, - { - id: 461094, - created_at: "2023-03-06T14:47:39.431Z", - log_source: "provisioner", - log_level: "info", - stage: "Detecting persistent resources", - output: "Terraform 1.1.9", - }, - { - id: 461095, - created_at: "2023-03-06T14:47:43.759Z", - log_source: "provisioner", - log_level: "error", - stage: "Detecting persistent resources", - output: - "Error: configuring Terraform AWS Provider: no valid credential sources for Terraform AWS Provider found.\n\nPlease see https://registry.terraform.io/providers/hashicorp/aws\nfor more information about providing credentials.\n\nError: failed to refresh cached credentials, no EC2 IMDS role found, operation error ec2imds: GetMetadata, http response error StatusCode: 404, request to EC2 IMDS failed\n", - }, - { - id: 461096, - created_at: "2023-03-06T14:47:43.759Z", - log_source: "provisioner", - log_level: "error", - stage: "Detecting persistent resources", - output: "", - }, - { - id: 461097, - created_at: "2023-03-06T14:47:43.777Z", - log_source: "provisioner_daemon", - log_level: "info", - stage: "Cleaning Up", - output: "", - }, - ], - }, -};