feat: add frontend support for mandating active template version (#10338)

This commit is contained in:
Jon Ayers
2023-10-19 18:21:52 -05:00
committed by GitHub
parent f5f150d568
commit d33526108f
15 changed files with 157 additions and 31 deletions
+1
View File
@@ -12,6 +12,7 @@
"template_icon": "",
"template_allow_user_cancel_workspace_jobs": false,
"template_active_version_id": "[version ID]",
"template_require_active_version": false,
"latest_build": {
"id": "[workspace build ID]",
"created_at": "[timestamp]",
+3
View File
@@ -11125,6 +11125,9 @@ const docTemplate = `{
"template_name": {
"type": "string"
},
"template_require_active_version": {
"type": "boolean"
},
"ttl_ms": {
"type": "integer"
},
+3
View File
@@ -10091,6 +10091,9 @@
"template_name": {
"type": "string"
},
"template_require_active_version": {
"type": "boolean"
},
"ttl_ms": {
"type": "integer"
},
+1
View File
@@ -1338,6 +1338,7 @@ func convertWorkspace(
TemplateDisplayName: template.DisplayName,
TemplateAllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
TemplateActiveVersionID: template.ActiveVersionID,
TemplateRequireActiveVersion: template.RequireActiveVersion,
Outdated: workspaceBuild.TemplateVersionID.String() != template.ActiveVersionID.String(),
Name: workspace.Name,
AutostartSchedule: autostartSchedule,
+1
View File
@@ -36,6 +36,7 @@ type Workspace struct {
TemplateIcon string `json:"template_icon"`
TemplateAllowUserCancelWorkspaceJobs bool `json:"template_allow_user_cancel_workspace_jobs"`
TemplateActiveVersionID uuid.UUID `json:"template_active_version_id" format:"uuid"`
TemplateRequireActiveVersion bool `json:"template_require_active_version"`
LatestBuild WorkspaceBuild `json:"latest_build"`
Outdated bool `json:"outdated"`
Name string `json:"name"`
+3
View File
@@ -5766,6 +5766,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"template_icon": "string",
"template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc",
"template_name": "string",
"template_require_active_version": true,
"ttl_ms": 0,
"updated_at": "2019-08-24T14:15:22Z"
}
@@ -5795,6 +5796,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
| `template_icon` | string | false | | |
| `template_id` | string | false | | |
| `template_name` | string | false | | |
| `template_require_active_version` | boolean | false | | |
| `ttl_ms` | integer | false | | |
| `updated_at` | string | false | | |
@@ -7014,6 +7016,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"template_icon": "string",
"template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc",
"template_name": "string",
"template_require_active_version": true,
"ttl_ms": 0,
"updated_at": "2019-08-24T14:15:22Z"
}
+5
View File
@@ -215,6 +215,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/member
"template_icon": "string",
"template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc",
"template_name": "string",
"template_require_active_version": true,
"ttl_ms": 0,
"updated_at": "2019-08-24T14:15:22Z"
}
@@ -423,6 +424,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam
"template_icon": "string",
"template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc",
"template_name": "string",
"template_require_active_version": true,
"ttl_ms": 0,
"updated_at": "2019-08-24T14:15:22Z"
}
@@ -630,6 +632,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \
"template_icon": "string",
"template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc",
"template_name": "string",
"template_require_active_version": true,
"ttl_ms": 0,
"updated_at": "2019-08-24T14:15:22Z"
}
@@ -839,6 +842,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \
"template_icon": "string",
"template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc",
"template_name": "string",
"template_require_active_version": true,
"ttl_ms": 0,
"updated_at": "2019-08-24T14:15:22Z"
}
@@ -1163,6 +1167,7 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/dormant \
"template_icon": "string",
"template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc",
"template_name": "string",
"template_require_active_version": true,
"ttl_ms": 0,
"updated_at": "2019-08-24T14:15:22Z"
}
+1
View File
@@ -1351,6 +1351,7 @@ export interface Workspace {
readonly template_icon: string;
readonly template_allow_user_cancel_workspace_jobs: boolean;
readonly template_active_version_id: string;
readonly template_require_active_version: boolean;
readonly latest_build: WorkspaceBuild;
readonly outdated: boolean;
readonly name: string;
@@ -37,6 +37,7 @@ export const getValidationSchema = (): Yup.AnyObjectSchema =>
),
allow_user_cancel_workspace_jobs: Yup.boolean(),
icon: iconValidator,
require_active_version: Yup.boolean(),
});
export interface TemplateSettingsForm {
@@ -47,6 +48,7 @@ export interface TemplateSettingsForm {
error?: unknown;
// Helpful to show field errors on Storybook
initialTouched?: FormikTouched<UpdateTemplateMeta>;
accessControlEnabled: boolean;
}
export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
@@ -56,6 +58,7 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
error,
isSubmitting,
initialTouched,
accessControlEnabled,
}) => {
const validationSchema = getValidationSchema();
const form: FormikContextType<UpdateTemplateMeta> =
@@ -69,7 +72,7 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
template.allow_user_cancel_workspace_jobs,
update_workspace_last_used_at: false,
update_workspace_dormant_at: false,
require_active_version: false,
require_active_version: template.require_active_version,
},
validationSchema,
onSubmit,
@@ -135,38 +138,72 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
title="Operations"
description="Regulate actions allowed on workspaces created from this template."
>
<label htmlFor="allow_user_cancel_workspace_jobs">
<Stack direction="row" spacing={1}>
<Checkbox
id="allow_user_cancel_workspace_jobs"
name="allow_user_cancel_workspace_jobs"
disabled={isSubmitting}
checked={form.values.allow_user_cancel_workspace_jobs}
onChange={form.handleChange}
/>
<Stack direction="column" spacing={5}>
<label htmlFor="allow_user_cancel_workspace_jobs">
<Stack direction="row" spacing={1}>
<Checkbox
id="allow_user_cancel_workspace_jobs"
name="allow_user_cancel_workspace_jobs"
disabled={isSubmitting}
checked={form.values.allow_user_cancel_workspace_jobs}
onChange={form.handleChange}
/>
<Stack direction="column" spacing={0.5}>
<Stack
direction="row"
alignItems="center"
spacing={0.5}
className={styles.optionText}
>
Allow users to cancel in-progress workspace jobs.
<HelpTooltip>
<HelpTooltipText>
If checked, users may be able to corrupt their workspace.
</HelpTooltipText>
</HelpTooltip>
<Stack direction="column" spacing={0.5}>
<Stack
direction="row"
alignItems="center"
spacing={0.5}
className={styles.optionText}
>
Allow users to cancel in-progress workspace jobs.
<HelpTooltip>
<HelpTooltipText>
If checked, users may be able to corrupt their workspace.
</HelpTooltipText>
</HelpTooltip>
</Stack>
<span className={styles.optionHelperText}>
Depending on your template, canceling builds may leave
workspaces in an unhealthy state. This option isn&apos;t
recommended for most use cases.
</span>
</Stack>
<span className={styles.optionHelperText}>
Depending on your template, canceling builds may leave
workspaces in an unhealthy state. This option isn&apos;t
recommended for most use cases.
</span>
</Stack>
</Stack>
</label>
</label>
{accessControlEnabled && (
<label htmlFor="require_active_version">
<Stack direction="row" spacing={1}>
<Checkbox
id="require_active_version"
name="require_active_version"
checked={form.values.require_active_version}
onChange={form.handleChange}
/>
<Stack direction="column" spacing={0.5}>
<Stack
direction="row"
alignItems="center"
spacing={0.5}
className={styles.optionText}
>
Require the active template version for workspace builds.
<HelpTooltip>
<HelpTooltipText>
This setting is not enforced for template admins.
</HelpTooltipText>
</HelpTooltip>
</Stack>
<span className={styles.optionHelperText}>
Workspaces that are manually started or auto-started will
use the promoted template version.
</span>
</Stack>
</Stack>
</label>
)}
</Stack>
</FormSection>
<FormFooter onCancel={onCancel} isLoading={isSubmitting} />
@@ -10,6 +10,7 @@ import { useTemplateSettings } from "../TemplateSettingsLayout";
import { TemplateSettingsPageView } from "./TemplateSettingsPageView";
import { templateByNameKey } from "api/queries/templates";
import { useOrganizationId } from "hooks";
import { useDashboard } from "components/Dashboard/DashboardProvider";
export const TemplateSettingsPage: FC = () => {
const { template: templateName } = useParams() as { template: string };
@@ -17,6 +18,11 @@ export const TemplateSettingsPage: FC = () => {
const orgId = useOrganizationId();
const { template } = useTemplateSettings();
const queryClient = useQueryClient();
const { entitlements, experiments } = useDashboard();
const accessControlEnabled =
entitlements.features["advanced_template_scheduling"].enabled &&
experiments.includes("template_update_policies");
const {
mutate: updateTemplate,
isLoading: isSubmitting,
@@ -51,6 +57,7 @@ export const TemplateSettingsPage: FC = () => {
...templateSettings,
});
}}
accessControlEnabled={accessControlEnabled}
/>
</>
);
@@ -12,6 +12,7 @@ export interface TemplateSettingsPageViewProps {
initialTouched?: ComponentProps<
typeof TemplateSettingsForm
>["initialTouched"];
accessControlEnabled: boolean;
}
export const TemplateSettingsPageView: FC<TemplateSettingsPageViewProps> = ({
@@ -21,6 +22,7 @@ export const TemplateSettingsPageView: FC<TemplateSettingsPageViewProps> = ({
isSubmitting,
submitError,
initialTouched,
accessControlEnabled,
}) => {
return (
<>
@@ -35,6 +37,7 @@ export const TemplateSettingsPageView: FC<TemplateSettingsPageViewProps> = ({
onSubmit={onSubmit}
onCancel={onCancel}
error={submitError}
accessControlEnabled={accessControlEnabled}
/>
</>
);
@@ -79,3 +79,17 @@ export const Updating: Story = {
workspace: Mocks.MockOutdatedWorkspace,
},
};
export const RequireActiveVersionStarted: Story = {
args: {
workspace: Mocks.MockOutdatedRunningWorkspaceRequireActiveVersion,
canChangeVersions: false,
},
};
export const RequireActiveVersionStopped: Story = {
args: {
workspace: Mocks.MockOutdatedStoppedWorkspaceRequireActiveVersion,
canChangeVersions: false,
},
};
@@ -61,7 +61,11 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
canCancel,
canAcceptJobs,
actions: actionsByStatus,
} = actionsByWorkspaceStatus(workspace, workspace.latest_build.status);
} = actionsByWorkspaceStatus(
workspace,
workspace.latest_build.status,
canChangeVersions,
);
const canBeUpdated = workspace.outdated && canAcceptJobs;
const menuTriggerRef = useRef<HTMLButtonElement>(null);
const [isMenuOpen, setIsMenuOpen] = useState(false);
@@ -33,6 +33,7 @@ interface WorkspaceAbilities {
export const actionsByWorkspaceStatus = (
workspace: Workspace,
status: WorkspaceStatus,
canChangeVersions: boolean,
): WorkspaceAbilities => {
if (workspace.dormant_at) {
return {
@@ -41,6 +42,26 @@ export const actionsByWorkspaceStatus = (
canAcceptJobs: false,
};
}
if (
workspace.template_require_active_version &&
workspace.outdated &&
!canChangeVersions
) {
if (status === "running") {
return {
actions: [ButtonTypesEnum.stop],
canCancel: false,
canAcceptJobs: true,
};
}
if (status === "stopped") {
return {
actions: [],
canCancel: false,
canAcceptJobs: true,
};
}
}
return statusToActions[status];
};
+22
View File
@@ -960,6 +960,7 @@ export const MockWorkspace: TypesGen.Workspace = {
template_allow_user_cancel_workspace_jobs:
MockTemplate.allow_user_cancel_workspace_jobs,
template_active_version_id: MockTemplate.active_version_id,
template_require_active_version: MockTemplate.require_active_version,
outdated: false,
owner_id: MockUser.id,
organization_id: MockOrganization.id,
@@ -1053,6 +1054,27 @@ export const MockOutdatedWorkspace: TypesGen.Workspace = {
outdated: true,
};
export const MockOutdatedRunningWorkspaceRequireActiveVersion: TypesGen.Workspace =
{
...MockWorkspace,
id: "test-outdated-workspace-require-active-version",
outdated: true,
template_require_active_version: true,
latest_build: {
...MockWorkspaceBuild,
status: "running",
},
};
export const MockOutdatedStoppedWorkspaceRequireActiveVersion: TypesGen.Workspace =
{
...MockOutdatedRunningWorkspaceRequireActiveVersion,
latest_build: {
...MockWorkspaceBuild,
status: "stopped",
},
};
export const MockPendingWorkspace: TypesGen.Workspace = {
...MockWorkspace,
id: "test-pending-workspace",