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) {
|
if errors.Is(err, errTemplateVersionNameConflict) {
|
||||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||||
Message: err.Error(),
|
Message: err.Error(),
|
||||||
|
Validations: []codersdk.ValidationError{
|
||||||
|
{Field: "name", Detail: "Name is already used"},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -373,6 +373,17 @@ export const updateActiveTemplateVersion = async (
|
|||||||
return response.data
|
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 (
|
export const updateTemplateMeta = async (
|
||||||
templateId: string,
|
templateId: string,
|
||||||
data: TypesGen.UpdateTemplateMeta,
|
data: TypesGen.UpdateTemplateMeta,
|
||||||
@ -1020,3 +1031,26 @@ const getMissingParameters = (
|
|||||||
|
|
||||||
return missingParameters
|
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 { bannerHeight } from "components/DeploymentBanner/DeploymentBannerView"
|
||||||
import { TemplateResourcesTable } from "components/TemplateResourcesTable/TemplateResourcesTable"
|
import { TemplateResourcesTable } from "components/TemplateResourcesTable/TemplateResourcesTable"
|
||||||
import { WorkspaceBuildLogs } from "components/WorkspaceBuildLogs/WorkspaceBuildLogs"
|
import { WorkspaceBuildLogs } from "components/WorkspaceBuildLogs/WorkspaceBuildLogs"
|
||||||
|
import { PublishVersionData } from "pages/TemplateVersionPage/TemplateVersionEditorPage/types"
|
||||||
import { FC, useCallback, useEffect, useRef, useState } from "react"
|
import { FC, useCallback, useEffect, useRef, useState } from "react"
|
||||||
import { navHeight, dashboardContentBottomPadding } from "theme/constants"
|
import { navHeight, dashboardContentBottomPadding } from "theme/constants"
|
||||||
import {
|
import {
|
||||||
@ -36,6 +37,7 @@ import {
|
|||||||
} from "./FileDialog"
|
} from "./FileDialog"
|
||||||
import { FileTreeView } from "./FileTreeView"
|
import { FileTreeView } from "./FileTreeView"
|
||||||
import { MonacoEditor } from "./MonacoEditor"
|
import { MonacoEditor } from "./MonacoEditor"
|
||||||
|
import { PublishTemplateVersionDialog } from "./PublishTemplateVersionDialog"
|
||||||
import {
|
import {
|
||||||
getStatus,
|
getStatus,
|
||||||
TemplateVersionStatusBadge,
|
TemplateVersionStatusBadge,
|
||||||
@ -51,7 +53,12 @@ export interface TemplateVersionEditorProps {
|
|||||||
disablePreview: boolean
|
disablePreview: boolean
|
||||||
disableUpdate: boolean
|
disableUpdate: boolean
|
||||||
onPreview: (files: FileTree) => void
|
onPreview: (files: FileTree) => void
|
||||||
onUpdate: () => void
|
onPublish: () => void
|
||||||
|
onConfirmPublish: (data: PublishVersionData) => void
|
||||||
|
onCancelPublish: () => void
|
||||||
|
publishingError: unknown
|
||||||
|
isAskingPublishParameters: boolean
|
||||||
|
isPublishing: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const topbarHeight = 80
|
const topbarHeight = 80
|
||||||
@ -76,7 +83,12 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
|
|||||||
templateVersion,
|
templateVersion,
|
||||||
defaultFileTree,
|
defaultFileTree,
|
||||||
onPreview,
|
onPreview,
|
||||||
onUpdate,
|
onPublish,
|
||||||
|
onConfirmPublish,
|
||||||
|
onCancelPublish,
|
||||||
|
publishingError,
|
||||||
|
isAskingPublishParameters,
|
||||||
|
isPublishing,
|
||||||
buildLogs,
|
buildLogs,
|
||||||
resources,
|
resources,
|
||||||
}) => {
|
}) => {
|
||||||
@ -156,233 +168,249 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.root}>
|
<>
|
||||||
<div className={styles.topbar}>
|
<div className={styles.root}>
|
||||||
<div className={styles.topbarSides}>
|
<div className={styles.topbar} data-testid="topbar">
|
||||||
<AvatarData
|
<div className={styles.topbarSides}>
|
||||||
title={template.display_name || template.name}
|
<AvatarData
|
||||||
subtitle={template.description}
|
title={template.display_name || template.name}
|
||||||
avatar={
|
subtitle={template.description}
|
||||||
hasIcon && (
|
avatar={
|
||||||
<Avatar src={template.icon} variant="square" fitImage />
|
hasIcon && (
|
||||||
)
|
<Avatar src={template.icon} variant="square" fitImage />
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.topbarSides}>
|
|
||||||
{/* Only start to show the build when a new template version is building */}
|
|
||||||
{templateVersion.id !== firstTemplateVersionOnEditor.current.id && (
|
|
||||||
<div className={styles.buildStatus}>
|
|
||||||
<TemplateVersionStatusBadge version={templateVersion} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
title="Build template (Ctrl + Enter)"
|
|
||||||
size="small"
|
|
||||||
variant="outlined"
|
|
||||||
disabled={disablePreview}
|
|
||||||
onClick={() => {
|
|
||||||
triggerPreview()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Build template
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
title={
|
|
||||||
dirty
|
|
||||||
? "You have edited files! Run another build before updating."
|
|
||||||
: templateVersion.job.status !== "succeeded"
|
|
||||||
? "Something"
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
size="small"
|
|
||||||
disabled={dirty || disableUpdate}
|
|
||||||
onClick={onUpdate}
|
|
||||||
>
|
|
||||||
Publish version
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.sidebarAndEditor}>
|
|
||||||
<div className={styles.sidebar}>
|
|
||||||
<div className={styles.sidebarTitle}>
|
|
||||||
Template files
|
|
||||||
<div className={styles.sidebarActions}>
|
|
||||||
<Tooltip title="Create File" placement="top">
|
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
aria-label="Create File"
|
|
||||||
onClick={(event) => {
|
|
||||||
setCreateFileOpen(true)
|
|
||||||
event.currentTarget.blur()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CreateIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
<CreateFileDialog
|
|
||||||
fileTree={fileTree}
|
|
||||||
open={createFileOpen}
|
|
||||||
onClose={() => {
|
|
||||||
setCreateFileOpen(false)
|
|
||||||
}}
|
|
||||||
checkExists={(path) => existsFile(path, fileTree)}
|
|
||||||
onConfirm={(path) => {
|
|
||||||
setFileTree((fileTree) => createFile(path, fileTree, ""))
|
|
||||||
setActivePath(path)
|
|
||||||
setCreateFileOpen(false)
|
|
||||||
setDirty(true)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<DeleteFileDialog
|
|
||||||
onConfirm={() => {
|
|
||||||
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 || ""}
|
|
||||||
/>
|
|
||||||
<RenameFileDialog
|
|
||||||
fileTree={fileTree}
|
|
||||||
open={Boolean(renameFileOpen)}
|
|
||||||
onClose={() => {
|
|
||||||
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)
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<FileTreeView
|
|
||||||
fileTree={fileTree}
|
<div className={styles.topbarSides}>
|
||||||
onDelete={(file) => setDeleteFileOpen(file)}
|
{/* Only start to show the build when a new template version is building */}
|
||||||
onSelect={(filePath) => {
|
{templateVersion.id !== firstTemplateVersionOnEditor.current.id && (
|
||||||
if (!isFolder(filePath, fileTree)) {
|
<div className={styles.buildStatus}>
|
||||||
setActivePath(filePath)
|
<TemplateVersionStatusBadge version={templateVersion} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
title="Build template (Ctrl + Enter)"
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
disabled={disablePreview}
|
||||||
|
onClick={() => {
|
||||||
|
triggerPreview()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Build template
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
title={
|
||||||
|
dirty
|
||||||
|
? "You have edited files! Run another build before updating."
|
||||||
|
: templateVersion.job.status !== "succeeded"
|
||||||
|
? "Something"
|
||||||
|
: ""
|
||||||
}
|
}
|
||||||
}}
|
size="small"
|
||||||
onRename={(file) => setRenameFileOpen(file)}
|
disabled={dirty || disableUpdate}
|
||||||
activePath={activePath}
|
onClick={onPublish}
|
||||||
/>
|
>
|
||||||
|
Publish version
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.editorPane}>
|
<div className={styles.sidebarAndEditor}>
|
||||||
<div className={styles.editor} data-chromatic="ignore">
|
<div className={styles.sidebar}>
|
||||||
{activePath ? (
|
<div className={styles.sidebarTitle}>
|
||||||
<MonacoEditor
|
Template files
|
||||||
value={editorValue}
|
<div className={styles.sidebarActions}>
|
||||||
path={activePath}
|
<Tooltip title="Create File" placement="top">
|
||||||
onChange={(value) => {
|
<IconButton
|
||||||
if (!activePath) {
|
size="small"
|
||||||
return
|
aria-label="Create File"
|
||||||
}
|
onClick={(event) => {
|
||||||
setFileTree((fileTree) =>
|
setCreateFileOpen(true)
|
||||||
updateFile(activePath, value, fileTree),
|
event.currentTarget.blur()
|
||||||
)
|
}}
|
||||||
|
>
|
||||||
|
<CreateIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<CreateFileDialog
|
||||||
|
fileTree={fileTree}
|
||||||
|
open={createFileOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setCreateFileOpen(false)
|
||||||
|
}}
|
||||||
|
checkExists={(path) => existsFile(path, fileTree)}
|
||||||
|
onConfirm={(path) => {
|
||||||
|
setFileTree((fileTree) => createFile(path, fileTree, ""))
|
||||||
|
setActivePath(path)
|
||||||
|
setCreateFileOpen(false)
|
||||||
setDirty(true)
|
setDirty(true)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
<DeleteFileDialog
|
||||||
<div>No file opened</div>
|
onConfirm={() => {
|
||||||
)}
|
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 || ""}
|
||||||
|
/>
|
||||||
|
<RenameFileDialog
|
||||||
|
fileTree={fileTree}
|
||||||
|
open={Boolean(renameFileOpen)}
|
||||||
|
onClose={() => {
|
||||||
|
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)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<FileTreeView
|
||||||
|
fileTree={fileTree}
|
||||||
|
onDelete={(file) => setDeleteFileOpen(file)}
|
||||||
|
onSelect={(filePath) => {
|
||||||
|
if (!isFolder(filePath, fileTree)) {
|
||||||
|
setActivePath(filePath)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onRename={(file) => setRenameFileOpen(file)}
|
||||||
|
activePath={activePath}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.panelWrapper}>
|
<div className={styles.editorPane}>
|
||||||
<div className={styles.tabs}>
|
<div className={styles.editor} data-chromatic="ignore">
|
||||||
<button
|
{activePath ? (
|
||||||
className={`${styles.tab} ${selectedTab === 0 ? "active" : ""}`}
|
<MonacoEditor
|
||||||
onClick={() => {
|
value={editorValue}
|
||||||
setSelectedTab(0)
|
path={activePath}
|
||||||
}}
|
onChange={(value) => {
|
||||||
>
|
if (!activePath) {
|
||||||
{templateVersion.job.status !== "succeeded" ? (
|
return
|
||||||
getStatus(templateVersion).icon
|
}
|
||||||
) : (
|
setFileTree((fileTree) =>
|
||||||
<BuildIcon />
|
updateFile(activePath, value, fileTree),
|
||||||
)}
|
)
|
||||||
Build Log
|
setDirty(true)
|
||||||
</button>
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div>No file opened</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{!disableUpdate && (
|
<div className={styles.panelWrapper}>
|
||||||
|
<div className={styles.tabs}>
|
||||||
<button
|
<button
|
||||||
className={`${styles.tab} ${
|
className={`${styles.tab} ${
|
||||||
selectedTab === 1 ? "active" : ""
|
selectedTab === 0 ? "active" : ""
|
||||||
}`}
|
}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedTab(1)
|
setSelectedTab(0)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PreviewIcon />
|
{templateVersion.job.status !== "succeeded" ? (
|
||||||
Workspace Preview
|
getStatus(templateVersion).icon
|
||||||
</button>
|
) : (
|
||||||
)}
|
<BuildIcon />
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={`${styles.panel} ${styles.buildLogs} ${
|
|
||||||
selectedTab === 0 ? "" : "hidden"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{buildLogs && (
|
|
||||||
<WorkspaceBuildLogs
|
|
||||||
templateEditorPane
|
|
||||||
hideTimestamps
|
|
||||||
logs={buildLogs}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{templateVersion.job.error && (
|
|
||||||
<div className={styles.buildLogError}>
|
|
||||||
{templateVersion.job.error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={`${styles.panel} ${styles.resources} ${
|
|
||||||
selectedTab === 1 ? "" : "hidden"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{resources && (
|
|
||||||
<TemplateResourcesTable
|
|
||||||
resources={resources.filter(
|
|
||||||
(r) => r.workspace_transition === "start",
|
|
||||||
)}
|
)}
|
||||||
/>
|
Build Log
|
||||||
)}
|
</button>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{templateVersionSucceeded && (
|
{!disableUpdate && (
|
||||||
<>
|
<button
|
||||||
<div className={styles.panelDivider} />
|
className={`${styles.tab} ${
|
||||||
</>
|
selectedTab === 1 ? "active" : ""
|
||||||
)}
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedTab(1)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PreviewIcon />
|
||||||
|
Workspace Preview
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`${styles.panel} ${styles.buildLogs} ${
|
||||||
|
selectedTab === 0 ? "" : "hidden"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{buildLogs && (
|
||||||
|
<WorkspaceBuildLogs
|
||||||
|
templateEditorPane
|
||||||
|
hideTimestamps
|
||||||
|
logs={buildLogs}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{templateVersion.job.error && (
|
||||||
|
<div className={styles.buildLogError}>
|
||||||
|
{templateVersion.job.error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`${styles.panel} ${styles.resources} ${
|
||||||
|
selectedTab === 1 ? "" : "hidden"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{resources && (
|
||||||
|
<TemplateResourcesTable
|
||||||
|
resources={resources.filter(
|
||||||
|
(r) => r.workspace_transition === "start",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{templateVersionSucceeded && (
|
||||||
|
<>
|
||||||
|
<div className={styles.panelDivider} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</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 { usePermissions } from "hooks/usePermissions"
|
||||||
import { FC } from "react"
|
import { FC } from "react"
|
||||||
import { Helmet } from "react-helmet-async"
|
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 { pageTitle } from "util/page"
|
||||||
import { templateVersionEditorMachine } from "xServices/templateVersionEditor/templateVersionEditorXService"
|
import { templateVersionEditorMachine } from "xServices/templateVersionEditor/templateVersionEditorXService"
|
||||||
import { useTemplateVersionData } from "./data"
|
import { useTemplateVersionData } from "./data"
|
||||||
@ -16,9 +16,15 @@ type Params = {
|
|||||||
|
|
||||||
export const TemplateVersionEditorPage: FC = () => {
|
export const TemplateVersionEditorPage: FC = () => {
|
||||||
const { version: versionName, template: templateName } = useParams() as Params
|
const { version: versionName, template: templateName } = useParams() as Params
|
||||||
|
const navigate = useNavigate()
|
||||||
const orgId = useOrganizationId()
|
const orgId = useOrganizationId()
|
||||||
const [editorState, sendEvent] = useMachine(templateVersionEditorMachine, {
|
const [editorState, sendEvent] = useMachine(templateVersionEditorMachine, {
|
||||||
context: { orgId },
|
context: { orgId },
|
||||||
|
actions: {
|
||||||
|
onPublish: () => {
|
||||||
|
navigate(`/templates/${templateName}`)
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
const permissions = usePermissions()
|
const permissions = usePermissions()
|
||||||
const { isSuccess, data } = useTemplateVersionData(
|
const { isSuccess, data } = useTemplateVersionData(
|
||||||
@ -53,11 +59,27 @@ export const TemplateVersionEditorPage: FC = () => {
|
|||||||
templateId: data.template.id,
|
templateId: data.template.id,
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
onUpdate={() => {
|
onCancelPublish={() => {
|
||||||
sendEvent({
|
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")}
|
disablePreview={editorState.hasTag("loading")}
|
||||||
disableUpdate={
|
disableUpdate={
|
||||||
editorState.hasTag("loading") ||
|
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,
|
autostart_schedule: MockWorkspaceAutostartEnabled.schedule,
|
||||||
ttl_ms: 2 * 60 * 60 * 1000,
|
ttl_ms: 2 * 60 * 60 * 1000,
|
||||||
latest_build: MockWorkspaceBuild,
|
latest_build: MockWorkspaceBuild,
|
||||||
last_used_at: "",
|
last_used_at: "2022-05-16T15:29:10.302441433Z",
|
||||||
organization_id: MockOrganization.id,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MockStoppedWorkspace: TypesGen.Workspace = {
|
export const MockStoppedWorkspace: TypesGen.Workspace = {
|
||||||
|
@ -4,6 +4,8 @@ import { CreateWorkspaceBuildRequest } from "../api/typesGenerated"
|
|||||||
import { permissionsToCheck } from "../xServices/auth/authXService"
|
import { permissionsToCheck } from "../xServices/auth/authXService"
|
||||||
import * as M from "./entities"
|
import * as M from "./entities"
|
||||||
import { MockGroup, MockWorkspaceQuota } from "./entities"
|
import { MockGroup, MockWorkspaceQuota } from "./entities"
|
||||||
|
import fs from "fs"
|
||||||
|
import path from "path"
|
||||||
|
|
||||||
export const handlers = [
|
export const handlers = [
|
||||||
rest.get("/api/v2/templates/:templateId/daus", async (req, res, ctx) => {
|
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]))
|
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 { FileTree, traverse } from "util/filetree"
|
||||||
import { isAllowedFile } from "util/templateVersion"
|
import { isAllowedFile } from "util/templateVersion"
|
||||||
import { TarReader, TarWriter } from "util/tar"
|
import { TarReader, TarWriter } from "util/tar"
|
||||||
|
import { PublishVersionData } from "pages/TemplateVersionPage/TemplateVersionEditorPage/types"
|
||||||
|
|
||||||
export interface CreateVersionData {
|
export interface CreateVersionData {
|
||||||
file: File
|
file: File
|
||||||
@ -24,6 +25,7 @@ export interface TemplateVersionEditorMachineContext {
|
|||||||
resources?: WorkspaceResource[]
|
resources?: WorkspaceResource[]
|
||||||
buildLogs?: ProvisionerJobLog[]
|
buildLogs?: ProvisionerJobLog[]
|
||||||
tarReader?: TarReader
|
tarReader?: TarReader
|
||||||
|
publishingError?: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
export const templateVersionEditorMachine = createMachine(
|
export const templateVersionEditorMachine = createMachine(
|
||||||
@ -41,7 +43,10 @@ export const templateVersionEditorMachine = createMachine(
|
|||||||
}
|
}
|
||||||
| { type: "CANCEL_VERSION" }
|
| { type: "CANCEL_VERSION" }
|
||||||
| { type: "ADD_BUILD_LOG"; log: ProvisionerJobLog }
|
| { type: "ADD_BUILD_LOG"; log: ProvisionerJobLog }
|
||||||
| { type: "UPDATE_ACTIVE_VERSION" },
|
| { type: "PUBLISH" }
|
||||||
|
| ({ type: "CONFIRM_PUBLISH" } & PublishVersionData)
|
||||||
|
| { type: "CANCEL_PUBLISH" },
|
||||||
|
|
||||||
services: {} as {
|
services: {} as {
|
||||||
uploadTar: {
|
uploadTar: {
|
||||||
data: UploadResponse
|
data: UploadResponse
|
||||||
@ -58,7 +63,7 @@ export const templateVersionEditorMachine = createMachine(
|
|||||||
getResources: {
|
getResources: {
|
||||||
data: WorkspaceResource[]
|
data: WorkspaceResource[]
|
||||||
}
|
}
|
||||||
updateActiveVersion: {
|
publishingVersion: {
|
||||||
data: void
|
data: void
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -80,18 +85,29 @@ export const templateVersionEditorMachine = createMachine(
|
|||||||
actions: ["assignCreateBuild"],
|
actions: ["assignCreateBuild"],
|
||||||
target: "cancelingBuild",
|
target: "cancelingBuild",
|
||||||
},
|
},
|
||||||
UPDATE_ACTIVE_VERSION: {
|
PUBLISH: {
|
||||||
target: "updatingActiveVersion",
|
target: "askPublishParameters",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
updatingActiveVersion: {
|
askPublishParameters: {
|
||||||
|
on: {
|
||||||
|
CANCEL_PUBLISH: "idle",
|
||||||
|
CONFIRM_PUBLISH: "publishingVersion",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
publishingVersion: {
|
||||||
tags: "loading",
|
tags: "loading",
|
||||||
|
entry: ["clearPublishingError"],
|
||||||
invoke: {
|
invoke: {
|
||||||
id: "updateActiveVersion",
|
id: "publishingVersion",
|
||||||
src: "updateActiveVersion",
|
src: "publishingVersion",
|
||||||
onDone: {
|
onDone: {
|
||||||
target: "idle",
|
actions: ["onPublish"],
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
actions: ["assignPublishingError"],
|
||||||
|
target: "askPublishParameters",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -215,6 +231,10 @@ export const templateVersionEditorMachine = createMachine(
|
|||||||
assignTarReader: assign({
|
assignTarReader: assign({
|
||||||
tarReader: (_, { tarReader }) => tarReader,
|
tarReader: (_, { tarReader }) => tarReader,
|
||||||
}),
|
}),
|
||||||
|
assignPublishingError: assign({
|
||||||
|
publishingError: (_, event) => event.data,
|
||||||
|
}),
|
||||||
|
clearPublishingError: assign({ publishingError: (_) => undefined }),
|
||||||
},
|
},
|
||||||
services: {
|
services: {
|
||||||
uploadTar: async ({ fileTree, tarReader }) => {
|
uploadTar: async ({ fileTree, tarReader }) => {
|
||||||
@ -285,28 +305,17 @@ export const templateVersionEditorMachine = createMachine(
|
|||||||
}
|
}
|
||||||
return API.getTemplateVersion(ctx.version.id)
|
return API.getTemplateVersion(ctx.version.id)
|
||||||
},
|
},
|
||||||
watchBuildLogs: (ctx) => async (callback) => {
|
watchBuildLogs:
|
||||||
return new Promise<void>((resolve, reject) => {
|
({ version }) =>
|
||||||
if (!ctx.version) {
|
async (callback) => {
|
||||||
return reject("version must be set")
|
if (!version) {
|
||||||
|
throw new Error("version must be set")
|
||||||
}
|
}
|
||||||
const proto = location.protocol === "https:" ? "wss:" : "ws:"
|
|
||||||
const socket = new WebSocket(
|
return API.watchBuildLogs(version.id, (log) => {
|
||||||
`${proto}//${location.host}/api/v2/templateversions/${ctx.version?.id}/logs?follow=true`,
|
callback({ type: "ADD_BUILD_LOG", log })
|
||||||
)
|
|
||||||
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()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
getResources: (ctx) => {
|
getResources: (ctx) => {
|
||||||
if (!ctx.version) {
|
if (!ctx.version) {
|
||||||
throw new Error("template version must be set")
|
throw new Error("template version must be set")
|
||||||
@ -321,16 +330,24 @@ export const templateVersionEditorMachine = createMachine(
|
|||||||
await API.cancelTemplateVersionBuild(ctx.version.id)
|
await API.cancelTemplateVersionBuild(ctx.version.id)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateActiveVersion: async (ctx) => {
|
publishingVersion: async (
|
||||||
if (!ctx.templateId) {
|
{ version, templateId },
|
||||||
throw new Error("template must be set")
|
{ name, isActiveVersion },
|
||||||
|
) => {
|
||||||
|
if (!version) {
|
||||||
|
throw new Error("Version is not set")
|
||||||
}
|
}
|
||||||
if (!ctx.version) {
|
if (!templateId) {
|
||||||
throw new Error("template version must be set")
|
throw new Error("Template is not set")
|
||||||
}
|
}
|
||||||
await API.updateActiveTemplateVersion(ctx.templateId, {
|
await Promise.all([
|
||||||
id: ctx.version.id,
|
API.patchTemplateVersion(version.id, { name }),
|
||||||
})
|
isActiveVersion
|
||||||
|
? API.updateActiveTemplateVersion(templateId, {
|
||||||
|
id: version.id,
|
||||||
|
})
|
||||||
|
: Promise.resolve(),
|
||||||
|
])
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user