Skip to content

Commit fb3984c

Browse files
committed
fix workspace query
1 parent 539fcf9 commit fb3984c

File tree

24 files changed

+388
-117
lines changed

24 files changed

+388
-117
lines changed

coderd/database/modelqueries.go

+1
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa
217217
arg.Name,
218218
arg.HasAgent,
219219
arg.AgentInactiveDisconnectTimeoutSeconds,
220+
arg.LockedAt,
220221
arg.Offset,
221222
arg.Limit,
222223
)

coderd/database/queries.sql.go

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

coderd/database/queries/workspaces.sql

+6
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,12 @@ WHERE
259259
) > 0
260260
ELSE true
261261
END
262+
-- Filter by locked workspaces.
263+
AND CASE
264+
WHEN @locked_at :: timestamptz > '0001-01-01 00:00:00+00'::timestamptz THEN
265+
locked_at IS NOT NULL AND locked_at >= @locked_at
266+
ELSE true
267+
END
262268
-- Authorize Filter clause will be injected below in GetAuthorizedWorkspaces
263269
-- @authorize_filter
264270
ORDER BY

coderd/searchquery/search.go

+1
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ func Workspaces(query string, page codersdk.Pagination, agentInactiveDisconnectT
114114
filter.Name = parser.String(values, "", "name")
115115
filter.Status = string(httpapi.ParseCustom(parser, values, "", "status", httpapi.ParseEnum[database.WorkspaceStatus]))
116116
filter.HasAgent = parser.String(values, "", "has-agent")
117+
filter.LockedAt = parser.Time(values, time.Time{}, "locked_at", "2006-01-02")
117118

118119
if _, ok := values["deleting_by"]; ok {
119120
postFilter.DeletingBy = ptr.Ref(parser.Time(values, time.Time{}, "deleting_by", "2006-01-02"))

coderd/workspaces_test.go

+40
Original file line numberDiff line numberDiff line change
@@ -1407,6 +1407,46 @@ func TestWorkspaceFilterManual(t *testing.T) {
14071407
// and template.InactivityTTL should be 0
14081408
assert.Len(t, res.Workspaces, 0)
14091409
})
1410+
1411+
t.Run("LockedAt", func(t *testing.T) {
1412+
// this test has a licensed counterpart in enterprise/coderd/workspaces_test.go: FilterQueryHasDeletingByAndLicensed
1413+
t.Parallel()
1414+
client := coderdtest.New(t, &coderdtest.Options{
1415+
IncludeProvisionerDaemon: true,
1416+
})
1417+
user := coderdtest.CreateFirstUser(t, client)
1418+
authToken := uuid.NewString()
1419+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
1420+
Parse: echo.ParseComplete,
1421+
ProvisionPlan: echo.ProvisionComplete,
1422+
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
1423+
})
1424+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
1425+
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
1426+
1427+
// update template with inactivity ttl
1428+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
1429+
defer cancel()
1430+
1431+
lockedWorkspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
1432+
_ = coderdtest.AwaitWorkspaceBuildJob(t, client, lockedWorkspace.LatestBuild.ID)
1433+
1434+
// Create another workspace to validate that we do not return unlocked workspaces.
1435+
_ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
1436+
_ = coderdtest.AwaitWorkspaceBuildJob(t, client, lockedWorkspace.LatestBuild.ID)
1437+
1438+
err := client.UpdateWorkspaceLock(ctx, lockedWorkspace.ID, codersdk.UpdateWorkspaceLock{
1439+
Lock: true,
1440+
})
1441+
require.NoError(t, err)
1442+
1443+
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
1444+
FilterQuery: fmt.Sprintf("locked_at:%s", time.Now().Add(-time.Minute).Format("2006-01-02")),
1445+
})
1446+
require.NoError(t, err)
1447+
require.Len(t, res.Workspaces, 1)
1448+
require.NotNil(t, res.Workspaces[0].LockedAt)
1449+
})
14101450
}
14111451

14121452
func TestOffsetLimit(t *testing.T) {

site/src/api/api.ts

+15
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,21 @@ export const cancelWorkspaceBuild = async (
554554
return response.data
555555
}
556556

557+
export const updateWorkspaceLock = async (
558+
workspaceId: string,
559+
lock: boolean,
560+
): Promise<Types.Message> => {
561+
const data: TypesGen.UpdateWorkspaceLock = {
562+
lock: lock,
563+
}
564+
565+
const response = await axios.put(
566+
`/api/v2/workspaces/${workspaceId}/lock`,
567+
data,
568+
)
569+
return response.data
570+
}
571+
557572
export const restartWorkspace = async ({
558573
workspace,
559574
buildParameters,

site/src/components/Workspace/Workspace.tsx

+20-11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Button from "@mui/material/Button"
22
import { makeStyles } from "@mui/styles"
3+
import LockIcon from "@mui/icons-material/Lock"
34
import { Avatar } from "components/Avatar/Avatar"
45
import { AgentRow } from "components/Resources/AgentRow"
56
import {
@@ -26,7 +27,7 @@ import {
2627
} from "components/PageHeader/FullWidthPageHeader"
2728
import { TemplateVersionWarnings } from "components/TemplateVersionWarnings/TemplateVersionWarnings"
2829
import { ErrorAlert } from "components/Alert/ErrorAlert"
29-
import { ImpendingDeletionBanner } from "components/WorkspaceDeletion"
30+
import { LockedWorkspaceBanner } from "components/WorkspaceDeletion"
3031
import { useLocalStorage } from "hooks"
3132
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"
3233
import AlertTitle from "@mui/material/AlertTitle"
@@ -53,6 +54,7 @@ export interface WorkspaceProps {
5354
handleCancel: () => void
5455
handleSettings: () => void
5556
handleChangeVersion: () => void
57+
handleUnlock: () => void
5658
isUpdating: boolean
5759
isRestarting: boolean
5860
workspace: TypesGen.Workspace
@@ -86,6 +88,7 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
8688
handleCancel,
8789
handleSettings,
8890
handleChangeVersion,
91+
handleUnlock,
8992
workspace,
9093
isUpdating,
9194
isRestarting,
@@ -167,14 +170,19 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
167170
<>
168171
<FullWidthPageHeader>
169172
<Stack direction="row" spacing={3} alignItems="center">
170-
<Avatar
171-
size="md"
172-
src={workspace.template_icon}
173-
variant={workspace.template_icon ? "square" : undefined}
174-
fitImage={Boolean(workspace.template_icon)}
175-
>
176-
{workspace.name}
177-
</Avatar>
173+
{workspace.locked_at ? (
174+
<LockIcon fontSize="large" color="error" />
175+
) : (
176+
<Avatar
177+
size="md"
178+
src={workspace.template_icon}
179+
variant={workspace.template_icon ? "square" : undefined}
180+
fitImage={Boolean(workspace.template_icon)}
181+
>
182+
{workspace.name}
183+
</Avatar>
184+
)}
185+
178186
<div>
179187
<PageHeaderTitle>{workspace.name}</PageHeaderTitle>
180188
<PageHeaderSubtitle>{workspace.owner_name}</PageHeaderSubtitle>
@@ -203,6 +211,7 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
203211
handleCancel={handleCancel}
204212
handleSettings={handleSettings}
205213
handleChangeVersion={handleChangeVersion}
214+
handleUnlock={handleUnlock}
206215
canChangeVersions={canChangeVersions}
207216
isUpdating={isUpdating}
208217
isRestarting={isRestarting}
@@ -240,8 +249,8 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
240249
</Cond>
241250
<Cond>
242251
{/* <ImpendingDeletionBanner/> determines its own visibility */}
243-
<ImpendingDeletionBanner
244-
workspace={workspace}
252+
<LockedWorkspaceBanner
253+
workspaces={[workspace]}
245254
shouldRedisplayBanner={
246255
getLocal("dismissedWorkspace") !== workspace.id
247256
}

site/src/components/WorkspaceActions/Buttons.tsx

+18
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import BlockIcon from "@mui/icons-material/Block"
33
import CloudQueueIcon from "@mui/icons-material/CloudQueue"
44
import CropSquareIcon from "@mui/icons-material/CropSquare"
55
import PlayCircleOutlineIcon from "@mui/icons-material/PlayCircleOutline"
6+
import LockOpenIcon from "@mui/icons-material/LockOpen"
67
import ReplayIcon from "@mui/icons-material/Replay"
78
import { LoadingButton } from "components/LoadingButton/LoadingButton"
89
import { FC } from "react"
@@ -34,6 +35,23 @@ export const UpdateButton: FC<WorkspaceAction> = ({
3435
)
3536
}
3637

38+
export const UnlockButton: FC<WorkspaceAction> = ({
39+
handleAction,
40+
loading,
41+
}) => {
42+
return (
43+
<LoadingButton
44+
loading={loading}
45+
loadingIndicator="Unlocking..."
46+
loadingPosition="start"
47+
startIcon={<LockOpenIcon />}
48+
onClick={handleAction}
49+
>
50+
Unlock
51+
</LoadingButton>
52+
)
53+
}
54+
3755
export const StartButton: FC<
3856
Omit<WorkspaceAction, "handleAction"> & {
3957
workspace: Workspace

site/src/components/WorkspaceActions/WorkspaceActions.tsx

+12-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import Menu from "@mui/material/Menu"
33
import { makeStyles } from "@mui/styles"
44
import MoreVertOutlined from "@mui/icons-material/MoreVertOutlined"
55
import { FC, Fragment, ReactNode, useRef, useState } from "react"
6-
import { Workspace, WorkspaceBuildParameter } from "api/typesGenerated"
6+
import {
7+
Workspace,
8+
WorkspaceStatus,
9+
WorkspaceBuildParameter,
10+
} from "api/typesGenerated"
711
import {
812
ActionLoadingButton,
913
CancelButton,
@@ -12,6 +16,7 @@ import {
1216
StopButton,
1317
RestartButton,
1418
UpdateButton,
19+
UnlockButton,
1520
} from "./Buttons"
1621
import {
1722
ButtonMapping,
@@ -33,6 +38,7 @@ export interface WorkspaceActionsProps {
3338
handleCancel: () => void
3439
handleSettings: () => void
3540
handleChangeVersion: () => void
41+
handleUnlock: () => void
3642
isUpdating: boolean
3743
isRestarting: boolean
3844
children?: ReactNode
@@ -49,6 +55,7 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
4955
handleCancel,
5056
handleSettings,
5157
handleChangeVersion,
58+
handleUnlock,
5259
isUpdating,
5360
isRestarting,
5461
canChangeVersions,
@@ -93,6 +100,10 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
93100
[ButtonTypesEnum.canceling]: <DisabledButton label="Canceling..." />,
94101
[ButtonTypesEnum.deleted]: <DisabledButton label="Deleted" />,
95102
[ButtonTypesEnum.pending]: <ActionLoadingButton label="Pending..." />,
103+
[ButtonTypesEnum.unlock]: <UnlockButton handleAction={handleUnlock} />,
104+
[ButtonTypesEnum.unlocking]: (
105+
<UnlockButton loading handleAction={handleUnlock} />
106+
),
96107
}
97108

98109
// Returns a function that will execute the action and close the menu

site/src/components/WorkspaceActions/constants.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { WorkspaceStatus } from "api/typesGenerated"
1+
import { Workspace, WorkspaceStatus } from "api/typesGenerated"
22
import { ReactNode } from "react"
33

44
// the button types we have
@@ -12,6 +12,8 @@ export enum ButtonTypesEnum {
1212
deleting = "deleting",
1313
update = "update",
1414
updating = "updating",
15+
unlock = "lock",
16+
unlocking = "unlocking",
1517
// disabled buttons
1618
canceling = "canceling",
1719
deleted = "deleted",
@@ -29,8 +31,16 @@ interface WorkspaceAbilities {
2931
}
3032

3133
export const actionsByWorkspaceStatus = (
34+
workspace: Workspace,
3235
status: WorkspaceStatus,
3336
): WorkspaceAbilities => {
37+
if (workspace.locked_at) {
38+
return {
39+
actions: [ButtonTypesEnum.unlock],
40+
canCancel: false,
41+
canAcceptJobs: false,
42+
}
43+
}
3444
return statusToActions[status]
3545
}
3646

site/src/components/WorkspaceDeletion/ImpendingDeletionBadge.tsx

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { Workspace } from "api/typesGenerated"
2-
import { displayImpendingDeletion } from "./utils"
2+
import { displayLockedWorkspace } from "./utils"
33
import { useDashboard } from "components/Dashboard/DashboardProvider"
44
import { Pill } from "components/Pill/Pill"
5-
import ErrorIcon from "@mui/icons-material/ErrorOutline"
5+
import LockIcon from "@mui/icons-material/Lock"
66

7-
export const ImpendingDeletionBadge = ({
7+
export const LockedBadge = ({
88
workspace,
99
}: {
1010
workspace: Workspace
@@ -18,7 +18,7 @@ export const ImpendingDeletionBadge = ({
1818
// return null
1919

2020
if (
21-
!displayImpendingDeletion(
21+
!displayLockedWorkspace(
2222
workspace,
2323
allowAdvancedScheduling,
2424
allowWorkspaceActions,
@@ -27,5 +27,5 @@ export const ImpendingDeletionBadge = ({
2727
return null
2828
}
2929

30-
return <Pill icon={<ErrorIcon />} text="Impending deletion" type="error" />
30+
return <Pill icon={<LockIcon />} text="Locked" type="error" />
3131
}

0 commit comments

Comments
 (0)