mirror of
https://github.com/coder/coder.git
synced 2025-07-12 00:14:10 +00:00
feat(site): display build logs on template creation (#12271)
This commit is contained in:
@ -49,6 +49,8 @@ global.TextDecoder = TextDecoder as any;
|
|||||||
global.Blob = Blob as any;
|
global.Blob = Blob as any;
|
||||||
global.scrollTo = jest.fn();
|
global.scrollTo = jest.fn();
|
||||||
|
|
||||||
|
window.HTMLElement.prototype.scrollIntoView = function () {};
|
||||||
|
|
||||||
// Polyfill the getRandomValues that is used on utils/random.ts
|
// Polyfill the getRandomValues that is used on utils/random.ts
|
||||||
Object.defineProperty(global.self, "crypto", {
|
Object.defineProperty(global.self, "crypto", {
|
||||||
value: {
|
value: {
|
||||||
|
3
site/src/@types/storybook.d.ts
vendored
3
site/src/@types/storybook.d.ts
vendored
@ -7,5 +7,8 @@ declare module "@storybook/react" {
|
|||||||
features?: FeatureName[];
|
features?: FeatureName[];
|
||||||
experiments?: Experiments;
|
experiments?: Experiments;
|
||||||
queries?: { key: QueryKey; data: unknown }[];
|
queries?: { key: QueryKey; data: unknown }[];
|
||||||
|
webSocket?: {
|
||||||
|
messages: string[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1543,7 +1543,7 @@ export const watchAgentMetadata = (agentId: string): EventSource => {
|
|||||||
type WatchBuildLogsByTemplateVersionIdOptions = {
|
type WatchBuildLogsByTemplateVersionIdOptions = {
|
||||||
after?: number;
|
after?: number;
|
||||||
onMessage: (log: TypesGen.ProvisionerJobLog) => void;
|
onMessage: (log: TypesGen.ProvisionerJobLog) => void;
|
||||||
onDone: () => void;
|
onDone?: () => void;
|
||||||
onError: (error: Error) => void;
|
onError: (error: Error) => void;
|
||||||
};
|
};
|
||||||
export const watchBuildLogsByTemplateVersionId = (
|
export const watchBuildLogsByTemplateVersionId = (
|
||||||
@ -1575,7 +1575,7 @@ export const watchBuildLogsByTemplateVersionId = (
|
|||||||
});
|
});
|
||||||
socket.addEventListener("close", () => {
|
socket.addEventListener("close", () => {
|
||||||
// When the socket closes, logs have finished streaming!
|
// When the socket closes, logs have finished streaming!
|
||||||
onDone();
|
onDone?.();
|
||||||
});
|
});
|
||||||
return socket;
|
return socket;
|
||||||
};
|
};
|
||||||
|
@ -206,16 +206,21 @@ export const createTemplate = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const createTemplateFn = async (options: {
|
export type CreateTemplateOptions = {
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
version: CreateTemplateVersionRequest;
|
version: CreateTemplateVersionRequest;
|
||||||
template: Omit<CreateTemplateRequest, "template_version_id">;
|
template: Omit<CreateTemplateRequest, "template_version_id">;
|
||||||
}) => {
|
onCreateVersion?: (version: TemplateVersion) => void;
|
||||||
|
onTemplateVersionChanges?: (version: TemplateVersion) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createTemplateFn = async (options: CreateTemplateOptions) => {
|
||||||
const version = await API.createTemplateVersion(
|
const version = await API.createTemplateVersion(
|
||||||
options.organizationId,
|
options.organizationId,
|
||||||
options.version,
|
options.version,
|
||||||
);
|
);
|
||||||
await waitBuildToBeFinished(version);
|
options.onCreateVersion?.(version);
|
||||||
|
await waitBuildToBeFinished(version, options.onTemplateVersionChanges);
|
||||||
return API.createTemplate(options.organizationId, {
|
return API.createTemplate(options.organizationId, {
|
||||||
...options.template,
|
...options.template,
|
||||||
template_version_id: version.id,
|
template_version_id: version.id,
|
||||||
@ -278,12 +283,17 @@ export const previousTemplateVersion = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const waitBuildToBeFinished = async (version: TemplateVersion) => {
|
const waitBuildToBeFinished = async (
|
||||||
|
version: TemplateVersion,
|
||||||
|
onRequest?: (data: TemplateVersion) => void,
|
||||||
|
) => {
|
||||||
let data: TemplateVersion;
|
let data: TemplateVersion;
|
||||||
let jobStatus: ProvisionerJobStatus;
|
let jobStatus: ProvisionerJobStatus | undefined = undefined;
|
||||||
do {
|
do {
|
||||||
await delay(1000);
|
// When pending we want to poll more frequently
|
||||||
|
await delay(jobStatus === "pending" ? 250 : 1000);
|
||||||
data = await API.getTemplateVersion(version.id);
|
data = await API.getTemplateVersion(version.id);
|
||||||
|
onRequest?.(data);
|
||||||
jobStatus = data.job.status;
|
jobStatus = data.job.status;
|
||||||
|
|
||||||
if (jobStatus === "succeeded") {
|
if (jobStatus === "succeeded") {
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
useContext,
|
useContext,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
ComponentProps,
|
ComponentProps,
|
||||||
|
forwardRef,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { AlphaBadge, DeprecatedBadge } from "components/Badges/Badges";
|
import { AlphaBadge, DeprecatedBadge } from "components/Badges/Badges";
|
||||||
import { Stack } from "components/Stack/Stack";
|
import { Stack } from "components/Stack/Stack";
|
||||||
@ -81,45 +82,34 @@ interface FormSectionProps {
|
|||||||
deprecated?: boolean;
|
deprecated?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FormSection: FC<FormSectionProps> = ({
|
export const FormSection = forwardRef<HTMLDivElement, FormSectionProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
children,
|
children,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
classes = {},
|
classes = {},
|
||||||
alpha = false,
|
alpha = false,
|
||||||
deprecated = false,
|
deprecated = false,
|
||||||
}) => {
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
const { direction } = useContext(FormContext);
|
const { direction } = useContext(FormContext);
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<section
|
||||||
css={{
|
ref={ref}
|
||||||
display: "flex",
|
css={[
|
||||||
alignItems: "flex-start",
|
styles.formSection,
|
||||||
flexDirection: direction === "horizontal" ? "row" : "column",
|
direction === "horizontal" && styles.formSectionHorizontal,
|
||||||
gap: direction === "horizontal" ? 120 : 24,
|
]}
|
||||||
|
|
||||||
[theme.breakpoints.down("md")]: {
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: 16,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
className={classes.root}
|
className={classes.root}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
css={{
|
css={[
|
||||||
width: "100%",
|
styles.formSectionInfo,
|
||||||
maxWidth: direction === "horizontal" ? 312 : undefined,
|
direction === "horizontal" && styles.formSectionInfoHorizontal,
|
||||||
flexShrink: 0,
|
]}
|
||||||
position: direction === "horizontal" ? "sticky" : undefined,
|
|
||||||
top: 24,
|
|
||||||
|
|
||||||
[theme.breakpoints.down("md")]: {
|
|
||||||
width: "100%",
|
|
||||||
position: "initial" as const,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
className={classes.sectionInfo}
|
className={classes.sectionInfo}
|
||||||
>
|
>
|
||||||
<h2 css={styles.formSectionInfoTitle} className={classes.infoTitle}>
|
<h2 css={styles.formSectionInfoTitle} className={classes.infoTitle}>
|
||||||
@ -131,9 +121,10 @@ export const FormSection: FC<FormSectionProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const FormFields: FC<ComponentProps<typeof Stack>> = (props) => {
|
export const FormFields: FC<ComponentProps<typeof Stack>> = (props) => {
|
||||||
return (
|
return (
|
||||||
@ -147,6 +138,35 @@ export const FormFields: FC<ComponentProps<typeof Stack>> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const styles = {
|
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) => ({
|
formSectionInfoTitle: (theme) => ({
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
|
@ -19,10 +19,12 @@ export interface FormFooterProps {
|
|||||||
styles?: FormFooterStyles;
|
styles?: FormFooterStyles;
|
||||||
submitLabel?: string;
|
submitLabel?: string;
|
||||||
submitDisabled?: boolean;
|
submitDisabled?: boolean;
|
||||||
|
extraActions?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FormFooter: FC<FormFooterProps> = ({
|
export const FormFooter: FC<FormFooterProps> = ({
|
||||||
onCancel,
|
onCancel,
|
||||||
|
extraActions,
|
||||||
isLoading,
|
isLoading,
|
||||||
submitDisabled,
|
submitDisabled,
|
||||||
submitLabel = Language.defaultSubmitLabel,
|
submitLabel = Language.defaultSubmitLabel,
|
||||||
@ -52,6 +54,7 @@ export const FormFooter: FC<FormFooterProps> = ({
|
|||||||
>
|
>
|
||||||
{Language.cancelLabel}
|
{Language.cancelLabel}
|
||||||
</Button>
|
</Button>
|
||||||
|
{extraActions}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
42
site/src/modules/templates/useWatchVersionLogs.ts
Normal file
42
site/src/modules/templates/useWatchVersionLogs.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { watchBuildLogsByTemplateVersionId } from "api/api";
|
||||||
|
import { ProvisionerJobLog, TemplateVersion } from "api/typesGenerated";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
export const useWatchVersionLogs = (
|
||||||
|
templateVersion: TemplateVersion | undefined,
|
||||||
|
options?: { onDone: () => Promise<unknown> },
|
||||||
|
) => {
|
||||||
|
const [logs, setLogs] = useState<ProvisionerJobLog[] | undefined>();
|
||||||
|
const templateVersionId = templateVersion?.id;
|
||||||
|
const templateVersionStatus = templateVersion?.job.status;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLogs(undefined);
|
||||||
|
}, [templateVersionId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!templateVersionId || !templateVersionStatus) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (templateVersionStatus !== "running") {
|
||||||
|
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;
|
||||||
|
};
|
@ -112,7 +112,7 @@ const styles = {
|
|||||||
borderRadius: "0 0 8px 8px",
|
borderRadius: "0 0 8px 8px",
|
||||||
},
|
},
|
||||||
|
|
||||||
"&:first-child": {
|
"&:first-of-type": {
|
||||||
borderRadius: "8px 8px 0 0",
|
borderRadius: "8px 8px 0 0",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
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 { withWebSocket } from "testHelpers/storybook";
|
||||||
|
|
||||||
|
const meta: Meta<typeof BuildLogsDrawer> = {
|
||||||
|
title: "pages/CreateTemplatePage/BuildLogsDrawer",
|
||||||
|
component: BuildLogsDrawer,
|
||||||
|
args: {
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof BuildLogsDrawer>;
|
||||||
|
|
||||||
|
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: [withWebSocket],
|
||||||
|
parameters: {
|
||||||
|
webSocket: {
|
||||||
|
messages: MockWorkspaceBuildLogs.map((log) => JSON.stringify(log)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
179
site/src/pages/CreateTemplatePage/BuildLogsDrawer.tsx
Normal file
179
site/src/pages/CreateTemplatePage/BuildLogsDrawer.tsx
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
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, useLayoutEffect, useRef } from "react";
|
||||||
|
import { TemplateVersion } from "api/typesGenerated";
|
||||||
|
import { WorkspaceBuildLogs } from "modules/workspaces/WorkspaceBuildLogs/WorkspaceBuildLogs";
|
||||||
|
import { Interpolation, Theme } from "@emotion/react";
|
||||||
|
import { navHeight } from "theme/constants";
|
||||||
|
import { useWatchVersionLogs } from "modules/templates/useWatchVersionLogs";
|
||||||
|
import { JobError } from "api/queries/templates";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import { Loader } from "components/Loader/Loader";
|
||||||
|
import WarningOutlined from "@mui/icons-material/WarningOutlined";
|
||||||
|
|
||||||
|
type BuildLogsDrawerProps = {
|
||||||
|
error: unknown;
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
templateVersion: TemplateVersion | undefined;
|
||||||
|
variablesSectionRef: React.RefObject<HTMLDivElement>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BuildLogsDrawer: FC<BuildLogsDrawerProps> = ({
|
||||||
|
templateVersion,
|
||||||
|
error,
|
||||||
|
variablesSectionRef,
|
||||||
|
...drawerProps
|
||||||
|
}) => {
|
||||||
|
const logs = useWatchVersionLogs(templateVersion);
|
||||||
|
const logsContainer = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (logsContainer.current) {
|
||||||
|
logsContainer.current.scrollTop = logsContainer.current.scrollHeight;
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
}, [logs]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (drawerProps.open) {
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
}, [drawerProps.open]);
|
||||||
|
|
||||||
|
const isMissingVariables =
|
||||||
|
error instanceof JobError &&
|
||||||
|
error.job.error_code === "REQUIRED_TEMPLATE_VARIABLES";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer anchor="right" {...drawerProps}>
|
||||||
|
<div css={styles.root}>
|
||||||
|
<header css={styles.header}>
|
||||||
|
<h3 css={styles.title}>Creating template...</h3>
|
||||||
|
<IconButton size="small" onClick={drawerProps.onClose}>
|
||||||
|
<Close css={styles.closeIcon} />
|
||||||
|
<span style={visuallyHidden}>Close build logs</span>
|
||||||
|
</IconButton>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{isMissingVariables ? (
|
||||||
|
<MissingVariablesBanner
|
||||||
|
onFillVariables={() => {
|
||||||
|
variablesSectionRef.current?.scrollIntoView({
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
const firstVariableInput =
|
||||||
|
variablesSectionRef.current?.querySelector("input");
|
||||||
|
setTimeout(() => firstVariableInput?.focus(), 0);
|
||||||
|
drawerProps.onClose();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : logs ? (
|
||||||
|
<section ref={logsContainer} css={styles.logs}>
|
||||||
|
<WorkspaceBuildLogs logs={logs} css={{ border: 0 }} />
|
||||||
|
</section>
|
||||||
|
) : (
|
||||||
|
<Loader />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const MissingVariablesBanner: FC<{ onFillVariables: () => void }> = ({
|
||||||
|
onFillVariables,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div css={bannerStyles.root}>
|
||||||
|
<div css={bannerStyles.content}>
|
||||||
|
<WarningOutlined css={bannerStyles.icon} />
|
||||||
|
<h4 css={bannerStyles.title}>Missing variables</h4>
|
||||||
|
<p css={bannerStyles.description}>
|
||||||
|
During the build process, we identified some missing variables. Rest
|
||||||
|
assured, we have automatically added them to the form for you.
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
css={bannerStyles.button}
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
onClick={onFillVariables}
|
||||||
|
>
|
||||||
|
Fill variables
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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<string, Interpolation<Theme>>;
|
||||||
|
|
||||||
|
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<string, Interpolation<Theme>>;
|
@ -11,7 +11,7 @@ import { CreateTemplateForm } from "./CreateTemplateForm";
|
|||||||
import type { Meta, StoryObj } from "@storybook/react";
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
|
|
||||||
const meta: Meta<typeof CreateTemplateForm> = {
|
const meta: Meta<typeof CreateTemplateForm> = {
|
||||||
title: "pages/CreateTemplatePage",
|
title: "pages/CreateTemplatePage/CreateTemplateForm",
|
||||||
component: CreateTemplateForm,
|
component: CreateTemplateForm,
|
||||||
args: {
|
args: {
|
||||||
isSubmitting: false,
|
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: "",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { type Interpolation, type Theme } from "@emotion/react";
|
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
import { type FC, useEffect } from "react";
|
import { type FC } from "react";
|
||||||
import camelCase from "lodash/camelCase";
|
import camelCase from "lodash/camelCase";
|
||||||
import capitalize from "lodash/capitalize";
|
import capitalize from "lodash/capitalize";
|
||||||
import * as Yup from "yup";
|
import * as Yup from "yup";
|
||||||
@ -12,7 +11,6 @@ import type {
|
|||||||
TemplateVersionVariable,
|
TemplateVersionVariable,
|
||||||
VariableValue,
|
VariableValue,
|
||||||
} from "api/typesGenerated";
|
} from "api/typesGenerated";
|
||||||
import { Stack } from "components/Stack/Stack";
|
|
||||||
import { SelectedTemplate } from "pages/CreateWorkspacePage/SelectedTemplate";
|
import { SelectedTemplate } from "pages/CreateWorkspacePage/SelectedTemplate";
|
||||||
import {
|
import {
|
||||||
nameValidator,
|
nameValidator,
|
||||||
@ -25,7 +23,6 @@ import {
|
|||||||
type TemplateAutostopRequirementDaysValue,
|
type TemplateAutostopRequirementDaysValue,
|
||||||
} from "utils/schedule";
|
} from "utils/schedule";
|
||||||
import { sortedDays } from "modules/templates/TemplateScheduleAutostart/TemplateScheduleAutostart";
|
import { sortedDays } from "modules/templates/TemplateScheduleAutostart/TemplateScheduleAutostart";
|
||||||
import { WorkspaceBuildLogs } from "modules/workspaces/WorkspaceBuildLogs/WorkspaceBuildLogs";
|
|
||||||
import { IconField } from "components/IconField/IconField";
|
import { IconField } from "components/IconField/IconField";
|
||||||
import {
|
import {
|
||||||
HorizontalForm,
|
HorizontalForm,
|
||||||
@ -166,24 +163,28 @@ export type CreateTemplateFormProps = (
|
|||||||
) & {
|
) & {
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onSubmit: (data: CreateTemplateData) => void;
|
onSubmit: (data: CreateTemplateData) => void;
|
||||||
|
onOpenBuildLogsDrawer: () => void;
|
||||||
isSubmitting: boolean;
|
isSubmitting: boolean;
|
||||||
variables?: TemplateVersionVariable[];
|
variables?: TemplateVersionVariable[];
|
||||||
error?: unknown;
|
error?: unknown;
|
||||||
jobError?: string;
|
jobError?: string;
|
||||||
logs?: ProvisionerJobLog[];
|
logs?: ProvisionerJobLog[];
|
||||||
allowAdvancedScheduling: boolean;
|
allowAdvancedScheduling: boolean;
|
||||||
|
variablesSectionRef: React.RefObject<HTMLDivElement>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
|
export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
|
||||||
const {
|
const {
|
||||||
onCancel,
|
onCancel,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
onOpenBuildLogsDrawer,
|
||||||
variables,
|
variables,
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
error,
|
error,
|
||||||
jobError,
|
jobError,
|
||||||
logs,
|
logs,
|
||||||
allowAdvancedScheduling,
|
allowAdvancedScheduling,
|
||||||
|
variablesSectionRef,
|
||||||
} = props;
|
} = props;
|
||||||
const form = useFormik<CreateTemplateData>({
|
const form = useFormik<CreateTemplateData>({
|
||||||
initialValues: getInitialValues({
|
initialValues: getInitialValues({
|
||||||
@ -198,18 +199,6 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
|
|||||||
});
|
});
|
||||||
const getFieldHelpers = getFormHelpers<CreateTemplateData>(form, error);
|
const getFieldHelpers = getFormHelpers<CreateTemplateData>(form, error);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (error) {
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
}
|
|
||||||
}, [error]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (jobError) {
|
|
||||||
window.scrollTo(0, document.body.scrollHeight);
|
|
||||||
}
|
|
||||||
}, [logs, jobError]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HorizontalForm onSubmit={form.handleSubmit}>
|
<HorizontalForm onSubmit={form.handleSubmit}>
|
||||||
{/* General info */}
|
{/* General info */}
|
||||||
@ -283,6 +272,7 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
|
|||||||
{/* Variables */}
|
{/* Variables */}
|
||||||
{variables && variables.length > 0 && (
|
{variables && variables.length > 0 && (
|
||||||
<FormSection
|
<FormSection
|
||||||
|
ref={variablesSectionRef}
|
||||||
title="Variables"
|
title="Variables"
|
||||||
description="Input variables allow you to customize templates without altering their source code."
|
description="Input variables allow you to customize templates without altering their source code."
|
||||||
>
|
>
|
||||||
@ -305,27 +295,37 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
|
|||||||
</FormSection>
|
</FormSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{jobError && (
|
<div className="flex items-center">
|
||||||
<Stack>
|
|
||||||
<div css={styles.error}>
|
|
||||||
<h5 css={styles.errorTitle}>Error during provisioning</h5>
|
|
||||||
<p css={styles.errorDescription}>
|
|
||||||
Looks like we found an error during the template provisioning. You
|
|
||||||
can see the logs bellow.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<code css={styles.errorDetails}>{jobError}</code>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<WorkspaceBuildLogs logs={logs ?? []} />
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<FormFooter
|
<FormFooter
|
||||||
|
extraActions={
|
||||||
|
logs && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onOpenBuildLogsDrawer}
|
||||||
|
css={(theme) => ({
|
||||||
|
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
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
isLoading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
submitLabel={jobError ? "Retry" : "Create template"}
|
submitLabel={jobError ? "Retry" : "Create template"}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</HorizontalForm>
|
</HorizontalForm>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -344,44 +344,3 @@ const fillNameAndDisplayWithFilename = async (
|
|||||||
form.setFieldValue("display_name", capitalize(name)),
|
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<string, Interpolation<Theme>>;
|
|
||||||
|
@ -19,7 +19,7 @@ const renderPage = async (searchParams: URLSearchParams) => {
|
|||||||
route: `/templates/new?${searchParams.toString()}`,
|
route: `/templates/new?${searchParams.toString()}`,
|
||||||
path: "/templates/new",
|
path: "/templates/new",
|
||||||
// We need this because after creation, the user will be redirected to here
|
// 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
|
// It is lazy loaded, so we have to wait for it to be rendered to not get an
|
||||||
// act error
|
// act error
|
||||||
@ -62,6 +62,12 @@ test("Create template from starter template", async () => {
|
|||||||
within(form).getByRole("button", { name: /create template/i }),
|
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
|
// Wait for the variables form to be rendered and fill it
|
||||||
await screen.findByText(/Variables/, undefined, { timeout: 5_000 });
|
await screen.findByText(/Variables/, undefined, { timeout: 5_000 });
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { type FC } from "react";
|
import { useState, type FC, useRef } from "react";
|
||||||
import { Helmet } from "react-helmet-async";
|
import { Helmet } from "react-helmet-async";
|
||||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import { pageTitle } from "utils/page";
|
import { pageTitle } from "utils/page";
|
||||||
@ -6,20 +6,40 @@ import { FullPageHorizontalForm } from "components/FullPageForm/FullPageHorizont
|
|||||||
import { DuplicateTemplateView } from "./DuplicateTemplateView";
|
import { DuplicateTemplateView } from "./DuplicateTemplateView";
|
||||||
import { ImportStarterTemplateView } from "./ImportStarterTemplateView";
|
import { ImportStarterTemplateView } from "./ImportStarterTemplateView";
|
||||||
import { UploadTemplateView } from "./UploadTemplateView";
|
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 CreateTemplatePage: FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
const [isBuildLogsOpen, setIsBuildLogsOpen] = useState(false);
|
||||||
const onSuccess = (template: Template) => {
|
const [templateVersion, setTemplateVersion] = useState<TemplateVersion>();
|
||||||
navigate(`/templates/${template.name}/files`);
|
const createTemplateMutation = useMutation(createTemplate());
|
||||||
};
|
const variablesSectionRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const onCancel = () => {
|
const onCancel = () => {
|
||||||
navigate(-1);
|
navigate(-1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const pageViewProps: CreateTemplatePageViewProps = {
|
||||||
|
onCreateTemplate: async (options) => {
|
||||||
|
setIsBuildLogsOpen(true);
|
||||||
|
const template = await createTemplateMutation.mutateAsync({
|
||||||
|
...options,
|
||||||
|
onCreateVersion: setTemplateVersion,
|
||||||
|
onTemplateVersionChanges: setTemplateVersion,
|
||||||
|
});
|
||||||
|
navigate(`/templates/${template.name}/files`);
|
||||||
|
},
|
||||||
|
onOpenBuildLogsDrawer: () => setIsBuildLogsOpen(true),
|
||||||
|
error: createTemplateMutation.error,
|
||||||
|
isCreating: createTemplateMutation.isLoading,
|
||||||
|
variablesSectionRef,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
@ -28,13 +48,21 @@ const CreateTemplatePage: FC = () => {
|
|||||||
|
|
||||||
<FullPageHorizontalForm title="Create Template" onCancel={onCancel}>
|
<FullPageHorizontalForm title="Create Template" onCancel={onCancel}>
|
||||||
{searchParams.has("fromTemplate") ? (
|
{searchParams.has("fromTemplate") ? (
|
||||||
<DuplicateTemplateView onSuccess={onSuccess} />
|
<DuplicateTemplateView {...pageViewProps} />
|
||||||
) : searchParams.has("exampleId") ? (
|
) : searchParams.has("exampleId") ? (
|
||||||
<ImportStarterTemplateView onSuccess={onSuccess} />
|
<ImportStarterTemplateView {...pageViewProps} />
|
||||||
) : (
|
) : (
|
||||||
<UploadTemplateView onSuccess={onSuccess} />
|
<UploadTemplateView {...pageViewProps} />
|
||||||
)}
|
)}
|
||||||
</FullPageHorizontalForm>
|
</FullPageHorizontalForm>
|
||||||
|
|
||||||
|
<BuildLogsDrawer
|
||||||
|
error={createTemplateMutation.error}
|
||||||
|
open={isBuildLogsOpen}
|
||||||
|
onClose={() => setIsBuildLogsOpen(false)}
|
||||||
|
templateVersion={templateVersion}
|
||||||
|
variablesSectionRef={variablesSectionRef}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { type FC } from "react";
|
import { type FC } from "react";
|
||||||
import { useQuery, useMutation } from "react-query";
|
import { useQuery } from "react-query";
|
||||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
templateVersionLogs,
|
templateVersionLogs,
|
||||||
@ -7,7 +7,6 @@ import {
|
|||||||
templateVersion,
|
templateVersion,
|
||||||
templateVersionVariables,
|
templateVersionVariables,
|
||||||
JobError,
|
JobError,
|
||||||
createTemplate,
|
|
||||||
} from "api/queries/templates";
|
} from "api/queries/templates";
|
||||||
import { useOrganizationId } from "contexts/auth/useOrganizationId";
|
import { useOrganizationId } from "contexts/auth/useOrganizationId";
|
||||||
import { useDashboard } from "modules/dashboard/useDashboard";
|
import { useDashboard } from "modules/dashboard/useDashboard";
|
||||||
@ -15,14 +14,14 @@ import { ErrorAlert } from "components/Alert/ErrorAlert";
|
|||||||
import { Loader } from "components/Loader/Loader";
|
import { Loader } from "components/Loader/Loader";
|
||||||
import { CreateTemplateForm } from "./CreateTemplateForm";
|
import { CreateTemplateForm } from "./CreateTemplateForm";
|
||||||
import { firstVersionFromFile, getFormPermissions, newTemplate } from "./utils";
|
import { firstVersionFromFile, getFormPermissions, newTemplate } from "./utils";
|
||||||
import { Template } from "api/typesGenerated";
|
import { CreateTemplatePageViewProps } from "./types";
|
||||||
|
|
||||||
type DuplicateTemplateViewProps = {
|
export const DuplicateTemplateView: FC<CreateTemplatePageViewProps> = ({
|
||||||
onSuccess: (template: Template) => void;
|
onCreateTemplate,
|
||||||
};
|
onOpenBuildLogsDrawer,
|
||||||
|
variablesSectionRef,
|
||||||
export const DuplicateTemplateView: FC<DuplicateTemplateViewProps> = ({
|
error,
|
||||||
onSuccess,
|
isCreating,
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
@ -51,11 +50,9 @@ export const DuplicateTemplateView: FC<DuplicateTemplateViewProps> = ({
|
|||||||
const dashboard = useDashboard();
|
const dashboard = useDashboard();
|
||||||
const formPermissions = getFormPermissions(dashboard.entitlements);
|
const formPermissions = getFormPermissions(dashboard.entitlements);
|
||||||
|
|
||||||
const createTemplateMutation = useMutation(createTemplate());
|
const isJobError = error instanceof JobError;
|
||||||
const createError = createTemplateMutation.error;
|
|
||||||
const isJobError = createError instanceof JobError;
|
|
||||||
const templateVersionLogsQuery = useQuery({
|
const templateVersionLogsQuery = useQuery({
|
||||||
...templateVersionLogs(isJobError ? createError.version.id : ""),
|
...templateVersionLogs(isJobError ? error.version.id : ""),
|
||||||
enabled: isJobError,
|
enabled: isJobError,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -70,15 +67,17 @@ export const DuplicateTemplateView: FC<DuplicateTemplateViewProps> = ({
|
|||||||
return (
|
return (
|
||||||
<CreateTemplateForm
|
<CreateTemplateForm
|
||||||
{...formPermissions}
|
{...formPermissions}
|
||||||
|
variablesSectionRef={variablesSectionRef}
|
||||||
|
onOpenBuildLogsDrawer={onOpenBuildLogsDrawer}
|
||||||
copiedTemplate={templateByNameQuery.data!}
|
copiedTemplate={templateByNameQuery.data!}
|
||||||
error={createTemplateMutation.error}
|
error={error}
|
||||||
isSubmitting={createTemplateMutation.isLoading}
|
isSubmitting={isCreating}
|
||||||
variables={templateVersionVariablesQuery.data}
|
variables={templateVersionVariablesQuery.data}
|
||||||
onCancel={() => navigate(-1)}
|
onCancel={() => navigate(-1)}
|
||||||
jobError={isJobError ? createError.job.error : undefined}
|
jobError={isJobError ? error.job.error : undefined}
|
||||||
logs={templateVersionLogsQuery.data}
|
logs={templateVersionLogsQuery.data}
|
||||||
onSubmit={async (formData) => {
|
onSubmit={async (formData) => {
|
||||||
const template = await createTemplateMutation.mutateAsync({
|
await onCreateTemplate({
|
||||||
organizationId,
|
organizationId,
|
||||||
version: firstVersionFromFile(
|
version: firstVersionFromFile(
|
||||||
templateVersionQuery.data!.job.file_id,
|
templateVersionQuery.data!.job.file_id,
|
||||||
@ -86,7 +85,6 @@ export const DuplicateTemplateView: FC<DuplicateTemplateViewProps> = ({
|
|||||||
),
|
),
|
||||||
template: newTemplate(formData),
|
template: newTemplate(formData),
|
||||||
});
|
});
|
||||||
onSuccess(template);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import { type FC } from "react";
|
import { type FC } from "react";
|
||||||
import { useQuery, useMutation } from "react-query";
|
import { useQuery } from "react-query";
|
||||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
templateVersionLogs,
|
templateVersionLogs,
|
||||||
JobError,
|
JobError,
|
||||||
createTemplate,
|
|
||||||
templateExamples,
|
templateExamples,
|
||||||
templateVersionVariables,
|
templateVersionVariables,
|
||||||
} from "api/queries/templates";
|
} from "api/queries/templates";
|
||||||
@ -18,14 +17,14 @@ import {
|
|||||||
getFormPermissions,
|
getFormPermissions,
|
||||||
newTemplate,
|
newTemplate,
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
import { Template } from "api/typesGenerated";
|
import { CreateTemplatePageViewProps } from "./types";
|
||||||
|
|
||||||
type ImportStarterTemplateViewProps = {
|
export const ImportStarterTemplateView: FC<CreateTemplatePageViewProps> = ({
|
||||||
onSuccess: (template: Template) => void;
|
onCreateTemplate,
|
||||||
};
|
onOpenBuildLogsDrawer,
|
||||||
|
variablesSectionRef,
|
||||||
export const ImportStarterTemplateView: FC<ImportStarterTemplateViewProps> = ({
|
error,
|
||||||
onSuccess,
|
isCreating,
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
@ -41,19 +40,17 @@ export const ImportStarterTemplateView: FC<ImportStarterTemplateViewProps> = ({
|
|||||||
const dashboard = useDashboard();
|
const dashboard = useDashboard();
|
||||||
const formPermissions = getFormPermissions(dashboard.entitlements);
|
const formPermissions = getFormPermissions(dashboard.entitlements);
|
||||||
|
|
||||||
const createTemplateMutation = useMutation(createTemplate());
|
const isJobError = error instanceof JobError;
|
||||||
const createError = createTemplateMutation.error;
|
|
||||||
const isJobError = createError instanceof JobError;
|
|
||||||
const templateVersionLogsQuery = useQuery({
|
const templateVersionLogsQuery = useQuery({
|
||||||
...templateVersionLogs(isJobError ? createError.version.id : ""),
|
...templateVersionLogs(isJobError ? error.version.id : ""),
|
||||||
enabled: isJobError,
|
enabled: isJobError,
|
||||||
});
|
});
|
||||||
|
|
||||||
const missedVariables = useQuery({
|
const missedVariables = useQuery({
|
||||||
...templateVersionVariables(isJobError ? createError.version.id : ""),
|
...templateVersionVariables(isJobError ? error.version.id : ""),
|
||||||
|
keepPreviousData: true,
|
||||||
enabled:
|
enabled:
|
||||||
isJobError &&
|
isJobError && error.job.error_code === "REQUIRED_TEMPLATE_VARIABLES",
|
||||||
createError.job.error_code === "REQUIRED_TEMPLATE_VARIABLES",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
@ -67,15 +64,17 @@ export const ImportStarterTemplateView: FC<ImportStarterTemplateViewProps> = ({
|
|||||||
return (
|
return (
|
||||||
<CreateTemplateForm
|
<CreateTemplateForm
|
||||||
{...formPermissions}
|
{...formPermissions}
|
||||||
|
variablesSectionRef={variablesSectionRef}
|
||||||
|
onOpenBuildLogsDrawer={onOpenBuildLogsDrawer}
|
||||||
starterTemplate={templateExample!}
|
starterTemplate={templateExample!}
|
||||||
variables={missedVariables.data}
|
variables={missedVariables.data}
|
||||||
error={createTemplateMutation.error}
|
error={error}
|
||||||
isSubmitting={createTemplateMutation.isLoading}
|
isSubmitting={isCreating}
|
||||||
onCancel={() => navigate(-1)}
|
onCancel={() => navigate(-1)}
|
||||||
jobError={isJobError ? createError.job.error : undefined}
|
jobError={isJobError ? error.job.error : undefined}
|
||||||
logs={templateVersionLogsQuery.data}
|
logs={templateVersionLogsQuery.data}
|
||||||
onSubmit={async (formData) => {
|
onSubmit={async (formData) => {
|
||||||
const template = await createTemplateMutation.mutateAsync({
|
await onCreateTemplate({
|
||||||
organizationId,
|
organizationId,
|
||||||
version: firstVersionFromExample(
|
version: firstVersionFromExample(
|
||||||
templateExample!,
|
templateExample!,
|
||||||
@ -83,7 +82,6 @@ export const ImportStarterTemplateView: FC<ImportStarterTemplateViewProps> = ({
|
|||||||
),
|
),
|
||||||
template: newTemplate(formData),
|
template: newTemplate(formData),
|
||||||
});
|
});
|
||||||
onSuccess(template);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -3,7 +3,6 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import {
|
import {
|
||||||
templateVersionLogs,
|
templateVersionLogs,
|
||||||
JobError,
|
JobError,
|
||||||
createTemplate,
|
|
||||||
templateVersionVariables,
|
templateVersionVariables,
|
||||||
} from "api/queries/templates";
|
} from "api/queries/templates";
|
||||||
import { uploadFile } from "api/queries/files";
|
import { uploadFile } from "api/queries/files";
|
||||||
@ -11,15 +10,15 @@ import { useOrganizationId } from "contexts/auth/useOrganizationId";
|
|||||||
import { useDashboard } from "modules/dashboard/useDashboard";
|
import { useDashboard } from "modules/dashboard/useDashboard";
|
||||||
import { CreateTemplateForm } from "./CreateTemplateForm";
|
import { CreateTemplateForm } from "./CreateTemplateForm";
|
||||||
import { firstVersionFromFile, getFormPermissions, newTemplate } from "./utils";
|
import { firstVersionFromFile, getFormPermissions, newTemplate } from "./utils";
|
||||||
import { Template } from "api/typesGenerated";
|
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
|
import { CreateTemplatePageViewProps } from "./types";
|
||||||
|
|
||||||
type UploadTemplateViewProps = {
|
export const UploadTemplateView: FC<CreateTemplatePageViewProps> = ({
|
||||||
onSuccess: (template: Template) => void;
|
onCreateTemplate,
|
||||||
};
|
onOpenBuildLogsDrawer,
|
||||||
|
variablesSectionRef,
|
||||||
export const UploadTemplateView: FC<UploadTemplateViewProps> = ({
|
isCreating,
|
||||||
onSuccess,
|
error,
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
@ -30,29 +29,28 @@ export const UploadTemplateView: FC<UploadTemplateViewProps> = ({
|
|||||||
const uploadFileMutation = useMutation(uploadFile());
|
const uploadFileMutation = useMutation(uploadFile());
|
||||||
const uploadedFile = uploadFileMutation.data;
|
const uploadedFile = uploadFileMutation.data;
|
||||||
|
|
||||||
const createTemplateMutation = useMutation(createTemplate());
|
const isJobError = error instanceof JobError;
|
||||||
const createError = createTemplateMutation.error;
|
|
||||||
const isJobError = createError instanceof JobError;
|
|
||||||
const templateVersionLogsQuery = useQuery({
|
const templateVersionLogsQuery = useQuery({
|
||||||
...templateVersionLogs(isJobError ? createError.version.id : ""),
|
...templateVersionLogs(isJobError ? error.version.id : ""),
|
||||||
enabled: isJobError,
|
enabled: isJobError,
|
||||||
});
|
});
|
||||||
|
|
||||||
const missedVariables = useQuery({
|
const missedVariables = useQuery({
|
||||||
...templateVersionVariables(isJobError ? createError.version.id : ""),
|
...templateVersionVariables(isJobError ? error.version.id : ""),
|
||||||
enabled:
|
enabled:
|
||||||
isJobError &&
|
isJobError && error.job.error_code === "REQUIRED_TEMPLATE_VARIABLES",
|
||||||
createError.job.error_code === "REQUIRED_TEMPLATE_VARIABLES",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CreateTemplateForm
|
<CreateTemplateForm
|
||||||
{...formPermissions}
|
{...formPermissions}
|
||||||
|
onOpenBuildLogsDrawer={onOpenBuildLogsDrawer}
|
||||||
|
variablesSectionRef={variablesSectionRef}
|
||||||
variables={missedVariables.data}
|
variables={missedVariables.data}
|
||||||
error={createTemplateMutation.error}
|
error={error}
|
||||||
isSubmitting={createTemplateMutation.isLoading}
|
isSubmitting={isCreating}
|
||||||
onCancel={() => navigate(-1)}
|
onCancel={() => navigate(-1)}
|
||||||
jobError={isJobError ? createError.job.error : undefined}
|
jobError={isJobError ? error.job.error : undefined}
|
||||||
logs={templateVersionLogsQuery.data}
|
logs={templateVersionLogsQuery.data}
|
||||||
upload={{
|
upload={{
|
||||||
onUpload: uploadFileMutation.mutateAsync,
|
onUpload: uploadFileMutation.mutateAsync,
|
||||||
@ -61,7 +59,7 @@ export const UploadTemplateView: FC<UploadTemplateViewProps> = ({
|
|||||||
file: uploadFileMutation.variables,
|
file: uploadFileMutation.variables,
|
||||||
}}
|
}}
|
||||||
onSubmit={async (formData) => {
|
onSubmit={async (formData) => {
|
||||||
const template = await createTemplateMutation.mutateAsync({
|
await onCreateTemplate({
|
||||||
organizationId,
|
organizationId,
|
||||||
version: firstVersionFromFile(
|
version: firstVersionFromFile(
|
||||||
uploadedFile!.hash,
|
uploadedFile!.hash,
|
||||||
@ -69,7 +67,6 @@ export const UploadTemplateView: FC<UploadTemplateViewProps> = ({
|
|||||||
),
|
),
|
||||||
template: newTemplate(formData),
|
template: newTemplate(formData),
|
||||||
});
|
});
|
||||||
onSuccess(template);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
9
site/src/pages/CreateTemplatePage/types.ts
Normal file
9
site/src/pages/CreateTemplatePage/types.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { CreateTemplateOptions } from "api/queries/templates";
|
||||||
|
|
||||||
|
export type CreateTemplatePageViewProps = {
|
||||||
|
onCreateTemplate: (options: CreateTemplateOptions) => Promise<void>;
|
||||||
|
onOpenBuildLogsDrawer: () => void;
|
||||||
|
variablesSectionRef: React.RefObject<HTMLDivElement>;
|
||||||
|
error: unknown;
|
||||||
|
isCreating: boolean;
|
||||||
|
};
|
@ -30,7 +30,6 @@ const meta: Meta<typeof TemplateVersionEditor> = {
|
|||||||
template: MockTemplate,
|
template: MockTemplate,
|
||||||
templateVersion: MockTemplateVersion,
|
templateVersion: MockTemplateVersion,
|
||||||
defaultFileTree: MockTemplateVersionFileTree,
|
defaultFileTree: MockTemplateVersionFileTree,
|
||||||
onPreview: action("onPreview"),
|
|
||||||
onPublish: action("onPublish"),
|
onPublish: action("onPublish"),
|
||||||
onConfirmPublish: action("onConfirmPublish"),
|
onConfirmPublish: action("onConfirmPublish"),
|
||||||
onCancelPublish: action("onCancelPublish"),
|
onCancelPublish: action("onCancelPublish"),
|
||||||
|
@ -67,7 +67,7 @@ export interface TemplateVersionEditorProps {
|
|||||||
resources?: WorkspaceResource[];
|
resources?: WorkspaceResource[];
|
||||||
disablePreview?: boolean;
|
disablePreview?: boolean;
|
||||||
disableUpdate?: boolean;
|
disableUpdate?: boolean;
|
||||||
onPreview: (files: FileTree) => void;
|
onPreview: (files: FileTree) => Promise<void>;
|
||||||
onPublish: () => void;
|
onPublish: () => void;
|
||||||
onConfirmPublish: (data: PublishVersionData) => void;
|
onConfirmPublish: (data: PublishVersionData) => void;
|
||||||
onCancelPublish: () => void;
|
onCancelPublish: () => void;
|
||||||
@ -122,14 +122,14 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
|
|||||||
const [renameFileOpen, setRenameFileOpen] = useState<string>();
|
const [renameFileOpen, setRenameFileOpen] = useState<string>();
|
||||||
const [dirty, setDirty] = useState(false);
|
const [dirty, setDirty] = useState(false);
|
||||||
|
|
||||||
const triggerPreview = useCallback(() => {
|
const triggerPreview = useCallback(async () => {
|
||||||
onPreview(fileTree);
|
await onPreview(fileTree);
|
||||||
setSelectedTab("logs");
|
setSelectedTab("logs");
|
||||||
}, [fileTree, onPreview]);
|
}, [fileTree, onPreview]);
|
||||||
|
|
||||||
// Stop ctrl+s from saving files and make ctrl+enter trigger a preview.
|
// Stop ctrl+s from saving files and make ctrl+enter trigger a preview.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const keyListener = (event: KeyboardEvent) => {
|
const keyListener = async (event: KeyboardEvent) => {
|
||||||
if (!(navigator.platform.match("Mac") ? event.metaKey : event.ctrlKey)) {
|
if (!(navigator.platform.match("Mac") ? event.metaKey : event.ctrlKey)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -140,7 +140,7 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
|
|||||||
break;
|
break;
|
||||||
case "Enter":
|
case "Enter":
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
triggerPreview();
|
await triggerPreview();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -252,8 +252,8 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
|
|||||||
}
|
}
|
||||||
title="Build template (Ctrl + Enter)"
|
title="Build template (Ctrl + Enter)"
|
||||||
disabled={disablePreview}
|
disabled={disablePreview}
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
triggerPreview();
|
await triggerPreview();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Build
|
Build
|
||||||
@ -719,7 +719,7 @@ const styles = {
|
|||||||
// Hack to update logs header and lines
|
// Hack to update logs header and lines
|
||||||
"& .logs-header": {
|
"& .logs-header": {
|
||||||
border: 0,
|
border: 0,
|
||||||
padding: "0 16px",
|
padding: "8px 16px",
|
||||||
fontFamily: MONOSPACE_FONT_FAMILY,
|
fontFamily: MONOSPACE_FONT_FAMILY,
|
||||||
|
|
||||||
"&:first-of-type": {
|
"&:first-of-type": {
|
||||||
|
@ -4,7 +4,7 @@ import {
|
|||||||
} from "testHelpers/renderHelpers";
|
} from "testHelpers/renderHelpers";
|
||||||
import TemplateVersionEditorPage from "./TemplateVersionEditorPage";
|
import TemplateVersionEditorPage from "./TemplateVersionEditorPage";
|
||||||
import { render, screen, waitFor, within } from "@testing-library/react";
|
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 * as api from "api/api";
|
||||||
import {
|
import {
|
||||||
MockTemplate,
|
MockTemplate,
|
||||||
@ -21,6 +21,7 @@ import { RequireAuth } from "contexts/auth/RequireAuth";
|
|||||||
import { server } from "testHelpers/server";
|
import { server } from "testHelpers/server";
|
||||||
import { rest } from "msw";
|
import { rest } from "msw";
|
||||||
import { AppProviders } from "App";
|
import { AppProviders } from "App";
|
||||||
|
import { TemplateVersion } from "api/typesGenerated";
|
||||||
|
|
||||||
// For some reason this component in Jest is throwing a MUI style warning so,
|
// 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
|
// 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 buildTemplateVersion = async (
|
||||||
const user = userEvent.setup();
|
templateVersion: TemplateVersion,
|
||||||
renderTemplateEditorPage();
|
user: UserEvent,
|
||||||
const topbar = await screen.findByTestId("topbar");
|
topbar: HTMLElement,
|
||||||
|
) => {
|
||||||
// Build Template
|
|
||||||
jest.spyOn(api, "uploadFile").mockResolvedValueOnce({ hash: "hash" });
|
jest.spyOn(api, "uploadFile").mockResolvedValueOnce({ hash: "hash" });
|
||||||
const newTemplateVersion = {
|
jest.spyOn(api, "createTemplateVersion").mockResolvedValue({
|
||||||
...MockTemplateVersion,
|
...templateVersion,
|
||||||
id: "new-version-id",
|
job: {
|
||||||
name: "new-version",
|
...templateVersion.job,
|
||||||
};
|
status: "running",
|
||||||
jest
|
},
|
||||||
.spyOn(api, "createTemplateVersion")
|
});
|
||||||
.mockResolvedValue(newTemplateVersion);
|
|
||||||
jest
|
jest
|
||||||
.spyOn(api, "getTemplateVersionByName")
|
.spyOn(api, "getTemplateVersionByName")
|
||||||
.mockResolvedValue(newTemplateVersion);
|
.mockResolvedValue(templateVersion);
|
||||||
jest
|
jest
|
||||||
.spyOn(api, "watchBuildLogsByTemplateVersionId")
|
.spyOn(api, "watchBuildLogsByTemplateVersionId")
|
||||||
.mockImplementation((_, options) => {
|
.mockImplementation((_, options) => {
|
||||||
options.onMessage(MockWorkspaceBuildLogs[0]);
|
options.onMessage(MockWorkspaceBuildLogs[0]);
|
||||||
options.onDone();
|
options.onDone?.();
|
||||||
return jest.fn() as never;
|
const wsMock = {
|
||||||
|
close: jest.fn(),
|
||||||
|
} as unknown;
|
||||||
|
return wsMock as WebSocket;
|
||||||
});
|
});
|
||||||
const buildButton = within(topbar).getByRole("button", {
|
const buildButton = within(topbar).getByRole("button", {
|
||||||
name: "Build",
|
name: "Build",
|
||||||
});
|
});
|
||||||
await user.click(buildButton);
|
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
|
// Publish
|
||||||
const patchTemplateVersion = jest
|
const patchTemplateVersion = jest
|
||||||
@ -87,7 +103,6 @@ test("Use custom name, message and set it as active when publishing", async () =
|
|||||||
const updateActiveTemplateVersion = jest
|
const updateActiveTemplateVersion = jest
|
||||||
.spyOn(api, "updateActiveTemplateVersion")
|
.spyOn(api, "updateActiveTemplateVersion")
|
||||||
.mockResolvedValue({ message: "" });
|
.mockResolvedValue({ message: "" });
|
||||||
await within(topbar).findByText("Success");
|
|
||||||
const publishButton = within(topbar).getByRole("button", {
|
const publishButton = within(topbar).getByRole("button", {
|
||||||
name: "Publish",
|
name: "Publish",
|
||||||
});
|
});
|
||||||
@ -118,30 +133,12 @@ test("Do not mark as active if promote is not checked", async () => {
|
|||||||
renderTemplateEditorPage();
|
renderTemplateEditorPage();
|
||||||
const topbar = await screen.findByTestId("topbar");
|
const topbar = await screen.findByTestId("topbar");
|
||||||
|
|
||||||
// Build Template
|
|
||||||
jest.spyOn(api, "uploadFile").mockResolvedValueOnce({ hash: "hash" });
|
|
||||||
const newTemplateVersion = {
|
const newTemplateVersion = {
|
||||||
...MockTemplateVersion,
|
...MockTemplateVersion,
|
||||||
id: "new-version-id",
|
id: "new-version-id",
|
||||||
name: "new-version",
|
name: "new-version",
|
||||||
};
|
};
|
||||||
jest
|
await buildTemplateVersion(newTemplateVersion, user, topbar);
|
||||||
.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);
|
|
||||||
|
|
||||||
// Publish
|
// Publish
|
||||||
const patchTemplateVersion = jest
|
const patchTemplateVersion = jest
|
||||||
@ -150,7 +147,6 @@ test("Do not mark as active if promote is not checked", async () => {
|
|||||||
const updateActiveTemplateVersion = jest
|
const updateActiveTemplateVersion = jest
|
||||||
.spyOn(api, "updateActiveTemplateVersion")
|
.spyOn(api, "updateActiveTemplateVersion")
|
||||||
.mockResolvedValue({ message: "" });
|
.mockResolvedValue({ message: "" });
|
||||||
await within(topbar).findByText("Success");
|
|
||||||
const publishButton = within(topbar).getByRole("button", {
|
const publishButton = within(topbar).getByRole("button", {
|
||||||
name: "Publish",
|
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 () => {
|
test("Patch request is not send when there are no changes", async () => {
|
||||||
const newTemplateVersion = {
|
|
||||||
...MockTemplateVersion,
|
|
||||||
id: "new-version-id",
|
|
||||||
name: "new-version",
|
|
||||||
};
|
|
||||||
const MockTemplateVersionWithEmptyMessage = {
|
|
||||||
...newTemplateVersion,
|
|
||||||
message: "",
|
|
||||||
};
|
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
renderTemplateEditorPage();
|
renderTemplateEditorPage();
|
||||||
const topbar = await screen.findByTestId("topbar");
|
const topbar = await screen.findByTestId("topbar");
|
||||||
|
|
||||||
// Build Template
|
const newTemplateVersion = {
|
||||||
jest.spyOn(api, "uploadFile").mockResolvedValueOnce({ hash: "hash" });
|
...MockTemplateVersion,
|
||||||
jest
|
id: "new-version-id",
|
||||||
.spyOn(api, "createTemplateVersion")
|
name: "new-version",
|
||||||
.mockResolvedValue(MockTemplateVersionWithEmptyMessage);
|
message: "",
|
||||||
jest
|
};
|
||||||
.spyOn(api, "getTemplateVersionByName")
|
await buildTemplateVersion(newTemplateVersion, user, topbar);
|
||||||
.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);
|
|
||||||
|
|
||||||
// Publish
|
// Publish
|
||||||
const patchTemplateVersion = jest
|
const patchTemplateVersion = jest
|
||||||
.spyOn(api, "patchTemplateVersion")
|
.spyOn(api, "patchTemplateVersion")
|
||||||
.mockResolvedValue(MockTemplateVersionWithEmptyMessage);
|
.mockResolvedValue(newTemplateVersion);
|
||||||
await within(topbar).findByText("Success");
|
|
||||||
const publishButton = within(topbar).getByRole("button", {
|
const publishButton = within(topbar).getByRole("button", {
|
||||||
name: "Publish",
|
name: "Publish",
|
||||||
});
|
});
|
||||||
@ -220,7 +194,7 @@ test("Patch request is not send when there are no changes", async () => {
|
|||||||
const publishDialog = await screen.findByTestId("dialog");
|
const publishDialog = await screen.findByTestId("dialog");
|
||||||
// It is using the name from the template
|
// It is using the name from the template
|
||||||
const nameField = within(publishDialog).getByLabelText("Version name");
|
const nameField = within(publishDialog).getByLabelText("Version name");
|
||||||
expect(nameField).toHaveValue(MockTemplateVersionWithEmptyMessage.name);
|
expect(nameField).toHaveValue(newTemplateVersion.name);
|
||||||
// Publish
|
// Publish
|
||||||
await user.click(
|
await user.click(
|
||||||
within(publishDialog).getByRole("button", { name: "Publish" }),
|
within(publishDialog).getByRole("button", { name: "Publish" }),
|
||||||
|
@ -5,14 +5,9 @@ import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
|||||||
import { TemplateVersionEditor } from "./TemplateVersionEditor";
|
import { TemplateVersionEditor } from "./TemplateVersionEditor";
|
||||||
import { useOrganizationId } from "contexts/auth/useOrganizationId";
|
import { useOrganizationId } from "contexts/auth/useOrganizationId";
|
||||||
import { pageTitle } from "utils/page";
|
import { pageTitle } from "utils/page";
|
||||||
import {
|
import { patchTemplateVersion, updateActiveTemplateVersion } from "api/api";
|
||||||
patchTemplateVersion,
|
|
||||||
updateActiveTemplateVersion,
|
|
||||||
watchBuildLogsByTemplateVersionId,
|
|
||||||
} from "api/api";
|
|
||||||
import type {
|
import type {
|
||||||
PatchTemplateVersionRequest,
|
PatchTemplateVersionRequest,
|
||||||
ProvisionerJobLog,
|
|
||||||
TemplateVersion,
|
TemplateVersion,
|
||||||
} from "api/typesGenerated";
|
} from "api/typesGenerated";
|
||||||
import {
|
import {
|
||||||
@ -28,6 +23,7 @@ import { FileTree, traverse } from "utils/filetree";
|
|||||||
import { createTemplateVersionFileTree } from "utils/templateVersion";
|
import { createTemplateVersionFileTree } from "utils/templateVersion";
|
||||||
import { displayError } from "components/GlobalSnackbar/utils";
|
import { displayError } from "components/GlobalSnackbar/utils";
|
||||||
import { FullScreenLoader } from "components/Loader/FullScreenLoader";
|
import { FullScreenLoader } from "components/Loader/FullScreenLoader";
|
||||||
|
import { useWatchVersionLogs } from "modules/templates/useWatchVersionLogs";
|
||||||
|
|
||||||
type Params = {
|
type Params = {
|
||||||
version: string;
|
version: string;
|
||||||
@ -58,7 +54,7 @@ export const TemplateVersionEditorPage: FC = () => {
|
|||||||
...resources(templateVersionQuery.data?.id ?? ""),
|
...resources(templateVersionQuery.data?.id ?? ""),
|
||||||
enabled: templateVersionQuery.data?.job.status === "succeeded",
|
enabled: templateVersionQuery.data?.job.status === "succeeded",
|
||||||
});
|
});
|
||||||
const { logs, setLogs } = useVersionLogs(templateVersionQuery.data, {
|
const logs = useWatchVersionLogs(templateVersionQuery.data, {
|
||||||
onDone: templateVersionQuery.refetch,
|
onDone: templateVersionQuery.refetch,
|
||||||
});
|
});
|
||||||
const { fileTree, tarFile } = useFileTree(templateVersionQuery.data);
|
const { fileTree, tarFile } = useFileTree(templateVersionQuery.data);
|
||||||
@ -97,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) => {
|
const onBuildEnds = (newVersion: TemplateVersion) => {
|
||||||
queryClient.setQueryData(templateVersionOptions.queryKey, newVersion);
|
queryClient.setQueryData(templateVersionOptions.queryKey, newVersion);
|
||||||
navigateToVersion(newVersion);
|
navigateToVersion(newVersion);
|
||||||
@ -145,7 +125,6 @@ export const TemplateVersionEditorPage: FC = () => {
|
|||||||
if (!tarFile) {
|
if (!tarFile) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onBuildStart();
|
|
||||||
const newVersionFile = await generateVersionFiles(
|
const newVersionFile = await generateVersionFiles(
|
||||||
tarFile,
|
tarFile,
|
||||||
newFileTree,
|
newFileTree,
|
||||||
@ -159,6 +138,7 @@ export const TemplateVersionEditorPage: FC = () => {
|
|||||||
template_id: templateQuery.data.id,
|
template_id: templateQuery.data.id,
|
||||||
file_id: serverFile.hash,
|
file_id: serverFile.hash,
|
||||||
});
|
});
|
||||||
|
|
||||||
onBuildEnds(newVersion);
|
onBuildEnds(newVersion);
|
||||||
}}
|
}}
|
||||||
onPublish={() => {
|
onPublish={() => {
|
||||||
@ -218,7 +198,6 @@ export const TemplateVersionEditorPage: FC = () => {
|
|||||||
if (!uploadFileMutation.data) {
|
if (!uploadFileMutation.data) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onBuildStart();
|
|
||||||
const newVersion = await createTemplateVersionMutation.mutateAsync({
|
const newVersion = await createTemplateVersionMutation.mutateAsync({
|
||||||
provisioner: "terraform",
|
provisioner: "terraform",
|
||||||
storage_method: "file",
|
storage_method: "file",
|
||||||
@ -276,45 +255,6 @@ const useFileTree = (templateVersion: TemplateVersion | undefined) => {
|
|||||||
return state;
|
return state;
|
||||||
};
|
};
|
||||||
|
|
||||||
const useVersionLogs = (
|
|
||||||
templateVersion: TemplateVersion | undefined,
|
|
||||||
options: { onDone: () => Promise<unknown> },
|
|
||||||
) => {
|
|
||||||
const [logs, setLogs] = useState<ProvisionerJobLog[]>();
|
|
||||||
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 useMissingVariables = (templateVersion: TemplateVersion | undefined) => {
|
||||||
const isRequiringVariables =
|
const isRequiringVariables =
|
||||||
templateVersion?.job.error_code === "REQUIRED_TEMPLATE_VARIABLES";
|
templateVersion?.job.error_code === "REQUIRED_TEMPLATE_VARIABLES";
|
||||||
|
@ -33,7 +33,7 @@ const renderWorkspacePage = async (workspace: Workspace) => {
|
|||||||
jest
|
jest
|
||||||
.spyOn(api, "watchWorkspaceAgentLogs")
|
.spyOn(api, "watchWorkspaceAgentLogs")
|
||||||
.mockImplementation((_, options) => {
|
.mockImplementation((_, options) => {
|
||||||
options.onDone && options.onDone();
|
options.onDone?.();
|
||||||
return new WebSocket("");
|
return new WebSocket("");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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) => {
|
rest.get("/api/v2/files/:fileId", (_, res, ctx) => {
|
||||||
const fileBuffer = fs.readFileSync(
|
const fileBuffer = fs.readFileSync(
|
||||||
path.resolve(__dirname, "./templateFiles.tar"),
|
path.resolve(__dirname, "./templateFiles.tar"),
|
||||||
|
@ -44,3 +44,30 @@ export const withDashboardProvider = (
|
|||||||
</DashboardContext.Provider>
|
</DashboardContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
window.WebSocket = function () {
|
||||||
|
return {
|
||||||
|
addEventListener: (
|
||||||
|
type: string,
|
||||||
|
callback: (ev: Record<"data", string>) => void,
|
||||||
|
) => {
|
||||||
|
if (type === "message") {
|
||||||
|
parameters.webSocket?.messages.forEach((message) => {
|
||||||
|
callback({ data: message });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
close: () => {},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Story />;
|
||||||
|
};
|
||||||
|
Reference in New Issue
Block a user