mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add impending deletion indicators to the workspace page (#7588)
* created WorkspaceDeletion directory * remove commented code * attempting to fix workspace stories * fix lint * fix the rest of the stories * fix right stories * PR comments * fix lint
This commit is contained in:
@@ -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<WorkspaceProps> = (args) => (
|
||||
<ProxyContext.Provider
|
||||
<DashboardProviderContext.Provider
|
||||
value={{
|
||||
proxyLatencies: MockProxyLatencies,
|
||||
proxy: getPreferredProxy([], undefined),
|
||||
proxies: [],
|
||||
isLoading: false,
|
||||
isFetched: true,
|
||||
setProxy: () => {
|
||||
return
|
||||
},
|
||||
buildInfo: Mocks.MockBuildInfo,
|
||||
entitlements: Mocks.MockEntitlementsWithScheduling,
|
||||
experiments: Mocks.MockExperiments,
|
||||
appearance: MockedAppearance,
|
||||
}}
|
||||
>
|
||||
<Workspace {...args} />
|
||||
</ProxyContext.Provider>
|
||||
<ProxyContext.Provider
|
||||
value={{
|
||||
proxyLatencies: Mocks.MockProxyLatencies,
|
||||
proxy: getPreferredProxy([], undefined),
|
||||
proxies: [],
|
||||
isLoading: false,
|
||||
isFetched: true,
|
||||
setProxy: () => {
|
||||
return
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Workspace {...args} />
|
||||
</ProxyContext.Provider>
|
||||
</DashboardProviderContext.Provider>
|
||||
)
|
||||
|
||||
export const Running = Template.bind({})
|
||||
|
||||
@@ -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 <Pill icon={<ErrorIcon />} text="Impending deletion" type="error" />
|
||||
}
|
||||
@@ -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 (
|
||||
<Maybe
|
||||
condition={Boolean(
|
||||
workspace &&
|
||||
displayImpendingDeletion(
|
||||
workspace,
|
||||
allowAdvancedScheduling,
|
||||
allowWorkspaceActions,
|
||||
) &&
|
||||
displayImpendingDeletionBanner,
|
||||
)}
|
||||
>
|
||||
<Alert severity="info" onDismiss={onDismiss} dismissible>
|
||||
You have workspaces that will be deleted soon.
|
||||
</Alert>
|
||||
</Maybe>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<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,
|
||||
},
|
||||
},
|
||||
}))
|
||||
@@ -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 <StyledSpan role="status">Impending deletion</StyledSpan>
|
||||
}
|
||||
|
||||
const StyledSpan = styled.span<{ theme?: MaterialUITheme }>`
|
||||
color: ${(props) => props.theme.palette.warning.light};
|
||||
font-weight: 600;
|
||||
`
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./ImpendingDeletionStat"
|
||||
export * from "./ImpendingDeletionBadge"
|
||||
export * from "./ImpendingDeletionText"
|
||||
export * from "./ImpendingDeletionBanner"
|
||||
@@ -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)
|
||||
},
|
||||
)
|
||||
})
|
||||
@@ -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),
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -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<WorkspaceStatsProps> = (args) => (
|
||||
<WorkspaceStats {...args} />
|
||||
<DashboardProviderContext.Provider
|
||||
value={{
|
||||
buildInfo: MockBuildInfo,
|
||||
entitlements: MockEntitlementsWithScheduling,
|
||||
experiments: MockExperiments,
|
||||
appearance: MockedAppearance,
|
||||
}}
|
||||
>
|
||||
<WorkspaceStats {...args} />
|
||||
</DashboardProviderContext.Provider>
|
||||
)
|
||||
|
||||
export const Example = Template.bind({})
|
||||
|
||||
@@ -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<WorkspaceStatsProps> = ({
|
||||
label="Status"
|
||||
value={<WorkspaceStatusText workspace={workspace} />}
|
||||
/>
|
||||
<ImpendingDeletionStat workspace={workspace} />
|
||||
<StatsItem
|
||||
className={styles.statsItem}
|
||||
label={Language.templateLabel}
|
||||
|
||||
@@ -10,19 +10,40 @@ import {
|
||||
MockStoppedWorkspace,
|
||||
MockStoppingWorkspace,
|
||||
MockWorkspace,
|
||||
MockBuildInfo,
|
||||
MockEntitlementsWithScheduling,
|
||||
MockExperiments,
|
||||
MockAppearance,
|
||||
} from "testHelpers/entities"
|
||||
import {
|
||||
WorkspaceStatusBadge,
|
||||
WorkspaceStatusBadgeProps,
|
||||
} from "./WorkspaceStatusBadge"
|
||||
import { DashboardProviderContext } from "components/Dashboard/DashboardProvider"
|
||||
|
||||
export default {
|
||||
title: "components/WorkspaceStatusBadge",
|
||||
component: WorkspaceStatusBadge,
|
||||
}
|
||||
|
||||
const MockedAppearance = {
|
||||
config: MockAppearance,
|
||||
preview: false,
|
||||
setPreview: () => null,
|
||||
save: () => null,
|
||||
}
|
||||
|
||||
const Template: Story<WorkspaceStatusBadgeProps> = (args) => (
|
||||
<WorkspaceStatusBadge {...args} />
|
||||
<DashboardProviderContext.Provider
|
||||
value={{
|
||||
buildInfo: MockBuildInfo,
|
||||
entitlements: MockEntitlementsWithScheduling,
|
||||
experiments: MockExperiments,
|
||||
appearance: MockedAppearance,
|
||||
}}
|
||||
>
|
||||
<WorkspaceStatusBadge {...args} />
|
||||
</DashboardProviderContext.Provider>
|
||||
)
|
||||
|
||||
export const Running = Template.bind({})
|
||||
|
||||
@@ -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 <CircularProgress size={10} style={{ color: "#FFF" }} />
|
||||
@@ -93,41 +96,21 @@ export type WorkspaceStatusBadgeProps = {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const ImpendingDeletionBadge: FC<Partial<WorkspaceStatusBadgeProps>> = ({
|
||||
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 (
|
||||
<Pill
|
||||
className={className}
|
||||
icon={<ErrorIcon />}
|
||||
text="Impending deletion"
|
||||
type="error"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const WorkspaceStatusBadge: FC<
|
||||
PropsWithChildren<WorkspaceStatusBadgeProps>
|
||||
> = ({ 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 <ImpendingDeletionBadge className={className} />
|
||||
}
|
||||
|
||||
const { text, icon, type } = getStatus(workspace.latest_build.status)
|
||||
return <Pill className={className} icon={icon} text={text} type={type} />
|
||||
return (
|
||||
<ChooseOne>
|
||||
{/* <ImpendingDeletionBadge/> determines its own visibility */}
|
||||
<Cond condition={Boolean(ImpendingDeletionBadge({ workspace }))}>
|
||||
<ImpendingDeletionBadge workspace={workspace} />
|
||||
</Cond>
|
||||
<Cond>
|
||||
<Pill className={className} icon={icon} text={text} type={type} />
|
||||
</Cond>
|
||||
</ChooseOne>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<span
|
||||
role="status"
|
||||
className={combineClasses([
|
||||
className,
|
||||
styles.root,
|
||||
styles[`type-${type}`],
|
||||
])}
|
||||
>
|
||||
{text}
|
||||
</span>
|
||||
<ChooseOne>
|
||||
{/* <ImpendingDeletionText/> determines its own visibility */}
|
||||
<Cond condition={Boolean(ImpendingDeletionText({ workspace }))}>
|
||||
<ImpendingDeletionText workspace={workspace} />
|
||||
</Cond>
|
||||
<Cond>
|
||||
<span
|
||||
role="status"
|
||||
className={combineClasses([
|
||||
className,
|
||||
styles.root,
|
||||
styles[`type-${type}`],
|
||||
])}
|
||||
>
|
||||
{text}
|
||||
</span>
|
||||
</Cond>
|
||||
</ChooseOne>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -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<typeof WorkspacesPageView>
|
||||
|
||||
const Template: Story<WorkspacesPageViewProps> = (args) => (
|
||||
<WorkspacesPageView {...args} />
|
||||
<DashboardProviderContext.Provider
|
||||
value={{
|
||||
buildInfo: MockBuildInfo,
|
||||
entitlements: MockEntitlementsWithScheduling,
|
||||
experiments: MockExperiments,
|
||||
appearance: MockedAppearance,
|
||||
}}
|
||||
>
|
||||
<WorkspacesPageView {...args} />
|
||||
</DashboardProviderContext.Provider>
|
||||
)
|
||||
|
||||
export const AllStates = Template.bind({})
|
||||
|
||||
@@ -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 (
|
||||
<Margins>
|
||||
<PageHeader>
|
||||
@@ -129,20 +117,16 @@ export const WorkspacesPageView: FC<
|
||||
<Maybe condition={Boolean(error)}>
|
||||
<ErrorAlert error={error} />
|
||||
</Maybe>
|
||||
<Maybe condition={displayImpendingDeletionBanner}>
|
||||
<Alert
|
||||
severity="info"
|
||||
onDismiss={() =>
|
||||
saveLocal(
|
||||
"dismissedWorkspaceList",
|
||||
JSON.stringify(workspaceIdsWithImpendingDeletions),
|
||||
)
|
||||
}
|
||||
dismissible
|
||||
>
|
||||
You have workspaces that will be deleted soon.
|
||||
</Alert>
|
||||
</Maybe>
|
||||
<ImpendingDeletionBanner
|
||||
workspace={workspaces?.find((workspace) => workspace.deleting_at)}
|
||||
displayImpendingDeletionBanner={isNewWorkspacesImpendingDeletion()}
|
||||
onDismiss={() =>
|
||||
saveLocal(
|
||||
"dismissedWorkspaceList",
|
||||
JSON.stringify(workspaceIdsWithImpendingDeletions),
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<SearchBarWithFilter
|
||||
filter={filter}
|
||||
|
||||
@@ -827,7 +827,7 @@ export const MockDeletingWorkspace: TypesGen.Workspace = {
|
||||
}
|
||||
|
||||
export const MockWorkspaceWithDeletion = {
|
||||
...MockWorkspace,
|
||||
...MockStoppedWorkspace,
|
||||
deleting_at: new Date().toISOString(),
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
getDisplayVersionStatus,
|
||||
getDisplayWorkspaceBuildInitiatedBy,
|
||||
getDisplayWorkspaceTemplateName,
|
||||
displayImpendingDeletion,
|
||||
isWorkspaceOn,
|
||||
} from "./workspace"
|
||||
|
||||
@@ -140,21 +139,4 @@ describe("util > 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user