Skip to content

Commit 2309bc4

Browse files
committed
Show build logs during template creation
1 parent 390217b commit 2309bc4

12 files changed

+213
-121
lines changed

site/src/api/api.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1502,7 +1502,7 @@ export const watchAgentMetadata = (agentId: string): EventSource => {
15021502
type WatchBuildLogsByTemplateVersionIdOptions = {
15031503
after?: number;
15041504
onMessage: (log: TypesGen.ProvisionerJobLog) => void;
1505-
onDone: () => void;
1505+
onDone?: () => void;
15061506
onError: (error: Error) => void;
15071507
};
15081508
export const watchBuildLogsByTemplateVersionId = (
@@ -1534,7 +1534,7 @@ export const watchBuildLogsByTemplateVersionId = (
15341534
});
15351535
socket.addEventListener("close", () => {
15361536
// When the socket closes, logs have finished streaming!
1537-
onDone();
1537+
onDone && onDone();
15381538
});
15391539
return socket;
15401540
};

site/src/api/queries/templates.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,15 +206,19 @@ export const createTemplate = () => {
206206
};
207207
};
208208

209-
const createTemplateFn = async (options: {
209+
export type CreateTemplateOptions = {
210210
organizationId: string;
211211
version: CreateTemplateVersionRequest;
212212
template: Omit<CreateTemplateRequest, "template_version_id">;
213-
}) => {
213+
onCreateVersion?: (version: TemplateVersion) => void;
214+
};
215+
216+
const createTemplateFn = async (options: CreateTemplateOptions) => {
214217
const version = await API.createTemplateVersion(
215218
options.organizationId,
216219
options.version,
217220
);
221+
options.onCreateVersion?.(version);
218222
await waitBuildToBeFinished(version);
219223
return API.createTemplate(options.organizationId, {
220224
...options.template,
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { watchBuildLogsByTemplateVersionId } from "api/api";
2+
import {
3+
ProvisionerJobLog,
4+
ProvisionerJobStatus,
5+
TemplateVersion,
6+
} from "api/typesGenerated";
7+
import { useState, useEffect } from "react";
8+
9+
export const useVersionLogs = (
10+
templateVersion: TemplateVersion | undefined,
11+
options?: { onDone: () => Promise<unknown> },
12+
) => {
13+
const [logs, setLogs] = useState<ProvisionerJobLog[]>();
14+
const templateVersionId = templateVersion?.id;
15+
const templateVersionStatus = templateVersion?.job.status;
16+
17+
useEffect(() => {
18+
const enabledStatuses: ProvisionerJobStatus[] = ["running", "pending"];
19+
20+
if (!templateVersionId || !templateVersionStatus) {
21+
return;
22+
}
23+
24+
if (!enabledStatuses.includes(templateVersionStatus)) {
25+
return;
26+
}
27+
28+
const socket = watchBuildLogsByTemplateVersionId(templateVersionId, {
29+
onMessage: (log) => {
30+
setLogs((logs) => (logs ? [...logs, log] : [log]));
31+
},
32+
onDone: options?.onDone,
33+
onError: (error) => {
34+
console.error(error);
35+
},
36+
});
37+
38+
return () => {
39+
socket.close();
40+
};
41+
}, [options?.onDone, templateVersionId, templateVersionStatus]);
42+
43+
return {
44+
logs,
45+
setLogs,
46+
};
47+
};
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import Drawer from "@mui/material/Drawer";
2+
import Close from "@mui/icons-material/Close";
3+
import IconButton from "@mui/material/IconButton";
4+
import { visuallyHidden } from "@mui/utils";
5+
import { FC, useEffect, useRef } from "react";
6+
import { TemplateVersion } from "api/typesGenerated";
7+
import { WorkspaceBuildLogs } from "modules/workspaces/WorkspaceBuildLogs/WorkspaceBuildLogs";
8+
import { useTheme } from "@emotion/react";
9+
import { navHeight } from "theme/constants";
10+
import { useVersionLogs } from "modules/templates/useVersionLogs";
11+
12+
type BuildLogsDrawerProps = {
13+
open: boolean;
14+
onClose: () => void;
15+
templateVersion: TemplateVersion | undefined;
16+
};
17+
18+
export const BuildLogsDrawer: FC<BuildLogsDrawerProps> = ({
19+
templateVersion,
20+
...drawerProps
21+
}) => {
22+
const theme = useTheme();
23+
const { logs } = useVersionLogs(templateVersion);
24+
25+
// Auto scroll
26+
const logsContainer = useRef<HTMLDivElement>(null);
27+
useEffect(() => {
28+
if (logsContainer.current) {
29+
logsContainer.current.scrollTop = logsContainer.current.scrollHeight;
30+
}
31+
}, [logs]);
32+
33+
return (
34+
<Drawer anchor="right" {...drawerProps}>
35+
<div
36+
css={{
37+
width: 800,
38+
height: "100%",
39+
display: "flex",
40+
flexDirection: "column",
41+
}}
42+
>
43+
<header
44+
css={{
45+
height: navHeight,
46+
padding: "0 20px",
47+
display: "flex",
48+
alignItems: "center",
49+
justifyContent: "space-between",
50+
borderBottom: `1px solid ${theme.palette.divider}`,
51+
}}
52+
>
53+
<h3 css={{ margin: 0, fontWeight: 500, fontSize: 16 }}>
54+
Creating template...
55+
</h3>
56+
<IconButton size="small" onClick={drawerProps.onClose}>
57+
<Close css={{ fontSize: 20 }} />
58+
<span style={visuallyHidden}>Close build logs</span>
59+
</IconButton>
60+
</header>
61+
62+
<section
63+
ref={logsContainer}
64+
css={{
65+
flex: 1,
66+
overflow: "auto",
67+
backgroundColor: theme.palette.background.default,
68+
}}
69+
>
70+
<WorkspaceBuildLogs logs={logs ?? []} css={{ border: 0 }} />
71+
</section>
72+
</div>
73+
</Drawer>
74+
);
75+
};

site/src/pages/CreateTemplatePage/CreateTemplatePage.tsx

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,41 @@
1-
import { type FC } from "react";
1+
import { useState, type FC } from "react";
22
import { Helmet } from "react-helmet-async";
33
import { useNavigate, useSearchParams } from "react-router-dom";
44
import { pageTitle } from "utils/page";
55
import { FullPageHorizontalForm } from "components/FullPageForm/FullPageHorizontalForm";
66
import { DuplicateTemplateView } from "./DuplicateTemplateView";
77
import { ImportStarterTemplateView } from "./ImportStarterTemplateView";
88
import { UploadTemplateView } from "./UploadTemplateView";
9-
import { Template } from "api/typesGenerated";
9+
import { BuildLogsDrawer } from "./BuildLogsDrawer";
10+
import { useMutation } from "react-query";
11+
import { createTemplate } from "api/queries/templates";
12+
import { CreateTemplatePageViewProps } from "./types";
13+
import { TemplateVersion } from "api/typesGenerated";
1014

1115
const CreateTemplatePage: FC = () => {
1216
const navigate = useNavigate();
1317
const [searchParams] = useSearchParams();
14-
15-
const onSuccess = (template: Template) => {
16-
navigate(`/templates/${template.name}/files`);
17-
};
18+
const [isBuildLogsOpen, setIsBuildLogsOpen] = useState(false);
19+
const [templateVersion, setTemplateVersion] = useState<TemplateVersion>();
20+
const createTemplateMutation = useMutation(createTemplate());
1821

1922
const onCancel = () => {
2023
navigate(-1);
2124
};
2225

26+
const pageViewProps: CreateTemplatePageViewProps = {
27+
onCreateTemplate: async (options) => {
28+
setIsBuildLogsOpen(true);
29+
const template = await createTemplateMutation.mutateAsync({
30+
...options,
31+
onCreateVersion: setTemplateVersion,
32+
});
33+
navigate(`/templates/${template.name}/files`);
34+
},
35+
error: createTemplateMutation.error,
36+
isCreating: createTemplateMutation.isLoading,
37+
};
38+
2339
return (
2440
<>
2541
<Helmet>
@@ -28,13 +44,19 @@ const CreateTemplatePage: FC = () => {
2844

2945
<FullPageHorizontalForm title="Create Template" onCancel={onCancel}>
3046
{searchParams.has("fromTemplate") ? (
31-
<DuplicateTemplateView onSuccess={onSuccess} />
47+
<DuplicateTemplateView {...pageViewProps} />
3248
) : searchParams.has("exampleId") ? (
33-
<ImportStarterTemplateView onSuccess={onSuccess} />
49+
<ImportStarterTemplateView {...pageViewProps} />
3450
) : (
35-
<UploadTemplateView onSuccess={onSuccess} />
51+
<UploadTemplateView {...pageViewProps} />
3652
)}
3753
</FullPageHorizontalForm>
54+
55+
<BuildLogsDrawer
56+
open={isBuildLogsOpen}
57+
onClose={() => setIsBuildLogsOpen(false)}
58+
templateVersion={templateVersion}
59+
/>
3860
</>
3961
);
4062
};

site/src/pages/CreateTemplatePage/DuplicateTemplateView.tsx

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,25 @@
11
import { type FC } from "react";
2-
import { useQuery, useMutation } from "react-query";
2+
import { useQuery } from "react-query";
33
import { useNavigate, useSearchParams } from "react-router-dom";
44
import {
55
templateVersionLogs,
66
templateByName,
77
templateVersion,
88
templateVersionVariables,
99
JobError,
10-
createTemplate,
1110
} from "api/queries/templates";
1211
import { useOrganizationId } from "contexts/auth/useOrganizationId";
1312
import { useDashboard } from "modules/dashboard/useDashboard";
1413
import { ErrorAlert } from "components/Alert/ErrorAlert";
1514
import { Loader } from "components/Loader/Loader";
1615
import { CreateTemplateForm } from "./CreateTemplateForm";
1716
import { firstVersionFromFile, getFormPermissions, newTemplate } from "./utils";
18-
import { Template } from "api/typesGenerated";
17+
import { CreateTemplatePageViewProps } from "./types";
1918

20-
type DuplicateTemplateViewProps = {
21-
onSuccess: (template: Template) => void;
22-
};
23-
24-
export const DuplicateTemplateView: FC<DuplicateTemplateViewProps> = ({
25-
onSuccess,
19+
export const DuplicateTemplateView: FC<CreateTemplatePageViewProps> = ({
20+
onCreateTemplate,
21+
error,
22+
isCreating,
2623
}) => {
2724
const navigate = useNavigate();
2825
const organizationId = useOrganizationId();
@@ -51,11 +48,9 @@ export const DuplicateTemplateView: FC<DuplicateTemplateViewProps> = ({
5148
const dashboard = useDashboard();
5249
const formPermissions = getFormPermissions(dashboard.entitlements);
5350

54-
const createTemplateMutation = useMutation(createTemplate());
55-
const createError = createTemplateMutation.error;
56-
const isJobError = createError instanceof JobError;
51+
const isJobError = error instanceof JobError;
5752
const templateVersionLogsQuery = useQuery({
58-
...templateVersionLogs(isJobError ? createError.version.id : ""),
53+
...templateVersionLogs(isJobError ? error.version.id : ""),
5954
enabled: isJobError,
6055
});
6156

@@ -71,22 +66,21 @@ export const DuplicateTemplateView: FC<DuplicateTemplateViewProps> = ({
7166
<CreateTemplateForm
7267
{...formPermissions}
7368
copiedTemplate={templateByNameQuery.data!}
74-
error={createTemplateMutation.error}
75-
isSubmitting={createTemplateMutation.isLoading}
69+
error={error}
70+
isSubmitting={isCreating}
7671
variables={templateVersionVariablesQuery.data}
7772
onCancel={() => navigate(-1)}
78-
jobError={isJobError ? createError.job.error : undefined}
73+
jobError={isJobError ? error.job.error : undefined}
7974
logs={templateVersionLogsQuery.data}
8075
onSubmit={async (formData) => {
81-
const template = await createTemplateMutation.mutateAsync({
76+
await onCreateTemplate({
8277
organizationId,
8378
version: firstVersionFromFile(
8479
templateVersionQuery.data!.job.file_id,
8580
formData.user_variable_values,
8681
),
8782
template: newTemplate(formData),
8883
});
89-
onSuccess(template);
9084
}}
9185
/>
9286
);

site/src/pages/CreateTemplatePage/ImportStarterTemplateView.tsx

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { type FC } from "react";
2-
import { useQuery, useMutation } from "react-query";
2+
import { useQuery } from "react-query";
33
import { useNavigate, useSearchParams } from "react-router-dom";
44
import {
55
templateVersionLogs,
66
JobError,
7-
createTemplate,
87
templateExamples,
98
templateVersionVariables,
109
} from "api/queries/templates";
@@ -18,14 +17,12 @@ import {
1817
getFormPermissions,
1918
newTemplate,
2019
} from "./utils";
21-
import { Template } from "api/typesGenerated";
20+
import { CreateTemplatePageViewProps } from "./types";
2221

23-
type ImportStarterTemplateViewProps = {
24-
onSuccess: (template: Template) => void;
25-
};
26-
27-
export const ImportStarterTemplateView: FC<ImportStarterTemplateViewProps> = ({
28-
onSuccess,
22+
export const ImportStarterTemplateView: FC<CreateTemplatePageViewProps> = ({
23+
onCreateTemplate,
24+
error,
25+
isCreating,
2926
}) => {
3027
const navigate = useNavigate();
3128
const organizationId = useOrganizationId();
@@ -41,19 +38,16 @@ export const ImportStarterTemplateView: FC<ImportStarterTemplateViewProps> = ({
4138
const dashboard = useDashboard();
4239
const formPermissions = getFormPermissions(dashboard.entitlements);
4340

44-
const createTemplateMutation = useMutation(createTemplate());
45-
const createError = createTemplateMutation.error;
46-
const isJobError = createError instanceof JobError;
41+
const isJobError = error instanceof JobError;
4742
const templateVersionLogsQuery = useQuery({
48-
...templateVersionLogs(isJobError ? createError.version.id : ""),
43+
...templateVersionLogs(isJobError ? error.version.id : ""),
4944
enabled: isJobError,
5045
});
5146

5247
const missedVariables = useQuery({
53-
...templateVersionVariables(isJobError ? createError.version.id : ""),
48+
...templateVersionVariables(isJobError ? error.version.id : ""),
5449
enabled:
55-
isJobError &&
56-
createError.job.error_code === "REQUIRED_TEMPLATE_VARIABLES",
50+
isJobError && error.job.error_code === "REQUIRED_TEMPLATE_VARIABLES",
5751
});
5852

5953
if (isLoading) {
@@ -69,21 +63,20 @@ export const ImportStarterTemplateView: FC<ImportStarterTemplateViewProps> = ({
6963
{...formPermissions}
7064
starterTemplate={templateExample!}
7165
variables={missedVariables.data}
72-
error={createTemplateMutation.error}
73-
isSubmitting={createTemplateMutation.isLoading}
66+
error={error}
67+
isSubmitting={isCreating}
7468
onCancel={() => navigate(-1)}
75-
jobError={isJobError ? createError.job.error : undefined}
69+
jobError={isJobError ? error.job.error : undefined}
7670
logs={templateVersionLogsQuery.data}
7771
onSubmit={async (formData) => {
78-
const template = await createTemplateMutation.mutateAsync({
72+
await onCreateTemplate({
7973
organizationId,
8074
version: firstVersionFromExample(
8175
templateExample!,
8276
formData.user_variable_values,
8377
),
8478
template: newTemplate(formData),
8579
});
86-
onSuccess(template);
8780
}}
8881
/>
8982
);

0 commit comments

Comments
 (0)