Skip to content

Commit 3691b49

Browse files
committed
created WorkspaceDeletion directory
1 parent dca77ba commit 3691b49

File tree

16 files changed

+324
-120
lines changed

16 files changed

+324
-120
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Workspace } from "api/typesGenerated"
2+
import { displayImpendingDeletion } from "./utils"
3+
import { useDashboard } from "components/Dashboard/DashboardProvider"
4+
import { Pill } from "components/Pill/Pill"
5+
import ErrorIcon from "@mui/icons-material/ErrorOutline"
6+
7+
export const DeletionBadge = ({
8+
workspace,
9+
}: {
10+
workspace: Workspace
11+
}): JSX.Element | null => {
12+
const { entitlements, experiments } = useDashboard()
13+
const allowAdvancedScheduling =
14+
entitlements.features["advanced_template_scheduling"].enabled
15+
// This check can be removed when https://github.com/coder/coder/milestone/19
16+
// is merged up
17+
const allowWorkspaceActions = experiments.includes("workspace_actions")
18+
// return null
19+
20+
if (
21+
!displayImpendingDeletion(
22+
workspace,
23+
allowAdvancedScheduling,
24+
allowWorkspaceActions,
25+
)
26+
) {
27+
return null
28+
}
29+
30+
return <Pill icon={<ErrorIcon />} text="Impending deletion" type="error" />
31+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Workspace } from "api/typesGenerated"
2+
import { displayImpendingDeletion } from "./utils"
3+
import { useDashboard } from "components/Dashboard/DashboardProvider"
4+
import { Maybe } from "components/Conditionals/Maybe"
5+
import { AlertBanner } from "components/AlertBanner/AlertBanner"
6+
7+
export const DeletionBanner = ({
8+
workspace,
9+
onDismiss,
10+
displayImpendingDeletionBanner,
11+
}: {
12+
workspace?: Workspace
13+
onDismiss: () => void
14+
displayImpendingDeletionBanner: boolean
15+
}): JSX.Element | null => {
16+
const { entitlements, experiments } = useDashboard()
17+
const allowAdvancedScheduling =
18+
entitlements.features["advanced_template_scheduling"].enabled
19+
// This check can be removed when https://github.com/coder/coder/milestone/19
20+
// is merged up
21+
const allowWorkspaceActions = experiments.includes("workspace_actions")
22+
23+
return (
24+
<Maybe
25+
condition={Boolean(
26+
workspace &&
27+
displayImpendingDeletion(
28+
workspace,
29+
allowAdvancedScheduling,
30+
allowWorkspaceActions,
31+
) &&
32+
displayImpendingDeletionBanner,
33+
)}
34+
>
35+
<AlertBanner
36+
severity="info"
37+
onDismiss={onDismiss}
38+
dismissible
39+
text="You have workspaces that will be deleted soon."
40+
/>
41+
</Maybe>
42+
)
43+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { Maybe } from "components/Conditionals/Maybe"
2+
import { StatsItem } from "components/Stats/Stats"
3+
import Link from "@mui/material/Link"
4+
import { Link as RouterLink } from "react-router-dom"
5+
import styled from "@emotion/styled"
6+
import { Workspace } from "api/typesGenerated"
7+
import { displayImpendingDeletion } from "./utils"
8+
import { useDashboard } from "components/Dashboard/DashboardProvider"
9+
10+
export const DeletionStat = ({
11+
workspace,
12+
}: {
13+
workspace: Workspace
14+
}): JSX.Element => {
15+
const { entitlements, experiments } = useDashboard()
16+
const allowAdvancedScheduling =
17+
entitlements.features["advanced_template_scheduling"].enabled
18+
// This check can be removed when https://github.com/coder/coder/milestone/19
19+
// is merged up
20+
const allowWorkspaceActions = experiments.includes("workspace_actions")
21+
22+
return (
23+
<Maybe
24+
condition={displayImpendingDeletion(
25+
workspace,
26+
allowAdvancedScheduling,
27+
allowWorkspaceActions,
28+
)}
29+
>
30+
<StyledStatsItem
31+
label="Deletion on"
32+
className="containerClass"
33+
value={
34+
<Link
35+
component={RouterLink}
36+
to={`/templates/${workspace.template_name}/settings/schedule`}
37+
title="Schedule settings"
38+
>
39+
{/* We check for string existence in the conditional */}
40+
{new Date(workspace.deleting_at as string).toLocaleString()}
41+
</Link>
42+
}
43+
/>
44+
</Maybe>
45+
)
46+
}
47+
48+
const StyledStatsItem = styled(StatsItem)(() => ({
49+
"&.containerClass": {
50+
flexDirection: "column",
51+
gap: 0,
52+
padding: 0,
53+
54+
"& > span:first-of-type": {
55+
fontSize: 12,
56+
fontWeight: 500,
57+
},
58+
},
59+
}))
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { Workspace } from "api/typesGenerated"
2+
import { displayImpendingDeletion } from "./utils"
3+
import { useDashboard } from "components/Dashboard/DashboardProvider"
4+
import styled from "@emotion/styled"
5+
import { Theme as MaterialUITheme } from "@mui/material/styles"
6+
7+
export const DeletionText = ({
8+
workspace,
9+
}: {
10+
workspace: Workspace
11+
}): JSX.Element | null => {
12+
const { entitlements, experiments } = useDashboard()
13+
const allowAdvancedScheduling =
14+
entitlements.features["advanced_template_scheduling"].enabled
15+
// This check can be removed when https://github.com/coder/coder/milestone/19
16+
// is merged up
17+
const allowWorkspaceActions = experiments.includes("workspace_actions")
18+
19+
if (
20+
!displayImpendingDeletion(
21+
workspace,
22+
allowAdvancedScheduling,
23+
allowWorkspaceActions,
24+
)
25+
) {
26+
return null
27+
}
28+
return <StyledSpan role="status">Impending deletion</StyledSpan>
29+
}
30+
31+
const StyledSpan = styled.span<{ theme?: MaterialUITheme }>`
32+
color: ${(props) => props.theme.palette.warning.light};
33+
font-weight: 600;
34+
`
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export * from "./DeletionStat"
2+
export * from "./DeletionBadge"
3+
export * from "./DeletionText"
4+
export * from "./DeletionBanner"
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import * as TypesGen from "api/typesGenerated"
2+
import * as Mocks from "testHelpers/entities"
3+
import { displayImpendingDeletion } from "./utils"
4+
5+
describe("util > workspace deletion", () => {
6+
describe("displayImpendingDeletion", () => {
7+
const today = new Date()
8+
it.each<[string, boolean, boolean, boolean]>([
9+
[
10+
new Date(new Date().setDate(today.getDate() + 15)).toISOString(),
11+
true,
12+
true,
13+
false,
14+
], // today + 15 days out
15+
[
16+
new Date(new Date().setDate(today.getDate() + 14)).toISOString(),
17+
true,
18+
true,
19+
true,
20+
], // today + 14
21+
[
22+
new Date(new Date().setDate(today.getDate() + 13)).toISOString(),
23+
true,
24+
true,
25+
true,
26+
], // today + 13
27+
[
28+
new Date(new Date().setDate(today.getDate() + 1)).toISOString(),
29+
true,
30+
true,
31+
true,
32+
], // today + 1
33+
[new Date().toISOString(), true, true, true], // today + 0
34+
[new Date().toISOString(), false, true, false], // Advanced Scheduling off
35+
[new Date().toISOString(), true, false, false], // Workspace Actions off
36+
])(
37+
`deleting_at=%p, allowAdvancedScheduling=%p, AllowWorkspaceActions=%p, shouldDisplay=%p`,
38+
(
39+
deleting_at,
40+
allowAdvancedScheduling,
41+
allowWorkspaceActions,
42+
shouldDisplay,
43+
) => {
44+
const workspace: TypesGen.Workspace = {
45+
...Mocks.MockWorkspace,
46+
deleting_at,
47+
}
48+
expect(
49+
displayImpendingDeletion(
50+
workspace,
51+
allowAdvancedScheduling,
52+
allowWorkspaceActions,
53+
),
54+
).toBe(shouldDisplay)
55+
},
56+
)
57+
})
58+
})
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Workspace } from "api/typesGenerated"
2+
3+
// This const dictates how far out we alert the user that a workspace
4+
// has an impending deletion (due to template.InactivityTTL being set)
5+
const IMPENDING_DELETION_DISPLAY_THRESHOLD = 14 // 14 days
6+
7+
/**
8+
* Returns a boolean indicating if an impending deletion indicator should be
9+
* displayed in the UI. Impending deletions are configured by setting the
10+
* Template.InactivityTTL
11+
* @param {TypesGen.Workspace} workspace
12+
* @returns {boolean}
13+
*/
14+
export const displayImpendingDeletion = (
15+
workspace: Workspace,
16+
allowAdvancedScheduling: boolean,
17+
allowWorkspaceActions: boolean,
18+
) => {
19+
const today = new Date()
20+
if (
21+
!workspace.deleting_at ||
22+
!allowAdvancedScheduling ||
23+
!allowWorkspaceActions
24+
) {
25+
return false
26+
}
27+
return (
28+
new Date(workspace.deleting_at) <=
29+
new Date(
30+
today.setDate(today.getDate() + IMPENDING_DELETION_DISPLAY_THRESHOLD),
31+
)
32+
)
33+
}

site/src/components/WorkspaceStats/WorkspaceStats.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import Popover from "@mui/material/Popover"
2020
import TextField from "@mui/material/TextField"
2121
import Button from "@mui/material/Button"
2222
import { WorkspaceStatusText } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge"
23+
import { DeletionStat } from "components/WorkspaceDeletion"
2324

2425
const Language = {
2526
workspaceDetails: "Workspace Details",
@@ -74,6 +75,7 @@ export const WorkspaceStats: FC<WorkspaceStatsProps> = ({
7475
label="Status"
7576
value={<WorkspaceStatusText workspace={workspace} />}
7677
/>
78+
<DeletionStat workspace={workspace} />
7779
<StatsItem
7880
className={styles.statsItem}
7981
label={Language.templateLabel}

site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx

Lines changed: 32 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import i18next from "i18next"
99
import { FC, PropsWithChildren } from "react"
1010
import { makeStyles } from "@mui/styles"
1111
import { combineClasses } from "utils/combineClasses"
12-
import { displayImpendingDeletion } from "utils/workspace"
13-
import { useDashboard } from "components/Dashboard/DashboardProvider"
12+
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"
13+
import { DeletionBadge, DeletionText } from "components/WorkspaceDeletion"
1414

1515
const LoadingIcon: FC = () => {
1616
return <CircularProgress size={10} style={{ color: "#FFF" }} />
@@ -93,59 +93,48 @@ export type WorkspaceStatusBadgeProps = {
9393
className?: string
9494
}
9595

96-
const ImpendingDeletionBadge: FC<Partial<WorkspaceStatusBadgeProps>> = ({
97-
className,
98-
}) => {
99-
const { entitlements, experiments } = useDashboard()
100-
const allowAdvancedScheduling =
101-
entitlements.features["advanced_template_scheduling"].enabled
102-
// This check can be removed when https://github.com/coder/coder/milestone/19
103-
// is merged up
104-
const allowWorkspaceActions = experiments.includes("workspace_actions")
105-
106-
if (!allowAdvancedScheduling || !allowWorkspaceActions) {
107-
return null
108-
}
109-
return (
110-
<Pill
111-
className={className}
112-
icon={<ErrorIcon />}
113-
text="Impending deletion"
114-
type="error"
115-
/>
116-
)
117-
}
118-
11996
export const WorkspaceStatusBadge: FC<
12097
PropsWithChildren<WorkspaceStatusBadgeProps>
12198
> = ({ workspace, className }) => {
122-
// The ImpendingDeletionBadge component itself checks to see if the
123-
// Advanced Scheduling feature is turned on and if the
124-
// Workspace Actions flag is turned on.
125-
if (displayImpendingDeletion(workspace)) {
126-
return <ImpendingDeletionBadge className={className} />
127-
}
128-
12999
const { text, icon, type } = getStatus(workspace.latest_build.status)
130-
return <Pill className={className} icon={icon} text={text} type={type} />
100+
return (
101+
<ChooseOne>
102+
{/* <DeletionBadge/> determines its own visibility */}
103+
<Cond condition={Boolean(DeletionBadge({ workspace }))}>
104+
<DeletionBadge workspace={workspace} />
105+
</Cond>
106+
<Cond>
107+
<Pill className={className} icon={icon} text={text} type={type} />
108+
</Cond>
109+
</ChooseOne>
110+
)
131111
}
132112

133113
export const WorkspaceStatusText: FC<
134114
PropsWithChildren<WorkspaceStatusBadgeProps>
135115
> = ({ workspace, className }) => {
136116
const styles = useStyles()
137117
const { text, type } = getStatus(workspace.latest_build.status)
118+
138119
return (
139-
<span
140-
role="status"
141-
className={combineClasses([
142-
className,
143-
styles.root,
144-
styles[`type-${type}`],
145-
])}
146-
>
147-
{text}
148-
</span>
120+
<ChooseOne>
121+
{/* <DeletionText/> determines its own visibility */}
122+
<Cond condition={Boolean(DeletionText({ workspace }))}>
123+
<DeletionText workspace={workspace} />
124+
</Cond>
125+
<Cond>
126+
<span
127+
role="status"
128+
className={combineClasses([
129+
className,
130+
styles.root,
131+
styles[`type-${type}`],
132+
])}
133+
>
134+
{text}
135+
</span>
136+
</Cond>
137+
</ChooseOne>
149138
)
150139
}
151140

0 commit comments

Comments
 (0)