Skip to content

Commit 1255b07

Browse files
committed
added status badge on workspaces page
1 parent 0ba53bd commit 1255b07

File tree

10 files changed

+125
-49
lines changed

10 files changed

+125
-49
lines changed

coderd/workspaces.go

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/go-chi/chi/v5"
1515
"github.com/google/uuid"
1616
"github.com/tabbed/pqtype"
17+
"golang.org/x/exp/slices"
1718
"golang.org/x/xerrors"
1819

1920
"cdr.dev/slog"
@@ -1171,7 +1172,7 @@ func convertWorkspace(
11711172

11721173
var (
11731174
ttlMillis = convertWorkspaceTTLMillis(workspace.Ttl)
1174-
deletingAt = calculateDeletingAt(workspace, template)
1175+
deletingAt = calculateDeletingAt(workspace, template, workspaceBuild)
11751176
)
11761177
return codersdk.Workspace{
11771178
ID: workspace.ID,
@@ -1206,19 +1207,11 @@ func convertWorkspaceTTLMillis(i sql.NullInt64) *int64 {
12061207

12071208
// Calculate the time of the upcoming workspace deletion, if applicable; otherwise, return nil.
12081209
// Workspaces may have impending deletions if InactivityTTL feature is turned on and the workspace is inactive.
1209-
func calculateDeletingAt(workspace database.Workspace, template database.Template) *time.Time {
1210-
// Workspace is recently inactive but hasn't been inactive for longer than
1211-
// the specified template.InactivityTTL threshold
1212-
1213-
fmt.Println("last used at before now (aka inactive)", workspace.LastUsedAt.Before(time.Now()))
1214-
fmt.Println("last used at is more recent than now minus the TTL ", workspace.LastUsedAt.After(time.Now().Add(-time.Duration(template.InactivityTTL)*time.Nanosecond)))
1215-
1216-
workspaceRecentlyInactive := workspace.LastUsedAt.Before(time.Now()) &&
1217-
workspace.LastUsedAt.After(time.Now().Add(-time.Duration(template.InactivityTTL)*time.Nanosecond))
1218-
1219-
// If InactivityTTL is turned off (set to 0), if the workspace has already been deleted,
1220-
// or if the workspace is only recently inactive, there is no impending deletion
1221-
if template.InactivityTTL == 0 || workspace.Deleted || workspaceRecentlyInactive {
1210+
func calculateDeletingAt(workspace database.Workspace, template database.Template, build codersdk.WorkspaceBuild) *time.Time {
1211+
inactiveStatuses := []codersdk.WorkspaceStatus{codersdk.WorkspaceStatusStopped, codersdk.WorkspaceStatusCanceled, codersdk.WorkspaceStatusFailed, codersdk.WorkspaceStatusDeleted}
1212+
isInactive := slices.Contains(inactiveStatuses, build.Status)
1213+
// If InactivityTTL is turned off (set to 0) or if the workspace is active, there is no impending deletion
1214+
if template.InactivityTTL == 0 || !isInactive {
12221215
return nil
12231216
}
12241217

coderd/workspaces_internal_test.go

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/coder/coder/coderd/database"
1010
"github.com/coder/coder/coderd/util/ptr"
11+
"github.com/coder/coder/codersdk"
1112
)
1213

1314
func Test_calculateDeletingAt(t *testing.T) {
@@ -17,17 +18,21 @@ func Test_calculateDeletingAt(t *testing.T) {
1718
name string
1819
workspace database.Workspace
1920
template database.Template
21+
build codersdk.WorkspaceBuild
2022
expected *time.Time
2123
}{
2224
{
23-
name: "DeletingAt",
25+
name: "InactiveWorkspace",
2426
workspace: database.Workspace{
2527
Deleted: false,
2628
LastUsedAt: time.Now().Add(time.Duration(-10) * time.Hour * 24), // 10 days ago
2729
},
2830
template: database.Template{
2931
InactivityTTL: int64(9 * 24 * time.Hour), // 9 days
3032
},
33+
build: codersdk.WorkspaceBuild{
34+
Status: codersdk.WorkspaceStatusStopped,
35+
},
3136
expected: ptr.Ref(time.Now().Add(time.Duration(-1) * time.Hour * 24)), // yesterday
3237
},
3338
{
@@ -39,27 +44,22 @@ func Test_calculateDeletingAt(t *testing.T) {
3944
template: database.Template{
4045
InactivityTTL: 0,
4146
},
42-
expected: nil,
43-
},
44-
{
45-
name: "DeletedWorkspace",
46-
workspace: database.Workspace{
47-
Deleted: true,
48-
LastUsedAt: time.Now().Add(time.Duration(-10) * time.Hour * 24),
49-
},
50-
template: database.Template{
51-
InactivityTTL: int64(9 * 24 * time.Hour),
47+
build: codersdk.WorkspaceBuild{
48+
Status: codersdk.WorkspaceStatusStopped,
5249
},
5350
expected: nil,
5451
},
5552
{
5653
name: "ActiveWorkspace",
5754
workspace: database.Workspace{
58-
Deleted: true,
59-
LastUsedAt: time.Now().Add(time.Duration(-5) * time.Hour), // 5 hours ago
55+
Deleted: false,
56+
LastUsedAt: time.Now(),
6057
},
6158
template: database.Template{
62-
InactivityTTL: int64(1 * 24 * time.Hour), // 1 day
59+
InactivityTTL: int64(1 * 24 * time.Hour),
60+
},
61+
build: codersdk.WorkspaceBuild{
62+
Status: codersdk.WorkspaceStatusRunning,
6363
},
6464
expected: nil,
6565
},
@@ -70,7 +70,7 @@ func Test_calculateDeletingAt(t *testing.T) {
7070
t.Run(tc.name, func(t *testing.T) {
7171
t.Parallel()
7272

73-
found := calculateDeletingAt(tc.workspace, tc.template)
73+
found := calculateDeletingAt(tc.workspace, tc.template, tc.build)
7474
if tc.expected == nil {
7575
require.Nil(t, found, "impending deletion should be nil")
7676
} else {

codersdk/workspacebuilds.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const (
2929
WorkspaceStatusStopped WorkspaceStatus = "stopped"
3030
WorkspaceStatusFailed WorkspaceStatus = "failed"
3131
WorkspaceStatusCanceling WorkspaceStatus = "canceling"
32-
WorkspaceStatusCanceled WorkspaceStatus = "canceled"
32+
WorkspaceStatusCanceled WorkspaceStatus = "canceled" //
3333
WorkspaceStatusDeleting WorkspaceStatus = "deleting"
3434
WorkspaceStatusDeleted WorkspaceStatus = "deleted"
3535
)

site/src/components/WorkspaceStats/WorkspaceStats.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export const WorkspaceStats: FC<WorkspaceStatsProps> = ({
7272
<StatsItem
7373
className={styles.statsItem}
7474
label="Status"
75-
value={<WorkspaceStatusText build={workspace.latest_build} />}
75+
value={<WorkspaceStatusText workspace={workspace} />}
7676
/>
7777
<StatsItem
7878
className={styles.statsItem}

site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.stories.tsx

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
MockStartingWorkspace,
1010
MockStoppedWorkspace,
1111
MockStoppingWorkspace,
12+
MockWorkspaceImpendingDeletion,
1213
MockWorkspace,
1314
} from "testHelpers/entities"
1415
import {
@@ -27,50 +28,55 @@ const Template: Story<WorkspaceStatusBadgeProps> = (args) => (
2728

2829
export const Running = Template.bind({})
2930
Running.args = {
30-
build: MockWorkspace.latest_build,
31+
workspace: MockWorkspace,
3132
}
3233

3334
export const Starting = Template.bind({})
3435
Starting.args = {
35-
build: MockStartingWorkspace.latest_build,
36+
workspace: MockStartingWorkspace,
3637
}
3738

3839
export const Stopped = Template.bind({})
3940
Stopped.args = {
40-
build: MockStoppedWorkspace.latest_build,
41+
workspace: MockStoppedWorkspace,
4142
}
4243

4344
export const Stopping = Template.bind({})
4445
Stopping.args = {
45-
build: MockStoppingWorkspace.latest_build,
46+
workspace: MockStoppingWorkspace,
4647
}
4748

4849
export const Deleting = Template.bind({})
4950
Deleting.args = {
50-
build: MockDeletingWorkspace.latest_build,
51+
workspace: MockDeletingWorkspace,
5152
}
5253

5354
export const Deleted = Template.bind({})
5455
Deleted.args = {
55-
build: MockDeletedWorkspace.latest_build,
56+
workspace: MockDeletedWorkspace,
5657
}
5758

5859
export const Canceling = Template.bind({})
5960
Canceling.args = {
60-
build: MockCancelingWorkspace.latest_build,
61+
workspace: MockCancelingWorkspace,
6162
}
6263

6364
export const Canceled = Template.bind({})
6465
Canceled.args = {
65-
build: MockCanceledWorkspace.latest_build,
66+
workspace: MockCanceledWorkspace,
6667
}
6768

6869
export const Failed = Template.bind({})
6970
Failed.args = {
70-
build: MockFailedWorkspace.latest_build,
71+
workspace: MockFailedWorkspace,
7172
}
7273

7374
export const Pending = Template.bind({})
7475
Pending.args = {
75-
build: MockPendingWorkspace.latest_build,
76+
workspace: MockPendingWorkspace,
77+
}
78+
79+
export const ImpendingDeletion = Template.bind({})
80+
ImpendingDeletion.args = {
81+
workspace: MockWorkspaceImpendingDeletion,
7682
}

site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ import ErrorIcon from "@mui/icons-material/ErrorOutline"
33
import StopIcon from "@mui/icons-material/StopOutlined"
44
import PlayIcon from "@mui/icons-material/PlayArrowOutlined"
55
import QueuedIcon from "@mui/icons-material/HourglassEmpty"
6-
import { WorkspaceBuild } from "api/typesGenerated"
6+
import { Workspace, WorkspaceBuild } from "api/typesGenerated"
77
import { Pill } from "components/Pill/Pill"
88
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"
1213

1314
const LoadingIcon: FC = () => {
1415
return <CircularProgress size={10} style={{ color: "#FFF" }} />
@@ -87,22 +88,33 @@ export const getStatus = (buildStatus: WorkspaceBuild["status"]) => {
8788
}
8889

8990
export type WorkspaceStatusBadgeProps = {
90-
build: WorkspaceBuild
91+
workspace: Workspace
9192
className?: string
9293
}
9394

9495
export const WorkspaceStatusBadge: FC<
9596
PropsWithChildren<WorkspaceStatusBadgeProps>
96-
> = ({ build, className }) => {
97-
const { text, icon, type } = getStatus(build.status)
97+
> = ({ workspace, className }) => {
98+
if (displayImpendingDeletion(workspace)) {
99+
return (
100+
<Pill
101+
className={className}
102+
icon={<ErrorIcon />}
103+
text="Impending deletion"
104+
type="error"
105+
/>
106+
)
107+
}
108+
109+
const { text, icon, type } = getStatus(workspace.latest_build.status)
98110
return <Pill className={className} icon={icon} text={text} type={type} />
99111
}
100112

101113
export const WorkspaceStatusText: FC<
102114
PropsWithChildren<WorkspaceStatusBadgeProps>
103-
> = ({ build, className }) => {
115+
> = ({ workspace, className }) => {
104116
const styles = useStyles()
105-
const { text, type } = getStatus(build.status)
117+
const { text, type } = getStatus(workspace.latest_build.status)
106118
return (
107119
<span
108120
role="status"

site/src/components/WorkspacesTable/WorkspacesRow.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export const WorkspacesRow: FC<{
6262
</TableCell>
6363

6464
<TableCell>
65-
<WorkspaceStatusBadge build={workspace.latest_build} />
65+
<WorkspaceStatusBadge workspace={workspace} />
6666
</TableCell>
6767

6868
<TableCell>

site/src/testHelpers/entities.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -721,7 +721,6 @@ export const MockWorkspace: TypesGen.Workspace = {
721721
ttl_ms: 2 * 60 * 60 * 1000,
722722
latest_build: MockWorkspaceBuild,
723723
last_used_at: "2022-05-16T15:29:10.302441433Z",
724-
deleting_at: "0001-01-01T00:00:00Z",
725724
}
726725

727726
export const MockStoppedWorkspace: TypesGen.Workspace = {
@@ -807,6 +806,12 @@ export const MockPendingWorkspace: TypesGen.Workspace = {
807806
},
808807
}
809808

809+
export const MockWorkspaceImpendingDeletion: TypesGen.Workspace = {
810+
...MockWorkspace,
811+
id: "test-workspace-impending-deletion",
812+
deleting_at: new Date().toISOString(),
813+
}
814+
810815
// just over one page of workspaces
811816
export const MockWorkspacesResponse: TypesGen.WorkspacesResponse = {
812817
workspaces: range(1, 27).map((id: number) => ({

site/src/utils/workspace.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
getDisplayVersionStatus,
77
getDisplayWorkspaceBuildInitiatedBy,
88
getDisplayWorkspaceTemplateName,
9+
displayImpendingDeletion,
910
isWorkspaceOn,
1011
} from "./workspace"
1112

@@ -139,4 +140,21 @@ describe("util > workspace", () => {
139140
expect(displayed).toEqual(workspace.template_display_name)
140141
})
141142
})
143+
144+
describe("displayImpendingDeletion", () => {
145+
const today = new Date()
146+
it.each<[string, boolean]>([
147+
[new Date(new Date().setDate(today.getDate() + 15)).toISOString(), false], // today + 15 days out
148+
[new Date(new Date().setDate(today.getDate() + 14)).toISOString(), true], // today + 14
149+
[new Date(new Date().setDate(today.getDate() + 13)).toISOString(), true], // today + 13
150+
[new Date(new Date().setDate(today.getDate() + 1)).toISOString(), true], // today + 1
151+
[new Date().toISOString(), true], // today + 0
152+
])(`deleting_at=%p, isWorkspaceOn=%p`, (deleting_at, shouldDisplay) => {
153+
const workspace: TypesGen.Workspace = {
154+
...Mocks.MockWorkspace,
155+
deleting_at,
156+
}
157+
expect(displayImpendingDeletion(workspace)).toBe(shouldDisplay)
158+
})
159+
})
142160
})

site/src/utils/workspace.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,45 @@ export const getDisplayWorkspaceTemplateName = (
185185
? workspace.template_display_name
186186
: workspace.template_name
187187
}
188+
189+
// This const dictates how far out we alert the user that a workspace
190+
// has an impending deletion (due to template.InactivityTTL being set)
191+
const IMPENDING_DELETION_DISPLAY_THRESHOLD = 14 // 14 days
192+
193+
/**
194+
* Returns a boolean indicating if an impending deletion indicator should be
195+
* displayed in the UI. Impending deletions are configured by setting the
196+
* Template.InactivityTTL
197+
* @param {TypesGen.Workspace} workspace
198+
* @returns {boolean}
199+
*/
200+
export const displayImpendingDeletion = (workspace: TypesGen.Workspace) => {
201+
const today = new Date()
202+
if (!workspace.deleting_at) {
203+
return false
204+
}
205+
206+
console.log("today", today)
207+
console.log("workspace.deletingAt", new Date(workspace.deleting_at))
208+
console.log(
209+
"set",
210+
new Date(
211+
today.setDate(today.getDate() - IMPENDING_DELETION_DISPLAY_THRESHOLD),
212+
),
213+
)
214+
215+
console.log(
216+
"compare",
217+
new Date(workspace.deleting_at) <=
218+
new Date(
219+
today.setDate(today.getDate() + IMPENDING_DELETION_DISPLAY_THRESHOLD),
220+
),
221+
)
222+
223+
return (
224+
new Date(workspace.deleting_at) <=
225+
new Date(
226+
today.setDate(today.getDate() + IMPENDING_DELETION_DISPLAY_THRESHOLD),
227+
)
228+
)
229+
}

0 commit comments

Comments
 (0)