fix(site): fix template schedule update overriding other settings (#13114)

This commit is contained in:
Bruno Quaresma 2024-05-01 10:25:40 -03:00 committed by GitHub
parent f2dd0a8e5d
commit 71a03a8b1d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 123 additions and 111 deletions

View File

@ -232,7 +232,7 @@ type UpdateTemplateMeta struct {
// RequireActiveVersion mandates workspaces built using this template
// use the active version of the template. This option has no
// effect on template admins.
RequireActiveVersion bool `json:"require_active_version"`
RequireActiveVersion bool `json:"require_active_version,omitempty"`
// DeprecationMessage if set, will mark the template as deprecated and block
// any new workspaces from using this template.
// If passed an empty string, will remove the deprecated message, making

View File

@ -1,5 +1,5 @@
import { test, expect } from "@playwright/test";
import { beforeCoderTest } from "../hooks";
import { beforeCoderTest } from "../../hooks";
test.beforeEach(({ page }) => beforeCoderTest(page));

View File

@ -0,0 +1,45 @@
import { expect, test } from "@playwright/test";
import { createTemplate, createTemplateVersion, getTemplate } from "api/api";
import { getCurrentOrgId, setupApiCalls } from "../../api";
import { beforeCoderTest } from "../../hooks";
test.beforeEach(({ page }) => beforeCoderTest(page));
test("update template schedule settings without override other settings", async ({
page,
baseURL,
}) => {
await setupApiCalls(page);
const orgId = await getCurrentOrgId();
const templateVersion = await createTemplateVersion(orgId, {
storage_method: "file" as const,
provisioner: "echo",
user_variable_values: [],
example_id: "docker",
tags: {},
});
const template = await createTemplate(orgId, {
name: "test-template",
display_name: "Test Template",
template_version_id: templateVersion.id,
disable_everyone_group_access: false,
require_active_version: true,
});
await page.goto(`${baseURL}/templates/${template.name}/settings/schedule`, {
waitUntil: "domcontentloaded",
});
await page.getByLabel("Default autostop (hours)").fill("48");
await page.getByRole("button", { name: "Submit" }).click();
await expect(page.getByText("Template updated successfully")).toBeVisible();
const updatedTemplate = await getTemplate(template.id);
// Validate that the template data remains consistent, with the exception of
// the 'default_ttl_ms' field (updated during the test) and the 'updated at'
// field (automatically updated by the backend).
expect({
...template,
default_ttl_ms: 48 * 60 * 60 * 1000,
updated_at: updatedTemplate.updated_at,
}).toStrictEqual(updatedTemplate);
});

View File

@ -1320,7 +1320,7 @@ export interface UpdateTemplateMeta {
readonly time_til_dormant_autodelete_ms?: number;
readonly update_workspace_last_used_at: boolean;
readonly update_workspace_dormant_at: boolean;
readonly require_active_version: boolean;
readonly require_active_version?: boolean;
readonly deprecation_message?: string;
readonly disable_everyone_group_access: boolean;
readonly max_port_share_level?: WorkspaceAgentPortShareLevel;

View File

@ -22,9 +22,13 @@ export const StackLabel: FC<ComponentProps<typeof Stack>> = (props) => {
export const StackLabelHelperText: FC<FormHelperTextProps> = (props) => {
return (
<FormHelperText
css={{
css={(theme) => ({
marginTop: 0,
}}
"& strong": {
color: theme.palette.text.primary,
},
})}
{...props}
/>
);

View File

@ -1,5 +1,6 @@
import type { Interpolation, Theme } from "@emotion/react";
import Checkbox from "@mui/material/Checkbox";
import FormControlLabel from "@mui/material/FormControlLabel";
import FormHelperText from "@mui/material/FormHelperText";
import MenuItem from "@mui/material/MenuItem";
import TextField from "@mui/material/TextField";
import { type FormikContextType, type FormikTouched, useFormik } from "formik";
@ -17,14 +18,12 @@ import {
HorizontalForm,
FormFooter,
} from "components/Form/Form";
import {
HelpTooltip,
HelpTooltipContent,
HelpTooltipText,
HelpTooltipTrigger,
} from "components/HelpTooltip/HelpTooltip";
import { IconField } from "components/IconField/IconField";
import { Stack } from "components/Stack/Stack";
import {
StackLabel,
StackLabelHelperText,
} from "components/StackLabel/StackLabel";
import {
getFormHelpers,
nameValidator,
@ -160,92 +159,74 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
title="Operations"
description="Regulate actions allowed on workspaces created from this template."
>
<Stack direction="column" spacing={5}>
<label htmlFor="allow_user_cancel_workspace_jobs">
<Stack direction="row" spacing={1}>
<FormFields spacing={6}>
<FormControlLabel
control={
<Checkbox
size="small"
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}
css={styles.optionText}
>
Allow users to cancel in-progress workspace jobs.
<HelpTooltip>
<HelpTooltipTrigger />
<HelpTooltipContent>
<HelpTooltipText>
If checked, users may be able to corrupt their
workspace.
</HelpTooltipText>
</HelpTooltipContent>
</HelpTooltip>
</Stack>
<span css={styles.optionHelperText}>
}
label={
<StackLabel>
Allow users to cancel in-progress workspace jobs.
<StackLabelHelperText>
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>
<Stack spacing={2}>
<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}
disabled={
!template.require_active_version &&
!advancedSchedulingEnabled
}
/>
recommended for most use cases.{" "}
<strong>
If checked, users may be able to corrupt their workspace.
</strong>
</StackLabelHelperText>
</StackLabel>
}
/>
<Stack direction="column" spacing={0.5}>
<Stack
direction="row"
alignItems="center"
spacing={0.5}
css={styles.optionText}
>
Require workspaces automatically update when started.
<HelpTooltip>
<HelpTooltipTrigger />
<HelpTooltipContent>
<HelpTooltipText>
This setting is not enforced for template admins.
</HelpTooltipText>
</HelpTooltipContent>
</HelpTooltip>
</Stack>
<span css={styles.optionHelperText}>
<FormControlLabel
control={
<Checkbox
size="small"
id="require_active_version"
name="require_active_version"
checked={form.values.require_active_version}
onChange={form.handleChange}
disabled={
!template.require_active_version && !advancedSchedulingEnabled
}
/>
}
label={
<StackLabel>
Require workspaces automatically update when started.
<StackLabelHelperText>
<span>
Workspaces that are manually started or auto-started will
use the active template version.
use the active template version.{" "}
<strong>
This setting is not enforced for template admins.
</strong>
</span>
</Stack>
</Stack>
</label>
{!advancedSchedulingEnabled && (
<Stack direction="row">
<EnterpriseBadge />
<span css={styles.optionHelperText}>
Enterprise license required to enabled.
</span>
</Stack>
)}
</Stack>
</Stack>
{!advancedSchedulingEnabled && (
<Stack
direction="row"
spacing={2}
alignItems="center"
css={{ marginTop: 16 }}
>
<EnterpriseBadge />
<span>Enterprise license required to enabled.</span>
</Stack>
)}
</StackLabelHelperText>
</StackLabel>
}
/>
</FormFields>
</FormSection>
<FormSection
@ -265,13 +246,13 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
label="Deprecation Message"
/>
{!accessControlEnabled && (
<Stack direction="row">
<Stack direction="row" spacing={2} alignItems="center">
<EnterpriseBadge />
<span css={styles.optionHelperText}>
<FormHelperText>
Enterprise license required to deprecate templates.
{template.deprecated &&
" You cannot change the message, but you may remove it to mark this template as no longer deprecated."}
</span>
</FormHelperText>
</Stack>
)}
</FormFields>
@ -306,11 +287,11 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
<MenuItem value="public">Public</MenuItem>
</TextField>
{!portSharingControlsEnabled && (
<Stack direction="row">
<Stack direction="row" spacing={2} alignItems="center">
<EnterpriseBadge />
<span css={styles.optionHelperText}>
<FormHelperText>
Enterprise license required to control max port sharing level.
</span>
</FormHelperText>
</Stack>
)}
</FormFields>
@ -321,15 +302,3 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
</HorizontalForm>
);
};
const styles = {
optionText: (theme) => ({
fontSize: 16,
color: theme.palette.text.primary,
}),
optionHelperText: (theme) => ({
fontSize: 12,
color: theme.palette.text.secondary,
}),
} satisfies Record<string, Interpolation<Theme>>;

View File

@ -234,7 +234,6 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
allow_user_autostop: form.values.allow_user_autostop,
update_workspace_last_used_at: form.values.update_workspace_last_used_at,
update_workspace_dormant_at: form.values.update_workspace_dormant_at,
require_active_version: false,
disable_everyone_group_access: false,
});
};
@ -533,14 +532,9 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
<StackLabelHelperText>
When enabled, Coder will permanently delete dormant
workspaces after a period of time.{" "}
<span
css={(theme) => ({
fontWeight: 500,
color: theme.palette.text.primary,
})}
>
<strong>
Once a workspace is deleted it cannot be recovered.
</span>
</strong>
</StackLabelHelperText>
</StackLabel>
}