Skip to content

feat(site): display build logs on template creation #12271

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Show build logs during template creation
  • Loading branch information
BrunoQuaresma committed Feb 9, 2024
commit 2309bc45f8acc1f29d250b61b65dfd7278eb06e4
4 changes: 2 additions & 2 deletions site/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down Expand Up @@ -1534,7 +1534,7 @@ export const watchBuildLogsByTemplateVersionId = (
});
socket.addEventListener("close", () => {
// When the socket closes, logs have finished streaming!
onDone();
onDone && onDone();
});
return socket;
};
Expand Down
8 changes: 6 additions & 2 deletions site/src/api/queries/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,15 +206,19 @@ export const createTemplate = () => {
};
};

const createTemplateFn = async (options: {
export type CreateTemplateOptions = {
organizationId: string;
version: CreateTemplateVersionRequest;
template: Omit<CreateTemplateRequest, "template_version_id">;
}) => {
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,
Expand Down
47 changes: 47 additions & 0 deletions site/src/modules/templates/useVersionLogs.ts
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracted from ImportStarterTemplateView

Original file line number Diff line number Diff line change
@@ -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<unknown> },
) => {
const [logs, setLogs] = useState<ProvisionerJobLog[]>();
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,
};
};
75 changes: 75 additions & 0 deletions site/src/pages/CreateTemplatePage/BuildLogsDrawer.tsx
Original file line number Diff line number Diff line change
@@ -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<BuildLogsDrawerProps> = ({
templateVersion,
...drawerProps
}) => {
const theme = useTheme();
const { logs } = useVersionLogs(templateVersion);

// Auto scroll
const logsContainer = useRef<HTMLDivElement>(null);
useEffect(() => {
if (logsContainer.current) {
logsContainer.current.scrollTop = logsContainer.current.scrollHeight;
}
}, [logs]);

return (
<Drawer anchor="right" {...drawerProps}>
<div
css={{
width: 800,
height: "100%",
display: "flex",
flexDirection: "column",
}}
>
<header
css={{
height: navHeight,
padding: "0 20px",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
borderBottom: `1px solid ${theme.palette.divider}`,
}}
>
<h3 css={{ margin: 0, fontWeight: 500, fontSize: 16 }}>
Creating template...
</h3>
<IconButton size="small" onClick={drawerProps.onClose}>
<Close css={{ fontSize: 20 }} />
<span style={visuallyHidden}>Close build logs</span>
</IconButton>
</header>

<section
ref={logsContainer}
css={{
flex: 1,
overflow: "auto",
backgroundColor: theme.palette.background.default,
}}
>
<WorkspaceBuildLogs logs={logs ?? []} css={{ border: 0 }} />
</section>
</div>
</Drawer>
);
};
40 changes: 31 additions & 9 deletions site/src/pages/CreateTemplatePage/CreateTemplatePage.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,41 @@
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";
import { FullPageHorizontalForm } from "components/FullPageForm/FullPageHorizontalForm";
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<TemplateVersion>();
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 (
<>
<Helmet>
Expand All @@ -28,13 +44,19 @@ const CreateTemplatePage: FC = () => {

<FullPageHorizontalForm title="Create Template" onCancel={onCancel}>
{searchParams.has("fromTemplate") ? (
<DuplicateTemplateView onSuccess={onSuccess} />
<DuplicateTemplateView {...pageViewProps} />
) : searchParams.has("exampleId") ? (
<ImportStarterTemplateView onSuccess={onSuccess} />
<ImportStarterTemplateView {...pageViewProps} />
) : (
<UploadTemplateView onSuccess={onSuccess} />
<UploadTemplateView {...pageViewProps} />
)}
</FullPageHorizontalForm>

<BuildLogsDrawer
open={isBuildLogsOpen}
onClose={() => setIsBuildLogsOpen(false)}
templateVersion={templateVersion}
/>
</>
);
};
Expand Down
30 changes: 12 additions & 18 deletions site/src/pages/CreateTemplatePage/DuplicateTemplateView.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,25 @@
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,
templateByName,
templateVersion,
templateVersionVariables,
JobError,
createTemplate,
} from "api/queries/templates";
import { useOrganizationId } from "contexts/auth/useOrganizationId";
import { useDashboard } from "modules/dashboard/useDashboard";
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<DuplicateTemplateViewProps> = ({
onSuccess,
export const DuplicateTemplateView: FC<CreateTemplatePageViewProps> = ({
onCreateTemplate,
error,
isCreating,
}) => {
const navigate = useNavigate();
const organizationId = useOrganizationId();
Expand Down Expand Up @@ -51,11 +48,9 @@ export const DuplicateTemplateView: FC<DuplicateTemplateViewProps> = ({
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,
});

Expand All @@ -71,22 +66,21 @@ export const DuplicateTemplateView: FC<DuplicateTemplateViewProps> = ({
<CreateTemplateForm
{...formPermissions}
copiedTemplate={templateByNameQuery.data!}
error={createTemplateMutation.error}
isSubmitting={createTemplateMutation.isLoading}
error={error}
isSubmitting={isCreating}
variables={templateVersionVariablesQuery.data}
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: firstVersionFromFile(
templateVersionQuery.data!.job.file_id,
formData.user_variable_values,
),
template: newTemplate(formData),
});
onSuccess(template);
}}
/>
);
Expand Down
35 changes: 14 additions & 21 deletions site/src/pages/CreateTemplatePage/ImportStarterTemplateView.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<ImportStarterTemplateViewProps> = ({
onSuccess,
export const ImportStarterTemplateView: FC<CreateTemplatePageViewProps> = ({
onCreateTemplate,
error,
isCreating,
}) => {
const navigate = useNavigate();
const organizationId = useOrganizationId();
Expand All @@ -41,19 +38,16 @@ export const ImportStarterTemplateView: FC<ImportStarterTemplateViewProps> = ({
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) {
Expand All @@ -69,21 +63,20 @@ export const ImportStarterTemplateView: FC<ImportStarterTemplateViewProps> = ({
{...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!,
formData.user_variable_values,
),
template: newTemplate(formData),
});
onSuccess(template);
}}
/>
);
Expand Down
Loading