From 5334762aa7b1b00d9c0e4a984af61a873a177bb8 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 13 Jul 2023 17:29:49 +0000 Subject: [PATCH 01/10] Add base for ephemeral form --- site/src/api/api.ts | 16 ++- .../LoadingButton/LoadingButton.tsx | 20 ++-- site/src/components/Workspace/Workspace.tsx | 3 +- .../BuildParametersPopover.tsx | 49 ++++++++ .../components/WorkspaceActions/Buttons.tsx | 108 ++++++++++++++++-- .../WorkspaceActions.stories.tsx | 23 ++-- .../WorkspaceActions/WorkspaceActions.tsx | 19 +-- .../WorkspacePage/WorkspaceReadyPage.tsx | 2 - .../WorkspaceParametersForm.tsx | 17 +-- .../WorkspaceParametersPage.tsx | 21 +--- site/src/utils/richParameters.ts | 18 +++ .../xServices/workspace/workspaceXService.ts | 5 +- 12 files changed, 221 insertions(+), 80 deletions(-) create mode 100644 site/src/components/WorkspaceActions/BuildParametersPopover.tsx diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 2801c6b9886fe..0119821be4f31 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -481,8 +481,8 @@ export function waitForBuild(build: TypesGen.WorkspaceBuild) { let latestJobInfo: TypesGen.ProvisionerJob | undefined = undefined while ( - !["succeeded", "canceled"].some( - (status) => latestJobInfo?.status.includes(status), + !["succeeded", "canceled"].some((status) => + latestJobInfo?.status.includes(status), ) ) { const { job } = await getWorkspaceBuildByNumber( @@ -1346,3 +1346,15 @@ export const issueReconnectingPTYSignedToken = async ( ) return response.data } + +export const getWorkspaceParameters = async (workspace: TypesGen.Workspace) => { + const latestBuild = workspace.latest_build + const [templateVersionRichParameters, buildParameters] = await Promise.all([ + getTemplateVersionRichParameters(latestBuild.template_version_id), + getWorkspaceBuildParameters(latestBuild.id), + ]) + return { + templateVersionRichParameters, + buildParameters, + } +} diff --git a/site/src/components/LoadingButton/LoadingButton.tsx b/site/src/components/LoadingButton/LoadingButton.tsx index 1f2a93cc6b0b7..5270df37c43a5 100644 --- a/site/src/components/LoadingButton/LoadingButton.tsx +++ b/site/src/components/LoadingButton/LoadingButton.tsx @@ -1,21 +1,25 @@ -import { FC } from "react" +import { forwardRef } from "react" import MuiLoadingButton, { LoadingButtonProps as MuiLoadingButtonProps, } from "@mui/lab/LoadingButton" export type LoadingButtonProps = MuiLoadingButtonProps -export const LoadingButton: FC = ({ - children, - loadingIndicator, - ...buttonProps -}) => { +export const LoadingButton = forwardRef< + HTMLButtonElement, + MuiLoadingButtonProps +>(({ children, loadingIndicator, ...buttonProps }, ref) => { return ( - + {/* known issue: https://github.com/mui/material-ui/issues/27853 */} {buttonProps.loading && loadingIndicator ? loadingIndicator : children} ) -} +}) diff --git a/site/src/components/Workspace/Workspace.tsx b/site/src/components/Workspace/Workspace.tsx index 63effb681752e..51474729a62a3 100644 --- a/site/src/components/Workspace/Workspace.tsx +++ b/site/src/components/Workspace/Workspace.tsx @@ -194,8 +194,7 @@ export const Workspace: FC> = ({ { + const form = useFormik({ + initialValues: { + rich_parameter_values: getInitialParameterValues( + ephemeralParameters, + buildParameters, + ), + }, + onSubmit: () => {}, + }) + const getFieldHelpers = getFormHelpers(form) + + return ( +
+ {ephemeralParameters.map((parameter, index) => { + return ( + { + await form.setFieldValue(`rich_parameter_values[${index}]`, { + name: parameter.name, + value: value, + }) + }} + /> + ) + })} + + ) +} diff --git a/site/src/components/WorkspaceActions/Buttons.tsx b/site/src/components/WorkspaceActions/Buttons.tsx index 9c508216ad027..27eeb76b97376 100644 --- a/site/src/components/WorkspaceActions/Buttons.tsx +++ b/site/src/components/WorkspaceActions/Buttons.tsx @@ -5,8 +5,21 @@ import CropSquareIcon from "@mui/icons-material/CropSquare" import PlayCircleOutlineIcon from "@mui/icons-material/PlayCircleOutline" import ReplayIcon from "@mui/icons-material/Replay" import { LoadingButton } from "components/LoadingButton/LoadingButton" -import { FC } from "react" +import { FC, useRef, useState } from "react" import BlockOutlined from "@mui/icons-material/BlockOutlined" +import ButtonGroup from "@mui/material/ButtonGroup" +import ExpandMoreOutlined from "@mui/icons-material/ExpandMoreOutlined" +import Popover from "@mui/material/Popover" +import { + HelpTooltipText, + HelpTooltipTitle, +} from "components/Tooltips/HelpTooltip/HelpTooltip" +import Box from "@mui/material/Box" +import { useQuery } from "@tanstack/react-query" +import { Workspace } from "api/typesGenerated" +import { getWorkspaceParameters } from "api/api" +import { BuildParametersForm } from "./BuildParametersPopover" +import { Loader } from "components/Loader/Loader" interface WorkspaceAction { loading?: boolean @@ -31,17 +44,92 @@ export const UpdateButton: FC = ({ ) } -export const StartButton: FC = ({ handleAction, loading }) => { +export const StartButton: FC = ({ + handleAction, + workspace, + loading, +}) => { + const anchorRef = useRef(null) + const [isOpen, setIsOpen] = useState(false) + const { data: parameters } = useQuery({ + queryKey: ["workspace", workspace.id, "parameters"], + queryFn: () => getWorkspaceParameters(workspace), + enabled: isOpen, + }) + const ephemeralParameters = parameters + ? parameters.templateVersionRichParameters.filter((p) => p.ephemeral) + : undefined + return ( - } - onClick={handleAction} + button:hover + button": { + borderLeft: "1px solid #FFF", + }, + }} > - Start - + } + onClick={handleAction} + > + Start + + + { + setIsOpen(false) + }} + anchorOrigin={{ + vertical: "bottom", + horizontal: "right", + }} + transformOrigin={{ + vertical: "top", + horizontal: "right", + }} + sx={{ + ".MuiPaper-root": { + p: 2.5, + width: (theme) => theme.spacing(38), + marginTop: 1, + }, + }} + > + theme.palette.text.secondary }}> + Build Options + + These parameters only apply for a single workspace start. + + + + {parameters && parameters.buildParameters && ephemeralParameters ? ( + + ) : ( + + )} + + + ) } diff --git a/site/src/components/WorkspaceActions/WorkspaceActions.stories.tsx b/site/src/components/WorkspaceActions/WorkspaceActions.stories.tsx index b2b2526811d0d..3697a20a4d453 100644 --- a/site/src/components/WorkspaceActions/WorkspaceActions.stories.tsx +++ b/site/src/components/WorkspaceActions/WorkspaceActions.stories.tsx @@ -26,67 +26,66 @@ const defaultArgs = { export const Starting = Template.bind({}) Starting.args = { ...defaultArgs, - workspaceStatus: Mocks.MockStartingWorkspace.latest_build.status, + workspace: Mocks.MockStartingWorkspace, } export const Running = Template.bind({}) Running.args = { ...defaultArgs, - workspaceStatus: Mocks.MockWorkspace.latest_build.status, + workspace: Mocks.MockWorkspace, } export const Stopping = Template.bind({}) Stopping.args = { ...defaultArgs, - workspaceStatus: Mocks.MockStoppingWorkspace.latest_build.status, + workspace: Mocks.MockStoppingWorkspace, } export const Stopped = Template.bind({}) Stopped.args = { ...defaultArgs, - workspaceStatus: Mocks.MockStoppedWorkspace.latest_build.status, + workspace: Mocks.MockStoppedWorkspace, } export const Canceling = Template.bind({}) Canceling.args = { ...defaultArgs, - workspaceStatus: Mocks.MockCancelingWorkspace.latest_build.status, + workspace: Mocks.MockCancelingWorkspace, } export const Canceled = Template.bind({}) Canceled.args = { ...defaultArgs, - workspaceStatus: Mocks.MockCanceledWorkspace.latest_build.status, + workspace: Mocks.MockCanceledWorkspace, } export const Deleting = Template.bind({}) Deleting.args = { ...defaultArgs, - workspaceStatus: Mocks.MockDeletingWorkspace.latest_build.status, + workspace: Mocks.MockDeletingWorkspace, } export const Deleted = Template.bind({}) Deleted.args = { ...defaultArgs, - workspaceStatus: Mocks.MockDeletedWorkspace.latest_build.status, + workspace: Mocks.MockDeletedWorkspace, } export const Outdated = Template.bind({}) Outdated.args = { ...defaultArgs, - isOutdated: true, - workspaceStatus: Mocks.MockOutdatedWorkspace.latest_build.status, + workspace: Mocks.MockOutdatedWorkspace, } export const Failed = Template.bind({}) Failed.args = { ...defaultArgs, - workspaceStatus: Mocks.MockFailedWorkspace.latest_build.status, + workspace: Mocks.MockFailedWorkspace, } export const Updating = Template.bind({}) Updating.args = { ...defaultArgs, isUpdating: true, - workspaceStatus: Mocks.MockOutdatedWorkspace.latest_build.status, + workspace: Mocks.MockOutdatedWorkspace, } diff --git a/site/src/components/WorkspaceActions/WorkspaceActions.tsx b/site/src/components/WorkspaceActions/WorkspaceActions.tsx index f874b606658ff..e06182b2b6cb2 100644 --- a/site/src/components/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/components/WorkspaceActions/WorkspaceActions.tsx @@ -3,7 +3,7 @@ import Menu from "@mui/material/Menu" import { makeStyles } from "@mui/styles" import MoreVertOutlined from "@mui/icons-material/MoreVertOutlined" import { FC, Fragment, ReactNode, useRef, useState } from "react" -import { WorkspaceStatus } from "api/typesGenerated" +import { Workspace } from "api/typesGenerated" import { ActionLoadingButton, CancelButton, @@ -24,8 +24,7 @@ import DeleteOutlined from "@mui/icons-material/DeleteOutlined" import IconButton from "@mui/material/IconButton" export interface WorkspaceActionsProps { - workspaceStatus: WorkspaceStatus - isOutdated: boolean + workspace: Workspace handleStart: () => void handleStop: () => void handleRestart: () => void @@ -41,8 +40,8 @@ export interface WorkspaceActionsProps { } export const WorkspaceActions: FC = ({ - workspaceStatus, - isOutdated, + workspace, + handleStart, handleStop, handleRestart, @@ -60,8 +59,8 @@ export const WorkspaceActions: FC = ({ canCancel, canAcceptJobs, actions: actionsByStatus, - } = actionsByWorkspaceStatus(workspaceStatus) - const canBeUpdated = isOutdated && canAcceptJobs + } = actionsByWorkspaceStatus(workspace.latest_build.status) + const canBeUpdated = workspace.outdated && canAcceptJobs const menuTriggerRef = useRef(null) const [isMenuOpen, setIsMenuOpen] = useState(false) @@ -71,9 +70,11 @@ export const WorkspaceActions: FC = ({ [ButtonTypesEnum.updating]: ( ), - [ButtonTypesEnum.start]: , + [ButtonTypesEnum.start]: ( + + ), [ButtonTypesEnum.starting]: ( - + ), [ButtonTypesEnum.stop]: , [ButtonTypesEnum.stopping]: ( diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index abb87c4ffb6dd..1c89b1bd9e9eb 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -100,13 +100,11 @@ export const WorkspaceReadyPage = ({ ["canceling", "deleting", "pending", "starting", "stopping"].includes( workspace.latest_build.status, )) - const { mutate: restartWorkspace, error: restartBuildError, isLoading: isRestarting, } = useRestartWorkspace() - // keep banner machine in sync with workspace useEffect(() => { bannerSend({ type: "REFRESH_WORKSPACE", workspace }) diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersForm.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersForm.tsx index 10c621cc142f3..ade36fc6443a4 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersForm.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersForm.tsx @@ -9,6 +9,7 @@ import { useFormik } from "formik" import { FC } from "react" import { useTranslation } from "react-i18next" import { + getInitialParameterValues, useValidationSchemaForRichParameters, workspaceBuildParameterValue, } from "utils/richParameters" @@ -48,18 +49,10 @@ export const WorkspaceParametersForm: FC<{ const form = useFormik({ onSubmit, initialValues: { - rich_parameter_values: mutableParameters.map((parameter) => { - const buildParameter = buildParameters.find( - (p) => p.name === parameter.name, - ) - if (!buildParameter) { - return { - name: parameter.name, - value: parameter.default_value, - } - } - return buildParameter - }), + rich_parameter_values: getInitialParameterValues( + mutableParameters, + buildParameters, + ), }, validationSchema: Yup.object({ rich_parameter_values: useValidationSchemaForRichParameters( diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx index f6d97c01cf79f..9ae8173eaa7b3 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx @@ -1,9 +1,4 @@ -import { - getTemplateVersionRichParameters, - getWorkspaceBuildParameters, - postWorkspaceBuild, -} from "api/api" -import { Workspace } from "api/typesGenerated" +import { getWorkspaceParameters, postWorkspaceBuild } from "api/api" import { Helmet } from "react-helmet-async" import { pageTitle } from "utils/page" import { useWorkspaceSettingsContext } from "../WorkspaceSettingsLayout" @@ -21,22 +16,10 @@ import { FC } from "react" import { isApiValidationError } from "api/errors" import { ErrorAlert } from "components/Alert/ErrorAlert" -const getWorkspaceParameters = async (workspace: Workspace) => { - const latestBuild = workspace.latest_build - const [templateVersionRichParameters, buildParameters] = await Promise.all([ - getTemplateVersionRichParameters(latestBuild.template_version_id), - getWorkspaceBuildParameters(latestBuild.id), - ]) - return { - templateVersionRichParameters, - buildParameters, - } -} - const WorkspaceParametersPage = () => { const { workspace } = useWorkspaceSettingsContext() const query = useQuery({ - queryKey: ["workspaceSettings", workspace.id], + queryKey: ["workspace", workspace.id, "parameters"], queryFn: () => getWorkspaceParameters(workspace), }) const navigate = useNavigate() diff --git a/site/src/utils/richParameters.ts b/site/src/utils/richParameters.ts index d542cc212e7bd..9f7bff7522cf6 100644 --- a/site/src/utils/richParameters.ts +++ b/site/src/utils/richParameters.ts @@ -182,3 +182,21 @@ export const workspaceBuildParameterValue = ( }) return (buildParameter && buildParameter.value) || "" } + +export const getInitialParameterValues = ( + templateParameters: TemplateVersionParameter[], + buildParameters: WorkspaceBuildParameter[], +) => { + return templateParameters.map((parameter) => { + const buildParameter = buildParameters.find( + (p) => p.name === parameter.name, + ) + if (!buildParameter) { + return { + name: parameter.name, + value: parameter.default_value, + } + } + return buildParameter + }) +} diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index e4baa7d9ded84..67e54d1b87eaf 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -138,7 +138,7 @@ const permissionsToCheck = ( }, action: "read", }, - }) as const + } as const) export const workspaceMachine = createMachine( { @@ -152,9 +152,6 @@ export const workspaceMachine = createMachine( loadInitialWorkspaceData: { data: Awaited> } - getTemplateParameters: { - data: TypesGen.TemplateVersionParameter[] - } updateWorkspace: { data: TypesGen.WorkspaceBuild } From 6872b63593cfc675fa23a3fcc47e8cd02134af20 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 14 Jul 2023 17:35:28 +0000 Subject: [PATCH 02/10] Make fields look better --- site/src/api/api.ts | 16 +- site/src/components/Markdown/Markdown.tsx | 8 +- .../RichParameterInput.stories.tsx | 381 ++++++++++-------- .../RichParameterInput/RichParameterInput.tsx | 80 +++- site/src/components/Workspace/Workspace.tsx | 4 +- .../BuildParametersPopover.tsx | 149 ++++++- .../components/WorkspaceActions/Buttons.tsx | 134 ++---- .../WorkspaceActions/WorkspaceActions.tsx | 21 +- .../WorkspacePage/WorkspaceReadyPage.tsx | 10 +- .../xServices/workspace/workspaceXService.ts | 9 +- 10 files changed, 504 insertions(+), 308 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 0119821be4f31..ba412e39e8764 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -481,8 +481,8 @@ export function waitForBuild(build: TypesGen.WorkspaceBuild) { let latestJobInfo: TypesGen.ProvisionerJob | undefined = undefined while ( - !["succeeded", "canceled"].some((status) => - latestJobInfo?.status.includes(status), + !["succeeded", "canceled"].some( + (status) => latestJobInfo?.status.includes(status), ) ) { const { job } = await getWorkspaceBuildByNumber( @@ -519,11 +519,13 @@ export const startWorkspace = ( workspaceId: string, templateVersionId: string, logLevel?: TypesGen.CreateWorkspaceBuildRequest["log_level"], + buildParameters?: TypesGen.WorkspaceBuildParameter[], ) => postWorkspaceBuild(workspaceId, { transition: "start", template_version_id: templateVersionId, log_level: logLevel, + rich_parameter_values: buildParameters, }) export const stopWorkspace = ( workspaceId: string, @@ -552,7 +554,13 @@ export const cancelWorkspaceBuild = async ( return response.data } -export const restartWorkspace = async (workspace: TypesGen.Workspace) => { +export const restartWorkspace = async ({ + workspace, + buildParameters, +}: { + workspace: TypesGen.Workspace + buildParameters?: TypesGen.WorkspaceBuildParameter[] +}) => { const stopBuild = await stopWorkspace(workspace.id) const awaitedStopBuild = await waitForBuild(stopBuild) @@ -564,6 +572,8 @@ export const restartWorkspace = async (workspace: TypesGen.Workspace) => { const startBuild = await startWorkspace( workspace.id, workspace.latest_build.template_version_id, + undefined, + buildParameters, ) await waitForBuild(startBuild) } diff --git a/site/src/components/Markdown/Markdown.tsx b/site/src/components/Markdown/Markdown.tsx index 982a2b583edd3..85261e5993a57 100644 --- a/site/src/components/Markdown/Markdown.tsx +++ b/site/src/components/Markdown/Markdown.tsx @@ -12,17 +12,21 @@ import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" import gfm from "remark-gfm" import { colors } from "theme/colors" import { darcula } from "react-syntax-highlighter/dist/cjs/styles/prism" +import { combineClasses } from "utils/combineClasses" export interface MarkdownProps { children: string } -export const Markdown: FC<{ children: string }> = ({ children }) => { +export const Markdown: FC<{ children: string; className?: string }> = ({ + children, + className, +}) => { const styles = useStyles() return ( ( diff --git a/site/src/components/RichParameterInput/RichParameterInput.stories.tsx b/site/src/components/RichParameterInput/RichParameterInput.stories.tsx index 11b7361d3ce84..2048a96f7e766 100644 --- a/site/src/components/RichParameterInput/RichParameterInput.stories.tsx +++ b/site/src/components/RichParameterInput/RichParameterInput.stories.tsx @@ -1,18 +1,14 @@ -import { Story } from "@storybook/react" import { TemplateVersionParameter } from "api/typesGenerated" -import { - RichParameterInput, - RichParameterInputProps, -} from "./RichParameterInput" +import { RichParameterInput } from "./RichParameterInput" +import type { Meta, StoryObj } from "@storybook/react" -export default { +const meta: Meta = { title: "components/RichParameterInput", component: RichParameterInput, } -const Template: Story = ( - args: RichParameterInputProps, -) => +export default meta +type Story = StoryObj const createTemplateVersionParameter = ( partial: Partial, @@ -37,154 +33,221 @@ const createTemplateVersionParameter = ( } } -export const Basic = Template.bind({}) -Basic.args = { - initialValue: "initial-value", - id: "project_name", - parameter: createTemplateVersionParameter({ - name: "project_name", - description: - "Customize the name of a Google Cloud project that will be created!", - }), -} - -export const NumberType = Template.bind({}) -NumberType.args = { - initialValue: "4", - id: "number_parameter", - parameter: createTemplateVersionParameter({ - name: "number_parameter", - type: "number", - description: "Numeric parameter", - }), -} - -export const BooleanType = Template.bind({}) -BooleanType.args = { - initialValue: "false", - id: "bool_parameter", - parameter: createTemplateVersionParameter({ - name: "bool_parameter", - type: "bool", - description: "Boolean parameter", - }), -} - -export const OptionsType = Template.bind({}) -OptionsType.args = { - initialValue: "first_option", - id: "options_parameter", - parameter: createTemplateVersionParameter({ - name: "options_parameter", - type: "string", - description: "Parameter with options", - options: [ - { - name: "First option", - value: "first_option", - description: "This is option 1", - icon: "", - }, - { - name: "Second option", - value: "second_option", - description: "This is option 2", - icon: "/icon/database.svg", - }, - { - name: "Third option", - value: "third_option", - description: "This is option 3", - icon: "/icon/aws.png", - }, - ], - }), -} - -export const ListStringType = Template.bind({}) -ListStringType.args = { - initialValue: JSON.stringify(["first", "second", "third"]), - id: "list_string_parameter", - parameter: createTemplateVersionParameter({ - name: "list_string_parameter", - type: "list(string)", - description: "List string parameter", - }), -} - -export const IconLabel = Template.bind({}) -IconLabel.args = { - initialValue: "initial-value", - id: "project_name", - parameter: createTemplateVersionParameter({ - name: "project_name", - description: - "Customize the name of a Google Cloud project that will be created!", - icon: "/emojis/1f30e.png", - }), -} - -export const NoDescription = Template.bind({}) -NoDescription.args = { - initialValue: "", - id: "region", - parameter: createTemplateVersionParameter({ - name: "Region", - description: "", - description_plaintext: "", - type: "string", - mutable: false, - default_value: "", - icon: "/emojis/1f30e.png", - options: [ - { - name: "Pittsburgh", - description: "", - value: "us-pittsburgh", - icon: "/emojis/1f1fa-1f1f8.png", - }, - { - name: "Helsinki", - description: "", - value: "eu-helsinki", - icon: "/emojis/1f1eb-1f1ee.png", - }, - { - name: "Sydney", - description: "", - value: "ap-sydney", - icon: "/emojis/1f1e6-1f1fa.png", - }, - ], - }), -} - -export const DescriptionWithLinks = Template.bind({}) -DescriptionWithLinks.args = { - initialValue: "", - id: "coder-repository-directory", - parameter: createTemplateVersionParameter({ - name: "Coder Repository Directory", - description: - "The directory specified will be created and [coder/coder](https://github.com/coder/coder) will be automatically cloned into it 🪄.", - description_plaintext: - "The directory specified will be created and coder/coder (https://github.com/coder/coder) will be automatically cloned into it 🪄.", - type: "string", - mutable: true, - default_value: "~/coder", - icon: "", - options: [], - }), -} - -export const BasicWithDisplayName = Template.bind({}) -BasicWithDisplayName.args = { - initialValue: "initial-value", - id: "project_name", - parameter: createTemplateVersionParameter({ - name: "project_name", - display_name: "Project Name", - description: - "Customize the name of a Google Cloud project that will be created!", - }), +export const Basic: Story = { + args: { + initialValue: "initial-value", + id: "project_name", + parameter: createTemplateVersionParameter({ + name: "project_name", + description: + "Customize the name of a Google Cloud project that will be created!", + }), + }, +} + +export const NumberType: Story = { + args: { + initialValue: "4", + id: "number_parameter", + parameter: createTemplateVersionParameter({ + name: "number_parameter", + type: "number", + description: "Numeric parameter", + }), + }, +} + +export const BooleanType: Story = { + args: { + initialValue: "false", + id: "bool_parameter", + parameter: createTemplateVersionParameter({ + name: "bool_parameter", + type: "bool", + description: "Boolean parameter", + }), + }, +} + +export const OptionsType: Story = { + args: { + initialValue: "first_option", + id: "options_parameter", + parameter: createTemplateVersionParameter({ + name: "options_parameter", + type: "string", + description: "Parameter with options", + options: [ + { + name: "First option", + value: "first_option", + description: "This is option 1", + icon: "", + }, + { + name: "Second option", + value: "second_option", + description: "This is option 2", + icon: "/icon/database.svg", + }, + { + name: "Third option", + value: "third_option", + description: "This is option 3", + icon: "/icon/aws.png", + }, + ], + }), + }, +} + +export const ListStringType: Story = { + args: { + initialValue: JSON.stringify(["first", "second", "third"]), + id: "list_string_parameter", + parameter: createTemplateVersionParameter({ + name: "list_string_parameter", + type: "list(string)", + description: "List string parameter", + }), + }, +} + +export const IconLabel: Story = { + args: { + initialValue: "initial-value", + id: "project_name", + parameter: createTemplateVersionParameter({ + name: "project_name", + description: + "Customize the name of a Google Cloud project that will be created!", + icon: "/emojis/1f30e.png", + }), + }, +} + +export const NoDescription: Story = { + args: { + initialValue: "", + id: "region", + parameter: createTemplateVersionParameter({ + name: "Region", + description: "", + description_plaintext: "", + type: "string", + mutable: false, + default_value: "", + icon: "/emojis/1f30e.png", + options: [ + { + name: "Pittsburgh", + description: "", + value: "us-pittsburgh", + icon: "/emojis/1f1fa-1f1f8.png", + }, + { + name: "Helsinki", + description: "", + value: "eu-helsinki", + icon: "/emojis/1f1eb-1f1ee.png", + }, + { + name: "Sydney", + description: "", + value: "ap-sydney", + icon: "/emojis/1f1e6-1f1fa.png", + }, + ], + }), + }, +} + +export const DescriptionWithLinks: Story = { + args: { + initialValue: "", + id: "coder-repository-directory", + parameter: createTemplateVersionParameter({ + name: "Coder Repository Directory", + description: + "The directory specified will be created and [coder/coder](https://github.com/coder/coder) will be automatically cloned into it 🪄.", + description_plaintext: + "The directory specified will be created and coder/coder (https://github.com/coder/coder) will be automatically cloned into it 🪄.", + type: "string", + mutable: true, + default_value: "~/coder", + icon: "", + options: [], + }), + }, +} + +export const BasicWithDisplayName: Story = { + args: { + initialValue: "initial-value", + id: "project_name", + parameter: createTemplateVersionParameter({ + name: "project_name", + display_name: "Project Name", + description: + "Customize the name of a Google Cloud project that will be created!", + }), + }, +} + +// Smaller version of the components. Used in popovers. + +export const SmallBasic: Story = { + args: { + ...Basic.args, + size: "small", + }, +} + +export const SmallNumberType: Story = { + args: { + ...NumberType.args, + size: "small", + }, +} + +export const SmallBooleanType: Story = { + args: { + ...BooleanType.args, + size: "small", + }, +} + +export const SmallOptionsType: Story = { + args: { + ...OptionsType.args, + size: "small", + }, +} + +export const SmallListStringType: Story = { + args: { + ...ListStringType.args, + size: "small", + }, +} + +export const SmallIconLabel: Story = { + args: { + ...IconLabel.args, + size: "small", + }, +} + +export const SmallNoDescription: Story = { + args: { + ...NoDescription.args, + size: "small", + }, +} + +export const SmallBasicWithDisplayName: Story = { + args: { + ...BasicWithDisplayName.args, + size: "small", + }, } diff --git a/site/src/components/RichParameterInput/RichParameterInput.tsx b/site/src/components/RichParameterInput/RichParameterInput.tsx index b1d0943bd5fd0..3dd5f3f70c4da 100644 --- a/site/src/components/RichParameterInput/RichParameterInput.tsx +++ b/site/src/components/RichParameterInput/RichParameterInput.tsx @@ -9,6 +9,8 @@ import { TemplateVersionParameter } from "../../api/typesGenerated" import { colors } from "theme/colors" import { MemoizedMarkdown } from "components/Markdown/Markdown" import { MultiTextField } from "components/MultiTextField/MultiTextField" +import Box from "@mui/material/Box" +import { Theme } from "@mui/material/styles" const isBoolean = (parameter: TemplateVersionParameter) => { return parameter.type === "bool" @@ -42,9 +44,9 @@ const ParameterLabel: FC = ({ id, parameter }) => { {hasDescription ? ( {displayName} - - {parameter.description} - + + {parameter.description} + ) : ( {displayName} @@ -54,12 +56,18 @@ const ParameterLabel: FC = ({ id, parameter }) => { ) } -export type RichParameterInputProps = Omit & { +type Size = "medium" | "small" + +export type RichParameterInputProps = Omit< + TextFieldProps, + "onChange" | "size" +> & { index: number parameter: TemplateVersionParameter onChange: (value: string) => void initialValue?: string id: string + size?: Size } export const RichParameterInput: FC = ({ @@ -68,14 +76,17 @@ export const RichParameterInput: FC = ({ onChange, parameter, initialValue, + size = "medium", ...fieldProps }) => { - const styles = useStyles() - return ( - + -
+ = ({ parameter={parameter} initialValue={initialValue} /> -
+
) } @@ -94,6 +105,7 @@ const RichParameterField: React.FC = ({ onChange, parameter, initialValue, + size, ...props }) => { const [parameterValue, setParameterValue] = useState(initialValue) @@ -102,6 +114,7 @@ const RichParameterField: React.FC = ({ if (isBoolean(parameter)) { return ( { onChange(event.target.value) @@ -126,6 +139,7 @@ const RichParameterField: React.FC = ({ if (parameter.options.length > 0) { return ( { onChange(event.target.value) @@ -192,6 +206,7 @@ const RichParameterField: React.FC = ({ return ( = ({ ) } -const optionIconSize = 20 - -const useStyles = makeStyles((theme) => ({ +const useStyles = makeStyles((theme) => ({ label: { marginBottom: theme.spacing(0.5), }, labelCaption: { fontSize: 14, color: theme.palette.text.secondary, + + ".small &": { + fontSize: 13, + lineHeight: "140%", + }, }, labelPrimary: { fontSize: 16, @@ -224,15 +242,34 @@ const useStyles = makeStyles((theme) => ({ margin: 0, lineHeight: "24px", // Keep the same as ParameterInput }, + + ".small &": { + fontSize: 14, + }, }, labelImmutable: { marginTop: theme.spacing(0.5), marginBottom: theme.spacing(0.5), color: colors.yellow[7], }, - input: { - display: "flex", - flexDirection: "column", + textField: { + ".small & .MuiInputBase-root": { + height: 36, + fontSize: 14, + borderRadius: 6, + }, + }, + radioGroup: { + ".small & .MuiFormControlLabel-label": { + fontSize: 14, + }, + ".small & .MuiRadio-root": { + padding: theme.spacing(0.75, "9px"), // 8px + 1px border + }, + ".small & .MuiRadio-root svg": { + width: 16, + height: 16, + }, }, checkbox: { display: "flex", @@ -243,6 +280,10 @@ const useStyles = makeStyles((theme) => ({ width: theme.spacing(2.5), height: theme.spacing(2.5), display: "block", + + ".small &": { + display: "none", + }, }, labelIcon: { width: "100%", @@ -255,7 +296,12 @@ const useStyles = makeStyles((theme) => ({ gap: theme.spacing(1.5), }, optionIcon: { - maxHeight: optionIconSize, - width: optionIconSize, + maxHeight: 20, + width: 20, + + ".small &": { + maxHeight: 16, + width: 16, + }, }, })) diff --git a/site/src/components/Workspace/Workspace.tsx b/site/src/components/Workspace/Workspace.tsx index 51474729a62a3..bfa4d8bbb97d5 100644 --- a/site/src/components/Workspace/Workspace.tsx +++ b/site/src/components/Workspace/Workspace.tsx @@ -45,9 +45,9 @@ export interface WorkspaceProps { maxDeadlineIncrease: number maxDeadlineDecrease: number } - handleStart: () => void + handleStart: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void handleStop: () => void - handleRestart: () => void + handleRestart: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void handleDelete: () => void handleUpdate: () => void handleCancel: () => void diff --git a/site/src/components/WorkspaceActions/BuildParametersPopover.tsx b/site/src/components/WorkspaceActions/BuildParametersPopover.tsx index 9e188043b1768..48507a571b2cf 100644 --- a/site/src/components/WorkspaceActions/BuildParametersPopover.tsx +++ b/site/src/components/WorkspaceActions/BuildParametersPopover.tsx @@ -1,18 +1,119 @@ +import ExpandMoreOutlined from "@mui/icons-material/ExpandMoreOutlined" +import Box from "@mui/material/Box" +import Button from "@mui/material/Button" +import Popover from "@mui/material/Popover" +import { useQuery } from "@tanstack/react-query" +import { getWorkspaceParameters } from "api/api" import { TemplateVersionParameter, + Workspace, WorkspaceBuildParameter, } from "api/typesGenerated" +import { FormFields } from "components/Form/Form" +import { Loader } from "components/Loader/Loader" import { RichParameterInput } from "components/RichParameterInput/RichParameterInput" +import { + HelpTooltipText, + HelpTooltipTitle, +} from "components/Tooltips/HelpTooltip/HelpTooltip" import { useFormik } from "formik" +import { useRef, useState } from "react" import { getFormHelpers } from "utils/formUtils" import { getInitialParameterValues } from "utils/richParameters" -export const BuildParametersForm = ({ +export const BuildParametersPopover = ({ + workspace, + disabled, + onSubmit, +}: { + workspace: Workspace + disabled?: boolean + onSubmit: (buildParameters: WorkspaceBuildParameter[]) => void +}) => { + const anchorRef = useRef(null) + const [isOpen, setIsOpen] = useState(false) + const { data: parameters } = useQuery({ + queryKey: ["workspace", workspace.id, "parameters"], + queryFn: () => getWorkspaceParameters(workspace), + enabled: isOpen, + }) + const ephemeralParameters = parameters + ? parameters.templateVersionRichParameters.filter((p) => p.ephemeral) + : undefined + + return ( + <> + + { + setIsOpen(false) + }} + anchorOrigin={{ + vertical: "bottom", + horizontal: "right", + }} + transformOrigin={{ + vertical: "top", + horizontal: "right", + }} + sx={{ + ".MuiPaper-root": { + width: (theme) => theme.spacing(38), + marginTop: 1, + }, + }} + > + theme.palette.text.secondary, + p: 2.5, + borderBottom: (theme) => `1px solid ${theme.palette.divider}`, + }} + > + Build Options + + These parameters only apply for a single workspace start. + + + + {parameters && parameters.buildParameters && ephemeralParameters ? ( +
{ + onSubmit(buildParameters) + setIsOpen(false) + }} + ephemeralParameters={ephemeralParameters} + buildParameters={parameters.buildParameters} + /> + ) : ( + + )} + + + + ) +} + +const Form = ({ ephemeralParameters, buildParameters, + onSubmit, }: { ephemeralParameters: TemplateVersionParameter[] buildParameters: WorkspaceBuildParameter[] + onSubmit: (buildParameters: WorkspaceBuildParameter[]) => void }) => { const form = useFormik({ initialValues: { @@ -21,29 +122,39 @@ export const BuildParametersForm = ({ buildParameters, ), }, - onSubmit: () => {}, + onSubmit: (values) => { + onSubmit(values.rich_parameter_values) + }, }) const getFieldHelpers = getFormHelpers(form) return ( - {ephemeralParameters.map((parameter, index) => { - return ( - { - await form.setFieldValue(`rich_parameter_values[${index}]`, { - name: parameter.name, - value: value, - }) - }} - /> - ) - })} + + {ephemeralParameters.map((parameter, index) => { + return ( + { + await form.setFieldValue(`rich_parameter_values[${index}]`, { + name: parameter.name, + value: value, + }) + }} + /> + ) + })} + + + + ) } diff --git a/site/src/components/WorkspaceActions/Buttons.tsx b/site/src/components/WorkspaceActions/Buttons.tsx index 27eeb76b97376..950a0d28e5281 100644 --- a/site/src/components/WorkspaceActions/Buttons.tsx +++ b/site/src/components/WorkspaceActions/Buttons.tsx @@ -5,21 +5,11 @@ import CropSquareIcon from "@mui/icons-material/CropSquare" import PlayCircleOutlineIcon from "@mui/icons-material/PlayCircleOutline" import ReplayIcon from "@mui/icons-material/Replay" import { LoadingButton } from "components/LoadingButton/LoadingButton" -import { FC, useRef, useState } from "react" +import { FC } from "react" import BlockOutlined from "@mui/icons-material/BlockOutlined" import ButtonGroup from "@mui/material/ButtonGroup" -import ExpandMoreOutlined from "@mui/icons-material/ExpandMoreOutlined" -import Popover from "@mui/material/Popover" -import { - HelpTooltipText, - HelpTooltipTitle, -} from "components/Tooltips/HelpTooltip/HelpTooltip" -import Box from "@mui/material/Box" -import { useQuery } from "@tanstack/react-query" -import { Workspace } from "api/typesGenerated" -import { getWorkspaceParameters } from "api/api" -import { BuildParametersForm } from "./BuildParametersPopover" -import { Loader } from "components/Loader/Loader" +import { Workspace, WorkspaceBuildParameter } from "api/typesGenerated" +import { BuildParametersPopover } from "./BuildParametersPopover" interface WorkspaceAction { loading?: boolean @@ -44,22 +34,12 @@ export const UpdateButton: FC = ({ ) } -export const StartButton: FC = ({ - handleAction, - workspace, - loading, -}) => { - const anchorRef = useRef(null) - const [isOpen, setIsOpen] = useState(false) - const { data: parameters } = useQuery({ - queryKey: ["workspace", workspace.id, "parameters"], - queryFn: () => getWorkspaceParameters(workspace), - enabled: isOpen, - }) - const ephemeralParameters = parameters - ? parameters.templateVersionRichParameters.filter((p) => p.ephemeral) - : undefined - +export const StartButton: FC< + WorkspaceAction & { + workspace: Workspace + handleAction: (buildParameters?: WorkspaceBuildParameter[]) => void + } +> = ({ handleAction, workspace, loading }) => { return ( = ({ > Start - - { - setIsOpen(false) - }} - anchorOrigin={{ - vertical: "bottom", - horizontal: "right", - }} - transformOrigin={{ - vertical: "top", - horizontal: "right", - }} - sx={{ - ".MuiPaper-root": { - p: 2.5, - width: (theme) => theme.spacing(38), - marginTop: 1, - }, - }} - > - theme.palette.text.secondary }}> - Build Options - - These parameters only apply for a single workspace start. - - - - {parameters && parameters.buildParameters && ephemeralParameters ? ( - - ) : ( - - )} - - + onSubmit={handleAction} + /> ) } @@ -147,21 +82,38 @@ export const StopButton: FC = ({ handleAction, loading }) => { ) } -export const RestartButton: FC = ({ - handleAction, - loading, -}) => { +export const RestartButton: FC< + WorkspaceAction & { + workspace: Workspace + handleAction: (buildParameters?: WorkspaceBuildParameter[]) => void + } +> = ({ handleAction, loading, workspace }) => { return ( - } - onClick={handleAction} - data-testid="workspace-restart-button" + button:hover + button": { + borderLeft: "1px solid #FFF", + }, + }} > - Restart - + } + onClick={handleAction} + data-testid="workspace-restart-button" + > + Restart + + + ) } diff --git a/site/src/components/WorkspaceActions/WorkspaceActions.tsx b/site/src/components/WorkspaceActions/WorkspaceActions.tsx index e06182b2b6cb2..8242b470c8c67 100644 --- a/site/src/components/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/components/WorkspaceActions/WorkspaceActions.tsx @@ -3,7 +3,7 @@ import Menu from "@mui/material/Menu" import { makeStyles } from "@mui/styles" import MoreVertOutlined from "@mui/icons-material/MoreVertOutlined" import { FC, Fragment, ReactNode, useRef, useState } from "react" -import { Workspace } from "api/typesGenerated" +import { Workspace, WorkspaceBuildParameter } from "api/typesGenerated" import { ActionLoadingButton, CancelButton, @@ -25,9 +25,9 @@ import IconButton from "@mui/material/IconButton" export interface WorkspaceActionsProps { workspace: Workspace - handleStart: () => void + handleStart: (buildParameters?: WorkspaceBuildParameter[]) => void handleStop: () => void - handleRestart: () => void + handleRestart: (buildParameters?: WorkspaceBuildParameter[]) => void handleDelete: () => void handleUpdate: () => void handleCancel: () => void @@ -41,7 +41,6 @@ export interface WorkspaceActionsProps { export const WorkspaceActions: FC = ({ workspace, - handleStart, handleStop, handleRestart, @@ -71,18 +70,24 @@ export const WorkspaceActions: FC = ({ ), [ButtonTypesEnum.start]: ( - + ), [ButtonTypesEnum.starting]: ( - + ), [ButtonTypesEnum.stop]: , [ButtonTypesEnum.stopping]: ( ), - [ButtonTypesEnum.restart]: , + [ButtonTypesEnum.restart]: ( + + ), [ButtonTypesEnum.restarting]: ( - + ), [ButtonTypesEnum.deleting]: , [ButtonTypesEnum.canceling]: , diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index 1c89b1bd9e9eb..cca7ec98b0986 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -149,12 +149,14 @@ export const WorkspaceReadyPage = ({ isUpdating={workspaceState.matches("ready.build.requestingUpdate")} isRestarting={isRestarting} workspace={workspace} - handleStart={() => workspaceSend({ type: "START" })} + handleStart={(buildParameters) => + workspaceSend({ type: "START", buildParameters }) + } handleStop={() => workspaceSend({ type: "STOP" })} handleDelete={() => workspaceSend({ type: "ASK_DELETE" })} - handleRestart={() => { + handleRestart={(buildParameters) => { if (isWarningIgnored("restart")) { - restartWorkspace(workspace) + restartWorkspace({ workspace, buildParameters }) } else { setIsConfirmingRestart(true) } @@ -258,7 +260,7 @@ export const WorkspaceReadyPage = ({ if (shouldIgnore) { ignoreWarning("restart") } - restartWorkspace(workspace) + restartWorkspace({ workspace }) setIsConfirmingRestart(false) }} onClose={() => setIsConfirmingRestart(false)} diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index 67e54d1b87eaf..b8dac17eeccf8 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -71,11 +71,13 @@ export interface WorkspaceContext { sshPrefix?: string // Change version templateVersionIdToChange?: TypesGen.TemplateVersion["id"] + // One time build parameters + oneTimeBuildParameters?: TypesGen.WorkspaceBuildParameter[] } export type WorkspaceEvent = | { type: "REFRESH_WORKSPACE"; data: TypesGen.ServerSentEvent["data"] } - | { type: "START" } + | { type: "START"; buildParameters?: TypesGen.WorkspaceBuildParameter[] } | { type: "STOP" } | { type: "ASK_DELETE" } | { type: "DELETE" } @@ -138,7 +140,7 @@ const permissionsToCheck = ( }, action: "read", }, - } as const) + }) as const export const workspaceMachine = createMachine( { @@ -626,12 +628,13 @@ export const workspaceMachine = createMachine( send({ type: "REFRESH_TIMELINE" }) return build }, - startWorkspace: (context) => async (send) => { + startWorkspace: (context, data) => async (send) => { if (context.workspace) { const startWorkspacePromise = await API.startWorkspace( context.workspace.id, context.workspace.latest_build.template_version_id, context.createBuildLogLevel, + "buildParameters" in data ? data.buildParameters : undefined, ) send({ type: "REFRESH_TIMELINE" }) return startWorkspacePromise From c4984b8b369b2909bd4efe1cbe03be8aa9220c29 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 14 Jul 2023 17:55:08 +0000 Subject: [PATCH 03/10] Add empty state --- .../BuildParametersPopover.tsx | 78 +++++++++++++------ 1 file changed, 55 insertions(+), 23 deletions(-) diff --git a/site/src/components/WorkspaceActions/BuildParametersPopover.tsx b/site/src/components/WorkspaceActions/BuildParametersPopover.tsx index 48507a571b2cf..e6a40cd91e094 100644 --- a/site/src/components/WorkspaceActions/BuildParametersPopover.tsx +++ b/site/src/components/WorkspaceActions/BuildParametersPopover.tsx @@ -13,6 +13,8 @@ import { FormFields } from "components/Form/Form" import { Loader } from "components/Loader/Loader" import { RichParameterInput } from "components/RichParameterInput/RichParameterInput" import { + HelpTooltipLink, + HelpTooltipLinksGroup, HelpTooltipText, HelpTooltipTitle, } from "components/Tooltips/HelpTooltip/HelpTooltip" @@ -75,28 +77,53 @@ export const BuildParametersPopover = ({ }, }} > - theme.palette.text.secondary, - p: 2.5, - borderBottom: (theme) => `1px solid ${theme.palette.divider}`, - }} - > - Build Options - - These parameters only apply for a single workspace start. - - - + {parameters && parameters.buildParameters && ephemeralParameters ? ( -
{ - onSubmit(buildParameters) - setIsOpen(false) - }} - ephemeralParameters={ephemeralParameters} - buildParameters={parameters.buildParameters} - /> + ephemeralParameters.length > 0 ? ( + <> + theme.palette.text.secondary, + p: 2.5, + borderBottom: (theme) => + `1px solid ${theme.palette.divider}`, + }} + > + Build Options + + These parameters only apply for a single workspace start. + + + + { + onSubmit(buildParameters) + setIsOpen(false) + }} + ephemeralParameters={ephemeralParameters} + buildParameters={parameters.buildParameters} + /> + + + ) : ( + theme.palette.text.secondary, + p: 2.5, + borderBottom: (theme) => `1px solid ${theme.palette.divider}`, + }} + > + Build Options + + This template has no ephemeral build options. + + + + Read the docs + + + + ) ) : ( )} @@ -129,7 +156,7 @@ const Form = ({ const getFieldHelpers = getFormHelpers(form) return ( - + {ephemeralParameters.map((parameter, index) => { return ( @@ -151,7 +178,12 @@ const Form = ({ })} - From 36e5c8c529e19a0a059f009ff5811a0786e47cb4 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 17 Jul 2023 17:18:49 +0000 Subject: [PATCH 04/10] Fix initial values --- site/src/components/WorkspaceActions/BuildParametersPopover.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/components/WorkspaceActions/BuildParametersPopover.tsx b/site/src/components/WorkspaceActions/BuildParametersPopover.tsx index e6a40cd91e094..02c884ac94027 100644 --- a/site/src/components/WorkspaceActions/BuildParametersPopover.tsx +++ b/site/src/components/WorkspaceActions/BuildParametersPopover.tsx @@ -164,7 +164,7 @@ const Form = ({ {...getFieldHelpers("rich_parameter_values[" + index + "].value")} key={parameter.name} parameter={parameter} - initialValue="" + initialValue={form.values.rich_parameter_values[index]?.value} index={index} size="small" onChange={async (value) => { From 1269df1a83e7fac96a8d328d7422bcf1a7b7b695 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 17 Jul 2023 17:25:59 +0000 Subject: [PATCH 05/10] Remove context value --- site/src/xServices/workspace/workspaceXService.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index b8dac17eeccf8..c3010526ed71c 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -71,8 +71,6 @@ export interface WorkspaceContext { sshPrefix?: string // Change version templateVersionIdToChange?: TypesGen.TemplateVersion["id"] - // One time build parameters - oneTimeBuildParameters?: TypesGen.WorkspaceBuildParameter[] } export type WorkspaceEvent = From f5a820b2bd0a7789cba58457b92c6660c8e44ef5 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 17 Jul 2023 17:38:33 +0000 Subject: [PATCH 06/10] Add ephemeral parameters section --- .../WorkspaceParametersForm.tsx | 88 ++++++++++++++----- 1 file changed, 66 insertions(+), 22 deletions(-) diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersForm.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersForm.tsx index ade36fc6443a4..15c38499b8e1c 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersForm.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersForm.tsx @@ -65,36 +65,80 @@ export const WorkspaceParametersForm: FC<{ form, error, ) + const hasEphemeralParameters = mutableParameters.some( + (parameter) => parameter.ephemeral, + ) + const hasNonEphemeralParameters = mutableParameters.some( + (parameter) => !parameter.ephemeral, + ) return ( - {mutableParameters.length > 0 && ( + {hasNonEphemeralParameters && ( - {mutableParameters.map((parameter, index) => ( - { - await form.setFieldValue("rich_parameter_values." + index, { - name: parameter.name, - value: value, - }) - }} - parameter={parameter} - initialValue={workspaceBuildParameterValue( - buildParameters, - parameter, - )} - /> - ))} + {mutableParameters.map((parameter, index) => + // Since we are adding the values to the form based on the index + // we can't filter them to not loose the right index position + parameter.ephemeral ? null : ( + { + await form.setFieldValue("rich_parameter_values." + index, { + name: parameter.name, + value: value, + }) + }} + parameter={parameter} + initialValue={workspaceBuildParameterValue( + buildParameters, + parameter, + )} + /> + ), + )} + + + )} + {hasEphemeralParameters && ( + + + {mutableParameters.map((parameter, index) => + // Since we are adding the values to the form based on the index + // we can't filter them to not loose the right index position + parameter.ephemeral ? ( + { + await form.setFieldValue("rich_parameter_values." + index, { + name: parameter.name, + value: value, + }) + }} + parameter={parameter} + initialValue={workspaceBuildParameterValue( + buildParameters, + parameter, + )} + /> + ) : null, + )} )} From 98fd6f8606f28feaf8e6a64e1a93f60852cecd22 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 18 Jul 2023 12:36:12 +0000 Subject: [PATCH 07/10] Don't show ephemeral parameters on create form --- .../components/WorkspaceActions/Buttons.tsx | 2 ++ .../CreateWorkspacePageView.tsx | 18 +++++++++--------- .../TemplateEmbedPage/TemplateEmbedPage.tsx | 5 ++++- site/src/utils/workspace.tsx | 4 ++++ 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/site/src/components/WorkspaceActions/Buttons.tsx b/site/src/components/WorkspaceActions/Buttons.tsx index 950a0d28e5281..9b1c7a8eeb773 100644 --- a/site/src/components/WorkspaceActions/Buttons.tsx +++ b/site/src/components/WorkspaceActions/Buttons.tsx @@ -42,6 +42,7 @@ export const StartButton: FC< > = ({ handleAction, workspace, loading }) => { return ( = ({ handleAction, loading, workspace }) => { return ( > = (props) => { + const templateParameters = props.templateParameters?.filter( + paramUsedToCreateWorkspace, + ) const initialRichParameterValues = selectInitialRichParametersValues( - props.templateParameters, + templateParameters, props.defaultParameterValues, ) const [gitAuthErrors, setGitAuthErrors] = useState>({}) @@ -72,20 +76,16 @@ export const CreateWorkspacePageView: FC< // to disappear. setGitAuthErrors({}) }, [props.templateGitAuth]) - const workspaceErrors = props.createWorkspaceErrors[CreateWorkspaceErrors.CREATE_WORKSPACE_ERROR] - // Scroll to top of page if errors are present useEffect(() => { if (props.hasTemplateErrors || Boolean(workspaceErrors)) { window.scrollTo(0, 0) } }, [props.hasTemplateErrors, workspaceErrors]) - const { t } = useTranslation("createWorkspacePage") const styles = useStyles() - const form: FormikContextType = useFormik({ initialValues: { @@ -97,7 +97,7 @@ export const CreateWorkspacePageView: FC< name: nameValidator(t("nameLabel", { ns: "createWorkspacePage" })), rich_parameter_values: useValidationSchemaForRichParameters( "createWorkspacePage", - props.templateParameters, + templateParameters, ), }), enableReinitialize: true, @@ -240,10 +240,10 @@ export const CreateWorkspacePageView: FC< )} - {props.templateParameters && ( + {templateParameters && ( <> { return { ...getFieldHelpers( @@ -264,7 +264,7 @@ export const CreateWorkspacePageView: FC< }} /> { return { diff --git a/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.tsx b/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.tsx index cc48cdf6e1816..8874bc8d2bed0 100644 --- a/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.tsx +++ b/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.tsx @@ -21,6 +21,7 @@ import { selectInitialRichParametersValues, workspaceBuildParameterValue, } from "utils/richParameters" +import { paramUsedToCreateWorkspace } from "utils/workspace" type ButtonValues = Record @@ -38,7 +39,9 @@ const TemplateEmbedPage = () => { ) diff --git a/site/src/utils/workspace.tsx b/site/src/utils/workspace.tsx index 30e3409ccd7ed..61213bf21b81f 100644 --- a/site/src/utils/workspace.tsx +++ b/site/src/utils/workspace.tsx @@ -285,3 +285,7 @@ const LoadingIcon = () => { export const hasJobError = (workspace: TypesGen.Workspace) => { return workspace.latest_build.job.error !== undefined } + +export const paramUsedToCreateWorkspace = ( + param: TypesGen.TemplateVersionParameter, +) => !param.ephemeral From 469dca27d97ed577ac0c7633a41142653bb59fb0 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 18 Jul 2023 13:01:57 +0000 Subject: [PATCH 08/10] Minor improvements --- .../TemplateVersionEditor/TemplateVersionEditor.tsx | 10 ++++++++-- site/src/components/WorkspaceActions/Buttons.tsx | 2 -- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx index 0855c7dd57250..d4a81a3363b7a 100644 --- a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx +++ b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx @@ -13,7 +13,7 @@ import { VariableValue, WorkspaceResource, } from "api/typesGenerated" -import { Alert } from "components/Alert/Alert" +import { Alert, AlertDetail } from "components/Alert/Alert" import { Avatar } from "components/Avatar/Avatar" import { AvatarData } from "components/AvatarData/AvatarData" import { bannerHeight } from "components/DeploymentBanner/DeploymentBannerView" @@ -47,6 +47,7 @@ import { TemplateVersionStatusBadge, } from "./TemplateVersionStatusBadge" import { Theme } from "@mui/material/styles" +import AlertTitle from "@mui/material/AlertTitle" export interface TemplateVersionEditorProps { template: Template @@ -374,7 +375,12 @@ export const TemplateVersionEditor: FC = ({ }`} > {templateVersion.job.error && ( - {templateVersion.job.error} +
+ + Error during the build + {templateVersion.job.error} + +
)} {buildLogs && buildLogs.length > 0 && ( diff --git a/site/src/components/WorkspaceActions/Buttons.tsx b/site/src/components/WorkspaceActions/Buttons.tsx index 9b1c7a8eeb773..950a0d28e5281 100644 --- a/site/src/components/WorkspaceActions/Buttons.tsx +++ b/site/src/components/WorkspaceActions/Buttons.tsx @@ -42,7 +42,6 @@ export const StartButton: FC< > = ({ handleAction, workspace, loading }) => { return ( = ({ handleAction, loading, workspace }) => { return ( Date: Tue, 18 Jul 2023 13:11:20 +0000 Subject: [PATCH 09/10] Fix circular json error --- site/src/components/WorkspaceActions/Buttons.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/site/src/components/WorkspaceActions/Buttons.tsx b/site/src/components/WorkspaceActions/Buttons.tsx index 950a0d28e5281..4e268c70a223d 100644 --- a/site/src/components/WorkspaceActions/Buttons.tsx +++ b/site/src/components/WorkspaceActions/Buttons.tsx @@ -35,7 +35,7 @@ export const UpdateButton: FC = ({ } export const StartButton: FC< - WorkspaceAction & { + Omit & { workspace: Workspace handleAction: (buildParameters?: WorkspaceBuildParameter[]) => void } @@ -55,7 +55,7 @@ export const StartButton: FC< loadingIndicator="Starting..." loadingPosition="start" startIcon={} - onClick={handleAction} + onClick={() => handleAction()} > Start @@ -83,7 +83,7 @@ export const StopButton: FC = ({ handleAction, loading }) => { } export const RestartButton: FC< - WorkspaceAction & { + Omit & { workspace: Workspace handleAction: (buildParameters?: WorkspaceBuildParameter[]) => void } @@ -103,7 +103,7 @@ export const RestartButton: FC< loadingIndicator="Restarting..." loadingPosition="start" startIcon={} - onClick={handleAction} + onClick={() => handleAction()} data-testid="workspace-restart-button" > Restart From 5d35955cc55ca40c4fefc29685ff83734006f9aa Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 18 Jul 2023 14:53:46 +0000 Subject: [PATCH 10/10] Always use default values for ephemeral parameters --- site/src/utils/richParameters.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/site/src/utils/richParameters.ts b/site/src/utils/richParameters.ts index 9f7bff7522cf6..f4c87eff48bd9 100644 --- a/site/src/utils/richParameters.ts +++ b/site/src/utils/richParameters.ts @@ -32,6 +32,10 @@ export const selectInitialRichParametersValues = ( return } + if (parameter.ephemeral) { + parameterValue = parameter.default_value + } + if (defaultValuesFromQuery && defaultValuesFromQuery[parameter.name]) { parameterValue = defaultValuesFromQuery[parameter.name] } @@ -191,7 +195,7 @@ export const getInitialParameterValues = ( const buildParameter = buildParameters.find( (p) => p.name === parameter.name, ) - if (!buildParameter) { + if (!buildParameter || parameter.ephemeral) { return { name: parameter.name, value: parameter.default_value,