diff --git a/coderd/templateversions.go b/coderd/templateversions.go index ebbf4ae79f72b..55468a510ae35 100644 --- a/coderd/templateversions.go +++ b/coderd/templateversions.go @@ -123,6 +123,9 @@ func (api *API) patchTemplateVersion(rw http.ResponseWriter, r *http.Request) { if errors.Is(err, errTemplateVersionNameConflict) { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: err.Error(), + Validations: []codersdk.ValidationError{ + {Field: "name", Detail: "Name is already used"}, + }, }) return } diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 73a133f27e02d..a1763ab75fcb6 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -373,6 +373,17 @@ export const updateActiveTemplateVersion = async ( return response.data } +export const patchTemplateVersion = async ( + templateVersionId: string, + data: TypesGen.PatchTemplateVersionRequest, +) => { + const response = await axios.patch( + `/api/v2/templateversions/${templateVersionId}`, + data, + ) + return response.data +} + export const updateTemplateMeta = async ( templateId: string, data: TypesGen.UpdateTemplateMeta, @@ -1020,3 +1031,26 @@ const getMissingParameters = ( return missingParameters } + +export const watchBuildLogs = ( + versionId: string, + onMessage: (log: TypesGen.ProvisionerJobLog) => void, +) => { + return new Promise((resolve, reject) => { + const proto = location.protocol === "https:" ? "wss:" : "ws:" + const socket = new WebSocket( + `${proto}//${location.host}/api/v2/templateversions/${versionId}/logs?follow=true`, + ) + socket.binaryType = "blob" + socket.addEventListener("message", (event) => + onMessage(JSON.parse(event.data) as TypesGen.ProvisionerJobLog), + ) + socket.addEventListener("error", () => { + reject(new Error("Connection for logs failed.")) + }) + socket.addEventListener("close", () => { + // When the socket closes, logs have finished streaming! + resolve() + }) + }) +} diff --git a/site/src/components/TemplateVersionEditor/PublishTemplateVersionDialog.tsx b/site/src/components/TemplateVersionEditor/PublishTemplateVersionDialog.tsx new file mode 100644 index 0000000000000..236a91935dc34 --- /dev/null +++ b/site/src/components/TemplateVersionEditor/PublishTemplateVersionDialog.tsx @@ -0,0 +1,98 @@ +import { DialogProps } from "components/Dialogs/Dialog" +import { FC } from "react" +import { getFormHelpers } from "util/formUtils" +import { FormFields } from "components/Form/Form" +import { useFormik } from "formik" +import * as Yup from "yup" +import { PublishVersionData } from "pages/TemplateVersionPage/TemplateVersionEditorPage/types" +import TextField from "@material-ui/core/TextField" +import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog" +import Checkbox from "@material-ui/core/Checkbox" +import FormControlLabel from "@material-ui/core/FormControlLabel" +import { Stack } from "components/Stack/Stack" + +export type PublishTemplateVersionDialogProps = DialogProps & { + defaultName: string + isPublishing: boolean + publishingError?: unknown + onClose: () => void + onConfirm: (data: PublishVersionData) => void +} + +export const PublishTemplateVersionDialog: FC< + PublishTemplateVersionDialogProps +> = ({ + onConfirm, + isPublishing, + onClose, + defaultName, + publishingError, + ...dialogProps +}) => { + const form = useFormik({ + initialValues: { + name: defaultName, + isActiveVersion: false, + }, + validationSchema: Yup.object({ + name: Yup.string().required(), + isActiveVersion: Yup.boolean(), + }), + onSubmit: onConfirm, + }) + const getFieldHelpers = getFormHelpers(form, publishingError) + const handleClose = () => { + form.resetForm() + onClose() + } + + return ( + { + await form.submitForm() + }} + hideCancel={false} + type="success" + cancelText="Cancel" + confirmText="Publish" + title="Publish new version" + description={ + +

You are about to publish a new version of this template.

+ + + + { + await form.setFieldValue( + "isActiveVersion", + e.target.checked, + ) + }} + name="isActiveVersion" + color="primary" + /> + } + /> + +
+ } + /> + ) +} diff --git a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx index 0bcaf8640adaa..267e6debf92e0 100644 --- a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx +++ b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx @@ -16,6 +16,7 @@ import { AvatarData } from "components/AvatarData/AvatarData" import { bannerHeight } from "components/DeploymentBanner/DeploymentBannerView" import { TemplateResourcesTable } from "components/TemplateResourcesTable/TemplateResourcesTable" import { WorkspaceBuildLogs } from "components/WorkspaceBuildLogs/WorkspaceBuildLogs" +import { PublishVersionData } from "pages/TemplateVersionPage/TemplateVersionEditorPage/types" import { FC, useCallback, useEffect, useRef, useState } from "react" import { navHeight, dashboardContentBottomPadding } from "theme/constants" import { @@ -36,6 +37,7 @@ import { } from "./FileDialog" import { FileTreeView } from "./FileTreeView" import { MonacoEditor } from "./MonacoEditor" +import { PublishTemplateVersionDialog } from "./PublishTemplateVersionDialog" import { getStatus, TemplateVersionStatusBadge, @@ -51,7 +53,12 @@ export interface TemplateVersionEditorProps { disablePreview: boolean disableUpdate: boolean onPreview: (files: FileTree) => void - onUpdate: () => void + onPublish: () => void + onConfirmPublish: (data: PublishVersionData) => void + onCancelPublish: () => void + publishingError: unknown + isAskingPublishParameters: boolean + isPublishing: boolean } const topbarHeight = 80 @@ -76,7 +83,12 @@ export const TemplateVersionEditor: FC = ({ templateVersion, defaultFileTree, onPreview, - onUpdate, + onPublish, + onConfirmPublish, + onCancelPublish, + publishingError, + isAskingPublishParameters, + isPublishing, buildLogs, resources, }) => { @@ -156,233 +168,249 @@ export const TemplateVersionEditor: FC = ({ }) return ( -
-
-
- - ) - } - /> -
- -
- {/* Only start to show the build when a new template version is building */} - {templateVersion.id !== firstTemplateVersionOnEditor.current.id && ( -
- -
- )} - - - - -
-
- -
-
-
- Template files -
- - { - setCreateFileOpen(true) - event.currentTarget.blur() - }} - > - - - -
- { - setCreateFileOpen(false) - }} - checkExists={(path) => existsFile(path, fileTree)} - onConfirm={(path) => { - setFileTree((fileTree) => createFile(path, fileTree, "")) - setActivePath(path) - setCreateFileOpen(false) - setDirty(true) - }} - /> - { - if (!deleteFileOpen) { - throw new Error("delete file must be set") - } - setFileTree((fileTree) => removeFile(deleteFileOpen, fileTree)) - setDeleteFileOpen(undefined) - if (activePath === deleteFileOpen) { - setActivePath(undefined) - } - setDirty(true) - }} - open={Boolean(deleteFileOpen)} - onClose={() => setDeleteFileOpen(undefined)} - filename={deleteFileOpen || ""} - /> - { - setRenameFileOpen(undefined) - }} - filename={renameFileOpen || ""} - checkExists={(path) => existsFile(path, fileTree)} - onConfirm={(newPath) => { - if (!renameFileOpen) { - return - } - setFileTree((fileTree) => - moveFile(renameFileOpen, newPath, fileTree), + <> +
+
+
+ ) - setActivePath(newPath) - setRenameFileOpen(undefined) - setDirty(true) - }} + } />
- setDeleteFileOpen(file)} - onSelect={(filePath) => { - if (!isFolder(filePath, fileTree)) { - setActivePath(filePath) + +
+ {/* Only start to show the build when a new template version is building */} + {templateVersion.id !== firstTemplateVersionOnEditor.current.id && ( +
+ +
+ )} + + + + +
-
-
- {activePath ? ( - { - if (!activePath) { +
+
+
+ Template files +
+ + { + setCreateFileOpen(true) + event.currentTarget.blur() + }} + > + + + +
+ { + setCreateFileOpen(false) + }} + checkExists={(path) => existsFile(path, fileTree)} + onConfirm={(path) => { + setFileTree((fileTree) => createFile(path, fileTree, "")) + setActivePath(path) + setCreateFileOpen(false) + setDirty(true) + }} + /> + { + if (!deleteFileOpen) { + throw new Error("delete file must be set") + } + setFileTree((fileTree) => + removeFile(deleteFileOpen, fileTree), + ) + setDeleteFileOpen(undefined) + if (activePath === deleteFileOpen) { + setActivePath(undefined) + } + setDirty(true) + }} + open={Boolean(deleteFileOpen)} + onClose={() => setDeleteFileOpen(undefined)} + filename={deleteFileOpen || ""} + /> + { + setRenameFileOpen(undefined) + }} + filename={renameFileOpen || ""} + checkExists={(path) => existsFile(path, fileTree)} + onConfirm={(newPath) => { + if (!renameFileOpen) { return } setFileTree((fileTree) => - updateFile(activePath, value, fileTree), + moveFile(renameFileOpen, newPath, fileTree), ) + setActivePath(newPath) + setRenameFileOpen(undefined) setDirty(true) }} /> - ) : ( -
No file opened
- )} +
+ setDeleteFileOpen(file)} + onSelect={(filePath) => { + if (!isFolder(filePath, fileTree)) { + setActivePath(filePath) + } + }} + onRename={(file) => setRenameFileOpen(file)} + activePath={activePath} + />
-
-
- +
+
+ {activePath ? ( + { + if (!activePath) { + return + } + setFileTree((fileTree) => + updateFile(activePath, value, fileTree), + ) + setDirty(true) + }} + /> + ) : ( +
No file opened
+ )} +
- {!disableUpdate && ( +
+
- )} -
-
- {buildLogs && ( - - )} - {templateVersion.job.error && ( -
- {templateVersion.job.error} -
- )} -
+ {!disableUpdate && ( + + )} +
-
- {resources && ( - r.workspace_transition === "start", - )} - /> - )} +
+ {buildLogs && ( + + )} + {templateVersion.job.error && ( +
+ {templateVersion.job.error} +
+ )} +
+ +
+ {resources && ( + r.workspace_transition === "start", + )} + /> + )} +
-
- {templateVersionSucceeded && ( - <> -
- - )} + {templateVersionSucceeded && ( + <> +
+ + )} +
-
+ + + ) } diff --git a/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx b/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx new file mode 100644 index 0000000000000..07886ec58f7d2 --- /dev/null +++ b/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx @@ -0,0 +1,183 @@ +import { + MockTemplateVersion, + MockWorkspaceBuildLogs, + renderWithAuth, +} from "testHelpers/renderHelpers" +import TemplateVersionEditorPage from "./TemplateVersionEditorPage" +import { screen, waitFor, within } from "@testing-library/react" +import userEvent from "@testing-library/user-event" +import * as api from "api/api" + +// 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 +jest.mock("components/TemplateResourcesTable/TemplateResourcesTable", () => { + return { + TemplateResourcesTable: () =>
, + } +}) + +test("Use custom name and set it as active when publishing", async () => { + const user = userEvent.setup() + renderWithAuth(, { + extraRoutes: [ + { + path: "/templates/:templateId", + element:
, + }, + ], + }) + const topbar = await screen.findByTestId("topbar") + + // Build Template + jest.spyOn(api, "uploadTemplateFile").mockResolvedValueOnce({ hash: "hash" }) + jest + .spyOn(api, "createTemplateVersion") + .mockResolvedValueOnce(MockTemplateVersion) + jest + .spyOn(api, "getTemplateVersion") + .mockResolvedValue({ ...MockTemplateVersion, id: "new-version-id" }) + jest.spyOn(api, "watchBuildLogs").mockImplementation((_, onMessage) => { + onMessage(MockWorkspaceBuildLogs[0]) + return Promise.resolve() + }) + const buildButton = within(topbar).getByRole("button", { + name: "Build template", + }) + await user.click(buildButton) + + // Publish + const patchTemplateVersion = jest + .spyOn(api, "patchTemplateVersion") + .mockResolvedValue(MockTemplateVersion) + const updateActiveTemplateVersion = jest + .spyOn(api, "updateActiveTemplateVersion") + .mockResolvedValue({ message: "" }) + await within(topbar).findByText("Success") + const publishButton = within(topbar).getByRole("button", { + name: "Publish version", + }) + await user.click(publishButton) + const publishDialog = await screen.findByTestId("dialog") + const nameField = within(publishDialog).getByLabelText("Version name") + await user.clear(nameField) + await user.type(nameField, "v1.0") + await user.click( + within(publishDialog).getByLabelText("Promote to default version"), + ) + await user.click( + within(publishDialog).getByRole("button", { name: "Publish" }), + ) + await waitFor(() => { + expect(patchTemplateVersion).toBeCalledWith("new-version-id", { + name: "v1.0", + }) + expect(updateActiveTemplateVersion).toBeCalledWith("test-template", { + id: "new-version-id", + }) + }) +}) + +test("Do not mark as active if promote is not checked", async () => { + const user = userEvent.setup() + renderWithAuth(, { + extraRoutes: [ + { + path: "/templates/:templateId", + element:
, + }, + ], + }) + const topbar = await screen.findByTestId("topbar") + + // Build Template + jest.spyOn(api, "uploadTemplateFile").mockResolvedValueOnce({ hash: "hash" }) + jest + .spyOn(api, "createTemplateVersion") + .mockResolvedValueOnce(MockTemplateVersion) + jest + .spyOn(api, "getTemplateVersion") + .mockResolvedValue({ ...MockTemplateVersion, id: "new-version-id" }) + jest.spyOn(api, "watchBuildLogs").mockImplementation((_, onMessage) => { + onMessage(MockWorkspaceBuildLogs[0]) + return Promise.resolve() + }) + const buildButton = within(topbar).getByRole("button", { + name: "Build template", + }) + await user.click(buildButton) + + // Publish + const patchTemplateVersion = jest + .spyOn(api, "patchTemplateVersion") + .mockResolvedValue(MockTemplateVersion) + const updateActiveTemplateVersion = jest + .spyOn(api, "updateActiveTemplateVersion") + .mockResolvedValue({ message: "" }) + await within(topbar).findByText("Success") + const publishButton = within(topbar).getByRole("button", { + name: "Publish version", + }) + await user.click(publishButton) + const publishDialog = await screen.findByTestId("dialog") + const nameField = within(publishDialog).getByLabelText("Version name") + await user.clear(nameField) + await user.type(nameField, "v1.0") + await user.click( + within(publishDialog).getByRole("button", { name: "Publish" }), + ) + await waitFor(() => { + expect(patchTemplateVersion).toBeCalledWith("new-version-id", { + name: "v1.0", + }) + }) + expect(updateActiveTemplateVersion).toBeCalledTimes(0) +}) + +test("The default version name is used when a new one is not used", async () => { + const user = userEvent.setup() + renderWithAuth(, { + extraRoutes: [ + { + path: "/templates/:templateId", + element:
, + }, + ], + }) + const topbar = await screen.findByTestId("topbar") + + // Build Template + jest.spyOn(api, "uploadTemplateFile").mockResolvedValueOnce({ hash: "hash" }) + jest + .spyOn(api, "createTemplateVersion") + .mockResolvedValueOnce(MockTemplateVersion) + jest + .spyOn(api, "getTemplateVersion") + .mockResolvedValue({ ...MockTemplateVersion, id: "new-version-id" }) + jest.spyOn(api, "watchBuildLogs").mockImplementation((_, onMessage) => { + onMessage(MockWorkspaceBuildLogs[0]) + return Promise.resolve() + }) + const buildButton = within(topbar).getByRole("button", { + name: "Build template", + }) + await user.click(buildButton) + + // Publish + const patchTemplateVersion = jest + .spyOn(api, "patchTemplateVersion") + .mockResolvedValue(MockTemplateVersion) + await within(topbar).findByText("Success") + const publishButton = within(topbar).getByRole("button", { + name: "Publish version", + }) + await user.click(publishButton) + const publishDialog = await screen.findByTestId("dialog") + await user.click( + within(publishDialog).getByRole("button", { name: "Publish" }), + ) + await waitFor(() => { + expect(patchTemplateVersion).toBeCalledWith("new-version-id", { + name: MockTemplateVersion.name, + }) + }) +}) diff --git a/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx b/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx index d5dcfd71abfa3..dfd2967367136 100644 --- a/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx +++ b/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx @@ -4,7 +4,7 @@ import { useOrganizationId } from "hooks/useOrganizationId" import { usePermissions } from "hooks/usePermissions" import { FC } from "react" import { Helmet } from "react-helmet-async" -import { useParams } from "react-router-dom" +import { useNavigate, useParams } from "react-router-dom" import { pageTitle } from "util/page" import { templateVersionEditorMachine } from "xServices/templateVersionEditor/templateVersionEditorXService" import { useTemplateVersionData } from "./data" @@ -16,9 +16,15 @@ type Params = { export const TemplateVersionEditorPage: FC = () => { const { version: versionName, template: templateName } = useParams() as Params + const navigate = useNavigate() const orgId = useOrganizationId() const [editorState, sendEvent] = useMachine(templateVersionEditorMachine, { context: { orgId }, + actions: { + onPublish: () => { + navigate(`/templates/${templateName}`) + }, + }, }) const permissions = usePermissions() const { isSuccess, data } = useTemplateVersionData( @@ -53,11 +59,27 @@ export const TemplateVersionEditorPage: FC = () => { templateId: data.template.id, }) }} - onUpdate={() => { + onCancelPublish={() => { + sendEvent({ + type: "CANCEL_PUBLISH", + }) + }} + onPublish={() => { + sendEvent({ + type: "PUBLISH", + }) + }} + onConfirmPublish={(data) => { sendEvent({ - type: "UPDATE_ACTIVE_VERSION", + type: "CONFIRM_PUBLISH", + ...data, }) }} + isAskingPublishParameters={editorState.matches( + "askPublishParameters", + )} + publishingError={editorState.context.publishingError} + isPublishing={editorState.matches("publishingVersion")} disablePreview={editorState.hasTag("loading")} disableUpdate={ editorState.hasTag("loading") || diff --git a/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/types.ts b/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/types.ts new file mode 100644 index 0000000000000..ca8dc50c48ebe --- /dev/null +++ b/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/types.ts @@ -0,0 +1,4 @@ +export type PublishVersionData = { + name: string + isActiveVersion: boolean +} diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index f504abea3f161..0d866762e48d9 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -669,8 +669,7 @@ export const MockWorkspace: TypesGen.Workspace = { autostart_schedule: MockWorkspaceAutostartEnabled.schedule, ttl_ms: 2 * 60 * 60 * 1000, latest_build: MockWorkspaceBuild, - last_used_at: "", - organization_id: MockOrganization.id, + last_used_at: "2022-05-16T15:29:10.302441433Z", } export const MockStoppedWorkspace: TypesGen.Workspace = { diff --git a/site/src/testHelpers/handlers.ts b/site/src/testHelpers/handlers.ts index ff18cbf6a6f2c..9a0bc8f309cfb 100644 --- a/site/src/testHelpers/handlers.ts +++ b/site/src/testHelpers/handlers.ts @@ -4,6 +4,8 @@ import { CreateWorkspaceBuildRequest } from "../api/typesGenerated" import { permissionsToCheck } from "../xServices/auth/authXService" import * as M from "./entities" import { MockGroup, MockWorkspaceQuota } from "./entities" +import fs from "fs" +import path from "path" export const handlers = [ rest.get("/api/v2/templates/:templateId/daus", async (req, res, ctx) => { @@ -318,4 +320,17 @@ export const handlers = [ return res(ctx.status(200), ctx.json([M.MockWorkspaceBuildParameter1])) }, ), + + rest.get("api/v2/files/:fileId", (_, res, ctx) => { + const fileBuffer = fs.readFileSync( + path.resolve(__dirname, "./templateFiles.tar"), + ) + + return res( + ctx.set("Content-Length", fileBuffer.byteLength.toString()), + ctx.set("Content-Type", "application/octet-stream"), + // Respond with the "ArrayBuffer". + ctx.body(fileBuffer), + ) + }), ] diff --git a/site/src/testHelpers/templateFiles.tar b/site/src/testHelpers/templateFiles.tar new file mode 100644 index 0000000000000..b0bbdff89fcb7 Binary files /dev/null and b/site/src/testHelpers/templateFiles.tar differ diff --git a/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts b/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts index 75b28b2888bd2..45f9ee9d2dd68 100644 --- a/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts +++ b/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts @@ -10,6 +10,7 @@ import * as API from "api/api" import { FileTree, traverse } from "util/filetree" import { isAllowedFile } from "util/templateVersion" import { TarReader, TarWriter } from "util/tar" +import { PublishVersionData } from "pages/TemplateVersionPage/TemplateVersionEditorPage/types" export interface CreateVersionData { file: File @@ -24,6 +25,7 @@ export interface TemplateVersionEditorMachineContext { resources?: WorkspaceResource[] buildLogs?: ProvisionerJobLog[] tarReader?: TarReader + publishingError?: unknown } export const templateVersionEditorMachine = createMachine( @@ -41,7 +43,10 @@ export const templateVersionEditorMachine = createMachine( } | { type: "CANCEL_VERSION" } | { type: "ADD_BUILD_LOG"; log: ProvisionerJobLog } - | { type: "UPDATE_ACTIVE_VERSION" }, + | { type: "PUBLISH" } + | ({ type: "CONFIRM_PUBLISH" } & PublishVersionData) + | { type: "CANCEL_PUBLISH" }, + services: {} as { uploadTar: { data: UploadResponse @@ -58,7 +63,7 @@ export const templateVersionEditorMachine = createMachine( getResources: { data: WorkspaceResource[] } - updateActiveVersion: { + publishingVersion: { data: void } }, @@ -80,18 +85,29 @@ export const templateVersionEditorMachine = createMachine( actions: ["assignCreateBuild"], target: "cancelingBuild", }, - UPDATE_ACTIVE_VERSION: { - target: "updatingActiveVersion", + PUBLISH: { + target: "askPublishParameters", }, }, }, - updatingActiveVersion: { + askPublishParameters: { + on: { + CANCEL_PUBLISH: "idle", + CONFIRM_PUBLISH: "publishingVersion", + }, + }, + publishingVersion: { tags: "loading", + entry: ["clearPublishingError"], invoke: { - id: "updateActiveVersion", - src: "updateActiveVersion", + id: "publishingVersion", + src: "publishingVersion", onDone: { - target: "idle", + actions: ["onPublish"], + }, + onError: { + actions: ["assignPublishingError"], + target: "askPublishParameters", }, }, }, @@ -215,6 +231,10 @@ export const templateVersionEditorMachine = createMachine( assignTarReader: assign({ tarReader: (_, { tarReader }) => tarReader, }), + assignPublishingError: assign({ + publishingError: (_, event) => event.data, + }), + clearPublishingError: assign({ publishingError: (_) => undefined }), }, services: { uploadTar: async ({ fileTree, tarReader }) => { @@ -285,28 +305,17 @@ export const templateVersionEditorMachine = createMachine( } return API.getTemplateVersion(ctx.version.id) }, - watchBuildLogs: (ctx) => async (callback) => { - return new Promise((resolve, reject) => { - if (!ctx.version) { - return reject("version must be set") + watchBuildLogs: + ({ version }) => + async (callback) => { + if (!version) { + throw new Error("version must be set") } - const proto = location.protocol === "https:" ? "wss:" : "ws:" - const socket = new WebSocket( - `${proto}//${location.host}/api/v2/templateversions/${ctx.version?.id}/logs?follow=true`, - ) - socket.binaryType = "blob" - socket.addEventListener("message", (event) => { - callback({ type: "ADD_BUILD_LOG", log: JSON.parse(event.data) }) - }) - socket.addEventListener("error", () => { - reject(new Error("socket errored")) - }) - socket.addEventListener("close", () => { - // When the socket closes, logs have finished streaming! - resolve() + + return API.watchBuildLogs(version.id, (log) => { + callback({ type: "ADD_BUILD_LOG", log }) }) - }) - }, + }, getResources: (ctx) => { if (!ctx.version) { throw new Error("template version must be set") @@ -321,16 +330,24 @@ export const templateVersionEditorMachine = createMachine( await API.cancelTemplateVersionBuild(ctx.version.id) } }, - updateActiveVersion: async (ctx) => { - if (!ctx.templateId) { - throw new Error("template must be set") + publishingVersion: async ( + { version, templateId }, + { name, isActiveVersion }, + ) => { + if (!version) { + throw new Error("Version is not set") } - if (!ctx.version) { - throw new Error("template version must be set") + if (!templateId) { + throw new Error("Template is not set") } - await API.updateActiveTemplateVersion(ctx.templateId, { - id: ctx.version.id, - }) + await Promise.all([ + API.patchTemplateVersion(version.id, { name }), + isActiveVersion + ? API.updateActiveTemplateVersion(templateId, { + id: version.id, + }) + : Promise.resolve(), + ]) }, }, },