Skip to content

Commit d335261

Browse files
authored
feat: add frontend support for mandating active template version (#10338)
1 parent f5f150d commit d335261

File tree

15 files changed

+157
-31
lines changed

15 files changed

+157
-31
lines changed

cli/testdata/coder_list_--output_json.golden

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"template_icon": "",
1313
"template_allow_user_cancel_workspace_jobs": false,
1414
"template_active_version_id": "[version ID]",
15+
"template_require_active_version": false,
1516
"latest_build": {
1617
"id": "[workspace build ID]",
1718
"created_at": "[timestamp]",

coderd/apidoc/docs.go

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/workspaces.go

+1
Original file line numberDiff line numberDiff line change
@@ -1338,6 +1338,7 @@ func convertWorkspace(
13381338
TemplateDisplayName: template.DisplayName,
13391339
TemplateAllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
13401340
TemplateActiveVersionID: template.ActiveVersionID,
1341+
TemplateRequireActiveVersion: template.RequireActiveVersion,
13411342
Outdated: workspaceBuild.TemplateVersionID.String() != template.ActiveVersionID.String(),
13421343
Name: workspace.Name,
13431344
AutostartSchedule: autostartSchedule,

codersdk/workspaces.go

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ type Workspace struct {
3636
TemplateIcon string `json:"template_icon"`
3737
TemplateAllowUserCancelWorkspaceJobs bool `json:"template_allow_user_cancel_workspace_jobs"`
3838
TemplateActiveVersionID uuid.UUID `json:"template_active_version_id" format:"uuid"`
39+
TemplateRequireActiveVersion bool `json:"template_require_active_version"`
3940
LatestBuild WorkspaceBuild `json:"latest_build"`
4041
Outdated bool `json:"outdated"`
4142
Name string `json:"name"`

docs/api/schemas.md

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/api/workspaces.md

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

site/src/api/typesGenerated.ts

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx

+67-30
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export const getValidationSchema = (): Yup.AnyObjectSchema =>
3737
),
3838
allow_user_cancel_workspace_jobs: Yup.boolean(),
3939
icon: iconValidator,
40+
require_active_version: Yup.boolean(),
4041
});
4142

4243
export interface TemplateSettingsForm {
@@ -47,6 +48,7 @@ export interface TemplateSettingsForm {
4748
error?: unknown;
4849
// Helpful to show field errors on Storybook
4950
initialTouched?: FormikTouched<UpdateTemplateMeta>;
51+
accessControlEnabled: boolean;
5052
}
5153

5254
export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
@@ -56,6 +58,7 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
5658
error,
5759
isSubmitting,
5860
initialTouched,
61+
accessControlEnabled,
5962
}) => {
6063
const validationSchema = getValidationSchema();
6164
const form: FormikContextType<UpdateTemplateMeta> =
@@ -69,7 +72,7 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
6972
template.allow_user_cancel_workspace_jobs,
7073
update_workspace_last_used_at: false,
7174
update_workspace_dormant_at: false,
72-
require_active_version: false,
75+
require_active_version: template.require_active_version,
7376
},
7477
validationSchema,
7578
onSubmit,
@@ -135,38 +138,72 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
135138
title="Operations"
136139
description="Regulate actions allowed on workspaces created from this template."
137140
>
138-
<label htmlFor="allow_user_cancel_workspace_jobs">
139-
<Stack direction="row" spacing={1}>
140-
<Checkbox
141-
id="allow_user_cancel_workspace_jobs"
142-
name="allow_user_cancel_workspace_jobs"
143-
disabled={isSubmitting}
144-
checked={form.values.allow_user_cancel_workspace_jobs}
145-
onChange={form.handleChange}
146-
/>
141+
<Stack direction="column" spacing={5}>
142+
<label htmlFor="allow_user_cancel_workspace_jobs">
143+
<Stack direction="row" spacing={1}>
144+
<Checkbox
145+
id="allow_user_cancel_workspace_jobs"
146+
name="allow_user_cancel_workspace_jobs"
147+
disabled={isSubmitting}
148+
checked={form.values.allow_user_cancel_workspace_jobs}
149+
onChange={form.handleChange}
150+
/>
147151

148-
<Stack direction="column" spacing={0.5}>
149-
<Stack
150-
direction="row"
151-
alignItems="center"
152-
spacing={0.5}
153-
className={styles.optionText}
154-
>
155-
Allow users to cancel in-progress workspace jobs.
156-
<HelpTooltip>
157-
<HelpTooltipText>
158-
If checked, users may be able to corrupt their workspace.
159-
</HelpTooltipText>
160-
</HelpTooltip>
152+
<Stack direction="column" spacing={0.5}>
153+
<Stack
154+
direction="row"
155+
alignItems="center"
156+
spacing={0.5}
157+
className={styles.optionText}
158+
>
159+
Allow users to cancel in-progress workspace jobs.
160+
<HelpTooltip>
161+
<HelpTooltipText>
162+
If checked, users may be able to corrupt their workspace.
163+
</HelpTooltipText>
164+
</HelpTooltip>
165+
</Stack>
166+
<span className={styles.optionHelperText}>
167+
Depending on your template, canceling builds may leave
168+
workspaces in an unhealthy state. This option isn&apos;t
169+
recommended for most use cases.
170+
</span>
161171
</Stack>
162-
<span className={styles.optionHelperText}>
163-
Depending on your template, canceling builds may leave
164-
workspaces in an unhealthy state. This option isn&apos;t
165-
recommended for most use cases.
166-
</span>
167172
</Stack>
168-
</Stack>
169-
</label>
173+
</label>
174+
{accessControlEnabled && (
175+
<label htmlFor="require_active_version">
176+
<Stack direction="row" spacing={1}>
177+
<Checkbox
178+
id="require_active_version"
179+
name="require_active_version"
180+
checked={form.values.require_active_version}
181+
onChange={form.handleChange}
182+
/>
183+
184+
<Stack direction="column" spacing={0.5}>
185+
<Stack
186+
direction="row"
187+
alignItems="center"
188+
spacing={0.5}
189+
className={styles.optionText}
190+
>
191+
Require the active template version for workspace builds.
192+
<HelpTooltip>
193+
<HelpTooltipText>
194+
This setting is not enforced for template admins.
195+
</HelpTooltipText>
196+
</HelpTooltip>
197+
</Stack>
198+
<span className={styles.optionHelperText}>
199+
Workspaces that are manually started or auto-started will
200+
use the promoted template version.
201+
</span>
202+
</Stack>
203+
</Stack>
204+
</label>
205+
)}
206+
</Stack>
170207
</FormSection>
171208

172209
<FormFooter onCancel={onCancel} isLoading={isSubmitting} />

site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,19 @@ import { useTemplateSettings } from "../TemplateSettingsLayout";
1010
import { TemplateSettingsPageView } from "./TemplateSettingsPageView";
1111
import { templateByNameKey } from "api/queries/templates";
1212
import { useOrganizationId } from "hooks";
13+
import { useDashboard } from "components/Dashboard/DashboardProvider";
1314

1415
export const TemplateSettingsPage: FC = () => {
1516
const { template: templateName } = useParams() as { template: string };
1617
const navigate = useNavigate();
1718
const orgId = useOrganizationId();
1819
const { template } = useTemplateSettings();
1920
const queryClient = useQueryClient();
21+
const { entitlements, experiments } = useDashboard();
22+
const accessControlEnabled =
23+
entitlements.features["advanced_template_scheduling"].enabled &&
24+
experiments.includes("template_update_policies");
25+
2026
const {
2127
mutate: updateTemplate,
2228
isLoading: isSubmitting,
@@ -51,6 +57,7 @@ export const TemplateSettingsPage: FC = () => {
5157
...templateSettings,
5258
});
5359
}}
60+
accessControlEnabled={accessControlEnabled}
5461
/>
5562
</>
5663
);

site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export interface TemplateSettingsPageViewProps {
1212
initialTouched?: ComponentProps<
1313
typeof TemplateSettingsForm
1414
>["initialTouched"];
15+
accessControlEnabled: boolean;
1516
}
1617

1718
export const TemplateSettingsPageView: FC<TemplateSettingsPageViewProps> = ({
@@ -21,6 +22,7 @@ export const TemplateSettingsPageView: FC<TemplateSettingsPageViewProps> = ({
2122
isSubmitting,
2223
submitError,
2324
initialTouched,
25+
accessControlEnabled,
2426
}) => {
2527
return (
2628
<>
@@ -35,6 +37,7 @@ export const TemplateSettingsPageView: FC<TemplateSettingsPageViewProps> = ({
3537
onSubmit={onSubmit}
3638
onCancel={onCancel}
3739
error={submitError}
40+
accessControlEnabled={accessControlEnabled}
3841
/>
3942
</>
4043
);

site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.stories.tsx

+14
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,17 @@ export const Updating: Story = {
7979
workspace: Mocks.MockOutdatedWorkspace,
8080
},
8181
};
82+
83+
export const RequireActiveVersionStarted: Story = {
84+
args: {
85+
workspace: Mocks.MockOutdatedRunningWorkspaceRequireActiveVersion,
86+
canChangeVersions: false,
87+
},
88+
};
89+
90+
export const RequireActiveVersionStopped: Story = {
91+
args: {
92+
workspace: Mocks.MockOutdatedStoppedWorkspaceRequireActiveVersion,
93+
canChangeVersions: false,
94+
},
95+
};

site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,11 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
6161
canCancel,
6262
canAcceptJobs,
6363
actions: actionsByStatus,
64-
} = actionsByWorkspaceStatus(workspace, workspace.latest_build.status);
64+
} = actionsByWorkspaceStatus(
65+
workspace,
66+
workspace.latest_build.status,
67+
canChangeVersions,
68+
);
6569
const canBeUpdated = workspace.outdated && canAcceptJobs;
6670
const menuTriggerRef = useRef<HTMLButtonElement>(null);
6771
const [isMenuOpen, setIsMenuOpen] = useState(false);

site/src/pages/WorkspacePage/WorkspaceActions/constants.ts

+21
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ interface WorkspaceAbilities {
3333
export const actionsByWorkspaceStatus = (
3434
workspace: Workspace,
3535
status: WorkspaceStatus,
36+
canChangeVersions: boolean,
3637
): WorkspaceAbilities => {
3738
if (workspace.dormant_at) {
3839
return {
@@ -41,6 +42,26 @@ export const actionsByWorkspaceStatus = (
4142
canAcceptJobs: false,
4243
};
4344
}
45+
if (
46+
workspace.template_require_active_version &&
47+
workspace.outdated &&
48+
!canChangeVersions
49+
) {
50+
if (status === "running") {
51+
return {
52+
actions: [ButtonTypesEnum.stop],
53+
canCancel: false,
54+
canAcceptJobs: true,
55+
};
56+
}
57+
if (status === "stopped") {
58+
return {
59+
actions: [],
60+
canCancel: false,
61+
canAcceptJobs: true,
62+
};
63+
}
64+
}
4465
return statusToActions[status];
4566
};
4667

0 commit comments

Comments
 (0)