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:
Kira Pilot
2024-01-26 09:06:01 -05:00
committed by GitHub
parent 52c08a98bb
commit 4c71cccbc3
4 changed files with 221 additions and 27 deletions
@@ -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,
},
};
@@ -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();
});
});
@@ -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>
);
};
@@ -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),