mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix(site): disable autostart and autostop according to template settings (#11809)
* fix (site): disable autostart and autostop according to template settings * checking form values again; wrote tests * fixed closure and label bugs * fix broken query key * tweaks
This commit is contained in:
+14
-5
@@ -19,8 +19,17 @@ const meta: Meta<typeof WorkspaceScheduleForm> = {
|
||||
title: "pages/WorkspaceSettingsPage/WorkspaceScheduleForm",
|
||||
component: WorkspaceScheduleForm,
|
||||
args: {
|
||||
enableAutoStart: true,
|
||||
enableAutoStop: true,
|
||||
allowTemplateAutoStart: true,
|
||||
allowTemplateAutoStop: true,
|
||||
allowedTemplateAutoStartDays: [
|
||||
"sunday",
|
||||
"monday",
|
||||
"tuesday",
|
||||
"wednesday",
|
||||
"thursday",
|
||||
"friday",
|
||||
"saturday",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -42,8 +51,8 @@ export const AllDisabled: Story = {
|
||||
autostopEnabled: false,
|
||||
ttl: emptyTTL,
|
||||
},
|
||||
enableAutoStart: false,
|
||||
enableAutoStop: false,
|
||||
allowTemplateAutoStart: false,
|
||||
allowTemplateAutoStop: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -55,7 +64,7 @@ export const Autostart: Story = {
|
||||
autostopEnabled: false,
|
||||
ttl: emptyTTL,
|
||||
},
|
||||
enableAutoStop: false,
|
||||
allowTemplateAutoStop: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
+142
@@ -3,8 +3,14 @@ import {
|
||||
ttlShutdownAt,
|
||||
validationSchema,
|
||||
WorkspaceScheduleFormValues,
|
||||
WorkspaceScheduleForm,
|
||||
} from "./WorkspaceScheduleForm";
|
||||
import { timeZones } from "utils/timeZones";
|
||||
import * as API from "api/api";
|
||||
import { MockTemplate } from "testHelpers/entities";
|
||||
import { render } from "testHelpers/renderHelpers";
|
||||
import { defaultSchedule } from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule";
|
||||
import { screen } from "@testing-library/react";
|
||||
|
||||
const valid: WorkspaceScheduleFormValues = {
|
||||
autostartEnabled: true,
|
||||
@@ -237,3 +243,139 @@ describe("ttlShutdownAt", () => {
|
||||
expect(ttlShutdownAt(ttlHours)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
const autoStartDayLabels = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||
const defaultFormProps = {
|
||||
submitScheduleError: "",
|
||||
initialValues: {
|
||||
...defaultSchedule(),
|
||||
autostartEnabled: true,
|
||||
autostopEnabled: true,
|
||||
ttl: 24,
|
||||
},
|
||||
isLoading: false,
|
||||
defaultTTL: 24,
|
||||
onCancel: () => null,
|
||||
onSubmit: () => null,
|
||||
allowedTemplateAutoStartDays: autoStartDayLabels,
|
||||
allowTemplateAutoStart: true,
|
||||
allowTemplateAutoStop: true,
|
||||
};
|
||||
|
||||
describe("templateInheritance", () => {
|
||||
it("disables the entire autostart feature appropriately", async () => {
|
||||
jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate);
|
||||
render(
|
||||
<WorkspaceScheduleForm
|
||||
{...defaultFormProps}
|
||||
allowTemplateAutoStart={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
const autoStartToggle = await screen.findByLabelText("Enable Autostart");
|
||||
expect(autoStartToggle).toBeDisabled();
|
||||
|
||||
const startTimeInput = await screen.findByLabelText("Start time");
|
||||
expect(startTimeInput).toBeDisabled();
|
||||
|
||||
const timezoneInput = await screen.findByLabelText("Timezone");
|
||||
// MUI's input is wrapped in a div so we look at the aria-attribute instead
|
||||
expect(timezoneInput).toHaveAttribute("aria-disabled");
|
||||
|
||||
for (const label of autoStartDayLabels) {
|
||||
const checkbox = await screen.findByLabelText(label);
|
||||
expect(checkbox).toBeDisabled();
|
||||
}
|
||||
});
|
||||
it("disables the autostart days of the week appropriately", async () => {
|
||||
const enabledDayLabels = ["Sat", "Sun"];
|
||||
|
||||
jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate);
|
||||
render(
|
||||
<WorkspaceScheduleForm
|
||||
{...defaultFormProps}
|
||||
allowedTemplateAutoStartDays={["saturday", "sunday"]}
|
||||
/>,
|
||||
);
|
||||
|
||||
const autoStartToggle = await screen.findByLabelText("Enable Autostart");
|
||||
expect(autoStartToggle).toBeEnabled();
|
||||
|
||||
const startTimeInput = await screen.findByLabelText("Start time");
|
||||
expect(startTimeInput).toBeEnabled();
|
||||
|
||||
const timezoneInput = await screen.findByLabelText("Timezone");
|
||||
// MUI's input is wrapped in a div so we look at the aria-attribute instead
|
||||
expect(timezoneInput).not.toHaveAttribute("aria-disabled");
|
||||
|
||||
for (const label of enabledDayLabels) {
|
||||
const checkbox = await screen.findByLabelText(label);
|
||||
expect(checkbox).toBeEnabled();
|
||||
}
|
||||
|
||||
for (const label of autoStartDayLabels.filter(
|
||||
(day) => !enabledDayLabels.includes(day),
|
||||
)) {
|
||||
const checkbox = await screen.findByLabelText(label);
|
||||
expect(checkbox).toBeDisabled();
|
||||
}
|
||||
});
|
||||
it("disables the entire autostop feature appropriately", async () => {
|
||||
jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate);
|
||||
render(
|
||||
<WorkspaceScheduleForm
|
||||
{...defaultFormProps}
|
||||
allowTemplateAutoStop={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
const autoStopToggle = await screen.findByLabelText("Enable Autostop");
|
||||
expect(autoStopToggle).toBeDisabled();
|
||||
|
||||
const ttlInput = await screen.findByLabelText(
|
||||
"Time until shutdown (hours)",
|
||||
);
|
||||
expect(ttlInput).toBeDisabled();
|
||||
});
|
||||
it("disables secondary autostart fields if main feature switch is toggled off", async () => {
|
||||
jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate);
|
||||
render(
|
||||
<WorkspaceScheduleForm
|
||||
{...defaultFormProps}
|
||||
initialValues={{
|
||||
...defaultFormProps.initialValues,
|
||||
autostartEnabled: false,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const startTimeInput = await screen.findByLabelText("Start time");
|
||||
expect(startTimeInput).toBeDisabled();
|
||||
|
||||
const timezoneInput = await screen.findByLabelText("Timezone");
|
||||
// MUI's input is wrapped in a div so we look at the aria-attribute instead
|
||||
expect(timezoneInput).toHaveAttribute("aria-disabled");
|
||||
|
||||
autoStartDayLabels.forEach(async (label) => {
|
||||
const checkbox = await screen.findByLabelText(label);
|
||||
expect(checkbox).toBeDisabled();
|
||||
});
|
||||
});
|
||||
it("disables secondary autostop fields if main feature switch is toggled off", async () => {
|
||||
jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate);
|
||||
render(
|
||||
<WorkspaceScheduleForm
|
||||
{...defaultFormProps}
|
||||
initialValues={{
|
||||
...defaultFormProps.initialValues,
|
||||
autostopEnabled: false,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const ttlInput = await screen.findByLabelText(
|
||||
"Time until shutdown (hours)",
|
||||
);
|
||||
expect(ttlInput).toBeDisabled();
|
||||
});
|
||||
});
|
||||
+56
-19
@@ -32,6 +32,8 @@ import { timeZones } from "utils/timeZones";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import { formatDuration, intervalToDuration } from "date-fns";
|
||||
import { DisabledBadge } from "components/Badges/Badges";
|
||||
import { TemplateAutostartRequirement } from "api/typesGenerated";
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
// REMARK: some plugins depend on utc, so it's listed first. Otherwise they're
|
||||
// sorted alphabetically.
|
||||
@@ -73,8 +75,9 @@ export interface WorkspaceScheduleFormProps {
|
||||
submitScheduleError?: unknown;
|
||||
initialValues: WorkspaceScheduleFormValues;
|
||||
isLoading: boolean;
|
||||
enableAutoStop: boolean;
|
||||
enableAutoStart: boolean;
|
||||
allowedTemplateAutoStartDays: TemplateAutostartRequirement["days_of_week"];
|
||||
allowTemplateAutoStop: boolean;
|
||||
allowTemplateAutoStart: boolean;
|
||||
onCancel: () => void;
|
||||
onSubmit: (values: WorkspaceScheduleFormValues) => void;
|
||||
// for storybook
|
||||
@@ -182,7 +185,7 @@ export const validationSchema = Yup.object({
|
||||
});
|
||||
|
||||
export const WorkspaceScheduleForm: FC<
|
||||
React.PropsWithChildren<WorkspaceScheduleFormProps>
|
||||
PropsWithChildren<WorkspaceScheduleFormProps>
|
||||
> = ({
|
||||
submitScheduleError,
|
||||
initialValues,
|
||||
@@ -191,8 +194,9 @@ export const WorkspaceScheduleForm: FC<
|
||||
onSubmit,
|
||||
initialTouched,
|
||||
defaultTTL,
|
||||
enableAutoStop,
|
||||
enableAutoStart,
|
||||
allowedTemplateAutoStartDays,
|
||||
allowTemplateAutoStop,
|
||||
allowTemplateAutoStart,
|
||||
}) => {
|
||||
const form = useFormik<WorkspaceScheduleFormValues>({
|
||||
initialValues,
|
||||
@@ -207,11 +211,6 @@ export const WorkspaceScheduleForm: FC<
|
||||
);
|
||||
|
||||
const checkboxes: Array<{ value: boolean; name: string; label: string }> = [
|
||||
{
|
||||
value: form.values.sunday,
|
||||
name: "sunday",
|
||||
label: Language.daySundayLabel,
|
||||
},
|
||||
{
|
||||
value: form.values.monday,
|
||||
name: "monday",
|
||||
@@ -242,6 +241,11 @@ export const WorkspaceScheduleForm: FC<
|
||||
name: "saturday",
|
||||
label: Language.daySaturdayLabel,
|
||||
},
|
||||
{
|
||||
value: form.values.sunday,
|
||||
name: "sunday",
|
||||
label: Language.daySundayLabel,
|
||||
},
|
||||
];
|
||||
|
||||
const handleToggleAutostart = async (e: ChangeEvent) => {
|
||||
@@ -288,7 +292,7 @@ export const WorkspaceScheduleForm: FC<
|
||||
Select the time and days of week on which you want the workspace
|
||||
starting automatically.
|
||||
</div>
|
||||
{!enableAutoStart && (
|
||||
{!allowTemplateAutoStart && (
|
||||
<Tooltip title="This option can be enabled in the template settings">
|
||||
<DisabledBadge />
|
||||
</Tooltip>
|
||||
@@ -300,7 +304,7 @@ export const WorkspaceScheduleForm: FC<
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
disabled={!enableAutoStart}
|
||||
disabled={!allowTemplateAutoStart}
|
||||
name="autostartEnabled"
|
||||
checked={form.values.autostartEnabled}
|
||||
onChange={handleToggleAutostart}
|
||||
@@ -311,14 +315,26 @@ export const WorkspaceScheduleForm: FC<
|
||||
<Stack direction="row">
|
||||
<TextField
|
||||
{...formHelpers("startTime")}
|
||||
disabled={isLoading || !form.values.autostartEnabled}
|
||||
// disabled if template does not allow autostart
|
||||
// or if primary feature is toggled off via the switch above
|
||||
disabled={
|
||||
isLoading ||
|
||||
!allowTemplateAutoStart ||
|
||||
!form.values.autostartEnabled
|
||||
}
|
||||
label={Language.startTimeLabel}
|
||||
type="time"
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
{...formHelpers("timezone")}
|
||||
disabled={isLoading || !form.values.autostartEnabled}
|
||||
// disabled if template does not allow autostart
|
||||
// or if primary feature is toggled off via the switch above
|
||||
disabled={
|
||||
isLoading ||
|
||||
!allowTemplateAutoStart ||
|
||||
!form.values.autostartEnabled
|
||||
}
|
||||
label={Language.timezoneLabel}
|
||||
select
|
||||
fullWidth
|
||||
@@ -349,7 +365,15 @@ export const WorkspaceScheduleForm: FC<
|
||||
control={
|
||||
<Checkbox
|
||||
checked={checkbox.value}
|
||||
disabled={isLoading || !form.values.autostartEnabled}
|
||||
// template admins can disable the autostart feature in general,
|
||||
// or they can disallow autostart on specific days of the week.
|
||||
// also disabled if primary feature switch (above) is toggled off
|
||||
disabled={
|
||||
isLoading ||
|
||||
!allowTemplateAutoStart ||
|
||||
!allowedTemplateAutoStartDays.includes(checkbox.name) ||
|
||||
!form.values.autostartEnabled
|
||||
}
|
||||
onChange={form.handleChange}
|
||||
name={checkbox.name}
|
||||
size="small"
|
||||
@@ -378,7 +402,7 @@ export const WorkspaceScheduleForm: FC<
|
||||
extended by 1 hour after last activity in the workspace was
|
||||
detected.
|
||||
</div>
|
||||
{!enableAutoStop && (
|
||||
{!allowTemplateAutoStop && (
|
||||
<Tooltip title="This option can be enabled in the template settings">
|
||||
<DisabledBadge />
|
||||
</Tooltip>
|
||||
@@ -393,7 +417,7 @@ export const WorkspaceScheduleForm: FC<
|
||||
name="autostopEnabled"
|
||||
checked={form.values.autostopEnabled}
|
||||
onChange={handleToggleAutostop}
|
||||
disabled={!enableAutoStop}
|
||||
disabled={!allowTemplateAutoStop}
|
||||
/>
|
||||
}
|
||||
label={Language.stopSwitch}
|
||||
@@ -403,7 +427,13 @@ export const WorkspaceScheduleForm: FC<
|
||||
helperText: ttlShutdownAt(form.values.ttl),
|
||||
backendFieldName: "ttl_ms",
|
||||
})}
|
||||
disabled={isLoading || !form.values.autostopEnabled}
|
||||
// disabled if autostop disabled at template level or
|
||||
// if autostop feature is toggled off via the switch above
|
||||
disabled={
|
||||
isLoading ||
|
||||
!allowTemplateAutoStop ||
|
||||
!form.values.autostopEnabled
|
||||
}
|
||||
inputProps={{ min: 0, step: "any" }}
|
||||
label={Language.ttlLabel}
|
||||
type="number"
|
||||
@@ -411,7 +441,14 @@ export const WorkspaceScheduleForm: FC<
|
||||
/>
|
||||
</FormFields>
|
||||
</FormSection>
|
||||
<FormFooter onCancel={onCancel} isLoading={isLoading} />
|
||||
<FormFooter
|
||||
onCancel={onCancel}
|
||||
isLoading={isLoading}
|
||||
submitDisabled={
|
||||
(!allowTemplateAutoStart && !allowTemplateAutoStop) ||
|
||||
(!form.values.autostartEnabled && !form.values.autostopEnabled)
|
||||
}
|
||||
/>
|
||||
</HorizontalForm>
|
||||
);
|
||||
};
|
||||
|
||||
+9
-3
@@ -59,7 +59,10 @@ export const WorkspaceSchedulePage: FC = () => {
|
||||
mutationFn: submitSchedule,
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries(
|
||||
workspaceByOwnerAndNameKey(params.username, params.workspace),
|
||||
workspaceByOwnerAndNameKey(
|
||||
params.username.replace(/^@/, ""),
|
||||
params.workspace,
|
||||
),
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -98,8 +101,11 @@ export const WorkspaceSchedulePage: FC = () => {
|
||||
|
||||
{template && (
|
||||
<WorkspaceScheduleForm
|
||||
enableAutoStart={template.allow_user_autostart}
|
||||
enableAutoStop={template.allow_user_autostop}
|
||||
allowedTemplateAutoStartDays={
|
||||
template.autostart_requirement.days_of_week
|
||||
}
|
||||
allowTemplateAutoStart={template.allow_user_autostart}
|
||||
allowTemplateAutoStop={template.allow_user_autostop}
|
||||
submitScheduleError={submitScheduleMutation.error}
|
||||
initialValues={{
|
||||
...getAutostart(workspace),
|
||||
|
||||
Reference in New Issue
Block a user