mirror of
https://github.com/coder/coder.git
synced 2025-07-15 22:20:27 +00:00
feat: add last used to Workspaces page (#3816)
This commit is contained in:
@ -105,7 +105,7 @@ func TestAgent(t *testing.T) {
|
|||||||
var ok bool
|
var ok bool
|
||||||
s, ok = (<-stats)
|
s, ok = (<-stats)
|
||||||
return ok && s.NumConns > 0 && s.RxBytes > 0 && s.TxBytes > 0
|
return ok && s.NumConns > 0 && s.RxBytes > 0 && s.TxBytes > 0
|
||||||
}, testutil.WaitShort, testutil.IntervalFast,
|
}, testutil.WaitLong, testutil.IntervalFast,
|
||||||
"never saw stats: %+v", s,
|
"never saw stats: %+v", s,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -2208,6 +2208,22 @@ func (q *fakeQuerier) UpdateWorkspaceTTL(_ context.Context, arg database.UpdateW
|
|||||||
return sql.ErrNoRows
|
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 {
|
func (q *fakeQuerier) UpdateWorkspaceBuildByID(_ context.Context, arg database.UpdateWorkspaceBuildByIDParams) error {
|
||||||
q.mutex.Lock()
|
q.mutex.Lock()
|
||||||
defer q.mutex.Unlock()
|
defer q.mutex.Unlock()
|
||||||
|
3
coderd/database/dump.sql
generated
3
coderd/database/dump.sql
generated
@ -377,7 +377,8 @@ CREATE TABLE workspaces (
|
|||||||
deleted boolean DEFAULT false NOT NULL,
|
deleted boolean DEFAULT false NOT NULL,
|
||||||
name character varying(64) NOT NULL,
|
name character varying(64) NOT NULL,
|
||||||
autostart_schedule text,
|
autostart_schedule text,
|
||||||
ttl bigint
|
ttl bigint,
|
||||||
|
last_used_at timestamp without time zone DEFAULT '0001-01-01 00:00:00'::timestamp without time zone NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
ALTER TABLE ONLY licenses ALTER COLUMN id SET DEFAULT nextval('public.licenses_id_seq'::regclass);
|
ALTER TABLE ONLY licenses ALTER COLUMN id SET DEFAULT nextval('public.licenses_id_seq'::regclass);
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE workspaces
|
||||||
|
DROP COLUMN last_used_at;
|
@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE workspaces
|
||||||
|
ADD COLUMN last_used_at timestamp NOT NULL DEFAULT '0001-01-01 00:00:00+00:00';
|
@ -525,6 +525,7 @@ type Workspace struct {
|
|||||||
Name string `db:"name" json:"name"`
|
Name string `db:"name" json:"name"`
|
||||||
AutostartSchedule sql.NullString `db:"autostart_schedule" json:"autostart_schedule"`
|
AutostartSchedule sql.NullString `db:"autostart_schedule" json:"autostart_schedule"`
|
||||||
Ttl sql.NullInt64 `db:"ttl" json:"ttl"`
|
Ttl sql.NullInt64 `db:"ttl" json:"ttl"`
|
||||||
|
LastUsedAt time.Time `db:"last_used_at" json:"last_used_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorkspaceAgent struct {
|
type WorkspaceAgent struct {
|
||||||
|
@ -150,6 +150,7 @@ type querier interface {
|
|||||||
UpdateWorkspaceAutostart(ctx context.Context, arg UpdateWorkspaceAutostartParams) error
|
UpdateWorkspaceAutostart(ctx context.Context, arg UpdateWorkspaceAutostartParams) error
|
||||||
UpdateWorkspaceBuildByID(ctx context.Context, arg UpdateWorkspaceBuildByIDParams) error
|
UpdateWorkspaceBuildByID(ctx context.Context, arg UpdateWorkspaceBuildByIDParams) error
|
||||||
UpdateWorkspaceDeletedByID(ctx context.Context, arg UpdateWorkspaceDeletedByIDParams) error
|
UpdateWorkspaceDeletedByID(ctx context.Context, arg UpdateWorkspaceDeletedByIDParams) error
|
||||||
|
UpdateWorkspaceLastUsedAt(ctx context.Context, arg UpdateWorkspaceLastUsedAtParams) error
|
||||||
UpdateWorkspaceTTL(ctx context.Context, arg UpdateWorkspaceTTLParams) error
|
UpdateWorkspaceTTL(ctx context.Context, arg UpdateWorkspaceTTLParams) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4562,7 +4562,7 @@ func (q *sqlQuerier) InsertWorkspaceResourceMetadata(ctx context.Context, arg In
|
|||||||
|
|
||||||
const getWorkspaceByID = `-- name: GetWorkspaceByID :one
|
const getWorkspaceByID = `-- name: GetWorkspaceByID :one
|
||||||
SELECT
|
SELECT
|
||||||
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl
|
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at
|
||||||
FROM
|
FROM
|
||||||
workspaces
|
workspaces
|
||||||
WHERE
|
WHERE
|
||||||
@ -4585,13 +4585,14 @@ func (q *sqlQuerier) GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Worksp
|
|||||||
&i.Name,
|
&i.Name,
|
||||||
&i.AutostartSchedule,
|
&i.AutostartSchedule,
|
||||||
&i.Ttl,
|
&i.Ttl,
|
||||||
|
&i.LastUsedAt,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const getWorkspaceByOwnerIDAndName = `-- name: GetWorkspaceByOwnerIDAndName :one
|
const getWorkspaceByOwnerIDAndName = `-- name: GetWorkspaceByOwnerIDAndName :one
|
||||||
SELECT
|
SELECT
|
||||||
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl
|
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at
|
||||||
FROM
|
FROM
|
||||||
workspaces
|
workspaces
|
||||||
WHERE
|
WHERE
|
||||||
@ -4621,6 +4622,7 @@ func (q *sqlQuerier) GetWorkspaceByOwnerIDAndName(ctx context.Context, arg GetWo
|
|||||||
&i.Name,
|
&i.Name,
|
||||||
&i.AutostartSchedule,
|
&i.AutostartSchedule,
|
||||||
&i.Ttl,
|
&i.Ttl,
|
||||||
|
&i.LastUsedAt,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@ -4669,7 +4671,7 @@ func (q *sqlQuerier) GetWorkspaceOwnerCountsByTemplateIDs(ctx context.Context, i
|
|||||||
|
|
||||||
const getWorkspaces = `-- name: GetWorkspaces :many
|
const getWorkspaces = `-- name: GetWorkspaces :many
|
||||||
SELECT
|
SELECT
|
||||||
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl
|
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at
|
||||||
FROM
|
FROM
|
||||||
workspaces
|
workspaces
|
||||||
WHERE
|
WHERE
|
||||||
@ -4745,6 +4747,7 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams)
|
|||||||
&i.Name,
|
&i.Name,
|
||||||
&i.AutostartSchedule,
|
&i.AutostartSchedule,
|
||||||
&i.Ttl,
|
&i.Ttl,
|
||||||
|
&i.LastUsedAt,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -4761,7 +4764,7 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams)
|
|||||||
|
|
||||||
const getWorkspacesAutostart = `-- name: GetWorkspacesAutostart :many
|
const getWorkspacesAutostart = `-- name: GetWorkspacesAutostart :many
|
||||||
SELECT
|
SELECT
|
||||||
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl
|
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at
|
||||||
FROM
|
FROM
|
||||||
workspaces
|
workspaces
|
||||||
WHERE
|
WHERE
|
||||||
@ -4794,6 +4797,7 @@ func (q *sqlQuerier) GetWorkspacesAutostart(ctx context.Context) ([]Workspace, e
|
|||||||
&i.Name,
|
&i.Name,
|
||||||
&i.AutostartSchedule,
|
&i.AutostartSchedule,
|
||||||
&i.Ttl,
|
&i.Ttl,
|
||||||
|
&i.LastUsedAt,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -4822,7 +4826,7 @@ INSERT INTO
|
|||||||
ttl
|
ttl
|
||||||
)
|
)
|
||||||
VALUES
|
VALUES
|
||||||
($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl
|
($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at
|
||||||
`
|
`
|
||||||
|
|
||||||
type InsertWorkspaceParams struct {
|
type InsertWorkspaceParams struct {
|
||||||
@ -4861,6 +4865,7 @@ func (q *sqlQuerier) InsertWorkspace(ctx context.Context, arg InsertWorkspacePar
|
|||||||
&i.Name,
|
&i.Name,
|
||||||
&i.AutostartSchedule,
|
&i.AutostartSchedule,
|
||||||
&i.Ttl,
|
&i.Ttl,
|
||||||
|
&i.LastUsedAt,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@ -4873,7 +4878,7 @@ SET
|
|||||||
WHERE
|
WHERE
|
||||||
id = $1
|
id = $1
|
||||||
AND deleted = false
|
AND deleted = false
|
||||||
RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl
|
RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateWorkspaceParams struct {
|
type UpdateWorkspaceParams struct {
|
||||||
@ -4895,6 +4900,7 @@ func (q *sqlQuerier) UpdateWorkspace(ctx context.Context, arg UpdateWorkspacePar
|
|||||||
&i.Name,
|
&i.Name,
|
||||||
&i.AutostartSchedule,
|
&i.AutostartSchedule,
|
||||||
&i.Ttl,
|
&i.Ttl,
|
||||||
|
&i.LastUsedAt,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@ -4937,6 +4943,25 @@ func (q *sqlQuerier) UpdateWorkspaceDeletedByID(ctx context.Context, arg UpdateW
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateWorkspaceLastUsedAt = `-- name: UpdateWorkspaceLastUsedAt :exec
|
||||||
|
UPDATE
|
||||||
|
workspaces
|
||||||
|
SET
|
||||||
|
last_used_at = $2
|
||||||
|
WHERE
|
||||||
|
id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateWorkspaceLastUsedAtParams struct {
|
||||||
|
ID uuid.UUID `db:"id" json:"id"`
|
||||||
|
LastUsedAt time.Time `db:"last_used_at" json:"last_used_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *sqlQuerier) UpdateWorkspaceLastUsedAt(ctx context.Context, arg UpdateWorkspaceLastUsedAtParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, updateWorkspaceLastUsedAt, arg.ID, arg.LastUsedAt)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
const updateWorkspaceTTL = `-- name: UpdateWorkspaceTTL :exec
|
const updateWorkspaceTTL = `-- name: UpdateWorkspaceTTL :exec
|
||||||
UPDATE
|
UPDATE
|
||||||
workspaces
|
workspaces
|
||||||
|
@ -137,3 +137,11 @@ SET
|
|||||||
ttl = $2
|
ttl = $2
|
||||||
WHERE
|
WHERE
|
||||||
id = $1;
|
id = $1;
|
||||||
|
|
||||||
|
-- name: UpdateWorkspaceLastUsedAt :exec
|
||||||
|
UPDATE
|
||||||
|
workspaces
|
||||||
|
SET
|
||||||
|
last_used_at = $2
|
||||||
|
WHERE
|
||||||
|
id = $1;
|
||||||
|
@ -608,6 +608,10 @@ func TestTemplateDAUs(t *testing.T) {
|
|||||||
Entries: []codersdk.DAUEntry{},
|
Entries: []codersdk.DAUEntry{},
|
||||||
}, daus, "no DAUs when stats are empty")
|
}, 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)
|
conn, err := client.DialWorkspaceAgent(ctx, resources[0].Agents[0].ID, opts)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer func() {
|
defer func() {
|
||||||
@ -641,4 +645,10 @@ func TestTemplateDAUs(t *testing.T) {
|
|||||||
testutil.WaitShort, testutil.IntervalFast,
|
testutil.WaitShort, testutil.IntervalFast,
|
||||||
"got %+v != %+v", daus, want,
|
"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,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -830,18 +830,20 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques
|
|||||||
// We will see duplicate reports when on idle connections
|
// We will see duplicate reports when on idle connections
|
||||||
// (e.g. web terminal left open) or when there are no connections at
|
// (e.g. web terminal left open) or when there are no connections at
|
||||||
// all.
|
// 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",
|
api.Logger.Debug(ctx, "read stats report",
|
||||||
slog.F("interval", api.AgentStatsRefreshInterval),
|
slog.F("interval", api.AgentStatsRefreshInterval),
|
||||||
slog.F("agent", workspaceAgent.ID),
|
slog.F("agent", workspaceAgent.ID),
|
||||||
slog.F("resource", resource.ID),
|
slog.F("resource", resource.ID),
|
||||||
slog.F("workspace", workspace.ID),
|
slog.F("workspace", workspace.ID),
|
||||||
slog.F("insert", insert),
|
slog.F("update_db", updateDB),
|
||||||
slog.F("payload", rep),
|
slog.F("payload", rep),
|
||||||
)
|
)
|
||||||
|
|
||||||
if insert {
|
if updateDB {
|
||||||
lastReport = rep
|
lastReport = rep
|
||||||
|
|
||||||
_, err = api.Database.InsertAgentStat(ctx, database.InsertAgentStatParams{
|
_, err = api.Database.InsertAgentStat(ctx, database.InsertAgentStatParams{
|
||||||
@ -860,6 +862,18 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques
|
|||||||
})
|
})
|
||||||
return
|
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 {
|
select {
|
||||||
|
@ -941,6 +941,7 @@ func convertWorkspace(
|
|||||||
Name: workspace.Name,
|
Name: workspace.Name,
|
||||||
AutostartSchedule: autostartSchedule,
|
AutostartSchedule: autostartSchedule,
|
||||||
TTLMillis: ttlMillis,
|
TTLMillis: ttlMillis,
|
||||||
|
LastUsedAt: workspace.LastUsedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,6 +30,7 @@ type Workspace struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
AutostartSchedule *string `json:"autostart_schedule,omitempty"`
|
AutostartSchedule *string `json:"autostart_schedule,omitempty"`
|
||||||
TTLMillis *int64 `json:"ttl_ms,omitempty"`
|
TTLMillis *int64 `json:"ttl_ms,omitempty"`
|
||||||
|
LastUsedAt time.Time `json:"last_used_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateWorkspaceBuildRequest provides options to update the latest workspace build.
|
// CreateWorkspaceBuildRequest provides options to update the latest workspace build.
|
||||||
|
@ -95,6 +95,7 @@ var AuditableResources = auditMap(map[any]map[string]Action{
|
|||||||
"name": ActionTrack,
|
"name": ActionTrack,
|
||||||
"autostart_schedule": ActionTrack,
|
"autostart_schedule": ActionTrack,
|
||||||
"ttl": ActionTrack,
|
"ttl": ActionTrack,
|
||||||
|
"last_used_at": ActionIgnore,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -514,6 +514,7 @@ export interface Workspace {
|
|||||||
readonly name: string
|
readonly name: string
|
||||||
readonly autostart_schedule?: string
|
readonly autostart_schedule?: string
|
||||||
readonly ttl_ms?: number
|
readonly ttl_ms?: number
|
||||||
|
readonly last_used_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// From codersdk/workspaceresources.go
|
// From codersdk/workspaceresources.go
|
||||||
|
39
site/src/components/WorkspacesTable/WorkspaceLastUsed.tsx
Normal file
39
site/src/components/WorkspacesTable/WorkspaceLastUsed.tsx
Normal file
@ -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>
|
||||||
|
}
|
@ -15,6 +15,7 @@ import {
|
|||||||
} from "../TableCellData/TableCellData"
|
} from "../TableCellData/TableCellData"
|
||||||
import { TableCellLink } from "../TableCellLink/TableCellLink"
|
import { TableCellLink } from "../TableCellLink/TableCellLink"
|
||||||
import { OutdatedHelpTooltip } from "../Tooltips"
|
import { OutdatedHelpTooltip } from "../Tooltips"
|
||||||
|
import { WorkspaceLastUsed } from "./WorkspaceLastUsed"
|
||||||
|
|
||||||
const Language = {
|
const Language = {
|
||||||
upToDateLabel: "Up to date",
|
upToDateLabel: "Up to date",
|
||||||
@ -64,6 +65,12 @@ export const WorkspacesRow: FC<
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</TableCellLink>
|
</TableCellLink>
|
||||||
|
<TableCellLink to={workspacePageLink}>
|
||||||
|
<TableCellData>
|
||||||
|
<WorkspaceLastUsed lastUsedAt={workspace.last_used_at} />
|
||||||
|
</TableCellData>
|
||||||
|
</TableCellLink>
|
||||||
|
|
||||||
<TableCellLink to={workspacePageLink}>
|
<TableCellLink to={workspacePageLink}>
|
||||||
{workspace.outdated ? (
|
{workspace.outdated ? (
|
||||||
<span className={styles.outdatedLabel}>
|
<span className={styles.outdatedLabel}>
|
||||||
|
@ -11,6 +11,7 @@ import { WorkspacesTableBody } from "./WorkspacesTableBody"
|
|||||||
const Language = {
|
const Language = {
|
||||||
name: "Name",
|
name: "Name",
|
||||||
template: "Template",
|
template: "Template",
|
||||||
|
lastUsed: "Last Used",
|
||||||
version: "Version",
|
version: "Version",
|
||||||
status: "Status",
|
status: "Status",
|
||||||
lastBuiltBy: "Last Built By",
|
lastBuiltBy: "Last Built By",
|
||||||
@ -34,6 +35,7 @@ export const WorkspacesTable: FC<React.PropsWithChildren<WorkspacesTableProps>>
|
|||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell width="25%">{Language.name}</TableCell>
|
<TableCell width="25%">{Language.name}</TableCell>
|
||||||
<TableCell width="35%">{Language.template}</TableCell>
|
<TableCell width="35%">{Language.template}</TableCell>
|
||||||
|
<TableCell width="20%">{Language.lastUsed}</TableCell>
|
||||||
<TableCell width="20%">{Language.version}</TableCell>
|
<TableCell width="20%">{Language.version}</TableCell>
|
||||||
<TableCell width="20%">{Language.status}</TableCell>
|
<TableCell width="20%">{Language.status}</TableCell>
|
||||||
<TableCell width="1%"></TableCell>
|
<TableCell width="1%"></TableCell>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { ComponentMeta, Story } from "@storybook/react"
|
import { ComponentMeta, Story } from "@storybook/react"
|
||||||
|
import dayjs from "dayjs"
|
||||||
import { spawn } from "xstate"
|
import { spawn } from "xstate"
|
||||||
import { ProvisionerJobStatus, WorkspaceTransition } from "../../api/typesGenerated"
|
import { ProvisionerJobStatus, WorkspaceTransition } from "../../api/typesGenerated"
|
||||||
import { MockWorkspace } from "../../testHelpers/entities"
|
import { MockWorkspace } from "../../testHelpers/entities"
|
||||||
@ -13,6 +14,7 @@ const createWorkspaceItemRef = (
|
|||||||
status: ProvisionerJobStatus,
|
status: ProvisionerJobStatus,
|
||||||
transition: WorkspaceTransition = "start",
|
transition: WorkspaceTransition = "start",
|
||||||
outdated = false,
|
outdated = false,
|
||||||
|
lastUsedAt = "0001-01-01",
|
||||||
): WorkspaceItemMachineRef => {
|
): WorkspaceItemMachineRef => {
|
||||||
return spawn(
|
return spawn(
|
||||||
workspaceItemMachine.withContext({
|
workspaceItemMachine.withContext({
|
||||||
@ -27,6 +29,7 @@ const createWorkspaceItemRef = (
|
|||||||
status: status,
|
status: status,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
last_used_at: lastUsedAt,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@ -48,6 +51,15 @@ const additionalWorkspaces: Record<string, WorkspaceItemMachineRef> = {
|
|||||||
succeededAndStop: createWorkspaceItemRef("succeeded", "stop"),
|
succeededAndStop: createWorkspaceItemRef("succeeded", "stop"),
|
||||||
runningAndDelete: createWorkspaceItemRef("running", "delete"),
|
runningAndDelete: createWorkspaceItemRef("running", "delete"),
|
||||||
outdated: createWorkspaceItemRef("running", "delete", true),
|
outdated: createWorkspaceItemRef("running", "delete", true),
|
||||||
|
active: createWorkspaceItemRef("running", undefined, true, dayjs().toString()),
|
||||||
|
today: createWorkspaceItemRef("running", undefined, true, dayjs().subtract(3, "hour").toString()),
|
||||||
|
old: createWorkspaceItemRef("running", undefined, true, dayjs().subtract(1, "week").toString()),
|
||||||
|
veryOld: createWorkspaceItemRef(
|
||||||
|
"running",
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
dayjs().subtract(1, "month").subtract(4, "day").toString(),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -236,6 +236,7 @@ export const MockWorkspace: TypesGen.Workspace = {
|
|||||||
autostart_schedule: MockWorkspaceAutostartEnabled.schedule,
|
autostart_schedule: MockWorkspaceAutostartEnabled.schedule,
|
||||||
ttl_ms: 2 * 60 * 60 * 1000, // 2 hours as milliseconds
|
ttl_ms: 2 * 60 * 60 * 1000, // 2 hours as milliseconds
|
||||||
latest_build: MockWorkspaceBuild,
|
latest_build: MockWorkspaceBuild,
|
||||||
|
last_used_at: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MockStoppedWorkspace: TypesGen.Workspace = {
|
export const MockStoppedWorkspace: TypesGen.Workspace = {
|
||||||
|
Reference in New Issue
Block a user