import TextField from "@material-ui/core/TextField"
import { Template, UpdateTemplateMeta } from "api/typesGenerated"
import { FormikTouched, useFormik } from "formik"
import { FC, ChangeEvent } from "react"
import { getFormHelpers } from "utils/formUtils"
import * as Yup from "yup"
import i18next from "i18next"
import { useTranslation } from "react-i18next"
import { Maybe } from "components/Conditionals/Maybe"
import {
FormSection,
HorizontalForm,
FormFooter,
FormFields,
} from "components/Form/Form"
import { Stack } from "components/Stack/Stack"
import { makeStyles } from "@material-ui/core/styles"
import Link from "@material-ui/core/Link"
import Checkbox from "@material-ui/core/Checkbox"
import FormControlLabel from "@material-ui/core/FormControlLabel"
import Switch from "@material-ui/core/Switch"
const TTLHelperText = ({
ttl,
translationName,
}: {
ttl?: number
translationName: string
}) => {
const { t } = useTranslation("templateSettingsPage")
const count = typeof ttl !== "number" ? 0 : ttl
return (
// no helper text if ttl is negative - error will show once field is considered touched
= 0}>
{t(translationName, { count })}
)
}
const MAX_TTL_DAYS = 7
const MS_HOUR_CONVERSION = 3600000
const MS_DAY_CONVERSION = 86400000
const FAILURE_CLEANUP_DEFAULT = 7
const INACTIVITY_CLEANUP_DEFAULT = 180
export interface TemplateScheduleFormValues extends UpdateTemplateMeta {
failure_cleanup_enabled: boolean
inactivity_cleanup_enabled: boolean
}
export const getValidationSchema = (): Yup.AnyObjectSchema =>
Yup.object({
default_ttl_ms: Yup.number()
.integer()
.min(0, i18next.t("defaultTTLMinError", { ns: "templateSettingsPage" }))
.max(
24 * MAX_TTL_DAYS /* 7 days in hours */,
i18next.t("defaultTTLMaxError", { ns: "templateSettingsPage" }),
),
max_ttl_ms: Yup.number()
.integer()
.min(0, i18next.t("maxTTLMinError", { ns: "templateSettingsPage" }))
.max(
24 * MAX_TTL_DAYS /* 7 days in hours */,
i18next.t("maxTTLMaxError", { ns: "templateSettingsPage" }),
),
failure_ttl_ms: Yup.number()
.integer()
.min(0, "Failure cleanup days must not be less than 0.")
.test(
"positive-if-enabled",
"Failure cleanup days must be greater than zero when enabled.",
function (value) {
const parent = this.parent as TemplateScheduleFormValues
if (parent.failure_cleanup_enabled) {
return Boolean(value)
} else {
return true
}
},
),
inactivity_ttl_ms: Yup.number()
.integer()
.min(0, "Inactivity cleanup days must not be less than 0.")
.test(
"positive-if-enabled",
"Inactivity cleanup days must be greater than zero when enabled.",
function (value) {
const parent = this.parent as TemplateScheduleFormValues
if (parent.inactivity_cleanup_enabled) {
return Boolean(value)
} else {
return true
}
},
),
allow_user_autostart: Yup.boolean(),
allow_user_autostop: Yup.boolean(),
})
export interface TemplateScheduleForm {
template: Template
onSubmit: (data: UpdateTemplateMeta) => void
onCancel: () => void
isSubmitting: boolean
error?: unknown
allowAdvancedScheduling: boolean
allowWorkspaceActions: boolean
// Helpful to show field errors on Storybook
initialTouched?: FormikTouched
}
export const TemplateScheduleForm: FC = ({
template,
onSubmit,
onCancel,
error,
allowAdvancedScheduling,
allowWorkspaceActions,
isSubmitting,
initialTouched,
}) => {
const { t: commonT } = useTranslation("common")
const validationSchema = getValidationSchema()
const form = useFormik({
initialValues: {
// on display, convert from ms => hours
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.
max_ttl_ms: allowAdvancedScheduling
? template.max_ttl_ms / MS_HOUR_CONVERSION
: 0,
failure_ttl_ms: allowAdvancedScheduling
? template.failure_ttl_ms / MS_DAY_CONVERSION
: 0,
inactivity_ttl_ms: allowAdvancedScheduling
? template.inactivity_ttl_ms / MS_DAY_CONVERSION
: 0,
allow_user_autostart: template.allow_user_autostart,
allow_user_autostop: template.allow_user_autostop,
failure_cleanup_enabled:
allowAdvancedScheduling && Boolean(template.failure_ttl_ms),
inactivity_cleanup_enabled:
allowAdvancedScheduling && Boolean(template.inactivity_ttl_ms),
},
validationSchema,
onSubmit: (formData) => {
// on submit, convert from hours => ms
onSubmit({
default_ttl_ms: formData.default_ttl_ms
? formData.default_ttl_ms * MS_HOUR_CONVERSION
: undefined,
max_ttl_ms: formData.max_ttl_ms
? formData.max_ttl_ms * MS_HOUR_CONVERSION
: undefined,
failure_ttl_ms: formData.failure_ttl_ms
? formData.failure_ttl_ms * MS_DAY_CONVERSION
: undefined,
inactivity_ttl_ms: formData.inactivity_ttl_ms
? formData.inactivity_ttl_ms * MS_DAY_CONVERSION
: undefined,
allow_user_autostart: formData.allow_user_autostart,
allow_user_autostop: formData.allow_user_autostop,
})
},
initialTouched,
})
const getFieldHelpers = getFormHelpers(
form,
error,
)
const { t } = useTranslation("templateSettingsPage")
const styles = useStyles()
const handleToggleFailureCleanup = async (e: ChangeEvent) => {
form.handleChange(e)
if (!form.values.failure_cleanup_enabled) {
// fill failure_ttl_ms with defaults
await form.setValues({
...form.values,
failure_cleanup_enabled: true,
failure_ttl_ms: FAILURE_CLEANUP_DEFAULT,
})
} else {
// clear failure_ttl_ms
await form.setValues({
...form.values,
failure_cleanup_enabled: false,
failure_ttl_ms: 0,
})
}
}
const handleToggleInactivityCleanup = async (e: ChangeEvent) => {
form.handleChange(e)
if (!form.values.inactivity_cleanup_enabled) {
// fill inactivity_ttl_ms with defaults
await form.setValues({
...form.values,
inactivity_cleanup_enabled: true,
inactivity_ttl_ms: INACTIVITY_CLEANUP_DEFAULT,
})
} else {
// clear inactivity_ttl_ms
await form.setValues({
...form.values,
inactivity_cleanup_enabled: false,
inactivity_ttl_ms: 0,
})
}
}
return (
,
)}
disabled={isSubmitting}
fullWidth
inputProps={{ min: 0, step: 1 }}
label={t("defaultTtlLabel")}
variant="outlined"
type="number"
/>
) : (
<>
{commonT("licenseFieldTextHelper")}{" "}
{commonT("learnMore")}
.
>
),
)}
disabled={isSubmitting || !allowAdvancedScheduling}
fullWidth
inputProps={{ min: 0, step: 1 }}
label={t("maxTtlLabel")}
variant="outlined"
type="number"
/>
{
await form.setFieldValue(
"allow_user_autostart",
!form.values.allow_user_autostart,
)
}}
name="allow_user_autostart"
checked={form.values.allow_user_autostart}
/>
Allow users to autostart workspaces on a schedule.
{
await form.setFieldValue(
"allow_user_autostop",
!form.values.allow_user_autostop,
)
}}
name="allow_user_autostop"
checked={form.values.allow_user_autostop}
/>
Allow users to customize autostop duration for workspaces.
Workspaces will always use the default TTL if this is set.
Regardless of this setting, workspaces can only stay on for the
max lifetime.
{allowAdvancedScheduling && allowWorkspaceActions && (
<>
}
label="Enable Failure Cleanup"
/>
,
)}
disabled={isSubmitting || !form.values.failure_cleanup_enabled}
fullWidth
inputProps={{ min: 0, step: 1 }}
label="Time until cleanup (days)"
variant="outlined"
type="number"
aria-label="Failure Cleanup"
/>
}
label="Enable Inactivity Cleanup"
/>
,
)}
disabled={
isSubmitting || !form.values.inactivity_cleanup_enabled
}
fullWidth
inputProps={{ min: 0, step: 1 }}
label="Time until cleanup (days)"
variant="outlined"
type="number"
aria-label="Inactivity Cleanup"
/>
>
)}
)
}
const useStyles = makeStyles((theme) => ({
ttlFields: {
width: "100%",
},
optionDescription: {
fontSize: 12,
color: theme.palette.text.secondary,
},
}))