-
Notifications
You must be signed in to change notification settings - Fork 894
feat: add impending deletion indicators to the workspace page #7588
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
Changes from 1 commit
3691b49
92d0365
819983c
8f728bb
e987c70
8322b63
5aec820
d94f9fb
20ba906
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 DeletionBadge = ({ | ||
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 <Pill icon={<ErrorIcon />} text="Impending deletion" type="error" /> | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { Workspace } from "api/typesGenerated" | ||
import { displayImpendingDeletion } from "./utils" | ||
import { useDashboard } from "components/Dashboard/DashboardProvider" | ||
import { Maybe } from "components/Conditionals/Maybe" | ||
import { AlertBanner } from "components/AlertBanner/AlertBanner" | ||
|
||
export const DeletionBanner = ({ | ||
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 ( | ||
<Maybe | ||
condition={Boolean( | ||
workspace && | ||
displayImpendingDeletion( | ||
workspace, | ||
allowAdvancedScheduling, | ||
allowWorkspaceActions, | ||
) && | ||
displayImpendingDeletionBanner, | ||
)} | ||
> | ||
<AlertBanner | ||
severity="info" | ||
onDismiss={onDismiss} | ||
dismissible | ||
text="You have workspaces that will be deleted soon." | ||
/> | ||
</Maybe> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 DeletionStat = ({ | ||
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 ( | ||
<Maybe | ||
condition={displayImpendingDeletion( | ||
workspace, | ||
allowAdvancedScheduling, | ||
allowWorkspaceActions, | ||
)} | ||
> | ||
<StyledStatsItem | ||
label="Deletion on" | ||
className="containerClass" | ||
value={ | ||
<Link | ||
component={RouterLink} | ||
to={`/templates/${workspace.template_name}/settings/schedule`} | ||
title="Schedule settings" | ||
> | ||
{/* We check for string existence in the conditional */} | ||
{new Date(workspace.deleting_at as string).toLocaleString()} | ||
</Link> | ||
} | ||
/> | ||
</Maybe> | ||
) | ||
} | ||
|
||
const StyledStatsItem = styled(StatsItem)(() => ({ | ||
"&.containerClass": { | ||
flexDirection: "column", | ||
gap: 0, | ||
padding: 0, | ||
|
||
"& > span:first-of-type": { | ||
fontSize: 12, | ||
fontWeight: 500, | ||
}, | ||
}, | ||
})) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 DeletionText = ({ | ||
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 <StyledSpan role="status">Impending deletion</StyledSpan> | ||
} | ||
|
||
const StyledSpan = styled.span<{ theme?: MaterialUITheme }>` | ||
color: ${(props) => props.theme.palette.warning.light}; | ||
font-weight: 600; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In MUI v5 you can style things like ChakraUI: <Box
component="span"
fontWeight={600}
color={theme => theme.palette.warning.light}
>
Impending Deletion
</Box> or <Box
component="span"
sx={{
fontWeight: 600,
color: theme.palette.warning.light,
}}
>
Impending Deletion
</Box> Just to let you know in case you find it better |
||
` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export * from "./DeletionStat" | ||
export * from "./DeletionBadge" | ||
export * from "./DeletionText" | ||
export * from "./DeletionBanner" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we really need this? Why not access those components directly? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good q!
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import * as TypesGen from "api/typesGenerated" | ||
import * as Mocks from "testHelpers/entities" | ||
import { displayImpendingDeletion } from "./utils" | ||
|
||
describe("util > workspace deletion", () => { | ||
describe("displayImpendingDeletion", () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we could let only this describe here. The previous one didn't "make sense" to me since we are only testing this function. |
||
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) | ||
}, | ||
) | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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), | ||
) | ||
) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Since we are using the copy "Impending deletion" should we not call this
ImpendingDeletionBadge
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure! I can adjust the names