diff --git a/site/src/components/Workspace/Workspace.stories.tsx b/site/src/components/Workspace/Workspace.stories.tsx index fd36e6997e4da..8ea8504a9e18d 100644 --- a/site/src/components/Workspace/Workspace.stories.tsx +++ b/site/src/components/Workspace/Workspace.stories.tsx @@ -2,12 +2,12 @@ import { action } from "@storybook/addon-actions" import { Story } from "@storybook/react" import { WatchAgentMetadataContext } from "components/Resources/AgentMetadata" import { ProvisionerJobLog } from "api/typesGenerated" -import * as Mocks from "../../testHelpers/entities" +import * as Mocks from "testHelpers/entities" import { Workspace, WorkspaceErrors, WorkspaceProps } from "./Workspace" import { withReactContext } from "storybook-react-context" import EventSource from "eventsourcemock" import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext" -import { MockProxyLatencies } from "../../testHelpers/entities" +import { DashboardProviderContext } from "components/Dashboard/DashboardProvider" export default { title: "components/Workspace", @@ -24,21 +24,37 @@ export default { ], } +const MockedAppearance = { + config: Mocks.MockAppearance, + preview: false, + setPreview: () => null, + save: () => null, +} + const Template: Story = (args) => ( - { - return - }, + buildInfo: Mocks.MockBuildInfo, + entitlements: Mocks.MockEntitlementsWithScheduling, + experiments: Mocks.MockExperiments, + appearance: MockedAppearance, }} > - - + { + return + }, + }} + > + + + ) export const Running = Template.bind({}) diff --git a/site/src/components/WorkspaceDeletion/ImpendingDeletionBadge.tsx b/site/src/components/WorkspaceDeletion/ImpendingDeletionBadge.tsx new file mode 100644 index 0000000000000..eb27db82a3e2e --- /dev/null +++ b/site/src/components/WorkspaceDeletion/ImpendingDeletionBadge.tsx @@ -0,0 +1,31 @@ +import { Workspace } from "api/typesGenerated" +import { displayImpendingDeletion } from "./utils" +import { useDashboard } from "components/Dashboard/DashboardProvider" +import { Pill } from "components/Pill/Pill" +import ErrorIcon from "@mui/icons-material/ErrorOutline" + +export const ImpendingDeletionBadge = ({ + workspace, +}: { + workspace: Workspace +}): JSX.Element | null => { + const { entitlements, experiments } = useDashboard() + const allowAdvancedScheduling = + entitlements.features["advanced_template_scheduling"].enabled + // This check can be removed when https://github.com/coder/coder/milestone/19 + // is merged up + const allowWorkspaceActions = experiments.includes("workspace_actions") + // return null + + if ( + !displayImpendingDeletion( + workspace, + allowAdvancedScheduling, + allowWorkspaceActions, + ) + ) { + return null + } + + return } text="Impending deletion" type="error" /> +} diff --git a/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx b/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx new file mode 100644 index 0000000000000..e75294945a048 --- /dev/null +++ b/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx @@ -0,0 +1,40 @@ +import { Workspace } from "api/typesGenerated" +import { displayImpendingDeletion } from "./utils" +import { useDashboard } from "components/Dashboard/DashboardProvider" +import { Maybe } from "components/Conditionals/Maybe" +import { Alert } from "components/Alert/Alert" + +export const ImpendingDeletionBanner = ({ + workspace, + onDismiss, + displayImpendingDeletionBanner, +}: { + workspace?: Workspace + onDismiss: () => void + displayImpendingDeletionBanner: boolean +}): JSX.Element | null => { + const { entitlements, experiments } = useDashboard() + const allowAdvancedScheduling = + entitlements.features["advanced_template_scheduling"].enabled + // This check can be removed when https://github.com/coder/coder/milestone/19 + // is merged up + const allowWorkspaceActions = experiments.includes("workspace_actions") + + return ( + + + You have workspaces that will be deleted soon. + + + ) +} diff --git a/site/src/components/WorkspaceDeletion/ImpendingDeletionStat.tsx b/site/src/components/WorkspaceDeletion/ImpendingDeletionStat.tsx new file mode 100644 index 0000000000000..aca1e21b22a97 --- /dev/null +++ b/site/src/components/WorkspaceDeletion/ImpendingDeletionStat.tsx @@ -0,0 +1,59 @@ +import { Maybe } from "components/Conditionals/Maybe" +import { StatsItem } from "components/Stats/Stats" +import Link from "@mui/material/Link" +import { Link as RouterLink } from "react-router-dom" +import styled from "@emotion/styled" +import { Workspace } from "api/typesGenerated" +import { displayImpendingDeletion } from "./utils" +import { useDashboard } from "components/Dashboard/DashboardProvider" + +export const ImpendingDeletionStat = ({ + workspace, +}: { + workspace: Workspace +}): JSX.Element => { + const { entitlements, experiments } = useDashboard() + const allowAdvancedScheduling = + entitlements.features["advanced_template_scheduling"].enabled + // This check can be removed when https://github.com/coder/coder/milestone/19 + // is merged up + const allowWorkspaceActions = experiments.includes("workspace_actions") + + return ( + + + {/* We check for string existence in the conditional */} + {new Date(workspace.deleting_at as string).toLocaleString()} + + } + /> + + ) +} + +const StyledStatsItem = styled(StatsItem)(() => ({ + "&.containerClass": { + flexDirection: "column", + gap: 0, + padding: 0, + + "& > span:first-of-type": { + fontSize: 12, + fontWeight: 500, + }, + }, +})) diff --git a/site/src/components/WorkspaceDeletion/ImpendingDeletionText.tsx b/site/src/components/WorkspaceDeletion/ImpendingDeletionText.tsx new file mode 100644 index 0000000000000..6191ed927cc69 --- /dev/null +++ b/site/src/components/WorkspaceDeletion/ImpendingDeletionText.tsx @@ -0,0 +1,34 @@ +import { Workspace } from "api/typesGenerated" +import { displayImpendingDeletion } from "./utils" +import { useDashboard } from "components/Dashboard/DashboardProvider" +import styled from "@emotion/styled" +import { Theme as MaterialUITheme } from "@mui/material/styles" + +export const ImpendingDeletionText = ({ + workspace, +}: { + workspace: Workspace +}): JSX.Element | null => { + const { entitlements, experiments } = useDashboard() + const allowAdvancedScheduling = + entitlements.features["advanced_template_scheduling"].enabled + // This check can be removed when https://github.com/coder/coder/milestone/19 + // is merged up + const allowWorkspaceActions = experiments.includes("workspace_actions") + + if ( + !displayImpendingDeletion( + workspace, + allowAdvancedScheduling, + allowWorkspaceActions, + ) + ) { + return null + } + return Impending deletion +} + +const StyledSpan = styled.span<{ theme?: MaterialUITheme }>` + color: ${(props) => props.theme.palette.warning.light}; + font-weight: 600; +` diff --git a/site/src/components/WorkspaceDeletion/index.ts b/site/src/components/WorkspaceDeletion/index.ts new file mode 100644 index 0000000000000..d58af8e83ac57 --- /dev/null +++ b/site/src/components/WorkspaceDeletion/index.ts @@ -0,0 +1,4 @@ +export * from "./ImpendingDeletionStat" +export * from "./ImpendingDeletionBadge" +export * from "./ImpendingDeletionText" +export * from "./ImpendingDeletionBanner" diff --git a/site/src/components/WorkspaceDeletion/utils.test.ts b/site/src/components/WorkspaceDeletion/utils.test.ts new file mode 100644 index 0000000000000..9436926ec7108 --- /dev/null +++ b/site/src/components/WorkspaceDeletion/utils.test.ts @@ -0,0 +1,56 @@ +import * as TypesGen from "api/typesGenerated" +import * as Mocks from "testHelpers/entities" +import { displayImpendingDeletion } from "./utils" + +describe("displayImpendingDeletion", () => { + const today = new Date() + it.each<[string, boolean, boolean, boolean]>([ + [ + new Date(new Date().setDate(today.getDate() + 15)).toISOString(), + true, + true, + false, + ], // today + 15 days out + [ + new Date(new Date().setDate(today.getDate() + 14)).toISOString(), + true, + true, + true, + ], // today + 14 + [ + new Date(new Date().setDate(today.getDate() + 13)).toISOString(), + true, + true, + true, + ], // today + 13 + [ + new Date(new Date().setDate(today.getDate() + 1)).toISOString(), + true, + true, + true, + ], // today + 1 + [new Date().toISOString(), true, true, true], // today + 0 + [new Date().toISOString(), false, true, false], // Advanced Scheduling off + [new Date().toISOString(), true, false, false], // Workspace Actions off + ])( + `deleting_at=%p, allowAdvancedScheduling=%p, AllowWorkspaceActions=%p, shouldDisplay=%p`, + ( + deleting_at, + allowAdvancedScheduling, + allowWorkspaceActions, + shouldDisplay, + ) => { + const workspace: TypesGen.Workspace = { + ...Mocks.MockWorkspace, + deleting_at, + } + expect( + displayImpendingDeletion( + workspace, + allowAdvancedScheduling, + allowWorkspaceActions, + ), + ).toBe(shouldDisplay) + }, + ) +}) diff --git a/site/src/components/WorkspaceDeletion/utils.ts b/site/src/components/WorkspaceDeletion/utils.ts new file mode 100644 index 0000000000000..745bd48ad8941 --- /dev/null +++ b/site/src/components/WorkspaceDeletion/utils.ts @@ -0,0 +1,33 @@ +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 + +/** + * Returns a boolean indicating if an impending deletion indicator should be + * displayed in the UI. Impending deletions are configured by setting the + * Template.InactivityTTL + * @param {TypesGen.Workspace} workspace + * @returns {boolean} + */ +export const displayImpendingDeletion = ( + workspace: Workspace, + allowAdvancedScheduling: boolean, + allowWorkspaceActions: boolean, +) => { + const today = new Date() + if ( + !workspace.deleting_at || + !allowAdvancedScheduling || + !allowWorkspaceActions + ) { + return false + } + return ( + new Date(workspace.deleting_at) <= + new Date( + today.setDate(today.getDate() + IMPENDING_DELETION_DISPLAY_THRESHOLD), + ) + ) +} diff --git a/site/src/components/WorkspaceStats/WorkspaceStats.stories.tsx b/site/src/components/WorkspaceStats/WorkspaceStats.stories.tsx index 2dc0f331d48ec..96fe4d636fee8 100644 --- a/site/src/components/WorkspaceStats/WorkspaceStats.stories.tsx +++ b/site/src/components/WorkspaceStats/WorkspaceStats.stories.tsx @@ -1,17 +1,40 @@ import { Story } from "@storybook/react" -import { MockWorkspace } from "testHelpers/entities" +import { + MockWorkspace, + MockAppearance, + MockBuildInfo, + MockEntitlementsWithScheduling, + MockExperiments, +} from "testHelpers/entities" import { WorkspaceStats, WorkspaceStatsProps, } from "../WorkspaceStats/WorkspaceStats" +import { DashboardProviderContext } from "components/Dashboard/DashboardProvider" export default { title: "components/WorkspaceStats", component: WorkspaceStats, } +const MockedAppearance = { + config: MockAppearance, + preview: false, + setPreview: () => null, + save: () => null, +} + const Template: Story = (args) => ( - + + + ) export const Example = Template.bind({}) diff --git a/site/src/components/WorkspaceStats/WorkspaceStats.tsx b/site/src/components/WorkspaceStats/WorkspaceStats.tsx index 26eb80d7ba364..c7d2f1a941de9 100644 --- a/site/src/components/WorkspaceStats/WorkspaceStats.tsx +++ b/site/src/components/WorkspaceStats/WorkspaceStats.tsx @@ -20,6 +20,7 @@ import Popover from "@mui/material/Popover" import TextField from "@mui/material/TextField" import Button from "@mui/material/Button" import { WorkspaceStatusText } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge" +import { ImpendingDeletionStat } from "components/WorkspaceDeletion" const Language = { workspaceDetails: "Workspace Details", @@ -74,6 +75,7 @@ export const WorkspaceStats: FC = ({ label="Status" value={} /> + null, + save: () => null, +} + const Template: Story = (args) => ( - + + + ) export const Running = Template.bind({}) diff --git a/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx b/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx index c7fff79d42ee2..64a0a91cc51a7 100644 --- a/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx +++ b/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx @@ -9,8 +9,11 @@ import i18next from "i18next" import { FC, PropsWithChildren } from "react" import { makeStyles } from "@mui/styles" import { combineClasses } from "utils/combineClasses" -import { displayImpendingDeletion } from "utils/workspace" -import { useDashboard } from "components/Dashboard/DashboardProvider" +import { ChooseOne, Cond } from "components/Conditionals/ChooseOne" +import { + ImpendingDeletionBadge, + ImpendingDeletionText, +} from "components/WorkspaceDeletion" const LoadingIcon: FC = () => { return @@ -93,41 +96,21 @@ export type WorkspaceStatusBadgeProps = { className?: string } -const ImpendingDeletionBadge: FC> = ({ - className, -}) => { - const { entitlements, experiments } = useDashboard() - const allowAdvancedScheduling = - entitlements.features["advanced_template_scheduling"].enabled - // This check can be removed when https://github.com/coder/coder/milestone/19 - // is merged up - const allowWorkspaceActions = experiments.includes("workspace_actions") - - if (!allowAdvancedScheduling || !allowWorkspaceActions) { - return null - } - return ( - } - text="Impending deletion" - type="error" - /> - ) -} - export const WorkspaceStatusBadge: FC< PropsWithChildren > = ({ workspace, className }) => { - // The ImpendingDeletionBadge component itself checks to see if the - // Advanced Scheduling feature is turned on and if the - // Workspace Actions flag is turned on. - if (displayImpendingDeletion(workspace)) { - return - } - const { text, icon, type } = getStatus(workspace.latest_build.status) - return + return ( + + {/* determines its own visibility */} + + + + + + + + ) } export const WorkspaceStatusText: FC< @@ -135,17 +118,26 @@ export const WorkspaceStatusText: FC< > = ({ workspace, className }) => { const styles = useStyles() const { text, type } = getStatus(workspace.latest_build.status) + return ( - - {text} - + + {/* determines its own visibility */} + + + + + + {text} + + + ) } diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx index af5424ab8cbf4..8bc0d9256fb18 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx @@ -9,6 +9,7 @@ import { useNavigate, useParams } from "react-router-dom" import { pageTitle } from "utils/page" import { useTemplateSettingsContext } from "../TemplateSettingsLayout" import { TemplateSchedulePageView } from "./TemplateSchedulePageView" +import { useLocalStorage } from "hooks" const TemplateSchedulePage: FC = () => { const { template: templateName } = useParams() as { template: string } @@ -20,6 +21,7 @@ const TemplateSchedulePage: FC = () => { // This check can be removed when https://github.com/coder/coder/milestone/19 // is merged up const allowWorkspaceActions = experiments.includes("workspace_actions") + const { clearLocal } = useLocalStorage() const { mutate: updateTemplate, @@ -30,6 +32,8 @@ const TemplateSchedulePage: FC = () => { { onSuccess: () => { displaySuccess("Template updated successfully") + // clear browser-stored list of workspaces impending deletion + clearLocal("dismissedWorkspaceList") }, }, ) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index 315bd7ec898ad..ad818cb501944 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -18,9 +18,11 @@ import { MockCanceledWorkspace, MockDeletingWorkspace, MockDeletedWorkspace, + MockWorkspaceWithDeletion, MockBuilds, MockTemplateVersion3, MockUser, + MockEntitlementsWithScheduling, } from "testHelpers/entities" import * as api from "../../api/api" import { Workspace } from "../../api/typesGenerated" @@ -373,6 +375,13 @@ describe("WorkspacePage", () => { ) }) + it("shows the Impending deletion status when the workspace is impending deletion", async () => { + jest + .spyOn(api, "getEntitlements") + .mockResolvedValue(MockEntitlementsWithScheduling) + await testStatus(MockWorkspaceWithDeletion, "Impending deletion") + }) + it("shows the timeline build", async () => { await renderWorkspacePage() const table = await screen.findByTestId("builds-table") diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index f69baa7d8fc20..cdd0753f26a32 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -6,17 +6,10 @@ import { workspaceFilterQuery } from "utils/filters" import { pageTitle } from "utils/page" import { useWorkspacesData, useWorkspaceUpdate } from "./data" import { WorkspacesPageView } from "./WorkspacesPageView" -import { useDashboard } from "components/Dashboard/DashboardProvider" const WorkspacesPage: FC = () => { const filter = useFilter(workspaceFilterQuery.me) const pagination = usePagination() - const { entitlements, experiments } = useDashboard() - const allowAdvancedScheduling = - entitlements.features["advanced_template_scheduling"].enabled - // This check can be removed when https://github.com/coder/coder/milestone/19 - // is merged up - const allowWorkspaceActions = experiments.includes("workspace_actions") const { data, error, queryKey } = useWorkspacesData({ ...pagination, @@ -42,8 +35,6 @@ const WorkspacesPage: FC = () => { onUpdateWorkspace={(workspace) => { updateWorkspace.mutate(workspace) }} - allowAdvancedScheduling={allowAdvancedScheduling} - allowWorkspaceActions={allowWorkspaceActions} /> ) diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx index 3282bc8eb067c..8308f5343031c 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx @@ -6,13 +6,20 @@ import { Workspace, WorkspaceStatus, WorkspaceStatuses, -} from "../../api/typesGenerated" -import { MockWorkspace } from "../../testHelpers/entities" -import { workspaceFilterQuery } from "../../utils/filters" +} from "api/typesGenerated" +import { + MockWorkspace, + MockAppearance, + MockBuildInfo, + MockEntitlementsWithScheduling, + MockExperiments, +} from "testHelpers/entities" +import { workspaceFilterQuery } from "utils/filters" import { WorkspacesPageView, WorkspacesPageViewProps, } from "./WorkspacesPageView" +import { DashboardProviderContext } from "components/Dashboard/DashboardProvider" const createWorkspace = ( status: WorkspaceStatus, @@ -55,6 +62,13 @@ const allWorkspaces = [ ...Object.values(additionalWorkspaces), ] +const MockedAppearance = { + config: MockAppearance, + preview: false, + setPreview: () => null, + save: () => null, +} + export default { title: "pages/WorkspacesPageView", component: WorkspacesPageView, @@ -64,7 +78,16 @@ export default { } as ComponentMeta const Template: Story = (args) => ( - + + + ) export const AllStates = Template.bind({}) diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index f85e3babce790..8d9e2dbfaffcd 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -1,6 +1,5 @@ import Link from "@mui/material/Link" import { Workspace } from "api/typesGenerated" -import { Alert } from "components/Alert/Alert" import { Maybe } from "components/Conditionals/Maybe" import { PaginationWidgetBase } from "components/PaginationWidget/PaginationWidgetBase" import { FC } from "react" @@ -18,6 +17,7 @@ import { WorkspacesTable } from "components/WorkspacesTable/WorkspacesTable" import { workspaceFilterQuery } from "utils/filters" import { useLocalStorage } from "hooks" import difference from "lodash/difference" +import { ImpendingDeletionBanner } from "components/WorkspaceDeletion" import { ErrorAlert } from "components/Alert/ErrorAlert" export const Language = { @@ -52,8 +52,6 @@ export interface WorkspacesPageViewProps { onPageChange: (page: number) => void onFilter: (query: string) => void onUpdateWorkspace: (workspace: Workspace) => void - allowAdvancedScheduling: boolean - allowWorkspaceActions: boolean } export const WorkspacesPageView: FC< @@ -68,8 +66,6 @@ export const WorkspacesPageView: FC< onFilter, onPageChange, onUpdateWorkspace, - allowAdvancedScheduling, - allowWorkspaceActions, }) => { const { saveLocal, getLocal } = useLocalStorage() @@ -98,14 +94,6 @@ export const WorkspacesPageView: FC< return diff && diff.length > 0 } - const displayImpendingDeletionBanner = - (allowAdvancedScheduling && - allowWorkspaceActions && - workspaceIdsWithImpendingDeletions && - workspaceIdsWithImpendingDeletions.length > 0 && - isNewWorkspacesImpendingDeletion()) ?? - false - return ( @@ -129,20 +117,16 @@ export const WorkspacesPageView: FC< - - - saveLocal( - "dismissedWorkspaceList", - JSON.stringify(workspaceIdsWithImpendingDeletions), - ) - } - dismissible - > - You have workspaces that will be deleted soon. - - + workspace.deleting_at)} + displayImpendingDeletionBanner={isNewWorkspacesImpendingDeletion()} + onDismiss={() => + saveLocal( + "dismissedWorkspaceList", + JSON.stringify(workspaceIdsWithImpendingDeletions), + ) + } + /> workspace", () => { expect(displayed).toEqual(workspace.template_display_name) }) }) - - describe("displayImpendingDeletion", () => { - const today = new Date() - it.each<[string, boolean]>([ - [new Date(new Date().setDate(today.getDate() + 15)).toISOString(), false], // today + 15 days out - [new Date(new Date().setDate(today.getDate() + 14)).toISOString(), true], // today + 14 - [new Date(new Date().setDate(today.getDate() + 13)).toISOString(), true], // today + 13 - [new Date(new Date().setDate(today.getDate() + 1)).toISOString(), true], // today + 1 - [new Date().toISOString(), true], // today + 0 - ])(`deleting_at=%p, isWorkspaceOn=%p`, (deleting_at, shouldDisplay) => { - const workspace: TypesGen.Workspace = { - ...Mocks.MockWorkspace, - deleting_at, - } - expect(displayImpendingDeletion(workspace)).toBe(shouldDisplay) - }) - }) }) diff --git a/site/src/utils/workspace.ts b/site/src/utils/workspace.ts index 44bd1e6c9c7c3..d180222e0a83d 100644 --- a/site/src/utils/workspace.ts +++ b/site/src/utils/workspace.ts @@ -185,27 +185,3 @@ export const getDisplayWorkspaceTemplateName = ( ? workspace.template_display_name : workspace.template_name } - -// 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 - -/** - * Returns a boolean indicating if an impending deletion indicator should be - * displayed in the UI. Impending deletions are configured by setting the - * Template.InactivityTTL - * @param {TypesGen.Workspace} workspace - * @returns {boolean} - */ -export const displayImpendingDeletion = (workspace: TypesGen.Workspace) => { - const today = new Date() - if (!workspace.deleting_at) { - return false - } - return ( - new Date(workspace.deleting_at) <= - new Date( - today.setDate(today.getDate() + IMPENDING_DELETION_DISPLAY_THRESHOLD), - ) - ) -}