Skip to content

Commit 04b0379

Browse files
authored
feat: add last used to Workspaces page (#3816)
1 parent 80e9f24 commit 04b0379

20 files changed

+156
-11
lines changed

agent/agent_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ func TestAgent(t *testing.T) {
105105
var ok bool
106106
s, ok = (<-stats)
107107
return ok && s.NumConns > 0 && s.RxBytes > 0 && s.TxBytes > 0
108-
}, testutil.WaitShort, testutil.IntervalFast,
108+
}, testutil.WaitLong, testutil.IntervalFast,
109109
"never saw stats: %+v", s,
110110
)
111111
})

coderd/database/databasefake/databasefake.go

+16
Original file line numberDiff line numberDiff line change
@@ -2208,6 +2208,22 @@ func (q *fakeQuerier) UpdateWorkspaceTTL(_ context.Context, arg database.UpdateW
22082208
return sql.ErrNoRows
22092209
}
22102210

2211+
func (q *fakeQuerier) UpdateWorkspaceLastUsedAt(_ context.Context, arg database.UpdateWorkspaceLastUsedAtParams) error {
2212+
q.mutex.Lock()
2213+
defer q.mutex.Unlock()
2214+
2215+
for index, workspace := range q.workspaces {
2216+
if workspace.ID != arg.ID {
2217+
continue
2218+
}
2219+
workspace.LastUsedAt = arg.LastUsedAt
2220+
q.workspaces[index] = workspace
2221+
return nil
2222+
}
2223+
2224+
return sql.ErrNoRows
2225+
}
2226+
22112227
func (q *fakeQuerier) UpdateWorkspaceBuildByID(_ context.Context, arg database.UpdateWorkspaceBuildByIDParams) error {
22122228
q.mutex.Lock()
22132229
defer q.mutex.Unlock()

coderd/database/dump.sql

+2-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ALTER TABLE workspaces
2+
DROP COLUMN last_used_at;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ALTER TABLE workspaces
2+
ADD COLUMN last_used_at timestamp NOT NULL DEFAULT '0001-01-01 00:00:00+00:00';

coderd/database/models.go

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/querier.go

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries.sql.go

+31-6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/workspaces.sql

+8
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,11 @@ SET
137137
ttl = $2
138138
WHERE
139139
id = $1;
140+
141+
-- name: UpdateWorkspaceLastUsedAt :exec
142+
UPDATE
143+
workspaces
144+
SET
145+
last_used_at = $2
146+
WHERE
147+
id = $1;

coderd/templates_test.go

+10
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,10 @@ func TestTemplateDAUs(t *testing.T) {
608608
Entries: []codersdk.DAUEntry{},
609609
}, daus, "no DAUs when stats are empty")
610610

611+
workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{})
612+
require.NoError(t, err)
613+
assert.Zero(t, workspaces[0].LastUsedAt)
614+
611615
conn, err := client.DialWorkspaceAgent(ctx, resources[0].Agents[0].ID, opts)
612616
require.NoError(t, err)
613617
defer func() {
@@ -641,4 +645,10 @@ func TestTemplateDAUs(t *testing.T) {
641645
testutil.WaitShort, testutil.IntervalFast,
642646
"got %+v != %+v", daus, want,
643647
)
648+
649+
workspaces, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{})
650+
require.NoError(t, err)
651+
assert.WithinDuration(t,
652+
time.Now(), workspaces[0].LastUsedAt, time.Minute,
653+
)
644654
}

coderd/workspaceagents.go

+17-3
Original file line numberDiff line numberDiff line change
@@ -830,18 +830,20 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques
830830
// We will see duplicate reports when on idle connections
831831
// (e.g. web terminal left open) or when there are no connections at
832832
// all.
833-
var insert = !reflect.DeepEqual(lastReport, rep)
833+
// We also don't want to update the workspace last used at on duplicate
834+
// reports.
835+
var updateDB = !reflect.DeepEqual(lastReport, rep)
834836

835837
api.Logger.Debug(ctx, "read stats report",
836838
slog.F("interval", api.AgentStatsRefreshInterval),
837839
slog.F("agent", workspaceAgent.ID),
838840
slog.F("resource", resource.ID),
839841
slog.F("workspace", workspace.ID),
840-
slog.F("insert", insert),
842+
slog.F("update_db", updateDB),
841843
slog.F("payload", rep),
842844
)
843845

844-
if insert {
846+
if updateDB {
845847
lastReport = rep
846848

847849
_, err = api.Database.InsertAgentStat(ctx, database.InsertAgentStatParams{
@@ -860,6 +862,18 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques
860862
})
861863
return
862864
}
865+
866+
err = api.Database.UpdateWorkspaceLastUsedAt(ctx, database.UpdateWorkspaceLastUsedAtParams{
867+
ID: build.WorkspaceID,
868+
LastUsedAt: time.Now(),
869+
})
870+
if err != nil {
871+
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
872+
Message: "Failed to update workspace last used at.",
873+
Detail: err.Error(),
874+
})
875+
return
876+
}
863877
}
864878

865879
select {

coderd/workspaces.go

+1
Original file line numberDiff line numberDiff line change
@@ -941,6 +941,7 @@ func convertWorkspace(
941941
Name: workspace.Name,
942942
AutostartSchedule: autostartSchedule,
943943
TTLMillis: ttlMillis,
944+
LastUsedAt: workspace.LastUsedAt,
944945
}
945946
}
946947

codersdk/workspaces.go

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ type Workspace struct {
3030
Name string `json:"name"`
3131
AutostartSchedule *string `json:"autostart_schedule,omitempty"`
3232
TTLMillis *int64 `json:"ttl_ms,omitempty"`
33+
LastUsedAt time.Time `json:"last_used_at"`
3334
}
3435

3536
// CreateWorkspaceBuildRequest provides options to update the latest workspace build.

enterprise/audit/table.go

+1
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ var AuditableResources = auditMap(map[any]map[string]Action{
9595
"name": ActionTrack,
9696
"autostart_schedule": ActionTrack,
9797
"ttl": ActionTrack,
98+
"last_used_at": ActionIgnore,
9899
},
99100
})
100101

site/src/api/typesGenerated.ts

+1
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,7 @@ export interface Workspace {
514514
readonly name: string
515515
readonly autostart_schedule?: string
516516
readonly ttl_ms?: number
517+
readonly last_used_at: string
517518
}
518519

519520
// From codersdk/workspaceresources.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { Theme, useTheme } from "@material-ui/core/styles"
2+
import { FC } from "react"
3+
4+
import dayjs from "dayjs"
5+
import relativeTime from "dayjs/plugin/relativeTime"
6+
7+
dayjs.extend(relativeTime)
8+
9+
interface WorkspaceLastUsedProps {
10+
lastUsedAt: string
11+
}
12+
13+
export const WorkspaceLastUsed: FC<WorkspaceLastUsedProps> = ({ lastUsedAt }) => {
14+
const theme: Theme = useTheme()
15+
16+
const t = dayjs(lastUsedAt)
17+
const now = dayjs()
18+
19+
let color = theme.palette.text.secondary
20+
let message = t.fromNow()
21+
22+
if (t.isAfter(now.subtract(1, "hour"))) {
23+
color = theme.palette.success.main
24+
// Since the agent reports on a 10m interval,
25+
// the last_used_at can be inaccurate when recent.
26+
message = "In the last hour"
27+
} else if (t.isAfter(now.subtract(1, "day"))) {
28+
color = theme.palette.primary.main
29+
} else if (t.isAfter(now.subtract(1, "month"))) {
30+
color = theme.palette.text.secondary
31+
} else if (t.isAfter(now.subtract(100, "year"))) {
32+
color = theme.palette.warning.light
33+
} else {
34+
color = theme.palette.error.light
35+
message = "Never"
36+
}
37+
38+
return <span style={{ color: color }}>{message}</span>
39+
}

site/src/components/WorkspacesTable/WorkspacesRow.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
} from "../TableCellData/TableCellData"
1616
import { TableCellLink } from "../TableCellLink/TableCellLink"
1717
import { OutdatedHelpTooltip } from "../Tooltips"
18+
import { WorkspaceLastUsed } from "./WorkspaceLastUsed"
1819

1920
const Language = {
2021
upToDateLabel: "Up to date",
@@ -64,6 +65,12 @@ export const WorkspacesRow: FC<
6465
}
6566
/>
6667
</TableCellLink>
68+
<TableCellLink to={workspacePageLink}>
69+
<TableCellData>
70+
<WorkspaceLastUsed lastUsedAt={workspace.last_used_at} />
71+
</TableCellData>
72+
</TableCellLink>
73+
6774
<TableCellLink to={workspacePageLink}>
6875
{workspace.outdated ? (
6976
<span className={styles.outdatedLabel}>

site/src/components/WorkspacesTable/WorkspacesTable.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { WorkspacesTableBody } from "./WorkspacesTableBody"
1111
const Language = {
1212
name: "Name",
1313
template: "Template",
14+
lastUsed: "Last Used",
1415
version: "Version",
1516
status: "Status",
1617
lastBuiltBy: "Last Built By",
@@ -34,6 +35,7 @@ export const WorkspacesTable: FC<React.PropsWithChildren<WorkspacesTableProps>>
3435
<TableRow>
3536
<TableCell width="25%">{Language.name}</TableCell>
3637
<TableCell width="35%">{Language.template}</TableCell>
38+
<TableCell width="20%">{Language.lastUsed}</TableCell>
3739
<TableCell width="20%">{Language.version}</TableCell>
3840
<TableCell width="20%">{Language.status}</TableCell>
3941
<TableCell width="1%"></TableCell>

0 commit comments

Comments
 (0)