mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore(site): remove template version editor xservice (#10490)
Close https://github.com/coder/coder/issues/9942
This commit is contained in:
@@ -5,3 +5,10 @@ export const uploadFile = () => {
|
||||
mutationFn: API.uploadFile,
|
||||
};
|
||||
};
|
||||
|
||||
export const file = (fileId: string) => {
|
||||
return {
|
||||
queryKey: ["files", fileId],
|
||||
queryFn: () => API.getFile(fileId),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -129,6 +129,15 @@ export const templateVersionVariables = (versionId: string) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const createTemplateVersion = (orgId: string) => {
|
||||
return {
|
||||
mutationFn: async (request: CreateTemplateVersionRequest) => {
|
||||
const newVersion = await API.createTemplateVersion(orgId, request);
|
||||
return newVersion;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const createAndBuildTemplateVersion = (orgId: string) => {
|
||||
return {
|
||||
mutationFn: async (request: CreateTemplateVersionRequest) => {
|
||||
@@ -216,6 +225,13 @@ export const richParameters = (versionId: string) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const resources = (versionId: string) => {
|
||||
return {
|
||||
queryKey: ["templateVersion", versionId, "resources"],
|
||||
queryFn: () => API.getTemplateVersionResources(versionId),
|
||||
};
|
||||
};
|
||||
|
||||
const waitBuildToBeFinished = async (version: TemplateVersion) => {
|
||||
let data: TemplateVersion;
|
||||
let jobStatus: ProvisionerJobStatus;
|
||||
|
||||
@@ -34,9 +34,9 @@ type Story = StoryObj<typeof TemplateVersionEditor>;
|
||||
|
||||
export const Example: Story = {};
|
||||
|
||||
export const Logs = {
|
||||
export const Logs: Story = {
|
||||
args: {
|
||||
isBuildingNewVersion: true,
|
||||
defaultTab: "logs",
|
||||
buildLogs: MockWorkspaceBuildLogs,
|
||||
templateVersion: {
|
||||
...MockTemplateVersion,
|
||||
@@ -47,7 +47,7 @@ export const Logs = {
|
||||
|
||||
export const Resources: Story = {
|
||||
args: {
|
||||
isBuildingNewVersion: true,
|
||||
defaultTab: "resources",
|
||||
buildLogs: MockWorkspaceBuildLogs,
|
||||
resources: [
|
||||
MockWorkspaceResource,
|
||||
@@ -60,9 +60,9 @@ export const Resources: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const ManyLogs = {
|
||||
export const WithError = {
|
||||
args: {
|
||||
isBuildingNewVersion: true,
|
||||
defaultTab: "logs",
|
||||
templateVersion: {
|
||||
...MockTemplateVersion,
|
||||
job: {
|
||||
|
||||
@@ -49,10 +49,10 @@ import AlertTitle from "@mui/material/AlertTitle";
|
||||
import { DashboardFullPage } from "components/Dashboard/DashboardLayout";
|
||||
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
|
||||
|
||||
type Tab = "logs" | "resources" | undefined; // Undefined is to hide the tab
|
||||
export interface TemplateVersionEditorProps {
|
||||
template: Template;
|
||||
templateVersion: TemplateVersion;
|
||||
isBuildingNewVersion: boolean;
|
||||
defaultFileTree: FileTree;
|
||||
buildLogs?: ProvisionerJobLog[];
|
||||
resources?: WorkspaceResource[];
|
||||
@@ -71,6 +71,7 @@ export interface TemplateVersionEditorProps {
|
||||
missingVariables?: TemplateVersionVariable[];
|
||||
onSubmitMissingVariableValues: (values: VariableValue[]) => void;
|
||||
onCancelSubmitMissingVariableValues: () => void;
|
||||
defaultTab?: Tab;
|
||||
}
|
||||
|
||||
const topbarHeight = 80;
|
||||
@@ -92,7 +93,6 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
|
||||
disableUpdate,
|
||||
template,
|
||||
templateVersion,
|
||||
isBuildingNewVersion,
|
||||
defaultFileTree,
|
||||
onPreview,
|
||||
onPublish,
|
||||
@@ -109,11 +109,10 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
|
||||
missingVariables,
|
||||
onSubmitMissingVariableValues,
|
||||
onCancelSubmitMissingVariableValues,
|
||||
defaultTab,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
// If resources are provided, show them by default!
|
||||
// This is for Storybook!
|
||||
const [selectedTab, setSelectedTab] = useState(() => (resources ? 1 : 0));
|
||||
const [selectedTab, setSelectedTab] = useState<Tab>(defaultTab);
|
||||
const [fileTree, setFileTree] = useState(defaultFileTree);
|
||||
const [createFileOpen, setCreateFileOpen] = useState(false);
|
||||
const [deleteFileOpen, setDeleteFileOpen] = useState<string>();
|
||||
@@ -125,8 +124,7 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
|
||||
|
||||
const triggerPreview = useCallback(() => {
|
||||
onPreview(fileTree);
|
||||
// Switch to the build log!
|
||||
setSelectedTab(0);
|
||||
setSelectedTab("logs");
|
||||
}, [fileTree, onPreview]);
|
||||
|
||||
// Stop ctrl+s from saving files and make ctrl+enter trigger a preview.
|
||||
@@ -159,11 +157,12 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
|
||||
previousVersion.current = templateVersion;
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
["running", "pending"].includes(previousVersion.current.job.status) &&
|
||||
templateVersion.job.status === "succeeded"
|
||||
) {
|
||||
setSelectedTab(1);
|
||||
setSelectedTab("resources");
|
||||
setDirty(false);
|
||||
}
|
||||
previousVersion.current = templateVersion;
|
||||
@@ -218,7 +217,7 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
|
||||
)}
|
||||
|
||||
<div css={styles.topbarSides}>
|
||||
{isBuildingNewVersion && (
|
||||
{buildLogs && (
|
||||
<TemplateVersionStatusBadge version={templateVersion} />
|
||||
)}
|
||||
|
||||
@@ -358,9 +357,9 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
|
||||
<div css={styles.tabs}>
|
||||
<button
|
||||
css={styles.tab}
|
||||
className={selectedTab === 0 ? "active" : ""}
|
||||
className={selectedTab === "logs" ? "active" : ""}
|
||||
onClick={() => {
|
||||
setSelectedTab(0);
|
||||
setSelectedTab("logs");
|
||||
}}
|
||||
>
|
||||
{templateVersion.job.status !== "succeeded" ? (
|
||||
@@ -374,9 +373,9 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
|
||||
{!disableUpdate && (
|
||||
<button
|
||||
css={styles.tab}
|
||||
className={selectedTab === 1 ? "active" : ""}
|
||||
className={selectedTab === "resources" ? "active" : ""}
|
||||
onClick={() => {
|
||||
setSelectedTab(1);
|
||||
setSelectedTab("resources");
|
||||
}}
|
||||
>
|
||||
<PreviewIcon />
|
||||
@@ -389,7 +388,7 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
|
||||
css={[
|
||||
styles.panel,
|
||||
{
|
||||
display: selectedTab !== 0 ? "none" : "flex",
|
||||
display: selectedTab !== "logs" ? "none" : "flex",
|
||||
flexDirection: "column",
|
||||
},
|
||||
]}
|
||||
@@ -427,7 +426,7 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
|
||||
styles.panel,
|
||||
{
|
||||
paddingBottom: theme.spacing(2),
|
||||
display: selectedTab !== 1 ? "none" : undefined,
|
||||
display: selectedTab !== "resources" ? "none" : undefined,
|
||||
},
|
||||
]}
|
||||
>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { screen, waitFor, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import * as api from "api/api";
|
||||
import {
|
||||
MockTemplate,
|
||||
MockTemplateVersion,
|
||||
MockWorkspaceBuildLogs,
|
||||
} from "testHelpers/entities";
|
||||
@@ -17,9 +18,10 @@ jest.mock("components/TemplateResourcesTable/TemplateResourcesTable", () => {
|
||||
};
|
||||
});
|
||||
|
||||
test("Use custom name, message and set it as active when publishing", async () => {
|
||||
const user = userEvent.setup();
|
||||
const renderTemplateEditorPage = () => {
|
||||
renderWithAuth(<TemplateVersionEditorPage />, {
|
||||
route: `/templates/${MockTemplate.name}/versions/${MockTemplateVersion.name}/edit`,
|
||||
path: "/templates/:template/versions/:version/edit",
|
||||
extraRoutes: [
|
||||
{
|
||||
path: "/templates/:templateId",
|
||||
@@ -27,16 +29,26 @@ test("Use custom name, message and set it as active when publishing", async () =
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
test("Use custom name, message and set it as active when publishing", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderTemplateEditorPage();
|
||||
const topbar = await screen.findByTestId("topbar");
|
||||
|
||||
// Build Template
|
||||
jest.spyOn(api, "uploadFile").mockResolvedValueOnce({ hash: "hash" });
|
||||
const newTemplateVersion = {
|
||||
...MockTemplateVersion,
|
||||
id: "new-version-id",
|
||||
name: "new-version",
|
||||
};
|
||||
jest
|
||||
.spyOn(api, "createTemplateVersion")
|
||||
.mockResolvedValueOnce(MockTemplateVersion);
|
||||
.mockResolvedValue(newTemplateVersion);
|
||||
jest
|
||||
.spyOn(api, "getTemplateVersion")
|
||||
.mockResolvedValue({ ...MockTemplateVersion, id: "new-version-id" });
|
||||
.spyOn(api, "getTemplateVersionByName")
|
||||
.mockResolvedValue(newTemplateVersion);
|
||||
jest
|
||||
.spyOn(api, "watchBuildLogsByTemplateVersionId")
|
||||
.mockImplementation((_, options) => {
|
||||
@@ -52,7 +64,7 @@ test("Use custom name, message and set it as active when publishing", async () =
|
||||
// Publish
|
||||
const patchTemplateVersion = jest
|
||||
.spyOn(api, "patchTemplateVersion")
|
||||
.mockResolvedValue(MockTemplateVersion);
|
||||
.mockResolvedValue(newTemplateVersion);
|
||||
const updateActiveTemplateVersion = jest
|
||||
.spyOn(api, "updateActiveTemplateVersion")
|
||||
.mockResolvedValue({ message: "" });
|
||||
@@ -84,24 +96,22 @@ test("Use custom name, message and set it as active when publishing", async () =
|
||||
|
||||
test("Do not mark as active if promote is not checked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithAuth(<TemplateVersionEditorPage />, {
|
||||
extraRoutes: [
|
||||
{
|
||||
path: "/templates/:templateId",
|
||||
element: <div />,
|
||||
},
|
||||
],
|
||||
});
|
||||
renderTemplateEditorPage();
|
||||
const topbar = await screen.findByTestId("topbar");
|
||||
|
||||
// Build Template
|
||||
jest.spyOn(api, "uploadFile").mockResolvedValueOnce({ hash: "hash" });
|
||||
const newTemplateVersion = {
|
||||
...MockTemplateVersion,
|
||||
id: "new-version-id",
|
||||
name: "new-version",
|
||||
};
|
||||
jest
|
||||
.spyOn(api, "createTemplateVersion")
|
||||
.mockResolvedValueOnce(MockTemplateVersion);
|
||||
.mockResolvedValue(newTemplateVersion);
|
||||
jest
|
||||
.spyOn(api, "getTemplateVersion")
|
||||
.mockResolvedValue({ ...MockTemplateVersion, id: "new-version-id" });
|
||||
.spyOn(api, "getTemplateVersionByName")
|
||||
.mockResolvedValue(newTemplateVersion);
|
||||
jest
|
||||
.spyOn(api, "watchBuildLogsByTemplateVersionId")
|
||||
.mockImplementation((_, options) => {
|
||||
@@ -117,7 +127,7 @@ test("Do not mark as active if promote is not checked", async () => {
|
||||
// Publish
|
||||
const patchTemplateVersion = jest
|
||||
.spyOn(api, "patchTemplateVersion")
|
||||
.mockResolvedValue(MockTemplateVersion);
|
||||
.mockResolvedValue(newTemplateVersion);
|
||||
const updateActiveTemplateVersion = jest
|
||||
.spyOn(api, "updateActiveTemplateVersion")
|
||||
.mockResolvedValue({ message: "" });
|
||||
@@ -146,30 +156,27 @@ test("Do not mark as active if promote is not checked", async () => {
|
||||
});
|
||||
|
||||
test("Patch request is not send when there are no changes", async () => {
|
||||
const MockTemplateVersionWithEmptyMessage = {
|
||||
const newTemplateVersion = {
|
||||
...MockTemplateVersion,
|
||||
id: "new-version-id",
|
||||
name: "new-version",
|
||||
};
|
||||
const MockTemplateVersionWithEmptyMessage = {
|
||||
...newTemplateVersion,
|
||||
message: "",
|
||||
};
|
||||
const user = userEvent.setup();
|
||||
renderWithAuth(<TemplateVersionEditorPage />, {
|
||||
extraRoutes: [
|
||||
{
|
||||
path: "/templates/:templateId",
|
||||
element: <div />,
|
||||
},
|
||||
],
|
||||
});
|
||||
renderTemplateEditorPage();
|
||||
const topbar = await screen.findByTestId("topbar");
|
||||
|
||||
// Build Template
|
||||
jest.spyOn(api, "uploadFile").mockResolvedValueOnce({ hash: "hash" });
|
||||
jest
|
||||
.spyOn(api, "createTemplateVersion")
|
||||
.mockResolvedValueOnce(MockTemplateVersionWithEmptyMessage);
|
||||
jest.spyOn(api, "getTemplateVersion").mockResolvedValue({
|
||||
...MockTemplateVersionWithEmptyMessage,
|
||||
id: "new-version-id",
|
||||
});
|
||||
.mockResolvedValue(MockTemplateVersionWithEmptyMessage);
|
||||
jest
|
||||
.spyOn(api, "getTemplateVersionByName")
|
||||
.mockResolvedValue(MockTemplateVersionWithEmptyMessage);
|
||||
jest
|
||||
.spyOn(api, "watchBuildLogsByTemplateVersionId")
|
||||
.mockImplementation((_, options) => {
|
||||
|
||||
@@ -1,12 +1,35 @@
|
||||
import { useMachine } from "@xstate/react";
|
||||
import { TemplateVersionEditor } from "./TemplateVersionEditor";
|
||||
import { useOrganizationId } from "hooks/useOrganizationId";
|
||||
import { FC } from "react";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { pageTitle } from "utils/page";
|
||||
import { templateVersionEditorMachine } from "xServices/templateVersionEditor/templateVersionEditorXService";
|
||||
import { useTemplateVersionData } from "./data";
|
||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import {
|
||||
createTemplateVersion,
|
||||
resources,
|
||||
templateByName,
|
||||
templateVersionByName,
|
||||
templateVersionVariables,
|
||||
} from "api/queries/templates";
|
||||
import { file, uploadFile } from "api/queries/files";
|
||||
import { TarFileTypeCodes, TarReader, TarWriter } from "utils/tar";
|
||||
import { FileTree, traverse } from "utils/filetree";
|
||||
import {
|
||||
createTemplateVersionFileTree,
|
||||
isAllowedFile,
|
||||
} from "utils/templateVersion";
|
||||
import {
|
||||
patchTemplateVersion,
|
||||
updateActiveTemplateVersion,
|
||||
watchBuildLogsByTemplateVersionId,
|
||||
} from "api/api";
|
||||
import {
|
||||
PatchTemplateVersionRequest,
|
||||
ProvisionerJobLog,
|
||||
TemplateVersion,
|
||||
} from "api/typesGenerated";
|
||||
import { displayError } from "components/GlobalSnackbar/utils";
|
||||
|
||||
type Params = {
|
||||
version: string;
|
||||
@@ -14,25 +37,69 @@ type Params = {
|
||||
};
|
||||
|
||||
export const TemplateVersionEditorPage: FC = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const { version: versionName, template: templateName } =
|
||||
const { version: initialVersionName, template: templateName } =
|
||||
useParams() as Params;
|
||||
const orgId = useOrganizationId();
|
||||
const [editorState, sendEvent] = useMachine(templateVersionEditorMachine, {
|
||||
context: { orgId },
|
||||
});
|
||||
const { isSuccess, data } = useTemplateVersionData(
|
||||
{
|
||||
orgId,
|
||||
templateName,
|
||||
versionName,
|
||||
},
|
||||
{
|
||||
onSuccess(data) {
|
||||
sendEvent({ type: "INITIALIZE", tarReader: data.tarReader });
|
||||
},
|
||||
},
|
||||
const [currentVersionName, setCurrentVersionName] =
|
||||
useState(initialVersionName);
|
||||
const templateQuery = useQuery(templateByName(orgId, templateName));
|
||||
const templateVersionOptions = templateVersionByName(
|
||||
orgId,
|
||||
templateName,
|
||||
currentVersionName,
|
||||
);
|
||||
const templateVersionQuery = useQuery({
|
||||
...templateVersionOptions,
|
||||
keepPreviousData: true,
|
||||
});
|
||||
const uploadFileMutation = useMutation(uploadFile());
|
||||
const createTemplateVersionMutation = useMutation(
|
||||
createTemplateVersion(orgId),
|
||||
);
|
||||
const resourcesQuery = useQuery({
|
||||
...resources(templateVersionQuery.data?.id ?? ""),
|
||||
enabled: templateVersionQuery.data?.job.status === "succeeded",
|
||||
});
|
||||
const { logs, setLogs } = useVersionLogs(templateVersionQuery.data, {
|
||||
onDone: templateVersionQuery.refetch,
|
||||
});
|
||||
const { fileTree, tarFile } = useFileTree(templateVersionQuery.data);
|
||||
const {
|
||||
missingVariables,
|
||||
setIsMissingVariablesDialogOpen,
|
||||
isMissingVariablesDialogOpen,
|
||||
} = useMissingVariables(templateVersionQuery.data);
|
||||
|
||||
// Handle template publishing
|
||||
const [isPublishingDialogOpen, setIsPublishingDialogOpen] = useState(false);
|
||||
const publishVersionMutation = useMutation({
|
||||
mutationFn: publishVersion,
|
||||
});
|
||||
const [lastSuccessfulPublishedVersion, setLastSuccessfulPublishedVersion] =
|
||||
useState<TemplateVersion>();
|
||||
|
||||
// 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) => {
|
||||
setCurrentVersionName(newVersion.name);
|
||||
queryClient.setQueryData(templateVersionOptions.queryKey, newVersion);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -40,45 +107,54 @@ export const TemplateVersionEditorPage: FC = () => {
|
||||
<title>{pageTitle(`${templateName} · Template Editor`)}</title>
|
||||
</Helmet>
|
||||
|
||||
{isSuccess && (
|
||||
{templateQuery.data && templateVersionQuery.data && fileTree && (
|
||||
<TemplateVersionEditor
|
||||
template={data.template}
|
||||
templateVersion={editorState.context.version || data.version}
|
||||
isBuildingNewVersion={Boolean(editorState.context.version)}
|
||||
defaultFileTree={data.fileTree}
|
||||
onPreview={(fileTree) => {
|
||||
sendEvent({
|
||||
type: "CREATE_VERSION",
|
||||
fileTree,
|
||||
templateId: data.template.id,
|
||||
template={templateQuery.data}
|
||||
templateVersion={templateVersionQuery.data}
|
||||
defaultFileTree={fileTree}
|
||||
onPreview={async (newFileTree) => {
|
||||
if (!tarFile) {
|
||||
return;
|
||||
}
|
||||
onBuildStart();
|
||||
const newVersionFile = await generateVersionFiles(
|
||||
tarFile,
|
||||
newFileTree,
|
||||
);
|
||||
const serverFile = await uploadFileMutation.mutateAsync(
|
||||
newVersionFile,
|
||||
);
|
||||
const newVersion = await createTemplateVersionMutation.mutateAsync({
|
||||
provisioner: "terraform",
|
||||
storage_method: "file",
|
||||
tags: {},
|
||||
template_id: templateQuery.data.id,
|
||||
file_id: serverFile.hash,
|
||||
});
|
||||
onBuildEnds(newVersion);
|
||||
}}
|
||||
onPublish={() => {
|
||||
sendEvent({
|
||||
type: "PUBLISH",
|
||||
});
|
||||
setIsPublishingDialogOpen(true);
|
||||
}}
|
||||
onCancelPublish={() => {
|
||||
sendEvent({
|
||||
type: "CANCEL_PUBLISH",
|
||||
});
|
||||
setIsPublishingDialogOpen(false);
|
||||
}}
|
||||
onConfirmPublish={(data) => {
|
||||
sendEvent({
|
||||
type: "CONFIRM_PUBLISH",
|
||||
...data,
|
||||
onConfirmPublish={async ({ isActiveVersion, ...data }) => {
|
||||
await publishVersionMutation.mutateAsync({
|
||||
isActiveVersion,
|
||||
data,
|
||||
version: templateVersionQuery.data,
|
||||
});
|
||||
setIsPublishingDialogOpen(false);
|
||||
setLastSuccessfulPublishedVersion(templateVersionQuery.data);
|
||||
}}
|
||||
isAskingPublishParameters={editorState.matches(
|
||||
"askPublishParameters",
|
||||
)}
|
||||
isPublishing={editorState.matches("publishingVersion")}
|
||||
publishingError={editorState.context.publishingError}
|
||||
publishedVersion={editorState.context.lastSuccessfulPublishedVersion}
|
||||
isAskingPublishParameters={isPublishingDialogOpen}
|
||||
isPublishing={publishVersionMutation.isLoading}
|
||||
publishingError={publishVersionMutation.error}
|
||||
publishedVersion={lastSuccessfulPublishedVersion}
|
||||
onCreateWorkspace={() => {
|
||||
const params = new URLSearchParams();
|
||||
const publishedVersion =
|
||||
editorState.context.lastSuccessfulPublishedVersion;
|
||||
const publishedVersion = lastSuccessfulPublishedVersion;
|
||||
if (publishedVersion) {
|
||||
params.set("version", publishedVersion.id);
|
||||
}
|
||||
@@ -86,25 +162,40 @@ export const TemplateVersionEditorPage: FC = () => {
|
||||
`/templates/${templateName}/workspace?${params.toString()}`,
|
||||
);
|
||||
}}
|
||||
disablePreview={editorState.hasTag("loading")}
|
||||
disableUpdate={
|
||||
editorState.hasTag("loading") ||
|
||||
editorState.context.version?.job.status !== "succeeded"
|
||||
disablePreview={
|
||||
templateVersionQuery.data.job.status === "running" ||
|
||||
templateVersionQuery.data.job.status === "pending" ||
|
||||
createTemplateVersionMutation.isLoading ||
|
||||
uploadFileMutation.isLoading
|
||||
}
|
||||
resources={editorState.context.resources}
|
||||
buildLogs={editorState.context.buildLogs}
|
||||
isPromptingMissingVariables={editorState.matches("promptVariables")}
|
||||
missingVariables={editorState.context.missingVariables}
|
||||
onSubmitMissingVariableValues={(values) => {
|
||||
sendEvent({
|
||||
type: "SET_MISSING_VARIABLE_VALUES",
|
||||
values,
|
||||
disableUpdate={
|
||||
templateVersionQuery.data.job.status !== "succeeded" ||
|
||||
templateVersionQuery.data.name === initialVersionName ||
|
||||
templateVersionQuery.data.name ===
|
||||
lastSuccessfulPublishedVersion?.name
|
||||
}
|
||||
resources={resourcesQuery.data}
|
||||
buildLogs={logs}
|
||||
isPromptingMissingVariables={isMissingVariablesDialogOpen}
|
||||
missingVariables={missingVariables}
|
||||
onSubmitMissingVariableValues={async (values) => {
|
||||
if (!uploadFileMutation.data) {
|
||||
return;
|
||||
}
|
||||
onBuildStart();
|
||||
const newVersion = await createTemplateVersionMutation.mutateAsync({
|
||||
provisioner: "terraform",
|
||||
storage_method: "file",
|
||||
tags: {},
|
||||
template_id: templateQuery.data.id,
|
||||
file_id: uploadFileMutation.data.hash,
|
||||
user_variable_values: values,
|
||||
});
|
||||
onBuildEnds(newVersion);
|
||||
setIsMissingVariablesDialogOpen(false);
|
||||
}}
|
||||
onCancelSubmitMissingVariableValues={() => {
|
||||
sendEvent({
|
||||
type: "CANCEL_MISSING_VARIABLE_VALUES",
|
||||
});
|
||||
setIsMissingVariablesDialogOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -112,4 +203,163 @@ export const TemplateVersionEditorPage: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const useFileTree = (templateVersion: TemplateVersion | undefined) => {
|
||||
const fileQuery = useQuery({
|
||||
...file(templateVersion?.job.file_id ?? ""),
|
||||
enabled: templateVersion !== undefined,
|
||||
});
|
||||
const [state, setState] = useState<{
|
||||
fileTree?: FileTree;
|
||||
tarFile?: TarReader;
|
||||
}>({
|
||||
fileTree: undefined,
|
||||
tarFile: undefined,
|
||||
});
|
||||
useEffect(() => {
|
||||
const initializeFileTree = async (file: ArrayBuffer) => {
|
||||
const tarFile = new TarReader();
|
||||
await tarFile.readFile(file);
|
||||
const fileTree = await createTemplateVersionFileTree(tarFile);
|
||||
setState({ fileTree, tarFile });
|
||||
};
|
||||
|
||||
if (fileQuery.data) {
|
||||
initializeFileTree(fileQuery.data).catch(() => {
|
||||
displayError("Error on initializing the editor");
|
||||
});
|
||||
}
|
||||
}, [fileQuery.data]);
|
||||
|
||||
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 { data: missingVariables } = useQuery({
|
||||
...templateVersionVariables(templateVersion?.id ?? ""),
|
||||
enabled: templateVersion?.job.error_code === "REQUIRED_TEMPLATE_VARIABLES",
|
||||
});
|
||||
const [isMissingVariablesDialogOpen, setIsMissingVariablesDialogOpen] =
|
||||
useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (missingVariables) {
|
||||
setIsMissingVariablesDialogOpen(true);
|
||||
}
|
||||
}, [missingVariables]);
|
||||
|
||||
return {
|
||||
missingVariables,
|
||||
isMissingVariablesDialogOpen,
|
||||
setIsMissingVariablesDialogOpen,
|
||||
};
|
||||
};
|
||||
|
||||
const generateVersionFiles = async (
|
||||
tarReader: TarReader,
|
||||
fileTree: FileTree,
|
||||
) => {
|
||||
const tar = new TarWriter();
|
||||
|
||||
// Add previous non editable files
|
||||
for (const file of tarReader.fileInfo) {
|
||||
if (!isAllowedFile(file.name)) {
|
||||
if (file.type === TarFileTypeCodes.Dir) {
|
||||
tar.addFolder(file.name, {
|
||||
mode: file.mode, // https://github.com/beatgammit/tar-js/blob/master/lib/tar.js#L42
|
||||
mtime: file.mtime,
|
||||
user: file.user,
|
||||
group: file.group,
|
||||
});
|
||||
} else {
|
||||
tar.addFile(file.name, tarReader.getTextFile(file.name) as string, {
|
||||
mode: file.mode, // https://github.com/beatgammit/tar-js/blob/master/lib/tar.js#L42
|
||||
mtime: file.mtime,
|
||||
user: file.user,
|
||||
group: file.group,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// Add the editable files
|
||||
traverse(fileTree, (content, _filename, fullPath) => {
|
||||
// When a file is deleted. Don't add it to the tar.
|
||||
if (content === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof content === "string") {
|
||||
tar.addFile(fullPath, content);
|
||||
return;
|
||||
}
|
||||
|
||||
tar.addFolder(fullPath);
|
||||
});
|
||||
const blob = (await tar.write()) as Blob;
|
||||
return new File([blob], "template.tar");
|
||||
};
|
||||
|
||||
const publishVersion = async (options: {
|
||||
version: TemplateVersion;
|
||||
data: PatchTemplateVersionRequest;
|
||||
isActiveVersion: boolean;
|
||||
}) => {
|
||||
const { version, data, isActiveVersion } = options;
|
||||
const haveChanges =
|
||||
data.name !== version.name || data.message !== version.message;
|
||||
const publishActions: Promise<unknown>[] = [];
|
||||
|
||||
if (haveChanges) {
|
||||
publishActions.push(patchTemplateVersion(version.id, data));
|
||||
}
|
||||
|
||||
if (isActiveVersion) {
|
||||
publishActions.push(
|
||||
updateActiveTemplateVersion(version.template_id!, {
|
||||
id: version.id,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.all(publishActions);
|
||||
};
|
||||
|
||||
export default TemplateVersionEditorPage;
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import { useQuery, UseQueryOptions } from "react-query";
|
||||
import { getFile, getTemplateByName, getTemplateVersionByName } from "api/api";
|
||||
import { TarReader } from "utils/tar";
|
||||
import { createTemplateVersionFileTree } from "utils/templateVersion";
|
||||
|
||||
const getTemplateVersionData = async (
|
||||
orgId: string,
|
||||
templateName: string,
|
||||
versionName: string,
|
||||
) => {
|
||||
const [template, version] = await Promise.all([
|
||||
getTemplateByName(orgId, templateName),
|
||||
getTemplateVersionByName(orgId, templateName, versionName),
|
||||
]);
|
||||
const tarFile = await getFile(version.job.file_id);
|
||||
const tarReader = new TarReader();
|
||||
await tarReader.readFile(tarFile);
|
||||
const fileTree = await createTemplateVersionFileTree(tarReader);
|
||||
|
||||
return {
|
||||
template,
|
||||
version,
|
||||
fileTree,
|
||||
tarReader,
|
||||
};
|
||||
};
|
||||
|
||||
type GetTemplateVersionResponse = Awaited<
|
||||
ReturnType<typeof getTemplateVersionData>
|
||||
>;
|
||||
|
||||
type UseTemplateVersionDataParams = {
|
||||
orgId: string;
|
||||
templateName: string;
|
||||
versionName: string;
|
||||
};
|
||||
|
||||
export const useTemplateVersionData = (
|
||||
{ templateName, versionName, orgId }: UseTemplateVersionDataParams,
|
||||
options?: UseQueryOptions<GetTemplateVersionResponse>,
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: ["templateVersion", templateName, versionName],
|
||||
queryFn: () => getTemplateVersionData(orgId, templateName, versionName),
|
||||
...options,
|
||||
});
|
||||
};
|
||||
@@ -1,448 +0,0 @@
|
||||
import {
|
||||
ProvisionerJobLog,
|
||||
ProvisionerJobStatus,
|
||||
TemplateVersion,
|
||||
TemplateVersionVariable,
|
||||
UploadResponse,
|
||||
VariableValue,
|
||||
WorkspaceResource,
|
||||
} from "api/typesGenerated";
|
||||
import { assign, createMachine } from "xstate";
|
||||
import * as API from "api/api";
|
||||
import { FileTree, traverse } from "utils/filetree";
|
||||
import { isAllowedFile } from "utils/templateVersion";
|
||||
import { TarReader, TarWriter } from "utils/tar";
|
||||
import { PublishVersionData } from "pages/TemplateVersionEditorPage/types";
|
||||
|
||||
export interface TemplateVersionEditorMachineContext {
|
||||
orgId: string;
|
||||
templateId?: string;
|
||||
fileTree?: FileTree;
|
||||
uploadResponse?: UploadResponse;
|
||||
version?: TemplateVersion;
|
||||
resources?: WorkspaceResource[];
|
||||
buildLogs?: ProvisionerJobLog[];
|
||||
tarReader?: TarReader;
|
||||
publishingError?: unknown;
|
||||
lastSuccessfulPublishedVersion?: TemplateVersion;
|
||||
missingVariables?: TemplateVersionVariable[];
|
||||
missingVariableValues?: VariableValue[];
|
||||
}
|
||||
|
||||
export const templateVersionEditorMachine = createMachine(
|
||||
{
|
||||
/** @xstate-layout N4IgpgJg5mDOIC5QBcwFsAOAbAhqgamAE6wCWA9gHYCiEpy5RAdKZfaTlqQF6tQDEASQByggCqCAggBlBALWoBtAAwBdRKAzkyyCpQ0gAHogC0AFgDsypgDYAnAFZlFpzefKbADgA0IAJ6IAIzKngDMTIF2FmYONoFW9s4Avkm+qJi4BMRkVLT0jCwQWGD8AMIAStSSYtQA+vjU5QDKggDywirqSCBaOnoGxggmoTYOTKEATIEOThNxDmajvgEIgTFjDhaeNpPTZpbRKWno2HhghCR6eQzMpEUlAAoAqgBCsk0AEp0GvexUA6Y5mNlKEzBNPJ5Ap4JjCbEt-EEJsozEw7MoHKEQhYJqFPBYbBYjiB0qcspdcnQboVivwACKCJoAWQZTVqzzeDI+tRekmEwka326v10-26gxMcM8TGcoRGc0CNjMyg8yyCkzsqOUkWcnjMEJhRJJmXO2SulIKOFgAGsHgBXABGXFgAAsHjgiDg0GBUCQyrzStRpGzXu8vmofto-voxaZAvFxnZ9mFonY7NswarVuCLEwLPjQlCPGsJodUsSTsaLjkaObmJabQ6na73Z7vdkyu0AGKCcqM4Mcz6CzSRkXR0CDOM5ta6ya4jHbZR2TNx1O5iHTHEWQLgzyGitnKtm-LMDCN0guviHqj8CBUMAsSgAN3IVvvp8d5+dl9NVCHPRH-QxggSrWOioSOHCdhzCEMzLuCGoFqMkJmGsgRynuGQHj+NbHkw75Nt+5KUPwxBEAUpIAGaMGgeFnhelBQFelB-sKgHjkEcbStESKbGYaZFoEy6LlKcJePE4IzDYEwYaSJpEdcBQAMY4JQilgFwDGCJQDxkVARBwLALy2qQWAQDed4Ps+r5MMpqnqUZJkQCxAGiuxQyhA4UpmLiLhePYaEjMuFgFqibjOPmqbKNJZZGlh8m1kwtrYOQOAQGI7rmZQ96sFZ95JVgKVpe6zl9K5RimFETAljioI4mmdgjBYy7QhsepWE4DVQShMmVthCnMIp+l4HwDmmZl2VPi+96DWAZyjU54ZCi5Y7lcBDgTNKeITGm+LYutNjNRMrV4uii7gRM+w9XF1b9UwADueCKV+DHzdI5BQLA-CSLStLck8gjSL90itAA4iVUYAggjg5o4UxJutkzbEFDgakionbImMKeVdZI3QlD3IE9I3GaZb0ffwLz-YDtS0u0SiLcOpUrYMvlMJJowwgqiZpsumPSoWnjIrEMTBTjcl47hBNEy9JMQGTn2lP6gb1I0LTtODo6QyYeKoidaZxBd0JxMuUnWCjFipiMITeeBYtMbdUvPVAr3vQrlTVHUDTNG0HQM-+TNa3ENi5vEnjQ6m20o4dgS2GC6L1W4upmHbfUJRR3rS4x2HjZZU1MOnhPOkxGtsatUkorqmxWLKFuC4JCIIHDwcuAj60uKCiwp-FuEF5nTE5zlee90X2GKIEXSMxDQHBDsuZah5cbBHYAWZk3uYzJEDVxlJdhdxLVIYGRmDIPg7ocI6cBMAVqV8Iy55kAxp9EOfxSfbeWW59ZsW40eB9HxgJ8z44AvrAK+hVb730vEAkBCBB7KVHJ0EuZVBhhVRAFTwvFI5TFXrVJg3l9Qll1CEGwe9f7kX-oA5+wDX7UhKE0agYhajMiaC0YQIN6iSHKFIN4nsZBPGoE0JBzNEAEgiDuWUippgW28qvdaG0rBrB3iEKKhIYr7h-hSXCh9yDHyfi-S+dwaSK2EAGIMzDWHsPwJw7h0heHSH4YIv2rFkFBEklVUI0ipgNSsPsVeJZg5RCxmhWYkRQikM0VSYe5Q4DkFtEQNSb8LKD2sjAZA0TYCxPiXAIRkNixjDQpCDwgTtz4hNk4WwDgFTKgxHxLcYSiSUHIBAOABhv7izIUQCMAcgImC3MHGUcog4gQOg3VMYx7DREWDEaEkJorHEwhonCVJWDsE4DwPgXSp5uQlCFPpUIkTInsBmBucYPARGiFFSYJYPAFnCUsgohiwCbM1j0gs8jqlgjRKmbcy4PIbTxEnI6BYYIODubdesdoPwujdB6L0Pp4BLW6ds7cGow6eVlOta2YIRkrG3MiTUGIsQ4jxASMFCV8KfkItWZ5pdBgeSlM4GYwtrZOF8ScuM4QkTgX2ASZUaZ6nzNkvbBKtk1IaSgFpHS719KwEMrLGlLihhKgZSUuuQIwg4tcZVYSWp4hSW2GEMluF8qFXSp0xFWzVrDEcNKSY6JDYzxxA4Q64R1ptxBEcw5RqqQzWGjLRyCrhGrEWGzDxVgwjeXWpCHwJzoSuqOkCKEnlwQkLUQs9pESCiO2Jo5eWgbIZwg2nHeeSIrAesOv0o5BIwR6lxLvNNQrU49wzk7Ji+agJeE5QSfyTgUYwjMKvLUGwbl2FGAU7qDberdz-jogBejqEtItS8ty20Y6JkWDCFw0zjkrCxuEFCCxLBxDzF6yd10Ol4QofOkBYCb4MTvrKqBVCQHtrcrKDU66pIlgWJ5HdiAcRKjQQsD1wRtoCvLOm4VWir3QJoY819q0wioo6mjLY2JFg4MVHg6Y4FZzbn7d6goUSYlxISQhicHiEJxCVCMewiotSrwKbYXYwQPAoTxCkFIQA */
|
||||
predictableActionArguments: true,
|
||||
id: "templateVersionEditor",
|
||||
schema: {
|
||||
context: {} as TemplateVersionEditorMachineContext,
|
||||
events: {} as
|
||||
| { type: "INITIALIZE"; tarReader: TarReader }
|
||||
| {
|
||||
type: "CREATE_VERSION";
|
||||
fileTree: FileTree;
|
||||
templateId: string;
|
||||
}
|
||||
| { type: "CANCEL_VERSION" }
|
||||
| { type: "SET_MISSING_VARIABLE_VALUES"; values: VariableValue[] }
|
||||
| { type: "CANCEL_MISSING_VARIABLE_VALUES" }
|
||||
| { type: "ADD_BUILD_LOG"; log: ProvisionerJobLog }
|
||||
| { type: "BUILD_DONE" }
|
||||
| { type: "PUBLISH" }
|
||||
| ({ type: "CONFIRM_PUBLISH" } & PublishVersionData)
|
||||
| { type: "CANCEL_PUBLISH" },
|
||||
|
||||
services: {} as {
|
||||
uploadTar: {
|
||||
data: UploadResponse;
|
||||
};
|
||||
createBuild: {
|
||||
data: TemplateVersion;
|
||||
};
|
||||
cancelBuild: {
|
||||
data: void;
|
||||
};
|
||||
fetchVersion: {
|
||||
data: TemplateVersion;
|
||||
};
|
||||
getResources: {
|
||||
data: WorkspaceResource[];
|
||||
};
|
||||
publishingVersion: {
|
||||
data: void;
|
||||
};
|
||||
loadMissingVariables: {
|
||||
data: TemplateVersionVariable[];
|
||||
};
|
||||
},
|
||||
},
|
||||
tsTypes: {} as import("./templateVersionEditorXService.typegen").Typegen0,
|
||||
initial: "initializing",
|
||||
states: {
|
||||
initializing: {
|
||||
on: {
|
||||
INITIALIZE: {
|
||||
actions: ["assignTarReader"],
|
||||
target: "idle",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
idle: {
|
||||
on: {
|
||||
CREATE_VERSION: {
|
||||
actions: ["assignCreateBuild"],
|
||||
target: "cancelingInProgressBuild",
|
||||
},
|
||||
PUBLISH: {
|
||||
target: "askPublishParameters",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
askPublishParameters: {
|
||||
on: {
|
||||
CANCEL_PUBLISH: "idle",
|
||||
CONFIRM_PUBLISH: "publishingVersion",
|
||||
},
|
||||
},
|
||||
|
||||
publishingVersion: {
|
||||
tags: "loading",
|
||||
entry: ["clearPublishingError", "clearLastSuccessfulPublishedVersion"],
|
||||
invoke: {
|
||||
id: "publishingVersion",
|
||||
src: "publishingVersion",
|
||||
|
||||
onError: {
|
||||
actions: ["assignPublishingError"],
|
||||
target: "askPublishParameters",
|
||||
},
|
||||
|
||||
onDone: {
|
||||
actions: ["assignLastSuccessfulPublishedVersion"],
|
||||
target: ["idle"],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
cancelingInProgressBuild: {
|
||||
tags: "loading",
|
||||
invoke: {
|
||||
id: "cancelBuild",
|
||||
src: "cancelBuild",
|
||||
onDone: {
|
||||
target: "uploadTar",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
uploadTar: {
|
||||
tags: "loading",
|
||||
invoke: {
|
||||
id: "uploadTar",
|
||||
src: "uploadTar",
|
||||
onDone: {
|
||||
target: "creatingBuild",
|
||||
actions: "assignUploadResponse",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
creatingBuild: {
|
||||
tags: "loading",
|
||||
invoke: {
|
||||
id: "createBuild",
|
||||
src: "createBuild",
|
||||
onDone: {
|
||||
actions: "assignBuild",
|
||||
target: "watchingBuildLogs",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
watchingBuildLogs: {
|
||||
tags: "loading",
|
||||
invoke: {
|
||||
id: "watchBuildLogs",
|
||||
src: "watchBuildLogs",
|
||||
},
|
||||
on: {
|
||||
ADD_BUILD_LOG: {
|
||||
actions: "addBuildLog",
|
||||
},
|
||||
BUILD_DONE: "fetchingVersion",
|
||||
CANCEL_VERSION: {
|
||||
target: "cancelingInProgressBuild",
|
||||
},
|
||||
CREATE_VERSION: {
|
||||
actions: ["assignCreateBuild"],
|
||||
target: "uploadTar",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
fetchingVersion: {
|
||||
tags: "loading",
|
||||
invoke: {
|
||||
id: "fetchVersion",
|
||||
src: "fetchVersion",
|
||||
|
||||
onDone: [
|
||||
{
|
||||
actions: ["assignBuild"],
|
||||
target: "promptVariables",
|
||||
cond: "jobFailedWithMissingVariables",
|
||||
},
|
||||
{
|
||||
actions: ["assignBuild"],
|
||||
target: "fetchResources",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
promptVariables: {
|
||||
initial: "loadingMissingVariables",
|
||||
states: {
|
||||
loadingMissingVariables: {
|
||||
invoke: {
|
||||
src: "loadMissingVariables",
|
||||
onDone: {
|
||||
actions: "assignMissingVariables",
|
||||
target: "idle",
|
||||
},
|
||||
},
|
||||
},
|
||||
idle: {
|
||||
on: {
|
||||
SET_MISSING_VARIABLE_VALUES: {
|
||||
actions: "assignMissingVariableValues",
|
||||
target: "#templateVersionEditor.creatingBuild",
|
||||
},
|
||||
CANCEL_MISSING_VARIABLE_VALUES: {
|
||||
target: "#templateVersionEditor.idle",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
fetchResources: {
|
||||
tags: "loading",
|
||||
invoke: {
|
||||
id: "getResources",
|
||||
src: "getResources",
|
||||
onDone: {
|
||||
actions: ["assignResources"],
|
||||
target: "idle",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
actions: {
|
||||
assignCreateBuild: assign({
|
||||
fileTree: (_, event) => event.fileTree,
|
||||
templateId: (_, event) => event.templateId,
|
||||
buildLogs: (_, _1) => [],
|
||||
resources: (_, _1) => [],
|
||||
}),
|
||||
assignResources: assign({
|
||||
resources: (_, event) => event.data,
|
||||
}),
|
||||
assignUploadResponse: assign({
|
||||
uploadResponse: (_, event) => event.data,
|
||||
}),
|
||||
assignBuild: assign({
|
||||
version: (_, event) => event.data,
|
||||
}),
|
||||
assignLastSuccessfulPublishedVersion: assign({
|
||||
lastSuccessfulPublishedVersion: (ctx) => ctx.version,
|
||||
version: () => undefined,
|
||||
}),
|
||||
addBuildLog: assign({
|
||||
buildLogs: (context, event) => {
|
||||
const previousLogs = context.buildLogs ?? [];
|
||||
return [...previousLogs, event.log];
|
||||
},
|
||||
// Instead of periodically fetching the version,
|
||||
// we just assume the state is running after the first log.
|
||||
//
|
||||
// The machine fetches the version after the log stream ends anyways!
|
||||
version: (context) => {
|
||||
if (!context.version || context.buildLogs?.length !== 0) {
|
||||
return context.version;
|
||||
}
|
||||
return {
|
||||
...context.version,
|
||||
job: {
|
||||
...context.version.job,
|
||||
status: "running" as ProvisionerJobStatus,
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
assignTarReader: assign({
|
||||
tarReader: (_, { tarReader }) => tarReader,
|
||||
}),
|
||||
assignPublishingError: assign({
|
||||
publishingError: (_, event) => event.data,
|
||||
}),
|
||||
clearPublishingError: assign({ publishingError: (_) => undefined }),
|
||||
clearLastSuccessfulPublishedVersion: assign({
|
||||
lastSuccessfulPublishedVersion: (_) => undefined,
|
||||
}),
|
||||
assignMissingVariables: assign({
|
||||
missingVariables: (_, event) => event.data,
|
||||
}),
|
||||
assignMissingVariableValues: assign({
|
||||
missingVariableValues: (_, event) => event.values,
|
||||
}),
|
||||
},
|
||||
services: {
|
||||
uploadTar: async ({ fileTree, tarReader }) => {
|
||||
if (!fileTree) {
|
||||
throw new Error("file tree must to be set");
|
||||
}
|
||||
if (!tarReader) {
|
||||
throw new Error("tar reader must to be set");
|
||||
}
|
||||
const tar = new TarWriter();
|
||||
|
||||
// Add previous non editable files
|
||||
for (const file of tarReader.fileInfo) {
|
||||
if (!isAllowedFile(file.name)) {
|
||||
if (file.type === "5") {
|
||||
tar.addFolder(file.name, {
|
||||
mode: file.mode, // https://github.com/beatgammit/tar-js/blob/master/lib/tar.js#L42
|
||||
mtime: file.mtime,
|
||||
user: file.user,
|
||||
group: file.group,
|
||||
});
|
||||
} else {
|
||||
tar.addFile(
|
||||
file.name,
|
||||
tarReader.getTextFile(file.name) as string,
|
||||
{
|
||||
mode: file.mode, // https://github.com/beatgammit/tar-js/blob/master/lib/tar.js#L42
|
||||
mtime: file.mtime,
|
||||
user: file.user,
|
||||
group: file.group,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Add the editable files
|
||||
traverse(fileTree, (content, _filename, fullPath) => {
|
||||
// When a file is deleted. Don't add it to the tar.
|
||||
if (content === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof content === "string") {
|
||||
tar.addFile(fullPath, content);
|
||||
return;
|
||||
}
|
||||
|
||||
tar.addFolder(fullPath);
|
||||
});
|
||||
const blob = (await tar.write()) as Blob;
|
||||
return API.uploadFile(new File([blob], "template.tar"));
|
||||
},
|
||||
createBuild: (ctx) => {
|
||||
if (!ctx.uploadResponse) {
|
||||
throw new Error("no upload response");
|
||||
}
|
||||
return API.createTemplateVersion(ctx.orgId, {
|
||||
provisioner: "terraform",
|
||||
storage_method: "file",
|
||||
tags: {},
|
||||
template_id: ctx.templateId,
|
||||
file_id: ctx.uploadResponse.hash,
|
||||
user_variable_values: ctx.missingVariableValues,
|
||||
});
|
||||
},
|
||||
fetchVersion: (ctx) => {
|
||||
if (!ctx.version) {
|
||||
throw new Error("template version must be set");
|
||||
}
|
||||
return API.getTemplateVersion(ctx.version.id);
|
||||
},
|
||||
watchBuildLogs:
|
||||
({ version }) =>
|
||||
async (callback) => {
|
||||
if (!version) {
|
||||
throw new Error("version must be set");
|
||||
}
|
||||
|
||||
const socket = API.watchBuildLogsByTemplateVersionId(version.id, {
|
||||
onMessage: (log) => {
|
||||
callback({ type: "ADD_BUILD_LOG", log });
|
||||
},
|
||||
onDone: () => {
|
||||
callback({ type: "BUILD_DONE" });
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.close();
|
||||
};
|
||||
},
|
||||
getResources: (ctx) => {
|
||||
if (!ctx.version) {
|
||||
throw new Error("template version must be set");
|
||||
}
|
||||
return API.getTemplateVersionResources(ctx.version.id);
|
||||
},
|
||||
cancelBuild: async (ctx) => {
|
||||
if (!ctx.version) {
|
||||
return;
|
||||
}
|
||||
if (ctx.version.job.status === "running") {
|
||||
await API.cancelTemplateVersionBuild(ctx.version.id);
|
||||
}
|
||||
},
|
||||
publishingVersion: async (
|
||||
{ version, templateId },
|
||||
{ name, message, isActiveVersion },
|
||||
) => {
|
||||
if (!version) {
|
||||
throw new Error("Version is not set");
|
||||
}
|
||||
if (!templateId) {
|
||||
throw new Error("Template is not set");
|
||||
}
|
||||
const haveChanges =
|
||||
name !== version.name || message !== version.message;
|
||||
await Promise.all([
|
||||
haveChanges
|
||||
? API.patchTemplateVersion(version.id, { name, message })
|
||||
: Promise.resolve(),
|
||||
isActiveVersion
|
||||
? API.updateActiveTemplateVersion(templateId, {
|
||||
id: version.id,
|
||||
})
|
||||
: Promise.resolve(),
|
||||
]);
|
||||
},
|
||||
loadMissingVariables: ({ version }) => {
|
||||
if (!version) {
|
||||
throw new Error("Version is not set");
|
||||
}
|
||||
const variables = API.getTemplateVersionVariables(version.id);
|
||||
return variables;
|
||||
},
|
||||
},
|
||||
guards: {
|
||||
jobFailedWithMissingVariables: (_, { data }) => {
|
||||
return data.job.error_code === "REQUIRED_TEMPLATE_VARIABLES";
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user