Skip to content

feat(site): Ask for version name and if it is active when publishing a new version on editor #6756

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Mar 27, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add tests
  • Loading branch information
BrunoQuaresma committed Mar 23, 2023
commit a2eec188e8a78d2ad0836dbcf6ff5ac25b1c8003
25 changes: 24 additions & 1 deletion site/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ export const patchTemplateVersion = async (
templateVersionId: string,
data: TypesGen.PatchTemplateVersionRequest,
) => {
const response = await axios.patch<Types.Message>(
const response = await axios.patch<TypesGen.TemplateVersion>(
`/api/v2/templateversions/${templateVersionId}`,
data,
)
Expand Down Expand Up @@ -1015,3 +1015,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()
})
})
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { DialogProps } from "components/Dialogs/Dialog"
import { FC } from "react"
import { getFormHelpers, nameValidator } from "util/formUtils"
import { getFormHelpers } from "util/formUtils"
import { FormFields } from "components/Form/Form"
import { useFormik } from "formik"
import * as Yup from "yup"
Expand All @@ -12,21 +12,22 @@ import FormControlLabel from "@material-ui/core/FormControlLabel"
import { Stack } from "components/Stack/Stack"

export type PublishTemplateVersionDialogProps = DialogProps & {
defaultName: string
isPublishing: boolean
onClose: () => void
onConfirm: (data: PublishVersionData) => void
}

export const PublishTemplateVersionDialog: FC<
PublishTemplateVersionDialogProps
> = ({ onConfirm, isPublishing, onClose, ...dialogProps }) => {
> = ({ onConfirm, isPublishing, onClose, defaultName, ...dialogProps }) => {
const form = useFormik({
initialValues: {
name: "",
name: defaultName,
isActiveVersion: false,
},
validationSchema: Yup.object({
name: nameValidator("name").optional(),
name: Yup.string().required(),
isActiveVersion: Yup.boolean(),
}),
onSubmit: onConfirm,
Expand Down Expand Up @@ -62,7 +63,6 @@ export const PublishTemplateVersionDialog: FC<
InputLabelProps={{
shrink: true,
}}
helperText="If you leave this blank, the version name will be automatically generated."
/>

<FormControlLabel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ 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}
Expand Down Expand Up @@ -404,6 +404,7 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
onClose={onCancelPublish}
onConfirm={onConfirmPublish}
isPublishing={isPublishing}
defaultName={templateVersion.name}
/>
</>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
})
})
})
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type PublishVersionData = {
name?: string
name: string
isActiveVersion: boolean
}
5 changes: 3 additions & 2 deletions site/src/testHelpers/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -657,9 +657,10 @@ export const MockWorkspace: TypesGen.Workspace = {
owner_id: MockUser.id,
owner_name: MockUser.username,
autostart_schedule: MockWorkspaceAutostartEnabled.schedule,
ttl_ms: 2 * 60 * 60 * 1000, // 2 hours as milliseconds
ttl_ms: 2 * 60 * 60 * 1000,
latest_build: MockWorkspaceBuild,
last_used_at: "",
last_used_at: "2022-05-16T15:29:10.302441433Z",
organization_id: MockOrganization.id,
}

export const MockStoppedWorkspace: TypesGen.Workspace = {
Expand Down
15 changes: 15 additions & 0 deletions site/src/testHelpers/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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),
)
}),
]
Binary file added site/src/testHelpers/templateFiles.tar
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -295,28 +295,17 @@ 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) => {
if (!ctx.version) {
throw new Error("template version must be set")
Expand All @@ -342,7 +331,7 @@ export const templateVersionEditorMachine = createMachine(
throw new Error("Template is not set")
}
await Promise.all([
API.patchTemplateVersion(version.id, { name: name ?? version.name }),
API.patchTemplateVersion(version.id, { name }),
isActiveVersion
? API.updateActiveTemplateVersion(templateId, {
id: version.id,
Expand Down