Skip to content

Commit e73a202

Browse files
authored
feat: show dormant workspaces by default (#11053)
1 parent be31b2e commit e73a202

File tree

15 files changed

+196
-183
lines changed

15 files changed

+196
-183
lines changed

coderd/database/dbmem/dbmem.go

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7289,12 +7289,7 @@ func (q *FakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database.
72897289
}
72907290
}
72917291

7292-
// We omit locked workspaces by default.
7293-
if arg.IsDormant == "" && workspace.DormantAt.Valid {
7294-
continue
7295-
}
7296-
7297-
if arg.IsDormant != "" && !workspace.DormantAt.Valid {
7292+
if arg.Dormant && !workspace.DormantAt.Valid {
72987293
continue
72997294
}
73007295

coderd/database/modelqueries.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa
221221
arg.Name,
222222
arg.HasAgent,
223223
arg.AgentInactiveDisconnectTimeoutSeconds,
224-
arg.IsDormant,
224+
arg.Dormant,
225225
arg.LastUsedBefore,
226226
arg.LastUsedAfter,
227227
arg.Offset,

coderd/database/queries.sql.go

Lines changed: 5 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/workspaces.sql

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -239,13 +239,11 @@ WHERE
239239
) > 0
240240
ELSE true
241241
END
242-
-- Filter by dormant workspaces. By default we do not return dormant
243-
-- workspaces since they are considered soft-deleted.
242+
-- Filter by dormant workspaces.
244243
AND CASE
245-
WHEN @is_dormant :: text != '' THEN
244+
WHEN @dormant :: boolean != 'false' THEN
246245
dormant_at IS NOT NULL
247-
ELSE
248-
dormant_at IS NULL
246+
ELSE true
249247
END
250248
-- Filter by last_used
251249
AND CASE

coderd/searchquery/search.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ func Workspaces(query string, page codersdk.Pagination, agentInactiveDisconnectT
107107
filter.Name = parser.String(values, "", "name")
108108
filter.Status = string(httpapi.ParseCustom(parser, values, "", "status", httpapi.ParseEnum[database.WorkspaceStatus]))
109109
filter.HasAgent = parser.String(values, "", "has-agent")
110-
filter.IsDormant = parser.String(values, "", "is-dormant")
110+
filter.Dormant = parser.Boolean(values, false, "dormant")
111111
filter.LastUsedAfter = parser.Time3339Nano(values, time.Time{}, "last_used_after")
112112
filter.LastUsedBefore = parser.Time3339Nano(values, time.Time{}, "last_used_before")
113113

coderd/workspaces_test.go

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1503,43 +1503,50 @@ func TestWorkspaceFilterManual(t *testing.T) {
15031503
}, testutil.IntervalMedium, "agent status timeout")
15041504
})
15051505

1506-
t.Run("IsDormant", func(t *testing.T) {
1506+
t.Run("Dormant", func(t *testing.T) {
15071507
// this test has a licensed counterpart in enterprise/coderd/workspaces_test.go: FilterQueryHasDeletingByAndLicensed
15081508
t.Parallel()
1509-
client := coderdtest.New(t, &coderdtest.Options{
1510-
IncludeProvisionerDaemon: true,
1511-
})
1509+
client, db := coderdtest.NewWithDatabase(t, nil)
15121510
user := coderdtest.CreateFirstUser(t, client)
1513-
authToken := uuid.NewString()
1514-
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
1515-
Parse: echo.ParseComplete,
1516-
ProvisionPlan: echo.PlanComplete,
1517-
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
1518-
})
1519-
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
1520-
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
1511+
template := dbfake.TemplateVersion(t, db).Seed(database.TemplateVersion{
1512+
OrganizationID: user.OrganizationID,
1513+
CreatedBy: user.UserID,
1514+
}).Do().Template
15211515

15221516
// update template with inactivity ttl
15231517
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
15241518
defer cancel()
15251519

1526-
dormantWorkspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
1527-
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, dormantWorkspace.LatestBuild.ID)
1520+
dormantWorkspace := dbfake.WorkspaceBuild(t, db, database.Workspace{
1521+
TemplateID: template.ID,
1522+
OwnerID: user.UserID,
1523+
OrganizationID: user.OrganizationID,
1524+
}).Do().Workspace
15281525

15291526
// Create another workspace to validate that we do not return active workspaces.
1530-
_ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
1531-
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, dormantWorkspace.LatestBuild.ID)
1527+
_ = dbfake.WorkspaceBuild(t, db, database.Workspace{
1528+
TemplateID: template.ID,
1529+
OwnerID: user.UserID,
1530+
OrganizationID: user.OrganizationID,
1531+
}).Do()
15321532

15331533
err := client.UpdateWorkspaceDormancy(ctx, dormantWorkspace.ID, codersdk.UpdateWorkspaceDormancy{
15341534
Dormant: true,
15351535
})
15361536
require.NoError(t, err)
15371537

1538-
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
1539-
FilterQuery: "is-dormant:true",
1538+
// Test that no filter returns both workspaces.
1539+
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{})
1540+
require.NoError(t, err)
1541+
require.Len(t, res.Workspaces, 2)
1542+
1543+
// Test that filtering for dormant only returns our dormant workspace.
1544+
res, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
1545+
FilterQuery: "dormant:true",
15401546
})
15411547
require.NoError(t, err)
15421548
require.Len(t, res.Workspaces, 1)
1549+
require.Equal(t, dormantWorkspace.ID, res.Workspaces[0].ID)
15431550
require.NotNil(t, res.Workspaces[0].DormantAt)
15441551
})
15451552

enterprise/coderd/workspaces_test.go

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -991,53 +991,55 @@ func TestExecutorAutostartBlocked(t *testing.T) {
991991
func TestWorkspacesFiltering(t *testing.T) {
992992
t.Parallel()
993993

994-
t.Run("IsDormant", func(t *testing.T) {
994+
t.Run("Dormant", func(t *testing.T) {
995995
t.Parallel()
996996

997997
ctx := testutil.Context(t, testutil.WaitMedium)
998-
client, user := coderdenttest.New(t, &coderdenttest.Options{
998+
client, db, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
999999
Options: &coderdtest.Options{
1000-
IncludeProvisionerDaemon: true,
1001-
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore()),
1000+
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore()),
10021001
},
10031002
LicenseOptions: &coderdenttest.LicenseOptions{
10041003
Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1},
10051004
},
10061005
})
1007-
templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleTemplateAdmin())
1006+
templateAdminClient, templateAdmin := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin())
10081007

1009-
// Create a template version that passes to get a functioning workspace.
1010-
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
1011-
Parse: echo.ParseComplete,
1012-
ProvisionPlan: echo.PlanComplete,
1013-
ProvisionApply: echo.ApplyComplete,
1014-
})
1015-
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
1016-
1017-
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
1008+
resp := dbfake.TemplateVersion(t, db).Seed(database.TemplateVersion{
1009+
OrganizationID: owner.OrganizationID,
1010+
CreatedBy: owner.UserID,
1011+
}).Do()
10181012

1019-
dormantWS1 := coderdtest.CreateWorkspace(t, templateAdmin, user.OrganizationID, template.ID)
1020-
coderdtest.AwaitWorkspaceBuildJobCompleted(t, templateAdmin, dormantWS1.LatestBuild.ID)
1013+
dormantWS1 := dbfake.WorkspaceBuild(t, db, database.Workspace{
1014+
OwnerID: templateAdmin.ID,
1015+
OrganizationID: owner.OrganizationID,
1016+
}).Do().Workspace
10211017

1022-
dormantWS2 := coderdtest.CreateWorkspace(t, templateAdmin, user.OrganizationID, template.ID)
1023-
coderdtest.AwaitWorkspaceBuildJobCompleted(t, templateAdmin, dormantWS2.LatestBuild.ID)
1018+
dormantWS2 := dbfake.WorkspaceBuild(t, db, database.Workspace{
1019+
OwnerID: templateAdmin.ID,
1020+
OrganizationID: owner.OrganizationID,
1021+
TemplateID: resp.Template.ID,
1022+
}).Do().Workspace
10241023

1025-
activeWS := coderdtest.CreateWorkspace(t, templateAdmin, user.OrganizationID, template.ID)
1026-
coderdtest.AwaitWorkspaceBuildJobCompleted(t, templateAdmin, activeWS.LatestBuild.ID)
1024+
_ = dbfake.WorkspaceBuild(t, db, database.Workspace{
1025+
OwnerID: templateAdmin.ID,
1026+
OrganizationID: owner.OrganizationID,
1027+
TemplateID: resp.Template.ID,
1028+
}).Do().Workspace
10271029

1028-
err := templateAdmin.UpdateWorkspaceDormancy(ctx, dormantWS1.ID, codersdk.UpdateWorkspaceDormancy{Dormant: true})
1030+
err := templateAdminClient.UpdateWorkspaceDormancy(ctx, dormantWS1.ID, codersdk.UpdateWorkspaceDormancy{Dormant: true})
10291031
require.NoError(t, err)
10301032

1031-
err = templateAdmin.UpdateWorkspaceDormancy(ctx, dormantWS2.ID, codersdk.UpdateWorkspaceDormancy{Dormant: true})
1033+
err = templateAdminClient.UpdateWorkspaceDormancy(ctx, dormantWS2.ID, codersdk.UpdateWorkspaceDormancy{Dormant: true})
10321034
require.NoError(t, err)
10331035

1034-
resp, err := templateAdmin.Workspaces(ctx, codersdk.WorkspaceFilter{
1035-
FilterQuery: "is-dormant:true",
1036+
workspaces, err := templateAdminClient.Workspaces(ctx, codersdk.WorkspaceFilter{
1037+
FilterQuery: "dormant:true",
10361038
})
10371039
require.NoError(t, err)
1038-
require.Len(t, resp.Workspaces, 2)
1040+
require.Len(t, workspaces.Workspaces, 2)
10391041

1040-
for _, ws := range resp.Workspaces {
1042+
for _, ws := range workspaces.Workspaces {
10411043
if ws.ID != dormantWS1.ID && ws.ID != dormantWS2.ID {
10421044
t.Fatalf("Unexpected workspace %+v", ws)
10431045
}
Lines changed: 29 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import { formatDistanceToNow } from "date-fns";
2-
import Link from "@mui/material/Link";
3-
import { type FC } from "react";
4-
import { Link as RouterLink } from "react-router-dom";
2+
import { ReactNode, type FC } from "react";
53
import type { Workspace } from "api/typesGenerated";
64
import { useIsWorkspaceActionsEnabled } from "components/Dashboard/DashboardProvider";
75
import { Alert } from "components/Alert/Alert";
@@ -12,90 +10,67 @@ export enum Count {
1210
}
1311

1412
interface DormantWorkspaceBannerProps {
15-
workspaces?: Workspace[];
13+
workspace: Workspace;
1614
onDismiss: () => void;
1715
shouldRedisplayBanner: boolean;
18-
count?: Count;
1916
}
2017

2118
export const DormantWorkspaceBanner: FC<DormantWorkspaceBannerProps> = ({
22-
workspaces,
19+
workspace,
2320
onDismiss,
2421
shouldRedisplayBanner,
25-
count = Count.Singular,
2622
}) => {
2723
const experimentEnabled = useIsWorkspaceActionsEnabled();
2824

29-
if (!workspaces) {
30-
return null;
31-
}
32-
33-
const hasDormantWorkspaces = workspaces.find(
34-
(workspace) => workspace.dormant_at,
35-
);
36-
37-
const hasDeletionScheduledWorkspaces = workspaces.find(
38-
(workspace) => workspace.deleting_at,
39-
);
40-
4125
if (
4226
// Only show this if the experiment is included.
4327
!experimentEnabled ||
44-
!hasDormantWorkspaces ||
28+
!workspace.dormant_at ||
4529
// Banners should be redisplayed after dismissal when additional workspaces are newly scheduled for deletion
4630
!shouldRedisplayBanner
4731
) {
4832
return null;
4933
}
5034

51-
const formatDate = (dateStr: string): string => {
35+
const formatDate = (dateStr: string, timestamp: boolean): string => {
5236
const date = new Date(dateStr);
5337
return date.toLocaleDateString(undefined, {
5438
month: "long",
5539
day: "numeric",
5640
year: "numeric",
41+
...(timestamp ? { hour: "numeric", minute: "numeric" } : {}),
5742
});
5843
};
5944

60-
const alertText = (): string => {
61-
if (workspaces.length === 1) {
62-
if (
63-
hasDeletionScheduledWorkspaces &&
64-
hasDeletionScheduledWorkspaces.deleting_at &&
65-
hasDeletionScheduledWorkspaces.dormant_at
66-
) {
67-
return `This workspace has been dormant for ${formatDistanceToNow(
68-
Date.parse(hasDeletionScheduledWorkspaces.dormant_at),
69-
)} and is scheduled to be deleted on ${formatDate(
70-
hasDeletionScheduledWorkspaces.deleting_at,
71-
)} . To keep it you must activate the workspace.`;
72-
} else if (hasDormantWorkspaces && hasDormantWorkspaces.dormant_at) {
73-
return `This workspace has been dormant for ${formatDistanceToNow(
74-
Date.parse(hasDormantWorkspaces.dormant_at),
75-
)}
76-
and cannot be interacted
77-
with. Dormant workspaces are eligible for
78-
permanent deletion. To prevent deletion, activate
79-
the workspace.`;
80-
}
45+
const alertText = (): ReactNode => {
46+
if (workspace.deleting_at && workspace.dormant_at) {
47+
return (
48+
<>
49+
This workspace has not been used for{" "}
50+
{formatDistanceToNow(Date.parse(workspace.last_used_at))} and was
51+
marked dormant on {formatDate(workspace.dormant_at, false)}. It is
52+
scheduled to be deleted on {formatDate(workspace.deleting_at, true)}.
53+
To keep it you must activate the workspace.
54+
</>
55+
);
56+
} else if (workspace.dormant_at) {
57+
return (
58+
<>
59+
This workspace has not been used for{" "}
60+
{formatDistanceToNow(Date.parse(workspace.last_used_at))} and was
61+
marked dormant on {formatDate(workspace.dormant_at, false)}. It is not
62+
scheduled for auto-deletion but will become a candidate if
63+
auto-deletion is enabled on this template. To keep it you must
64+
activate the workspace.
65+
</>
66+
);
8167
}
8268
return "";
8369
};
8470

8571
return (
8672
<Alert severity="warning" onDismiss={onDismiss} dismissible>
87-
{count === Count.Singular ? (
88-
alertText()
89-
) : (
90-
<>
91-
<span>There are</span>{" "}
92-
<Link component={RouterLink} to="/workspaces?filter=is-dormant:true">
93-
workspaces
94-
</Link>{" "}
95-
that may be deleted soon due to inactivity. Activate the workspaces
96-
you wish to retain.
97-
</>
98-
)}
73+
{alertText()}
9974
</Alert>
10075
);
10176
};

0 commit comments

Comments
 (0)