mirror of https://github.com/coder/coder.git
feat: add experimental template autostop requirement template settings UI (#9417)
This commit is contained in:
parent
d2462e5b88
commit
1de61246a3
|
@ -317,7 +317,7 @@ func TestScheduleOverride(t *testing.T) {
|
|||
)
|
||||
require.Zero(t, template.DefaultTTLMillis)
|
||||
require.Empty(t, template.AutostopRequirement.DaysOfWeek)
|
||||
require.Zero(t, template.AutostopRequirement.Weeks)
|
||||
require.EqualValues(t, 1, template.AutostopRequirement.Weeks)
|
||||
|
||||
// Unset the workspace TTL
|
||||
err = client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{TTLMillis: nil})
|
||||
|
|
|
@ -1,7 +1,25 @@
|
|||
BEGIN;
|
||||
|
||||
DROP VIEW template_with_users;
|
||||
|
||||
ALTER TABLE templates RENAME COLUMN autostop_requirement_days_of_week TO restart_requirement_days_of_week;
|
||||
|
||||
ALTER TABLE templates RENAME COLUMN autostop_requirement_weeks TO restart_requirement_weeks;
|
||||
|
||||
CREATE VIEW
|
||||
template_with_users
|
||||
AS
|
||||
SELECT
|
||||
templates.*,
|
||||
coalesce(visible_users.avatar_url, '') AS created_by_avatar_url,
|
||||
coalesce(visible_users.username, '') AS created_by_username
|
||||
FROM
|
||||
templates
|
||||
LEFT JOIN
|
||||
visible_users
|
||||
ON
|
||||
templates.created_by = visible_users.id;
|
||||
|
||||
COMMENT ON VIEW template_with_users IS 'Joins in the username + avatar url of the created by user.';
|
||||
|
||||
COMMIT;
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
BEGIN;
|
||||
|
||||
DROP VIEW template_with_users;
|
||||
|
||||
ALTER TABLE templates RENAME COLUMN restart_requirement_days_of_week TO autostop_requirement_days_of_week;
|
||||
|
||||
ALTER TABLE templates RENAME COLUMN restart_requirement_weeks TO autostop_requirement_weeks;
|
||||
|
||||
DROP VIEW template_with_users;
|
||||
|
||||
CREATE VIEW
|
||||
template_with_users
|
||||
AS
|
||||
|
|
|
@ -72,8 +72,8 @@ func VerifyTemplateAutostopRequirement(days uint8, weeks int64) error {
|
|||
if days > 0b11111111 {
|
||||
return xerrors.New("invalid autostop requirement days, too large")
|
||||
}
|
||||
if weeks < 0 {
|
||||
return xerrors.New("invalid autostop requirement weeks, negative")
|
||||
if weeks < 1 {
|
||||
return xerrors.New("invalid autostop requirement weeks, less than 1")
|
||||
}
|
||||
if weeks > MaxTemplateAutostopRequirementWeeks {
|
||||
return xerrors.New("invalid autostop requirement weeks, too large")
|
||||
|
@ -154,8 +154,10 @@ func (*agplTemplateScheduleStore) Get(ctx context.Context, db database.Store, te
|
|||
UseAutostopRequirement: false,
|
||||
MaxTTL: 0,
|
||||
AutostopRequirement: TemplateAutostopRequirement{
|
||||
// No days means never. The weeks value should always be greater
|
||||
// than zero though.
|
||||
DaysOfWeek: 0,
|
||||
Weeks: 0,
|
||||
Weeks: 1,
|
||||
},
|
||||
FailureTTL: 0,
|
||||
TimeTilDormant: 0,
|
||||
|
|
|
@ -527,6 +527,12 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
|
|||
if req.AutostopRequirement.Weeks < 0 {
|
||||
validErrs = append(validErrs, codersdk.ValidationError{Field: "autostop_requirement.weeks", Detail: "Must be a positive integer."})
|
||||
}
|
||||
if req.AutostopRequirement.Weeks == 0 {
|
||||
req.AutostopRequirement.Weeks = 1
|
||||
}
|
||||
if template.AutostopRequirementWeeks <= 0 {
|
||||
template.AutostopRequirementWeeks = 1
|
||||
}
|
||||
if req.AutostopRequirement.Weeks > schedule.MaxTemplateAutostopRequirementWeeks {
|
||||
validErrs = append(validErrs, codersdk.ValidationError{Field: "autostop_requirement.weeks", Detail: fmt.Sprintf("Must be less than %d.", schedule.MaxTemplateAutostopRequirementWeeks)})
|
||||
}
|
||||
|
@ -737,6 +743,11 @@ func (api *API) convertTemplate(
|
|||
|
||||
buildTimeStats := api.metricsCache.TemplateBuildTimeStats(template.ID)
|
||||
|
||||
autostopRequirementWeeks := template.AutostopRequirementWeeks
|
||||
if autostopRequirementWeeks < 1 {
|
||||
autostopRequirementWeeks = 1
|
||||
}
|
||||
|
||||
return codersdk.Template{
|
||||
ID: template.ID,
|
||||
CreatedAt: template.CreatedAt,
|
||||
|
@ -762,7 +773,7 @@ func (api *API) convertTemplate(
|
|||
TimeTilDormantAutoDeleteMillis: time.Duration(template.TimeTilDormantAutoDelete).Milliseconds(),
|
||||
AutostopRequirement: codersdk.TemplateAutostopRequirement{
|
||||
DaysOfWeek: codersdk.BitmapToWeekdays(uint8(template.AutostopRequirementDaysOfWeek)),
|
||||
Weeks: template.AutostopRequirementWeeks,
|
||||
Weeks: autostopRequirementWeeks,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -296,7 +296,7 @@ func TestPostTemplateByOrganization(t *testing.T) {
|
|||
|
||||
require.EqualValues(t, 1, atomic.LoadInt64(&setCalled))
|
||||
require.Empty(t, got.AutostopRequirement.DaysOfWeek)
|
||||
require.Zero(t, got.AutostopRequirement.Weeks)
|
||||
require.EqualValues(t, 1, got.AutostopRequirement.Weeks)
|
||||
})
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
|
@ -379,7 +379,7 @@ func TestPostTemplateByOrganization(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
// ignored and use AGPL defaults
|
||||
require.Empty(t, got.AutostopRequirement.DaysOfWeek)
|
||||
require.Zero(t, got.AutostopRequirement.Weeks)
|
||||
require.EqualValues(t, 1, got.AutostopRequirement.Weeks)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -1006,7 +1006,7 @@ func TestPatchTemplateMeta(t *testing.T) {
|
|||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
require.EqualValues(t, 1, atomic.LoadInt64(&setCalled))
|
||||
require.Empty(t, template.AutostopRequirement.DaysOfWeek)
|
||||
require.Zero(t, template.AutostopRequirement.Weeks)
|
||||
require.EqualValues(t, 1, template.AutostopRequirement.Weeks)
|
||||
req := codersdk.UpdateTemplateMeta{
|
||||
Name: template.Name,
|
||||
DisplayName: template.DisplayName,
|
||||
|
@ -1045,7 +1045,7 @@ func TestPatchTemplateMeta(t *testing.T) {
|
|||
SetFn: func(ctx context.Context, db database.Store, template database.Template, options schedule.TemplateScheduleOptions) (database.Template, error) {
|
||||
if atomic.AddInt64(&setCalled, 1) == 2 {
|
||||
assert.EqualValues(t, 0, options.AutostopRequirement.DaysOfWeek)
|
||||
assert.EqualValues(t, 0, options.AutostopRequirement.Weeks)
|
||||
assert.EqualValues(t, 1, options.AutostopRequirement.Weeks)
|
||||
}
|
||||
|
||||
err := db.UpdateTemplateScheduleByID(ctx, database.UpdateTemplateScheduleByIDParams{
|
||||
|
@ -1102,12 +1102,12 @@ func TestPatchTemplateMeta(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
require.EqualValues(t, 2, atomic.LoadInt64(&setCalled))
|
||||
require.Empty(t, updated.AutostopRequirement.DaysOfWeek)
|
||||
require.EqualValues(t, 0, updated.AutostopRequirement.Weeks)
|
||||
require.EqualValues(t, 1, updated.AutostopRequirement.Weeks)
|
||||
|
||||
template, err = client.Template(ctx, template.ID)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, template.AutostopRequirement.DaysOfWeek)
|
||||
require.EqualValues(t, 0, template.AutostopRequirement.Weeks)
|
||||
require.EqualValues(t, 1, template.AutostopRequirement.Weeks)
|
||||
})
|
||||
|
||||
t.Run("EnterpriseOnly", func(t *testing.T) {
|
||||
|
@ -1118,7 +1118,7 @@ func TestPatchTemplateMeta(t *testing.T) {
|
|||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
require.Empty(t, template.AutostopRequirement.DaysOfWeek)
|
||||
require.Zero(t, template.AutostopRequirement.Weeks)
|
||||
require.EqualValues(t, 1, template.AutostopRequirement.Weeks)
|
||||
req := codersdk.UpdateTemplateMeta{
|
||||
Name: template.Name,
|
||||
DisplayName: template.DisplayName,
|
||||
|
@ -1138,12 +1138,12 @@ func TestPatchTemplateMeta(t *testing.T) {
|
|||
updated, err := client.UpdateTemplateMeta(ctx, template.ID, req)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, updated.AutostopRequirement.DaysOfWeek)
|
||||
require.Zero(t, updated.AutostopRequirement.Weeks)
|
||||
require.EqualValues(t, 1, updated.AutostopRequirement.Weeks)
|
||||
|
||||
template, err = client.Template(ctx, template.ID)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, template.AutostopRequirement.DaysOfWeek)
|
||||
require.Zero(t, template.AutostopRequirement.Weeks)
|
||||
require.EqualValues(t, 1, template.AutostopRequirement.Weeks)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -83,7 +83,7 @@ func WeekdaysToBitmap(days []string) (uint8, error) {
|
|||
// BitmapToWeekdays converts a bitmap to a list of weekdays in accordance with
|
||||
// the schedule package's rules (see above).
|
||||
func BitmapToWeekdays(bitmap uint8) []string {
|
||||
var days []string
|
||||
days := []string{}
|
||||
for i := 0; i < 7; i++ {
|
||||
if bitmap&(1<<i) != 0 {
|
||||
switch i {
|
||||
|
|
|
@ -68,6 +68,9 @@ func (s *EnterpriseTemplateScheduleStore) Get(ctx context.Context, db database.S
|
|||
if tpl.AutostopRequirementDaysOfWeek > 0b11111111 {
|
||||
return agpl.TemplateScheduleOptions{}, xerrors.New("invalid autostop requirement days, too large")
|
||||
}
|
||||
if tpl.AutostopRequirementWeeks == 0 {
|
||||
tpl.AutostopRequirementWeeks = 1
|
||||
}
|
||||
err = agpl.VerifyTemplateAutostopRequirement(uint8(tpl.AutostopRequirementDaysOfWeek), tpl.AutostopRequirementWeeks)
|
||||
if err != nil {
|
||||
return agpl.TemplateScheduleOptions{}, err
|
||||
|
@ -94,6 +97,13 @@ func (s *EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.S
|
|||
ctx, span := tracing.StartSpan(ctx)
|
||||
defer span.End()
|
||||
|
||||
if opts.AutostopRequirement.Weeks <= 0 {
|
||||
opts.AutostopRequirement.Weeks = 1
|
||||
}
|
||||
if tpl.AutostopRequirementWeeks <= 0 {
|
||||
tpl.AutostopRequirementWeeks = 1
|
||||
}
|
||||
|
||||
if int64(opts.DefaultTTL) == tpl.DefaultTTL &&
|
||||
int64(opts.MaxTTL) == tpl.MaxTTL &&
|
||||
int16(opts.AutostopRequirement.DaysOfWeek) == tpl.AutostopRequirementDaysOfWeek &&
|
||||
|
|
|
@ -158,7 +158,7 @@ func TestTemplates(t *testing.T) {
|
|||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
require.Empty(t, 0, template.AutostopRequirement.DaysOfWeek)
|
||||
require.Zero(t, template.AutostopRequirement.Weeks)
|
||||
require.EqualValues(t, 1, template.AutostopRequirement.Weeks)
|
||||
|
||||
// ctx := testutil.Context(t, testutil.WaitLong)
|
||||
ctx := context.Background()
|
||||
|
|
|
@ -351,7 +351,7 @@ export const WorkspaceScheduleForm: FC<
|
|||
|
||||
<FormSection
|
||||
title="Autostop"
|
||||
description="Set how many hours should elapse after a workspace is started before it automatically shuts down. If workspace connection activity is detected, the autostop timer will be bumped up one hour."
|
||||
description="Set how many hours should elapse after a workspace is started before it automatically shuts down. If workspace connection activity is detected, the autostop timer will be bumped by this value."
|
||||
>
|
||||
<FormFields>
|
||||
<FormControlLabel
|
||||
|
|
|
@ -28,12 +28,18 @@
|
|||
"icon": "Icon",
|
||||
"autostop": "Default autostop (hours)",
|
||||
"maxTTL": "Max lifetime (hours)",
|
||||
"allowUsersToCancel": "Allow users to cancel in-progress workspace jobs"
|
||||
"allowUsersToCancel": "Allow users to cancel in-progress workspace jobs",
|
||||
"autostopRequirementDays": "Days with required stop",
|
||||
"autostopRequirementDays_off": "Off",
|
||||
"autostopRequirementDays_daily": "Daily",
|
||||
"autostopRequirementDays_saturday": "Saturday",
|
||||
"autostopRequirementDays_sunday": "Sunday",
|
||||
"autostopRequirementWeeks": "Weeks between required stops"
|
||||
},
|
||||
"helperText": {
|
||||
"defaultTTLHelperText_zero": "Workspaces will run until stopped manually.",
|
||||
"defaultTTLHelperText_one": "Workspaces will default to stopping after {{count}} hour. If Coder detects workspace connection activity, the autostop timer is bumped up one hour.",
|
||||
"defaultTTLHelperText_other": "Workspaces will default to stopping after {{count}} hours. If Coder detects workspace connection activity, the autostop timer is bumped up one hour.",
|
||||
"defaultTTLHelperText_one": "Workspaces will default to stopping after {{count}} hour. If Coder detects workspace connection activity, the autostop timer is bumped by this value.",
|
||||
"defaultTTLHelperText_other": "Workspaces will default to stopping after {{count}} hours. If Coder detects workspace connection activity, the autostop timer is bumped by this value.",
|
||||
"maxTTLHelperText_zero": "Workspaces may run indefinitely.",
|
||||
"maxTTLHelperText_one": "Workspaces must stop within 1 hour of starting, regardless of any active connections.",
|
||||
"maxTTLHelperText_other": "Workspaces must stop within {{count}} hours of starting, regardless of any active connections.",
|
||||
|
|
|
@ -6,19 +6,32 @@
|
|||
"descriptionMaxError": "Please enter a description that is less than or equal to 128 characters.",
|
||||
"defaultTtlLabel": "Default autostop (hours)",
|
||||
"maxTtlLabel": "Max lifetime (hours)",
|
||||
"autostopRequirementDaysLabel": "Days with required stop",
|
||||
"autostopRequirementWeeksLabel": "Weeks between required stops",
|
||||
"iconLabel": "Icon",
|
||||
"formAriaLabel": "Template settings form",
|
||||
"selectEmoji": "Select emoji",
|
||||
"defaultTTLMaxError": "Please enter a limit that is less than or equal to 720 hours (30 days).",
|
||||
"defaultTTLMinError": "Default time until autostop must not be less than 0.",
|
||||
"defaultTTLHelperText_zero": "Workspaces will run until stopped manually.",
|
||||
"defaultTTLHelperText_one": "Workspaces will default to stopping after {{count}} hour. If Coder detects workspace connection activity, the autostop timer is bumped up one hour.",
|
||||
"defaultTTLHelperText_other": "Workspaces will default to stopping after {{count}} hours. If Coder detects workspace connection activity, the autostop timer is bumped up one hour.",
|
||||
"defaultTTLHelperText_one": "Workspaces will default to stopping after {{count}} hour. If Coder detects workspace connection activity, the autostop timer is bumped by this value.",
|
||||
"defaultTTLHelperText_other": "Workspaces will default to stopping after {{count}} hours. If Coder detects workspace connection activity, the autostop timer is bumped by this value.",
|
||||
"maxTTLMaxError": "Please enter a limit that is less than or equal to 720 hours (30 days).",
|
||||
"maxTTLMinError": "Maximum time until autostop must not be less than 0.",
|
||||
"maxTTLHelperText_zero": "Workspaces may run indefinitely.",
|
||||
"maxTTLHelperText_one": "Workspaces must stop within 1 hour of starting, regardless of any active connections.",
|
||||
"maxTTLHelperText_other": "Workspaces must stop within {{count}} hours of starting, regardless of any active connections.",
|
||||
"autostopRequirementDays_off": "Off",
|
||||
"autostopRequirementDays_daily": "Daily",
|
||||
"autostopRequirementDays_saturday": "Saturday",
|
||||
"autostopRequirementDays_sunday": "Sunday",
|
||||
"autostopRequirementDaysHelperText_off": "Workspaces are not required to stop periodically.",
|
||||
"autostopRequirementDaysHelperText_daily": "Workspaces are required to be automatically stopped daily in the user's quiet hours and timezone.",
|
||||
"autostopRequirementDaysHelperText_saturday": "Workspaces are required to be automatically stopped every Saturday in the user's quiet hours and timezone.",
|
||||
"autostopRequirementDaysHelperText_sunday": "Workspaces are required to be automatically stopped every Sunday in the user's quiet hours and timezone.",
|
||||
"autostopRequirementWeeksHelperText_disabled": "Weeks between required stops cannot be set unless days between required stops is Saturday or Sunday.",
|
||||
"autostopRequirementWeeksHelperText_one": "Workspaces are required to be automatically stopped every week on the specified day in the user's quiet hours and timezone.",
|
||||
"autostopRequirementWeeksHelperText_other": "Workspaces are required to be automatically stopped every {{count}} weeks on the specified day in the user's quiet hours and timezone.",
|
||||
"failureTTLHelperText_zero": "Coder will not automatically stop failed workspaces",
|
||||
"failureTTLHelperText_one": "Coder will attempt to stop failed workspaces after {{count}} day.",
|
||||
"failureTTLHelperText_other": "Coder will attempt to stop failed workspaces after {{count}} days.",
|
||||
|
@ -43,6 +56,10 @@
|
|||
"title": "Schedule",
|
||||
"description": "Define when workspaces created from this template are stopped."
|
||||
},
|
||||
"autostopRequirement": {
|
||||
"title": "Autostop Requirement",
|
||||
"description": "Define when workspaces created from this template are stopped periodically to enforce template updates and ensure idle workspaces are stopped."
|
||||
},
|
||||
"operations": {
|
||||
"title": "Operations",
|
||||
"description": "Regulate actions allowed on workspaces created from this template."
|
||||
|
|
|
@ -40,6 +40,11 @@ import camelCase from "lodash/camelCase"
|
|||
import capitalize from "lodash/capitalize"
|
||||
import { VariableInput } from "./VariableInput"
|
||||
import { docs } from "utils/docs"
|
||||
import {
|
||||
AutostopRequirementDaysHelperText,
|
||||
AutostopRequirementWeeksHelperText,
|
||||
} from "pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/AutostopRequirementHelperText"
|
||||
import MenuItem from "@mui/material/MenuItem"
|
||||
|
||||
const MAX_DESCRIPTION_CHAR_LIMIT = 128
|
||||
const MAX_TTL_DAYS = 30
|
||||
|
@ -89,6 +94,8 @@ const validationSchema = Yup.object({
|
|||
24 * MAX_TTL_DAYS /* 30 days in hours */,
|
||||
"Please enter a limit that is less than or equal to 720 hours (30 days).",
|
||||
),
|
||||
autostop_requirement_days_of_week: Yup.string().required(),
|
||||
autostop_requirement_weeks: Yup.number().required().min(1).max(16),
|
||||
})
|
||||
|
||||
const defaultInitialValues: CreateTemplateData = {
|
||||
|
@ -103,6 +110,14 @@ const defaultInitialValues: CreateTemplateData = {
|
|||
// The maximum value is 30 days but we default to 7 days as it's a much more
|
||||
// sensible value for most teams.
|
||||
max_ttl_hours: 24 * 7,
|
||||
// autostop_requirement is an enterprise-only feature, and the server ignores
|
||||
// the value if you are not licensed. We hide the form value based on
|
||||
// entitlements.
|
||||
//
|
||||
// Default to requiring restart every Sunday in the user's quiet hours in the
|
||||
// user's timezone.
|
||||
autostop_requirement_days_of_week: "sunday",
|
||||
autostop_requirement_weeks: 1,
|
||||
allow_user_cancel_workspace_jobs: false,
|
||||
allow_user_autostart: false,
|
||||
allow_user_autostop: false,
|
||||
|
@ -180,6 +195,7 @@ export interface CreateTemplateFormProps {
|
|||
allowAdvancedScheduling: boolean
|
||||
copiedTemplate?: Template
|
||||
allowDisableEveryoneAccess: boolean
|
||||
allowAutostopRequirement: boolean
|
||||
}
|
||||
|
||||
export const CreateTemplateForm: FC<CreateTemplateFormProps> = ({
|
||||
|
@ -195,6 +211,7 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = ({
|
|||
logs,
|
||||
allowAdvancedScheduling,
|
||||
allowDisableEveryoneAccess,
|
||||
allowAutostopRequirement,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
const form = useFormik<CreateTemplateData>({
|
||||
|
@ -223,6 +240,25 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = ({
|
|||
}
|
||||
}, [logs, jobError])
|
||||
|
||||
// Set autostop_requirement weeks to 1 when days_of_week is set to "off" or
|
||||
// "daily". Technically you can set weeks to a different value in the backend
|
||||
// and it will work, but this is a UX decision so users don't set days=daily
|
||||
// and weeks=2 and get confused when workspaces only restart daily during
|
||||
// every second week.
|
||||
//
|
||||
// We want to set the value to 1 when the user selects "off" or "daily"
|
||||
// because the input gets disabled so they can't change it to 1 themselves.
|
||||
const {
|
||||
values: { autostop_requirement_days_of_week },
|
||||
setFieldValue,
|
||||
} = form
|
||||
useEffect(() => {
|
||||
if (!["saturday", "sunday"].includes(autostop_requirement_days_of_week)) {
|
||||
// This is async but we don't really need to await the value.
|
||||
void setFieldValue("autostop_requirement_weeks", 1)
|
||||
}
|
||||
}, [autostop_requirement_days_of_week, setFieldValue])
|
||||
|
||||
return (
|
||||
<HorizontalForm onSubmit={form.handleSubmit}>
|
||||
{/* General info */}
|
||||
|
@ -312,30 +348,84 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = ({
|
|||
type="number"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
{...getFieldHelpers(
|
||||
"max_ttl_hours",
|
||||
allowAdvancedScheduling ? (
|
||||
<TTLHelperText
|
||||
translationName="form.helperText.maxTTLHelperText"
|
||||
ttl={form.values.max_ttl_hours}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{commonT("licenseFieldTextHelper")}{" "}
|
||||
<Link href={docs("/enterprise")}>
|
||||
{commonT("learnMore")}
|
||||
</Link>
|
||||
.
|
||||
</>
|
||||
),
|
||||
)}
|
||||
disabled={isSubmitting || !allowAdvancedScheduling}
|
||||
fullWidth
|
||||
label={t("form.fields.maxTTL")}
|
||||
type="number"
|
||||
/>
|
||||
{!allowAutostopRequirement && (
|
||||
<TextField
|
||||
{...getFieldHelpers(
|
||||
"max_ttl_hours",
|
||||
allowAdvancedScheduling ? (
|
||||
<TTLHelperText
|
||||
translationName="form.helperText.maxTTLHelperText"
|
||||
ttl={form.values.max_ttl_hours}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{commonT("licenseFieldTextHelper")}{" "}
|
||||
<Link href={docs("/enterprise")}>
|
||||
{commonT("learnMore")}
|
||||
</Link>
|
||||
.
|
||||
</>
|
||||
),
|
||||
)}
|
||||
disabled={isSubmitting || !allowAdvancedScheduling}
|
||||
fullWidth
|
||||
label={t("form.fields.maxTTL")}
|
||||
type="number"
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{allowAutostopRequirement && (
|
||||
<Stack direction="row" className={styles.ttlFields}>
|
||||
<TextField
|
||||
{...getFieldHelpers(
|
||||
"autostop_requirement_days_of_week",
|
||||
<AutostopRequirementDaysHelperText
|
||||
days={form.values.autostop_requirement_days_of_week}
|
||||
/>,
|
||||
)}
|
||||
disabled={isSubmitting}
|
||||
fullWidth
|
||||
select
|
||||
value={form.values.autostop_requirement_days_of_week}
|
||||
label={t("form.fields.autostopRequirementDays")}
|
||||
>
|
||||
<MenuItem key="off" value="off">
|
||||
{t("form.fields.autostopRequirementDays_off")}
|
||||
</MenuItem>
|
||||
<MenuItem key="daily" value="daily">
|
||||
{t("form.fields.autostopRequirementDays_daily")}
|
||||
</MenuItem>
|
||||
<MenuItem key="saturday" value="saturday">
|
||||
{t("form.fields.autostopRequirementDays_saturday")}
|
||||
</MenuItem>
|
||||
<MenuItem key="sunday" value="sunday">
|
||||
{t("form.fields.autostopRequirementDays_sunday")}
|
||||
</MenuItem>
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
{...getFieldHelpers(
|
||||
"autostop_requirement_weeks",
|
||||
<AutostopRequirementWeeksHelperText
|
||||
days={form.values.autostop_requirement_days_of_week}
|
||||
weeks={form.values.autostop_requirement_weeks}
|
||||
/>,
|
||||
)}
|
||||
disabled={
|
||||
isSubmitting ||
|
||||
!["saturday", "sunday"].includes(
|
||||
form.values.autostop_requirement_days_of_week || "",
|
||||
)
|
||||
}
|
||||
fullWidth
|
||||
inputProps={{ min: 1, max: 16, step: 1 }}
|
||||
label={t("form.fields.autostopRequirementWeeks")}
|
||||
type="number"
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<Stack direction="column">
|
||||
<Stack direction="row" alignItems="center">
|
||||
<Checkbox
|
||||
|
|
|
@ -36,13 +36,16 @@ const CreateTemplatePage: FC = () => {
|
|||
const { starterTemplate, error, file, jobError, jobLogs, variables } =
|
||||
state.context
|
||||
const shouldDisplayForm = !state.hasTag("loading")
|
||||
const { entitlements } = useDashboard()
|
||||
const { entitlements, experiments } = useDashboard()
|
||||
const allowAdvancedScheduling =
|
||||
entitlements.features["advanced_template_scheduling"].enabled
|
||||
// Requires the template RBAC feature, otherwise disabling everyone access
|
||||
// means no one can access.
|
||||
const allowDisableEveryoneAccess =
|
||||
entitlements.features["template_rbac"].enabled
|
||||
const allowAutostopRequirement = experiments.includes(
|
||||
"template_autostop_requirement",
|
||||
)
|
||||
|
||||
const onCancel = () => {
|
||||
navigate(-1)
|
||||
|
@ -69,6 +72,7 @@ const CreateTemplatePage: FC = () => {
|
|||
copiedTemplate={state.context.copiedTemplate}
|
||||
allowAdvancedScheduling={allowAdvancedScheduling}
|
||||
allowDisableEveryoneAccess={allowDisableEveryoneAccess}
|
||||
allowAutostopRequirement={allowAutostopRequirement}
|
||||
error={error}
|
||||
starterTemplate={starterTemplate}
|
||||
isSubmitting={state.hasTag("submitting")}
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
import { Template } from "api/typesGenerated"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
export type TemplateAutostopRequirementDaysValue =
|
||||
| "off"
|
||||
| "daily"
|
||||
| "saturday"
|
||||
| "sunday"
|
||||
|
||||
export const convertAutostopRequirementDaysValue = (
|
||||
days: Template["autostop_requirement"]["days_of_week"],
|
||||
): TemplateAutostopRequirementDaysValue => {
|
||||
if (days.length === 7) {
|
||||
return "daily"
|
||||
} else if (days.length === 1 && days[0] === "saturday") {
|
||||
return "saturday"
|
||||
} else if (days.length === 1 && days[0] === "sunday") {
|
||||
return "sunday"
|
||||
}
|
||||
|
||||
// On unsupported values we default to "off".
|
||||
return "off"
|
||||
}
|
||||
|
||||
export const calculateAutostopRequirementDaysValue = (
|
||||
value: TemplateAutostopRequirementDaysValue,
|
||||
): Template["autostop_requirement"]["days_of_week"] => {
|
||||
switch (value) {
|
||||
case "daily":
|
||||
return [
|
||||
"monday",
|
||||
"tuesday",
|
||||
"wednesday",
|
||||
"thursday",
|
||||
"friday",
|
||||
"saturday",
|
||||
"sunday",
|
||||
]
|
||||
case "saturday":
|
||||
return ["saturday"]
|
||||
case "sunday":
|
||||
return ["sunday"]
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
export const AutostopRequirementDaysHelperText = ({
|
||||
days,
|
||||
}: {
|
||||
days: TemplateAutostopRequirementDaysValue
|
||||
}) => {
|
||||
const { t } = useTranslation("templateSettingsPage")
|
||||
|
||||
let str = "off"
|
||||
if (days) {
|
||||
str = days
|
||||
}
|
||||
|
||||
return <span>{t("autostopRequirementDaysHelperText_" + str)}</span>
|
||||
}
|
||||
|
||||
export const AutostopRequirementWeeksHelperText = ({
|
||||
days,
|
||||
weeks,
|
||||
}: {
|
||||
days: TemplateAutostopRequirementDaysValue
|
||||
weeks: number
|
||||
}) => {
|
||||
const { t } = useTranslation("templateSettingsPage")
|
||||
|
||||
let str = "disabled"
|
||||
if (days === "saturday" || days === "sunday") {
|
||||
if (weeks === 0 || weeks === 1) {
|
||||
str = "one"
|
||||
} else {
|
||||
str = "other"
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
{t("autostopRequirementWeeksHelperText_" + str, { count: weeks })}
|
||||
</span>
|
||||
)
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import TextField from "@mui/material/TextField"
|
||||
import { Template, UpdateTemplateMeta } from "api/typesGenerated"
|
||||
import { FormikTouched, useFormik } from "formik"
|
||||
import { FC, ChangeEvent, useState } from "react"
|
||||
import { FC, ChangeEvent, useState, useEffect } from "react"
|
||||
import { getFormHelpers } from "utils/formUtils"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import {
|
||||
|
@ -24,6 +24,13 @@ import { TemplateScheduleFormValues, getValidationSchema } from "./formHelpers"
|
|||
import { TTLHelperText } from "./TTLHelperText"
|
||||
import { docs } from "utils/docs"
|
||||
import { ScheduleDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"
|
||||
import MenuItem from "@mui/material/MenuItem"
|
||||
import {
|
||||
AutostopRequirementDaysHelperText,
|
||||
AutostopRequirementWeeksHelperText,
|
||||
calculateAutostopRequirementDaysValue,
|
||||
convertAutostopRequirementDaysValue,
|
||||
} from "./AutostopRequirementHelperText"
|
||||
|
||||
const MS_HOUR_CONVERSION = 3600000
|
||||
const MS_DAY_CONVERSION = 86400000
|
||||
|
@ -39,6 +46,7 @@ export interface TemplateScheduleForm {
|
|||
error?: unknown
|
||||
allowAdvancedScheduling: boolean
|
||||
allowWorkspaceActions: boolean
|
||||
allowAutostopRequirement: boolean
|
||||
// Helpful to show field errors on Storybook
|
||||
initialTouched?: FormikTouched<UpdateTemplateMeta>
|
||||
}
|
||||
|
@ -50,6 +58,7 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
|
|||
error,
|
||||
allowAdvancedScheduling,
|
||||
allowWorkspaceActions,
|
||||
allowAutostopRequirement,
|
||||
isSubmitting,
|
||||
initialTouched,
|
||||
}) => {
|
||||
|
@ -74,10 +83,16 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
|
|||
? template.time_til_dormant_autodelete_ms / MS_DAY_CONVERSION
|
||||
: 0,
|
||||
|
||||
autostop_requirement: {
|
||||
days_of_week: template.autostop_requirement.days_of_week,
|
||||
weeks: template.autostop_requirement.weeks,
|
||||
},
|
||||
autostop_requirement_days_of_week: allowAutostopRequirement
|
||||
? convertAutostopRequirementDaysValue(
|
||||
template.autostop_requirement.days_of_week,
|
||||
)
|
||||
: "off",
|
||||
autostop_requirement_weeks: allowAutostopRequirement
|
||||
? template.autostop_requirement.weeks > 0
|
||||
? template.autostop_requirement.weeks
|
||||
: 1
|
||||
: 1,
|
||||
|
||||
allow_user_autostart: template.allow_user_autostart,
|
||||
allow_user_autostop: template.allow_user_autostop,
|
||||
|
@ -120,6 +135,7 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
|
|||
},
|
||||
initialTouched,
|
||||
})
|
||||
|
||||
const getFieldHelpers = getFormHelpers<TemplateScheduleFormValues>(
|
||||
form,
|
||||
error,
|
||||
|
@ -167,6 +183,12 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
|
|||
useState<boolean>(false)
|
||||
|
||||
const submitValues = () => {
|
||||
const autostop_requirement_weeks = ["saturday", "sunday"].includes(
|
||||
form.values.autostop_requirement_days_of_week,
|
||||
)
|
||||
? form.values.autostop_requirement_weeks
|
||||
: 1
|
||||
|
||||
// on submit, convert from hours => ms
|
||||
onSubmit({
|
||||
default_ttl_ms: form.values.default_ttl_ms
|
||||
|
@ -185,6 +207,13 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
|
|||
? form.values.time_til_dormant_autodelete_ms * MS_DAY_CONVERSION
|
||||
: undefined,
|
||||
|
||||
autostop_requirement: {
|
||||
days_of_week: calculateAutostopRequirementDaysValue(
|
||||
form.values.autostop_requirement_days_of_week,
|
||||
),
|
||||
weeks: autostop_requirement_weeks,
|
||||
},
|
||||
|
||||
allow_user_autostart: form.values.allow_user_autostart,
|
||||
allow_user_autostop: form.values.allow_user_autostop,
|
||||
update_workspace_last_used_at: form.values.update_workspace_last_used_at,
|
||||
|
@ -192,6 +221,30 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
|
|||
})
|
||||
}
|
||||
|
||||
// Set autostop_requirement weeks to 1 when days_of_week is set to "off" or
|
||||
// "daily". Technically you can set weeks to a different value in the backend
|
||||
// and it will work, but this is a UX decision so users don't set days=daily
|
||||
// and weeks=2 and get confused when workspaces only restart daily during
|
||||
// every second week.
|
||||
//
|
||||
// We want to set the value to 1 when the user selects "off" or "daily"
|
||||
// because the input gets disabled so they can't change it to 1 themselves.
|
||||
const { values: currentValues, setValues } = form
|
||||
useEffect(() => {
|
||||
if (
|
||||
!["saturday", "sunday"].includes(
|
||||
currentValues.autostop_requirement_days_of_week,
|
||||
) &&
|
||||
currentValues.autostop_requirement_weeks !== 1
|
||||
) {
|
||||
// This is async but we don't really need to await the value.
|
||||
void setValues({
|
||||
...currentValues,
|
||||
autostop_requirement_weeks: 1,
|
||||
})
|
||||
}
|
||||
}, [currentValues, setValues])
|
||||
|
||||
const handleToggleFailureCleanup = async (e: ChangeEvent) => {
|
||||
form.handleChange(e)
|
||||
if (!form.values.failure_cleanup_enabled) {
|
||||
|
@ -274,31 +327,91 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
|
|||
type="number"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
{...getFieldHelpers(
|
||||
"max_ttl_ms",
|
||||
allowAdvancedScheduling ? (
|
||||
<TTLHelperText
|
||||
translationName="maxTTLHelperText"
|
||||
ttl={form.values.max_ttl_ms}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{commonT("licenseFieldTextHelper")}{" "}
|
||||
<Link href={docs("/enterprise")}>{commonT("learnMore")}</Link>
|
||||
.
|
||||
</>
|
||||
),
|
||||
)}
|
||||
disabled={isSubmitting || !allowAdvancedScheduling}
|
||||
fullWidth
|
||||
inputProps={{ min: 0, step: 1 }}
|
||||
label={t("maxTtlLabel")}
|
||||
type="number"
|
||||
/>
|
||||
{!allowAutostopRequirement && (
|
||||
<TextField
|
||||
{...getFieldHelpers(
|
||||
"max_ttl_ms",
|
||||
allowAdvancedScheduling ? (
|
||||
<TTLHelperText
|
||||
translationName="maxTTLHelperText"
|
||||
ttl={form.values.max_ttl_ms}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{commonT("licenseFieldTextHelper")}{" "}
|
||||
<Link href={docs("/enterprise")}>
|
||||
{commonT("learnMore")}
|
||||
</Link>
|
||||
.
|
||||
</>
|
||||
),
|
||||
)}
|
||||
disabled={isSubmitting || !allowAdvancedScheduling}
|
||||
fullWidth
|
||||
inputProps={{ min: 0, step: 1 }}
|
||||
label={t("maxTtlLabel")}
|
||||
type="number"
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</FormSection>
|
||||
|
||||
{allowAutostopRequirement && (
|
||||
<FormSection
|
||||
title={t("autostopRequirement.title").toString()}
|
||||
description={t("autostopRequirement.description").toString()}
|
||||
>
|
||||
<Stack direction="row" className={styles.ttlFields}>
|
||||
<TextField
|
||||
{...getFieldHelpers(
|
||||
"autostop_requirement_days_of_week",
|
||||
<AutostopRequirementDaysHelperText
|
||||
days={form.values.autostop_requirement_days_of_week}
|
||||
/>,
|
||||
)}
|
||||
disabled={isSubmitting}
|
||||
fullWidth
|
||||
select
|
||||
value={form.values.autostop_requirement_days_of_week}
|
||||
label={t("autostopRequirementDaysLabel")}
|
||||
>
|
||||
<MenuItem key="off" value="off">
|
||||
{t("autostopRequirementDays_off")}
|
||||
</MenuItem>
|
||||
<MenuItem key="daily" value="daily">
|
||||
{t("autostopRequirementDays_daily")}
|
||||
</MenuItem>
|
||||
<MenuItem key="saturday" value="saturday">
|
||||
{t("autostopRequirementDays_saturday")}
|
||||
</MenuItem>
|
||||
<MenuItem key="sunday" value="sunday">
|
||||
{t("autostopRequirementDays_sunday")}
|
||||
</MenuItem>
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
{...getFieldHelpers(
|
||||
"autostop_requirement_weeks",
|
||||
<AutostopRequirementWeeksHelperText
|
||||
days={form.values.autostop_requirement_days_of_week}
|
||||
weeks={form.values.autostop_requirement_weeks}
|
||||
/>,
|
||||
)}
|
||||
disabled={
|
||||
isSubmitting ||
|
||||
!["saturday", "sunday"].includes(
|
||||
form.values.autostop_requirement_days_of_week || "",
|
||||
)
|
||||
}
|
||||
fullWidth
|
||||
inputProps={{ min: 1, max: 16, step: 1 }}
|
||||
label={t("autostopRequirementWeeksLabel")}
|
||||
type="number"
|
||||
/>
|
||||
</Stack>
|
||||
</FormSection>
|
||||
)}
|
||||
|
||||
<FormSection
|
||||
title="Allow users scheduling"
|
||||
description="Allow users to set custom autostart and autostop scheduling options for workspaces created from this template."
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import { UpdateTemplateMeta } from "api/typesGenerated"
|
||||
import * as Yup from "yup"
|
||||
import i18next from "i18next"
|
||||
import { TemplateAutostopRequirementDaysValue } from "./AutostopRequirementHelperText"
|
||||
|
||||
export interface TemplateScheduleFormValues extends UpdateTemplateMeta {
|
||||
export interface TemplateScheduleFormValues
|
||||
extends Omit<UpdateTemplateMeta, "autostop_requirement"> {
|
||||
autostop_requirement_days_of_week: TemplateAutostopRequirementDaysValue
|
||||
autostop_requirement_weeks: number
|
||||
failure_cleanup_enabled: boolean
|
||||
inactivity_cleanup_enabled: boolean
|
||||
dormant_autodeletion_cleanup_enabled: boolean
|
||||
|
@ -80,4 +84,7 @@ export const getValidationSchema = (): Yup.AnyObjectSchema =>
|
|||
),
|
||||
allow_user_autostart: Yup.boolean(),
|
||||
allow_user_autostop: Yup.boolean(),
|
||||
|
||||
autostop_requirement_days_of_week: Yup.string().required(),
|
||||
autostop_requirement_weeks: Yup.number().required().min(1).max(16),
|
||||
})
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { screen, waitFor } from "@testing-library/react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
import * as API from "api/api"
|
||||
import { UpdateTemplateMeta } from "api/typesGenerated"
|
||||
import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter"
|
||||
import {
|
||||
MockEntitlementsWithScheduling,
|
||||
|
@ -11,13 +10,16 @@ import {
|
|||
renderWithTemplateSettingsLayout,
|
||||
waitForLoaderToBeRemoved,
|
||||
} from "testHelpers/renderHelpers"
|
||||
import { getValidationSchema } from "./TemplateScheduleForm/formHelpers"
|
||||
import {
|
||||
TemplateScheduleFormValues,
|
||||
getValidationSchema,
|
||||
} from "./TemplateScheduleForm/formHelpers"
|
||||
import TemplateSchedulePage from "./TemplateSchedulePage"
|
||||
import i18next from "i18next"
|
||||
|
||||
const { t } = i18next
|
||||
|
||||
const validFormValues = {
|
||||
const validFormValues: TemplateScheduleFormValues = {
|
||||
default_ttl_ms: 1,
|
||||
max_ttl_ms: 2,
|
||||
failure_ttl_ms: 7,
|
||||
|
@ -25,6 +27,11 @@ const validFormValues = {
|
|||
time_til_dormant_autodelete_ms: 30,
|
||||
update_workspace_last_used_at: false,
|
||||
update_workspace_dormant_at: false,
|
||||
autostop_requirement_days_of_week: "off",
|
||||
autostop_requirement_weeks: 1,
|
||||
failure_cleanup_enabled: false,
|
||||
inactivity_cleanup_enabled: false,
|
||||
dormant_autodeletion_cleanup_enabled: false,
|
||||
}
|
||||
|
||||
const renderTemplateSchedulePage = async () => {
|
||||
|
@ -42,40 +49,51 @@ const fillAndSubmitForm = async ({
|
|||
time_til_dormant_ms,
|
||||
time_til_dormant_autodelete_ms,
|
||||
}: {
|
||||
default_ttl_ms: number
|
||||
max_ttl_ms: number
|
||||
failure_ttl_ms: number
|
||||
time_til_dormant_ms: number
|
||||
time_til_dormant_autodelete_ms: number
|
||||
default_ttl_ms?: number
|
||||
max_ttl_ms?: number
|
||||
failure_ttl_ms?: number
|
||||
time_til_dormant_ms?: number
|
||||
time_til_dormant_autodelete_ms?: number
|
||||
}) => {
|
||||
const user = userEvent.setup()
|
||||
const defaultTtlLabel = t("defaultTtlLabel", { ns: "templateSettingsPage" })
|
||||
const defaultTtlField = await screen.findByLabelText(defaultTtlLabel)
|
||||
await user.clear(defaultTtlField)
|
||||
await user.type(defaultTtlField, default_ttl_ms.toString())
|
||||
|
||||
const maxTtlLabel = t("maxTtlLabel", { ns: "templateSettingsPage" })
|
||||
const maxTtlField = await screen.findByLabelText(maxTtlLabel)
|
||||
await user.clear(maxTtlField)
|
||||
await user.type(maxTtlField, max_ttl_ms.toString())
|
||||
if (default_ttl_ms) {
|
||||
const defaultTtlLabel = t("defaultTtlLabel", { ns: "templateSettingsPage" })
|
||||
const defaultTtlField = await screen.findByLabelText(defaultTtlLabel)
|
||||
await user.clear(defaultTtlField)
|
||||
await user.type(defaultTtlField, default_ttl_ms.toString())
|
||||
}
|
||||
|
||||
const failureTtlField = screen.getByRole("checkbox", {
|
||||
name: /Failure Cleanup/i,
|
||||
})
|
||||
await user.type(failureTtlField, failure_ttl_ms.toString())
|
||||
if (max_ttl_ms) {
|
||||
const maxTtlLabel = t("maxTtlLabel", { ns: "templateSettingsPage" })
|
||||
const maxTtlField = await screen.findByLabelText(maxTtlLabel)
|
||||
await user.clear(maxTtlField)
|
||||
await user.type(maxTtlField, max_ttl_ms.toString())
|
||||
}
|
||||
|
||||
const inactivityTtlField = screen.getByRole("checkbox", {
|
||||
name: /Dormancy Threshold/i,
|
||||
})
|
||||
await user.type(inactivityTtlField, time_til_dormant_ms.toString())
|
||||
if (failure_ttl_ms) {
|
||||
const failureTtlField = screen.getByRole("checkbox", {
|
||||
name: /Failure Cleanup/i,
|
||||
})
|
||||
await user.type(failureTtlField, failure_ttl_ms.toString())
|
||||
}
|
||||
|
||||
const dormancyAutoDeletionField = screen.getByRole("checkbox", {
|
||||
name: /Dormancy Auto-Deletion/i,
|
||||
})
|
||||
await user.type(
|
||||
dormancyAutoDeletionField,
|
||||
time_til_dormant_autodelete_ms.toString(),
|
||||
)
|
||||
if (time_til_dormant_ms) {
|
||||
const inactivityTtlField = screen.getByRole("checkbox", {
|
||||
name: /Dormancy Threshold/i,
|
||||
})
|
||||
await user.type(inactivityTtlField, time_til_dormant_ms.toString())
|
||||
}
|
||||
|
||||
if (time_til_dormant_autodelete_ms) {
|
||||
const dormancyAutoDeletionField = screen.getByRole("checkbox", {
|
||||
name: /Dormancy Auto-Deletion/i,
|
||||
})
|
||||
await user.type(
|
||||
dormancyAutoDeletionField,
|
||||
time_til_dormant_autodelete_ms.toString(),
|
||||
)
|
||||
}
|
||||
|
||||
const submitButton = await screen.findByText(
|
||||
FooterFormLanguage.defaultSubmitLabel,
|
||||
|
@ -121,8 +139,8 @@ describe("TemplateSchedulePage", () => {
|
|||
expect(API.updateTemplateMeta).toBeCalledWith(
|
||||
"test-template",
|
||||
expect.objectContaining({
|
||||
default_ttl_ms: validFormValues.default_ttl_ms * 3600000,
|
||||
max_ttl_ms: validFormValues.max_ttl_ms * 3600000,
|
||||
default_ttl_ms: (validFormValues.default_ttl_ms || 0) * 3600000,
|
||||
max_ttl_ms: (validFormValues.max_ttl_ms || 0) * 3600000,
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
@ -142,17 +160,18 @@ describe("TemplateSchedulePage", () => {
|
|||
expect(API.updateTemplateMeta).toBeCalledWith(
|
||||
"test-template",
|
||||
expect.objectContaining({
|
||||
failure_ttl_ms: validFormValues.failure_ttl_ms * 86400000,
|
||||
time_til_dormant_ms: validFormValues.time_til_dormant_ms * 86400000,
|
||||
failure_ttl_ms: (validFormValues.failure_ttl_ms || 0) * 86400000,
|
||||
time_til_dormant_ms:
|
||||
(validFormValues.time_til_dormant_ms || 0) * 86400000,
|
||||
time_til_dormant_autodelete_ms:
|
||||
validFormValues.time_til_dormant_autodelete_ms * 86400000,
|
||||
(validFormValues.time_til_dormant_autodelete_ms || 0) * 86400000,
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
it("allows a default ttl of 7 days", () => {
|
||||
const values: UpdateTemplateMeta = {
|
||||
const values: TemplateScheduleFormValues = {
|
||||
...validFormValues,
|
||||
default_ttl_ms: 24 * 7,
|
||||
}
|
||||
|
@ -161,7 +180,7 @@ describe("TemplateSchedulePage", () => {
|
|||
})
|
||||
|
||||
it("allows default ttl of 0", () => {
|
||||
const values: UpdateTemplateMeta = {
|
||||
const values: TemplateScheduleFormValues = {
|
||||
...validFormValues,
|
||||
default_ttl_ms: 0,
|
||||
}
|
||||
|
@ -170,7 +189,7 @@ describe("TemplateSchedulePage", () => {
|
|||
})
|
||||
|
||||
it("allows a default ttl of 30 days", () => {
|
||||
const values: UpdateTemplateMeta = {
|
||||
const values: TemplateScheduleFormValues = {
|
||||
...validFormValues,
|
||||
default_ttl_ms: 24 * 30,
|
||||
}
|
||||
|
@ -179,7 +198,7 @@ describe("TemplateSchedulePage", () => {
|
|||
})
|
||||
|
||||
it("disallows a default ttl of 30 days + 1 hour", () => {
|
||||
const values: UpdateTemplateMeta = {
|
||||
const values: TemplateScheduleFormValues = {
|
||||
...validFormValues,
|
||||
default_ttl_ms: 24 * 30 + 1,
|
||||
}
|
||||
|
@ -190,7 +209,7 @@ describe("TemplateSchedulePage", () => {
|
|||
})
|
||||
|
||||
it("allows a failure ttl of 7 days", () => {
|
||||
const values: UpdateTemplateMeta = {
|
||||
const values: TemplateScheduleFormValues = {
|
||||
...validFormValues,
|
||||
failure_ttl_ms: 86400000 * 7,
|
||||
}
|
||||
|
@ -199,7 +218,7 @@ describe("TemplateSchedulePage", () => {
|
|||
})
|
||||
|
||||
it("allows failure ttl of 0", () => {
|
||||
const values: UpdateTemplateMeta = {
|
||||
const values: TemplateScheduleFormValues = {
|
||||
...validFormValues,
|
||||
failure_ttl_ms: 0,
|
||||
}
|
||||
|
@ -208,7 +227,7 @@ describe("TemplateSchedulePage", () => {
|
|||
})
|
||||
|
||||
it("disallows a negative failure ttl", () => {
|
||||
const values: UpdateTemplateMeta = {
|
||||
const values: TemplateScheduleFormValues = {
|
||||
...validFormValues,
|
||||
failure_ttl_ms: -1,
|
||||
}
|
||||
|
@ -219,7 +238,7 @@ describe("TemplateSchedulePage", () => {
|
|||
})
|
||||
|
||||
it("allows an inactivity ttl of 7 days", () => {
|
||||
const values: UpdateTemplateMeta = {
|
||||
const values: TemplateScheduleFormValues = {
|
||||
...validFormValues,
|
||||
time_til_dormant_ms: 86400000 * 7,
|
||||
}
|
||||
|
@ -228,7 +247,7 @@ describe("TemplateSchedulePage", () => {
|
|||
})
|
||||
|
||||
it("allows an inactivity ttl of 0", () => {
|
||||
const values: UpdateTemplateMeta = {
|
||||
const values: TemplateScheduleFormValues = {
|
||||
...validFormValues,
|
||||
time_til_dormant_ms: 0,
|
||||
}
|
||||
|
@ -237,7 +256,7 @@ describe("TemplateSchedulePage", () => {
|
|||
})
|
||||
|
||||
it("disallows a negative inactivity ttl", () => {
|
||||
const values: UpdateTemplateMeta = {
|
||||
const values: TemplateScheduleFormValues = {
|
||||
...validFormValues,
|
||||
time_til_dormant_ms: -1,
|
||||
}
|
||||
|
@ -248,7 +267,7 @@ describe("TemplateSchedulePage", () => {
|
|||
})
|
||||
|
||||
it("allows a dormancy ttl of 7 days", () => {
|
||||
const values: UpdateTemplateMeta = {
|
||||
const values: TemplateScheduleFormValues = {
|
||||
...validFormValues,
|
||||
time_til_dormant_autodelete_ms: 86400000 * 7,
|
||||
}
|
||||
|
@ -257,7 +276,7 @@ describe("TemplateSchedulePage", () => {
|
|||
})
|
||||
|
||||
it("allows a dormancy ttl of 0", () => {
|
||||
const values: UpdateTemplateMeta = {
|
||||
const values: TemplateScheduleFormValues = {
|
||||
...validFormValues,
|
||||
time_til_dormant_autodelete_ms: 0,
|
||||
}
|
||||
|
@ -266,7 +285,7 @@ describe("TemplateSchedulePage", () => {
|
|||
})
|
||||
|
||||
it("disallows a negative inactivity ttl", () => {
|
||||
const values: UpdateTemplateMeta = {
|
||||
const values: TemplateScheduleFormValues = {
|
||||
...validFormValues,
|
||||
time_til_dormant_autodelete_ms: -1,
|
||||
}
|
||||
|
@ -275,4 +294,41 @@ describe("TemplateSchedulePage", () => {
|
|||
"Dormancy auto-deletion days must not be less than 0.",
|
||||
)
|
||||
})
|
||||
|
||||
it("allows an autostop requirement weeks of 1", () => {
|
||||
const values: TemplateScheduleFormValues = {
|
||||
...validFormValues,
|
||||
autostop_requirement_days_of_week: "saturday",
|
||||
autostop_requirement_weeks: 1,
|
||||
}
|
||||
const validate = () => getValidationSchema().validateSync(values)
|
||||
expect(validate).not.toThrowError()
|
||||
})
|
||||
|
||||
it("allows a autostop requirement weeks of 16", () => {
|
||||
const values: TemplateScheduleFormValues = {
|
||||
...validFormValues,
|
||||
autostop_requirement_weeks: 16,
|
||||
}
|
||||
const validate = () => getValidationSchema().validateSync(values)
|
||||
expect(validate).not.toThrowError()
|
||||
})
|
||||
|
||||
it("disallows a autostop requirement weeks of 0", () => {
|
||||
const values: TemplateScheduleFormValues = {
|
||||
...validFormValues,
|
||||
autostop_requirement_weeks: 0,
|
||||
}
|
||||
const validate = () => getValidationSchema().validateSync(values)
|
||||
expect(validate).toThrowError()
|
||||
})
|
||||
|
||||
it("disallows a autostop requirement weeks of 17", () => {
|
||||
const values: TemplateScheduleFormValues = {
|
||||
...validFormValues,
|
||||
autostop_requirement_weeks: 17,
|
||||
}
|
||||
const validate = () => getValidationSchema().validateSync(values)
|
||||
expect(validate).toThrowError()
|
||||
})
|
||||
})
|
||||
|
|
|
@ -21,6 +21,9 @@ const TemplateSchedulePage: FC = () => {
|
|||
// This check can be removed when https://github.com/coder/coder/milestone/19
|
||||
// is merged up
|
||||
const allowWorkspaceActions = experiments.includes("workspace_actions")
|
||||
const allowAutostopRequirement = experiments.includes(
|
||||
"template_autostop_requirement",
|
||||
)
|
||||
const { clearLocal } = useLocalStorage()
|
||||
|
||||
const {
|
||||
|
@ -47,6 +50,7 @@ const TemplateSchedulePage: FC = () => {
|
|||
<TemplateSchedulePageView
|
||||
allowAdvancedScheduling={allowAdvancedScheduling}
|
||||
allowWorkspaceActions={allowWorkspaceActions}
|
||||
allowAutostopRequirement={allowAutostopRequirement}
|
||||
isSubmitting={isSubmitting}
|
||||
template={template}
|
||||
submitError={submitError}
|
||||
|
|
|
@ -13,6 +13,7 @@ export interface TemplateSchedulePageViewProps {
|
|||
initialTouched?: ComponentProps<typeof TemplateScheduleForm>["initialTouched"]
|
||||
allowAdvancedScheduling: boolean
|
||||
allowWorkspaceActions: boolean
|
||||
allowAutostopRequirement: boolean
|
||||
}
|
||||
|
||||
export const TemplateSchedulePageView: FC<TemplateSchedulePageViewProps> = ({
|
||||
|
@ -22,6 +23,7 @@ export const TemplateSchedulePageView: FC<TemplateSchedulePageViewProps> = ({
|
|||
isSubmitting,
|
||||
allowAdvancedScheduling,
|
||||
allowWorkspaceActions,
|
||||
allowAutostopRequirement,
|
||||
submitError,
|
||||
initialTouched,
|
||||
}) => {
|
||||
|
@ -36,6 +38,7 @@ export const TemplateSchedulePageView: FC<TemplateSchedulePageViewProps> = ({
|
|||
<TemplateScheduleForm
|
||||
allowAdvancedScheduling={allowAdvancedScheduling}
|
||||
allowWorkspaceActions={allowWorkspaceActions}
|
||||
allowAutostopRequirement={allowAutostopRequirement}
|
||||
initialTouched={initialTouched}
|
||||
isSubmitting={isSubmitting}
|
||||
template={template}
|
||||
|
|
|
@ -20,6 +20,10 @@ import {
|
|||
VariableValue,
|
||||
} from "api/typesGenerated"
|
||||
import { displayError } from "components/GlobalSnackbar/utils"
|
||||
import {
|
||||
TemplateAutostopRequirementDaysValue,
|
||||
calculateAutostopRequirementDaysValue,
|
||||
} from "pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/AutostopRequirementHelperText"
|
||||
import { delay } from "utils/delay"
|
||||
import { assign, createMachine } from "xstate"
|
||||
|
||||
|
@ -45,6 +49,8 @@ export interface CreateTemplateData {
|
|||
icon: string
|
||||
default_ttl_hours: number
|
||||
max_ttl_hours: number
|
||||
autostop_requirement_days_of_week: TemplateAutostopRequirementDaysValue
|
||||
autostop_requirement_weeks: number
|
||||
allow_user_autostart: boolean
|
||||
allow_user_autostop: boolean
|
||||
allow_user_cancel_workspace_jobs: boolean
|
||||
|
@ -464,15 +470,24 @@ export const createTemplateMachine =
|
|||
max_ttl_hours,
|
||||
parameter_values_by_name,
|
||||
allow_everyone_group_access,
|
||||
autostop_requirement_days_of_week,
|
||||
autostop_requirement_weeks,
|
||||
...safeTemplateData
|
||||
} = templateData
|
||||
|
||||
return createTemplate(organizationId, {
|
||||
...safeTemplateData,
|
||||
disable_everyone_group_access: !allow_everyone_group_access,
|
||||
disable_everyone_group_access:
|
||||
!templateData.allow_everyone_group_access,
|
||||
default_ttl_ms: templateData.default_ttl_hours * 60 * 60 * 1000, // Convert hours to ms
|
||||
max_ttl_ms: templateData.max_ttl_hours * 60 * 60 * 1000, // Convert hours to ms
|
||||
template_version_id: version.id,
|
||||
autostop_requirement: {
|
||||
days_of_week: calculateAutostopRequirementDaysValue(
|
||||
templateData.autostop_requirement_days_of_week,
|
||||
),
|
||||
weeks: templateData.autostop_requirement_weeks,
|
||||
},
|
||||
})
|
||||
},
|
||||
loadVersionLogs: ({ version }) => {
|
||||
|
|
Loading…
Reference in New Issue