fix: add front-end fixes for minor workspace action bugs (#8252)

* fix: incorrect copy on inactivity_ttl field

* disabling locked fields unless inactivity TTL is set

* scoping inactivity dialog message to template

* fixed pluralization for inactivity dialog

* amending logic gate to show inactivity dialog

* fixed pagination bug
This commit is contained in:
Kira Pilot 2023-06-30 09:45:44 -04:00 committed by GitHub
parent 9f76dab348
commit c569528fb7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 72 additions and 22 deletions

View File

@ -1,5 +1,8 @@
import { Workspace } from "api/typesGenerated"
import { displayImpendingDeletion } from "./utils"
import {
displayImpendingDeletion,
IMPENDING_DELETION_DISPLAY_THRESHOLD,
} from "./utils"
import { useDashboard } from "components/Dashboard/DashboardProvider"
import { Alert } from "components/Alert/Alert"
import { formatDistanceToNow, differenceInDays, add, format } from "date-fns"
@ -48,7 +51,9 @@ export const ImpendingDeletionBanner = ({
new Date(),
)
const plusFourteen = add(new Date(), { days: 14 })
const plusFourteen = add(new Date(), {
days: IMPENDING_DELETION_DISPLAY_THRESHOLD,
})
return (
<Alert

View File

@ -2,7 +2,7 @@ import { Workspace } from "api/typesGenerated"
// This const dictates how far out we alert the user that a workspace
// has an impending deletion (due to template.InactivityTTL being set)
const IMPENDING_DELETION_DISPLAY_THRESHOLD = 14 // 14 days
export const IMPENDING_DELETION_DISPLAY_THRESHOLD = 14 // 14 days
/**
* Returns a boolean indicating if an impending deletion indicator should be

View File

@ -22,12 +22,14 @@
"failureTTLHelperText_zero": "Coder will not automatically stop failed workspaces",
"failureTTLHelperText_one": "Coder will attempt to stop failed workspaces after {{count}} day.",
"failureTTLHelperText_other": "Coder will attempt to stop failed workspaces after {{count}} days.",
"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.",
"inactivityTTLHelperText_zero": "Coder will not automatically lock inactive workspaces",
"inactivityTTLHelperText_one": "Coder will automatically lock inactive workspaces after {{count}} day.",
"inactivityTTLHelperText_other": "Coder will automatically lock 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.",
"inactivityDialogDescription_one": "There is {{count}} workspace that already matches this filter and will be deleted upon form submission. Are you sure you want to proceed?",
"inactivityDialogDescription_other": "There are {{count}} workspaces that already match this filter and will be deleted upon form submission. Are you sure you want to proceed?",
"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.",

View File

@ -14,6 +14,6 @@ export const OpenDialog: Story = {
submitValues: () => null,
isInactivityDialogOpen: true,
setIsInactivityDialogOpen: () => null,
workspacesToBeDeletedToday: 2,
numberWorkspacesToBeDeletedToday: 2,
},
}

View File

@ -1,16 +1,19 @@
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"
import { useTranslation } from "react-i18next"
export const InactivityDialog = ({
submitValues,
isInactivityDialogOpen,
setIsInactivityDialogOpen,
workspacesToBeDeletedToday,
numberWorkspacesToBeDeletedToday,
}: {
submitValues: () => void
isInactivityDialogOpen: boolean
setIsInactivityDialogOpen: (arg0: boolean) => void
workspacesToBeDeletedToday: number
numberWorkspacesToBeDeletedToday: number
}) => {
const { t } = useTranslation("templateSettingsPage")
return (
<ConfirmDialog
type="delete"
@ -22,9 +25,9 @@ export const InactivityDialog = ({
onClose={() => setIsInactivityDialogOpen(false)}
title="Delete inactive workspaces"
confirmText="Delete Workspaces"
description={`There are ${
workspacesToBeDeletedToday ? workspacesToBeDeletedToday : ""
} workspaces that already match this filter and will be deleted upon form submission. Are you sure you want to proceed?`}
description={t("inactivityDialogDescription", {
count: numberWorkspacesToBeDeletedToday,
})}
/>
)
}

View File

@ -82,7 +82,7 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
validationSchema,
onSubmit: () => {
if (
form.values.inactivity_cleanup_enabled &&
form.values.locked_cleanup_enabled &&
workspacesToBeDeletedToday &&
workspacesToBeDeletedToday.length > 0
) {
@ -100,7 +100,10 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
const { t } = useTranslation("templateSettingsPage")
const styles = useStyles()
const workspacesToBeDeletedToday = useWorkspacesToBeDeleted(form.values)
const workspacesToBeDeletedToday = useWorkspacesToBeDeleted(
form.values,
template.name,
)
const [isInactivityDialogOpen, setIsInactivityDialogOpen] =
useState<boolean>(false)
@ -305,6 +308,7 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
onChange={handleToggleFailureCleanup}
/>
}
disabled={isSubmitting}
label="Enable Failure Cleanup"
/>
<TextField
@ -337,6 +341,7 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
onChange={handleToggleInactivityCleanup}
/>
}
disabled={isSubmitting}
label="Enable Inactivity Cleanup"
/>
<TextField
@ -371,6 +376,9 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
onChange={handleToggleLockedCleanup}
/>
}
disabled={
isSubmitting || !form.values.inactivity_cleanup_enabled
}
label="Enable Locked Cleanup"
/>
<TextField
@ -396,7 +404,9 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
submitValues={submitValues}
isInactivityDialogOpen={isInactivityDialogOpen}
setIsInactivityDialogOpen={setIsInactivityDialogOpen}
workspacesToBeDeletedToday={workspacesToBeDeletedToday?.length ?? 0}
numberWorkspacesToBeDeletedToday={
workspacesToBeDeletedToday?.length ?? 0
}
/>
<FormFooter
onCancel={onCancel}

View File

@ -13,11 +13,12 @@ const inactiveStatuses: WorkspaceStatus[] = [
export const useWorkspacesToBeDeleted = (
formValues: TemplateScheduleFormValues,
templateName: string,
) => {
const { data: workspacesData } = useQuery({
queryKey: ["workspaces"],
queryFn: () => getWorkspaces({}),
enabled: formValues.inactivity_cleanup_enabled,
queryFn: () => getWorkspaces({ q: `template:${templateName}` }),
enabled: formValues.locked_cleanup_enabled,
})
return workspacesData?.workspaces?.filter((workspace: Workspace) => {
const isInactive = inactiveStatuses.includes(workspace.latest_build.status)

View File

@ -2,7 +2,11 @@ import { usePagination } from "hooks/usePagination"
import { FC } from "react"
import { Helmet } from "react-helmet-async"
import { pageTitle } from "utils/page"
import { useWorkspacesData, useWorkspaceUpdate } from "./data"
import {
useWorkspacesData,
useWorkspaceUpdate,
useWorkspacesWithImpendingDeletions,
} from "./data"
import { WorkspacesPageView } from "./WorkspacesPageView"
import { useOrganizationId, usePermissions } from "hooks"
import { useTemplateFilterMenu, useStatusFilterMenu } from "./filter/menus"
@ -29,6 +33,9 @@ const WorkspacesPage: FC = () => {
query: filter.query,
})
const updateWorkspace = useWorkspaceUpdate(queryKey)
const { data: workspacesWithImpendingDeletions } =
useWorkspacesWithImpendingDeletions()
const permissions = usePermissions()
const canFilterByUser = permissions.viewDeploymentValues
const userMenu = useUserFilterMenu({
@ -57,6 +64,7 @@ const WorkspacesPage: FC = () => {
<WorkspacesPageView
workspaces={data?.workspaces}
workspacesWithDeletions={workspacesWithImpendingDeletions?.workspaces}
error={error}
count={data?.count}
page={pagination.page}

View File

@ -33,6 +33,7 @@ export const Language = {
export interface WorkspacesPageViewProps {
error: unknown
workspaces?: Workspace[]
workspacesWithDeletions?: Workspace[]
count?: number
filterProps: ComponentProps<typeof WorkspacesFilter>
page: number
@ -45,6 +46,7 @@ export const WorkspacesPageView: FC<
React.PropsWithChildren<WorkspacesPageViewProps>
> = ({
workspaces,
workspacesWithDeletions,
error,
limit,
count,
@ -55,9 +57,9 @@ export const WorkspacesPageView: FC<
}) => {
const { saveLocal, getLocal } = useLocalStorage()
const workspaceIdsWithImpendingDeletions = workspaces
?.filter((workspace) => workspace.deleting_at)
.map((workspace) => workspace.id)
const workspaceIdsWithImpendingDeletions = workspacesWithDeletions?.map(
(workspace) => workspace.id,
)
/**
* Returns a boolean indicating if there are workspaces that have been
@ -105,7 +107,9 @@ export const WorkspacesPageView: FC<
</Maybe>
{/* <ImpendingDeletionBanner/> determines its own visibility */}
<ImpendingDeletionBanner
workspace={workspaces?.find((workspace) => workspace.deleting_at)}
workspace={workspacesWithDeletions?.find(
(workspace) => workspace.deleting_at,
)}
shouldRedisplayBanner={isNewWorkspacesImpendingDeletion()}
onDismiss={() =>
saveLocal(

View File

@ -14,6 +14,8 @@ import {
import { displayError } from "components/GlobalSnackbar/utils"
import { useState } from "react"
import { useTranslation } from "react-i18next"
import { add, format } from "date-fns"
import { IMPENDING_DELETION_DISPLAY_THRESHOLD } from "components/WorkspaceDeletion/utils"
type UseWorkspacesDataParams = {
page: number
@ -79,6 +81,21 @@ export const useWorkspaceUpdate = (queryKey: QueryKey) => {
})
}
// Returns workspace that will be deleted within 14 days.
// This query is used for the ImpendingDeletionBanner
export const useWorkspacesWithImpendingDeletions = () => {
return useQuery({
queryKey: ["workspacesWithImpendingDeletions"],
queryFn: () =>
getWorkspaces({
q: `deleting_by:"${format(
add(new Date(), { days: IMPENDING_DELETION_DISPLAY_THRESHOLD }),
"yyyy-MM-dd",
)}"`,
}),
})
}
const assignLatestBuild = (
oldResponse: WorkspacesResponse,
build: WorkspaceBuild,