mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: allow templates to specify max_ttl or autostop_requirement (#10920)
This commit is contained in:
Generated
+1
-4
@@ -958,6 +958,7 @@ export interface Template {
|
||||
readonly deprecation_message: string;
|
||||
readonly icon: string;
|
||||
readonly default_ttl_ms: number;
|
||||
readonly use_max_ttl: boolean;
|
||||
readonly max_ttl_ms: number;
|
||||
readonly autostop_requirement: TemplateAutostopRequirement;
|
||||
readonly autostart_requirement: TemplateAutostartRequirement;
|
||||
@@ -1776,7 +1777,6 @@ export type Experiment =
|
||||
| "moons"
|
||||
| "single_tailnet"
|
||||
| "tailnet_pg_coordinator"
|
||||
| "template_autostop_requirement"
|
||||
| "template_update_policies"
|
||||
| "workspace_actions";
|
||||
export const Experiments: Experiment[] = [
|
||||
@@ -1784,7 +1784,6 @@ export const Experiments: Experiment[] = [
|
||||
"moons",
|
||||
"single_tailnet",
|
||||
"tailnet_pg_coordinator",
|
||||
"template_autostop_requirement",
|
||||
"template_update_policies",
|
||||
"workspace_actions",
|
||||
];
|
||||
@@ -1801,7 +1800,6 @@ export type FeatureName =
|
||||
| "high_availability"
|
||||
| "multiple_external_auth"
|
||||
| "scim"
|
||||
| "template_autostop_requirement"
|
||||
| "template_rbac"
|
||||
| "user_limit"
|
||||
| "user_role_management"
|
||||
@@ -1818,7 +1816,6 @@ export const FeatureNames: FeatureName[] = [
|
||||
"high_availability",
|
||||
"multiple_external_auth",
|
||||
"scim",
|
||||
"template_autostop_requirement",
|
||||
"template_rbac",
|
||||
"user_limit",
|
||||
"user_role_management",
|
||||
|
||||
@@ -122,6 +122,23 @@ export const AlphaBadge: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const DeprecatedBadge: FC = () => {
|
||||
return (
|
||||
<span
|
||||
css={[
|
||||
styles.badge,
|
||||
{
|
||||
border: `1px solid ${colors.orange[600]}`,
|
||||
backgroundColor: colors.orange[950],
|
||||
color: colors.orange[50],
|
||||
},
|
||||
]}
|
||||
>
|
||||
Deprecated
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export const Badges: FC<PropsWithChildren> = ({ children }) => {
|
||||
return (
|
||||
<Stack
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
type PropsWithChildren,
|
||||
useContext,
|
||||
} from "react";
|
||||
import { AlphaBadge } from "components/Badges/Badges";
|
||||
import { AlphaBadge, DeprecatedBadge } from "components/Badges/Badges";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import {
|
||||
FormFooter as BaseFormFooter,
|
||||
@@ -77,8 +77,16 @@ export const FormSection: FC<
|
||||
infoTitle?: string;
|
||||
};
|
||||
alpha?: boolean;
|
||||
deprecated?: boolean;
|
||||
}
|
||||
> = ({ children, title, description, classes = {}, alpha = false }) => {
|
||||
> = ({
|
||||
children,
|
||||
title,
|
||||
description,
|
||||
classes = {},
|
||||
alpha = false,
|
||||
deprecated = false,
|
||||
}) => {
|
||||
const { direction } = useContext(FormContext);
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -121,6 +129,7 @@ export const FormSection: FC<
|
||||
>
|
||||
{title}
|
||||
{alpha && <AlphaBadge />}
|
||||
{deprecated && <DeprecatedBadge />}
|
||||
</h2>
|
||||
<div css={styles.formSectionInfoDescription}>{description}</div>
|
||||
</div>
|
||||
|
||||
@@ -60,6 +60,7 @@ export interface CreateTemplateData {
|
||||
description: string;
|
||||
icon: string;
|
||||
default_ttl_hours: number;
|
||||
use_max_ttl: boolean;
|
||||
max_ttl_hours: number;
|
||||
autostart_requirement_days_of_week: TemplateAutostartRequirementDaysValue[];
|
||||
autostop_requirement_days_of_week: TemplateAutostopRequirementDaysValue;
|
||||
@@ -110,6 +111,7 @@ 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.
|
||||
use_max_ttl: false, // autostop_requirement is default
|
||||
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
|
||||
@@ -145,6 +147,8 @@ const getInitialValues = ({
|
||||
initialValues = {
|
||||
...initialValues,
|
||||
max_ttl_hours: 0,
|
||||
autostop_requirement_days_of_week: "off",
|
||||
autostop_requirement_weeks: 1,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -202,7 +206,6 @@ export type CreateTemplateFormProps = (
|
||||
logs?: ProvisionerJobLog[];
|
||||
allowAdvancedScheduling: boolean;
|
||||
allowDisableEveryoneAccess: boolean;
|
||||
allowAutostopRequirement: boolean;
|
||||
};
|
||||
|
||||
export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
|
||||
@@ -216,7 +219,6 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
|
||||
logs,
|
||||
allowAdvancedScheduling,
|
||||
allowDisableEveryoneAccess,
|
||||
allowAutostopRequirement,
|
||||
} = props;
|
||||
const form = useFormik<CreateTemplateData>({
|
||||
initialValues: getInitialValues({
|
||||
@@ -262,6 +264,27 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
|
||||
}
|
||||
}, [autostop_requirement_days_of_week, setFieldValue]);
|
||||
|
||||
const handleToggleUseMaxTTL = async () => {
|
||||
const val = !form.values.use_max_ttl;
|
||||
if (val) {
|
||||
// set max_ttl to 1, set autostop_requirement to empty
|
||||
await form.setValues({
|
||||
...form.values,
|
||||
use_max_ttl: val,
|
||||
max_ttl_hours: 1,
|
||||
autostop_requirement_days_of_week: "off",
|
||||
autostop_requirement_weeks: 1,
|
||||
});
|
||||
} else {
|
||||
// set max_ttl to 0
|
||||
await form.setValues({
|
||||
...form.values,
|
||||
use_max_ttl: val,
|
||||
max_ttl_hours: 0,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<HorizontalForm onSubmit={form.handleSubmit}>
|
||||
{/* General info */}
|
||||
@@ -349,78 +372,107 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
|
||||
label="Default autostop (hours)"
|
||||
type="number"
|
||||
/>
|
||||
|
||||
{!allowAutostopRequirement && (
|
||||
<TextField
|
||||
{...getFieldHelpers(
|
||||
"max_ttl_hours",
|
||||
allowAdvancedScheduling ? (
|
||||
<MaxTTLHelperText ttl={form.values.max_ttl_hours} />
|
||||
) : (
|
||||
<>
|
||||
You need an enterprise license to use it.{" "}
|
||||
<Link href={docs("/enterprise")}>Learn more</Link>.
|
||||
</>
|
||||
),
|
||||
)}
|
||||
disabled={isSubmitting || !allowAdvancedScheduling}
|
||||
fullWidth
|
||||
label="Max lifetime (hours)"
|
||||
type="number"
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{allowAutostopRequirement && (
|
||||
<Stack direction="row" css={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="Days with required stop"
|
||||
>
|
||||
<MenuItem key="off" value="off">
|
||||
Off
|
||||
</MenuItem>
|
||||
<MenuItem key="daily" value="daily">
|
||||
Daily
|
||||
</MenuItem>
|
||||
<MenuItem key="saturday" value="saturday">
|
||||
Saturday
|
||||
</MenuItem>
|
||||
<MenuItem key="sunday" value="sunday">
|
||||
Sunday
|
||||
</MenuItem>
|
||||
</TextField>
|
||||
<Stack direction="row" css={styles.ttlFields}>
|
||||
<TextField
|
||||
{...getFieldHelpers(
|
||||
"autostop_requirement_days_of_week",
|
||||
<AutostopRequirementDaysHelperText
|
||||
days={form.values.autostop_requirement_days_of_week}
|
||||
/>,
|
||||
)}
|
||||
disabled={
|
||||
isSubmitting ||
|
||||
form.values.use_max_ttl ||
|
||||
!allowAdvancedScheduling
|
||||
}
|
||||
fullWidth
|
||||
select
|
||||
value={form.values.autostop_requirement_days_of_week}
|
||||
label="Days with required stop"
|
||||
>
|
||||
<MenuItem key="off" value="off">
|
||||
Off
|
||||
</MenuItem>
|
||||
<MenuItem key="daily" value="daily">
|
||||
Daily
|
||||
</MenuItem>
|
||||
<MenuItem key="saturday" value="saturday">
|
||||
Saturday
|
||||
</MenuItem>
|
||||
<MenuItem key="sunday" value="sunday">
|
||||
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="Weeks between required stops"
|
||||
type="number"
|
||||
<TextField
|
||||
{...getFieldHelpers(
|
||||
"autostop_requirement_weeks",
|
||||
<AutostopRequirementWeeksHelperText
|
||||
days={form.values.autostop_requirement_days_of_week}
|
||||
weeks={form.values.autostop_requirement_weeks}
|
||||
/>,
|
||||
)}
|
||||
disabled={
|
||||
isSubmitting ||
|
||||
form.values.use_max_ttl ||
|
||||
!allowAdvancedScheduling ||
|
||||
!["saturday", "sunday"].includes(
|
||||
form.values.autostop_requirement_days_of_week || "",
|
||||
)
|
||||
}
|
||||
fullWidth
|
||||
inputProps={{ min: 1, max: 16, step: 1 }}
|
||||
label="Weeks between required stops"
|
||||
type="number"
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Stack direction="column">
|
||||
<Stack direction="row" alignItems="center">
|
||||
<Checkbox
|
||||
id="use_max_ttl"
|
||||
size="small"
|
||||
disabled={isSubmitting || !allowAdvancedScheduling}
|
||||
onChange={handleToggleUseMaxTTL}
|
||||
name="use_max_ttl"
|
||||
checked={form.values.use_max_ttl}
|
||||
/>
|
||||
<Stack spacing={0.5}>
|
||||
<strong>
|
||||
Use a max lifetime instead of a required autostop schedule.
|
||||
</strong>
|
||||
<span css={styles.optionHelperText}>
|
||||
Use a maximum lifetime for workspaces created from this
|
||||
template instead of an autostop requirement as configured
|
||||
above.
|
||||
</span>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
{...getFieldHelpers(
|
||||
"max_ttl_hours",
|
||||
allowAdvancedScheduling ? (
|
||||
<MaxTTLHelperText ttl={form.values.max_ttl_hours} />
|
||||
) : (
|
||||
<>
|
||||
You need an enterprise license to use it.{" "}
|
||||
<Link href={docs("/enterprise")}>Learn more</Link>.
|
||||
</>
|
||||
),
|
||||
)}
|
||||
disabled={
|
||||
isSubmitting ||
|
||||
!form.values.use_max_ttl ||
|
||||
!allowAdvancedScheduling
|
||||
}
|
||||
fullWidth
|
||||
label="Max lifetime (hours)"
|
||||
type="number"
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Stack direction="column">
|
||||
<Stack direction="row" alignItems="center">
|
||||
@@ -482,8 +534,8 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
|
||||
</strong>
|
||||
<span css={styles.optionHelperText}>
|
||||
Workspaces will always use the default TTL if this is set.
|
||||
Regardless of this setting, workspaces can only stay on for
|
||||
the max TTL.
|
||||
Regardless of this setting, workspaces will still stop due to
|
||||
the autostop requirement policy.
|
||||
</span>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
@@ -12,30 +12,45 @@ const provisioner: ProvisionerType =
|
||||
typeof (window as any).playwright !== "undefined" ? "echo" : "terraform";
|
||||
|
||||
export const newTemplate = (formData: CreateTemplateData) => {
|
||||
const {
|
||||
default_ttl_hours,
|
||||
let {
|
||||
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
|
||||
} = formData;
|
||||
|
||||
const safeTemplateData = {
|
||||
name: formData.name,
|
||||
display_name: formData.display_name,
|
||||
description: formData.description,
|
||||
icon: formData.icon,
|
||||
use_max_ttl: formData.use_max_ttl,
|
||||
allow_user_autostart: formData.allow_user_autostart,
|
||||
allow_user_autostop: formData.allow_user_autostop,
|
||||
allow_user_cancel_workspace_jobs: formData.allow_user_cancel_workspace_jobs,
|
||||
user_variable_values: formData.user_variable_values,
|
||||
allow_everyone_group_access: formData.allow_everyone_group_access,
|
||||
};
|
||||
|
||||
if (formData.use_max_ttl) {
|
||||
autostop_requirement_days_of_week = "off";
|
||||
autostop_requirement_weeks = 1;
|
||||
} else {
|
||||
max_ttl_hours = 0;
|
||||
}
|
||||
|
||||
return {
|
||||
...safeTemplateData,
|
||||
disable_everyone_group_access: !formData.allow_everyone_group_access,
|
||||
default_ttl_ms: formData.default_ttl_hours * 60 * 60 * 1000, // Convert hours to ms
|
||||
max_ttl_ms: formData.max_ttl_hours * 60 * 60 * 1000, // Convert hours to ms
|
||||
max_ttl_ms: max_ttl_hours * 60 * 60 * 1000, // Convert hours to ms
|
||||
autostop_requirement: {
|
||||
days_of_week: calculateAutostopRequirementDaysValue(
|
||||
formData.autostop_requirement_days_of_week,
|
||||
autostop_requirement_days_of_week,
|
||||
),
|
||||
weeks: formData.autostop_requirement_weeks,
|
||||
weeks: autostop_requirement_weeks,
|
||||
},
|
||||
autostart_requirement: {
|
||||
days_of_week: autostart_requirement_days_of_week,
|
||||
days_of_week: formData.autostart_requirement_days_of_week,
|
||||
},
|
||||
require_active_version: false,
|
||||
};
|
||||
@@ -48,13 +63,10 @@ export const getFormPermissions = (entitlements: Entitlements) => {
|
||||
// means no one can access.
|
||||
const allowDisableEveryoneAccess =
|
||||
entitlements.features["template_rbac"].enabled;
|
||||
const allowAutostopRequirement =
|
||||
entitlements.features["template_autostop_requirement"].enabled;
|
||||
|
||||
return {
|
||||
allowAdvancedScheduling,
|
||||
allowDisableEveryoneAccess,
|
||||
allowAutostopRequirement,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
+152
-85
@@ -55,7 +55,6 @@ export interface TemplateScheduleForm {
|
||||
error?: unknown;
|
||||
allowAdvancedScheduling: boolean;
|
||||
allowWorkspaceActions: boolean;
|
||||
allowAutostopRequirement: boolean;
|
||||
// Helpful to show field errors on Storybook
|
||||
initialTouched?: FormikTouched<UpdateTemplateMeta>;
|
||||
}
|
||||
@@ -67,7 +66,6 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
|
||||
error,
|
||||
allowAdvancedScheduling,
|
||||
allowWorkspaceActions,
|
||||
allowAutostopRequirement,
|
||||
isSubmitting,
|
||||
initialTouched,
|
||||
}) => {
|
||||
@@ -78,6 +76,10 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
|
||||
default_ttl_ms: template.default_ttl_ms / MS_HOUR_CONVERSION,
|
||||
// the API ignores these values, but to avoid tripping up validation set
|
||||
// it to zero if the user can't set the field.
|
||||
use_max_ttl:
|
||||
template.use_max_ttl === undefined
|
||||
? template.max_ttl_ms > 0
|
||||
: template.use_max_ttl,
|
||||
max_ttl_ms: allowAdvancedScheduling
|
||||
? template.max_ttl_ms / MS_HOUR_CONVERSION
|
||||
: 0,
|
||||
@@ -91,12 +93,12 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
|
||||
? template.time_til_dormant_autodelete_ms / MS_DAY_CONVERSION
|
||||
: 0,
|
||||
|
||||
autostop_requirement_days_of_week: allowAutostopRequirement
|
||||
autostop_requirement_days_of_week: allowAdvancedScheduling
|
||||
? convertAutostopRequirementDaysValue(
|
||||
template.autostop_requirement.days_of_week,
|
||||
)
|
||||
: "off",
|
||||
autostop_requirement_weeks: allowAutostopRequirement
|
||||
autostop_requirement_weeks: allowAdvancedScheduling
|
||||
? template.autostop_requirement.weeks > 0
|
||||
? template.autostop_requirement.weeks
|
||||
: 1
|
||||
@@ -205,9 +207,10 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
|
||||
default_ttl_ms: form.values.default_ttl_ms
|
||||
? form.values.default_ttl_ms * MS_HOUR_CONVERSION
|
||||
: undefined,
|
||||
max_ttl_ms: form.values.max_ttl_ms
|
||||
? form.values.max_ttl_ms * MS_HOUR_CONVERSION
|
||||
: undefined,
|
||||
max_ttl_ms:
|
||||
form.values.max_ttl_ms && form.values.use_max_ttl
|
||||
? form.values.max_ttl_ms * MS_HOUR_CONVERSION
|
||||
: undefined,
|
||||
failure_ttl_ms: form.values.failure_ttl_ms
|
||||
? form.values.failure_ttl_ms * MS_DAY_CONVERSION
|
||||
: undefined,
|
||||
@@ -218,12 +221,14 @@ 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,
|
||||
},
|
||||
autostop_requirement: form.values.use_max_ttl
|
||||
? undefined
|
||||
: {
|
||||
days_of_week: calculateAutostopRequirementDaysValue(
|
||||
form.values.autostop_requirement_days_of_week,
|
||||
),
|
||||
weeks: autostop_requirement_weeks,
|
||||
},
|
||||
autostart_requirement: {
|
||||
days_of_week: form.values.autostart_requirement_days_of_week,
|
||||
},
|
||||
@@ -317,6 +322,27 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleUseMaxTTL = async () => {
|
||||
const val = !form.values.use_max_ttl;
|
||||
if (val) {
|
||||
// set max_ttl to 1, set autostop_requirement to empty
|
||||
await form.setValues({
|
||||
...form.values,
|
||||
use_max_ttl: val,
|
||||
max_ttl_ms: 1,
|
||||
autostop_requirement_days_of_week: "off",
|
||||
autostop_requirement_weeks: 1,
|
||||
});
|
||||
} else {
|
||||
// set max_ttl to 0
|
||||
await form.setValues({
|
||||
...form.values,
|
||||
use_max_ttl: val,
|
||||
max_ttl_ms: 0,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<HorizontalForm
|
||||
onSubmit={form.handleSubmit}
|
||||
@@ -338,85 +364,126 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
|
||||
label="Default autostop (hours)"
|
||||
type="number"
|
||||
/>
|
||||
|
||||
{!allowAutostopRequirement && (
|
||||
<TextField
|
||||
{...getFieldHelpers(
|
||||
"max_ttl_ms",
|
||||
allowAdvancedScheduling ? (
|
||||
<MaxTTLHelperText ttl={form.values.max_ttl_ms} />
|
||||
) : (
|
||||
<>
|
||||
You need an enterprise license to use it{" "}
|
||||
<Link href={docs("/enterprise")}>Learn more</Link>.
|
||||
</>
|
||||
),
|
||||
)}
|
||||
disabled={isSubmitting || !allowAdvancedScheduling}
|
||||
fullWidth
|
||||
inputProps={{ min: 0, step: 1 }}
|
||||
label="Max lifetime (hours)"
|
||||
type="number"
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</FormSection>
|
||||
|
||||
{allowAutostopRequirement && (
|
||||
<FormSection
|
||||
title="Autostop Requirement"
|
||||
description="Define when workspaces created from this template are stopped periodically to enforce template updates and ensure idle workspaces are stopped."
|
||||
>
|
||||
<Stack direction="row" css={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="Days with required stop"
|
||||
>
|
||||
<MenuItem key="off" value="off">
|
||||
Off
|
||||
</MenuItem>
|
||||
<MenuItem key="daily" value="daily">
|
||||
Daily
|
||||
</MenuItem>
|
||||
<MenuItem key="saturday" value="saturday">
|
||||
Saturday
|
||||
</MenuItem>
|
||||
<MenuItem key="sunday" value="sunday">
|
||||
Sunday
|
||||
</MenuItem>
|
||||
</TextField>
|
||||
<FormSection
|
||||
title="Autostop Requirement"
|
||||
description="Define when workspaces created from this template are stopped periodically to enforce template updates and ensure idle workspaces are stopped."
|
||||
>
|
||||
<Stack direction="row" css={styles.ttlFields}>
|
||||
<TextField
|
||||
{...getFieldHelpers(
|
||||
"autostop_requirement_days_of_week",
|
||||
<AutostopRequirementDaysHelperText
|
||||
days={form.values.autostop_requirement_days_of_week}
|
||||
/>,
|
||||
)}
|
||||
disabled={isSubmitting || form.values.use_max_ttl}
|
||||
fullWidth
|
||||
select
|
||||
value={form.values.autostop_requirement_days_of_week}
|
||||
label="Days with required stop"
|
||||
>
|
||||
<MenuItem key="off" value="off">
|
||||
Off
|
||||
</MenuItem>
|
||||
<MenuItem key="daily" value="daily">
|
||||
Daily
|
||||
</MenuItem>
|
||||
<MenuItem key="saturday" value="saturday">
|
||||
Saturday
|
||||
</MenuItem>
|
||||
<MenuItem key="sunday" value="sunday">
|
||||
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 || "",
|
||||
)
|
||||
<TextField
|
||||
{...getFieldHelpers(
|
||||
"autostop_requirement_weeks",
|
||||
<AutostopRequirementWeeksHelperText
|
||||
days={form.values.autostop_requirement_days_of_week}
|
||||
weeks={form.values.autostop_requirement_weeks}
|
||||
/>,
|
||||
)}
|
||||
disabled={
|
||||
isSubmitting ||
|
||||
form.values.use_max_ttl ||
|
||||
!["saturday", "sunday"].includes(
|
||||
form.values.autostop_requirement_days_of_week || "",
|
||||
)
|
||||
}
|
||||
fullWidth
|
||||
inputProps={{ min: 1, max: 16, step: 1 }}
|
||||
label="Weeks between required stops"
|
||||
type="number"
|
||||
/>
|
||||
</Stack>
|
||||
</FormSection>
|
||||
|
||||
<FormSection
|
||||
title="Max Lifetime"
|
||||
description="Define the maximum lifetime for workspaces created from this template."
|
||||
deprecated
|
||||
>
|
||||
<Stack direction="column">
|
||||
<Stack direction="row" alignItems="center">
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
id="use_max_ttl"
|
||||
size="small"
|
||||
disabled={isSubmitting || !allowAdvancedScheduling}
|
||||
onChange={handleToggleUseMaxTTL}
|
||||
name="use_max_ttl"
|
||||
checked={form.values.use_max_ttl}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Stack spacing={0.5}>
|
||||
<strong>
|
||||
Use a max lifetime instead of a required autostop schedule.
|
||||
</strong>
|
||||
<span
|
||||
css={{
|
||||
fontSize: 12,
|
||||
color: theme.palette.text.secondary,
|
||||
}}
|
||||
>
|
||||
Use a maximum lifetime for workspaces created from this
|
||||
template instead of an autostop requirement as configured
|
||||
above.
|
||||
</span>
|
||||
</Stack>
|
||||
}
|
||||
fullWidth
|
||||
inputProps={{ min: 1, max: 16, step: 1 }}
|
||||
label="Weeks between required stops"
|
||||
type="number"
|
||||
/>
|
||||
</Stack>
|
||||
</FormSection>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
{...getFieldHelpers(
|
||||
"max_ttl_ms",
|
||||
allowAdvancedScheduling ? (
|
||||
<MaxTTLHelperText ttl={form.values.max_ttl_ms} />
|
||||
) : (
|
||||
<>
|
||||
You need an enterprise license to use it{" "}
|
||||
<Link href={docs("/enterprise")}>Learn more</Link>.
|
||||
</>
|
||||
),
|
||||
)}
|
||||
disabled={
|
||||
isSubmitting ||
|
||||
!form.values.use_max_ttl ||
|
||||
!allowAdvancedScheduling
|
||||
}
|
||||
fullWidth
|
||||
inputProps={{ min: 0, step: 1 }}
|
||||
label="Max lifetime (hours)"
|
||||
type="number"
|
||||
/>
|
||||
</Stack>
|
||||
</FormSection>
|
||||
|
||||
<FormSection
|
||||
title="Allow users scheduling"
|
||||
|
||||
@@ -15,6 +15,7 @@ import TemplateSchedulePage from "./TemplateSchedulePage";
|
||||
|
||||
const validFormValues: TemplateScheduleFormValues = {
|
||||
default_ttl_ms: 1,
|
||||
use_max_ttl: true,
|
||||
max_ttl_ms: 2,
|
||||
failure_ttl_ms: 7,
|
||||
time_til_dormant_ms: 180,
|
||||
@@ -73,8 +74,12 @@ const fillAndSubmitForm = async ({
|
||||
}
|
||||
|
||||
if (max_ttl_ms) {
|
||||
const useMaxTtlCheckbox = screen.getByRole("checkbox", {
|
||||
name: /Use a max lifetime/i,
|
||||
});
|
||||
const maxTtlField = await screen.findByLabelText("Max lifetime (hours)");
|
||||
|
||||
await user.click(useMaxTtlCheckbox);
|
||||
await user.clear(maxTtlField);
|
||||
await user.type(maxTtlField, max_ttl_ms.toString());
|
||||
}
|
||||
|
||||
@@ -24,8 +24,6 @@ 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 =
|
||||
entitlements.features["template_autostop_requirement"].enabled;
|
||||
const { clearLocal } = useLocalStorage();
|
||||
|
||||
const {
|
||||
@@ -55,7 +53,6 @@ const TemplateSchedulePage: FC = () => {
|
||||
<TemplateSchedulePageView
|
||||
allowAdvancedScheduling={allowAdvancedScheduling}
|
||||
allowWorkspaceActions={allowWorkspaceActions}
|
||||
allowAutostopRequirement={allowAutostopRequirement}
|
||||
isSubmitting={isSubmitting}
|
||||
template={template}
|
||||
submitError={submitError}
|
||||
|
||||
@@ -14,7 +14,6 @@ export interface TemplateSchedulePageViewProps {
|
||||
>["initialTouched"];
|
||||
allowAdvancedScheduling: boolean;
|
||||
allowWorkspaceActions: boolean;
|
||||
allowAutostopRequirement: boolean;
|
||||
}
|
||||
|
||||
export const TemplateSchedulePageView: FC<TemplateSchedulePageViewProps> = ({
|
||||
@@ -24,7 +23,6 @@ export const TemplateSchedulePageView: FC<TemplateSchedulePageViewProps> = ({
|
||||
isSubmitting,
|
||||
allowAdvancedScheduling,
|
||||
allowWorkspaceActions,
|
||||
allowAutostopRequirement,
|
||||
submitError,
|
||||
initialTouched,
|
||||
}) => {
|
||||
@@ -37,7 +35,6 @@ export const TemplateSchedulePageView: FC<TemplateSchedulePageViewProps> = ({
|
||||
<TemplateScheduleForm
|
||||
allowAdvancedScheduling={allowAdvancedScheduling}
|
||||
allowWorkspaceActions={allowWorkspaceActions}
|
||||
allowAutostopRequirement={allowAutostopRequirement}
|
||||
initialTouched={initialTouched}
|
||||
isSubmitting={isSubmitting}
|
||||
template={template}
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface TemplateScheduleFormValues
|
||||
UpdateTemplateMeta,
|
||||
"autostop_requirement" | "autostart_requirement"
|
||||
> {
|
||||
use_max_ttl: boolean;
|
||||
autostart_requirement_days_of_week: TemplateAutostartRequirementDaysValue[];
|
||||
autostop_requirement_days_of_week: TemplateAutostopRequirementDaysValue;
|
||||
autostop_requirement_weeks: number;
|
||||
|
||||
@@ -7,13 +7,13 @@ import ScheduleIcon from "@mui/icons-material/EditCalendarOutlined";
|
||||
import SecurityIcon from "@mui/icons-material/LockOutlined";
|
||||
import type { User } from "api/typesGenerated";
|
||||
import { UserAvatar } from "components/UserAvatar/UserAvatar";
|
||||
import { useDashboard } from "components/Dashboard/DashboardProvider";
|
||||
import {
|
||||
Sidebar as BaseSidebar,
|
||||
SidebarHeader,
|
||||
SidebarNavItem,
|
||||
} from "components/Sidebar/Sidebar";
|
||||
import { GitIcon } from "components/Icons/GitIcon";
|
||||
import { useDashboard } from "components/Dashboard/DashboardProvider";
|
||||
|
||||
interface SidebarProps {
|
||||
user: User;
|
||||
@@ -21,8 +21,8 @@ interface SidebarProps {
|
||||
|
||||
export const Sidebar: FC<SidebarProps> = ({ user }) => {
|
||||
const { entitlements } = useDashboard();
|
||||
const allowAutostopRequirement =
|
||||
entitlements.features.template_autostop_requirement.enabled;
|
||||
const showSchedulePage =
|
||||
entitlements.features.advanced_template_scheduling.enabled;
|
||||
|
||||
return (
|
||||
<BaseSidebar>
|
||||
@@ -39,7 +39,7 @@ export const Sidebar: FC<SidebarProps> = ({ user }) => {
|
||||
<SidebarNavItem href="appearance" icon={AppearanceIcon}>
|
||||
Appearance
|
||||
</SidebarNavItem>
|
||||
{allowAutostopRequirement && (
|
||||
{showSchedulePage && (
|
||||
<SidebarNavItem href="schedule" icon={ScheduleIcon}>
|
||||
Schedule
|
||||
</SidebarNavItem>
|
||||
|
||||
@@ -445,9 +445,10 @@ export const MockTemplate: TypesGen.Template = {
|
||||
},
|
||||
description: "This is a test description.",
|
||||
default_ttl_ms: 24 * 60 * 60 * 1000,
|
||||
max_ttl_ms: 2 * 24 * 60 * 60 * 1000,
|
||||
use_max_ttl: false,
|
||||
max_ttl_ms: 0,
|
||||
autostop_requirement: {
|
||||
days_of_week: [],
|
||||
days_of_week: ["sunday"],
|
||||
weeks: 1,
|
||||
},
|
||||
autostart_requirement: {
|
||||
|
||||
Reference in New Issue
Block a user