mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
added status badge on workspaces page
This commit is contained in:
+7
-14
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/tabbed/pqtype"
|
||||
"golang.org/x/exp/slices"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"cdr.dev/slog"
|
||||
@@ -1171,7 +1172,7 @@ func convertWorkspace(
|
||||
|
||||
var (
|
||||
ttlMillis = convertWorkspaceTTLMillis(workspace.Ttl)
|
||||
deletingAt = calculateDeletingAt(workspace, template)
|
||||
deletingAt = calculateDeletingAt(workspace, template, workspaceBuild)
|
||||
)
|
||||
return codersdk.Workspace{
|
||||
ID: workspace.ID,
|
||||
@@ -1206,19 +1207,11 @@ func convertWorkspaceTTLMillis(i sql.NullInt64) *int64 {
|
||||
|
||||
// Calculate the time of the upcoming workspace deletion, if applicable; otherwise, return nil.
|
||||
// Workspaces may have impending deletions if InactivityTTL feature is turned on and the workspace is inactive.
|
||||
func calculateDeletingAt(workspace database.Workspace, template database.Template) *time.Time {
|
||||
// Workspace is recently inactive but hasn't been inactive for longer than
|
||||
// the specified template.InactivityTTL threshold
|
||||
|
||||
fmt.Println("last used at before now (aka inactive)", workspace.LastUsedAt.Before(time.Now()))
|
||||
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)))
|
||||
|
||||
workspaceRecentlyInactive := workspace.LastUsedAt.Before(time.Now()) &&
|
||||
workspace.LastUsedAt.After(time.Now().Add(-time.Duration(template.InactivityTTL)*time.Nanosecond))
|
||||
|
||||
// If InactivityTTL is turned off (set to 0), if the workspace has already been deleted,
|
||||
// or if the workspace is only recently inactive, there is no impending deletion
|
||||
if template.InactivityTTL == 0 || workspace.Deleted || workspaceRecentlyInactive {
|
||||
func calculateDeletingAt(workspace database.Workspace, template database.Template, build codersdk.WorkspaceBuild) *time.Time {
|
||||
inactiveStatuses := []codersdk.WorkspaceStatus{codersdk.WorkspaceStatusStopped, codersdk.WorkspaceStatusCanceled, codersdk.WorkspaceStatusFailed, codersdk.WorkspaceStatusDeleted}
|
||||
isInactive := slices.Contains(inactiveStatuses, build.Status)
|
||||
// If InactivityTTL is turned off (set to 0) or if the workspace is active, there is no impending deletion
|
||||
if template.InactivityTTL == 0 || !isInactive {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/util/ptr"
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
||||
func Test_calculateDeletingAt(t *testing.T) {
|
||||
@@ -17,10 +18,11 @@ func Test_calculateDeletingAt(t *testing.T) {
|
||||
name string
|
||||
workspace database.Workspace
|
||||
template database.Template
|
||||
build codersdk.WorkspaceBuild
|
||||
expected *time.Time
|
||||
}{
|
||||
{
|
||||
name: "DeletingAt",
|
||||
name: "InactiveWorkspace",
|
||||
workspace: database.Workspace{
|
||||
Deleted: false,
|
||||
LastUsedAt: time.Now().Add(time.Duration(-10) * time.Hour * 24), // 10 days ago
|
||||
@@ -28,6 +30,9 @@ func Test_calculateDeletingAt(t *testing.T) {
|
||||
template: database.Template{
|
||||
InactivityTTL: int64(9 * 24 * time.Hour), // 9 days
|
||||
},
|
||||
build: codersdk.WorkspaceBuild{
|
||||
Status: codersdk.WorkspaceStatusStopped,
|
||||
},
|
||||
expected: ptr.Ref(time.Now().Add(time.Duration(-1) * time.Hour * 24)), // yesterday
|
||||
},
|
||||
{
|
||||
@@ -39,27 +44,22 @@ func Test_calculateDeletingAt(t *testing.T) {
|
||||
template: database.Template{
|
||||
InactivityTTL: 0,
|
||||
},
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "DeletedWorkspace",
|
||||
workspace: database.Workspace{
|
||||
Deleted: true,
|
||||
LastUsedAt: time.Now().Add(time.Duration(-10) * time.Hour * 24),
|
||||
},
|
||||
template: database.Template{
|
||||
InactivityTTL: int64(9 * 24 * time.Hour),
|
||||
build: codersdk.WorkspaceBuild{
|
||||
Status: codersdk.WorkspaceStatusStopped,
|
||||
},
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "ActiveWorkspace",
|
||||
workspace: database.Workspace{
|
||||
Deleted: true,
|
||||
LastUsedAt: time.Now().Add(time.Duration(-5) * time.Hour), // 5 hours ago
|
||||
Deleted: false,
|
||||
LastUsedAt: time.Now(),
|
||||
},
|
||||
template: database.Template{
|
||||
InactivityTTL: int64(1 * 24 * time.Hour), // 1 day
|
||||
InactivityTTL: int64(1 * 24 * time.Hour),
|
||||
},
|
||||
build: codersdk.WorkspaceBuild{
|
||||
Status: codersdk.WorkspaceStatusRunning,
|
||||
},
|
||||
expected: nil,
|
||||
},
|
||||
@@ -70,7 +70,7 @@ func Test_calculateDeletingAt(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
found := calculateDeletingAt(tc.workspace, tc.template)
|
||||
found := calculateDeletingAt(tc.workspace, tc.template, tc.build)
|
||||
if tc.expected == nil {
|
||||
require.Nil(t, found, "impending deletion should be nil")
|
||||
} else {
|
||||
|
||||
@@ -29,7 +29,7 @@ const (
|
||||
WorkspaceStatusStopped WorkspaceStatus = "stopped"
|
||||
WorkspaceStatusFailed WorkspaceStatus = "failed"
|
||||
WorkspaceStatusCanceling WorkspaceStatus = "canceling"
|
||||
WorkspaceStatusCanceled WorkspaceStatus = "canceled"
|
||||
WorkspaceStatusCanceled WorkspaceStatus = "canceled" //
|
||||
WorkspaceStatusDeleting WorkspaceStatus = "deleting"
|
||||
WorkspaceStatusDeleted WorkspaceStatus = "deleted"
|
||||
)
|
||||
|
||||
@@ -72,7 +72,7 @@ export const WorkspaceStats: FC<WorkspaceStatsProps> = ({
|
||||
<StatsItem
|
||||
className={styles.statsItem}
|
||||
label="Status"
|
||||
value={<WorkspaceStatusText build={workspace.latest_build} />}
|
||||
value={<WorkspaceStatusText workspace={workspace} />}
|
||||
/>
|
||||
<StatsItem
|
||||
className={styles.statsItem}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
MockStartingWorkspace,
|
||||
MockStoppedWorkspace,
|
||||
MockStoppingWorkspace,
|
||||
MockWorkspaceImpendingDeletion,
|
||||
MockWorkspace,
|
||||
} from "testHelpers/entities"
|
||||
import {
|
||||
@@ -27,50 +28,55 @@ const Template: Story<WorkspaceStatusBadgeProps> = (args) => (
|
||||
|
||||
export const Running = Template.bind({})
|
||||
Running.args = {
|
||||
build: MockWorkspace.latest_build,
|
||||
workspace: MockWorkspace,
|
||||
}
|
||||
|
||||
export const Starting = Template.bind({})
|
||||
Starting.args = {
|
||||
build: MockStartingWorkspace.latest_build,
|
||||
workspace: MockStartingWorkspace,
|
||||
}
|
||||
|
||||
export const Stopped = Template.bind({})
|
||||
Stopped.args = {
|
||||
build: MockStoppedWorkspace.latest_build,
|
||||
workspace: MockStoppedWorkspace,
|
||||
}
|
||||
|
||||
export const Stopping = Template.bind({})
|
||||
Stopping.args = {
|
||||
build: MockStoppingWorkspace.latest_build,
|
||||
workspace: MockStoppingWorkspace,
|
||||
}
|
||||
|
||||
export const Deleting = Template.bind({})
|
||||
Deleting.args = {
|
||||
build: MockDeletingWorkspace.latest_build,
|
||||
workspace: MockDeletingWorkspace,
|
||||
}
|
||||
|
||||
export const Deleted = Template.bind({})
|
||||
Deleted.args = {
|
||||
build: MockDeletedWorkspace.latest_build,
|
||||
workspace: MockDeletedWorkspace,
|
||||
}
|
||||
|
||||
export const Canceling = Template.bind({})
|
||||
Canceling.args = {
|
||||
build: MockCancelingWorkspace.latest_build,
|
||||
workspace: MockCancelingWorkspace,
|
||||
}
|
||||
|
||||
export const Canceled = Template.bind({})
|
||||
Canceled.args = {
|
||||
build: MockCanceledWorkspace.latest_build,
|
||||
workspace: MockCanceledWorkspace,
|
||||
}
|
||||
|
||||
export const Failed = Template.bind({})
|
||||
Failed.args = {
|
||||
build: MockFailedWorkspace.latest_build,
|
||||
workspace: MockFailedWorkspace,
|
||||
}
|
||||
|
||||
export const Pending = Template.bind({})
|
||||
Pending.args = {
|
||||
build: MockPendingWorkspace.latest_build,
|
||||
workspace: MockPendingWorkspace,
|
||||
}
|
||||
|
||||
export const ImpendingDeletion = Template.bind({})
|
||||
ImpendingDeletion.args = {
|
||||
workspace: MockWorkspaceImpendingDeletion,
|
||||
}
|
||||
|
||||
@@ -3,12 +3,13 @@ import ErrorIcon from "@mui/icons-material/ErrorOutline"
|
||||
import StopIcon from "@mui/icons-material/StopOutlined"
|
||||
import PlayIcon from "@mui/icons-material/PlayArrowOutlined"
|
||||
import QueuedIcon from "@mui/icons-material/HourglassEmpty"
|
||||
import { WorkspaceBuild } from "api/typesGenerated"
|
||||
import { Workspace, WorkspaceBuild } from "api/typesGenerated"
|
||||
import { Pill } from "components/Pill/Pill"
|
||||
import i18next from "i18next"
|
||||
import { FC, PropsWithChildren } from "react"
|
||||
import { makeStyles } from "@mui/styles"
|
||||
import { combineClasses } from "utils/combineClasses"
|
||||
import { displayImpendingDeletion } from "utils/workspace"
|
||||
|
||||
const LoadingIcon: FC = () => {
|
||||
return <CircularProgress size={10} style={{ color: "#FFF" }} />
|
||||
@@ -87,22 +88,33 @@ export const getStatus = (buildStatus: WorkspaceBuild["status"]) => {
|
||||
}
|
||||
|
||||
export type WorkspaceStatusBadgeProps = {
|
||||
build: WorkspaceBuild
|
||||
workspace: Workspace
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const WorkspaceStatusBadge: FC<
|
||||
PropsWithChildren<WorkspaceStatusBadgeProps>
|
||||
> = ({ build, className }) => {
|
||||
const { text, icon, type } = getStatus(build.status)
|
||||
> = ({ workspace, className }) => {
|
||||
if (displayImpendingDeletion(workspace)) {
|
||||
return (
|
||||
<Pill
|
||||
className={className}
|
||||
icon={<ErrorIcon />}
|
||||
text="Impending deletion"
|
||||
type="error"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const { text, icon, type } = getStatus(workspace.latest_build.status)
|
||||
return <Pill className={className} icon={icon} text={text} type={type} />
|
||||
}
|
||||
|
||||
export const WorkspaceStatusText: FC<
|
||||
PropsWithChildren<WorkspaceStatusBadgeProps>
|
||||
> = ({ build, className }) => {
|
||||
> = ({ workspace, className }) => {
|
||||
const styles = useStyles()
|
||||
const { text, type } = getStatus(build.status)
|
||||
const { text, type } = getStatus(workspace.latest_build.status)
|
||||
return (
|
||||
<span
|
||||
role="status"
|
||||
|
||||
@@ -62,7 +62,7 @@ export const WorkspacesRow: FC<{
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<WorkspaceStatusBadge build={workspace.latest_build} />
|
||||
<WorkspaceStatusBadge workspace={workspace} />
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
|
||||
@@ -721,7 +721,6 @@ export const MockWorkspace: TypesGen.Workspace = {
|
||||
ttl_ms: 2 * 60 * 60 * 1000,
|
||||
latest_build: MockWorkspaceBuild,
|
||||
last_used_at: "2022-05-16T15:29:10.302441433Z",
|
||||
deleting_at: "0001-01-01T00:00:00Z",
|
||||
}
|
||||
|
||||
export const MockStoppedWorkspace: TypesGen.Workspace = {
|
||||
@@ -807,6 +806,12 @@ export const MockPendingWorkspace: TypesGen.Workspace = {
|
||||
},
|
||||
}
|
||||
|
||||
export const MockWorkspaceImpendingDeletion: TypesGen.Workspace = {
|
||||
...MockWorkspace,
|
||||
id: "test-workspace-impending-deletion",
|
||||
deleting_at: new Date().toISOString(),
|
||||
}
|
||||
|
||||
// just over one page of workspaces
|
||||
export const MockWorkspacesResponse: TypesGen.WorkspacesResponse = {
|
||||
workspaces: range(1, 27).map((id: number) => ({
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
getDisplayVersionStatus,
|
||||
getDisplayWorkspaceBuildInitiatedBy,
|
||||
getDisplayWorkspaceTemplateName,
|
||||
displayImpendingDeletion,
|
||||
isWorkspaceOn,
|
||||
} from "./workspace"
|
||||
|
||||
@@ -139,4 +140,21 @@ describe("util > workspace", () => {
|
||||
expect(displayed).toEqual(workspace.template_display_name)
|
||||
})
|
||||
})
|
||||
|
||||
describe("displayImpendingDeletion", () => {
|
||||
const today = new Date()
|
||||
it.each<[string, boolean]>([
|
||||
[new Date(new Date().setDate(today.getDate() + 15)).toISOString(), false], // today + 15 days out
|
||||
[new Date(new Date().setDate(today.getDate() + 14)).toISOString(), true], // today + 14
|
||||
[new Date(new Date().setDate(today.getDate() + 13)).toISOString(), true], // today + 13
|
||||
[new Date(new Date().setDate(today.getDate() + 1)).toISOString(), true], // today + 1
|
||||
[new Date().toISOString(), true], // today + 0
|
||||
])(`deleting_at=%p, isWorkspaceOn=%p`, (deleting_at, shouldDisplay) => {
|
||||
const workspace: TypesGen.Workspace = {
|
||||
...Mocks.MockWorkspace,
|
||||
deleting_at,
|
||||
}
|
||||
expect(displayImpendingDeletion(workspace)).toBe(shouldDisplay)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -185,3 +185,45 @@ export const getDisplayWorkspaceTemplateName = (
|
||||
? workspace.template_display_name
|
||||
: workspace.template_name
|
||||
}
|
||||
|
||||
// This const dictates how far out we alert the user that a workspace
|
||||
// has an impending deletion (due to template.InactivityTTL being set)
|
||||
const IMPENDING_DELETION_DISPLAY_THRESHOLD = 14 // 14 days
|
||||
|
||||
/**
|
||||
* Returns a boolean indicating if an impending deletion indicator should be
|
||||
* displayed in the UI. Impending deletions are configured by setting the
|
||||
* Template.InactivityTTL
|
||||
* @param {TypesGen.Workspace} workspace
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const displayImpendingDeletion = (workspace: TypesGen.Workspace) => {
|
||||
const today = new Date()
|
||||
if (!workspace.deleting_at) {
|
||||
return false
|
||||
}
|
||||
|
||||
console.log("today", today)
|
||||
console.log("workspace.deletingAt", new Date(workspace.deleting_at))
|
||||
console.log(
|
||||
"set",
|
||||
new Date(
|
||||
today.setDate(today.getDate() - IMPENDING_DELETION_DISPLAY_THRESHOLD),
|
||||
),
|
||||
)
|
||||
|
||||
console.log(
|
||||
"compare",
|
||||
new Date(workspace.deleting_at) <=
|
||||
new Date(
|
||||
today.setDate(today.getDate() + IMPENDING_DELETION_DISPLAY_THRESHOLD),
|
||||
),
|
||||
)
|
||||
|
||||
return (
|
||||
new Date(workspace.deleting_at) <=
|
||||
new Date(
|
||||
today.setDate(today.getDate() + IMPENDING_DELETION_DISPLAY_THRESHOLD),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user