diff --git a/coderd/templates.go b/coderd/templates.go index 38bc7b1f2c..63fec1c63c 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -610,6 +610,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { req.DefaultTTLMillis == time.Duration(template.DefaultTTL).Milliseconds() && req.MaxTTLMillis == time.Duration(template.MaxTTL).Milliseconds() && autostopRequirementDaysOfWeekParsed == scheduleOpts.AutostopRequirement.DaysOfWeek && + autostartRequirementDaysOfWeekParsed == scheduleOpts.AutostartRequirement.DaysOfWeek && req.AutostopRequirement.Weeks == scheduleOpts.AutostopRequirement.Weeks && req.FailureTTLMillis == time.Duration(template.FailureTTL).Milliseconds() && req.TimeTilDormantMillis == time.Duration(template.TimeTilDormant).Milliseconds() && diff --git a/site/src/components/TemplateScheduleAutostart/TemplateScheduleAutostart.tsx b/site/src/components/TemplateScheduleAutostart/TemplateScheduleAutostart.tsx new file mode 100644 index 0000000000..3fac410e1e --- /dev/null +++ b/site/src/components/TemplateScheduleAutostart/TemplateScheduleAutostart.tsx @@ -0,0 +1,137 @@ +import { FC } from "react"; +import { TemplateAutostartRequirementDaysValue } from "utils/schedule"; +import Button from "@mui/material/Button"; +import { Stack } from "components/Stack/Stack"; +import FormHelperText from "@mui/material/FormHelperText"; + +export interface TemplateScheduleAutostartProps { + allow_user_autostart?: boolean; + autostart_requirement_days_of_week: TemplateAutostartRequirementDaysValue[]; + isSubmitting: boolean; + onChange: (newDaysOfWeek: TemplateAutostartRequirementDaysValue[]) => void; +} + +export const TemplateScheduleAutostart: FC< + React.PropsWithChildren +> = ({ + autostart_requirement_days_of_week, + isSubmitting, + allow_user_autostart, + onChange, +}) => { + return ( + + + {( + [ + { value: "monday", key: "Mon" }, + { value: "tuesday", key: "Tue" }, + { value: "wednesday", key: "Wed" }, + { value: "thursday", key: "Thu" }, + { value: "friday", key: "Fri" }, + { value: "saturday", key: "Sat" }, + { value: "sunday", key: "Sun" }, + ] as { + value: TemplateAutostartRequirementDaysValue; + key: string; + }[] + ).map((day) => ( + + ))} + + + + + + ); +}; + +export const sortedDays = [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday", +] as TemplateAutostartRequirementDaysValue[]; + +const AutostartRequirementDaysHelperText: FC<{ + allowed?: boolean; + days: TemplateAutostartRequirementDaysValue[]; +}> = ({ allowed, days: unsortedDays }) => { + if (!allowed) { + return Workspaces are not allowed to auto start.; + } + // Sort the days + const days = unsortedDays.sort( + (a, b) => sortedDays.indexOf(a) - sortedDays.indexOf(b), + ); + + let daymsg = `Workspaces can autostart on ${days.join(", ")}.`; + if (days.length === 7) { + // If every day is allowed, no more explaining is needed. + return Workspaces are allowed to auto start on any day.; + } + if (days.length === 0) { + return ( + + Workspaces will never auto start. This is effectively the same as + disabling autostart. + + ); + } + if ( + days.length === 5 && + !days.includes("saturday") && + !days.includes("sunday") + ) { + daymsg = "Workspaces will never auto start on the weekends."; + } + return ( + {daymsg} These days are relative to the user's timezone. + ); +}; diff --git a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx index a7dbe7efb4..2c7640e811 100644 --- a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx +++ b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx @@ -42,7 +42,14 @@ import { AutostopRequirementWeeksHelperText, } from "pages/TemplateSettingsPage/TemplateSchedulePage/AutostopRequirementHelperText"; import MenuItem from "@mui/material/MenuItem"; -import { TemplateAutostopRequirementDaysValue } from "utils/schedule"; +import { + TemplateAutostartRequirementDaysValue, + TemplateAutostopRequirementDaysValue, +} from "utils/schedule"; +import { + TemplateScheduleAutostart, + sortedDays, +} from "components/TemplateScheduleAutostart/TemplateScheduleAutostart"; const MAX_DESCRIPTION_CHAR_LIMIT = 128; const MAX_TTL_DAYS = 30; @@ -54,6 +61,7 @@ export interface CreateTemplateData { icon: string; default_ttl_hours: number; max_ttl_hours: number; + autostart_requirement_days_of_week: TemplateAutostartRequirementDaysValue[]; autostop_requirement_days_of_week: TemplateAutostopRequirementDaysValue; autostop_requirement_weeks: number; allow_user_autostart: boolean; @@ -88,6 +96,7 @@ const validationSchema = Yup.object({ ), autostop_requirement_days_of_week: Yup.string().required(), autostop_requirement_weeks: Yup.number().required().min(1).max(16), + autostart_requirement_days_of_week: Yup.array().of(Yup.string()).required(), }); const defaultInitialValues: CreateTemplateData = { @@ -110,6 +119,7 @@ const defaultInitialValues: CreateTemplateData = { // user's timezone. autostop_requirement_days_of_week: "sunday", autostop_requirement_weeks: 1, + autostart_requirement_days_of_week: sortedDays, allow_user_cancel_workspace_jobs: false, allow_user_autostart: false, allow_user_autostop: false, @@ -434,6 +444,25 @@ export const CreateTemplateForm: FC = (props) => { + + {allowAdvancedScheduling && ( + { + await form.setFieldValue( + "autostart_requirement_days_of_week", + newDaysOfWeek, + ); + }} + /> + )} + { max_ttl_hours, parameter_values_by_name, allow_everyone_group_access, + autostart_requirement_days_of_week, autostop_requirement_days_of_week, autostop_requirement_weeks, ...safeTemplateData @@ -33,6 +34,9 @@ export const newTemplate = (formData: CreateTemplateData) => { ), weeks: formData.autostop_requirement_weeks, }, + autostart_requirement: { + days_of_week: autostart_requirement_days_of_week, + }, }; }; diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx index 7d98f9de12..54d5584677 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx @@ -9,7 +9,10 @@ import { FC, ChangeEvent, useState, useEffect } from "react"; import { Template, UpdateTemplateMeta } from "api/typesGenerated"; import { getFormHelpers } from "utils/formUtils"; import { docs } from "utils/docs"; -import { calculateAutostopRequirementDaysValue } from "utils/schedule"; +import { + TemplateAutostartRequirementDaysValue, + calculateAutostopRequirementDaysValue, +} from "utils/schedule"; import { FormSection, HorizontalForm, @@ -36,6 +39,7 @@ import { convertAutostopRequirementDaysValue, } from "./AutostopRequirementHelperText"; import { useTheme } from "@emotion/react"; +import { TemplateScheduleAutostart } from "components/TemplateScheduleAutostart/TemplateScheduleAutostart"; const MS_HOUR_CONVERSION = 3600000; const MS_DAY_CONVERSION = 86400000; @@ -95,6 +99,8 @@ export const TemplateScheduleForm: FC = ({ ? template.autostop_requirement.weeks : 1 : 1, + autostart_requirement_days_of_week: template.autostart_requirement + .days_of_week as TemplateAutostartRequirementDaysValue[], allow_user_autostart: template.allow_user_autostart, allow_user_autostop: template.allow_user_autostop, @@ -215,6 +221,9 @@ export const TemplateScheduleForm: FC = ({ ), weeks: autostop_requirement_weeks, }, + autostart_requirement: { + days_of_week: form.values.autostart_requirement_days_of_week, + }, allow_user_autostart: form.values.allow_user_autostart, allow_user_autostop: form.values.allow_user_autostop, @@ -430,6 +439,24 @@ export const TemplateScheduleForm: FC = ({ + {allowAdvancedScheduling && ( + { + await form.setFieldValue( + "autostart_requirement_days_of_week", + newDaysOfWeek, + ); + }} + /> + )} + { diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/formHelpers.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/formHelpers.tsx index b579fc94d0..9fc299fba1 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/formHelpers.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/formHelpers.tsx @@ -1,9 +1,16 @@ import { UpdateTemplateMeta } from "api/typesGenerated"; -import { TemplateAutostopRequirementDaysValue } from "utils/schedule"; +import { + TemplateAutostartRequirementDaysValue, + TemplateAutostopRequirementDaysValue, +} from "utils/schedule"; import * as Yup from "yup"; export interface TemplateScheduleFormValues - extends Omit { + extends Omit< + UpdateTemplateMeta, + "autostop_requirement" | "autostart_requirement" + > { + autostart_requirement_days_of_week: TemplateAutostartRequirementDaysValue[]; autostop_requirement_days_of_week: TemplateAutostopRequirementDaysValue; autostop_requirement_weeks: number; failure_cleanup_enabled: boolean; @@ -75,5 +82,6 @@ export const getValidationSchema = (): Yup.AnyObjectSchema => allow_user_autostop: Yup.boolean(), autostop_requirement_days_of_week: Yup.string().required(), + autostart_requirement_days_of_week: Yup.array().of(Yup.string()).required(), autostop_requirement_weeks: Yup.number().required().min(1).max(16), }); diff --git a/site/src/utils/schedule.ts b/site/src/utils/schedule.ts index fdd1bc1bf3..9deb11b621 100644 --- a/site/src/utils/schedule.ts +++ b/site/src/utils/schedule.ts @@ -211,6 +211,15 @@ export const quietHoursDisplay = ( return display; }; +export type TemplateAutostartRequirementDaysValue = + | "monday" + | "tuesday" + | "wednesday" + | "thursday" + | "friday" + | "saturday" + | "sunday"; + export type TemplateAutostopRequirementDaysValue = | "off" | "daily"