feat(site): Add workspace settings page (#6612)

This commit is contained in:
Bruno Quaresma
2023-03-16 11:00:19 -03:00
committed by GitHub
parent 854bcce5e0
commit 107ae55642
27 changed files with 458 additions and 1068 deletions

View File

@ -7,7 +7,6 @@ import GroupsPage from "pages/GroupsPage/GroupsPage"
import LoginPage from "pages/LoginPage/LoginPage"
import { SetupPage } from "pages/SetupPage/SetupPage"
import { TemplateSettingsPage } from "pages/TemplateSettingsPage/TemplateSettingsPage"
import { WorkspaceBuildParametersPage } from "pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage"
import TemplatesPage from "pages/TemplatesPage/TemplatesPage"
import UsersPage from "pages/UsersPage/UsersPage"
import WorkspacesPage from "pages/WorkspacesPage/WorkspacesPage"
@ -126,6 +125,9 @@ const CreateTemplatePage = lazy(
const TemplateVariablesPage = lazy(
() => import("./pages/TemplateVariablesPage/TemplateVariablesPage"),
)
const WorkspaceSettingsPage = lazy(
() => import("./pages/WorkspaceSettingsPage/WorkspaceSettingsPage"),
)
export const AppRouter: FC = () => {
return (
@ -230,10 +232,7 @@ export const AppRouter: FC = () => {
path="change-version"
element={<WorkspaceChangeVersionPage />}
/>
<Route
path="build-parameters"
element={<WorkspaceBuildParametersPage />}
/>
<Route path="settings" element={<WorkspaceSettingsPage />} />
</Route>
</Route>
</Route>

View File

@ -506,6 +506,13 @@ export const createWorkspace = async (
return response.data
}
export const patchWorkspace = async (
workspaceId: string,
data: TypesGen.UpdateWorkspaceRequest,
) => {
await axios.patch(`/api/v2/workspaces/${workspaceId}`, data)
}
export const getBuildInfo = async (): Promise<TypesGen.BuildInfoResponse> => {
const response = await axios.get("/api/v2/buildinfo")
return response.data

View File

@ -54,9 +54,9 @@ export const ChangeVersionButton: FC<
)
}
export const BuildParametersButton: FC<
React.PropsWithChildren<WorkspaceAction>
> = ({ handleAction }) => {
export const SettingsButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({
handleAction,
}) => {
const styles = useStyles()
const { t } = useTranslation("workspacePage")
@ -67,7 +67,7 @@ export const BuildParametersButton: FC<
startIcon={<SettingsOutlined />}
onClick={handleAction}
>
{t("actionButton.buildParameters")}
{t("actionButton.settings")}
</Button>
)
}

View File

@ -24,7 +24,7 @@ type FormProps = HTMLProps<HTMLFormElement> & {
}
export const Form: FC<FormProps> = ({ direction, className, ...formProps }) => {
const styles = useStyles()
const styles = useStyles({ direction })
return (
<FormContext.Provider value={{ direction }}>
@ -136,6 +136,7 @@ const useStyles = makeStyles((theme) => ({
},
formSectionInfo: {
width: "100%",
maxWidth: ({ direction }: FormContextValue = {}) =>
direction === "horizontal" ? 312 : undefined,
flexShrink: 0,

View File

@ -43,7 +43,7 @@ export interface WorkspaceProps {
handleUpdate: () => void
handleCancel: () => void
handleChangeVersion: () => void
handleBuildParameters: () => void
handleSettings: () => void
isUpdating: boolean
workspace: TypesGen.Workspace
resources?: TypesGen.WorkspaceResource[]
@ -55,7 +55,6 @@ export interface WorkspaceProps {
buildInfo?: TypesGen.BuildInfoResponse
applicationsHost?: string
template?: TypesGen.Template
templateParameters?: TypesGen.TemplateVersionParameter[]
quota_budget?: number
}
@ -70,7 +69,7 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
handleUpdate,
handleCancel,
handleChangeVersion,
handleBuildParameters,
handleSettings,
workspace,
isUpdating,
resources,
@ -82,7 +81,6 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
buildInfo,
applicationsHost,
template,
templateParameters,
quota_budget,
}) => {
const styles = useStyles()
@ -126,9 +124,6 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
/>
<WorkspaceActions
workspaceStatus={workspace.latest_build.status}
hasTemplateParameters={
templateParameters ? templateParameters.length > 0 : false
}
isOutdated={workspace.outdated}
handleStart={handleStart}
handleStop={handleStop}
@ -136,7 +131,7 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
handleUpdate={handleUpdate}
handleCancel={handleCancel}
handleChangeVersion={handleChangeVersion}
handleBuildParameters={handleBuildParameters}
handleSettings={handleSettings}
isUpdating={isUpdating}
/>
</Stack>

View File

@ -12,7 +12,6 @@ const renderComponent = async (props: Partial<WorkspaceActionsProps> = {}) => {
workspaceStatus={
props.workspaceStatus ?? Mocks.MockWorkspace.latest_build.status
}
hasTemplateParameters={props.hasTemplateParameters ?? false}
isOutdated={props.isOutdated ?? false}
handleStart={jest.fn()}
handleStop={jest.fn()}
@ -20,7 +19,7 @@ const renderComponent = async (props: Partial<WorkspaceActionsProps> = {}) => {
handleUpdate={jest.fn()}
handleCancel={jest.fn()}
handleChangeVersion={jest.fn()}
handleBuildParameters={jest.fn()}
handleSettings={jest.fn()}
isUpdating={false}
/>,
)
@ -32,7 +31,6 @@ const renderAndClick = async (props: Partial<WorkspaceActionsProps> = {}) => {
workspaceStatus={
props.workspaceStatus ?? Mocks.MockWorkspace.latest_build.status
}
hasTemplateParameters={props.hasTemplateParameters ?? false}
isOutdated={props.isOutdated ?? false}
handleStart={jest.fn()}
handleStop={jest.fn()}
@ -40,7 +38,7 @@ const renderAndClick = async (props: Partial<WorkspaceActionsProps> = {}) => {
handleUpdate={jest.fn()}
handleCancel={jest.fn()}
handleChangeVersion={jest.fn()}
handleBuildParameters={jest.fn()}
handleSettings={jest.fn()}
isUpdating={false}
/>,
)
@ -91,20 +89,6 @@ describe("WorkspaceActions", () => {
)
})
})
describe("when the workspace with rich parameters is started", () => {
it("primary is stop; secondary is build parameters", async () => {
await renderAndClick({
workspaceStatus: Mocks.MockWorkspace.latest_build.status,
hasTemplateParameters: true,
})
expect(screen.getByTestId("primary-cta")).toHaveTextContent(
t("actionButton.stop", { ns: "workspacePage" }),
)
expect(screen.getByTestId("secondary-ctas")).toHaveTextContent(
t("actionButton.buildParameters", { ns: "workspacePage" }),
)
})
})
describe("when the workspace is stopping", () => {
it("primary is stopping; cancel is available; no secondary", async () => {
await renderComponent({

View File

@ -7,7 +7,7 @@ import {
ChangeVersionButton,
DeleteButton,
DisabledButton,
BuildParametersButton,
SettingsButton,
StartButton,
StopButton,
UpdateButton,
@ -16,7 +16,6 @@ import { ButtonMapping, ButtonTypesEnum, buttonAbilities } from "./constants"
export interface WorkspaceActionsProps {
workspaceStatus: WorkspaceStatus
hasTemplateParameters: boolean
isOutdated: boolean
handleStart: () => void
handleStop: () => void
@ -24,14 +23,13 @@ export interface WorkspaceActionsProps {
handleUpdate: () => void
handleCancel: () => void
handleChangeVersion: () => void
handleBuildParameters: () => void
handleSettings: () => void
isUpdating: boolean
children?: ReactNode
}
export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
workspaceStatus,
hasTemplateParameters,
isOutdated,
handleStart,
handleStop,
@ -39,14 +37,11 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
handleUpdate,
handleCancel,
handleChangeVersion,
handleBuildParameters,
handleSettings,
isUpdating,
}) => {
const { t } = useTranslation("workspacePage")
const { canCancel, canAcceptJobs, actions } = buttonAbilities(
workspaceStatus,
hasTemplateParameters,
)
const { canCancel, canAcceptJobs, actions } = buttonAbilities(workspaceStatus)
const canBeUpdated = isOutdated && canAcceptJobs
// A mapping of button type to the corresponding React component
@ -58,8 +53,8 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
[ButtonTypesEnum.changeVersion]: (
<ChangeVersionButton handleAction={handleChangeVersion} />
),
[ButtonTypesEnum.buildParameters]: (
<BuildParametersButton handleAction={handleBuildParameters} />
[ButtonTypesEnum.settings]: (
<SettingsButton handleAction={handleSettings} />
),
[ButtonTypesEnum.start]: <StartButton handleAction={handleStart} />,
[ButtonTypesEnum.starting]: (

View File

@ -12,7 +12,7 @@ export enum ButtonTypesEnum {
update = "update",
updating = "updating",
changeVersion = "changeVersion",
buildParameters = "buildParameters",
settings = "settings",
// disabled buttons
canceling = "canceling",
deleted = "deleted",
@ -31,19 +31,8 @@ interface WorkspaceAbilities {
export const buttonAbilities = (
status: WorkspaceStatus,
hasTemplateParameters: boolean,
): WorkspaceAbilities => {
if (hasTemplateParameters) {
return statusToAbilities[status]
}
const all = statusToAbilities[status]
return {
...all,
actions: all.actions.filter(
(action) => action !== ButtonTypesEnum.buildParameters,
),
}
return statusToAbilities[status]
}
const statusToAbilities: Record<WorkspaceStatus, WorkspaceAbilities> = {
@ -55,7 +44,7 @@ const statusToAbilities: Record<WorkspaceStatus, WorkspaceAbilities> = {
running: {
actions: [
ButtonTypesEnum.stop,
ButtonTypesEnum.buildParameters,
ButtonTypesEnum.settings,
ButtonTypesEnum.changeVersion,
ButtonTypesEnum.delete,
],
@ -70,7 +59,7 @@ const statusToAbilities: Record<WorkspaceStatus, WorkspaceAbilities> = {
stopped: {
actions: [
ButtonTypesEnum.start,
ButtonTypesEnum.buildParameters,
ButtonTypesEnum.settings,
ButtonTypesEnum.changeVersion,
ButtonTypesEnum.delete,
],
@ -81,7 +70,7 @@ const statusToAbilities: Record<WorkspaceStatus, WorkspaceAbilities> = {
actions: [
ButtonTypesEnum.start,
ButtonTypesEnum.stop,
ButtonTypesEnum.buildParameters,
ButtonTypesEnum.settings,
ButtonTypesEnum.changeVersion,
ButtonTypesEnum.delete,
],
@ -92,7 +81,7 @@ const statusToAbilities: Record<WorkspaceStatus, WorkspaceAbilities> = {
failed: {
actions: [
ButtonTypesEnum.start,
ButtonTypesEnum.buildParameters,
ButtonTypesEnum.settings,
ButtonTypesEnum.changeVersion,
ButtonTypesEnum.delete,
],

View File

@ -12,7 +12,6 @@ import templateSettingsPage from "./templateSettingsPage.json"
import templateVariablesPage from "./templateVariablesPage.json"
import templateVersionPage from "./templateVersionPage.json"
import loginPage from "./loginPage.json"
import workspaceBuildParametersPage from "./workspaceBuildParametersPage.json"
import workspaceChangeVersionPage from "./workspaceChangeVersionPage.json"
import workspaceSchedulePage from "./workspaceSchedulePage.json"
import appearanceSettings from "./appearanceSettings.json"
@ -21,6 +20,7 @@ import starterTemplatePage from "./starterTemplatePage.json"
import createTemplatePage from "./createTemplatePage.json"
import userSettingsPage from "./userSettingsPage.json"
import tokensPage from "./tokensPage.json"
import workspaceSettingsPage from "./workspaceSettingsPage.json"
export const en = {
common,
@ -37,7 +37,6 @@ export const en = {
templateVariablesPage,
templateVersionPage,
loginPage,
workspaceBuildParametersPage,
workspaceChangeVersionPage,
workspaceSchedulePage,
appearanceSettings,
@ -46,4 +45,5 @@ export const en = {
createTemplatePage,
userSettingsPage,
tokensPage,
workspaceSettingsPage,
}

View File

@ -1,10 +0,0 @@
{
"title": "Workspace build parameters",
"detail": "Those values were provided by the workspace owner.",
"noParametersDefined": "This template does not use any rich parameters.",
"validationNumberNotInRange": "Value must be between {{min}} and {{max}}.",
"validationPatternNotMatched": "{{error}} (value does not match the pattern {{pattern}}).",
"updateWorkspace": "Update workspace",
"validationNumberNotIncreasing": "The value must be equal or greater than the previous one {{last}}.",
"validationNumberNotDecreasing": "The value must be equal or lower than the previous one {{last}}."
}

View File

@ -29,7 +29,7 @@
"stopping": "Stopping...",
"deleting": "Deleting...",
"changeVersion": "Change version",
"buildParameters": "Build parameters"
"settings": "Settings"
},
"disabledButton": {
"canceling": "Canceling",

View File

@ -0,0 +1,9 @@
{
"title": "Workspace Settings",
"defaultErrorMessage": "Error on update workspace settings",
"nameLabel": "Name",
"generalInfo": "General info",
"generalInfoDescription": "The name of your new workspace.",
"parameters": "Parameters",
"parametersDescription": "Those values are provided by your template's Terraform configuration. Values can be changed after creating the workspace."
}

View File

@ -24,6 +24,7 @@ import { makeStyles } from "@material-ui/core/styles"
import {
selectInitialRichParametersValues,
useValidationSchemaForRichParameters,
workspaceBuildParameterValue,
} from "util/richParameters"
export enum CreateWorkspaceErrors {
@ -401,13 +402,3 @@ const useStyles = makeStyles((theme) => ({
marginRight: -theme.spacing(10),
},
}))
export const workspaceBuildParameterValue = (
workspaceBuildParameters: TypesGen.WorkspaceBuildParameter[],
parameter: TypesGen.TemplateVersionParameter,
): string => {
const buildParameter = workspaceBuildParameters.find((buildParameter) => {
return buildParameter.name === parameter.name
})
return (buildParameter && buildParameter.value) || ""
}

View File

@ -1,210 +0,0 @@
import { fireEvent, screen } from "@testing-library/react"
import {
MockTemplateVersionParameter1,
MockTemplateVersionParameter2,
MockTemplateVersionParameter5,
MockWorkspace,
MockWorkspaceBuildParameter1,
MockWorkspaceBuildParameter2,
MockWorkspaceBuildParameter5,
renderWithAuth,
} from "testHelpers/renderHelpers"
import * as API from "api/api"
import i18next from "i18next"
import { WorkspaceBuildParametersPage } from "./WorkspaceBuildParametersPage"
const { t } = i18next
const pageTitleText = t("title", { ns: "workspaceBuildParametersPage" })
const validationNumberNotInRangeText = t("validationNumberNotInRange", {
ns: "workspaceBuildParametersPage",
min: "1",
max: "3",
})
const validationNumberNotIncreasing = t("validationNumberNotIncreasing", {
ns: "workspaceBuildParametersPage",
last: "3",
})
const validationNumberNotDecreasing = t("validationNumberNotDecreasing", {
ns: "workspaceBuildParametersPage",
last: "5",
})
const renderWorkspaceBuildParametersPage = () => {
return renderWithAuth(<WorkspaceBuildParametersPage />, {
route: `/@${MockWorkspace.owner_name}/${MockWorkspace.name}/build-parameters`,
path: `/@:ownerName/:workspaceName/build-parameters`,
})
}
describe("WorkspaceBuildParametersPage", () => {
it("renders without rich parameters", async () => {
jest.spyOn(API, "getWorkspace").mockResolvedValueOnce(MockWorkspace)
jest
.spyOn(API, "getTemplateVersionRichParameters")
.mockResolvedValueOnce([])
jest
.spyOn(API, "getWorkspaceBuildParameters")
.mockResolvedValueOnce([
MockWorkspaceBuildParameter1,
MockWorkspaceBuildParameter2,
])
renderWorkspaceBuildParametersPage()
const element = await screen.findByText(pageTitleText)
expect(element).toBeDefined()
const goBackButton = await screen.findByText("Go back")
expect(goBackButton).toBeDefined()
})
it("renders with rich parameter", async () => {
jest.spyOn(API, "getWorkspace").mockResolvedValueOnce(MockWorkspace)
jest
.spyOn(API, "getTemplateVersionRichParameters")
.mockResolvedValueOnce([
MockTemplateVersionParameter1,
MockTemplateVersionParameter2,
])
jest
.spyOn(API, "getWorkspaceBuildParameters")
.mockResolvedValueOnce([
MockWorkspaceBuildParameter1,
MockWorkspaceBuildParameter2,
])
renderWorkspaceBuildParametersPage()
const element = await screen.findByText(pageTitleText)
expect(element).toBeDefined()
const firstParameter = await screen.findByLabelText(
MockTemplateVersionParameter1.name,
{ exact: false },
)
expect(firstParameter).toBeDefined()
const secondParameter = await screen.findByLabelText(
MockTemplateVersionParameter2.name,
{ exact: false },
)
expect(secondParameter).toBeDefined()
})
it("rich parameter: number validation fails", async () => {
jest
.spyOn(API, "getTemplateVersionRichParameters")
.mockResolvedValueOnce([
MockTemplateVersionParameter1,
MockTemplateVersionParameter2,
])
jest
.spyOn(API, "getWorkspaceBuildParameters")
.mockResolvedValueOnce([
MockWorkspaceBuildParameter1,
MockWorkspaceBuildParameter2,
])
renderWorkspaceBuildParametersPage()
const element = await screen.findByText(pageTitleText)
expect(element).toBeDefined()
const secondParameter = await screen.findByText(
MockTemplateVersionParameter2.description,
)
expect(secondParameter).toBeDefined()
const secondParameterField = await screen.findByLabelText(
MockTemplateVersionParameter2.name,
{ exact: false },
)
expect(secondParameterField).toBeDefined()
fireEvent.change(secondParameterField, {
target: { value: "4" },
})
fireEvent.submit(secondParameter)
const validationError = await screen.findByText(
validationNumberNotInRangeText,
)
expect(validationError).toBeDefined()
})
it("rich parameter: number is not monotonically increasing", async () => {
jest
.spyOn(API, "getTemplateVersionRichParameters")
.mockResolvedValueOnce([
MockTemplateVersionParameter1,
MockTemplateVersionParameter2,
])
jest
.spyOn(API, "getWorkspaceBuildParameters")
.mockResolvedValueOnce([
MockWorkspaceBuildParameter1,
MockWorkspaceBuildParameter2,
])
renderWorkspaceBuildParametersPage()
const element = await screen.findByText(pageTitleText)
expect(element).toBeDefined()
const secondParameter = await screen.findByText(
MockTemplateVersionParameter2.description,
)
expect(secondParameter).toBeDefined()
const secondParameterField = await screen.findByLabelText(
MockTemplateVersionParameter2.name,
{ exact: false },
)
expect(secondParameterField).toBeDefined()
fireEvent.change(secondParameterField, {
target: { value: "1" },
})
fireEvent.submit(secondParameter)
const validationError = await screen.findByText(
validationNumberNotIncreasing,
)
expect(validationError).toBeDefined()
})
it("rich parameter: number is not monotonically decreasing", async () => {
jest
.spyOn(API, "getTemplateVersionRichParameters")
.mockResolvedValueOnce([
MockTemplateVersionParameter1,
MockTemplateVersionParameter5,
])
jest
.spyOn(API, "getWorkspaceBuildParameters")
.mockResolvedValueOnce([
MockWorkspaceBuildParameter1,
MockWorkspaceBuildParameter5,
])
renderWorkspaceBuildParametersPage()
const element = await screen.findByText(pageTitleText)
expect(element).toBeDefined()
const secondParameter = await screen.findByText(
MockTemplateVersionParameter5.description,
)
expect(secondParameter).toBeDefined()
const secondParameterField = await screen.findByLabelText(
MockTemplateVersionParameter5.name,
{ exact: false },
)
expect(secondParameterField).toBeDefined()
fireEvent.change(secondParameterField, {
target: { value: "6" },
})
fireEvent.submit(secondParameter)
const validationError = await screen.findByText(
validationNumberNotDecreasing,
)
expect(validationError).toBeDefined()
})
})

View File

@ -1,82 +0,0 @@
import { FC } from "react"
import { Helmet } from "react-helmet-async"
import { useTranslation } from "react-i18next"
import { pageTitle } from "util/page"
import { useMachine } from "@xstate/react"
import { useNavigate, useParams } from "react-router-dom"
import { workspaceBuildParametersMachine } from "xServices/workspace/workspaceBuildParametersXService"
import {
UpdateWorkspaceErrors,
WorkspaceBuildParametersPageView,
} from "./WorkspaceBuildParametersPageView"
import { orderedTemplateParameters } from "pages/CreateWorkspacePage/CreateWorkspacePage"
export const WorkspaceBuildParametersPage: FC = () => {
const { t } = useTranslation("workspaceBuildParametersPage")
const navigate = useNavigate()
const { owner: workspaceOwner, workspace: workspaceName } = useParams() as {
owner: string
workspace: string
}
const [state, send] = useMachine(workspaceBuildParametersMachine, {
context: {
workspaceOwner,
workspaceName,
},
actions: {
onUpdateWorkspace: (_, event) => {
navigate(
`/@${event.data.workspace_owner_name}/${event.data.workspace_name}`,
)
},
},
})
const {
selectedWorkspace,
templateParameters,
workspaceBuildParameters,
getWorkspaceError,
getTemplateParametersError,
getWorkspaceBuildParametersError,
updateWorkspaceError,
} = state.context
return (
<>
<Helmet>
<title>{pageTitle(t("title"))}</title>
</Helmet>
<WorkspaceBuildParametersPageView
workspace={selectedWorkspace}
templateParameters={orderedTemplateParameters(templateParameters)}
workspaceBuildParameters={workspaceBuildParameters}
isLoading={
state.matches("gettingWorkspace") ||
state.matches("gettingTemplateParameters") ||
state.matches("gettingWorkspaceBuildParameters")
}
updatingWorkspace={state.matches("updatingWorkspace")}
hasErrors={state.matches("error")}
updateWorkspaceErrors={{
[UpdateWorkspaceErrors.GET_WORKSPACE_ERROR]: getWorkspaceError,
[UpdateWorkspaceErrors.GET_TEMPLATE_PARAMETERS_ERROR]:
getTemplateParametersError,
[UpdateWorkspaceErrors.GET_WORKSPACE_BUILD_PARAMETERS_ERROR]:
getWorkspaceBuildParametersError,
[UpdateWorkspaceErrors.UPDATE_WORKSPACE_ERROR]: updateWorkspaceError,
}}
onCancel={() => {
// Go back
navigate(-1)
}}
onSubmit={(request) => {
send({
type: "UPDATE_WORKSPACE",
request,
})
}}
/>
</>
)
}

View File

@ -1,48 +0,0 @@
import { ComponentMeta, Story } from "@storybook/react"
import {
MockTemplateVersionParameter1,
MockTemplateVersionParameter2,
MockTemplateVersionParameter3,
MockTemplateVersionParameter4,
MockWorkspace,
} from "testHelpers/entities"
import {
WorkspaceBuildParametersPageView,
WorkspaceBuildParametersPageViewProps,
} from "./WorkspaceBuildParametersPageView"
export default {
title: "pages/WorkspaceBuildParametersPageView",
component: WorkspaceBuildParametersPageView,
} as ComponentMeta<typeof WorkspaceBuildParametersPageView>
const Template: Story<WorkspaceBuildParametersPageViewProps> = (args) => (
<WorkspaceBuildParametersPageView {...args} />
)
export const NoRichParametersDefined = Template.bind({})
NoRichParametersDefined.args = {
workspace: MockWorkspace,
templateParameters: [],
workspaceBuildParameters: [],
updateWorkspaceErrors: {},
initialTouched: {
name: true,
},
}
export const RichParametersDefined = Template.bind({})
RichParametersDefined.args = {
workspace: MockWorkspace,
templateParameters: [
MockTemplateVersionParameter1,
MockTemplateVersionParameter2,
MockTemplateVersionParameter3,
MockTemplateVersionParameter4,
],
workspaceBuildParameters: [],
updateWorkspaceErrors: {},
initialTouched: {
name: true,
},
}

View File

@ -1,367 +0,0 @@
import { FC } from "react"
import { FullPageForm } from "components/FullPageForm/FullPageForm"
import { useTranslation } from "react-i18next"
import * as TypesGen from "api/typesGenerated"
import { AlertBanner } from "components/AlertBanner/AlertBanner"
import { Stack } from "components/Stack/Stack"
import { makeStyles } from "@material-ui/core/styles"
import { getFormHelpers } from "util/formUtils"
import { FormikContextType, FormikTouched, useFormik } from "formik"
import { RichParameterInput } from "components/RichParameterInput/RichParameterInput"
import { workspaceBuildParameterValue } from "pages/CreateWorkspacePage/CreateWorkspacePageView"
import { FormFooter } from "components/FormFooter/FormFooter"
import * as Yup from "yup"
import { Maybe } from "components/Conditionals/Maybe"
import { GoBackButton } from "components/GoBackButton/GoBackButton"
import { useValidationSchemaForRichParameters } from "util/richParameters"
export enum UpdateWorkspaceErrors {
GET_WORKSPACE_ERROR = "getWorkspaceError",
GET_TEMPLATE_PARAMETERS_ERROR = "getTemplateParametersError",
GET_WORKSPACE_BUILD_PARAMETERS_ERROR = "getWorkspaceBuildParametersError",
UPDATE_WORKSPACE_ERROR = "updateWorkspaceError",
}
export interface WorkspaceBuildParametersPageViewProps {
workspace?: TypesGen.Workspace
templateParameters?: TypesGen.TemplateVersionParameter[]
workspaceBuildParameters?: TypesGen.WorkspaceBuildParameter[]
isLoading: boolean
initialTouched?: FormikTouched<TypesGen.CreateWorkspaceRequest>
updatingWorkspace: boolean
onCancel: () => void
onSubmit: (req: TypesGen.CreateWorkspaceBuildRequest) => void
hasErrors: boolean
updateWorkspaceErrors: Partial<Record<UpdateWorkspaceErrors, Error | unknown>>
}
export const WorkspaceBuildParametersPageView: FC<
React.PropsWithChildren<WorkspaceBuildParametersPageViewProps>
> = (props) => {
const { t } = useTranslation("workspaceBuildParametersPage")
const styles = useStyles()
const formFooterStyles = useFormFooterStyles()
const initialRichParameterValues = selectInitialRichParametersValues(
props.templateParameters,
props.workspaceBuildParameters,
)
const form: FormikContextType<TypesGen.CreateWorkspaceBuildRequest> =
useFormik<TypesGen.CreateWorkspaceBuildRequest>({
initialValues: {
template_version_id: props.workspace
? props.workspace.latest_build.template_version_id
: "",
transition: "start",
rich_parameter_values: initialRichParameterValues,
},
validationSchema: Yup.object({
rich_parameter_values: useValidationSchemaForRichParameters(
"workspaceBuildParametersPage",
props.templateParameters,
initialRichParameterValues,
),
}),
enableReinitialize: true,
initialTouched: props.initialTouched,
onSubmit: (request) => {
props.onSubmit(
stripImmutableParameters(request, props.templateParameters),
)
form.setSubmitting(false)
},
})
const getFieldHelpers = getFormHelpers<TypesGen.CreateWorkspaceBuildRequest>(
form,
props.updateWorkspaceErrors[UpdateWorkspaceErrors.UPDATE_WORKSPACE_ERROR],
)
{
props.hasErrors && (
<Stack>
{Boolean(
props.updateWorkspaceErrors[
UpdateWorkspaceErrors.GET_WORKSPACE_ERROR
],
) && (
<AlertBanner
severity="error"
error={
props.updateWorkspaceErrors[
UpdateWorkspaceErrors.GET_WORKSPACE_ERROR
]
}
/>
)}
{Boolean(
props.updateWorkspaceErrors[
UpdateWorkspaceErrors.GET_TEMPLATE_PARAMETERS_ERROR
],
) && (
<AlertBanner
severity="error"
error={
props.updateWorkspaceErrors[
UpdateWorkspaceErrors.GET_TEMPLATE_PARAMETERS_ERROR
]
}
/>
)}
{Boolean(
props.updateWorkspaceErrors[
UpdateWorkspaceErrors.GET_WORKSPACE_BUILD_PARAMETERS_ERROR
],
) && (
<AlertBanner
severity="error"
error={
props.updateWorkspaceErrors[
UpdateWorkspaceErrors.GET_WORKSPACE_BUILD_PARAMETERS_ERROR
]
}
/>
)}
</Stack>
)
}
return (
<FullPageForm title={t("title")} detail={t("detail")}>
<Maybe
condition={Boolean(
props.updateWorkspaceErrors[
UpdateWorkspaceErrors.UPDATE_WORKSPACE_ERROR
],
)}
>
<AlertBanner
severity="error"
error={
props.updateWorkspaceErrors[
UpdateWorkspaceErrors.UPDATE_WORKSPACE_ERROR
]
}
/>
</Maybe>
<Maybe
condition={Boolean(
!props.isLoading &&
props.templateParameters &&
props.templateParameters.length === 0,
)}
>
<div className={styles.formSection}>
<AlertBanner severity="info" text={t("noParametersDefined")} />
<div className={styles.goBackSection}>
<GoBackButton onClick={props.onCancel} />
</div>
</div>
</Maybe>
{!props.isLoading &&
props.templateParameters &&
props.templateParameters.length > 0 &&
props.workspaceBuildParameters && (
<div className={styles.formSection}>
<form onSubmit={form.handleSubmit}>
<Stack
direction="column"
spacing={4} // Spacing here is diff because the fields here don't have the MUI floating label spacing
className={styles.formSectionFields}
>
{props.templateParameters.filter((p) => !p.mutable).length >
0 && (
<div className={styles.formSectionParameterTitle}>
Immutable parameters
</div>
)}
{props.templateParameters.map(
(parameter, index) =>
!parameter.mutable && (
<RichParameterInput
{...getFieldHelpers(
"rich_parameter_values[" + index + "].value",
)}
disabled={!parameter.mutable || form.isSubmitting}
index={index}
key={parameter.name}
onChange={(value) => {
form.setFieldValue("rich_parameter_values." + index, {
name: parameter.name,
value: value,
})
}}
parameter={parameter}
initialValue={workspaceBuildParameterValue(
initialRichParameterValues,
parameter,
)}
/>
),
)}
{props.templateParameters.filter((p) => p.mutable).length >
0 && (
<div className={styles.formSectionParameterTitle}>
Mutable parameters
</div>
)}
{props.templateParameters.map(
(parameter, index) =>
parameter.mutable && (
<RichParameterInput
{...getFieldHelpers(
"rich_parameter_values[" + index + "].value",
)}
disabled={!parameter.mutable || form.isSubmitting}
index={index}
key={parameter.name}
onChange={(value) => {
form.setFieldValue("rich_parameter_values." + index, {
name: parameter.name,
value: value,
})
}}
parameter={parameter}
initialValue={workspaceBuildParameterValue(
initialRichParameterValues,
parameter,
)}
/>
),
)}
<FormFooter
styles={formFooterStyles}
onCancel={props.onCancel}
isLoading={props.updatingWorkspace}
submitLabel={t("updateWorkspace")}
/>
</Stack>
</form>
</div>
)}
</FullPageForm>
)
}
const selectInitialRichParametersValues = (
templateParameters?: TypesGen.TemplateVersionParameter[],
workspaceBuildParameters?: TypesGen.WorkspaceBuildParameter[],
): TypesGen.WorkspaceBuildParameter[] => {
const defaults: TypesGen.WorkspaceBuildParameter[] = []
if (!templateParameters) {
return defaults
}
templateParameters.forEach((parameter) => {
if (parameter.options.length > 0) {
let parameterValue = parameter.options[0].value
if (workspaceBuildParameters) {
const foundBuildParameter = workspaceBuildParameters.find(
(buildParameter) => {
return buildParameter.name === parameter.name
},
)
if (foundBuildParameter) {
parameterValue = foundBuildParameter.value
}
}
const buildParameter: TypesGen.WorkspaceBuildParameter = {
name: parameter.name,
value: parameterValue,
}
defaults.push(buildParameter)
return
}
let parameterValue = parameter.default_value
if (workspaceBuildParameters) {
const foundBuildParameter = workspaceBuildParameters.find(
(buildParameter) => {
return buildParameter.name === parameter.name
},
)
if (foundBuildParameter) {
parameterValue = foundBuildParameter.value
}
}
const buildParameter: TypesGen.WorkspaceBuildParameter = {
name: parameter.name,
value: parameterValue || "",
}
defaults.push(buildParameter)
})
return defaults
}
const stripImmutableParameters = (
request: TypesGen.CreateWorkspaceBuildRequest,
templateParameters?: TypesGen.TemplateVersionParameter[],
): TypesGen.CreateWorkspaceBuildRequest => {
if (!templateParameters || !request.rich_parameter_values) {
return request
}
const mutableBuildParameters = request.rich_parameter_values.filter(
(buildParameter) =>
templateParameters.find(
(templateParameter) => templateParameter.name === buildParameter.name,
)?.mutable,
)
return {
...request,
rich_parameter_values: mutableBuildParameters,
}
}
const useStyles = makeStyles((theme) => ({
goBackSection: {
display: "flex",
width: "100%",
marginTop: 32,
},
formSection: {
marginTop: 20,
},
formSectionFields: {
width: "100%",
},
formSectionParameterTitle: {
fontSize: 20,
color: theme.palette.text.primary,
fontWeight: 400,
margin: 0,
marginBottom: theme.spacing(1),
},
}))
const useFormFooterStyles = makeStyles((theme) => ({
button: {
minWidth: theme.spacing(23),
[theme.breakpoints.down("sm")]: {
width: "100%",
},
},
footer: {
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
flexDirection: "row-reverse",
gap: theme.spacing(2),
[theme.breakpoints.down("sm")]: {
flexDirection: "column",
gap: theme.spacing(1),
},
},
}))

View File

@ -46,7 +46,6 @@ export const WorkspaceReadyPage = ({
const {
workspace,
template,
templateParameters,
builds,
getBuildsError,
buildError,
@ -113,7 +112,7 @@ export const WorkspaceReadyPage = ({
handleUpdate={() => workspaceSend({ type: "UPDATE" })}
handleCancel={() => workspaceSend({ type: "CANCEL" })}
handleChangeVersion={() => navigate("change-version")}
handleBuildParameters={() => navigate("build-parameters")}
handleSettings={() => navigate("settings")}
resources={workspace.latest_build.resources}
builds={builds}
canUpdateWorkspace={canUpdateWorkspace}
@ -127,7 +126,6 @@ export const WorkspaceReadyPage = ({
buildInfo={buildInfo}
applicationsHost={applicationsHost}
template={template}
templateParameters={templateParameters}
quota_budget={quotaState.context.quota?.budget}
/>
<DeleteDialog

View File

@ -0,0 +1,109 @@
import {
FormFields,
FormFooter,
FormSection,
HorizontalForm,
} from "components/Form/Form"
import { RichParameterInput } from "components/RichParameterInput/RichParameterInput"
import { useFormik } from "formik"
import { FC } from "react"
import { useTranslation } from "react-i18next"
import {
useValidationSchemaForRichParameters,
workspaceBuildParameterValue,
} from "util/richParameters"
import { WorkspaceSettings, WorkspaceSettingsFormValue } from "./data"
import * as Yup from "yup"
import { nameValidator, getFormHelpers, onChangeTrimmed } from "util/formUtils"
import TextField from "@material-ui/core/TextField"
export const WorkspaceSettingsForm: FC<{
isSubmitting: boolean
settings: WorkspaceSettings
error: unknown
onCancel: () => void
onSubmit: (values: WorkspaceSettingsFormValue) => void
}> = ({ onCancel, onSubmit, settings, error, isSubmitting }) => {
const { t } = useTranslation("workspaceSettingsPage")
const mutableParameters = settings.templateVersionRichParameters.filter(
(param) => param.mutable,
)
const form = useFormik<WorkspaceSettingsFormValue>({
onSubmit,
initialValues: {
name: settings.workspace.name,
rich_parameter_values: mutableParameters.map((parameter) => {
const buildParameter = settings.buildParameters.find(
(p) => p.name === parameter.name,
)
if (!buildParameter) {
throw new Error("Missing build parameter for " + parameter.name)
}
return buildParameter
}),
},
validationSchema: Yup.object({
name: nameValidator(t("nameLabel")),
rich_parameter_values: useValidationSchemaForRichParameters(
"createWorkspacePage",
settings.templateVersionRichParameters,
),
}),
})
const getFieldHelpers = getFormHelpers<WorkspaceSettingsFormValue>(
form,
error,
)
return (
<HorizontalForm onSubmit={form.handleSubmit} data-testid="form">
<FormSection
title={t("generalInfo")}
description={t("generalInfoDescription")}
>
<FormFields>
<TextField
{...getFieldHelpers("name")}
disabled={form.isSubmitting}
onChange={onChangeTrimmed(form)}
autoFocus
fullWidth
label={t("nameLabel")}
variant="outlined"
/>
</FormFields>
</FormSection>
{mutableParameters.length > 0 && (
<FormSection
title={t("parameters")}
description={t("parametersDescription")}
>
<FormFields>
{settings.templateVersionRichParameters.map((parameter, index) => (
<RichParameterInput
{...getFieldHelpers(
"rich_parameter_values[" + index + "].value",
)}
disabled={isSubmitting}
index={index}
key={parameter.name}
onChange={async (value) => {
await form.setFieldValue("rich_parameter_values." + index, {
name: parameter.name,
value: value,
})
}}
parameter={parameter}
initialValue={workspaceBuildParameterValue(
settings.buildParameters,
parameter,
)}
/>
))}
</FormFields>
</FormSection>
)}
<FormFooter onCancel={onCancel} isLoading={isSubmitting} />
</HorizontalForm>
)
}

View File

@ -0,0 +1,80 @@
import userEvent from "@testing-library/user-event"
import {
MockTemplateVersionParameter1,
MockTemplateVersionParameter2,
MockWorkspace,
MockWorkspaceBuild,
MockWorkspaceBuildParameter1,
MockWorkspaceBuildParameter2,
renderWithAuth,
waitForLoaderToBeRemoved,
} from "testHelpers/renderHelpers"
import WorkspaceSettingsPage from "./WorkspaceSettingsPage"
import { screen, waitFor, within } from "@testing-library/react"
import * as api from "api/api"
test("Submit the workspace settings page successfully", async () => {
// Mock the API calls that loads data
jest
.spyOn(api, "getWorkspaceByOwnerAndName")
.mockResolvedValueOnce(MockWorkspace)
jest
.spyOn(api, "getTemplateVersionRichParameters")
.mockResolvedValueOnce([
MockTemplateVersionParameter1,
MockTemplateVersionParameter2,
])
jest
.spyOn(api, "getWorkspaceBuildParameters")
.mockResolvedValueOnce([
MockWorkspaceBuildParameter1,
MockWorkspaceBuildParameter2,
])
// Mock the API calls that submit data
const patchWorkspaceSpy = jest
.spyOn(api, "patchWorkspace")
.mockResolvedValue()
const postWorkspaceBuildSpy = jest
.spyOn(api, "postWorkspaceBuild")
.mockResolvedValue(MockWorkspaceBuild)
// Setup event and rendering
const user = userEvent.setup()
renderWithAuth(<WorkspaceSettingsPage />, {
route: "/@test-user/test-workspace/settings",
path: "/@:username/:workspace/settings",
// Need this because after submit the user is redirected
extraRoutes: [{ path: "/@:username/:workspace", element: <div /> }],
})
await waitForLoaderToBeRemoved()
// Fill the form and submit
const form = screen.getByTestId("form")
const name = within(form).getByLabelText("Name")
await user.clear(name)
await user.type(within(form).getByLabelText("Name"), "new-name")
const parameter1 = within(form).getByLabelText(
MockWorkspaceBuildParameter1.name,
{ exact: false },
)
await user.clear(parameter1)
await user.type(parameter1, "new-value")
const parameter2 = within(form).getByLabelText(
MockWorkspaceBuildParameter2.name,
{ exact: false },
)
await user.clear(parameter2)
await user.type(parameter2, "1")
await user.click(within(form).getByRole("button", { name: "Submit" }))
// Assert that the API calls were made with the correct data
await waitFor(() => {
expect(patchWorkspaceSpy).toHaveBeenCalledWith(MockWorkspace.id, {
name: "new-name",
})
expect(postWorkspaceBuildSpy).toHaveBeenCalledWith(MockWorkspace.id, {
transition: "start",
rich_parameter_values: [
{ name: MockTemplateVersionParameter1.name, value: "new-value" },
{ name: MockTemplateVersionParameter2.name, value: "1" },
],
})
})
})

View File

@ -0,0 +1,49 @@
import { getErrorMessage } from "api/errors"
import { displayError } from "components/GlobalSnackbar/utils"
import { Helmet } from "react-helmet-async"
import { useTranslation } from "react-i18next"
import { useNavigate, useParams } from "react-router-dom"
import { pageTitle } from "util/page"
import { useUpdateWorkspaceSettings, useWorkspaceSettings } from "./data"
import { WorkspaceSettingsPageView } from "./WorkspaceSettingsPageView"
const WorkspaceSettingsPage = () => {
const { t } = useTranslation("workspaceSettingsPage")
const { username, workspace: workspaceName } = useParams() as {
username: string
workspace: string
}
const {
data: settings,
error,
isLoading,
} = useWorkspaceSettings(username, workspaceName)
const navigate = useNavigate()
const updateSettings = useUpdateWorkspaceSettings(settings?.workspace.id, {
onSuccess: ({ name }) => {
navigate(`/@${username}/${name}`)
},
onError: (error) =>
displayError(getErrorMessage(error, t("defaultErrorMessage"))),
})
return (
<>
<Helmet>
<title>{pageTitle(t("title"))}</title>
</Helmet>
<WorkspaceSettingsPageView
formError={updateSettings.error}
loadingError={error}
isLoading={isLoading}
isSubmitting={updateSettings.isLoading}
settings={settings}
onCancel={() => navigate(-1)}
onSubmit={updateSettings.mutate}
/>
</>
)
}
export default WorkspaceSettingsPage

View File

@ -0,0 +1,41 @@
import { ComponentMeta, Story } from "@storybook/react"
import {
MockTemplateVersionParameter1,
MockTemplateVersionParameter2,
MockWorkspace,
MockWorkspaceBuildParameter1,
MockWorkspaceBuildParameter2,
} from "testHelpers/entities"
import {
WorkspaceSettingsPageView,
WorkspaceSettingsPageViewProps,
} from "./WorkspaceSettingsPageView"
export default {
title: "pages/WorkspaceSettingsPageView",
component: WorkspaceSettingsPageView,
args: {
formError: undefined,
loadingError: undefined,
isLoading: false,
isSubmitting: false,
settings: {
workspace: MockWorkspace,
buildParameters: [
MockWorkspaceBuildParameter1,
MockWorkspaceBuildParameter2,
],
templateVersionRichParameters: [
MockTemplateVersionParameter1,
MockTemplateVersionParameter2,
],
},
},
} as ComponentMeta<typeof WorkspaceSettingsPageView>
const Template: Story<WorkspaceSettingsPageViewProps> = (args) => (
<WorkspaceSettingsPageView {...args} />
)
export const Example = Template.bind({})
Example.args = {}

View File

@ -0,0 +1,47 @@
import { AlertBanner } from "components/AlertBanner/AlertBanner"
import { FullPageHorizontalForm } from "components/FullPageForm/FullPageHorizontalForm"
import { Loader } from "components/Loader/Loader"
import { FC } from "react"
import { useTranslation } from "react-i18next"
import { WorkspaceSettings, WorkspaceSettingsFormValue } from "./data"
import { WorkspaceSettingsForm } from "./WorkspaceSettingsForm"
export type WorkspaceSettingsPageViewProps = {
formError: unknown
loadingError: unknown
isLoading: boolean
isSubmitting: boolean
settings: WorkspaceSettings | undefined
onCancel: () => void
onSubmit: (formValues: WorkspaceSettingsFormValue) => void
}
export const WorkspaceSettingsPageView: FC<WorkspaceSettingsPageViewProps> = ({
onCancel,
onSubmit,
isLoading,
isSubmitting,
settings,
formError,
loadingError,
}) => {
const { t } = useTranslation("workspaceSettingsPage")
return (
<FullPageHorizontalForm title={t("title")} onCancel={onCancel}>
<>
{loadingError && <AlertBanner error={loadingError} severity="error" />}
{isLoading && <Loader />}
{settings && (
<WorkspaceSettingsForm
error={formError}
isSubmitting={isSubmitting}
settings={settings}
onCancel={onCancel}
onSubmit={onSubmit}
/>
)}
</>
</FullPageHorizontalForm>
)
}

View File

@ -0,0 +1,73 @@
import { useMutation, useQuery } from "@tanstack/react-query"
import {
getWorkspaceByOwnerAndName,
getWorkspaceBuildParameters,
getTemplateVersionRichParameters,
patchWorkspace,
postWorkspaceBuild,
} from "api/api"
import { WorkspaceBuildParameter } from "api/typesGenerated"
const getWorkspaceSettings = async (owner: string, name: string) => {
const workspace = await getWorkspaceByOwnerAndName(owner, name)
const latestBuild = workspace.latest_build
const [templateVersionRichParameters, buildParameters] = await Promise.all([
getTemplateVersionRichParameters(latestBuild.template_version_id),
getWorkspaceBuildParameters(latestBuild.id),
])
return {
workspace,
templateVersionRichParameters,
buildParameters,
}
}
export const useWorkspaceSettings = (owner: string, workspace: string) => {
return useQuery({
queryKey: ["workspaceSettings", owner, workspace],
queryFn: () => getWorkspaceSettings(owner, workspace),
})
}
export type WorkspaceSettings = Awaited<ReturnType<typeof getWorkspaceSettings>>
export type WorkspaceSettingsFormValue = {
name: string
rich_parameter_values: WorkspaceBuildParameter[]
}
const updateWorkspaceSettings = async (
workspaceId: string,
formValues: WorkspaceSettingsFormValue,
) => {
await Promise.all([
patchWorkspace(workspaceId, { name: formValues.name }),
postWorkspaceBuild(workspaceId, {
transition: "start",
rich_parameter_values: formValues.rich_parameter_values,
}),
])
return formValues // So we can get then on the onSuccess callback
}
export const useUpdateWorkspaceSettings = (
workspaceId?: string,
options?: {
onSuccess?: (
result: Awaited<ReturnType<typeof updateWorkspaceSettings>>,
) => void
onError?: (error: unknown) => void
},
) => {
return useMutation({
mutationFn: (formValues: WorkspaceSettingsFormValue) => {
if (!workspaceId) {
throw new Error("No workspace id")
}
return updateWorkspaceSettings(workspaceId, formValues)
},
onSuccess: options?.onSuccess,
onError: options?.onError,
})
}

View File

@ -146,3 +146,13 @@ export const useValidationSchemaForRichParameters = (
)
.required()
}
export const workspaceBuildParameterValue = (
workspaceBuildParameters: WorkspaceBuildParameter[],
parameter: TemplateVersionParameter,
): string => {
const buildParameter = workspaceBuildParameters.find((buildParameter) => {
return buildParameter.name === parameter.name
})
return (buildParameter && buildParameter.value) || ""
}

View File

@ -1,223 +0,0 @@
import {
getTemplateVersionRichParameters,
getWorkspaceByOwnerAndName,
getWorkspaceBuildParameters,
postWorkspaceBuild,
} from "api/api"
import {
CreateWorkspaceBuildRequest,
Template,
TemplateVersionParameter,
Workspace,
WorkspaceBuild,
WorkspaceBuildParameter,
} from "api/typesGenerated"
import { assign, createMachine } from "xstate"
type WorkspaceBuildParametersContext = {
workspaceOwner: string
workspaceName: string
selectedWorkspace?: Workspace
selectedTemplate?: Template
templateParameters?: TemplateVersionParameter[]
workspaceBuildParameters?: WorkspaceBuildParameter[]
createWorkspaceBuildRequest?: CreateWorkspaceBuildRequest
getWorkspaceError?: Error | unknown
getTemplateParametersError?: Error | unknown
getWorkspaceBuildParametersError?: Error | unknown
updateWorkspaceError?: Error | unknown
}
type UpdateWorkspaceEvent = {
type: "UPDATE_WORKSPACE"
request: CreateWorkspaceBuildRequest
}
export const workspaceBuildParametersMachine = createMachine(
{
id: "workspaceBuildParametersState",
predictableActionArguments: true,
tsTypes:
{} as import("./workspaceBuildParametersXService.typegen").Typegen0,
schema: {
context: {} as WorkspaceBuildParametersContext,
events: {} as UpdateWorkspaceEvent,
services: {} as {
getWorkspace: {
data: Workspace
}
getTemplateParameters: {
data: TemplateVersionParameter[]
}
getWorkspaceBuildParameters: {
data: WorkspaceBuildParameter[]
}
updateWorkspace: {
data: WorkspaceBuild
}
},
},
initial: "gettingWorkspace",
states: {
gettingWorkspace: {
entry: "clearGetWorkspaceError",
invoke: {
src: "getWorkspace",
onDone: [
{
actions: ["assignWorkspace"],
target: "gettingTemplateParameters",
},
],
onError: {
actions: ["assignGetWorkspaceError"],
target: "error",
},
},
},
gettingTemplateParameters: {
entry: "clearGetTemplateParametersError",
invoke: {
src: "getTemplateParameters",
onDone: [
{
actions: ["assignTemplateParameters"],
target: "gettingWorkspaceBuildParameters",
},
],
onError: {
actions: ["assignGetTemplateParametersError"],
target: "error",
},
},
},
gettingWorkspaceBuildParameters: {
entry: "clearGetWorkspaceBuildParametersError",
invoke: {
src: "getWorkspaceBuildParameters",
onDone: {
actions: ["assignWorkspaceBuildParameters"],
target: "fillingParams",
},
onError: {
actions: ["assignGetWorkspaceBuildParametersError"],
target: "error",
},
},
},
fillingParams: {
on: {
UPDATE_WORKSPACE: {
actions: ["assignCreateWorkspaceBuildRequest"],
target: "updatingWorkspace",
},
},
},
updatingWorkspace: {
entry: "clearUpdateWorkspaceError",
invoke: {
src: "updateWorkspace",
onDone: {
actions: ["onUpdateWorkspace"],
target: "updated",
},
onError: {
actions: ["assignUpdateWorkspaceError"],
target: "fillingParams",
},
},
},
updated: {
entry: "onUpdateWorkspace",
type: "final",
},
error: {},
},
},
{
services: {
getWorkspace: (context) => {
const { workspaceOwner, workspaceName } = context
return getWorkspaceByOwnerAndName(workspaceOwner, workspaceName)
},
getTemplateParameters: (context) => {
const { selectedWorkspace } = context
if (!selectedWorkspace) {
throw new Error("No workspace selected")
}
return getTemplateVersionRichParameters(
selectedWorkspace.latest_build.template_version_id,
)
},
getWorkspaceBuildParameters: (context) => {
const { selectedWorkspace } = context
if (!selectedWorkspace) {
throw new Error("No workspace selected")
}
return getWorkspaceBuildParameters(selectedWorkspace.latest_build.id)
},
updateWorkspace: (context) => {
const { selectedWorkspace, createWorkspaceBuildRequest } = context
if (!selectedWorkspace) {
throw new Error("No workspace selected")
}
if (!createWorkspaceBuildRequest) {
throw new Error("No workspace build request")
}
return postWorkspaceBuild(
selectedWorkspace.id,
createWorkspaceBuildRequest,
)
},
},
actions: {
assignWorkspace: assign({
selectedWorkspace: (_, event) => event.data,
}),
assignTemplateParameters: assign({
templateParameters: (_, event) => event.data,
}),
assignWorkspaceBuildParameters: assign({
workspaceBuildParameters: (_, event) => event.data,
}),
assignCreateWorkspaceBuildRequest: assign({
createWorkspaceBuildRequest: (_, event) => event.request,
}),
assignGetWorkspaceError: assign({
getWorkspaceError: (_, event) => event.data,
}),
clearGetWorkspaceError: assign({
getWorkspaceError: (_) => undefined,
}),
assignGetTemplateParametersError: assign({
getTemplateParametersError: (_, event) => event.data,
}),
clearGetTemplateParametersError: assign({
getTemplateParametersError: (_) => undefined,
}),
clearGetWorkspaceBuildParametersError: assign({
getWorkspaceBuildParametersError: (_) => undefined,
}),
assignGetWorkspaceBuildParametersError: assign({
getWorkspaceBuildParametersError: (_, event) => event.data,
}),
clearUpdateWorkspaceError: assign({
updateWorkspaceError: (_) => undefined,
}),
assignUpdateWorkspaceError: assign({
updateWorkspaceError: (_, event) => event.data,
}),
},
},
)

View File

@ -55,7 +55,6 @@ export interface WorkspaceContext {
eventSource?: EventSource
workspace?: TypesGen.Workspace
template?: TypesGen.Template
templateParameters?: TypesGen.TemplateVersionParameter[]
build?: TypesGen.WorkspaceBuild
getWorkspaceError?: Error | unknown
getTemplateWarning: Error | unknown
@ -203,7 +202,7 @@ export const workspaceMachine = createMachine(
onDone: [
{
actions: ["assignTemplate", "clearGetTemplateWarning"],
target: "gettingTemplateParameters",
target: "gettingPermissions",
},
],
onError: [
@ -218,31 +217,6 @@ export const workspaceMachine = createMachine(
},
tags: "loading",
},
gettingTemplateParameters: {
invoke: {
src: "getTemplateParameters",
id: "getTemplateParameters",
onDone: [
{
actions: [
"assignTemplateParameters",
"clearGetTemplateParametersWarning",
],
target: "gettingPermissions",
},
],
onError: [
{
actions: [
"assignGetTemplateParametersWarning",
"displayGetTemplateParametersWarning",
],
target: "error",
},
],
},
tags: "loading",
},
gettingPermissions: {
invoke: {
src: "checkPermissions",
@ -524,9 +498,6 @@ export const workspaceMachine = createMachine(
assignTemplate: assign({
template: (_, event) => event.data,
}),
assignTemplateParameters: assign({
templateParameters: (_, event) => event.data,
}),
assignPermissions: assign({
// Setting event.data as Permissions to be more stricted. So we know
// what permissions we asked for.
@ -587,15 +558,6 @@ export const workspaceMachine = createMachine(
clearGetTemplateWarning: assign({
getTemplateWarning: (_) => undefined,
}),
assignGetTemplateParametersWarning: assign({
getTemplateParametersWarning: (_, event) => event.data,
}),
displayGetTemplateParametersWarning: () => {
displayError(Language.getTemplateParametersWarning)
},
clearGetTemplateParametersWarning: assign({
getTemplateParametersWarning: (_) => undefined,
}),
// Timeline
assignBuilds: assign({
builds: (_, event) => event.data,
@ -667,15 +629,6 @@ export const workspaceMachine = createMachine(
throw Error("Cannot get template without workspace")
}
},
getTemplateParameters: async (context) => {
if (context.workspace) {
return await API.getTemplateVersionRichParameters(
context.workspace.latest_build.template_version_id,
)
} else {
throw Error("Cannot get template parameters without workspace")
}
},
updateWorkspace:
({ workspace }, { buildParameters }) =>
async (send) => {