mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add locked TTL field to template meta (#8020)
This commit is contained in:
@@ -179,6 +179,7 @@ export interface CreateTemplateRequest {
|
||||
readonly allow_user_autostop?: boolean
|
||||
readonly failure_ttl_ms?: number
|
||||
readonly inactivity_ttl_ms?: number
|
||||
readonly locked_ttl_ms?: number
|
||||
readonly disable_everyone_group_access: boolean
|
||||
}
|
||||
|
||||
@@ -805,6 +806,7 @@ export interface Template {
|
||||
readonly allow_user_cancel_workspace_jobs: boolean
|
||||
readonly failure_ttl_ms: number
|
||||
readonly inactivity_ttl_ms: number
|
||||
readonly locked_ttl_ms: number
|
||||
}
|
||||
|
||||
// From codersdk/templates.go
|
||||
@@ -972,6 +974,7 @@ export interface UpdateTemplateMeta {
|
||||
readonly allow_user_cancel_workspace_jobs?: boolean
|
||||
readonly failure_ttl_ms?: number
|
||||
readonly inactivity_ttl_ms?: number
|
||||
readonly locked_ttl_ms?: number
|
||||
}
|
||||
|
||||
// From codersdk/users.go
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
"inactivityTTLHelperText_zero": "Coder will not automatically delete inactive workspaces",
|
||||
"inactivityTTLHelperText_one": "Coder will automatically delete inactive workspaces after {{count}} day.",
|
||||
"inactivityTTLHelperText_other": "Coder will automatically delete inactive workspaces after {{count}} days.",
|
||||
"lockedTTLHelperText_zero": "Coder will not automatically delete locked workspaces",
|
||||
"lockedTTLHelperText_one": "Coder will automatically delete locked workspaces after {{count}} day.",
|
||||
"lockedTTLHelperText_other": "Coder will automatically delete locked workspaces after {{count}} days.",
|
||||
"allowUserCancelWorkspaceJobsLabel": "Allow users to cancel in-progress workspace jobs.",
|
||||
"allowUserCancelWorkspaceJobsNotice": "Depending on your template, canceling builds may leave workspaces in an unhealthy state. This option isn't recommended for most use cases.",
|
||||
"allowUsersCancelHelperText": "If checked, users may be able to corrupt their workspace.",
|
||||
|
||||
+1
@@ -28,6 +28,7 @@ const validFormValues: FormValues = {
|
||||
allow_user_autostop: false,
|
||||
failure_ttl_ms: 0,
|
||||
inactivity_ttl_ms: 0,
|
||||
locked_ttl_ms: 0,
|
||||
}
|
||||
|
||||
const renderTemplateSettingsPage = async () => {
|
||||
|
||||
+61
-1
@@ -25,6 +25,7 @@ const MS_HOUR_CONVERSION = 3600000
|
||||
const MS_DAY_CONVERSION = 86400000
|
||||
const FAILURE_CLEANUP_DEFAULT = 7
|
||||
const INACTIVITY_CLEANUP_DEFAULT = 180
|
||||
const LOCKED_CLEANUP_DEFAULT = 30
|
||||
|
||||
export interface TemplateScheduleForm {
|
||||
template: Template
|
||||
@@ -65,6 +66,9 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
|
||||
inactivity_ttl_ms: allowAdvancedScheduling
|
||||
? template.inactivity_ttl_ms / MS_DAY_CONVERSION
|
||||
: 0,
|
||||
locked_ttl_ms: allowAdvancedScheduling
|
||||
? template.locked_ttl_ms / MS_DAY_CONVERSION
|
||||
: 0,
|
||||
|
||||
allow_user_autostart: template.allow_user_autostart,
|
||||
allow_user_autostop: template.allow_user_autostop,
|
||||
@@ -72,6 +76,8 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
|
||||
allowAdvancedScheduling && Boolean(template.failure_ttl_ms),
|
||||
inactivity_cleanup_enabled:
|
||||
allowAdvancedScheduling && Boolean(template.inactivity_ttl_ms),
|
||||
locked_cleanup_enabled:
|
||||
allowAdvancedScheduling && Boolean(template.locked_ttl_ms),
|
||||
},
|
||||
validationSchema,
|
||||
onSubmit: () => {
|
||||
@@ -114,6 +120,9 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
|
||||
inactivity_ttl_ms: form.values.inactivity_ttl_ms
|
||||
? form.values.inactivity_ttl_ms * MS_DAY_CONVERSION
|
||||
: undefined,
|
||||
locked_ttl_ms: form.values.locked_ttl_ms
|
||||
? form.values.locked_ttl_ms * MS_DAY_CONVERSION
|
||||
: undefined,
|
||||
|
||||
allow_user_autostart: form.values.allow_user_autostart,
|
||||
allow_user_autostop: form.values.allow_user_autostop,
|
||||
@@ -158,6 +167,25 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
|
||||
}
|
||||
}
|
||||
|
||||
const handleToggleLockedCleanup = async (e: ChangeEvent) => {
|
||||
form.handleChange(e)
|
||||
if (!form.values.locked_cleanup_enabled) {
|
||||
// fill failure_ttl_ms with defaults
|
||||
await form.setValues({
|
||||
...form.values,
|
||||
locked_cleanup_enabled: true,
|
||||
locked_ttl_ms: LOCKED_CLEANUP_DEFAULT,
|
||||
})
|
||||
} else {
|
||||
// clear failure_ttl_ms
|
||||
await form.setValues({
|
||||
...form.values,
|
||||
locked_cleanup_enabled: false,
|
||||
locked_ttl_ms: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<HorizontalForm
|
||||
onSubmit={form.handleSubmit}
|
||||
@@ -298,7 +326,7 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
|
||||
</FormSection>
|
||||
<FormSection
|
||||
title="Inactivity Cleanup"
|
||||
description="When enabled, Coder will automatically delete workspaces that are in an inactive state after a specified number of days."
|
||||
description="When enabled, Coder will lock workspaces that have not been accessed after a specified number of days."
|
||||
>
|
||||
<FormFields>
|
||||
<FormControlLabel
|
||||
@@ -330,6 +358,38 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
|
||||
/>
|
||||
</FormFields>
|
||||
</FormSection>
|
||||
<FormSection
|
||||
title="Locked Cleanup"
|
||||
description="When enabled, Coder will permanently delete workspaces that have been locked for a specified number of days."
|
||||
>
|
||||
<FormFields>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
name="lockedCleanupEnabled"
|
||||
checked={form.values.locked_cleanup_enabled}
|
||||
onChange={handleToggleLockedCleanup}
|
||||
/>
|
||||
}
|
||||
label="Enable Locked Cleanup"
|
||||
/>
|
||||
<TextField
|
||||
{...getFieldHelpers(
|
||||
"locked_ttl_ms",
|
||||
<TTLHelperText
|
||||
translationName="lockedTTLHelperText"
|
||||
ttl={form.values.locked_ttl_ms}
|
||||
/>,
|
||||
)}
|
||||
disabled={isSubmitting || !form.values.locked_cleanup_enabled}
|
||||
fullWidth
|
||||
inputProps={{ min: 0, step: "any" }}
|
||||
label="Time until cleanup (days)"
|
||||
type="number"
|
||||
aria-label="Locked Cleanup"
|
||||
/>
|
||||
</FormFields>
|
||||
</FormSection>
|
||||
</>
|
||||
)}
|
||||
<InactivityDialog
|
||||
|
||||
+15
@@ -5,6 +5,7 @@ import i18next from "i18next"
|
||||
export interface TemplateScheduleFormValues extends UpdateTemplateMeta {
|
||||
failure_cleanup_enabled: boolean
|
||||
inactivity_cleanup_enabled: boolean
|
||||
locked_cleanup_enabled: boolean
|
||||
}
|
||||
|
||||
const MAX_TTL_DAYS = 7
|
||||
@@ -63,6 +64,20 @@ export const getValidationSchema = (): Yup.AnyObjectSchema =>
|
||||
}
|
||||
},
|
||||
),
|
||||
locked_ttl_ms: Yup.number()
|
||||
.min(0, "Locked cleanup days must not be less than 0.")
|
||||
.test(
|
||||
"positive-if-enabled",
|
||||
"Locked cleanup days must be greater than zero when enabled.",
|
||||
function (value) {
|
||||
const parent = this.parent as TemplateScheduleFormValues
|
||||
if (parent.locked_cleanup_enabled) {
|
||||
return Boolean(value)
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
},
|
||||
),
|
||||
allow_user_autostart: Yup.boolean(),
|
||||
allow_user_autostop: Yup.boolean(),
|
||||
})
|
||||
|
||||
+39
-1
@@ -22,6 +22,7 @@ const validFormValues = {
|
||||
max_ttl_ms: 2,
|
||||
failure_ttl_ms: 7,
|
||||
inactivity_ttl_ms: 180,
|
||||
locked_ttl_ms: 30,
|
||||
}
|
||||
|
||||
const renderTemplateSchedulePage = async () => {
|
||||
@@ -37,11 +38,13 @@ const fillAndSubmitForm = async ({
|
||||
max_ttl_ms,
|
||||
failure_ttl_ms,
|
||||
inactivity_ttl_ms,
|
||||
locked_ttl_ms,
|
||||
}: {
|
||||
default_ttl_ms: number
|
||||
max_ttl_ms: number
|
||||
failure_ttl_ms: number
|
||||
inactivity_ttl_ms: number
|
||||
locked_ttl_ms: number
|
||||
}) => {
|
||||
const user = userEvent.setup()
|
||||
const defaultTtlLabel = t("defaultTtlLabel", { ns: "templateSettingsPage" })
|
||||
@@ -64,6 +67,11 @@ const fillAndSubmitForm = async ({
|
||||
})
|
||||
await user.type(inactivityTtlField, inactivity_ttl_ms.toString())
|
||||
|
||||
const lockedTtlField = screen.getByRole("checkbox", {
|
||||
name: /Locked Cleanup/i,
|
||||
})
|
||||
await user.type(lockedTtlField, locked_ttl_ms.toString())
|
||||
|
||||
const submitButton = await screen.findByText(
|
||||
FooterFormLanguage.defaultSubmitLabel,
|
||||
)
|
||||
@@ -111,7 +119,7 @@ describe("TemplateSchedulePage", () => {
|
||||
)
|
||||
})
|
||||
|
||||
test("failure and inactivity ttl converted to and from days", async () => {
|
||||
test("failure, inactivity, and locked ttl converted to and from days", async () => {
|
||||
await renderTemplateSchedulePage()
|
||||
|
||||
jest.spyOn(API, "updateTemplateMeta").mockResolvedValueOnce({
|
||||
@@ -127,6 +135,7 @@ describe("TemplateSchedulePage", () => {
|
||||
expect.objectContaining({
|
||||
failure_ttl_ms: validFormValues.failure_ttl_ms * 86400000,
|
||||
inactivity_ttl_ms: validFormValues.inactivity_ttl_ms * 86400000,
|
||||
locked_ttl_ms: validFormValues.locked_ttl_ms * 86400000,
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -218,4 +227,33 @@ describe("TemplateSchedulePage", () => {
|
||||
"Inactivity cleanup days must not be less than 0.",
|
||||
)
|
||||
})
|
||||
|
||||
it("allows a locked ttl of 7 days", () => {
|
||||
const values: UpdateTemplateMeta = {
|
||||
...validFormValues,
|
||||
locked_ttl_ms: 86400000 * 7,
|
||||
}
|
||||
const validate = () => getValidationSchema().validateSync(values)
|
||||
expect(validate).not.toThrowError()
|
||||
})
|
||||
|
||||
it("allows a locked ttl of 0", () => {
|
||||
const values: UpdateTemplateMeta = {
|
||||
...validFormValues,
|
||||
locked_ttl_ms: 0,
|
||||
}
|
||||
const validate = () => getValidationSchema().validateSync(values)
|
||||
expect(validate).not.toThrowError()
|
||||
})
|
||||
|
||||
it("disallows a negative inactivity ttl", () => {
|
||||
const values: UpdateTemplateMeta = {
|
||||
...validFormValues,
|
||||
locked_ttl_ms: -1,
|
||||
}
|
||||
const validate = () => getValidationSchema().validateSync(values)
|
||||
expect(validate).toThrowError(
|
||||
"Locked cleanup days must not be less than 0.",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -388,6 +388,7 @@ export const MockTemplate: TypesGen.Template = {
|
||||
allow_user_cancel_workspace_jobs: true,
|
||||
failure_ttl_ms: 0,
|
||||
inactivity_ttl_ms: 0,
|
||||
locked_ttl_ms: 0,
|
||||
allow_user_autostart: false,
|
||||
allow_user_autostop: false,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user