From 27645b75575818ed80aacdd08b9184e8f1b544f8 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Tue, 22 Nov 2022 17:26:59 +0000 Subject: [PATCH 1/8] Add change versions --- .../components/DropdownButton/ActionCtas.tsx | 18 +++++++++++++++++ site/src/components/Workspace/Workspace.tsx | 3 +++ .../WorkspaceActions.test.tsx | 2 ++ .../WorkspaceActions/WorkspaceActions.tsx | 6 ++++++ .../components/WorkspaceActions/constants.ts | 20 ++++++++++++++++--- site/src/i18n/en/workspacePage.json | 3 ++- .../WorkspacePage/WorkspaceReadyPage.tsx | 3 +++ 7 files changed, 51 insertions(+), 4 deletions(-) diff --git a/site/src/components/DropdownButton/ActionCtas.tsx b/site/src/components/DropdownButton/ActionCtas.tsx index 0186797a9703d..1ccbb82ed2b72 100644 --- a/site/src/components/DropdownButton/ActionCtas.tsx +++ b/site/src/components/DropdownButton/ActionCtas.tsx @@ -3,6 +3,7 @@ import Button from "@material-ui/core/Button" import { makeStyles } from "@material-ui/core/styles" import BlockIcon from "@material-ui/icons/Block" import CloudQueueIcon from "@material-ui/icons/CloudQueue" +import UpdateOutlined from "@material-ui/icons/UpdateOutlined" import CropSquareIcon from "@material-ui/icons/CropSquare" import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline" import PlayCircleOutlineIcon from "@material-ui/icons/PlayCircleOutline" @@ -33,6 +34,23 @@ export const UpdateButton: FC> = ({ ) } +export const ChangeVersionButton: FC< + React.PropsWithChildren +> = ({ handleAction }) => { + const styles = useStyles() + const { t } = useTranslation("workspacePage") + + return ( + + ) +} + export const StartButton: FC> = ({ handleAction, }) => { diff --git a/site/src/components/Workspace/Workspace.tsx b/site/src/components/Workspace/Workspace.tsx index edd59eb123669..0634f10a07a4e 100644 --- a/site/src/components/Workspace/Workspace.tsx +++ b/site/src/components/Workspace/Workspace.tsx @@ -45,6 +45,7 @@ export interface WorkspaceProps { handleDelete: () => void handleUpdate: () => void handleCancel: () => void + handleChangeVersion: () => void isUpdating: boolean workspace: TypesGen.Workspace resources?: TypesGen.WorkspaceResource[] @@ -68,6 +69,7 @@ export const Workspace: FC> = ({ handleDelete, handleUpdate, handleCancel, + handleChangeVersion, workspace, isUpdating, resources, @@ -143,6 +145,7 @@ export const Workspace: FC> = ({ handleDelete={handleDelete} handleUpdate={handleUpdate} handleCancel={handleCancel} + handleChangeVersion={handleChangeVersion} isUpdating={isUpdating} /> diff --git a/site/src/components/WorkspaceActions/WorkspaceActions.test.tsx b/site/src/components/WorkspaceActions/WorkspaceActions.test.tsx index e8d4368f03053..27d59496fbf06 100644 --- a/site/src/components/WorkspaceActions/WorkspaceActions.test.tsx +++ b/site/src/components/WorkspaceActions/WorkspaceActions.test.tsx @@ -18,6 +18,7 @@ const renderComponent = async (props: Partial = {}) => { handleDelete={jest.fn()} handleUpdate={jest.fn()} handleCancel={jest.fn()} + handleChangeVersion={jest.fn()} isUpdating={false} />, ) @@ -35,6 +36,7 @@ const renderAndClick = async (props: Partial = {}) => { handleDelete={jest.fn()} handleUpdate={jest.fn()} handleCancel={jest.fn()} + handleChangeVersion={jest.fn()} isUpdating={false} />, ) diff --git a/site/src/components/WorkspaceActions/WorkspaceActions.tsx b/site/src/components/WorkspaceActions/WorkspaceActions.tsx index 503ae6e7947d6..0f5064fe82229 100644 --- a/site/src/components/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/components/WorkspaceActions/WorkspaceActions.tsx @@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next" import { WorkspaceStatus } from "../../api/typesGenerated" import { ActionLoadingButton, + ChangeVersionButton, DeleteButton, DisabledButton, StartButton, @@ -20,6 +21,7 @@ export interface WorkspaceActionsProps { handleDelete: () => void handleUpdate: () => void handleCancel: () => void + handleChangeVersion: () => void isUpdating: boolean children?: ReactNode } @@ -32,6 +34,7 @@ export const WorkspaceActions: FC = ({ handleDelete, handleUpdate, handleCancel, + handleChangeVersion, isUpdating, }) => { const { t } = useTranslation("workspacePage") @@ -45,6 +48,9 @@ export const WorkspaceActions: FC = ({ [ButtonTypesEnum.updating]: ( ), + [ButtonTypesEnum.changeVersion]: ( + + ), [ButtonTypesEnum.start]: , [ButtonTypesEnum.starting]: ( diff --git a/site/src/components/WorkspaceActions/constants.ts b/site/src/components/WorkspaceActions/constants.ts index fb3cb13dd0e5c..5d5c59434d48b 100644 --- a/site/src/components/WorkspaceActions/constants.ts +++ b/site/src/components/WorkspaceActions/constants.ts @@ -11,6 +11,7 @@ export enum ButtonTypesEnum { deleting = "deleting", update = "update", updating = "updating", + changeVersion = "changeVersion", // disabled buttons canceling = "canceling", deleted = "deleted", @@ -34,7 +35,11 @@ export const statusToAbilities: Record = { canAcceptJobs: false, }, running: { - actions: [ButtonTypesEnum.stop, ButtonTypesEnum.delete], + actions: [ + ButtonTypesEnum.stop, + ButtonTypesEnum.changeVersion, + ButtonTypesEnum.delete, + ], canCancel: false, canAcceptJobs: true, }, @@ -44,7 +49,11 @@ export const statusToAbilities: Record = { canAcceptJobs: false, }, stopped: { - actions: [ButtonTypesEnum.start, ButtonTypesEnum.delete], + actions: [ + ButtonTypesEnum.start, + ButtonTypesEnum.changeVersion, + ButtonTypesEnum.delete, + ], canCancel: false, canAcceptJobs: true, }, @@ -52,6 +61,7 @@ export const statusToAbilities: Record = { actions: [ ButtonTypesEnum.start, ButtonTypesEnum.stop, + ButtonTypesEnum.changeVersion, ButtonTypesEnum.delete, ], canCancel: false, @@ -59,7 +69,11 @@ export const statusToAbilities: Record = { }, // in the case of an error failed: { - actions: [ButtonTypesEnum.start, ButtonTypesEnum.delete], + actions: [ + ButtonTypesEnum.start, + ButtonTypesEnum.changeVersion, + ButtonTypesEnum.delete, + ], canCancel: false, canAcceptJobs: true, }, diff --git a/site/src/i18n/en/workspacePage.json b/site/src/i18n/en/workspacePage.json index 1c486a3275855..2ee997dccb439 100644 --- a/site/src/i18n/en/workspacePage.json +++ b/site/src/i18n/en/workspacePage.json @@ -27,7 +27,8 @@ "updating": "Updating", "starting": "Starting...", "stopping": "Stopping...", - "deleting": "Deleting..." + "deleting": "Deleting...", + "changeVersion": "Change version" }, "disabledButton": { "canceling": "Canceling", diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index ec0c96b402ca8..c5ef7b93e6d45 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -4,6 +4,7 @@ import dayjs from "dayjs" import { useContext } from "react" import { Helmet } from "react-helmet-async" import { useTranslation } from "react-i18next" +import { useNavigate } from "react-router-dom" import { getMaxDeadline, getMaxDeadlineChange, @@ -63,6 +64,7 @@ export const WorkspaceReadyPage = ({ const canUpdateWorkspace = Boolean(permissions?.updateWorkspace) const { t } = useTranslation("workspacePage") const favicon = getFaviconByStatus(workspace.latest_build) + const navigate = useNavigate() return ( <> @@ -114,6 +116,7 @@ export const WorkspaceReadyPage = ({ handleDelete={() => workspaceSend({ type: "ASK_DELETE" })} handleUpdate={() => workspaceSend({ type: "UPDATE" })} handleCancel={() => workspaceSend({ type: "CANCEL" })} + handleChangeVersion={() => navigate("/change-version")} resources={workspace.latest_build.resources} builds={builds} canUpdateWorkspace={canUpdateWorkspace} From db02cc016d4e002cd6c0db351b8e2327d614a824 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 23 Nov 2022 14:18:14 +0000 Subject: [PATCH 2/8] Add base change version flow --- site/src/AppRouter.tsx | 13 ++ .../WorkspaceChangeVersionPage.tsx | 199 ++++++++++++++++++ .../WorkspacePage/WorkspaceReadyPage.tsx | 2 +- .../changeWorkspaceVersionXService.tsx | 142 +++++++++++++ 4 files changed, 355 insertions(+), 1 deletion(-) create mode 100644 site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPage.tsx create mode 100644 site/src/xServices/workspace/changeWorkspaceVersionXService.tsx diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index 3159ad8d44324..1b1cf04006a98 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -46,6 +46,9 @@ const WorkspaceBuildPage = lazy( () => import("./pages/WorkspaceBuildPage/WorkspaceBuildPage"), ) const WorkspacePage = lazy(() => import("./pages/WorkspacePage/WorkspacePage")) +const WorkspaceChangeVersionPage = lazy( + () => import("./pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPage"), +) const WorkspaceSchedulePage = lazy( () => import("./pages/WorkspaceSchedulePage/WorkspaceSchedulePage"), ) @@ -360,6 +363,7 @@ export const AppRouter: FC = () => { } /> + { } /> + + + + + } + /> diff --git a/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPage.tsx b/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPage.tsx new file mode 100644 index 0000000000000..debb730fab3be --- /dev/null +++ b/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPage.tsx @@ -0,0 +1,199 @@ +import MenuItem from "@material-ui/core/MenuItem" +import { makeStyles } from "@material-ui/core/styles" +import TextField from "@material-ui/core/TextField" +import { useMachine } from "@xstate/react" +import { Template, TemplateVersion, Workspace } from "api/typesGenerated" +import { FormFooter } from "components/FormFooter/FormFooter" +import { FullPageForm } from "components/FullPageForm/FullPageForm" +import { Loader } from "components/Loader/Loader" +import { Pill } from "components/Pill/Pill" +import { Stack } from "components/Stack/Stack" +import { useFormik } from "formik" +import { FC } from "react" +import { useNavigate, useParams } from "react-router-dom" +import { createDayString } from "util/createDayString" +import { changeWorkspaceVersionMachine } from "xServices/workspace/changeWorkspaceVersionXService" + +const WorkspaceChangeVersionForm: FC<{ + isLoading: boolean + workspace: Workspace + template: Template + versions: TemplateVersion[] + onSubmit: (versionId: string) => void + onCancel: () => void +}> = ({ isLoading, workspace, template, versions, onSubmit, onCancel }) => { + const styles = useStyles() + const formik = useFormik({ + initialValues: { + versionId: workspace.latest_build.template_version_id, + }, + onSubmit: ({ versionId }) => onSubmit(versionId), + }) + + return ( +
+ + +
+ +
+ + {workspace.name} + + + {workspace.template_display_name.length > 0 + ? workspace.template_display_name + : workspace.template_name} + + +
+ + { + const version = versions.find( + (version) => version.id === versionId, + ) + if (!version) { + throw new Error(`${versionId} not found.`) + } + return <>{version.name} + }, + }} + {...formik.getFieldProps("versionId")} + > + {versions + .slice() + .reverse() + .map((version) => ( + +
+
{version.name}
+
+ Created by {version.created_by.username}{" "} + {createDayString(version.created_at)} +
+
+ + {template.active_version_id === version.id && ( + + )} +
+ ))} +
+
+ + + + ) +} + +export const WorkspaceChangeVersionPage: FC = () => { + const navigate = useNavigate() + const { username: owner, workspace: workspaceName } = useParams() as { + username: string + workspace: string + } + const [state, send] = useMachine(changeWorkspaceVersionMachine, { + context: { + owner, + workspaceName, + }, + actions: { + onUpdateVersion: () => { + navigate(-1) + }, + }, + }) + const { workspace, templateVersions, template } = state.context + + return ( + navigate(-1)}> + {workspace && template && templateVersions ? ( + { + send({ + type: "UPDATE_VERSION", + versionId, + }) + }} + onCancel={() => { + navigate(-1) + }} + /> + ) : ( + + )} + + ) +} + +const useStyles = makeStyles((theme) => ({ + workspace: { + padding: theme.spacing(2.5, 3), + borderRadius: theme.shape.borderRadius, + backgroundColor: theme.palette.background.paper, + border: `1px solid ${theme.palette.divider}`, + }, + + workspaceName: { + fontSize: 16, + }, + + workspaceDescription: { + fontSize: 14, + color: theme.palette.text.secondary, + }, + + workspaceIcon: { + width: theme.spacing(5), + lineHeight: 1, + + "& img": { + width: "100%", + }, + }, + + menuItem: { + paddingTop: theme.spacing(2), + paddingBottom: theme.spacing(2), + position: "relative", + }, + + versionDescription: { + fontSize: 12, + color: theme.palette.text.secondary, + }, + + activePill: { + position: "absolute", + top: theme.spacing(2), + right: theme.spacing(2), + }, +})) + +export default WorkspaceChangeVersionPage diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index c5ef7b93e6d45..ea87ad679a8c4 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -116,7 +116,7 @@ export const WorkspaceReadyPage = ({ handleDelete={() => workspaceSend({ type: "ASK_DELETE" })} handleUpdate={() => workspaceSend({ type: "UPDATE" })} handleCancel={() => workspaceSend({ type: "CANCEL" })} - handleChangeVersion={() => navigate("/change-version")} + handleChangeVersion={() => navigate("change-version")} resources={workspace.latest_build.resources} builds={builds} canUpdateWorkspace={canUpdateWorkspace} diff --git a/site/src/xServices/workspace/changeWorkspaceVersionXService.tsx b/site/src/xServices/workspace/changeWorkspaceVersionXService.tsx new file mode 100644 index 0000000000000..c9291426e6644 --- /dev/null +++ b/site/src/xServices/workspace/changeWorkspaceVersionXService.tsx @@ -0,0 +1,142 @@ +import { + getTemplate, + getTemplateVersions, + getWorkspaceByOwnerAndName, + startWorkspace, +} from "api/api" +import { + Template, + TemplateVersion, + Workspace, + WorkspaceBuild, +} from "api/typesGenerated" +import { assign, createMachine } from "xstate" + +interface ChangeWorkspaceVersionSchema { + context: { + owner: string + workspaceName: string + workspace?: Workspace + template?: Template + templateVersions?: TemplateVersion[] + error?: unknown + } + + services: { + getWorkspace: { + data: Workspace + } + getTemplateData: { + data: { + template: Template + versions: TemplateVersion[] + } + } + updateVersion: { + data: WorkspaceBuild + } + } + + events: { + type: "UPDATE_VERSION" + versionId: string + } +} + +export const changeWorkspaceVersionMachine = createMachine( + { + id: "changeWorkspaceVersion", + predictableActionArguments: true, + schema: {} as ChangeWorkspaceVersionSchema, + tsTypes: {} as import("./changeWorkspaceVersionXService.typegen").Typegen0, + initial: "loadingWorkspace", + states: { + loadingWorkspace: { + invoke: { + src: "getWorkspace", + onDone: { + target: "loadingTemplateData", + actions: "assignWorkspace", + }, + onError: { + target: "idle", + actions: "assignError", + }, + }, + }, + loadingTemplateData: { + invoke: { + src: "getTemplateData", + onDone: { + target: "idle", + actions: "assignTemplateData", + }, + onError: { + target: "idle", + actions: "assignError", + }, + }, + }, + idle: { + on: { + UPDATE_VERSION: "updatingVersion", + }, + }, + updatingVersion: { + invoke: { + src: "updateVersion", + onDone: { + target: "idle", + actions: "onUpdateVersion", + }, + onError: { + target: "idle", + actions: "assignError", + }, + }, + }, + }, + }, + { + services: { + getWorkspace: ({ owner, workspaceName }) => + getWorkspaceByOwnerAndName(owner, workspaceName), + + getTemplateData: async ({ workspace }) => { + if (!workspace) { + throw new Error("Workspace not defined.") + } + + const [template, versions] = await Promise.all([ + getTemplate(workspace.template_id), + getTemplateVersions(workspace.template_id), + ]) + + return { template, versions } + }, + + updateVersion: ({ workspace }, { versionId }) => { + if (!workspace) { + throw new Error("Workspace not defined.") + } + + return startWorkspace(workspace.id, versionId) + }, + }, + + actions: { + assignError: assign({ + error: (_, { data }) => data, + }), + + assignWorkspace: assign({ + workspace: (_, { data }) => data, + }), + + assignTemplateData: assign({ + template: (_, { data }) => data.template, + templateVersions: (_, { data }) => data.versions, + }), + }, + }, +) From 7ec2cd42325dd6174610706356f5aa8d6fe8b6c3 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 23 Nov 2022 17:29:06 +0000 Subject: [PATCH 3/8] Add storybook tests --- .../WorkspaceChangeVersionForm.tsx | 147 ++++++++++++++ .../WorkspaceChangeVersionPage.tsx | 191 ++---------------- ...WorkspaceChangeVersionPageView.stories.tsx | 51 +++++ .../WorkspaceChangeVersionPageView.tsx | 48 +++++ site/src/testHelpers/entities.ts | 21 +- .../changeWorkspaceVersionXService.tsx | 18 +- 6 files changed, 293 insertions(+), 183 deletions(-) create mode 100644 site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionForm.tsx create mode 100644 site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPageView.stories.tsx create mode 100644 site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPageView.tsx diff --git a/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionForm.tsx b/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionForm.tsx new file mode 100644 index 0000000000000..24c863c2a827d --- /dev/null +++ b/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionForm.tsx @@ -0,0 +1,147 @@ +import { makeStyles } from "@material-ui/core/styles" +import TextField from "@material-ui/core/TextField" +import Autocomplete from "@material-ui/lab/Autocomplete" +import { Template, TemplateVersion, Workspace } from "api/typesGenerated" +import { FormFooter } from "components/FormFooter/FormFooter" +import { Pill } from "components/Pill/Pill" +import { Stack } from "components/Stack/Stack" +import { useFormik } from "formik" +import { FC } from "react" +import { createDayString } from "util/createDayString" +import * as Yup from "yup" + +const validationSchema = Yup.object({ + versionId: Yup.string().required(), +}) + +export const WorkspaceChangeVersionForm: FC<{ + isLoading: boolean + workspace: Workspace + template: Template + versions: TemplateVersion[] + onSubmit: (versionId: string) => void + onCancel: () => void +}> = ({ isLoading, workspace, template, versions, onSubmit, onCancel }) => { + const styles = useStyles() + const formik = useFormik({ + initialValues: { + versionId: workspace.latest_build.template_version_id, + }, + validationSchema, + onSubmit: ({ versionId }) => onSubmit(versionId), + }) + + return ( +
+ + +
+ +
+ + {workspace.name} + + + {workspace.template_display_name.length > 0 + ? workspace.template_display_name + : workspace.template_name} + + +
+ + version.id === formik.values.versionId, + )} + renderInput={(params) => ( + + )} + getOptionLabel={(version: TemplateVersion) => version.name} + renderOption={(version: TemplateVersion) => ( +
+
+
{version.name}
+
+ Created by {version.created_by.username}{" "} + {createDayString(version.created_at)} +
+
+ + {template.active_version_id === version.id && ( + + )} +
+ )} + /> +
+ + + + ) +} + +const useStyles = makeStyles((theme) => ({ + workspace: { + padding: theme.spacing(2.5, 3), + borderRadius: theme.shape.borderRadius, + backgroundColor: theme.palette.background.paper, + border: `1px solid ${theme.palette.divider}`, + }, + + workspaceName: { + fontSize: 16, + }, + + workspaceDescription: { + fontSize: 14, + color: theme.palette.text.secondary, + }, + + workspaceIcon: { + width: theme.spacing(5), + lineHeight: 1, + + "& img": { + width: "100%", + }, + }, + + menuItem: { + paddingTop: theme.spacing(1), + paddingBottom: theme.spacing(1), + position: "relative", + width: "100%", + }, + + versionDescription: { + fontSize: 12, + color: theme.palette.text.secondary, + }, + + activePill: { + position: "absolute", + top: theme.spacing(2), + right: theme.spacing(2), + }, +})) diff --git a/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPage.tsx b/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPage.tsx index debb730fab3be..9418c91536c18 100644 --- a/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPage.tsx +++ b/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPage.tsx @@ -1,112 +1,9 @@ -import MenuItem from "@material-ui/core/MenuItem" -import { makeStyles } from "@material-ui/core/styles" -import TextField from "@material-ui/core/TextField" import { useMachine } from "@xstate/react" -import { Template, TemplateVersion, Workspace } from "api/typesGenerated" -import { FormFooter } from "components/FormFooter/FormFooter" -import { FullPageForm } from "components/FullPageForm/FullPageForm" -import { Loader } from "components/Loader/Loader" -import { Pill } from "components/Pill/Pill" -import { Stack } from "components/Stack/Stack" -import { useFormik } from "formik" import { FC } from "react" +import { Helmet } from "react-helmet-async" import { useNavigate, useParams } from "react-router-dom" -import { createDayString } from "util/createDayString" import { changeWorkspaceVersionMachine } from "xServices/workspace/changeWorkspaceVersionXService" - -const WorkspaceChangeVersionForm: FC<{ - isLoading: boolean - workspace: Workspace - template: Template - versions: TemplateVersion[] - onSubmit: (versionId: string) => void - onCancel: () => void -}> = ({ isLoading, workspace, template, versions, onSubmit, onCancel }) => { - const styles = useStyles() - const formik = useFormik({ - initialValues: { - versionId: workspace.latest_build.template_version_id, - }, - onSubmit: ({ versionId }) => onSubmit(versionId), - }) - - return ( -
- - -
- -
- - {workspace.name} - - - {workspace.template_display_name.length > 0 - ? workspace.template_display_name - : workspace.template_name} - - -
- - { - const version = versions.find( - (version) => version.id === versionId, - ) - if (!version) { - throw new Error(`${versionId} not found.`) - } - return <>{version.name} - }, - }} - {...formik.getFieldProps("versionId")} - > - {versions - .slice() - .reverse() - .map((version) => ( - -
-
{version.name}
-
- Created by {version.created_by.username}{" "} - {createDayString(version.created_at)} -
-
- - {template.active_version_id === version.id && ( - - )} -
- ))} -
-
- - - - ) -} +import WorkspaceChangeVersionPageView from "./WorkspaceChangeVersionPageView" export const WorkspaceChangeVersionPage: FC = () => { const navigate = useNavigate() @@ -125,75 +22,23 @@ export const WorkspaceChangeVersionPage: FC = () => { }, }, }) - const { workspace, templateVersions, template } = state.context return ( - navigate(-1)}> - {workspace && template && templateVersions ? ( - { - send({ - type: "UPDATE_VERSION", - versionId, - }) - }} - onCancel={() => { - navigate(-1) - }} - /> - ) : ( - - )} - + <> + + Codestin Search App + + + { + send({ + type: "UPDATE_VERSION", + versionId, + }) + }} + /> + ) } - -const useStyles = makeStyles((theme) => ({ - workspace: { - padding: theme.spacing(2.5, 3), - borderRadius: theme.shape.borderRadius, - backgroundColor: theme.palette.background.paper, - border: `1px solid ${theme.palette.divider}`, - }, - - workspaceName: { - fontSize: 16, - }, - - workspaceDescription: { - fontSize: 14, - color: theme.palette.text.secondary, - }, - - workspaceIcon: { - width: theme.spacing(5), - lineHeight: 1, - - "& img": { - width: "100%", - }, - }, - - menuItem: { - paddingTop: theme.spacing(2), - paddingBottom: theme.spacing(2), - position: "relative", - }, - - versionDescription: { - fontSize: 12, - color: theme.palette.text.secondary, - }, - - activePill: { - position: "absolute", - top: theme.spacing(2), - right: theme.spacing(2), - }, -})) - -export default WorkspaceChangeVersionPage diff --git a/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPageView.stories.tsx b/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPageView.stories.tsx new file mode 100644 index 0000000000000..ed6cbb279fa93 --- /dev/null +++ b/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPageView.stories.tsx @@ -0,0 +1,51 @@ +import { action } from "@storybook/addon-actions" +import { ComponentMeta, Story } from "@storybook/react" +import { + makeMockApiError, + MockTemplate, + MockTemplateVersion, + MockTemplateVersion2, + MockUser, + MockWorkspace, +} from "testHelpers/entities" +import { + WorkspaceChangeVersionPageView, + WorkspaceChangeVersionPageViewProps, +} from "./WorkspaceChangeVersionPageView" + +export default { + title: "pages/WorkspaceChangeVersionPageView", + component: WorkspaceChangeVersionPageView, +} as ComponentMeta + +const Template: Story = (args) => ( + +) + +export const Example = Template.bind({}) +Example.args = { + isUpdating: false, + onSubmit: action("submit"), + context: { + error: undefined, + owner: MockUser.username, + workspaceName: MockWorkspace.name, + template: MockTemplate, + templateVersions: [MockTemplateVersion2, MockTemplateVersion], + workspace: MockWorkspace, + }, +} + +export const Error = Template.bind({}) +Error.args = { + isUpdating: false, + onSubmit: action("submit"), + context: { + error: makeMockApiError({ message: "Error on updating the version." }), + owner: MockUser.username, + workspaceName: MockWorkspace.name, + template: MockTemplate, + templateVersions: [MockTemplateVersion2, MockTemplateVersion], + workspace: MockWorkspace, + }, +} diff --git a/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPageView.tsx b/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPageView.tsx new file mode 100644 index 0000000000000..e2ffb2a5ef504 --- /dev/null +++ b/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPageView.tsx @@ -0,0 +1,48 @@ +import { AlertBanner } from "components/AlertBanner/AlertBanner" +import { Maybe } from "components/Conditionals/Maybe" +import { FullPageForm } from "components/FullPageForm/FullPageForm" +import { Loader } from "components/Loader/Loader" +import { Stack } from "components/Stack/Stack" +import { FC } from "react" +import { useNavigate } from "react-router-dom" +import { ChangeWorkspaceVersionContext } from "xServices/workspace/changeWorkspaceVersionXService" +import { WorkspaceChangeVersionForm } from "./WorkspaceChangeVersionForm" + +export interface WorkspaceChangeVersionPageViewProps { + isUpdating: boolean + context: ChangeWorkspaceVersionContext + onSubmit: (versionId: string) => void +} + +export const WorkspaceChangeVersionPageView: FC< + WorkspaceChangeVersionPageViewProps +> = ({ context, onSubmit, isUpdating }) => { + const navigate = useNavigate() + const { workspace, templateVersions, template, error } = context + + return ( + navigate(-1)}> + + + + + {workspace && template && templateVersions ? ( + { + navigate(-1) + }} + /> + ) : ( + + )} + + + ) +} + +export default WorkspaceChangeVersionPageView diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 482f6e1d47b29..327b274798578 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -181,6 +181,23 @@ You can add instructions here created_by: MockUser, } +export const MockTemplateVersion2: TypesGen.TemplateVersion = { + id: "test-template-version-2", + created_at: "2022-05-17T17:39:01.382927298Z", + updated_at: "2022-05-17T17:39:01.382927298Z", + template_id: "test-template", + job: MockProvisionerJob, + name: "test-version-2", + readme: `--- +name:Template test 2 +--- +## Instructions +You can add instructions here + +[Some link info](https://coder.com)`, + created_by: MockUser, +} + export const MockTemplate: TypesGen.Template = { id: "test-template", created_at: "2022-05-17T17:39:01.382927298Z", @@ -383,7 +400,7 @@ export const MockWorkspaceBuild: TypesGen.WorkspaceBuild = { initiator_id: MockUser.id, initiator_name: MockUser.username, job: MockProvisionerJob, - template_version_id: "", + template_version_id: MockTemplateVersion.id, transition: "start", updated_at: "2022-05-17T17:39:01.382927298Z", workspace_name: "test-workspace", @@ -406,7 +423,7 @@ export const MockFailedWorkspaceBuild = ( initiator_id: MockUser.id, initiator_name: MockUser.username, job: MockFailedProvisionerJob, - template_version_id: "", + template_version_id: MockTemplateVersion.id, transition: transition, updated_at: "2022-05-17T17:39:01.382927298Z", workspace_name: "test-workspace", diff --git a/site/src/xServices/workspace/changeWorkspaceVersionXService.tsx b/site/src/xServices/workspace/changeWorkspaceVersionXService.tsx index c9291426e6644..4031287453c5b 100644 --- a/site/src/xServices/workspace/changeWorkspaceVersionXService.tsx +++ b/site/src/xServices/workspace/changeWorkspaceVersionXService.tsx @@ -12,15 +12,17 @@ import { } from "api/typesGenerated" import { assign, createMachine } from "xstate" +export interface ChangeWorkspaceVersionContext { + owner: string + workspaceName: string + workspace?: Workspace + template?: Template + templateVersions?: TemplateVersion[] + error?: unknown +} + interface ChangeWorkspaceVersionSchema { - context: { - owner: string - workspaceName: string - workspace?: Workspace - template?: Template - templateVersions?: TemplateVersion[] - error?: unknown - } + context: ChangeWorkspaceVersionContext services: { getWorkspace: { From b3ddc7a796243649057bd2c47c859b5d55369bf0 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 23 Nov 2022 17:43:58 +0000 Subject: [PATCH 4/8] Fix autocomplete change --- .../WorkspaceChangeVersionForm.tsx | 14 ++++++++++---- .../WorkspaceChangeVersionPage.tsx | 6 ++++-- .../WorkspaceChangeVersionPageView.tsx | 2 -- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionForm.tsx b/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionForm.tsx index 24c863c2a827d..2ff3eb97b8b61 100644 --- a/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionForm.tsx +++ b/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionForm.tsx @@ -30,6 +30,9 @@ export const WorkspaceChangeVersionForm: FC<{ validationSchema, onSubmit: ({ versionId }) => onSubmit(versionId), }) + const autocompleteValue = versions.find( + (version) => version.id === formik.values.versionId, + ) return (
@@ -57,16 +60,19 @@ export const WorkspaceChangeVersionForm: FC<{ version.id === formik.values.versionId, - )} + value={autocompleteValue} + onChange={async (_event, value) => { + if (value) { + await formik.setFieldValue("versionId", value.id) + } + }} renderInput={(params) => ( )} getOptionLabel={(version: TemplateVersion) => version.name} diff --git a/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPage.tsx b/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPage.tsx index 9418c91536c18..04502763a47b0 100644 --- a/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPage.tsx +++ b/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPage.tsx @@ -3,7 +3,7 @@ import { FC } from "react" import { Helmet } from "react-helmet-async" import { useNavigate, useParams } from "react-router-dom" import { changeWorkspaceVersionMachine } from "xServices/workspace/changeWorkspaceVersionXService" -import WorkspaceChangeVersionPageView from "./WorkspaceChangeVersionPageView" +import { WorkspaceChangeVersionPageView } from "./WorkspaceChangeVersionPageView" export const WorkspaceChangeVersionPage: FC = () => { const navigate = useNavigate() @@ -26,7 +26,7 @@ export const WorkspaceChangeVersionPage: FC = () => { return ( <> - Codestin Search App + Codestin Search App { ) } + +export default WorkspaceChangeVersionPage diff --git a/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPageView.tsx b/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPageView.tsx index e2ffb2a5ef504..0d0511d67be5a 100644 --- a/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPageView.tsx +++ b/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPageView.tsx @@ -44,5 +44,3 @@ export const WorkspaceChangeVersionPageView: FC< ) } - -export default WorkspaceChangeVersionPageView From 759649e002f14d291ada065e3097a251571e2982 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 23 Nov 2022 18:32:10 +0000 Subject: [PATCH 5/8] Add tests --- .../WorkspaceChangeVersionForm.tsx | 2 +- .../WorkspaceChangeVersionPage.test.tsx | 55 +++++++++++++++++++ site/src/testHelpers/handlers.ts | 5 +- 3 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPage.test.tsx diff --git a/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionForm.tsx b/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionForm.tsx index 2ff3eb97b8b61..1dc611b44bb83 100644 --- a/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionForm.tsx +++ b/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionForm.tsx @@ -58,6 +58,7 @@ export const WorkspaceChangeVersionForm: FC<{ ( { + renderWithAuth(, { + path: "/@:username/:workspace/change-version", + route: `/@${MockUser.username}/${MockWorkspace.name}/change-version`, + }) + await waitForLoaderToBeRemoved() +} + +describe("WorkspaceChangeVersionPage", () => { + beforeEach(() => { + jest + .spyOn(CreateDayString, "createDayString") + .mockImplementation(() => "a minute ago") + }) + + it("sends the update request with the right version", async () => { + const user = userEvent.setup() + const updateSpy = jest.spyOn(API, "startWorkspace") + await renderPage() + + // Type the version name and select it + const autocompleteInput = screen.getByLabelText("Workspace version") + await user.clear(autocompleteInput) + await user.type(autocompleteInput, MockTemplateVersion2.name) + const newOption = screen.getByRole("option", { + // Using RegExp so we can match a substring + name: new RegExp(MockTemplateVersion2.name), + }) + await user.click(newOption) + + // Submit the form + const submitButton = screen.getByRole("button", { name: "Update version" }) + await user.click(submitButton) + + await waitFor(() => { + expect(updateSpy).toBeCalledWith( + MockWorkspace.id, + MockTemplateVersion2.id, + ) + }) + }) +}) diff --git a/site/src/testHelpers/handlers.ts b/site/src/testHelpers/handlers.ts index 30e45fa727a2b..32382402d64f7 100644 --- a/site/src/testHelpers/handlers.ts +++ b/site/src/testHelpers/handlers.ts @@ -37,7 +37,10 @@ export const handlers = [ return res(ctx.status(200), ctx.json(M.MockTemplate)) }), rest.get("/api/v2/templates/:templateId/versions", async (req, res, ctx) => { - return res(ctx.status(200), ctx.json([M.MockTemplateVersion])) + return res( + ctx.status(200), + ctx.json([M.MockTemplateVersion2, M.MockTemplateVersion]), + ) }), rest.patch("/api/v2/templates/:templateId", async (req, res, ctx) => { return res(ctx.status(200), ctx.json(M.MockTemplate)) From b7eb83a1a2749742572611b413c9b4ae1cd7f25d Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 23 Nov 2022 18:42:30 +0000 Subject: [PATCH 6/8] Add translation --- site/src/i18n/en/index.ts | 2 ++ site/src/i18n/en/workspaceChangeVersionPage.json | 9 +++++++++ .../WorkspaceChangeVersionForm.tsx | 10 ++++++---- .../WorkspaceChangeVersionPage.test.tsx | 13 +++++++++++-- .../WorkspaceChangeVersionPage.tsx | 4 +++- 5 files changed, 31 insertions(+), 7 deletions(-) create mode 100644 site/src/i18n/en/workspaceChangeVersionPage.json diff --git a/site/src/i18n/en/index.ts b/site/src/i18n/en/index.ts index e37f8c6337760..a8146f03aa316 100644 --- a/site/src/i18n/en/index.ts +++ b/site/src/i18n/en/index.ts @@ -10,6 +10,7 @@ import workspacesPage from "./workspacesPage.json" import usersPage from "./usersPage.json" import templateVersionPage from "./templateVersionPage.json" import loginPage from "./loginPage.json" +import workspaceChangeVersionPage from "./workspaceChangeVersionPage.json" export const en = { common, @@ -24,4 +25,5 @@ export const en = { usersPage, templateVersionPage, loginPage, + workspaceChangeVersionPage, } diff --git a/site/src/i18n/en/workspaceChangeVersionPage.json b/site/src/i18n/en/workspaceChangeVersionPage.json new file mode 100644 index 0000000000000..08e58705655ec --- /dev/null +++ b/site/src/i18n/en/workspaceChangeVersionPage.json @@ -0,0 +1,9 @@ +{ + "title": "", + "labels": { + "workspaceVersion": "Workspace version", + "submit": "Update version", + "createdBy": "Created by", + "active": "Active" + } +} diff --git a/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionForm.tsx b/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionForm.tsx index 1dc611b44bb83..2fd199b13a0bb 100644 --- a/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionForm.tsx +++ b/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionForm.tsx @@ -7,6 +7,7 @@ import { Pill } from "components/Pill/Pill" import { Stack } from "components/Stack/Stack" import { useFormik } from "formik" import { FC } from "react" +import { useTranslation } from "react-i18next" import { createDayString } from "util/createDayString" import * as Yup from "yup" @@ -23,6 +24,7 @@ export const WorkspaceChangeVersionForm: FC<{ onCancel: () => void }> = ({ isLoading, workspace, template, versions, onSubmit, onCancel }) => { const styles = useStyles() + const { t } = useTranslation("workspaceChangeVersionPage") const formik = useFormik({ initialValues: { versionId: workspace.latest_build.template_version_id, @@ -70,7 +72,7 @@ export const WorkspaceChangeVersionForm: FC<{ renderInput={(params) => ( @@ -81,7 +83,7 @@ export const WorkspaceChangeVersionForm: FC<{
{version.name}
- Created by {version.created_by.username}{" "} + {t("labels.createdBy")} {version.created_by.username}{" "} {createDayString(version.created_at)}
@@ -89,7 +91,7 @@ export const WorkspaceChangeVersionForm: FC<{ {template.active_version_id === version.id && ( )} @@ -101,7 +103,7 @@ export const WorkspaceChangeVersionForm: FC<{ ) diff --git a/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPage.test.tsx b/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPage.test.tsx index 6b58a797a6f53..9d398dce6d3bd 100644 --- a/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPage.test.tsx +++ b/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPage.test.tsx @@ -10,6 +10,11 @@ import { screen, waitFor } from "@testing-library/react" import * as API from "api/api" import userEvent from "@testing-library/user-event" import * as CreateDayString from "util/createDayString" +import i18next from "i18next" + +const t = (path: string) => { + return i18next.t(path, { ns: "workspaceChangeVersionPage" }) +} const renderPage = async () => { renderWithAuth(, { @@ -32,7 +37,9 @@ describe("WorkspaceChangeVersionPage", () => { await renderPage() // Type the version name and select it - const autocompleteInput = screen.getByLabelText("Workspace version") + const autocompleteInput = screen.getByLabelText( + t("labels.workspaceVersion"), + ) await user.clear(autocompleteInput) await user.type(autocompleteInput, MockTemplateVersion2.name) const newOption = screen.getByRole("option", { @@ -42,7 +49,9 @@ describe("WorkspaceChangeVersionPage", () => { await user.click(newOption) // Submit the form - const submitButton = screen.getByRole("button", { name: "Update version" }) + const submitButton = screen.getByRole("button", { + name: t("labels.submit"), + }) await user.click(submitButton) await waitFor(() => { diff --git a/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPage.tsx b/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPage.tsx index 04502763a47b0..14a1ebfdf4e07 100644 --- a/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPage.tsx +++ b/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPage.tsx @@ -1,12 +1,14 @@ import { useMachine } from "@xstate/react" import { FC } from "react" import { Helmet } from "react-helmet-async" +import { useTranslation } from "react-i18next" import { useNavigate, useParams } from "react-router-dom" import { changeWorkspaceVersionMachine } from "xServices/workspace/changeWorkspaceVersionXService" import { WorkspaceChangeVersionPageView } from "./WorkspaceChangeVersionPageView" export const WorkspaceChangeVersionPage: FC = () => { const navigate = useNavigate() + const { t } = useTranslation("workspaceChangeVersionPage") const { username: owner, workspace: workspaceName } = useParams() as { username: string workspace: string @@ -26,7 +28,7 @@ export const WorkspaceChangeVersionPage: FC = () => { return ( <> - Codestin Search App + Codestin Search App Date: Wed, 23 Nov 2022 19:53:19 +0000 Subject: [PATCH 7/8] Fix file type --- ...spaceVersionXService.tsx => changeWorkspaceVersionXService.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename site/src/xServices/workspace/{changeWorkspaceVersionXService.tsx => changeWorkspaceVersionXService.ts} (100%) diff --git a/site/src/xServices/workspace/changeWorkspaceVersionXService.tsx b/site/src/xServices/workspace/changeWorkspaceVersionXService.ts similarity index 100% rename from site/src/xServices/workspace/changeWorkspaceVersionXService.tsx rename to site/src/xServices/workspace/changeWorkspaceVersionXService.ts From 0ac0e6a846546392f6875d40d986fdeb9c775f45 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 23 Nov 2022 19:54:39 +0000 Subject: [PATCH 8/8] Use theme for font sizes --- .../WorkspaceChangeVersionForm.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionForm.tsx b/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionForm.tsx index 2fd199b13a0bb..bcca66b5a9364 100644 --- a/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionForm.tsx +++ b/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionForm.tsx @@ -118,11 +118,11 @@ const useStyles = makeStyles((theme) => ({ }, workspaceName: { - fontSize: 16, + fontSize: theme.spacing(2), }, workspaceDescription: { - fontSize: 14, + fontSize: theme.spacing(1.75), color: theme.palette.text.secondary, }, @@ -143,7 +143,7 @@ const useStyles = makeStyles((theme) => ({ }, versionDescription: { - fontSize: 12, + fontSize: theme.spacing(1.5), color: theme.palette.text.secondary, },