Skip to content

feat: add workspace last used #3816

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

Merged
merged 7 commits into from
Sep 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion agent/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func TestAgent(t *testing.T) {
var ok bool
s, ok = (<-stats)
return ok && s.NumConns > 0 && s.RxBytes > 0 && s.TxBytes > 0
}, testutil.WaitShort, testutil.IntervalFast,
}, testutil.WaitLong, testutil.IntervalFast,
"never saw stats: %+v", s,
)
})
Expand Down
16 changes: 16 additions & 0 deletions coderd/database/databasefake/databasefake.go
Original file line number Diff line number Diff line change
Expand Up @@ -2208,6 +2208,22 @@ func (q *fakeQuerier) UpdateWorkspaceTTL(_ context.Context, arg database.UpdateW
return sql.ErrNoRows
}

func (q *fakeQuerier) UpdateWorkspaceLastUsedAt(_ context.Context, arg database.UpdateWorkspaceLastUsedAtParams) error {
q.mutex.Lock()
defer q.mutex.Unlock()

for index, workspace := range q.workspaces {
if workspace.ID != arg.ID {
continue
}
workspace.LastUsedAt = arg.LastUsedAt
q.workspaces[index] = workspace
return nil
}

return sql.ErrNoRows
}

func (q *fakeQuerier) UpdateWorkspaceBuildByID(_ context.Context, arg database.UpdateWorkspaceBuildByIDParams) error {
q.mutex.Lock()
defer q.mutex.Unlock()
Expand Down
3 changes: 2 additions & 1 deletion coderd/database/dump.sql

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE workspaces
DROP COLUMN last_used_at;
2 changes: 2 additions & 0 deletions coderd/database/migrations/000043_workspace_last_used.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE workspaces
ADD COLUMN last_used_at timestamp NOT NULL DEFAULT '0001-01-01 00:00:00+00:00';
1 change: 1 addition & 0 deletions coderd/database/models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions coderd/database/querier.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 31 additions & 6 deletions coderd/database/queries.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions coderd/database/queries/workspaces.sql
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,11 @@ SET
ttl = $2
WHERE
id = $1;

-- name: UpdateWorkspaceLastUsedAt :exec
UPDATE
workspaces
SET
last_used_at = $2
WHERE
id = $1;
10 changes: 10 additions & 0 deletions coderd/templates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,10 @@ func TestTemplateDAUs(t *testing.T) {
Entries: []codersdk.DAUEntry{},
}, daus, "no DAUs when stats are empty")

workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{})
require.NoError(t, err)
assert.Zero(t, workspaces[0].LastUsedAt)

conn, err := client.DialWorkspaceAgent(ctx, resources[0].Agents[0].ID, opts)
require.NoError(t, err)
defer func() {
Expand Down Expand Up @@ -641,4 +645,10 @@ func TestTemplateDAUs(t *testing.T) {
testutil.WaitShort, testutil.IntervalFast,
"got %+v != %+v", daus, want,
)

workspaces, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{})
require.NoError(t, err)
assert.WithinDuration(t,
time.Now(), workspaces[0].LastUsedAt, time.Minute,
)
}
20 changes: 17 additions & 3 deletions coderd/workspaceagents.go
Original file line number Diff line number Diff line change
Expand Up @@ -830,18 +830,20 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques
// We will see duplicate reports when on idle connections
// (e.g. web terminal left open) or when there are no connections at
// all.
var insert = !reflect.DeepEqual(lastReport, rep)
// We also don't want to update the workspace last used at on duplicate
// reports.
var updateDB = !reflect.DeepEqual(lastReport, rep)

api.Logger.Debug(ctx, "read stats report",
slog.F("interval", api.AgentStatsRefreshInterval),
slog.F("agent", workspaceAgent.ID),
slog.F("resource", resource.ID),
slog.F("workspace", workspace.ID),
slog.F("insert", insert),
slog.F("update_db", updateDB),
slog.F("payload", rep),
)

if insert {
if updateDB {
lastReport = rep

_, err = api.Database.InsertAgentStat(ctx, database.InsertAgentStatParams{
Expand All @@ -860,6 +862,18 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques
})
return
}

err = api.Database.UpdateWorkspaceLastUsedAt(ctx, database.UpdateWorkspaceLastUsedAtParams{
ID: build.WorkspaceID,
LastUsedAt: time.Now(),
})
if err != nil {
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
Message: "Failed to update workspace last used at.",
Detail: err.Error(),
})
return
}
}

select {
Expand Down
1 change: 1 addition & 0 deletions coderd/workspaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -941,6 +941,7 @@ func convertWorkspace(
Name: workspace.Name,
AutostartSchedule: autostartSchedule,
TTLMillis: ttlMillis,
LastUsedAt: workspace.LastUsedAt,
}
}

Expand Down
1 change: 1 addition & 0 deletions codersdk/workspaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type Workspace struct {
Name string `json:"name"`
AutostartSchedule *string `json:"autostart_schedule,omitempty"`
TTLMillis *int64 `json:"ttl_ms,omitempty"`
LastUsedAt time.Time `json:"last_used_at"`
}

// CreateWorkspaceBuildRequest provides options to update the latest workspace build.
Expand Down
1 change: 1 addition & 0 deletions enterprise/audit/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ var AuditableResources = auditMap(map[any]map[string]Action{
"name": ActionTrack,
"autostart_schedule": ActionTrack,
"ttl": ActionTrack,
"last_used_at": ActionIgnore,
},
})

Expand Down
1 change: 1 addition & 0 deletions site/src/api/typesGenerated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,7 @@ export interface Workspace {
readonly name: string
readonly autostart_schedule?: string
readonly ttl_ms?: number
readonly last_used_at: string
}

// From codersdk/workspaceresources.go
Expand Down
39 changes: 39 additions & 0 deletions site/src/components/WorkspacesTable/WorkspaceLastUsed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Theme, useTheme } from "@material-ui/core/styles"
import { FC } from "react"

import dayjs from "dayjs"
import relativeTime from "dayjs/plugin/relativeTime"

dayjs.extend(relativeTime)

interface WorkspaceLastUsedProps {
lastUsedAt: string
}

export const WorkspaceLastUsed: FC<WorkspaceLastUsedProps> = ({ lastUsedAt }) => {
const theme: Theme = useTheme()

const t = dayjs(lastUsedAt)
const now = dayjs()

let color = theme.palette.text.secondary
let message = t.fromNow()

if (t.isAfter(now.subtract(1, "hour"))) {
color = theme.palette.success.main
// Since the agent reports on a 10m interval,
// the last_used_at can be inaccurate when recent.
message = "In the last hour"
} else if (t.isAfter(now.subtract(1, "day"))) {
color = theme.palette.primary.main
} else if (t.isAfter(now.subtract(1, "month"))) {
color = theme.palette.text.secondary
} else if (t.isAfter(now.subtract(100, "year"))) {
color = theme.palette.warning.light
} else {
color = theme.palette.error.light
message = "Never"
}

return <span style={{ color: color }}>{message}</span>
}
7 changes: 7 additions & 0 deletions site/src/components/WorkspacesTable/WorkspacesRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from "../TableCellData/TableCellData"
import { TableCellLink } from "../TableCellLink/TableCellLink"
import { OutdatedHelpTooltip } from "../Tooltips"
import { WorkspaceLastUsed } from "./WorkspaceLastUsed"

const Language = {
upToDateLabel: "Up to date",
Expand Down Expand Up @@ -64,6 +65,12 @@ export const WorkspacesRow: FC<
}
/>
</TableCellLink>
<TableCellLink to={workspacePageLink}>
<TableCellData>
<WorkspaceLastUsed lastUsedAt={workspace.last_used_at} />
</TableCellData>
</TableCellLink>

<TableCellLink to={workspacePageLink}>
{workspace.outdated ? (
<span className={styles.outdatedLabel}>
Expand Down
2 changes: 2 additions & 0 deletions site/src/components/WorkspacesTable/WorkspacesTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { WorkspacesTableBody } from "./WorkspacesTableBody"
const Language = {
name: "Name",
template: "Template",
lastUsed: "Last Used",
version: "Version",
status: "Status",
lastBuiltBy: "Last Built By",
Expand All @@ -34,6 +35,7 @@ export const WorkspacesTable: FC<React.PropsWithChildren<WorkspacesTableProps>>
<TableRow>
<TableCell width="25%">{Language.name}</TableCell>
<TableCell width="35%">{Language.template}</TableCell>
<TableCell width="20%">{Language.lastUsed}</TableCell>
<TableCell width="20%">{Language.version}</TableCell>
<TableCell width="20%">{Language.status}</TableCell>
<TableCell width="1%"></TableCell>
Expand Down
Loading