mirror of
https://github.com/coder/coder.git
synced 2025-07-09 11:45:56 +00:00
feat(site): Ask for version name and if it is active when publishing a new version on editor (#6756)
This commit is contained in:
@ -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
|
||||
}
|
||||
|
@ -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<TypesGen.TemplateVersion>(
|
||||
`/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<void>((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()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -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 (
|
||||
<ConfirmDialog
|
||||
{...dialogProps}
|
||||
confirmLoading={isPublishing}
|
||||
onClose={handleClose}
|
||||
onConfirm={async () => {
|
||||
await form.submitForm()
|
||||
}}
|
||||
hideCancel={false}
|
||||
type="success"
|
||||
cancelText="Cancel"
|
||||
confirmText="Publish"
|
||||
title="Publish new version"
|
||||
description={
|
||||
<Stack>
|
||||
<p>You are about to publish a new version of this template.</p>
|
||||
<FormFields>
|
||||
<TextField
|
||||
{...getFieldHelpers("name")}
|
||||
label="Version name"
|
||||
autoFocus
|
||||
disabled={isPublishing}
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormControlLabel
|
||||
label="Promote to default version"
|
||||
control={
|
||||
<Checkbox
|
||||
size="small"
|
||||
checked={form.values.isActiveVersion}
|
||||
onChange={async (e) => {
|
||||
await form.setFieldValue(
|
||||
"isActiveVersion",
|
||||
e.target.checked,
|
||||
)
|
||||
}}
|
||||
name="isActiveVersion"
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</FormFields>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
@ -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<TemplateVersionEditorProps> = ({
|
||||
templateVersion,
|
||||
defaultFileTree,
|
||||
onPreview,
|
||||
onUpdate,
|
||||
onPublish,
|
||||
onConfirmPublish,
|
||||
onCancelPublish,
|
||||
publishingError,
|
||||
isAskingPublishParameters,
|
||||
isPublishing,
|
||||
buildLogs,
|
||||
resources,
|
||||
}) => {
|
||||
@ -156,8 +168,9 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.root}>
|
||||
<div className={styles.topbar}>
|
||||
<div className={styles.topbar} data-testid="topbar">
|
||||
<div className={styles.topbarSides}>
|
||||
<AvatarData
|
||||
title={template.display_name || template.name}
|
||||
@ -200,7 +213,7 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
|
||||
}
|
||||
size="small"
|
||||
disabled={dirty || disableUpdate}
|
||||
onClick={onUpdate}
|
||||
onClick={onPublish}
|
||||
>
|
||||
Publish version
|
||||
</Button>
|
||||
@ -244,7 +257,9 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
|
||||
if (!deleteFileOpen) {
|
||||
throw new Error("delete file must be set")
|
||||
}
|
||||
setFileTree((fileTree) => removeFile(deleteFileOpen, fileTree))
|
||||
setFileTree((fileTree) =>
|
||||
removeFile(deleteFileOpen, fileTree),
|
||||
)
|
||||
setDeleteFileOpen(undefined)
|
||||
if (activePath === deleteFileOpen) {
|
||||
setActivePath(undefined)
|
||||
@ -313,7 +328,9 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
|
||||
<div className={styles.panelWrapper}>
|
||||
<div className={styles.tabs}>
|
||||
<button
|
||||
className={`${styles.tab} ${selectedTab === 0 ? "active" : ""}`}
|
||||
className={`${styles.tab} ${
|
||||
selectedTab === 0 ? "active" : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
setSelectedTab(0)
|
||||
}}
|
||||
@ -383,6 +400,17 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PublishTemplateVersionDialog
|
||||
key={templateVersion.name}
|
||||
publishingError={publishingError}
|
||||
open={isAskingPublishParameters || isPublishing}
|
||||
onClose={onCancelPublish}
|
||||
onConfirm={onConfirmPublish}
|
||||
isPublishing={isPublishing}
|
||||
defaultName={templateVersion.name}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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: () => <div />,
|
||||
}
|
||||
})
|
||||
|
||||
test("Use custom name and set it as active when publishing", async () => {
|
||||
const user = userEvent.setup()
|
||||
renderWithAuth(<TemplateVersionEditorPage />, {
|
||||
extraRoutes: [
|
||||
{
|
||||
path: "/templates/:templateId",
|
||||
element: <div />,
|
||||
},
|
||||
],
|
||||
})
|
||||
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(<TemplateVersionEditorPage />, {
|
||||
extraRoutes: [
|
||||
{
|
||||
path: "/templates/:templateId",
|
||||
element: <div />,
|
||||
},
|
||||
],
|
||||
})
|
||||
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(<TemplateVersionEditorPage />, {
|
||||
extraRoutes: [
|
||||
{
|
||||
path: "/templates/:templateId",
|
||||
element: <div />,
|
||||
},
|
||||
],
|
||||
})
|
||||
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,
|
||||
})
|
||||
})
|
||||
})
|
@ -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: "UPDATE_ACTIVE_VERSION",
|
||||
type: "CANCEL_PUBLISH",
|
||||
})
|
||||
}}
|
||||
onPublish={() => {
|
||||
sendEvent({
|
||||
type: "PUBLISH",
|
||||
})
|
||||
}}
|
||||
onConfirmPublish={(data) => {
|
||||
sendEvent({
|
||||
type: "CONFIRM_PUBLISH",
|
||||
...data,
|
||||
})
|
||||
}}
|
||||
isAskingPublishParameters={editorState.matches(
|
||||
"askPublishParameters",
|
||||
)}
|
||||
publishingError={editorState.context.publishingError}
|
||||
isPublishing={editorState.matches("publishingVersion")}
|
||||
disablePreview={editorState.hasTag("loading")}
|
||||
disableUpdate={
|
||||
editorState.hasTag("loading") ||
|
||||
|
@ -0,0 +1,4 @@
|
||||
export type PublishVersionData = {
|
||||
name: string
|
||||
isActiveVersion: boolean
|
||||
}
|
@ -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 = {
|
||||
|
@ -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),
|
||||
)
|
||||
}),
|
||||
]
|
||||
|
BIN
site/src/testHelpers/templateFiles.tar
Normal file
BIN
site/src/testHelpers/templateFiles.tar
Normal file
Binary file not shown.
@ -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,26 +305,15 @@ export const templateVersionEditorMachine = createMachine(
|
||||
}
|
||||
return API.getTemplateVersion(ctx.version.id)
|
||||
},
|
||||
watchBuildLogs: (ctx) => async (callback) => {
|
||||
return new Promise<void>((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) => {
|
||||
@ -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(),
|
||||
])
|
||||
},
|
||||
},
|
||||
},
|
||||
|
Reference in New Issue
Block a user