Skip to content

feat: add impending deletion filter to workspaces page #7860

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Jun 12, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
cleaned up form
  • Loading branch information
Kira-Pilot committed Jun 5, 2023
commit 7eb45e57b7732f762274815894f7b4f73c0a4517
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"
import { useWorkspacesData } from "./useWorkspacesData"
import { TemplateScheduleFormValues } from "./formHelpers"

export const InactivityDialog = ({
formValues,
submitValues,
isInactivityDialogOpen,
setIsInactivityDialogOpen,
}: {
formValues: TemplateScheduleFormValues
submitValues: (arg0: TemplateScheduleFormValues) => void
isInactivityDialogOpen: boolean
setIsInactivityDialogOpen: (arg0: boolean) => void
}) => {
const workspacesToBeDeletedToday = useWorkspacesData(formValues)

return (
<ConfirmDialog
type="delete"
open={isInactivityDialogOpen}
onConfirm={() => {
submitValues(formValues)
setIsInactivityDialogOpen(false)
}}
onClose={() => setIsInactivityDialogOpen(false)}
title="Delete inactive workspaces"
confirmText="Delete Workspaces"
description={`There are ${
workspacesToBeDeletedToday?.length ?? ""
} workspaces that already match this filter and will be deleted upon form submission. Are you sure you want to proceed?`}
/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Maybe } from "components/Conditionals/Maybe"
import { useTranslation } from "react-i18next"

export const TTLHelperText = ({
ttl,
translationName,
}: {
ttl?: number
translationName: string
}) => {
const { t } = useTranslation("templateSettingsPage")
const count = typeof ttl !== "number" ? 0 : ttl
return (
// no helper text if ttl is negative - error will show once field is considered touched
<Maybe condition={count >= 0}>
<span>{t(translationName, { count })}</span>
</Maybe>
)
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,9 @@
import TextField from "@mui/material/TextField"
import {
Template,
UpdateTemplateMeta,
WorkspaceStatus,
Workspace,
} from "api/typesGenerated"
import { Template, UpdateTemplateMeta } from "api/typesGenerated"
import { FormikTouched, useFormik } from "formik"
import { FC, ChangeEvent, useState } from "react"
import { getFormHelpers } from "utils/formUtils"
import * as Yup from "yup"
import i18next from "i18next"
import { useTranslation } from "react-i18next"
import { Maybe } from "components/Conditionals/Maybe"
import {
FormSection,
HorizontalForm,
Expand All @@ -24,87 +16,16 @@ import Link from "@mui/material/Link"
import Checkbox from "@mui/material/Checkbox"
import FormControlLabel from "@mui/material/FormControlLabel"
import Switch from "@mui/material/Switch"
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"
import { useQuery } from "@tanstack/react-query"
import { getWorkspaces } from "api/api"
import { compareAsc, add, endOfToday } from "date-fns"

const TTLHelperText = ({
ttl,
translationName,
}: {
ttl?: number
translationName: string
}) => {
const { t } = useTranslation("templateSettingsPage")
const count = typeof ttl !== "number" ? 0 : ttl
return (
// no helper text if ttl is negative - error will show once field is considered touched
<Maybe condition={count >= 0}>
<span>{t(translationName, { count })}</span>
</Maybe>
)
}
import { InactivityDialog } from "./InactivityDialog"
import { useWorkspacesData } from "./useWorkspacesData"
import { TemplateScheduleFormValues, getValidationSchema } from "./formHelpers"
import { TTLHelperText } from "./TTLHelperText"

const MAX_TTL_DAYS = 7
const MS_HOUR_CONVERSION = 3600000
const MS_DAY_CONVERSION = 86400000
const FAILURE_CLEANUP_DEFAULT = 7
const INACTIVITY_CLEANUP_DEFAULT = 180

export interface TemplateScheduleFormValues extends UpdateTemplateMeta {
failure_cleanup_enabled: boolean
inactivity_cleanup_enabled: boolean
}

export const getValidationSchema = (): Yup.AnyObjectSchema =>
Yup.object({
default_ttl_ms: Yup.number()
.integer()
.min(0, i18next.t("defaultTTLMinError", { ns: "templateSettingsPage" }))
.max(
24 * MAX_TTL_DAYS /* 7 days in hours */,
i18next.t("defaultTTLMaxError", { ns: "templateSettingsPage" }),
),
max_ttl_ms: Yup.number()
.integer()
.min(0, i18next.t("maxTTLMinError", { ns: "templateSettingsPage" }))
.max(
24 * MAX_TTL_DAYS /* 7 days in hours */,
i18next.t("maxTTLMaxError", { ns: "templateSettingsPage" }),
),
failure_ttl_ms: Yup.number()
.min(0, "Failure cleanup days must not be less than 0.")
.test(
"positive-if-enabled",
"Failure cleanup days must be greater than zero when enabled.",
function (value) {
const parent = this.parent as TemplateScheduleFormValues
if (parent.failure_cleanup_enabled) {
return Boolean(value)
} else {
return true
}
},
),
inactivity_ttl_ms: Yup.number()
.min(0, "Inactivity cleanup days must not be less than 0.")
.test(
"positive-if-enabled",
"Inactivity cleanup days must be greater than zero when enabled.",
function (value) {
const parent = this.parent as TemplateScheduleFormValues
if (parent.inactivity_cleanup_enabled) {
return Boolean(value)
} else {
return true
}
},
),
allow_user_autostart: Yup.boolean(),
allow_user_autostop: Yup.boolean(),
})

export interface TemplateScheduleForm {
template: Template
onSubmit: (data: UpdateTemplateMeta) => void
Expand Down Expand Up @@ -172,6 +93,9 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
)
const { t } = useTranslation("templateSettingsPage")
const styles = useStyles()

const workspacesToBeDeletedToday = useWorkspacesData(form.values)

const [isInactivityDialogOpen, setIsInactivityDialogOpen] =
useState<boolean>(false)

Expand All @@ -196,35 +120,6 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
})
}

const { data: workspacesData } = useQuery({
queryKey: ["workspaces"],
queryFn: () => getWorkspaces({}),
enabled: form.values.inactivity_cleanup_enabled,
})

const inactiveStatuses: WorkspaceStatus[] = [
"stopped",
"canceled",
"failed",
"deleted",
]

const workspacesToBeDeletedToday = workspacesData?.workspaces?.filter(
(workspace: Workspace) => {
const isInactive = inactiveStatuses.includes(
workspace.latest_build.status,
)

const proposedDeletion = add(new Date(workspace.last_used_at), {
days: form.values.inactivity_ttl_ms,
})

if (isInactive && compareAsc(proposedDeletion, endOfToday()) < 1) {
return workspace
}
},
)

const handleToggleFailureCleanup = async (e: ChangeEvent) => {
form.handleChange(e)
if (!form.values.failure_cleanup_enabled) {
Expand Down Expand Up @@ -437,25 +332,17 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
</FormSection>
</>
)}
<InactivityDialog
formValues={form.values}
submitValues={submitValues}
isInactivityDialogOpen={isInactivityDialogOpen}
setIsInactivityDialogOpen={setIsInactivityDialogOpen}
/>
<FormFooter
onCancel={onCancel}
isLoading={isSubmitting}
submitDisabled={!form.isValid || !form.dirty}
/>
<ConfirmDialog
type="delete"
open={isInactivityDialogOpen}
onConfirm={() => {
submitValues(form.values)
setIsInactivityDialogOpen(false)
}}
onClose={() => setIsInactivityDialogOpen(false)}
title="Delete inactive workspaces"
confirmText="Delete Workspaces"
description={`There are ${
workspacesToBeDeletedToday?.length ?? ""
} workspaces that already match this filter and will be deleted upon form submission. Are you sure you want to proceed?`}
/>
</HorizontalForm>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { UpdateTemplateMeta } from "api/typesGenerated"
import * as Yup from "yup"
import i18next from "i18next"

export interface TemplateScheduleFormValues extends UpdateTemplateMeta {
failure_cleanup_enabled: boolean
inactivity_cleanup_enabled: boolean
}

const MAX_TTL_DAYS = 7

export const getValidationSchema = (): Yup.AnyObjectSchema =>
Yup.object({
default_ttl_ms: Yup.number()
.integer()
.min(0, i18next.t("defaultTTLMinError", { ns: "templateSettingsPage" }))
.max(
24 * MAX_TTL_DAYS /* 7 days in hours */,
i18next.t("defaultTTLMaxError", { ns: "templateSettingsPage" }),
),
max_ttl_ms: Yup.number()
.integer()
.min(0, i18next.t("maxTTLMinError", { ns: "templateSettingsPage" }))
.max(
24 * MAX_TTL_DAYS /* 7 days in hours */,
i18next.t("maxTTLMaxError", { ns: "templateSettingsPage" }),
),
failure_ttl_ms: Yup.number()
.min(0, "Failure cleanup days must not be less than 0.")
.test(
"positive-if-enabled",
"Failure cleanup days must be greater than zero when enabled.",
function (value) {
const parent = this.parent as TemplateScheduleFormValues
if (parent.failure_cleanup_enabled) {
return Boolean(value)
} else {
return true
}
},
),
inactivity_ttl_ms: Yup.number()
.min(0, "Inactivity cleanup days must not be less than 0.")
.test(
"positive-if-enabled",
"Inactivity cleanup days must be greater than zero when enabled.",
function (value) {
const parent = this.parent as TemplateScheduleFormValues
if (parent.inactivity_cleanup_enabled) {
return Boolean(value)
} else {
return true
}
},
),
allow_user_autostart: Yup.boolean(),
allow_user_autostop: Yup.boolean(),
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useQuery } from "@tanstack/react-query"
import { getWorkspaces } from "api/api"
import { compareAsc, add, endOfToday } from "date-fns"
import { WorkspaceStatus, Workspace } from "api/typesGenerated"
import { TemplateScheduleFormValues } from "./formHelpers"

const inactiveStatuses: WorkspaceStatus[] = [
"stopped",
"canceled",
"failed",
"deleted",
]

export const useWorkspacesData = (formValues: TemplateScheduleFormValues) => {
const { data: workspacesData } = useQuery({
queryKey: ["workspaces"],
queryFn: () => getWorkspaces({}),
enabled: formValues.inactivity_cleanup_enabled,
})
const workspacesToBeDeletedToday = workspacesData?.workspaces?.filter(
(workspace: Workspace) => {
const isInactive = inactiveStatuses.includes(
workspace.latest_build.status,
)

const proposedDeletion = add(new Date(workspace.last_used_at), {
days: formValues.inactivity_ttl_ms,
})

if (isInactive && compareAsc(proposedDeletion, endOfToday()) < 1) {
return workspace
}
},
)

return workspacesToBeDeletedToday
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
renderWithTemplateSettingsLayout,
waitForLoaderToBeRemoved,
} from "testHelpers/renderHelpers"
import { getValidationSchema } from "./TemplateScheduleForm"
import { getValidationSchema } from "./TemplateScheduleForm/formHelpers"
import TemplateSchedulePage from "./TemplateSchedulePage"
import i18next from "i18next"

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Template, UpdateTemplateMeta } from "api/typesGenerated"
import { ComponentProps, FC } from "react"
import { TemplateScheduleForm } from "./TemplateScheduleForm"
import { TemplateScheduleForm } from "./TemplateScheduleForm/TemplateScheduleForm"
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"
import { makeStyles } from "@mui/styles"

Expand Down