mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix: prevent editing build parameters if template requires active version (#11117)
Co-authored-by: McKayla Washburn <mckayla@hey.com>
This commit is contained in:
co-authored by
McKayla Washburn
parent
b7ea330aea
commit
ba3b835339
+129
-97
@@ -1,3 +1,7 @@
|
||||
import { useFormik } from "formik";
|
||||
import { type FC } from "react";
|
||||
import * as Yup from "yup";
|
||||
import { Alert } from "components/Alert/Alert";
|
||||
import {
|
||||
FormFields,
|
||||
FormFooter,
|
||||
@@ -5,16 +9,14 @@ import {
|
||||
HorizontalForm,
|
||||
} from "components/Form/Form";
|
||||
import { RichParameterInput } from "components/RichParameterInput/RichParameterInput";
|
||||
import { useFormik } from "formik";
|
||||
import { FC } from "react";
|
||||
import {
|
||||
getInitialRichParameterValues,
|
||||
useValidationSchemaForRichParameters,
|
||||
} from "utils/richParameters";
|
||||
import * as Yup from "yup";
|
||||
import { getFormHelpers } from "utils/formUtils";
|
||||
import {
|
||||
import type {
|
||||
TemplateVersionParameter,
|
||||
Workspace,
|
||||
WorkspaceBuildParameter,
|
||||
} from "api/typesGenerated";
|
||||
|
||||
@@ -22,19 +24,25 @@ export type WorkspaceParametersFormValues = {
|
||||
rich_parameter_values: WorkspaceBuildParameter[];
|
||||
};
|
||||
|
||||
export const WorkspaceParametersForm: FC<{
|
||||
isSubmitting: boolean;
|
||||
interface WorkspaceParameterFormProps {
|
||||
workspace: Workspace;
|
||||
templateVersionRichParameters: TemplateVersionParameter[];
|
||||
buildParameters: WorkspaceBuildParameter[];
|
||||
isSubmitting: boolean;
|
||||
canChangeVersions: boolean;
|
||||
error: unknown;
|
||||
onCancel: () => void;
|
||||
onSubmit: (values: WorkspaceParametersFormValues) => void;
|
||||
}> = ({
|
||||
}
|
||||
|
||||
export const WorkspaceParametersForm: FC<WorkspaceParameterFormProps> = ({
|
||||
workspace,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
templateVersionRichParameters,
|
||||
buildParameters,
|
||||
error,
|
||||
canChangeVersions,
|
||||
isSubmitting,
|
||||
}) => {
|
||||
const form = useFormik<WorkspaceParametersFormValues>({
|
||||
@@ -65,97 +73,121 @@ export const WorkspaceParametersForm: FC<{
|
||||
(parameter) => !parameter.mutable,
|
||||
);
|
||||
|
||||
const disabled =
|
||||
workspace.outdated &&
|
||||
workspace.template_require_active_version &&
|
||||
!canChangeVersions;
|
||||
|
||||
return (
|
||||
<HorizontalForm onSubmit={form.handleSubmit} data-testid="form">
|
||||
{hasNonEphemeralParameters && (
|
||||
<FormSection
|
||||
title="Parameters"
|
||||
description="Settings used by your template"
|
||||
>
|
||||
<FormFields>
|
||||
{templateVersionRichParameters.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.mutable && !parameter.ephemeral ? (
|
||||
<RichParameterInput
|
||||
{...getFieldHelpers(
|
||||
"rich_parameter_values[" + index + "].value",
|
||||
)}
|
||||
disabled={isSubmitting}
|
||||
key={parameter.name}
|
||||
onChange={async (value) => {
|
||||
await form.setFieldValue("rich_parameter_values." + index, {
|
||||
name: parameter.name,
|
||||
value: value,
|
||||
});
|
||||
}}
|
||||
parameter={parameter}
|
||||
/>
|
||||
) : null,
|
||||
)}
|
||||
</FormFields>
|
||||
</FormSection>
|
||||
<>
|
||||
{disabled && (
|
||||
<Alert severity="warning" css={{ marginBottom: 48 }}>
|
||||
The template for this workspace requires automatic updates. Update the
|
||||
workspace to edit parameters.
|
||||
</Alert>
|
||||
)}
|
||||
{hasEphemeralParameters && (
|
||||
<FormSection
|
||||
title="Ephemeral Parameters"
|
||||
description="These parameters only apply for a single workspace start."
|
||||
>
|
||||
<FormFields>
|
||||
{templateVersionRichParameters.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.mutable && parameter.ephemeral ? (
|
||||
<RichParameterInput
|
||||
{...getFieldHelpers(
|
||||
"rich_parameter_values[" + index + "].value",
|
||||
)}
|
||||
disabled={isSubmitting}
|
||||
key={parameter.name}
|
||||
onChange={async (value) => {
|
||||
await form.setFieldValue("rich_parameter_values." + index, {
|
||||
name: parameter.name,
|
||||
value: value,
|
||||
});
|
||||
}}
|
||||
parameter={parameter}
|
||||
/>
|
||||
) : null,
|
||||
)}
|
||||
</FormFields>
|
||||
</FormSection>
|
||||
)}
|
||||
{/* They are displayed here only for visibility purposes */}
|
||||
{hasImmutableParameters && (
|
||||
<FormSection
|
||||
title="Immutable parameters"
|
||||
description={
|
||||
<>
|
||||
These settings <strong>cannot be changed</strong> after creating
|
||||
the workspace.
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FormFields>
|
||||
{templateVersionRichParameters.map((parameter, index) =>
|
||||
!parameter.mutable ? (
|
||||
<RichParameterInput
|
||||
disabled
|
||||
{...getFieldHelpers(
|
||||
"rich_parameter_values[" + index + "].value",
|
||||
)}
|
||||
key={parameter.name}
|
||||
parameter={parameter}
|
||||
onChange={() => {
|
||||
throw new Error("Immutable parameters cannot be changed");
|
||||
}}
|
||||
/>
|
||||
) : null,
|
||||
)}
|
||||
</FormFields>
|
||||
</FormSection>
|
||||
)}
|
||||
<FormFooter onCancel={onCancel} isLoading={isSubmitting} />
|
||||
</HorizontalForm>
|
||||
|
||||
<HorizontalForm onSubmit={form.handleSubmit} data-testid="form">
|
||||
{hasNonEphemeralParameters && (
|
||||
<FormSection
|
||||
title="Parameters"
|
||||
description="Settings used by your template"
|
||||
>
|
||||
<FormFields>
|
||||
{templateVersionRichParameters.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.mutable && !parameter.ephemeral ? (
|
||||
<RichParameterInput
|
||||
{...getFieldHelpers(
|
||||
"rich_parameter_values[" + index + "].value",
|
||||
)}
|
||||
disabled={isSubmitting || disabled}
|
||||
key={parameter.name}
|
||||
onChange={async (value) => {
|
||||
await form.setFieldValue(
|
||||
"rich_parameter_values." + index,
|
||||
{
|
||||
name: parameter.name,
|
||||
value: value,
|
||||
},
|
||||
);
|
||||
}}
|
||||
parameter={parameter}
|
||||
/>
|
||||
) : null,
|
||||
)}
|
||||
</FormFields>
|
||||
</FormSection>
|
||||
)}
|
||||
{hasEphemeralParameters && (
|
||||
<FormSection
|
||||
title="Ephemeral Parameters"
|
||||
description="These parameters only apply for a single workspace start."
|
||||
>
|
||||
<FormFields>
|
||||
{templateVersionRichParameters.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.mutable && parameter.ephemeral ? (
|
||||
<RichParameterInput
|
||||
{...getFieldHelpers(
|
||||
"rich_parameter_values[" + index + "].value",
|
||||
)}
|
||||
disabled={isSubmitting || disabled}
|
||||
key={parameter.name}
|
||||
onChange={async (value) => {
|
||||
await form.setFieldValue(
|
||||
"rich_parameter_values." + index,
|
||||
{
|
||||
name: parameter.name,
|
||||
value: value,
|
||||
},
|
||||
);
|
||||
}}
|
||||
parameter={parameter}
|
||||
/>
|
||||
) : null,
|
||||
)}
|
||||
</FormFields>
|
||||
</FormSection>
|
||||
)}
|
||||
{/* They are displayed here only for visibility purposes */}
|
||||
{hasImmutableParameters && (
|
||||
<FormSection
|
||||
title="Immutable parameters"
|
||||
description={
|
||||
<>
|
||||
These settings <strong>cannot be changed</strong> after creating
|
||||
the workspace.
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FormFields>
|
||||
{templateVersionRichParameters.map((parameter, index) =>
|
||||
!parameter.mutable ? (
|
||||
<RichParameterInput
|
||||
disabled
|
||||
{...getFieldHelpers(
|
||||
"rich_parameter_values[" + index + "].value",
|
||||
)}
|
||||
key={parameter.name}
|
||||
parameter={parameter}
|
||||
onChange={() => {
|
||||
throw new Error("Immutable parameters cannot be changed");
|
||||
}}
|
||||
/>
|
||||
) : null,
|
||||
)}
|
||||
</FormFields>
|
||||
</FormSection>
|
||||
)}
|
||||
<FormFooter
|
||||
onCancel={onCancel}
|
||||
isLoading={isSubmitting}
|
||||
submitDisabled={disabled}
|
||||
/>
|
||||
</HorizontalForm>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+48
@@ -7,6 +7,8 @@ import {
|
||||
MockTemplateVersionParameter2,
|
||||
MockTemplateVersionParameter3,
|
||||
MockWorkspaceBuildParameter3,
|
||||
MockWorkspace,
|
||||
MockOutdatedStoppedWorkspaceRequireActiveVersion,
|
||||
} from "testHelpers/entities";
|
||||
|
||||
const meta: Meta<typeof WorkspaceParametersPageView> = {
|
||||
@@ -15,6 +17,8 @@ const meta: Meta<typeof WorkspaceParametersPageView> = {
|
||||
args: {
|
||||
submitError: undefined,
|
||||
isSubmitting: false,
|
||||
workspace: MockWorkspace,
|
||||
canChangeVersions: true,
|
||||
|
||||
data: {
|
||||
buildParameters: [
|
||||
@@ -48,4 +52,48 @@ export const Empty: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const RequireActiveVersionNoChangeVersion: Story = {
|
||||
args: {
|
||||
workspace: MockOutdatedStoppedWorkspaceRequireActiveVersion,
|
||||
canChangeVersions: false,
|
||||
data: {
|
||||
buildParameters: [
|
||||
MockWorkspaceBuildParameter1,
|
||||
MockWorkspaceBuildParameter2,
|
||||
MockWorkspaceBuildParameter3,
|
||||
],
|
||||
templateVersionRichParameters: [
|
||||
MockTemplateVersionParameter1,
|
||||
MockTemplateVersionParameter2,
|
||||
{
|
||||
...MockTemplateVersionParameter3,
|
||||
mutable: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const RequireActiveVersionCanChangeVersion: Story = {
|
||||
args: {
|
||||
workspace: MockOutdatedStoppedWorkspaceRequireActiveVersion,
|
||||
canChangeVersions: true,
|
||||
data: {
|
||||
buildParameters: [
|
||||
MockWorkspaceBuildParameter1,
|
||||
MockWorkspaceBuildParameter2,
|
||||
MockWorkspaceBuildParameter3,
|
||||
],
|
||||
templateVersionRichParameters: [
|
||||
MockTemplateVersionParameter1,
|
||||
MockTemplateVersionParameter2,
|
||||
{
|
||||
...MockTemplateVersionParameter3,
|
||||
mutable: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export { Example as WorkspaceParametersPage };
|
||||
|
||||
+38
-2
@@ -1,7 +1,13 @@
|
||||
import { getWorkspaceParameters, postWorkspaceBuild } from "api/api";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { pageTitle } from "utils/page";
|
||||
import {
|
||||
WorkspacePermissions,
|
||||
workspaceChecks,
|
||||
} from "../../WorkspacePage/permissions";
|
||||
import { checkAuthorization } from "api/queries/authCheck";
|
||||
import { useWorkspaceSettings } from "../WorkspaceSettingsLayout";
|
||||
import { templateByName } from "api/queries/templates";
|
||||
import { useMutation, useQuery } from "react-query";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import {
|
||||
@@ -13,7 +19,7 @@ import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader";
|
||||
import { type FC } from "react";
|
||||
import { isApiValidationError } from "api/errors";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { WorkspaceBuildParameter } from "api/typesGenerated";
|
||||
import { Workspace, WorkspaceBuildParameter } from "api/typesGenerated";
|
||||
import { EmptyState } from "components/EmptyState/EmptyState";
|
||||
import Button from "@mui/material/Button";
|
||||
import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined";
|
||||
@@ -37,6 +43,22 @@ const WorkspaceParametersPage: FC = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const templateQuery = useQuery({
|
||||
...templateByName(workspace.organization_id, workspace.template_name ?? ""),
|
||||
enabled: workspace !== undefined,
|
||||
});
|
||||
const template = templateQuery.data;
|
||||
|
||||
// Permissions
|
||||
const checks =
|
||||
workspace && template ? workspaceChecks(workspace, template) : {};
|
||||
const permissionsQuery = useQuery({
|
||||
...checkAuthorization({ checks }),
|
||||
enabled: workspace !== undefined && template !== undefined,
|
||||
});
|
||||
const permissions = permissionsQuery.data as WorkspacePermissions | undefined;
|
||||
const canChangeVersions = Boolean(permissions?.updateTemplate);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
@@ -44,6 +66,8 @@ const WorkspaceParametersPage: FC = () => {
|
||||
</Helmet>
|
||||
|
||||
<WorkspaceParametersPageView
|
||||
workspace={workspace}
|
||||
canChangeVersions={canChangeVersions}
|
||||
data={parameters.data}
|
||||
submitError={updateParameters.error}
|
||||
isSubmitting={updateParameters.isLoading}
|
||||
@@ -67,6 +91,8 @@ const WorkspaceParametersPage: FC = () => {
|
||||
};
|
||||
|
||||
export type WorkspaceParametersPageViewProps = {
|
||||
workspace: Workspace;
|
||||
canChangeVersions: boolean;
|
||||
data: Awaited<ReturnType<typeof getWorkspaceParameters>> | undefined;
|
||||
submitError: unknown;
|
||||
isSubmitting: boolean;
|
||||
@@ -76,7 +102,15 @@ export type WorkspaceParametersPageViewProps = {
|
||||
|
||||
export const WorkspaceParametersPageView: FC<
|
||||
WorkspaceParametersPageViewProps
|
||||
> = ({ data, submitError, isSubmitting, onSubmit, onCancel }) => {
|
||||
> = ({
|
||||
workspace,
|
||||
canChangeVersions,
|
||||
data,
|
||||
submitError,
|
||||
onSubmit,
|
||||
isSubmitting,
|
||||
onCancel,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<PageHeader css={{ paddingTop: 0 }}>
|
||||
@@ -90,6 +124,8 @@ export const WorkspaceParametersPageView: FC<
|
||||
{data ? (
|
||||
data.templateVersionRichParameters.length > 0 ? (
|
||||
<WorkspaceParametersForm
|
||||
workspace={workspace}
|
||||
canChangeVersions={canChangeVersions}
|
||||
buildParameters={data.buildParameters}
|
||||
templateVersionRichParameters={data.templateVersionRichParameters}
|
||||
error={submitError}
|
||||
|
||||
Reference in New Issue
Block a user