mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add frontend support for mandating active template version (#10338)
This commit is contained in:
@@ -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]",
|
||||
|
||||
Generated
+3
@@ -11125,6 +11125,9 @@ const docTemplate = `{
|
||||
"template_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"template_require_active_version": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"ttl_ms": {
|
||||
"type": "integer"
|
||||
},
|
||||
|
||||
Generated
+3
@@ -10091,6 +10091,9 @@
|
||||
"template_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"template_require_active_version": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"ttl_ms": {
|
||||
"type": "integer"
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"`
|
||||
|
||||
Generated
+3
@@ -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"
|
||||
}
|
||||
|
||||
Generated
+5
@@ -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"
|
||||
}
|
||||
|
||||
Generated
+1
@@ -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;
|
||||
|
||||
+67
-30
@@ -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'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'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} />
|
||||
|
||||
+7
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
+3
@@ -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];
|
||||
};
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user