Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 47020ef1bc | |||
| a67e4d5cb7 | |||
| 364c407826 | |||
| 5028362f34 | |||
| 4cf9216934 | |||
| 4270b9c04d |
@@ -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>
|
||||
);
|
||||
|
||||
+294
-212
@@ -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'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'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's built-in CORS protection.
|
||||
</span>
|
||||
</div>
|
||||
</FormFields>
|
||||
</FormSection>
|
||||
|
||||
|
||||
+58
-91
@@ -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>>;
|
||||
|
||||
+6
-14
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+251
-227
@@ -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",
|
||||
},
|
||||
};
|
||||
|
||||
+43
-43
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user