Compare commits

...

6 Commits

Author SHA1 Message Date
Jake Howell 47020ef1bc fix: update <DurationField /> 2026-02-10 11:00:19 +00:00
Jake Howell a67e4d5cb7 fix: remove helperText redefinition 2026-02-10 10:34:22 +00:00
Jake Howell 364c407826 feat: migrate last of the components 2026-02-10 07:40:11 +00:00
Jake Howell 5028362f34 fix: <Dialog /> 2026-02-10 07:34:32 +00:00
Jake Howell 4cf9216934 fix: remove <ScheduleDialog /> MUI components 2026-02-10 07:30:07 +00:00
Jake Howell 4270b9c04d feat: refactor forms 2026-02-10 07:27:17 +00:00
7 changed files with 748 additions and 710 deletions
@@ -1,9 +1,13 @@
import FormHelperText from "@mui/material/FormHelperText";
import MenuItem from "@mui/material/MenuItem";
import Select from "@mui/material/Select";
import TextField, { type TextFieldProps } from "@mui/material/TextField";
import { ChevronDownIcon } from "lucide-react";
import { type FC, useEffect, useReducer } from "react";
import { Input } from "components/Input/Input";
import { Label } from "components/Label/Label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "components/Select/Select";
import { type FC, type ReactNode, useEffect, useReducer } from "react";
import {
durationInDays,
durationInHours,
@@ -11,10 +15,16 @@ import {
type TimeUnit,
} from "utils/time";
type DurationFieldProps = Omit<TextFieldProps, "value" | "onChange"> & {
interface DurationFieldProps {
valueMs: number;
onChange: (value: number) => void;
};
label?: string;
disabled?: boolean;
helperText?: ReactNode;
error?: boolean;
name?: string;
id?: string;
}
type State = {
unit: TimeUnit;
@@ -59,13 +69,16 @@ const reducer = (state: State, action: Action): State => {
}
};
export const DurationField: FC<DurationFieldProps> = (props) => {
const {
valueMs: parentValueMs,
onChange,
helperText,
...textFieldProps
} = props;
export const DurationField: FC<DurationFieldProps> = ({
valueMs: parentValueMs,
onChange,
helperText,
error,
label,
disabled,
name,
id,
}) => {
const [state, dispatch] = useReducer(reducer, initState(parentValueMs));
const currentDurationMs = durationInMs(state.durationFieldValue, state.unit);
@@ -75,18 +88,19 @@ export const DurationField: FC<DurationFieldProps> = (props) => {
}
}, [currentDurationMs, parentValueMs]);
const inputId = id ?? name;
return (
<div>
<div
css={{
display: "flex",
gap: 8,
}}
>
<TextField
{...textFieldProps}
fullWidth
<div className="flex flex-col gap-2">
{label && <Label htmlFor={inputId}>{label}</Label>}
<div className="flex gap-2">
<Input
id={inputId}
name={name}
disabled={disabled}
className="flex-1"
value={state.durationFieldValue}
aria-invalid={error}
onChange={(e) => {
const durationFieldValue = intMask(e.currentTarget.value);
@@ -103,49 +117,50 @@ export const DurationField: FC<DurationFieldProps> = (props) => {
onChange(newDurationInMs);
}
}}
inputProps={{
step: 1,
}}
/>
<Select
disabled={props.disabled}
css={{ width: 120, "& .MuiSelect-icon": { padding: 2 } }}
disabled={disabled}
value={state.unit}
onChange={(e) => {
const unit = e.target.value as TimeUnit;
onValueChange={(value) => {
const unit = value as TimeUnit;
dispatch({
type: "CHANGE_TIME_UNIT",
unit,
});
// Calculate the new duration in ms after changing the unit
// Important: When changing from hours to days, we need to round up to nearest day
// but keep the millisecond value consistent for the parent component
// Calculate the new duration in ms after changing the unit.
// When changing from hours to days, we round up to the
// nearest day but keep the ms value consistent for the
// parent component.
let newDurationMs: number;
if (unit === "hours") {
// When switching to hours, use the current milliseconds to get exact hours
newDurationMs = currentDurationMs;
} else {
// When switching to days, round up to the nearest day
const daysValue = Math.ceil(durationInDays(currentDurationMs));
newDurationMs = daysToDuration(daysValue);
}
// Notify parent component if the value has changed
if (newDurationMs !== parentValueMs) {
onChange(newDurationMs);
}
}}
inputProps={{ "aria-label": "Time unit" }}
IconComponent={ChevronDownIcon}
>
<MenuItem value="hours">Hours</MenuItem>
<MenuItem value="days">Days</MenuItem>
<SelectTrigger className="w-[120px]" aria-label="Time unit">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="hours">Hours</SelectItem>
<SelectItem value="days">Days</SelectItem>
</SelectContent>
</Select>
</div>
{helperText && (
<FormHelperText error={props.error}>{helperText}</FormHelperText>
<span
className={`text-xs ${error ? "text-content-destructive" : "text-content-secondary"}`}
>
{helperText}
</span>
)}
</div>
);
@@ -1,8 +1,3 @@
import Checkbox from "@mui/material/Checkbox";
import FormControlLabel from "@mui/material/FormControlLabel";
import FormHelperText from "@mui/material/FormHelperText";
import MenuItem from "@mui/material/MenuItem";
import TextField from "@mui/material/TextField";
import {
CORSBehaviors,
type Template,
@@ -11,6 +6,7 @@ import {
} from "api/typesGenerated";
import { PremiumBadge } from "components/Badges/Badges";
import { Button } from "components/Button/Button";
import { Checkbox } from "components/Checkbox/Checkbox";
import {
FormFields,
FormFooter,
@@ -18,13 +14,18 @@ import {
HorizontalForm,
} from "components/Form/Form";
import { IconField } from "components/IconField/IconField";
import { Input } from "components/Input/Input";
import { Label } from "components/Label/Label";
import { Link } from "components/Link/Link";
import { Spinner } from "components/Spinner/Spinner";
import { Stack } from "components/Stack/Stack";
import {
StackLabel,
StackLabelHelperText,
} from "components/StackLabel/StackLabel";
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "components/Select/Select";
import { Spinner } from "components/Spinner/Spinner";
import { Textarea } from "components/Textarea/Textarea";
import { type FormikTouched, useFormik } from "formik";
import type { FC } from "react";
import { docs } from "utils/docs";
@@ -105,6 +106,15 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
});
const getFieldHelpers = getFormHelpers(form, error);
const nameField = getFieldHelpers("name");
const displayNameField = getFieldHelpers("display_name");
const descriptionField = getFieldHelpers("description", {
maxLength: MAX_DESCRIPTION_CHAR_LIMIT,
});
const deprecationField = getFieldHelpers("deprecation_message");
const portShareField = getFieldHelpers("max_port_share_level");
const corsField = getFieldHelpers("cors_behavior");
return (
<HorizontalForm
onSubmit={form.handleSubmit}
@@ -115,14 +125,24 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
description="The name is used to identify the template in URLs and the API."
>
<FormFields>
<TextField
{...getFieldHelpers("name")}
disabled={isSubmitting}
onChange={onChangeTrimmed(form)}
autoFocus
fullWidth
label="Name"
/>
<div className="flex flex-col items-start gap-2">
<Label htmlFor={nameField.id}>Name</Label>
<Input
id={nameField.id}
name={nameField.name}
value={nameField.value}
onChange={onChangeTrimmed(form)}
onBlur={nameField.onBlur}
disabled={isSubmitting}
autoFocus
aria-invalid={nameField.error}
/>
{nameField.error && (
<span className="text-xs text-content-destructive">
{nameField.helperText}
</span>
)}
</div>
</FormFields>
</FormSection>
@@ -131,23 +151,44 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
description="A friendly name, description, and icon to help developers identify your template."
>
<FormFields>
<TextField
{...getFieldHelpers("display_name")}
disabled={isSubmitting}
fullWidth
label="Display name"
/>
<div className="flex flex-col items-start gap-2">
<Label htmlFor={displayNameField.id}>Display name</Label>
<Input
id={displayNameField.id}
name={displayNameField.name}
value={displayNameField.value}
onChange={displayNameField.onChange}
onBlur={displayNameField.onBlur}
disabled={isSubmitting}
aria-invalid={displayNameField.error}
/>
{displayNameField.error && (
<span className="text-xs text-content-destructive">
{displayNameField.helperText}
</span>
)}
</div>
<TextField
{...getFieldHelpers("description", {
maxLength: MAX_DESCRIPTION_CHAR_LIMIT,
})}
multiline
disabled={isSubmitting}
fullWidth
label="Description"
rows={2}
/>
<div className="flex flex-col items-start gap-2">
<Label htmlFor={descriptionField.id}>Description</Label>
<Textarea
id={descriptionField.id}
name={descriptionField.name}
value={descriptionField.value}
onChange={descriptionField.onChange}
onBlur={descriptionField.onBlur}
disabled={isSubmitting}
rows={2}
aria-invalid={descriptionField.error}
/>
{descriptionField.helperText && (
<span
className={`text-xs ${descriptionField.error ? "text-content-destructive" : "text-content-secondary"}`}
>
{descriptionField.helperText}
</span>
)}
</div>
<IconField
{...getFieldHelpers("icon")}
@@ -165,138 +206,139 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
description="Regulate actions allowed on workspaces created from this template."
>
<FormFields spacing={6}>
<FormControlLabel
control={
<Checkbox
size="small"
id="allow_user_cancel_workspace_jobs"
name="allow_user_cancel_workspace_jobs"
disabled={isSubmitting}
checked={form.values.allow_user_cancel_workspace_jobs}
onChange={form.handleChange}
/>
}
label={
<StackLabel>
<div className="flex items-start gap-3">
<Checkbox
id="allow_user_cancel_workspace_jobs"
checked={form.values.allow_user_cancel_workspace_jobs}
onCheckedChange={(checked) =>
form.setFieldValue(
"allow_user_cancel_workspace_jobs",
checked === true,
)
}
disabled={isSubmitting}
className="mt-0.5"
/>
<label
htmlFor="allow_user_cancel_workspace_jobs"
className="flex flex-col gap-1 cursor-pointer"
>
<span className="text-sm font-medium">
Allow users to cancel in-progress workspace jobs.
<StackLabelHelperText>
Depending on your template, canceling builds may leave
workspaces in an unhealthy state. This option isn&apos;t
recommended for most use cases.{" "}
<strong>
If checked, users may be able to corrupt their workspace.
</strong>
</StackLabelHelperText>
</StackLabel>
}
/>
</span>
<span className="text-xs text-content-secondary">
Depending on your template, canceling builds may leave
workspaces in an unhealthy state. This option isn&apos;t
recommended for most use cases.{" "}
<strong className="text-content-primary">
If checked, users may be able to corrupt their workspace.
</strong>
</span>
</label>
</div>
<FormControlLabel
control={
<Checkbox
size="small"
id="require_active_version"
name="require_active_version"
checked={form.values.require_active_version}
onChange={form.handleChange}
disabled={
!template.require_active_version && !advancedSchedulingEnabled
}
/>
}
label={
<StackLabel>
<div className="flex items-start gap-3">
<Checkbox
id="require_active_version"
checked={form.values.require_active_version}
onCheckedChange={(checked) =>
form.setFieldValue("require_active_version", checked === true)
}
disabled={
!template.require_active_version && !advancedSchedulingEnabled
}
className="mt-0.5"
/>
<label
htmlFor="require_active_version"
className="flex flex-col gap-1 cursor-pointer"
>
<span className="text-sm font-medium">
Require workspaces automatically update when started.
<StackLabelHelperText>
<span>
Workspaces that are manually started or auto-started will
use the active template version.{" "}
<strong>
This setting is not enforced for template admins.
</strong>
</span>
<span className="text-xs text-content-secondary">
Workspaces that are manually started or auto-started will use
the active template version.{" "}
<strong className="text-content-primary">
This setting is not enforced for template admins.
</strong>
</span>
{!advancedSchedulingEnabled && (
<div className="flex items-center gap-2 mt-2">
<PremiumBadge />
<span className="text-xs text-content-secondary">
Premium license required to be enabled.
</span>
</div>
)}
</label>
</div>
{!advancedSchedulingEnabled && (
<Stack
direction="row"
spacing={2}
alignItems="center"
css={{ marginTop: 16 }}
>
<PremiumBadge />
<span>Premium license required to be enabled.</span>
</Stack>
<div className="flex items-start gap-3">
<Checkbox
id="use_classic_parameter_flow"
checked={!form.values.use_classic_parameter_flow}
onCheckedChange={(checked) =>
form.setFieldValue(
"use_classic_parameter_flow",
checked !== true,
)
}
className="mt-0.5"
/>
<label
htmlFor="use_classic_parameter_flow"
className="flex flex-col gap-1 cursor-pointer"
>
<span className="text-sm font-medium">
Enable dynamic parameters for workspace creation (recommended)
</span>
<span className="text-xs text-content-secondary">
The dynamic workspace form allows you to design your template
with additional form types and identity-aware conditional
parameters. This is the default option for new templates. The
classic workspace creation flow will be deprecated in a future
release.{" "}
<Link
className="text-xs inline-flex items-start pl-0"
href={docs(
"/admin/templates/extending-templates/dynamic-parameters",
)}
</StackLabelHelperText>
</StackLabel>
}
/>
<FormControlLabel
control={
<Checkbox
size="small"
id="use_classic_parameter_flow"
name="use_classic_parameter_flow"
checked={!form.values.use_classic_parameter_flow}
onChange={(event) =>
form.setFieldValue(
"use_classic_parameter_flow",
!event.currentTarget.checked,
)
}
disabled={false}
/>
}
label={
<StackLabel>
<span className="flex flex-row gap-2">
Enable dynamic parameters for workspace creation (recommended)
</span>
<StackLabelHelperText>
<div>
The dynamic workspace form allows you to design your
template with additional form types and identity-aware
conditional parameters. This is the default option for new
templates. The classic workspace creation flow will be
deprecated in a future release.
</div>
<Link
className="text-xs"
href={docs(
"/admin/templates/extending-templates/dynamic-parameters",
)}
>
Learn more
</Link>
</StackLabelHelperText>
</StackLabel>
}
/>
<FormControlLabel
control={
<Checkbox
size="small"
id="disable_module_cache"
name="disable_module_cache"
checked={form.values.disable_module_cache}
onChange={form.handleChange}
disabled={isSubmitting}
/>
}
label={
<StackLabel>
target="_blank"
>
Learn more
</Link>
</span>
</label>
</div>
<div className="flex items-start gap-3">
<Checkbox
id="disable_module_cache"
checked={form.values.disable_module_cache}
onCheckedChange={(checked) =>
form.setFieldValue("disable_module_cache", checked === true)
}
disabled={isSubmitting}
className="mt-0.5"
/>
<label
htmlFor="disable_module_cache"
className="flex flex-col gap-1 cursor-pointer"
>
<span className="text-sm font-medium">
Disable Terraform module caching
<StackLabelHelperText>
When checked, Terraform modules are re-downloaded for each
workspace build instead of using cached versions.{" "}
<strong>
Warning: This makes workspace builds less predictable and is
not recommended for production templates.
</strong>
</StackLabelHelperText>
</StackLabel>
}
/>
</span>
<span className="text-xs text-content-secondary">
When checked, Terraform modules are re-downloaded for each
workspace build instead of using cached versions.{" "}
<strong className="text-content-primary">
Warning: This makes workspace builds less predictable and is
not recommended for production templates.
</strong>
</span>
</label>
</div>
</FormFields>
</FormSection>
@@ -305,26 +347,40 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
description="Deprecating a template prevents any new workspaces from being created. Existing workspaces will continue to function."
>
<FormFields>
<TextField
{...getFieldHelpers("deprecation_message", {
helperText:
"Leave the message empty to keep the template active. Any message provided will mark the template as deprecated. Use this message to inform users of the deprecation and how to migrate to a new template.",
})}
disabled={
isSubmitting || (!template.deprecated && !accessControlEnabled)
}
fullWidth
label="Deprecation Message"
/>
<div className="flex flex-col items-start gap-2">
<Label htmlFor={deprecationField.id}>Deprecation Message</Label>
<Input
id={deprecationField.id}
name={deprecationField.name}
value={deprecationField.value}
onChange={deprecationField.onChange}
onBlur={deprecationField.onBlur}
disabled={
isSubmitting || (!template.deprecated && !accessControlEnabled)
}
aria-invalid={deprecationField.error}
/>
{deprecationField.error && (
<span className="text-xs text-content-destructive">
{deprecationField.helperText}
</span>
)}
<span className="text-xs text-content-secondary">
Leave the message empty to keep the template active. Any message
provided will mark the template as deprecated. Use this message to
inform users of the deprecation and how to migrate to a new
template.
</span>
</div>
{!accessControlEnabled && (
<Stack direction="row" spacing={2} alignItems="center">
<div className="flex items-center gap-2">
<PremiumBadge />
<FormHelperText>
<span className="text-xs text-content-secondary">
Premium license required to deprecate templates.
{template.deprecated &&
" You cannot change the message, but you may remove it to mark this template as no longer deprecated."}
</FormHelperText>
</Stack>
</span>
</div>
)}
</FormFields>
</FormSection>
@@ -337,33 +393,47 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
only be accessed by the workspace owner."
>
<FormFields>
<TextField
{...getFieldHelpers("max_port_share_level", {
helperText:
"The maximum level of port sharing allowed for workspaces.",
})}
disabled={isSubmitting || !portSharingControlsEnabled}
fullWidth
select
value={
portSharingControlsEnabled
? form.values.max_port_share_level
: "public"
}
label="Maximum Port Sharing Level"
>
<MenuItem value="owner">Owner</MenuItem>
<MenuItem value="organization">Organization</MenuItem>
<MenuItem value="authenticated">Authenticated</MenuItem>
<MenuItem value="public">Public</MenuItem>
</TextField>
<div className="flex flex-col items-start gap-2">
<Label htmlFor={portShareField.id}>
Maximum Port Sharing Level
</Label>
<Select
value={
portSharingControlsEnabled
? form.values.max_port_share_level
: "public"
}
onValueChange={(value) =>
form.setFieldValue("max_port_share_level", value)
}
disabled={isSubmitting || !portSharingControlsEnabled}
>
<SelectTrigger id={portShareField.id}>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="owner">Owner</SelectItem>
<SelectItem value="organization">Organization</SelectItem>
<SelectItem value="authenticated">Authenticated</SelectItem>
<SelectItem value="public">Public</SelectItem>
</SelectContent>
</Select>
{portShareField.error && (
<span className="text-xs text-content-destructive">
{portShareField.helperText}
</span>
)}
<span className="text-xs text-content-secondary">
The maximum level of port sharing allowed for workspaces.
</span>
</div>
{!portSharingControlsEnabled && (
<Stack direction="row" spacing={2} alignItems="center">
<div className="flex items-center gap-2">
<PremiumBadge />
<FormHelperText>
<span className="text-xs text-content-secondary">
Premium license required to control max port sharing level.
</FormHelperText>
</Stack>
</span>
</div>
)}
</FormFields>
</FormSection>
@@ -373,20 +443,32 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
description="Control how Cross-Origin Resource Sharing (CORS) requests are handled for all shared ports."
>
<FormFields>
<TextField
{...getFieldHelpers("cors_behavior", {
helperText:
"Use Passthru to bypass Coder's built-in CORS protection.",
})}
disabled={isSubmitting}
fullWidth
select
value={form.values.cors_behavior}
label="CORS Behavior"
>
<MenuItem value="simple">Simple (recommended)</MenuItem>
<MenuItem value="passthru">Passthru</MenuItem>
</TextField>
<div className="flex flex-col items-start gap-2">
<Label htmlFor={corsField.id}>CORS Behavior</Label>
<Select
value={form.values.cors_behavior}
onValueChange={(value) =>
form.setFieldValue("cors_behavior", value)
}
disabled={isSubmitting}
>
<SelectTrigger id={corsField.id}>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="simple">Simple (recommended)</SelectItem>
<SelectItem value="passthru">Passthru</SelectItem>
</SelectContent>
</Select>
{corsField.error && (
<span className="text-xs text-content-destructive">
{corsField.helperText}
</span>
)}
<span className="text-xs text-content-secondary">
Use Passthru to bypass Coder&apos;s built-in CORS protection.
</span>
</div>
</FormFields>
</FormSection>
@@ -1,6 +1,3 @@
import type { Interpolation, Theme } from "@emotion/react";
import MenuItem from "@mui/material/MenuItem";
import Select, { type SelectProps } from "@mui/material/Select";
import type {
Group,
ReducedUser,
@@ -21,8 +18,14 @@ import {
} from "components/DropdownMenu/DropdownMenu";
import { EmptyState } from "components/EmptyState/EmptyState";
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "components/Select/Select";
import { Spinner } from "components/Spinner/Spinner";
import { Stack } from "components/Stack/Stack";
import {
Table,
TableBody,
@@ -90,7 +93,7 @@ const AddTemplateUserOrGroup: FC<AddTemplateUserOrGroupProps> = ({
}
}}
>
<Stack direction="row" alignItems="center" spacing={1}>
<div className="flex flex-row items-center gap-2">
<UserOrGroupAutocomplete
exclude={excludeFromAutocomplete}
templateID={templateID}
@@ -102,19 +105,18 @@ const AddTemplateUserOrGroup: FC<AddTemplateUserOrGroupProps> = ({
<Select
defaultValue="use"
size="small"
css={styles.select}
disabled={isLoading}
onChange={(event) => {
setSelectedRole(event.target.value as TemplateRole);
onValueChange={(value) => {
setSelectedRole(value as TemplateRole);
}}
>
<MenuItem key="use" value="use">
Use
</MenuItem>
<MenuItem key="admin" value="admin">
Admin
</MenuItem>
<SelectTrigger className="w-[100px] text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="use">Use</SelectItem>
<SelectItem value="admin">Admin</SelectItem>
</SelectContent>
</Select>
<Button
@@ -126,35 +128,48 @@ const AddTemplateUserOrGroup: FC<AddTemplateUserOrGroupProps> = ({
</Spinner>
Add member
</Button>
</Stack>
</div>
</form>
);
};
const RoleSelect: FC<SelectProps> = (props) => {
interface RoleSelectProps {
value: TemplateRole;
disabled?: boolean;
onChange: (role: TemplateRole) => void;
}
const RoleSelect: FC<RoleSelectProps> = ({ value, disabled, onChange }) => {
return (
<Select
renderValue={(value) => <div css={styles.role}>{`${value}`}</div>}
css={styles.updateSelect}
{...props}
value={value}
disabled={disabled}
onValueChange={(val) => onChange(val as TemplateRole)}
>
<MenuItem key="use" value="use" css={styles.menuItem}>
<div>
<div>Use</div>
<div css={styles.menuItemSecondary}>
Can read and use this template to create workspaces.
<SelectTrigger className="w-[200px]">
<SelectValue>
<span className="capitalize">{value}</span>
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="use" className="py-3">
<div className="leading-snug w-[250px] whitespace-normal">
<div>Use</div>
<div className="text-sm text-content-secondary">
Can read and use this template to create workspaces.
</div>
</div>
</div>
</MenuItem>
<MenuItem key="admin" value="admin" css={styles.menuItem}>
<div>
<div>Admin</div>
<div css={styles.menuItemSecondary}>
Can modify all aspects of this template including permissions,
metadata, and template versions.
</SelectItem>
<SelectItem value="admin" className="py-3">
<div className="leading-snug w-[250px] whitespace-normal">
<div>Admin</div>
<div className="text-sm text-content-secondary">
Can modify all aspects of this template including permissions,
metadata, and template versions.
</div>
</div>
</div>
</MenuItem>
</SelectItem>
</SelectContent>
</Select>
);
};
@@ -216,7 +231,7 @@ export const TemplatePermissionsPageView: FC<
<PageHeaderTitle>Permissions</PageHeaderTitle>
</PageHeader>
<Stack spacing={2.5}>
<div className="flex flex-col gap-5">
{canUpdatePermissions && (
<AddTemplateUserOrGroup
templateACL={templateACL}
@@ -274,16 +289,13 @@ export const TemplatePermissionsPageView: FC<
<RoleSelect
value={group.role}
disabled={updatingGroupId === group.id}
onChange={(event) => {
onUpdateGroup(
group,
event.target.value as TemplateRole,
);
onChange={(role) => {
onUpdateGroup(group, role);
}}
/>
</Cond>
<Cond>
<div css={styles.role}>{group.role}</div>
<div className="capitalize">{group.role}</div>
</Cond>
</ChooseOne>
</TableCell>
@@ -330,16 +342,13 @@ export const TemplatePermissionsPageView: FC<
<RoleSelect
value={user.role}
disabled={updatingUserId === user.id}
onChange={(event) => {
onUpdateUser(
user,
event.target.value as TemplateRole,
);
onChange={(role) => {
onUpdateUser(user, role);
}}
/>
</Cond>
<Cond>
<div css={styles.role}>{user.role}</div>
<div className="capitalize">{user.role}</div>
</Cond>
</ChooseOne>
</TableCell>
@@ -374,49 +383,7 @@ export const TemplatePermissionsPageView: FC<
</ChooseOne>
</TableBody>
</Table>
</Stack>
</div>
</>
);
};
const styles = {
select: {
// Match button small height
fontSize: 14,
width: 100,
},
updateSelect: {
margin: 0,
// Set a fixed width for the select. It avoids selects having different sizes
// depending on how many roles they have selected.
width: 200,
"& .MuiSelect-root": {
// Adjusting padding because it does not have label
paddingTop: 12,
paddingBottom: 12,
".secondary": {
display: "none",
},
},
},
role: {
textTransform: "capitalize",
},
menuItem: {
lineHeight: "140%",
paddingTop: 12,
paddingBottom: 12,
whiteSpace: "normal",
inlineSize: "250px",
},
menuItemSecondary: (theme) => ({
fontSize: 14,
color: theme.palette.text.secondary,
}),
} satisfies Record<string, Interpolation<Theme>>;
@@ -1,7 +1,4 @@
import type { Interpolation, Theme } from "@emotion/react";
import Checkbox from "@mui/material/Checkbox";
import DialogActions from "@mui/material/DialogActions";
import FormControlLabel from "@mui/material/FormControlLabel";
import { Checkbox } from "components/Checkbox/Checkbox";
import type { ConfirmDialogProps } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
import { Dialog, DialogActionButtons } from "components/Dialogs/Dialog";
import type { FC } from "react";
@@ -54,19 +51,16 @@ export const ScheduleDialog: FC<ScheduleDialogProps> = ({
dormantWorkspacesToBeDeletedInWeek > 0);
return (
<Dialog
css={styles.dialogWrapper}
onClose={onClose}
open={open}
data-testid="dialog"
>
<div css={styles.dialogContent}>
<h3 css={styles.dialogTitle}>{title}</h3>
<Dialog onClose={onClose} open={open} data-testid="dialog">
<div className="text-content-secondary p-10">
<h3 className="m-0 mb-4 text-content-primary font-normal text-xl">
{title}
</h3>
{showDormancyWarning && (
<>
<h4>Dormancy Threshold</h4>
<p css={styles.dialogDescription}>
<p className="text-content-secondary leading-[160%] text-base [&_strong]:text-content-primary m-0 my-2">
This change will result in{" "}
<strong>{inactiveWorkspacesToGoDormant}</strong>{" "}
{inactiveWorkspacesToGoDormant === 1 ? "workspace" : "workspaces"}{" "}
@@ -78,25 +72,27 @@ export const ScheduleDialog: FC<ScheduleDialogProps> = ({
over the next 7 days. To prevent this, do you want to reset the
inactivity period for all template workspaces?
</p>
<FormControlLabel
css={{ marginTop: 16 }}
control={
<Checkbox
size="small"
onChange={(e) => {
updateInactiveWorkspaces(e.target.checked);
}}
/>
}
label="Prevent Dormancy - Reset all workspace inactivity periods"
/>
<div className="flex items-center gap-3 mt-4">
<Checkbox
id="prevent-dormancy"
onCheckedChange={(checked) => {
updateInactiveWorkspaces(checked === true);
}}
/>
<label
htmlFor="prevent-dormancy"
className="text-sm cursor-pointer"
>
Prevent Dormancy - Reset all workspace inactivity periods
</label>
</div>
</>
)}
{showDeletionWarning && (
<>
<h4>Dormancy Auto-Deletion</h4>
<p css={styles.dialogDescription}>
<p className="text-content-secondary leading-[160%] text-base [&_strong]:text-content-primary m-0 my-2">
This change will result in{" "}
<strong>{dormantWorkspacesToBeDeleted}</strong>{" "}
{dormantWorkspacesToBeDeleted === 1 ? "workspace" : "workspaces"}{" "}
@@ -108,23 +104,25 @@ export const ScheduleDialog: FC<ScheduleDialogProps> = ({
over the next 7 days. To prevent this, do you want to reset the
dormancy period for all template workspaces?
</p>
<FormControlLabel
css={{ marginTop: 16 }}
control={
<Checkbox
size="small"
onChange={(e) => {
updateDormantWorkspaces(e.target.checked);
}}
/>
}
label="Prevent Deletion - Reset all workspace dormancy periods"
/>
<div className="flex items-center gap-3 mt-4">
<Checkbox
id="prevent-deletion"
onCheckedChange={(checked) => {
updateDormantWorkspaces(checked === true);
}}
/>
<label
htmlFor="prevent-deletion"
className="text-sm cursor-pointer"
>
Prevent Deletion - Reset all workspace dormancy periods
</label>
</div>
</>
)}
</div>
<DialogActions>
<div className="flex justify-end gap-2 px-10 pb-10">
<DialogActionButtons
cancelText={cancelText}
confirmLoading={confirmLoading}
@@ -134,47 +132,7 @@ export const ScheduleDialog: FC<ScheduleDialogProps> = ({
onConfirm={onConfirm || onClose}
type="delete"
/>
</DialogActions>
</div>
</Dialog>
);
};
const styles = {
dialogWrapper: (theme) => ({
"& .MuiPaper-root": {
background: theme.palette.background.paper,
border: `1px solid ${theme.palette.divider}`,
},
"& .MuiDialogActions-spacing": {
padding: "0 40px 40px",
},
}),
dialogContent: (theme) => ({
color: theme.palette.text.secondary,
padding: 40,
}),
dialogTitle: (theme) => ({
margin: 0,
marginBottom: 16,
color: theme.palette.text.primary,
fontWeight: 400,
fontSize: 20,
}),
dialogDescription: (theme) => ({
color: theme.palette.text.secondary,
lineHeight: "160%",
fontSize: 16,
"& strong": {
color: theme.palette.text.primary,
},
"& p:not(.MuiFormHelperText-root)": {
margin: 0,
},
"& > p": {
margin: "8px 0",
},
}),
} satisfies Record<string, Interpolation<Theme>>;
@@ -1,6 +1,4 @@
import FormHelperText from "@mui/material/FormHelperText";
import { Button } from "components/Button/Button";
import { Stack } from "components/Stack/Stack";
import type { FC } from "react";
import {
sortedDays,
@@ -21,14 +19,8 @@ export const TemplateScheduleAutostart: FC<TemplateScheduleAutostartProps> = ({
onChange,
}) => {
return (
<Stack width="100%" alignItems="start" spacing={1}>
<Stack
direction="row"
spacing={0}
alignItems="baseline"
justifyContent="center"
className="w-full gap-0.5"
>
<div className="flex w-full flex-col items-start gap-2">
<div className="flex w-full flex-row items-baseline justify-center gap-0.5">
{(
[
{ value: "monday", key: "Mon" },
@@ -60,11 +52,11 @@ export const TemplateScheduleAutostart: FC<TemplateScheduleAutostartProps> = ({
{day.key}
</Button>
))}
</Stack>
<FormHelperText>
</div>
<span className="text-xs text-content-secondary">
<AutostartHelperText allowed={enabled} days={value} />
</FormHelperText>
</Stack>
</span>
</div>
);
};
@@ -1,10 +1,6 @@
import Checkbox from "@mui/material/Checkbox";
import FormControlLabel from "@mui/material/FormControlLabel";
import MenuItem from "@mui/material/MenuItem";
import Switch from "@mui/material/Switch";
import TextField from "@mui/material/TextField";
import type { Template, UpdateTemplateMeta } from "api/typesGenerated";
import { Button } from "components/Button/Button";
import { Checkbox } from "components/Checkbox/Checkbox";
import { DurationField } from "components/DurationField/DurationField";
import {
FormFields,
@@ -12,14 +8,19 @@ import {
FormSection,
HorizontalForm,
} from "components/Form/Form";
import { Spinner } from "components/Spinner/Spinner";
import { Stack } from "components/Stack/Stack";
import { Input } from "components/Input/Input";
import { Label } from "components/Label/Label";
import {
StackLabel,
StackLabelHelperText,
} from "components/StackLabel/StackLabel";
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "components/Select/Select";
import { Spinner } from "components/Spinner/Spinner";
import { Switch } from "components/Switch/Switch";
import { type FormikTouched, useFormik } from "formik";
import { type ChangeEvent, type FC, useEffect, useState } from "react";
import { type FC, useEffect, useState } from "react";
import { getFormHelpers } from "utils/formUtils";
import {
calculateAutostopRequirementDaysValue,
@@ -58,7 +59,6 @@ const DORMANT_AUTODELETION_DEFAULT = 30 * MS_DAY_CONVERSION;
* increase the space can make it feels lighter.
*/
const FORM_FIELDS_SPACING = 8;
const DORMANT_FIELDSET_SPACING = 4;
export interface TemplateScheduleForm {
template: Template;
@@ -152,6 +152,13 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
error,
);
const defaultTtlField = getFieldHelpers("default_ttl_ms");
const activityBumpField = getFieldHelpers("activity_bump_ms");
const autostopDaysField = getFieldHelpers(
"autostop_requirement_days_of_week",
);
const autostopWeeksField = getFieldHelpers("autostop_requirement_weeks");
const now = new Date();
const weekFromNow = new Date(now);
weekFromNow.setDate(now.getDate() + 7);
@@ -251,17 +258,14 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
}
}, [currentValues, setValues]);
const handleToggleFailureCleanup = async (e: ChangeEvent) => {
form.handleChange(e);
const handleToggleFailureCleanup = async () => {
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,
@@ -270,17 +274,14 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
}
};
const handleToggleInactivityCleanup = async (e: ChangeEvent) => {
form.handleChange(e);
const handleToggleInactivityCleanup = async () => {
if (!form.values.inactivity_cleanup_enabled) {
// fill time_til_dormant_ms with defaults
await form.setValues({
...form.values,
inactivity_cleanup_enabled: true,
time_til_dormant_ms: INACTIVITY_CLEANUP_DEFAULT,
});
} else {
// clear time_til_dormant_ms
await form.setValues({
...form.values,
inactivity_cleanup_enabled: false,
@@ -289,17 +290,14 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
}
};
const handleToggleDormantAutoDeletion = async (e: ChangeEvent) => {
form.handleChange(e);
const handleToggleDormantAutoDeletion = async () => {
if (!form.values.dormant_autodeletion_cleanup_enabled) {
// fill failure_ttl_ms with defaults
await form.setValues({
...form.values,
dormant_autodeletion_cleanup_enabled: true,
time_til_dormant_autodelete_ms: DORMANT_AUTODELETION_DEFAULT,
});
} else {
// clear failure_ttl_ms
await form.setValues({
...form.values,
dormant_autodeletion_cleanup_enabled: false,
@@ -318,107 +316,148 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
description="Define when workspaces created from this template are stopped."
>
<FormFields spacing={FORM_FIELDS_SPACING}>
<TextField
{...getFieldHelpers("default_ttl_ms", {
helperText: (
<DefaultTTLHelperText ttl={form.values.default_ttl_ms} />
),
})}
disabled={isSubmitting}
fullWidth
inputProps={{ min: 0, step: 1 }}
label="Default autostop (hours)"
type="number"
/>
<TextField
{...getFieldHelpers("activity_bump_ms", {
helperText: (
<ActivityBumpHelperText bump={form.values.activity_bump_ms} />
),
})}
disabled={isSubmitting}
fullWidth
inputProps={{ min: 0, step: 1 }}
label="Activity bump (hours)"
type="number"
/>
<Stack direction="row" css={styles.ttlFields}>
<TextField
{...getFieldHelpers("autostop_requirement_days_of_week", {
helperText: (
<AutostopRequirementDaysHelperText
days={form.values.autostop_requirement_days_of_week}
/>
),
})}
<div className="flex flex-col items-start gap-2">
<Label htmlFor={defaultTtlField.id}>Default autostop (hours)</Label>
<Input
id={defaultTtlField.id}
name={defaultTtlField.name}
value={defaultTtlField.value}
onChange={defaultTtlField.onChange}
onBlur={defaultTtlField.onBlur}
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>
<TextField
{...getFieldHelpers("autostop_requirement_weeks", {
helperText: (
<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"
min={0}
step={1}
aria-invalid={defaultTtlField.error}
/>
</Stack>
{defaultTtlField.error && (
<span className="text-xs text-content-destructive">
{defaultTtlField.helperText}
</span>
)}
<span className="text-xs text-content-secondary">
<DefaultTTLHelperText ttl={form.values.default_ttl_ms} />
</span>
</div>
<FormControlLabel
control={
<Checkbox
id="allow-user-autostop"
size="small"
disabled={isSubmitting || !allowAdvancedScheduling}
onChange={async (_, checked) => {
await form.setFieldValue("allow_user_autostop", checked);
}}
name="allow_user_autostop"
checked={form.values.allow_user_autostop}
<div className="flex flex-col items-start gap-2">
<Label htmlFor={activityBumpField.id}>Activity bump (hours)</Label>
<Input
id={activityBumpField.id}
name={activityBumpField.name}
value={activityBumpField.value}
onChange={activityBumpField.onChange}
onBlur={activityBumpField.onBlur}
disabled={isSubmitting}
type="number"
min={0}
step={1}
aria-invalid={activityBumpField.error}
/>
{activityBumpField.error && (
<span className="text-xs text-content-destructive">
{activityBumpField.helperText}
</span>
)}
<span className="text-xs text-content-secondary">
<ActivityBumpHelperText bump={form.values.activity_bump_ms} />
</span>
</div>
<div className="flex gap-4 w-full">
<div className="flex flex-col items-start gap-2 flex-1">
<Label htmlFor={autostopDaysField.id}>
Days with required stop
</Label>
<Select
value={form.values.autostop_requirement_days_of_week}
onValueChange={(value) =>
form.setFieldValue("autostop_requirement_days_of_week", value)
}
disabled={isSubmitting}
>
<SelectTrigger id={autostopDaysField.id}>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="off">Off</SelectItem>
<SelectItem value="daily">Daily</SelectItem>
<SelectItem value="saturday">Saturday</SelectItem>
<SelectItem value="sunday">Sunday</SelectItem>
</SelectContent>
</Select>
{autostopDaysField.error && (
<span className="text-xs text-content-destructive">
{autostopDaysField.helperText}
</span>
)}
<span className="text-xs text-content-secondary">
<AutostopRequirementDaysHelperText
days={form.values.autostop_requirement_days_of_week}
/>
</span>
</div>
<div className="flex flex-col items-start gap-2 flex-1">
<Label htmlFor={autostopWeeksField.id}>
Weeks between required stops
</Label>
<Input
id={autostopWeeksField.id}
name={autostopWeeksField.name}
value={autostopWeeksField.value}
onChange={autostopWeeksField.onChange}
onBlur={autostopWeeksField.onBlur}
disabled={
isSubmitting ||
!["saturday", "sunday"].includes(
form.values.autostop_requirement_days_of_week || "",
)
}
type="number"
min={1}
max={16}
step={1}
aria-invalid={autostopWeeksField.error}
/>
}
label={
<StackLabel>
{autostopWeeksField.error && (
<span className="text-xs text-content-destructive">
{autostopWeeksField.helperText}
</span>
)}
<span className="text-xs text-content-secondary">
<AutostopRequirementWeeksHelperText
days={form.values.autostop_requirement_days_of_week}
weeks={form.values.autostop_requirement_weeks}
/>
</span>
</div>
</div>
<div className="flex items-start gap-3">
<Checkbox
id="allow-user-autostop"
checked={form.values.allow_user_autostop}
onCheckedChange={(checked) =>
form.setFieldValue("allow_user_autostop", checked === true)
}
disabled={isSubmitting || !allowAdvancedScheduling}
className="mt-0.5"
/>
<label
htmlFor="allow-user-autostop"
className="flex flex-col gap-1 cursor-pointer"
>
<span className="text-sm font-medium">
Allow users to customize autostop duration for workspaces.
<StackLabelHelperText>
By default, workspaces will inherit the Autostop timer from
this template. Enabling this option allows users to set custom
Autostop timers on their workspaces or turn off the timer.
</StackLabelHelperText>
</StackLabel>
}
/>
</span>
<span className="text-xs text-content-secondary">
By default, workspaces will inherit the Autostop timer from this
template. Enabling this option allows users to set custom
Autostop timers on their workspaces or turn off the timer.
</span>
</label>
</div>
</FormFields>
</FormSection>
@@ -426,29 +465,24 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
title="Autostart"
description="Allow users to set custom autostart and autostop scheduling options for workspaces created from this template."
>
<Stack>
<FormControlLabel
control={
<Checkbox
id="allow_user_autostart"
size="small"
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}
/>
}
label={
<StackLabel>
Allow users to automatically start workspaces on a schedule.
</StackLabel>
}
/>
<div className="flex flex-col gap-4">
<div className="flex items-start gap-3">
<Checkbox
id="allow_user_autostart"
checked={form.values.allow_user_autostart}
onCheckedChange={(checked) =>
form.setFieldValue("allow_user_autostart", checked === true)
}
disabled={isSubmitting || !allowAdvancedScheduling}
className="mt-0.5"
/>
<label
htmlFor="allow_user_autostart"
className="text-sm font-medium cursor-pointer"
>
Allow users to automatically start workspaces on a schedule.
</label>
</div>
{allowAdvancedScheduling && (
<TemplateScheduleAutostart
@@ -465,7 +499,7 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
}}
/>
)}
</Stack>
</div>
</FormSection>
{allowAdvancedScheduling && (
@@ -474,27 +508,28 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
description="When enabled, Coder will mark workspaces as dormant after a period of time with no connections. Dormant workspaces can be auto-deleted (see below) or manually reviewed by the workspace owner or admins."
>
<FormFields spacing={FORM_FIELDS_SPACING}>
<Stack spacing={DORMANT_FIELDSET_SPACING}>
<FormControlLabel
control={
<Switch
size="small"
name="dormancyThreshold"
checked={form.values.inactivity_cleanup_enabled}
onChange={handleToggleInactivityCleanup}
/>
}
label={<StackLabel>Enable Dormancy Threshold</StackLabel>}
/>
<div className="flex flex-col gap-8">
<div className="flex items-center gap-3">
<Switch
id="dormancyThreshold"
checked={form.values.inactivity_cleanup_enabled}
onCheckedChange={handleToggleInactivityCleanup}
/>
<label
htmlFor="dormancyThreshold"
className="text-sm font-medium cursor-pointer"
>
Enable Dormancy Threshold
</label>
</div>
<DurationField
{...getFieldHelpers("time_til_dormant_ms", {
helperText: (
<DormancyTTLHelperText
ttl={form.values.time_til_dormant_ms}
/>
),
})}
{...getFieldHelpers("time_til_dormant_ms")}
helperText={
<DormancyTTLHelperText
ttl={form.values.time_til_dormant_ms}
/>
}
label="Time until dormant"
valueMs={form.values.time_til_dormant_ms ?? 0}
onChange={(v) => form.setFieldValue("time_til_dormant_ms", v)}
@@ -502,39 +537,38 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
isSubmitting || !form.values.inactivity_cleanup_enabled
}
/>
</Stack>
</div>
<Stack spacing={DORMANT_FIELDSET_SPACING}>
<FormControlLabel
control={
<Switch
size="small"
name="dormancyAutoDeletion"
checked={form.values.dormant_autodeletion_cleanup_enabled}
onChange={handleToggleDormantAutoDeletion}
<div className="flex flex-col gap-8">
<div className="flex items-center gap-3">
<Switch
id="dormancyAutoDeletion"
checked={form.values.dormant_autodeletion_cleanup_enabled}
onCheckedChange={handleToggleDormantAutoDeletion}
/>
<label
htmlFor="dormancyAutoDeletion"
className="flex flex-col gap-1 cursor-pointer"
>
<span className="text-sm font-medium">
Enable Dormancy Auto-Deletion
</span>
<span className="text-xs text-content-secondary">
When enabled, Coder will permanently delete dormant
workspaces after a period of time.{" "}
<strong className="text-content-primary">
Once a workspace is deleted it cannot be recovered.
</strong>
</span>
</label>
</div>
<DurationField
{...getFieldHelpers("time_til_dormant_autodelete_ms")}
helperText={
<DormancyAutoDeletionTTLHelperText
ttl={form.values.time_til_dormant_autodelete_ms}
/>
}
label={
<StackLabel>
Enable Dormancy Auto-Deletion
<StackLabelHelperText>
When enabled, Coder will permanently delete dormant
workspaces after a period of time.{" "}
<strong>
Once a workspace is deleted it cannot be recovered.
</strong>
</StackLabelHelperText>
</StackLabel>
}
/>
<DurationField
{...getFieldHelpers("time_til_dormant_autodelete_ms", {
helperText: (
<DormancyAutoDeletionTTLHelperText
ttl={form.values.time_til_dormant_autodelete_ms}
/>
),
})}
label="Time until deletion"
valueMs={form.values.time_til_dormant_autodelete_ms ?? 0}
onChange={(v) =>
@@ -545,40 +579,39 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
!form.values.dormant_autodeletion_cleanup_enabled
}
/>
</Stack>
</div>
<Stack spacing={DORMANT_FIELDSET_SPACING}>
<FormControlLabel
control={
<Switch
size="small"
name="failureCleanupEnabled"
checked={form.values.failure_cleanup_enabled}
onChange={handleToggleFailureCleanup}
/>
}
label={
<StackLabel>
<div className="flex flex-col gap-8">
<div className="flex items-center gap-3">
<Switch
id="failureCleanupEnabled"
checked={form.values.failure_cleanup_enabled}
onCheckedChange={handleToggleFailureCleanup}
/>
<label
htmlFor="failureCleanupEnabled"
className="flex flex-col gap-1 cursor-pointer"
>
<span className="text-sm font-medium">
Enable Failure Cleanup
<StackLabelHelperText>
When enabled, Coder will attempt to stop workspaces that
are in a failed state after a period of time.
</StackLabelHelperText>
</StackLabel>
}
/>
</span>
<span className="text-xs text-content-secondary">
When enabled, Coder will attempt to stop workspaces that are
in a failed state after a period of time.
</span>
</label>
</div>
<DurationField
{...getFieldHelpers("failure_ttl_ms", {
helperText: (
<FailureTTLHelperText ttl={form.values.failure_ttl_ms} />
),
})}
{...getFieldHelpers("failure_ttl_ms")}
helperText={
<FailureTTLHelperText ttl={form.values.failure_ttl_ms} />
}
label="Time until cleanup"
valueMs={form.values.failure_ttl_ms ?? 0}
onChange={(v) => form.setFieldValue("failure_ttl_ms", v)}
disabled={isSubmitting || !form.values.failure_cleanup_enabled}
/>
</Stack>
</div>
</FormFields>
</FormSection>
)}
@@ -646,12 +679,3 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
</HorizontalForm>
);
};
const styles = {
ttlFields: {
width: "100%",
},
dayButtons: {
borderRadius: "0px",
},
};
@@ -1,9 +1,9 @@
import FormControlLabel from "@mui/material/FormControlLabel";
import Radio from "@mui/material/Radio";
import RadioGroup from "@mui/material/RadioGroup";
import TextField from "@mui/material/TextField";
import type { TemplateVersionVariable } from "api/typesGenerated";
import { Input } from "components/Input/Input";
import { Label } from "components/Label/Label";
import { RadioGroup, RadioGroupItem } from "components/RadioGroup/RadioGroup";
import { type FC, useState } from "react";
export const SensitiveVariableHelperText: FC = () => {
return (
<span>
@@ -24,58 +24,58 @@ export const TemplateVariableField: FC<TemplateVariableFieldProps> = ({
initialValue,
disabled,
onChange,
...props
}) => {
const [variableValue, setVariableValue] = useState(initialValue);
if (isBoolean(templateVersionVariable)) {
return (
<RadioGroup
defaultValue={variableValue}
onChange={(event) => {
onChange(event.target.value);
onValueChange={(value) => {
onChange(value);
}}
disabled={disabled}
>
<FormControlLabel
disabled={disabled}
value="true"
control={<Radio size="small" />}
label="True"
/>
<FormControlLabel
disabled={disabled}
value="false"
control={<Radio size="small" />}
label="False"
/>
<div className="flex items-center gap-2">
<RadioGroupItem value="true" id="radio-true" />
<Label htmlFor="radio-true">True</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="false" id="radio-false" />
<Label htmlFor="radio-false">False</Label>
</div>
</RadioGroup>
);
}
return (
<TextField
{...props}
type={
templateVersionVariable.type === "number"
? "number"
: templateVersionVariable.sensitive
? "password"
: "string"
}
disabled={disabled}
autoFocus
fullWidth
label={templateVersionVariable.name}
value={variableValue}
placeholder={
templateVersionVariable.sensitive
? ""
: templateVersionVariable.default_value
}
onChange={(event) => {
setVariableValue(event.target.value);
onChange(event.target.value);
}}
/>
<div className="flex flex-col gap-2">
<Label htmlFor={`var-${templateVersionVariable.name}`}>
{templateVersionVariable.name}
</Label>
<Input
id={`var-${templateVersionVariable.name}`}
type={
templateVersionVariable.type === "number"
? "number"
: templateVersionVariable.sensitive
? "password"
: "text"
}
disabled={disabled}
autoFocus
className="w-full"
value={variableValue}
placeholder={
templateVersionVariable.sensitive
? ""
: templateVersionVariable.default_value
}
onChange={(event) => {
setVariableValue(event.target.value);
onChange(event.target.value);
}}
/>
</div>
);
};