mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(site): add advanced schedule settings to UI (#7061)
* feat: add advanced schedule settings * Update site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx Co-authored-by: Dean Sheather <dean@deansheather.com> * rename variable * fix optionText css --------- Co-authored-by: Dean Sheather <dean@deansheather.com>
This commit is contained in:
co-authored by
Dean Sheather
parent
264093a874
commit
d1d459cb79
@@ -105,6 +105,8 @@ const defaultInitialValues: CreateTemplateData = {
|
||||
// you are not licensed. We hide the form value based on entitlements.
|
||||
max_ttl_hours: 24 * 7,
|
||||
allow_user_cancel_workspace_jobs: false,
|
||||
allow_user_autostart: false,
|
||||
allow_user_autostop: false,
|
||||
}
|
||||
|
||||
type GetInitialValuesParams = {
|
||||
@@ -112,19 +114,19 @@ type GetInitialValuesParams = {
|
||||
fromCopy?: Template
|
||||
parameters?: ParameterSchema[]
|
||||
variables?: TemplateVersionVariable[]
|
||||
canSetMaxTTL: boolean
|
||||
allowAdvancedScheduling: boolean
|
||||
}
|
||||
|
||||
const getInitialValues = ({
|
||||
fromExample,
|
||||
fromCopy,
|
||||
canSetMaxTTL,
|
||||
allowAdvancedScheduling,
|
||||
variables,
|
||||
parameters,
|
||||
}: GetInitialValuesParams) => {
|
||||
let initialValues = defaultInitialValues
|
||||
|
||||
if (!canSetMaxTTL) {
|
||||
if (!allowAdvancedScheduling) {
|
||||
initialValues = {
|
||||
...initialValues,
|
||||
max_ttl_hours: 0,
|
||||
@@ -188,7 +190,7 @@ export interface CreateTemplateFormProps {
|
||||
error?: unknown
|
||||
jobError?: string
|
||||
logs?: ProvisionerJobLog[]
|
||||
canSetMaxTTL: boolean
|
||||
allowAdvancedScheduling: boolean
|
||||
copiedTemplate?: Template
|
||||
}
|
||||
|
||||
@@ -204,12 +206,12 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = ({
|
||||
error,
|
||||
jobError,
|
||||
logs,
|
||||
canSetMaxTTL,
|
||||
allowAdvancedScheduling,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
const form = useFormik<CreateTemplateData>({
|
||||
initialValues: getInitialValues({
|
||||
canSetMaxTTL,
|
||||
allowAdvancedScheduling,
|
||||
fromExample: starterTemplate,
|
||||
fromCopy: copiedTemplate,
|
||||
variables,
|
||||
@@ -319,7 +321,7 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = ({
|
||||
<TextField
|
||||
{...getFieldHelpers(
|
||||
"max_ttl_hours",
|
||||
canSetMaxTTL ? (
|
||||
allowAdvancedScheduling ? (
|
||||
<TTLHelperText
|
||||
translationName="form.helperText.maxTTLHelperText"
|
||||
ttl={form.values.max_ttl_hours}
|
||||
@@ -334,13 +336,62 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = ({
|
||||
</>
|
||||
),
|
||||
)}
|
||||
disabled={isSubmitting || !canSetMaxTTL}
|
||||
disabled={isSubmitting || !allowAdvancedScheduling}
|
||||
fullWidth
|
||||
label={t("form.fields.maxTTL")}
|
||||
variant="outlined"
|
||||
type="number"
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="column">
|
||||
<Stack direction="row" alignItems="center">
|
||||
<Checkbox
|
||||
id="allow_user_autostart"
|
||||
size="small"
|
||||
color="primary"
|
||||
disabled={isSubmitting || !allowAdvancedScheduling}
|
||||
onChange={async () => {
|
||||
await form.setFieldValue(
|
||||
"allow_user_autostart",
|
||||
!form.values.allow_user_autostart,
|
||||
)
|
||||
}}
|
||||
name="allow_user_autostart"
|
||||
checked={form.values.allow_user_autostart}
|
||||
/>
|
||||
<Stack spacing={0.5}>
|
||||
<strong>
|
||||
Allow users to autostart workspaces on a schedule.
|
||||
</strong>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack direction="row" alignItems="center">
|
||||
<Checkbox
|
||||
id="allow-user-autostop"
|
||||
size="small"
|
||||
color="primary"
|
||||
disabled={isSubmitting || !allowAdvancedScheduling}
|
||||
onChange={async () => {
|
||||
await form.setFieldValue(
|
||||
"allow_user_autostop",
|
||||
!form.values.allow_user_autostop,
|
||||
)
|
||||
}}
|
||||
name="allow-user-autostop"
|
||||
checked={form.values.allow_user_autostop}
|
||||
/>
|
||||
<Stack spacing={0.5}>
|
||||
<strong>
|
||||
Allow users to customize autostop duration for workspaces.
|
||||
</strong>
|
||||
<span className={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.
|
||||
</span>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</FormFields>
|
||||
</FormSection>
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ const CreateTemplatePage: FC = () => {
|
||||
} = state.context
|
||||
const shouldDisplayForm = !state.hasTag("loading")
|
||||
const { entitlements } = useDashboard()
|
||||
const canSetMaxTTL =
|
||||
const allowAdvancedScheduling =
|
||||
entitlements.features["advanced_template_scheduling"].enabled
|
||||
|
||||
const onCancel = () => {
|
||||
@@ -70,7 +70,7 @@ const CreateTemplatePage: FC = () => {
|
||||
{shouldDisplayForm && (
|
||||
<CreateTemplateForm
|
||||
copiedTemplate={state.context.copiedTemplate}
|
||||
canSetMaxTTL={canSetMaxTTL}
|
||||
allowAdvancedScheduling={allowAdvancedScheduling}
|
||||
error={error}
|
||||
starterTemplate={starterTemplate}
|
||||
isSubmitting={state.hasTag("submitting")}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import TextField from "@material-ui/core/TextField"
|
||||
import { Template, UpdateTemplateMeta } from "api/typesGenerated"
|
||||
import { FormikContextType, FormikTouched, useFormik } from "formik"
|
||||
import { FormikTouched, useFormik } from "formik"
|
||||
import { FC } from "react"
|
||||
import { getFormHelpers } from "util/formUtils"
|
||||
import * as Yup from "yup"
|
||||
@@ -11,6 +11,7 @@ import { FormSection, HorizontalForm, FormFooter } 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"
|
||||
|
||||
const TTLHelperText = ({
|
||||
ttl,
|
||||
@@ -48,6 +49,8 @@ export const getValidationSchema = (): Yup.AnyObjectSchema =>
|
||||
24 * MAX_TTL_DAYS /* 7 days in hours */,
|
||||
i18next.t("maxTTLMaxError", { ns: "templateSettingsPage" }),
|
||||
),
|
||||
allow_user_autostart: Yup.boolean(),
|
||||
allow_user_autostop: Yup.boolean(),
|
||||
})
|
||||
|
||||
export interface TemplateScheduleForm {
|
||||
@@ -56,7 +59,7 @@ export interface TemplateScheduleForm {
|
||||
onCancel: () => void
|
||||
isSubmitting: boolean
|
||||
error?: unknown
|
||||
canSetMaxTTL: boolean
|
||||
allowAdvancedScheduling: boolean
|
||||
// Helpful to show field errors on Storybook
|
||||
initialTouched?: FormikTouched<UpdateTemplateMeta>
|
||||
}
|
||||
@@ -66,35 +69,40 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
|
||||
onSubmit,
|
||||
onCancel,
|
||||
error,
|
||||
canSetMaxTTL,
|
||||
allowAdvancedScheduling,
|
||||
isSubmitting,
|
||||
initialTouched,
|
||||
}) => {
|
||||
const { t: commonT } = useTranslation("common")
|
||||
const validationSchema = getValidationSchema()
|
||||
const form: FormikContextType<UpdateTemplateMeta> =
|
||||
useFormik<UpdateTemplateMeta>({
|
||||
initialValues: {
|
||||
// on display, convert from ms => hours
|
||||
default_ttl_ms: template.default_ttl_ms / MS_HOUR_CONVERSION,
|
||||
// the API ignores this value, but to avoid tripping up validation set
|
||||
// it to zero if the user can't set the field.
|
||||
max_ttl_ms: canSetMaxTTL ? template.max_ttl_ms / MS_HOUR_CONVERSION : 0,
|
||||
},
|
||||
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,
|
||||
})
|
||||
},
|
||||
initialTouched,
|
||||
})
|
||||
const form = useFormik<UpdateTemplateMeta>({
|
||||
initialValues: {
|
||||
// on display, convert from ms => hours
|
||||
default_ttl_ms: template.default_ttl_ms / MS_HOUR_CONVERSION,
|
||||
// the API ignores this value, 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,
|
||||
allow_user_autostart: template.allow_user_autostart,
|
||||
allow_user_autostop: template.allow_user_autostop,
|
||||
},
|
||||
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,
|
||||
allow_user_autostart: formData.allow_user_autostart,
|
||||
allow_user_autostop: formData.allow_user_autostop,
|
||||
})
|
||||
},
|
||||
initialTouched,
|
||||
})
|
||||
const getFieldHelpers = getFormHelpers<UpdateTemplateMeta>(form, error)
|
||||
const { t } = useTranslation("templateSettingsPage")
|
||||
const styles = useStyles()
|
||||
@@ -128,7 +136,7 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
|
||||
<TextField
|
||||
{...getFieldHelpers(
|
||||
"max_ttl_ms",
|
||||
canSetMaxTTL ? (
|
||||
allowAdvancedScheduling ? (
|
||||
<TTLHelperText
|
||||
translationName="maxTTLHelperText"
|
||||
ttl={form.values.max_ttl_ms}
|
||||
@@ -143,7 +151,7 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
|
||||
</>
|
||||
),
|
||||
)}
|
||||
disabled={isSubmitting || !canSetMaxTTL}
|
||||
disabled={isSubmitting || !allowAdvancedScheduling}
|
||||
fullWidth
|
||||
inputProps={{ min: 0, step: 1 }}
|
||||
label={t("maxTtlLabel")}
|
||||
@@ -153,13 +161,72 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
|
||||
</Stack>
|
||||
</FormSection>
|
||||
|
||||
<FormSection
|
||||
title="Allow users scheduling"
|
||||
description="Allow users to set custom autostart and autostop scheduling options for workspaces created from this template."
|
||||
>
|
||||
<Stack direction="column">
|
||||
<Stack direction="row" alignItems="center">
|
||||
<Checkbox
|
||||
id="allow_user_autostart"
|
||||
size="small"
|
||||
color="primary"
|
||||
disabled={isSubmitting || !allowAdvancedScheduling}
|
||||
onChange={async () => {
|
||||
await form.setFieldValue(
|
||||
"allow_user_autostart",
|
||||
!form.values.allow_user_autostart,
|
||||
)
|
||||
}}
|
||||
name="allow_user_autostart"
|
||||
checked={form.values.allow_user_autostart}
|
||||
/>
|
||||
<Stack spacing={0.5}>
|
||||
<strong>
|
||||
Allow users to autostart workspaces on a schedule.
|
||||
</strong>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack direction="row" alignItems="center">
|
||||
<Checkbox
|
||||
id="allow-user-autostop"
|
||||
size="small"
|
||||
color="primary"
|
||||
disabled={isSubmitting || !allowAdvancedScheduling}
|
||||
onChange={async () => {
|
||||
await form.setFieldValue(
|
||||
"allow_user_autostop",
|
||||
!form.values.allow_user_autostop,
|
||||
)
|
||||
}}
|
||||
name="allow_user_autostop"
|
||||
checked={form.values.allow_user_autostop}
|
||||
/>
|
||||
<Stack spacing={0.5}>
|
||||
<strong>
|
||||
Allow users to customize autostop duration for workspaces.
|
||||
</strong>
|
||||
<span className={styles.optionDescription}>
|
||||
Workspaces will always use the default TTL if this is set.
|
||||
Regardless of this setting, workspaces can only stay on for the
|
||||
max lifetime.
|
||||
</span>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</FormSection>
|
||||
|
||||
<FormFooter onCancel={onCancel} isLoading={isSubmitting} />
|
||||
</HorizontalForm>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
ttlFields: {
|
||||
width: "100%",
|
||||
},
|
||||
optionDescription: {
|
||||
fontSize: 12,
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -15,7 +15,7 @@ const TemplateSchedulePage: FC = () => {
|
||||
const navigate = useNavigate()
|
||||
const { template } = useTemplateSettingsContext()
|
||||
const { entitlements } = useDashboard()
|
||||
const canSetMaxTTL =
|
||||
const allowAdvancedScheduling =
|
||||
entitlements.features["advanced_template_scheduling"].enabled
|
||||
const {
|
||||
mutate: updateTemplate,
|
||||
@@ -36,7 +36,7 @@ const TemplateSchedulePage: FC = () => {
|
||||
<title>{pageTitle([template.name, "Schedule"])}</title>
|
||||
</Helmet>
|
||||
<TemplateSchedulePageView
|
||||
canSetMaxTTL={canSetMaxTTL}
|
||||
allowAdvancedScheduling={allowAdvancedScheduling}
|
||||
isSubmitting={isSubmitting}
|
||||
template={template}
|
||||
submitError={submitError}
|
||||
|
||||
+2
-2
@@ -10,7 +10,7 @@ export default {
|
||||
title: "pages/TemplateSchedulePageView",
|
||||
component: TemplateSchedulePageView,
|
||||
args: {
|
||||
canSetMaxTTL: true,
|
||||
allowAdvancedScheduling: true,
|
||||
template: MockTemplate,
|
||||
onSubmit: action("onSubmit"),
|
||||
onCancel: action("cancel"),
|
||||
@@ -26,5 +26,5 @@ Example.args = {}
|
||||
|
||||
export const CantSetMaxTTL = Template.bind({})
|
||||
CantSetMaxTTL.args = {
|
||||
canSetMaxTTL: false,
|
||||
allowAdvancedScheduling: false,
|
||||
}
|
||||
|
||||
+3
-3
@@ -11,7 +11,7 @@ export interface TemplateSchedulePageViewProps {
|
||||
isSubmitting: boolean
|
||||
submitError?: unknown
|
||||
initialTouched?: ComponentProps<typeof TemplateScheduleForm>["initialTouched"]
|
||||
canSetMaxTTL: boolean
|
||||
allowAdvancedScheduling: boolean
|
||||
}
|
||||
|
||||
export const TemplateSchedulePageView: FC<TemplateSchedulePageViewProps> = ({
|
||||
@@ -19,7 +19,7 @@ export const TemplateSchedulePageView: FC<TemplateSchedulePageViewProps> = ({
|
||||
onCancel,
|
||||
onSubmit,
|
||||
isSubmitting,
|
||||
canSetMaxTTL,
|
||||
allowAdvancedScheduling,
|
||||
submitError,
|
||||
initialTouched,
|
||||
}) => {
|
||||
@@ -32,7 +32,7 @@ export const TemplateSchedulePageView: FC<TemplateSchedulePageViewProps> = ({
|
||||
</PageHeader>
|
||||
|
||||
<TemplateScheduleForm
|
||||
canSetMaxTTL={canSetMaxTTL}
|
||||
allowAdvancedScheduling={allowAdvancedScheduling}
|
||||
initialTouched={initialTouched}
|
||||
isSubmitting={isSubmitting}
|
||||
template={template}
|
||||
|
||||
@@ -44,6 +44,8 @@ export interface CreateTemplateData {
|
||||
icon: string
|
||||
default_ttl_hours: number
|
||||
max_ttl_hours: number
|
||||
allow_user_autostart: boolean
|
||||
allow_user_autostop: boolean
|
||||
allow_user_cancel_workspace_jobs: boolean
|
||||
parameter_values_by_name?: Record<string, string>
|
||||
user_variable_values?: VariableValue[]
|
||||
|
||||
Reference in New Issue
Block a user