From 0d79b8fee46e5445edd54c105d44f4e6c5f14bf0 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Fri, 14 Jul 2023 02:58:19 +0000 Subject: [PATCH 01/15] feat: add frontend for locked workspaces - Fix workspaces query for locked workspaces. --- coderd/database/modelqueries.go | 1 + coderd/database/queries.sql.go | 14 ++- coderd/database/queries/workspaces.sql | 6 ++ coderd/searchquery/search.go | 1 + coderd/workspaces_test.go | 40 +++++++++ site/src/api/api.ts | 15 ++++ site/src/components/Workspace/Workspace.tsx | 31 ++++--- .../components/WorkspaceActions/Buttons.tsx | 18 ++++ .../WorkspaceActions/WorkspaceActions.tsx | 13 ++- .../components/WorkspaceActions/constants.ts | 12 ++- .../ImpendingDeletionBadge.tsx | 10 +-- .../ImpendingDeletionBanner.tsx | 89 ++++++++++++------- .../src/components/WorkspaceDeletion/utils.ts | 23 +++++ .../WorkspaceStatusBadge.tsx | 6 +- site/src/i18n/en/templateSettingsPage.json | 6 +- .../InactivityDialog.stories.tsx | 2 +- .../TemplateScheduleForm/InactivityDialog.tsx | 35 +++++++- .../TemplateScheduleForm.tsx | 58 +++++++++--- .../useWorkspacesToBeDeleted.ts | 61 ++++++++++--- .../WorkspacePage/WorkspaceReadyPage.tsx | 9 ++ .../WorkspacesPage/WorkspacesPageView.tsx | 47 +++++----- .../pages/WorkspacesPage/filter/filter.tsx | 4 + site/src/utils/filters.ts | 1 + site/src/utils/workspace.tsx | 1 + 24 files changed, 387 insertions(+), 116 deletions(-) diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index ffa346d04998c..c75f38fc3b596 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -217,6 +217,7 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa arg.Name, arg.HasAgent, arg.AgentInactiveDisconnectTimeoutSeconds, + arg.LockedAt, arg.Offset, arg.Limit, ) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 95ad36d9593ca..a586fb105790f 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -8698,6 +8698,12 @@ WHERE ) > 0 ELSE true END + -- Filter by locked workspaces. + AND CASE + WHEN $10 :: timestamptz > '0001-01-01 00:00:00+00'::timestamptz THEN + locked_at IS NOT NULL AND locked_at >= $10 + ELSE true + END -- Authorize Filter clause will be injected below in GetAuthorizedWorkspaces -- @authorize_filter ORDER BY @@ -8709,11 +8715,11 @@ ORDER BY LOWER(workspaces.name) ASC LIMIT CASE - WHEN $11 :: integer > 0 THEN - $11 + WHEN $12 :: integer > 0 THEN + $12 END OFFSET - $10 + $11 ` type GetWorkspacesParams struct { @@ -8726,6 +8732,7 @@ type GetWorkspacesParams struct { Name string `db:"name" json:"name"` HasAgent string `db:"has_agent" json:"has_agent"` AgentInactiveDisconnectTimeoutSeconds int64 `db:"agent_inactive_disconnect_timeout_seconds" json:"agent_inactive_disconnect_timeout_seconds"` + LockedAt time.Time `db:"locked_at" json:"locked_at"` Offset int32 `db:"offset_" json:"offset_"` Limit int32 `db:"limit_" json:"limit_"` } @@ -8761,6 +8768,7 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) arg.Name, arg.HasAgent, arg.AgentInactiveDisconnectTimeoutSeconds, + arg.LockedAt, arg.Offset, arg.Limit, ) diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql index 5e540a0e5c90a..8594a3ee37944 100644 --- a/coderd/database/queries/workspaces.sql +++ b/coderd/database/queries/workspaces.sql @@ -259,6 +259,12 @@ WHERE ) > 0 ELSE true END + -- Filter by locked workspaces. + AND CASE + WHEN @locked_at :: timestamptz > '0001-01-01 00:00:00+00'::timestamptz THEN + locked_at IS NOT NULL AND locked_at >= @locked_at + ELSE true + END -- Authorize Filter clause will be injected below in GetAuthorizedWorkspaces -- @authorize_filter ORDER BY diff --git a/coderd/searchquery/search.go b/coderd/searchquery/search.go index 9b216d0180e15..3deafefd25193 100644 --- a/coderd/searchquery/search.go +++ b/coderd/searchquery/search.go @@ -114,6 +114,7 @@ func Workspaces(query string, page codersdk.Pagination, agentInactiveDisconnectT filter.Name = parser.String(values, "", "name") filter.Status = string(httpapi.ParseCustom(parser, values, "", "status", httpapi.ParseEnum[database.WorkspaceStatus])) filter.HasAgent = parser.String(values, "", "has-agent") + filter.LockedAt = parser.Time(values, time.Time{}, "locked_at", "2006-01-02") if _, ok := values["deleting_by"]; ok { postFilter.DeletingBy = ptr.Ref(parser.Time(values, time.Time{}, "deleting_by", "2006-01-02")) diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index db5db020488b7..2213cd4657a70 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -1407,6 +1407,46 @@ func TestWorkspaceFilterManual(t *testing.T) { // and template.InactivityTTL should be 0 assert.Len(t, res.Workspaces, 0) }) + + t.Run("LockedAt", func(t *testing.T) { + // this test has a licensed counterpart in enterprise/coderd/workspaces_test.go: FilterQueryHasDeletingByAndLicensed + t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{ + IncludeProvisionerDaemon: true, + }) + user := coderdtest.CreateFirstUser(t, client) + authToken := uuid.NewString() + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionPlan: echo.ProvisionComplete, + ProvisionApply: echo.ProvisionApplyWithAgent(authToken), + }) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + + // update template with inactivity ttl + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + lockedWorkspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + _ = coderdtest.AwaitWorkspaceBuildJob(t, client, lockedWorkspace.LatestBuild.ID) + + // Create another workspace to validate that we do not return unlocked workspaces. + _ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + _ = coderdtest.AwaitWorkspaceBuildJob(t, client, lockedWorkspace.LatestBuild.ID) + + err := client.UpdateWorkspaceLock(ctx, lockedWorkspace.ID, codersdk.UpdateWorkspaceLock{ + Lock: true, + }) + require.NoError(t, err) + + res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{ + FilterQuery: fmt.Sprintf("locked_at:%s", time.Now().Add(-time.Minute).Format("2006-01-02")), + }) + require.NoError(t, err) + require.Len(t, res.Workspaces, 1) + require.NotNil(t, res.Workspaces[0].LockedAt) + }) } func TestOffsetLimit(t *testing.T) { diff --git a/site/src/api/api.ts b/site/src/api/api.ts index ba412e39e8764..8c908c185d800 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -554,6 +554,21 @@ export const cancelWorkspaceBuild = async ( return response.data } +export const updateWorkspaceLock = async ( + workspaceId: string, + lock: boolean, +): Promise => { + const data: TypesGen.UpdateWorkspaceLock = { + lock: lock, + } + + const response = await axios.put( + `/api/v2/workspaces/${workspaceId}/lock`, + data, + ) + return response.data +} + export const restartWorkspace = async ({ workspace, buildParameters, diff --git a/site/src/components/Workspace/Workspace.tsx b/site/src/components/Workspace/Workspace.tsx index bfa4d8bbb97d5..613ed15219773 100644 --- a/site/src/components/Workspace/Workspace.tsx +++ b/site/src/components/Workspace/Workspace.tsx @@ -1,5 +1,6 @@ import Button from "@mui/material/Button" import { makeStyles } from "@mui/styles" +import LockIcon from "@mui/icons-material/Lock" import { Avatar } from "components/Avatar/Avatar" import { AgentRow } from "components/Resources/AgentRow" import { @@ -26,7 +27,7 @@ import { } from "components/PageHeader/FullWidthPageHeader" import { TemplateVersionWarnings } from "components/TemplateVersionWarnings/TemplateVersionWarnings" import { ErrorAlert } from "components/Alert/ErrorAlert" -import { ImpendingDeletionBanner } from "components/WorkspaceDeletion" +import { LockedWorkspaceBanner } from "components/WorkspaceDeletion" import { useLocalStorage } from "hooks" import { ChooseOne, Cond } from "components/Conditionals/ChooseOne" import AlertTitle from "@mui/material/AlertTitle" @@ -53,6 +54,7 @@ export interface WorkspaceProps { handleCancel: () => void handleSettings: () => void handleChangeVersion: () => void + handleUnlock: () => void isUpdating: boolean isRestarting: boolean workspace: TypesGen.Workspace @@ -86,6 +88,7 @@ export const Workspace: FC> = ({ handleCancel, handleSettings, handleChangeVersion, + handleUnlock, workspace, isUpdating, isRestarting, @@ -167,14 +170,19 @@ export const Workspace: FC> = ({ <> - - {workspace.name} - + {workspace.locked_at ? ( + + ) : ( + + {workspace.name} + + )} +
{workspace.name} {workspace.owner_name} @@ -203,6 +211,7 @@ export const Workspace: FC> = ({ handleCancel={handleCancel} handleSettings={handleSettings} handleChangeVersion={handleChangeVersion} + handleUnlock={handleUnlock} canChangeVersions={canChangeVersions} isUpdating={isUpdating} isRestarting={isRestarting} @@ -240,8 +249,8 @@ export const Workspace: FC> = ({ {/* determines its own visibility */} - = ({ ) } +export const UnlockButton: FC = ({ + handleAction, + loading, +}) => { + return ( + } + onClick={handleAction} + > + Unlock + + ) +} + export const StartButton: FC< Omit & { workspace: Workspace diff --git a/site/src/components/WorkspaceActions/WorkspaceActions.tsx b/site/src/components/WorkspaceActions/WorkspaceActions.tsx index 8242b470c8c67..46a1282ee2ee6 100644 --- a/site/src/components/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/components/WorkspaceActions/WorkspaceActions.tsx @@ -3,7 +3,11 @@ import Menu from "@mui/material/Menu" import { makeStyles } from "@mui/styles" import MoreVertOutlined from "@mui/icons-material/MoreVertOutlined" import { FC, Fragment, ReactNode, useRef, useState } from "react" -import { Workspace, WorkspaceBuildParameter } from "api/typesGenerated" +import { + Workspace, + WorkspaceStatus, + WorkspaceBuildParameter, +} from "api/typesGenerated" import { ActionLoadingButton, CancelButton, @@ -12,6 +16,7 @@ import { StopButton, RestartButton, UpdateButton, + UnlockButton, } from "./Buttons" import { ButtonMapping, @@ -33,6 +38,7 @@ export interface WorkspaceActionsProps { handleCancel: () => void handleSettings: () => void handleChangeVersion: () => void + handleUnlock: () => void isUpdating: boolean isRestarting: boolean children?: ReactNode @@ -49,6 +55,7 @@ export const WorkspaceActions: FC = ({ handleCancel, handleSettings, handleChangeVersion, + handleUnlock, isUpdating, isRestarting, canChangeVersions, @@ -93,6 +100,10 @@ export const WorkspaceActions: FC = ({ [ButtonTypesEnum.canceling]: , [ButtonTypesEnum.deleted]: , [ButtonTypesEnum.pending]: , + [ButtonTypesEnum.unlock]: , + [ButtonTypesEnum.unlocking]: ( + + ), } // Returns a function that will execute the action and close the menu diff --git a/site/src/components/WorkspaceActions/constants.ts b/site/src/components/WorkspaceActions/constants.ts index c4e81ca976ca5..c21fdedc98249 100644 --- a/site/src/components/WorkspaceActions/constants.ts +++ b/site/src/components/WorkspaceActions/constants.ts @@ -1,4 +1,4 @@ -import { WorkspaceStatus } from "api/typesGenerated" +import { Workspace, WorkspaceStatus } from "api/typesGenerated" import { ReactNode } from "react" // the button types we have @@ -12,6 +12,8 @@ export enum ButtonTypesEnum { deleting = "deleting", update = "update", updating = "updating", + unlock = "lock", + unlocking = "unlocking", // disabled buttons canceling = "canceling", deleted = "deleted", @@ -29,8 +31,16 @@ interface WorkspaceAbilities { } export const actionsByWorkspaceStatus = ( + workspace: Workspace, status: WorkspaceStatus, ): WorkspaceAbilities => { + if (workspace.locked_at) { + return { + actions: [ButtonTypesEnum.unlock], + canCancel: false, + canAcceptJobs: false, + } + } return statusToActions[status] } diff --git a/site/src/components/WorkspaceDeletion/ImpendingDeletionBadge.tsx b/site/src/components/WorkspaceDeletion/ImpendingDeletionBadge.tsx index eb27db82a3e2e..96b29a208e430 100644 --- a/site/src/components/WorkspaceDeletion/ImpendingDeletionBadge.tsx +++ b/site/src/components/WorkspaceDeletion/ImpendingDeletionBadge.tsx @@ -1,10 +1,10 @@ import { Workspace } from "api/typesGenerated" -import { displayImpendingDeletion } from "./utils" +import { displayLockedWorkspace } from "./utils" import { useDashboard } from "components/Dashboard/DashboardProvider" import { Pill } from "components/Pill/Pill" -import ErrorIcon from "@mui/icons-material/ErrorOutline" +import LockIcon from "@mui/icons-material/Lock" -export const ImpendingDeletionBadge = ({ +export const LockedBadge = ({ workspace, }: { workspace: Workspace @@ -18,7 +18,7 @@ export const ImpendingDeletionBadge = ({ // return null if ( - !displayImpendingDeletion( + !displayLockedWorkspace( workspace, allowAdvancedScheduling, allowWorkspaceActions, @@ -27,5 +27,5 @@ export const ImpendingDeletionBadge = ({ return null } - return } text="Impending deletion" type="error" /> + return } text="Locked" type="error" /> } diff --git a/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx b/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx index d793c16d5dc3a..f5766be168012 100644 --- a/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx +++ b/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx @@ -11,13 +11,13 @@ export enum Count { Multiple, } -export const ImpendingDeletionBanner = ({ - workspace, +export const LockedWorkspaceBanner = ({ + workspaces, onDismiss, shouldRedisplayBanner, count = Count.Singular, }: { - workspace?: Workspace + workspaces?: Workspace[] onDismiss: () => void shouldRedisplayBanner: boolean count?: Count @@ -29,51 +29,78 @@ export const ImpendingDeletionBanner = ({ // is merged up const allowWorkspaceActions = experiments.includes("workspace_actions") + if (!workspaces) { + return null + } + + const hasLockedWorkspaces = workspaces.find( + (workspace) => workspace.locked_at, + ) + + const hasDeletionScheduledWorkspaces = workspaces.find( + (workspace) => workspace.deleting_at, + ) + if ( - !workspace || - !displayImpendingDeletion( - workspace, - allowAdvancedScheduling, - allowWorkspaceActions, - ) || - // Banners should be redisplayed after dismissal when additional workspaces are newly scheduled for deletion - !shouldRedisplayBanner + (!hasLockedWorkspaces || + // Banners should be redisplayed after dismissal when additional workspaces are newly scheduled for deletion + !shouldRedisplayBanner) && + // Only show this if the experiment is included. + allowWorkspaceActions && + allowAdvancedScheduling ) { return null } - // if deleting_at is 7 days away or less, display an 'error' banner to convey urgency to user - const daysUntilDelete = differenceInDays( - Date.parse(workspace.last_used_at), - new Date(), - ) + const formatDate = (dateStr: string): string => { + const date = new Date(dateStr) + return date.toLocaleDateString(undefined, { + month: "long", + day: "numeric", + year: "numeric", + }) + } - const plusFourteen = add(new Date(), { days: 14 }) + const alertText = (): string => { + if (workspaces.length == 1) { + if ( + hasDeletionScheduledWorkspaces && + hasDeletionScheduledWorkspaces.deleting_at && + hasDeletionScheduledWorkspaces.locked_at + ) { + return `This workspace has been locked since ${formatDistanceToNow( + Date.parse(hasDeletionScheduledWorkspaces.locked_at), + )} and is scheduled to be deleted at ${formatDate( + hasDeletionScheduledWorkspaces.deleting_at, + )} . To keep it you must unlock the workspace.` + } else if (hasLockedWorkspaces && hasLockedWorkspaces.locked_at) { + return `This workspace has been locked since ${formatDate( + hasLockedWorkspaces.locked_at, + )} + and cannot be interacted + with. Locked workspaces are eligible for + permanent deletion. To prevent deletion, unlock + the workspace.` + } + } + return "" + } return ( - + {count === Count.Singular ? ( - `This workspace has been unused for ${formatDistanceToNow( - Date.parse(workspace.last_used_at), - )} and is scheduled for deletion. To keep it, connect via SSH or the web terminal.` + alertText() ) : ( <> There are{" "} workspaces {" "} - that will be deleted soon due to inactivity. To keep these workspaces, - connect to them via SSH or the web terminal. + that may be deleted soon due to inactivity. Unlock the workspaces you + wish to retain. )} diff --git a/site/src/components/WorkspaceDeletion/utils.ts b/site/src/components/WorkspaceDeletion/utils.ts index 745bd48ad8941..704fd750ea114 100644 --- a/site/src/components/WorkspaceDeletion/utils.ts +++ b/site/src/components/WorkspaceDeletion/utils.ts @@ -4,6 +4,29 @@ import { Workspace } from "api/typesGenerated" // 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 displayLockedWorkspace = ( + workspace: Workspace, + allowAdvancedScheduling: boolean, + allowWorkspaceActions: boolean, +) => { + const today = new Date() + if ( + !workspace.locked_at || + !allowAdvancedScheduling || + !allowWorkspaceActions + ) { + return false + } + return workspace.locked_at +} + /** * Returns a boolean indicating if an impending deletion indicator should be * displayed in the UI. Impending deletions are configured by setting the diff --git a/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx b/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx index 7f742dab2ff3c..2ace2d0b903b8 100644 --- a/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx +++ b/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx @@ -5,7 +5,7 @@ import { makeStyles } from "@mui/styles" import { combineClasses } from "utils/combineClasses" import { ChooseOne, Cond } from "components/Conditionals/ChooseOne" import { - ImpendingDeletionBadge, + LockedBadge, ImpendingDeletionText, } from "components/WorkspaceDeletion" import { getDisplayWorkspaceStatus } from "utils/workspace" @@ -25,8 +25,8 @@ export const WorkspaceStatusBadge: FC< return ( {/* determines its own visibility */} - - + + diff --git a/site/src/i18n/en/templateSettingsPage.json b/site/src/i18n/en/templateSettingsPage.json index 11f314e0a7472..d1b0927b5a78a 100644 --- a/site/src/i18n/en/templateSettingsPage.json +++ b/site/src/i18n/en/templateSettingsPage.json @@ -22,9 +22,9 @@ "failureTTLHelperText_zero": "Coder will not automatically stop failed workspaces", "failureTTLHelperText_one": "Coder will attempt to stop failed workspaces after {{count}} day.", "failureTTLHelperText_other": "Coder will attempt to stop failed workspaces after {{count}} days.", - "inactivityTTLHelperText_zero": "Coder will not automatically delete inactive workspaces", - "inactivityTTLHelperText_one": "Coder will automatically delete inactive workspaces after {{count}} day.", - "inactivityTTLHelperText_other": "Coder will automatically delete inactive workspaces after {{count}} days.", + "inactivityTTLHelperText_zero": "Coder will not automatically lock inactive workspaces", + "inactivityTTLHelperText_one": "Coder will automatically lock inactive workspaces after {{count}} day.", + "inactivityTTLHelperText_other": "Coder will automatically lock inactive workspaces after {{count}} days.", "lockedTTLHelperText_zero": "Coder will not automatically delete locked workspaces", "lockedTTLHelperText_one": "Coder will automatically delete locked workspaces after {{count}} day.", "lockedTTLHelperText_other": "Coder will automatically delete locked workspaces after {{count}} days.", diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/InactivityDialog.stories.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/InactivityDialog.stories.tsx index 645c7dfb84ddd..6128700299e28 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/InactivityDialog.stories.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/InactivityDialog.stories.tsx @@ -14,6 +14,6 @@ export const OpenDialog: Story = { submitValues: () => null, isInactivityDialogOpen: true, setIsInactivityDialogOpen: () => null, - workspacesToBeDeletedToday: 2, + workspacesToBeLockedToday: 2, }, } diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/InactivityDialog.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/InactivityDialog.tsx index 3f5ec252b08be..a9407f6d5eab5 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/InactivityDialog.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/InactivityDialog.tsx @@ -4,12 +4,12 @@ export const InactivityDialog = ({ submitValues, isInactivityDialogOpen, setIsInactivityDialogOpen, - workspacesToBeDeletedToday, + workspacesToBeLockedToday, }: { submitValues: () => void isInactivityDialogOpen: boolean setIsInactivityDialogOpen: (arg0: boolean) => void - workspacesToBeDeletedToday: number + workspacesToBeLockedToday: number }) => { return ( setIsInactivityDialogOpen(false)} - title="Delete inactive workspaces" + title="Lock inactive workspaces" + confirmText="Lock Workspaces" + description={`There are ${ + workspacesToBeLockedToday ? workspacesToBeLockedToday : "" + } workspaces that already match this filter and will be locked upon form submission. Are you sure you want to proceed?`} + /> + ) +} + +export const DeleteLockedDialog = ({ + submitValues, + isLockedDialogOpen, + setIsLockedDialogOpen, + workspacesToBeDeletedToday, +}: { + submitValues: () => void + isLockedDialogOpen: boolean + setIsLockedDialogOpen: (arg0: boolean) => void + workspacesToBeDeletedToday: number +}) => { + return ( + { + submitValues() + setIsLockedDialogOpen(false) + }} + onClose={() => setIsLockedDialogOpen(false)} + title="Delete Locked Workspaces" confirmText="Delete Workspaces" description={`There are ${ workspacesToBeDeletedToday ? workspacesToBeDeletedToday : "" diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx index 598f701338f1e..e23c83794bd6f 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx @@ -16,8 +16,11 @@ import Link from "@mui/material/Link" import Checkbox from "@mui/material/Checkbox" import FormControlLabel from "@mui/material/FormControlLabel" import Switch from "@mui/material/Switch" -import { InactivityDialog } from "./InactivityDialog" -import { useWorkspacesToBeDeleted } from "./useWorkspacesToBeDeleted" +import { DeleteLockedDialog, InactivityDialog } from "./InactivityDialog" +import { + useWorkspacesToBeLocked, + useWorkspacesToBeDeleted, +} from "./useWorkspacesToBeDeleted" import { TemplateScheduleFormValues, getValidationSchema } from "./formHelpers" import { TTLHelperText } from "./TTLHelperText" import { docs } from "utils/docs" @@ -89,10 +92,16 @@ export const TemplateScheduleForm: FC = ({ onSubmit: () => { if ( form.values.inactivity_cleanup_enabled && + workspacesToBeLockedToday && + workspacesToBeLockedToday.length > 0 + ) { + setIsInactivityDialogOpen(true) + } else if ( + form.values.locked_cleanup_enabled && workspacesToBeDeletedToday && workspacesToBeDeletedToday.length > 0 ) { - setIsInactivityDialogOpen(true) + setIsLockedDialogOpen(true) } else { submitValues() } @@ -106,10 +115,20 @@ export const TemplateScheduleForm: FC = ({ const { t } = useTranslation("templateSettingsPage") const styles = useStyles() - const workspacesToBeDeletedToday = useWorkspacesToBeDeleted(form.values) + const workspacesToBeLockedToday = useWorkspacesToBeLocked( + template, + form.values, + ) + const workspacesToBeDeletedToday = useWorkspacesToBeDeleted( + template, + form.values, + ) + console.log("workspaces to be deleted: ", workspacesToBeDeletedToday?.length) + console.log("workspaces to be locked: ", workspacesToBeLockedToday?.length) const [isInactivityDialogOpen, setIsInactivityDialogOpen] = useState(false) + const [isLockedDialogOpen, setIsLockedDialogOpen] = useState(false) const submitValues = () => { // on submit, convert from hours => ms @@ -329,7 +348,7 @@ export const TemplateScheduleForm: FC = ({ @@ -341,7 +360,7 @@ export const TemplateScheduleForm: FC = ({ onChange={handleToggleInactivityCleanup} /> } - label="Enable Inactivity Cleanup" + label="Enable Inactivity TTL" /> = ({ @@ -375,7 +394,7 @@ export const TemplateScheduleForm: FC = ({ onChange={handleToggleLockedCleanup} /> } - label="Enable Locked Cleanup" + label="Enable Locked TTL" /> = ({ )} - + {workspacesToBeLockedToday && workspacesToBeLockedToday.length > 0 && ( + + )} + {workspacesToBeDeletedToday && workspacesToBeDeletedToday.length > 0 && ( + + )} + { + const { data: workspacesData } = useQuery({ + queryKey: ["workspaces"], + queryFn: () => + getWorkspaces({ + q: "template:" + template.name, + }), + enabled: formValues.inactivity_cleanup_enabled, + }) + + return workspacesData?.workspaces?.filter((workspace: Workspace) => { + if (!formValues.inactivity_ttl_ms) { + return + } + + if (workspace.locked_at) { + return + } + + const proposedLocking = new Date( + new Date(workspace.last_used_at).getTime() + + formValues.inactivity_ttl_ms * 86400000, + ) + + if (compareAsc(proposedLocking, new Date()) < 1) { + return workspace + } + }) +} export const useWorkspacesToBeDeleted = ( + template: Template, formValues: TemplateScheduleFormValues, ) => { const { data: workspacesData } = useQuery({ queryKey: ["workspaces"], - queryFn: () => getWorkspaces({}), - enabled: formValues.inactivity_cleanup_enabled, + queryFn: () => + getWorkspaces({ + q: "template:" + template.name, + }), + enabled: formValues.locked_cleanup_enabled, }) return workspacesData?.workspaces?.filter((workspace: Workspace) => { - const isInactive = inactiveStatuses.includes(workspace.latest_build.status) + if (!workspace.locked_at || !formValues.locked_ttl_ms) { + return false + } - const proposedDeletion = add(new Date(workspace.last_used_at), { - days: formValues.inactivity_ttl_ms, - }) + const proposedLocking = new Date( + new Date(workspace.locked_at).getTime() + + formValues.locked_ttl_ms * 86400000, + ) - if (isInactive && compareAsc(proposedDeletion, endOfToday()) < 1) { + if (compareAsc(proposedLocking, new Date()) < 1) { return workspace } }) diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index 4407f8f017775..f1ca9ba71e1dc 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -176,6 +176,15 @@ export const WorkspaceReadyPage = ({ handleChangeVersion={() => { setChangeVersionDialogOpen(true) }} + handleUnlock={() => { + updateWorkspaceLock(workspace.id, false) + .then(() => { + window.location.reload() + }) + .catch((error) => { + console.log("ERROR: ", error) + }) + }} resources={workspace.latest_build.resources} builds={builds} canUpdateWorkspace={canUpdateWorkspace} diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index bafe93770d51c..1fad826eb5089 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -15,7 +15,7 @@ import { WorkspaceHelpTooltip } from "components/Tooltips" import { WorkspacesTable } from "components/WorkspacesTable/WorkspacesTable" import { useLocalStorage } from "hooks" import difference from "lodash/difference" -import { ImpendingDeletionBanner, Count } from "components/WorkspaceDeletion" +import { LockedWorkspaceBanner, Count } from "components/WorkspaceDeletion" import { ErrorAlert } from "components/Alert/ErrorAlert" import { WorkspacesFilter } from "./filter/filter" import { hasError, isApiValidationError } from "api/errors" @@ -55,31 +55,26 @@ export const WorkspacesPageView: FC< }) => { const { saveLocal, getLocal } = useLocalStorage() - const workspaceIdsWithImpendingDeletions = workspaces + const workspacesDeletionScheduled = workspaces ?.filter((workspace) => workspace.deleting_at) .map((workspace) => workspace.id) - /** - * Returns a boolean indicating if there are workspaces that have been - * recently marked for deletion but are not in local storage. - * If there are, we want to alert the user so they can potentially take action - * before deletion takes place. - * @returns {boolean} - */ - const isNewWorkspacesImpendingDeletion = (): boolean => { - const dismissedList = getLocal("dismissedWorkspaceList") - if (!dismissedList) { - return true - } - - const diff = difference( - workspaceIdsWithImpendingDeletions, - JSON.parse(dismissedList), - ) + const hasLockedWorkspace = + workspaces?.find((workspace) => workspace.locked_at) != undefined - return diff && diff.length > 0 + const hasLockedFilter = (): boolean => { + for (const key in filterProps.filter.values) { + if (key == "locked_at") { + return true + } + } + return false } + const shownWorkspaces = !hasLockedFilter() + ? workspaces?.filter((workspace) => !workspace.locked_at) + : workspaces + return ( @@ -104,13 +99,13 @@ export const WorkspacesPageView: FC< {/* determines its own visibility */} - workspace.deleting_at)} - shouldRedisplayBanner={isNewWorkspacesImpendingDeletion()} + saveLocal( "dismissedWorkspaceList", - JSON.stringify(workspaceIdsWithImpendingDeletions), + JSON.stringify(workspacesDeletionScheduled), ) } count={Count.Multiple} @@ -121,13 +116,13 @@ export const WorkspacesPageView: FC< Date: Fri, 21 Jul 2023 18:06:38 +0000 Subject: [PATCH 02/15] fix error --- site/src/components/WorkspaceActions/WorkspaceActions.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/components/WorkspaceActions/WorkspaceActions.tsx b/site/src/components/WorkspaceActions/WorkspaceActions.tsx index 46a1282ee2ee6..31568917b69ba 100644 --- a/site/src/components/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/components/WorkspaceActions/WorkspaceActions.tsx @@ -65,7 +65,7 @@ export const WorkspaceActions: FC = ({ canCancel, canAcceptJobs, actions: actionsByStatus, - } = actionsByWorkspaceStatus(workspace.latest_build.status) + } = actionsByWorkspaceStatus(workspace, workspace.latest_build.status) const canBeUpdated = workspace.outdated && canAcceptJobs const menuTriggerRef = useRef(null) const [isMenuOpen, setIsMenuOpen] = useState(false) From 6ac1ba27c9415f6e382b9dc71a3e37e74747c885 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Tue, 25 Jul 2023 22:25:33 +0000 Subject: [PATCH 03/15] do not return locked workspaces by default --- coderd/database/queries/workspaces.sql | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql index 8594a3ee37944..c6d5dfed9d8e2 100644 --- a/coderd/database/queries/workspaces.sql +++ b/coderd/database/queries/workspaces.sql @@ -259,11 +259,13 @@ WHERE ) > 0 ELSE true END - -- Filter by locked workspaces. + -- Filter by locked workspaces. By default we do not return locked + -- workspaces since they are considered soft-deleted. AND CASE WHEN @locked_at :: timestamptz > '0001-01-01 00:00:00+00'::timestamptz THEN locked_at IS NOT NULL AND locked_at >= @locked_at - ELSE true + ELSE + locked_at IS NULL END -- Authorize Filter clause will be injected below in GetAuthorizedWorkspaces -- @authorize_filter From 2bea4185228feef5b73da063b55e81bdf3d1427a Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Wed, 26 Jul 2023 01:20:11 +0000 Subject: [PATCH 04/15] integrate xstate --- coderd/apidoc/docs.go | 2 +- coderd/apidoc/swagger.json | 2 +- coderd/autobuild/lifecycle_executor.go | 2 +- coderd/database/dbauthz/dbauthz.go | 4 +- coderd/database/dbfake/dbfake.go | 10 +- coderd/database/dbmetrics/dbmetrics.go | 6 +- coderd/database/dbmock/dbmock.go | 7 +- coderd/database/querier.go | 2 +- coderd/database/queries.sql.go | 31 +++- coderd/database/queries/workspaces.sql | 5 +- coderd/workspaces.go | 26 ++- docs/api/workspaces.md | 164 ++++++++++++++++-- site/src/api/api.ts | 2 +- .../WorkspacePage/WorkspaceReadyPage.tsx | 10 +- .../xServices/workspace/workspaceXService.ts | 34 +++- 15 files changed, 251 insertions(+), 56 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index f6cff3d136c00..728b68fc6aad9 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -5931,7 +5931,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.Response" + "$ref": "#/definitions/codersdk.Workspace" } } } diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index d3bc648e764ec..efa86c1fbf1c4 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -5229,7 +5229,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.Response" + "$ref": "#/definitions/codersdk.Workspace" } } } diff --git a/coderd/autobuild/lifecycle_executor.go b/coderd/autobuild/lifecycle_executor.go index f7176ae8cd721..b36a265b7ee98 100644 --- a/coderd/autobuild/lifecycle_executor.go +++ b/coderd/autobuild/lifecycle_executor.go @@ -178,7 +178,7 @@ func (e *Executor) runOnce(t time.Time) Stats { // Lock the workspace if it has breached the template's // threshold for inactivity. if reason == database.BuildReasonAutolock { - err = tx.UpdateWorkspaceLockedDeletingAt(e.ctx, database.UpdateWorkspaceLockedDeletingAtParams{ + ws, err = tx.UpdateWorkspaceLockedDeletingAt(e.ctx, database.UpdateWorkspaceLockedDeletingAtParams{ ID: ws.ID, LockedAt: sql.NullTime{ Time: database.Now(), diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 348edf2644dc2..52ca71bf7c035 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -2511,11 +2511,11 @@ func (q *querier) UpdateWorkspaceLastUsedAt(ctx context.Context, arg database.Up return update(q.log, q.auth, fetch, q.db.UpdateWorkspaceLastUsedAt)(ctx, arg) } -func (q *querier) UpdateWorkspaceLockedDeletingAt(ctx context.Context, arg database.UpdateWorkspaceLockedDeletingAtParams) error { +func (q *querier) UpdateWorkspaceLockedDeletingAt(ctx context.Context, arg database.UpdateWorkspaceLockedDeletingAtParams) (database.Workspace, error) { fetch := func(ctx context.Context, arg database.UpdateWorkspaceLockedDeletingAtParams) (database.Workspace, error) { return q.db.GetWorkspaceByID(ctx, arg.ID) } - return update(q.log, q.auth, fetch, q.db.UpdateWorkspaceLockedDeletingAt)(ctx, arg) + return updateWithReturn(q.log, q.auth, fetch, q.db.UpdateWorkspaceLockedDeletingAt)(ctx, arg) } func (q *querier) UpdateWorkspaceProxy(ctx context.Context, arg database.UpdateWorkspaceProxyParams) (database.WorkspaceProxy, error) { diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go index 21b026bf2c782..34875c68b8e27 100644 --- a/coderd/database/dbfake/dbfake.go +++ b/coderd/database/dbfake/dbfake.go @@ -5082,9 +5082,9 @@ func (q *FakeQuerier) UpdateWorkspaceLastUsedAt(_ context.Context, arg database. return sql.ErrNoRows } -func (q *FakeQuerier) UpdateWorkspaceLockedDeletingAt(_ context.Context, arg database.UpdateWorkspaceLockedDeletingAtParams) error { +func (q *FakeQuerier) UpdateWorkspaceLockedDeletingAt(_ context.Context, arg database.UpdateWorkspaceLockedDeletingAtParams) (database.Workspace, error) { if err := validateDatabaseType(arg); err != nil { - return err + return database.Workspace{}, err } q.mutex.Lock() defer q.mutex.Unlock() @@ -5106,7 +5106,7 @@ func (q *FakeQuerier) UpdateWorkspaceLockedDeletingAt(_ context.Context, arg dat } } if template.ID == uuid.Nil { - return xerrors.Errorf("unable to find workspace template") + return database.Workspace{}, xerrors.Errorf("unable to find workspace template") } if template.LockedTTL > 0 { workspace.DeletingAt = sql.NullTime{ @@ -5116,9 +5116,9 @@ func (q *FakeQuerier) UpdateWorkspaceLockedDeletingAt(_ context.Context, arg dat } } q.workspaces[index] = workspace - return nil + return workspace, nil } - return sql.ErrNoRows + return database.Workspace{}, sql.ErrNoRows } func (q *FakeQuerier) UpdateWorkspaceProxy(_ context.Context, arg database.UpdateWorkspaceProxyParams) (database.WorkspaceProxy, error) { diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index aca079e80818e..aa3ffd087283d 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -1537,11 +1537,11 @@ func (m metricsStore) UpdateWorkspaceLastUsedAt(ctx context.Context, arg databas return err } -func (m metricsStore) UpdateWorkspaceLockedDeletingAt(ctx context.Context, arg database.UpdateWorkspaceLockedDeletingAtParams) error { +func (m metricsStore) UpdateWorkspaceLockedDeletingAt(ctx context.Context, arg database.UpdateWorkspaceLockedDeletingAtParams) (database.Workspace, error) { start := time.Now() - r0 := m.s.UpdateWorkspaceLockedDeletingAt(ctx, arg) + ws, r0 := m.s.UpdateWorkspaceLockedDeletingAt(ctx, arg) m.queryLatencies.WithLabelValues("UpdateWorkspaceLockedDeletingAt").Observe(time.Since(start).Seconds()) - return r0 + return ws, r0 } func (m metricsStore) UpdateWorkspaceProxy(ctx context.Context, arg database.UpdateWorkspaceProxyParams) (database.WorkspaceProxy, error) { diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 2e9d5042adbca..2144347b16374 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -3232,11 +3232,12 @@ func (mr *MockStoreMockRecorder) UpdateWorkspaceLastUsedAt(arg0, arg1 interface{ } // UpdateWorkspaceLockedDeletingAt mocks base method. -func (m *MockStore) UpdateWorkspaceLockedDeletingAt(arg0 context.Context, arg1 database.UpdateWorkspaceLockedDeletingAtParams) error { +func (m *MockStore) UpdateWorkspaceLockedDeletingAt(arg0 context.Context, arg1 database.UpdateWorkspaceLockedDeletingAtParams) (database.Workspace, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UpdateWorkspaceLockedDeletingAt", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 + ret0, _ := ret[0].(database.Workspace) + ret1, _ := ret[1].(error) + return ret0, ret1 } // UpdateWorkspaceLockedDeletingAt indicates an expected call of UpdateWorkspaceLockedDeletingAt. diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 471b0e1619d2f..459e42eb8c281 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -268,7 +268,7 @@ type sqlcQuerier interface { UpdateWorkspaceBuildCostByID(ctx context.Context, arg UpdateWorkspaceBuildCostByIDParams) error UpdateWorkspaceDeletedByID(ctx context.Context, arg UpdateWorkspaceDeletedByIDParams) error UpdateWorkspaceLastUsedAt(ctx context.Context, arg UpdateWorkspaceLastUsedAtParams) error - UpdateWorkspaceLockedDeletingAt(ctx context.Context, arg UpdateWorkspaceLockedDeletingAtParams) error + UpdateWorkspaceLockedDeletingAt(ctx context.Context, arg UpdateWorkspaceLockedDeletingAtParams) (Workspace, error) // This allows editing the properties of a workspace proxy. UpdateWorkspaceProxy(ctx context.Context, arg UpdateWorkspaceProxyParams) (WorkspaceProxy, error) UpdateWorkspaceProxyDeleted(ctx context.Context, arg UpdateWorkspaceProxyDeletedParams) error diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index a586fb105790f..ffc852b0ed94f 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -8698,11 +8698,13 @@ WHERE ) > 0 ELSE true END - -- Filter by locked workspaces. + -- Filter by locked workspaces. By default we do not return locked + -- workspaces since they are considered soft-deleted. AND CASE WHEN $10 :: timestamptz > '0001-01-01 00:00:00+00'::timestamptz THEN locked_at IS NOT NULL AND locked_at >= $10 - ELSE true + ELSE + locked_at IS NULL END -- Authorize Filter clause will be injected below in GetAuthorizedWorkspaces -- @authorize_filter @@ -9071,7 +9073,7 @@ func (q *sqlQuerier) UpdateWorkspaceLastUsedAt(ctx context.Context, arg UpdateWo return err } -const updateWorkspaceLockedDeletingAt = `-- name: UpdateWorkspaceLockedDeletingAt :exec +const updateWorkspaceLockedDeletingAt = `-- name: UpdateWorkspaceLockedDeletingAt :one UPDATE workspaces SET @@ -9088,6 +9090,7 @@ WHERE workspaces.template_id = templates.id AND workspaces.id = $1 +RETURNING workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.locked_at, workspaces.deleting_at ` type UpdateWorkspaceLockedDeletingAtParams struct { @@ -9095,9 +9098,25 @@ type UpdateWorkspaceLockedDeletingAtParams struct { LockedAt sql.NullTime `db:"locked_at" json:"locked_at"` } -func (q *sqlQuerier) UpdateWorkspaceLockedDeletingAt(ctx context.Context, arg UpdateWorkspaceLockedDeletingAtParams) error { - _, err := q.db.ExecContext(ctx, updateWorkspaceLockedDeletingAt, arg.ID, arg.LockedAt) - return err +func (q *sqlQuerier) UpdateWorkspaceLockedDeletingAt(ctx context.Context, arg UpdateWorkspaceLockedDeletingAtParams) (Workspace, error) { + row := q.db.QueryRowContext(ctx, updateWorkspaceLockedDeletingAt, arg.ID, arg.LockedAt) + var i Workspace + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.OwnerID, + &i.OrganizationID, + &i.TemplateID, + &i.Deleted, + &i.Name, + &i.AutostartSchedule, + &i.Ttl, + &i.LastUsedAt, + &i.LockedAt, + &i.DeletingAt, + ) + return i, err } const updateWorkspaceTTL = `-- name: UpdateWorkspaceTTL :exec diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql index c6d5dfed9d8e2..9dd8aa00b5f55 100644 --- a/coderd/database/queries/workspaces.sql +++ b/coderd/database/queries/workspaces.sql @@ -482,7 +482,7 @@ WHERE ) ) AND workspaces.deleted = 'false'; --- name: UpdateWorkspaceLockedDeletingAt :exec +-- name: UpdateWorkspaceLockedDeletingAt :one UPDATE workspaces SET @@ -498,7 +498,8 @@ FROM WHERE workspaces.template_id = templates.id AND - workspaces.id = $1; + workspaces.id = $1 +RETURNING workspaces.*; -- name: UpdateWorkspacesDeletingAtByTemplateID :exec UPDATE diff --git a/coderd/workspaces.go b/coderd/workspaces.go index b8cc33ac8cf9e..b6502130a2224 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -767,7 +767,7 @@ func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) { // @Tags Workspaces // @Param workspace path string true "Workspace ID" format(uuid) // @Param request body codersdk.UpdateWorkspaceLock true "Lock or unlock a workspace" -// @Success 200 {object} codersdk.Response +// @Success 200 {object} codersdk.Workspace // @Router /workspaces/{workspace}/lock [put] func (api *API) putWorkspaceLock(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -778,9 +778,6 @@ func (api *API) putWorkspaceLock(rw http.ResponseWriter, r *http.Request) { return } - code := http.StatusOK - resp := codersdk.Response{} - // If the workspace is already in the desired state do nothing! if workspace.LockedAt.Valid == req.Lock { httpapi.Write(ctx, rw, http.StatusNotModified, codersdk.Response{ @@ -796,7 +793,7 @@ func (api *API) putWorkspaceLock(rw http.ResponseWriter, r *http.Request) { lockedAt.Time = database.Now() } - err := api.Database.UpdateWorkspaceLockedDeletingAt(ctx, database.UpdateWorkspaceLockedDeletingAtParams{ + workspace, err := api.Database.UpdateWorkspaceLockedDeletingAt(ctx, database.UpdateWorkspaceLockedDeletingAtParams{ ID: workspace.ID, LockedAt: lockedAt, }) @@ -808,10 +805,21 @@ func (api *API) putWorkspaceLock(rw http.ResponseWriter, r *http.Request) { return } - // TODO should we kick off a build to stop the workspace if it's started - // from this endpoint? I'm leaning no to keep things simple and kick - // the responsibility back to the client. - httpapi.Write(ctx, rw, code, resp) + data, err := api.workspaceData(ctx, []database.Workspace{workspace}) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching workspace resources.", + Detail: err.Error(), + }) + return + } + + httpapi.Write(ctx, rw, http.StatusOK, convertWorkspace( + workspace, + data.builds[0], + data.templates[0], + findUser(workspace.OwnerID, data.users), + )) } // @Summary Extend workspace deadline by ID diff --git a/docs/api/workspaces.md b/docs/api/workspaces.md index 42f317616e55a..e05fb66a36104 100644 --- a/docs/api/workspaces.md +++ b/docs/api/workspaces.md @@ -931,22 +931,164 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/lock \ ```json { - "detail": "string", - "message": "string", - "validations": [ - { - "detail": "string", - "field": "string" - } - ] + "autostart_schedule": "string", + "created_at": "2019-08-24T14:15:22Z", + "deleting_at": "2019-08-24T14:15:22Z", + "health": { + "failing_agents": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], + "healthy": false + }, + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "last_used_at": "2019-08-24T14:15:22Z", + "latest_build": { + "build_number": 0, + "created_at": "2019-08-24T14:15:22Z", + "daily_cost": 0, + "deadline": "2019-08-24T14:15:22Z", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3", + "initiator_name": "string", + "job": { + "canceled_at": "2019-08-24T14:15:22Z", + "completed_at": "2019-08-24T14:15:22Z", + "created_at": "2019-08-24T14:15:22Z", + "error": "string", + "error_code": "MISSING_TEMPLATE_PARAMETER", + "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "queue_position": 0, + "queue_size": 0, + "started_at": "2019-08-24T14:15:22Z", + "status": "pending", + "tags": { + "property1": "string", + "property2": "string" + }, + "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b" + }, + "max_deadline": "2019-08-24T14:15:22Z", + "reason": "initiator", + "resources": [ + { + "agents": [ + { + "apps": [ + { + "command": "string", + "display_name": "string", + "external": true, + "health": "disabled", + "healthcheck": { + "interval": 0, + "threshold": 0, + "url": "string" + }, + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "sharing_level": "owner", + "slug": "string", + "subdomain": true, + "url": "string" + } + ], + "architecture": "string", + "connection_timeout_seconds": 0, + "created_at": "2019-08-24T14:15:22Z", + "directory": "string", + "disconnected_at": "2019-08-24T14:15:22Z", + "environment_variables": { + "property1": "string", + "property2": "string" + }, + "expanded_directory": "string", + "first_connected_at": "2019-08-24T14:15:22Z", + "health": { + "healthy": false, + "reason": "agent has lost connection" + }, + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "instance_id": "string", + "last_connected_at": "2019-08-24T14:15:22Z", + "latency": { + "property1": { + "latency_ms": 0, + "preferred": true + }, + "property2": { + "latency_ms": 0, + "preferred": true + } + }, + "lifecycle_state": "created", + "login_before_ready": true, + "name": "string", + "operating_system": "string", + "ready_at": "2019-08-24T14:15:22Z", + "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", + "shutdown_script": "string", + "shutdown_script_timeout_seconds": 0, + "started_at": "2019-08-24T14:15:22Z", + "startup_logs_length": 0, + "startup_logs_overflowed": true, + "startup_script": "string", + "startup_script_behavior": "blocking", + "startup_script_timeout_seconds": 0, + "status": "connecting", + "subsystem": "envbox", + "troubleshooting_url": "string", + "updated_at": "2019-08-24T14:15:22Z", + "version": "string" + } + ], + "created_at": "2019-08-24T14:15:22Z", + "daily_cost": 0, + "hide": true, + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "job_id": "453bd7d7-5355-4d6d-a38e-d9e7eb218c3f", + "metadata": [ + { + "key": "string", + "sensitive": true, + "value": "string" + } + ], + "name": "string", + "type": "string", + "workspace_transition": "start" + } + ], + "status": "pending", + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "template_version_name": "string", + "transition": "start", + "updated_at": "2019-08-24T14:15:22Z", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", + "workspace_name": "string", + "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", + "workspace_owner_name": "string" + }, + "locked_at": "2019-08-24T14:15:22Z", + "name": "string", + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", + "outdated": true, + "owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05", + "owner_name": "string", + "template_allow_user_cancel_workspace_jobs": true, + "template_display_name": "string", + "template_icon": "string", + "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", + "template_name": "string", + "ttl_ms": 0, + "updated_at": "2019-08-24T14:15:22Z" } ``` ### Responses -| Status | Meaning | Description | Schema | -| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------ | -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.Response](schemas.md#codersdkresponse) | +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | -------------------------------------------------- | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.Workspace](schemas.md#codersdkworkspace) | To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 8c908c185d800..b7b0801277b1c 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -557,7 +557,7 @@ export const cancelWorkspaceBuild = async ( export const updateWorkspaceLock = async ( workspaceId: string, lock: boolean, -): Promise => { +): Promise => { const data: TypesGen.UpdateWorkspaceLock = { lock: lock, } diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index f1ca9ba71e1dc..3fd3124399da3 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -176,15 +176,7 @@ export const WorkspaceReadyPage = ({ handleChangeVersion={() => { setChangeVersionDialogOpen(true) }} - handleUnlock={() => { - updateWorkspaceLock(workspace.id, false) - .then(() => { - window.location.reload() - }) - .catch((error) => { - console.log("ERROR: ", error) - }) - }} + handleUnlock={() => workspaceSend({ type: "UNLOCK" })} resources={workspace.latest_build.resources} builds={builds} canUpdateWorkspace={canUpdateWorkspace} diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index c3010526ed71c..729a717a84ce1 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -96,6 +96,7 @@ export type WorkspaceEvent = | { type: "INCREASE_DEADLINE"; hours: number } | { type: "DECREASE_DEADLINE"; hours: number } | { type: "RETRY_BUILD" } + | { type: "UNLOCK" } export const checks = { readWorkspace: "readWorkspace", @@ -170,6 +171,9 @@ export const workspaceMachine = createMachine( cancelWorkspace: { data: Types.Message } + unlockWorkspace: { + data: Types.Message + } listening: { data: TypesGen.ServerSentEvent } @@ -260,6 +264,7 @@ export const workspaceMachine = createMachine( actions: ["enableDebugMode"], }, ], + UNLOCK: "requestingUnlock", }, }, askingDelete: { @@ -405,6 +410,18 @@ export const workspaceMachine = createMachine( ], }, }, + requestingUnlock: { + entry: ["clearBuildError"], + invoke: { + src: "unlockWorkspace", + id: "unlockWorkspace", + onDone: "idle", + onError: { + target: "idle", + actions: ["displayUnlockError"], + }, + }, + }, }, }, timeline: { @@ -559,7 +576,10 @@ export const workspaceMachine = createMachine( ) displayError(message) }, - + displayUnlockError: (_, { data }) => { + const message = getErrorMessage(data, "Error unlocking workspace.") + displayError(message) + }, assignMissedParameters: assign({ missedParameters: (_, { data }) => { if (!(data instanceof API.MissingBuildParameters)) { @@ -675,6 +695,18 @@ export const workspaceMachine = createMachine( throw Error("Cannot cancel workspace without build id") } }, + unlockWorkspace: (context) => async (send) => { + if (context.workspace) { + const unlockWorkspacePromise = await API.updateWorkspaceLock( + context.workspace.id, + false, + ) + send({ type: "REFRESH_WORKSPACE", data: unlockWorkspacePromise }) + return unlockWorkspacePromise + } else { + throw Error("Cannot unlock workspace without workspace id") + } + }, listening: (context) => (send) => { if (!context.eventSource) { send({ type: "EVENT_SOURCE_ERROR", error: "error initializing sse" }) From 3c850ad8818dc439c1abdfa54f47a4b4cdebf5b8 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Tue, 1 Aug 2023 23:36:47 +0000 Subject: [PATCH 05/15] fix tests --- coderd/database/dbfake/dbfake.go | 10 ++++++++++ coderd/database/queries.sql.go | 2 +- coderd/searchquery/search.go | 5 +++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go index 34875c68b8e27..ca4617beebf18 100644 --- a/coderd/database/dbfake/dbfake.go +++ b/coderd/database/dbfake/dbfake.go @@ -5562,6 +5562,16 @@ func (q *FakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database. } } + // We omit locked workspaces by default. + if arg.LockedAt.IsZero() && workspace.LockedAt.Valid { + continue + } + + // Filter out workspaces that are locked after the timestamp. + if !arg.LockedAt.IsZero() && workspace.LockedAt.Time.Before(arg.LockedAt) { + continue + } + if len(arg.TemplateIDs) > 0 { match := false for _, id := range arg.TemplateIDs { diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index ffc852b0ed94f..04e3363f33d13 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -8702,7 +8702,7 @@ WHERE -- workspaces since they are considered soft-deleted. AND CASE WHEN $10 :: timestamptz > '0001-01-01 00:00:00+00'::timestamptz THEN - locked_at IS NOT NULL AND locked_at >= $10 + locked_at IS NOT NULL AND locked_at <= $10 ELSE locked_at IS NULL END diff --git a/coderd/searchquery/search.go b/coderd/searchquery/search.go index 3deafefd25193..3518f0744947e 100644 --- a/coderd/searchquery/search.go +++ b/coderd/searchquery/search.go @@ -118,6 +118,11 @@ func Workspaces(query string, page codersdk.Pagination, agentInactiveDisconnectT if _, ok := values["deleting_by"]; ok { postFilter.DeletingBy = ptr.Ref(parser.Time(values, time.Time{}, "deleting_by", "2006-01-02")) + // We want to make sure to grab locked workspaces since they + // are omitted by default. + if filter.LockedAt.IsZero() { + filter.LockedAt = time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC) + } } parser.ErrorExcessParams(values) From 4ddb62fa2f09a5fffa9238e3cfe1d605e2e19c65 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Tue, 1 Aug 2023 23:57:46 +0000 Subject: [PATCH 06/15] fix returned workspaces --- .../ImpendingDeletionBanner.tsx | 1 - .../TemplateScheduleForm.tsx | 2 +- .../pages/WorkspacesPage/WorkspacesPage.tsx | 40 ++++++++++++++++++- .../WorkspacesPage/WorkspacesPageView.tsx | 25 ++++-------- 4 files changed, 47 insertions(+), 21 deletions(-) diff --git a/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx b/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx index f5766be168012..d73dc79a73baa 100644 --- a/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx +++ b/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx @@ -1,5 +1,4 @@ import { Workspace } from "api/typesGenerated" -import { displayImpendingDeletion } from "./utils" import { useDashboard } from "components/Dashboard/DashboardProvider" import { Alert } from "components/Alert/Alert" import { formatDistanceToNow, differenceInDays, add, format } from "date-fns" diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx index e23c83794bd6f..d159a4d3d7d07 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx @@ -382,7 +382,7 @@ export const TemplateScheduleForm: FC = ({ diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index 440fa1c8fe81c..d72e518fd527c 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -1,5 +1,7 @@ import { usePagination } from "hooks/usePagination" -import { FC } from "react" +import { Workspace } from "api/typesGenerated" +import { useDashboard } from "components/Dashboard/DashboardProvider" +import { FC, useEffect, useState } from "react" import { Helmet } from "react-helmet-async" import { pageTitle } from "utils/page" import { useWorkspacesData, useWorkspaceUpdate } from "./data" @@ -9,9 +11,11 @@ import { useTemplateFilterMenu, useStatusFilterMenu } from "./filter/menus" import { useSearchParams } from "react-router-dom" import { useFilter } from "components/Filter/filter" import { useUserFilterMenu } from "components/Filter/UserFilter" +import { getWorkspaces, updateWorkspaceVersion } from "api/api" const WorkspacesPage: FC = () => { const orgId = useOrganizationId() + const [lockedWorkspaces, setLockedWorkspaces] = useState([]) // If we use a useSearchParams for each hook, the values will not be in sync. // So we have to use a single one, centralizing the values, and pass it to // each hook. @@ -28,6 +32,39 @@ const WorkspacesPage: FC = () => { ...pagination, query: filter.query, }) + + const { entitlements, experiments } = useDashboard() + const allowAdvancedScheduling = + entitlements.features["advanced_template_scheduling"].enabled + // This check can be removed when https://github.com/coder/coder/milestone/19 + // is merged up + const allowWorkspaceActions = experiments.includes("workspace_actions") + + if (allowWorkspaceActions && allowAdvancedScheduling) { + const includesLocked = filter.query.includes("locked_at") + const lockedQuery = includesLocked + ? filter.query + : filter.query + " locked_at:1970-01-01" + + useEffect(() => { + if (includesLocked && data) { + setLockedWorkspaces(data.workspaces) + } else { + getWorkspaces({ q: lockedQuery }) + .then((resp) => { + setLockedWorkspaces(resp.workspaces) + }) + .catch((err) => { + console.log(err) + }) + } + }) + } else { + // If the experiment isn't included then we'll pretend + // like locked workspaces don't exist. + setLockedWorkspaces([]) + } + const updateWorkspace = useWorkspaceUpdate(queryKey) const permissions = usePermissions() const canFilterByUser = permissions.viewDeploymentValues @@ -57,6 +94,7 @@ const WorkspacesPage: FC = () => { page: number @@ -45,6 +46,7 @@ export const WorkspacesPageView: FC< React.PropsWithChildren > = ({ workspaces, + lockedWorkspaces, error, limit, count, @@ -55,25 +57,12 @@ export const WorkspacesPageView: FC< }) => { const { saveLocal, getLocal } = useLocalStorage() - const workspacesDeletionScheduled = workspaces + const workspacesDeletionScheduled = lockedWorkspaces ?.filter((workspace) => workspace.deleting_at) .map((workspace) => workspace.id) const hasLockedWorkspace = - workspaces?.find((workspace) => workspace.locked_at) != undefined - - const hasLockedFilter = (): boolean => { - for (const key in filterProps.filter.values) { - if (key == "locked_at") { - return true - } - } - return false - } - - const shownWorkspaces = !hasLockedFilter() - ? workspaces?.filter((workspace) => !workspace.locked_at) - : workspaces + lockedWorkspaces !== undefined && lockedWorkspaces.length > 0 return ( @@ -100,7 +89,7 @@ export const WorkspacesPageView: FC< {/* determines its own visibility */} saveLocal( @@ -116,13 +105,13 @@ export const WorkspacesPageView: FC< Date: Wed, 2 Aug 2023 02:36:09 +0000 Subject: [PATCH 07/15] make gen --- coderd/database/queries.sql.go | 2 +- docs/api/workspaces.md | 4 ++-- site/src/components/WorkspaceActions/WorkspaceActions.tsx | 6 +----- .../WorkspaceDeletion/ImpendingDeletionBanner.tsx | 2 +- site/src/components/WorkspaceDeletion/utils.ts | 1 - .../TemplateScheduleForm/useWorkspacesToBeDeleted.ts | 2 +- site/src/pages/WorkspacesPage/WorkspacesPage.tsx | 6 +++--- site/src/pages/WorkspacesPage/WorkspacesPageView.tsx | 3 +-- 8 files changed, 10 insertions(+), 16 deletions(-) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 494594b25a699..8ede237618e4f 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -8832,7 +8832,7 @@ WHERE -- workspaces since they are considered soft-deleted. AND CASE WHEN $10 :: timestamptz > '0001-01-01 00:00:00+00'::timestamptz THEN - locked_at IS NOT NULL AND locked_at <= $10 + locked_at IS NOT NULL AND locked_at >= $10 ELSE locked_at IS NULL END diff --git a/docs/api/workspaces.md b/docs/api/workspaces.md index ba72c70bd4afe..6cfb468f6af50 100644 --- a/docs/api/workspaces.md +++ b/docs/api/workspaces.md @@ -1021,6 +1021,8 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/lock \ }, "lifecycle_state": "created", "login_before_ready": true, + "logs_length": 0, + "logs_overflowed": true, "name": "string", "operating_system": "string", "ready_at": "2019-08-24T14:15:22Z", @@ -1028,8 +1030,6 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/lock \ "shutdown_script": "string", "shutdown_script_timeout_seconds": 0, "started_at": "2019-08-24T14:15:22Z", - "startup_logs_length": 0, - "startup_logs_overflowed": true, "startup_script": "string", "startup_script_behavior": "blocking", "startup_script_timeout_seconds": 0, diff --git a/site/src/components/WorkspaceActions/WorkspaceActions.tsx b/site/src/components/WorkspaceActions/WorkspaceActions.tsx index 31568917b69ba..409c77e673eab 100644 --- a/site/src/components/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/components/WorkspaceActions/WorkspaceActions.tsx @@ -3,11 +3,7 @@ import Menu from "@mui/material/Menu" import { makeStyles } from "@mui/styles" import MoreVertOutlined from "@mui/icons-material/MoreVertOutlined" import { FC, Fragment, ReactNode, useRef, useState } from "react" -import { - Workspace, - WorkspaceStatus, - WorkspaceBuildParameter, -} from "api/typesGenerated" +import { Workspace, WorkspaceBuildParameter } from "api/typesGenerated" import { ActionLoadingButton, CancelButton, diff --git a/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx b/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx index d73dc79a73baa..93893c42a4ab5 100644 --- a/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx +++ b/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx @@ -61,7 +61,7 @@ export const LockedWorkspaceBanner = ({ } const alertText = (): string => { - if (workspaces.length == 1) { + if (workspaces.length === 1) { if ( hasDeletionScheduledWorkspaces && hasDeletionScheduledWorkspaces.deleting_at && diff --git a/site/src/components/WorkspaceDeletion/utils.ts b/site/src/components/WorkspaceDeletion/utils.ts index 704fd750ea114..049c14332dd52 100644 --- a/site/src/components/WorkspaceDeletion/utils.ts +++ b/site/src/components/WorkspaceDeletion/utils.ts @@ -16,7 +16,6 @@ export const displayLockedWorkspace = ( allowAdvancedScheduling: boolean, allowWorkspaceActions: boolean, ) => { - const today = new Date() if ( !workspace.locked_at || !allowAdvancedScheduling || diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/useWorkspacesToBeDeleted.ts b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/useWorkspacesToBeDeleted.ts index e92d01e98b38d..9836c5b273b3f 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/useWorkspacesToBeDeleted.ts +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/useWorkspacesToBeDeleted.ts @@ -1,6 +1,6 @@ import { useQuery } from "@tanstack/react-query" import { getWorkspaces } from "api/api" -import { compareAsc, add, endOfToday } from "date-fns" +import { compareAsc } from "date-fns" import { Workspace, Template } from "api/typesGenerated" import { TemplateScheduleFormValues } from "./formHelpers" diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index 51f72125e2f53..e20dd10d673df 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -14,7 +14,6 @@ import { useUserFilterMenu } from "components/Filter/UserFilter" import { getWorkspaces, updateWorkspaceVersion } from "api/api" const WorkspacesPage: FC = () => { - const orgId = useOrganizationId() const [lockedWorkspaces, setLockedWorkspaces] = useState([]) // If we use a useSearchParams for each hook, the values will not be in sync. // So we have to use a single one, centralizing the values, and pass it to @@ -49,10 +48,10 @@ const WorkspacesPage: FC = () => { setLockedWorkspaces(resp.workspaces) }) .catch((err) => { - console.log(err) + // TODO? }) } - }) + }, [includesLocked, data, lockedQuery]) } else { // If the experiment isn't included then we'll pretend // like locked workspaces don't exist. @@ -69,6 +68,7 @@ const WorkspacesPage: FC = () => { { - const { saveLocal, getLocal } = useLocalStorage() + const { saveLocal } = useLocalStorage() const workspacesDeletionScheduled = lockedWorkspaces ?.filter((workspace) => workspace.deleting_at) From 8d458b602f9c676408d3bb2e79bdd4ca9170768b Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Wed, 2 Aug 2023 02:56:37 +0000 Subject: [PATCH 08/15] fix test --- .../components/Dashboard/DashboardProvider.tsx | 10 ++++++++++ .../TemplateSchedulePage.test.tsx | 4 ++-- site/src/pages/WorkspacesPage/WorkspacesPage.tsx | 15 ++++++--------- site/src/pages/WorkspacesPage/filter/filter.tsx | 13 +++++++++---- 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/site/src/components/Dashboard/DashboardProvider.tsx b/site/src/components/Dashboard/DashboardProvider.tsx index fd9065abdb27a..ced621e98df83 100644 --- a/site/src/components/Dashboard/DashboardProvider.tsx +++ b/site/src/components/Dashboard/DashboardProvider.tsx @@ -87,3 +87,13 @@ export const useDashboard = (): DashboardProviderValue => { return context } + +export const isWorkspaceActionsEnabled = (): boolean => { + const { entitlements, experiments } = useDashboard() + const allowAdvancedScheduling = + entitlements.features["advanced_template_scheduling"].enabled + // This check can be removed when https://github.com/coder/coder/milestone/19 + // is merged up + const allowWorkspaceActions = experiments.includes("workspace_actions") + return allowWorkspaceActions && allowAdvancedScheduling +} diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx index 5b37f01f5b8ce..c1d10a6955a42 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx @@ -63,12 +63,12 @@ const fillAndSubmitForm = async ({ await user.type(failureTtlField, failure_ttl_ms.toString()) const inactivityTtlField = screen.getByRole("checkbox", { - name: /Inactivity Cleanup/i, + name: /Inactivity TTL/i, }) await user.type(inactivityTtlField, inactivity_ttl_ms.toString()) const lockedTtlField = screen.getByRole("checkbox", { - name: /Locked Cleanup/i, + name: /Locked TTL/i, }) await user.type(lockedTtlField, locked_ttl_ms.toString()) diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index e20dd10d673df..47733c6b6a91e 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -1,6 +1,6 @@ import { usePagination } from "hooks/usePagination" import { Workspace } from "api/typesGenerated" -import { useDashboard } from "components/Dashboard/DashboardProvider" +import { isWorkspaceActionsEnabled } from "components/Dashboard/DashboardProvider" import { FC, useEffect, useState } from "react" import { Helmet } from "react-helmet-async" import { pageTitle } from "utils/page" @@ -26,14 +26,11 @@ const WorkspacesPage: FC = () => { query: filterProps.filter.query, }) - const { entitlements, experiments } = useDashboard() - const allowAdvancedScheduling = - entitlements.features["advanced_template_scheduling"].enabled - // This check can be removed when https://github.com/coder/coder/milestone/19 - // is merged up - const allowWorkspaceActions = experiments.includes("workspace_actions") - - if (allowWorkspaceActions && allowAdvancedScheduling) { + // If workspace actions are enabled we need to fetch the locked + // workspaces as well. This lets us determine whether we should + // show a banner to the user indicating that some of their workspaces + // are at risk of being deleted. + if (isWorkspaceActionsEnabled()) { const includesLocked = filterProps.filter.query.includes("locked_at") const lockedQuery = includesLocked ? filterProps.filter.query diff --git a/site/src/pages/WorkspacesPage/filter/filter.tsx b/site/src/pages/WorkspacesPage/filter/filter.tsx index 3811fbf576744..78fb8621410eb 100644 --- a/site/src/pages/WorkspacesPage/filter/filter.tsx +++ b/site/src/pages/WorkspacesPage/filter/filter.tsx @@ -1,5 +1,6 @@ import { FC } from "react" import Box from "@mui/material/Box" +import { isWorkspaceActionsEnabled } from "components/Dashboard/DashboardProvider" import { Avatar, AvatarProps } from "components/Avatar/Avatar" import { Palette, PaletteColor } from "@mui/material/styles" import { TemplateFilterMenu, StatusFilterMenu } from "./menus" @@ -28,10 +29,6 @@ const PRESET_FILTERS = [ query: workspaceFilterQuery.failed, name: "Failed workspaces", }, - { - query: workspaceFilterQuery.locked, - name: "Locked workspaces", - }, ] export const WorkspacesFilter = ({ @@ -47,6 +44,14 @@ export const WorkspacesFilter = ({ status: StatusFilterMenu } }) => { + let presets = PRESET_FILTERS + if (isWorkspaceActionsEnabled()) { + presets.push({ + query: workspaceFilterQuery.locked, + name: "Locked workspaces", + }) + } + return ( Date: Wed, 2 Aug 2023 03:03:22 +0000 Subject: [PATCH 09/15] remove some redundant code --- .../ImpendingDeletionBadge.tsx | 19 ++-------------- .../ImpendingDeletionBanner.tsx | 15 +++++-------- .../src/components/WorkspaceDeletion/utils.ts | 22 ------------------- 3 files changed, 7 insertions(+), 49 deletions(-) diff --git a/site/src/components/WorkspaceDeletion/ImpendingDeletionBadge.tsx b/site/src/components/WorkspaceDeletion/ImpendingDeletionBadge.tsx index 96b29a208e430..08306478fa90f 100644 --- a/site/src/components/WorkspaceDeletion/ImpendingDeletionBadge.tsx +++ b/site/src/components/WorkspaceDeletion/ImpendingDeletionBadge.tsx @@ -1,6 +1,5 @@ import { Workspace } from "api/typesGenerated" -import { displayLockedWorkspace } from "./utils" -import { useDashboard } from "components/Dashboard/DashboardProvider" +import { isWorkspaceActionsEnabled } from "components/Dashboard/DashboardProvider" import { Pill } from "components/Pill/Pill" import LockIcon from "@mui/icons-material/Lock" @@ -9,21 +8,7 @@ export const LockedBadge = ({ }: { workspace: Workspace }): JSX.Element | null => { - const { entitlements, experiments } = useDashboard() - const allowAdvancedScheduling = - entitlements.features["advanced_template_scheduling"].enabled - // This check can be removed when https://github.com/coder/coder/milestone/19 - // is merged up - const allowWorkspaceActions = experiments.includes("workspace_actions") - // return null - - if ( - !displayLockedWorkspace( - workspace, - allowAdvancedScheduling, - allowWorkspaceActions, - ) - ) { + if (!workspace.locked_at || !isWorkspaceActionsEnabled()) { return null } diff --git a/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx b/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx index 93893c42a4ab5..f0fdafd1418eb 100644 --- a/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx +++ b/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx @@ -1,5 +1,8 @@ import { Workspace } from "api/typesGenerated" -import { useDashboard } from "components/Dashboard/DashboardProvider" +import { + isWorkspaceActionsEnabled, + useDashboard, +} from "components/Dashboard/DashboardProvider" import { Alert } from "components/Alert/Alert" import { formatDistanceToNow, differenceInDays, add, format } from "date-fns" import Link from "@mui/material/Link" @@ -21,13 +24,6 @@ export const LockedWorkspaceBanner = ({ shouldRedisplayBanner: boolean count?: Count }): JSX.Element | null => { - const { entitlements, experiments } = useDashboard() - const allowAdvancedScheduling = - entitlements.features["advanced_template_scheduling"].enabled - // This check can be removed when https://github.com/coder/coder/milestone/19 - // is merged up - const allowWorkspaceActions = experiments.includes("workspace_actions") - if (!workspaces) { return null } @@ -45,8 +41,7 @@ export const LockedWorkspaceBanner = ({ // Banners should be redisplayed after dismissal when additional workspaces are newly scheduled for deletion !shouldRedisplayBanner) && // Only show this if the experiment is included. - allowWorkspaceActions && - allowAdvancedScheduling + isWorkspaceActionsEnabled() ) { return null } diff --git a/site/src/components/WorkspaceDeletion/utils.ts b/site/src/components/WorkspaceDeletion/utils.ts index 049c14332dd52..745bd48ad8941 100644 --- a/site/src/components/WorkspaceDeletion/utils.ts +++ b/site/src/components/WorkspaceDeletion/utils.ts @@ -4,28 +4,6 @@ import { Workspace } from "api/typesGenerated" // 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 displayLockedWorkspace = ( - workspace: Workspace, - allowAdvancedScheduling: boolean, - allowWorkspaceActions: boolean, -) => { - if ( - !workspace.locked_at || - !allowAdvancedScheduling || - !allowWorkspaceActions - ) { - return false - } - return workspace.locked_at -} - /** * Returns a boolean indicating if an impending deletion indicator should be * displayed in the UI. Impending deletions are configured by setting the From 5d3fb7d232f897d7a44b72477cfbe59c2c4531a4 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Wed, 2 Aug 2023 03:08:51 +0000 Subject: [PATCH 10/15] try to fix linting --- site/src/components/Dashboard/DashboardProvider.tsx | 2 +- .../WorkspaceDeletion/ImpendingDeletionBadge.tsx | 4 ++-- .../WorkspaceDeletion/ImpendingDeletionBanner.tsx | 9 +++------ .../TemplateScheduleForm/TemplateScheduleForm.tsx | 2 -- site/src/pages/WorkspacesPage/WorkspacesPage.tsx | 4 ++-- site/src/pages/WorkspacesPage/filter/filter.tsx | 8 ++++---- site/src/utils/workspace.tsx | 1 - 7 files changed, 12 insertions(+), 18 deletions(-) diff --git a/site/src/components/Dashboard/DashboardProvider.tsx b/site/src/components/Dashboard/DashboardProvider.tsx index ced621e98df83..ed26b64ffc481 100644 --- a/site/src/components/Dashboard/DashboardProvider.tsx +++ b/site/src/components/Dashboard/DashboardProvider.tsx @@ -88,7 +88,7 @@ export const useDashboard = (): DashboardProviderValue => { return context } -export const isWorkspaceActionsEnabled = (): boolean => { +export const useIsWorkspaceActionsEnabled = (): boolean => { const { entitlements, experiments } = useDashboard() const allowAdvancedScheduling = entitlements.features["advanced_template_scheduling"].enabled diff --git a/site/src/components/WorkspaceDeletion/ImpendingDeletionBadge.tsx b/site/src/components/WorkspaceDeletion/ImpendingDeletionBadge.tsx index 08306478fa90f..59bcee81410da 100644 --- a/site/src/components/WorkspaceDeletion/ImpendingDeletionBadge.tsx +++ b/site/src/components/WorkspaceDeletion/ImpendingDeletionBadge.tsx @@ -1,5 +1,5 @@ import { Workspace } from "api/typesGenerated" -import { isWorkspaceActionsEnabled } from "components/Dashboard/DashboardProvider" +import { useIsWorkspaceActionsEnabled } from "components/Dashboard/DashboardProvider" import { Pill } from "components/Pill/Pill" import LockIcon from "@mui/icons-material/Lock" @@ -8,7 +8,7 @@ export const LockedBadge = ({ }: { workspace: Workspace }): JSX.Element | null => { - if (!workspace.locked_at || !isWorkspaceActionsEnabled()) { + if (!workspace.locked_at || !useIsWorkspaceActionsEnabled()) { return null } diff --git a/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx b/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx index f0fdafd1418eb..d506c93160a28 100644 --- a/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx +++ b/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx @@ -1,10 +1,7 @@ import { Workspace } from "api/typesGenerated" -import { - isWorkspaceActionsEnabled, - useDashboard, -} from "components/Dashboard/DashboardProvider" +import { useIsWorkspaceActionsEnabled } from "components/Dashboard/DashboardProvider" import { Alert } from "components/Alert/Alert" -import { formatDistanceToNow, differenceInDays, add, format } from "date-fns" +import { formatDistanceToNow } from "date-fns" import Link from "@mui/material/Link" import { Link as RouterLink } from "react-router-dom" @@ -41,7 +38,7 @@ export const LockedWorkspaceBanner = ({ // Banners should be redisplayed after dismissal when additional workspaces are newly scheduled for deletion !shouldRedisplayBanner) && // Only show this if the experiment is included. - isWorkspaceActionsEnabled() + useIsWorkspaceActionsEnabled() ) { return null } diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx index d159a4d3d7d07..8bfdeff8a0b27 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx @@ -123,8 +123,6 @@ export const TemplateScheduleForm: FC = ({ template, form.values, ) - console.log("workspaces to be deleted: ", workspacesToBeDeletedToday?.length) - console.log("workspaces to be locked: ", workspacesToBeLockedToday?.length) const [isInactivityDialogOpen, setIsInactivityDialogOpen] = useState(false) diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index 47733c6b6a91e..ecc74b8709250 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -1,6 +1,6 @@ import { usePagination } from "hooks/usePagination" import { Workspace } from "api/typesGenerated" -import { isWorkspaceActionsEnabled } from "components/Dashboard/DashboardProvider" +import { useIsWorkspaceActionsEnabled } from "components/Dashboard/DashboardProvider" import { FC, useEffect, useState } from "react" import { Helmet } from "react-helmet-async" import { pageTitle } from "utils/page" @@ -30,7 +30,7 @@ const WorkspacesPage: FC = () => { // workspaces as well. This lets us determine whether we should // show a banner to the user indicating that some of their workspaces // are at risk of being deleted. - if (isWorkspaceActionsEnabled()) { + if (useIsWorkspaceActionsEnabled()) { const includesLocked = filterProps.filter.query.includes("locked_at") const lockedQuery = includesLocked ? filterProps.filter.query diff --git a/site/src/pages/WorkspacesPage/filter/filter.tsx b/site/src/pages/WorkspacesPage/filter/filter.tsx index 78fb8621410eb..8d98c0b03182f 100644 --- a/site/src/pages/WorkspacesPage/filter/filter.tsx +++ b/site/src/pages/WorkspacesPage/filter/filter.tsx @@ -1,6 +1,6 @@ import { FC } from "react" import Box from "@mui/material/Box" -import { isWorkspaceActionsEnabled } from "components/Dashboard/DashboardProvider" +import { useIsWorkspaceActionsEnabled } from "components/Dashboard/DashboardProvider" import { Avatar, AvatarProps } from "components/Avatar/Avatar" import { Palette, PaletteColor } from "@mui/material/styles" import { TemplateFilterMenu, StatusFilterMenu } from "./menus" @@ -44,8 +44,8 @@ export const WorkspacesFilter = ({ status: StatusFilterMenu } }) => { - let presets = PRESET_FILTERS - if (isWorkspaceActionsEnabled()) { + const presets = [...PRESET_FILTERS] + if (useIsWorkspaceActionsEnabled()) { presets.push({ query: workspaceFilterQuery.locked, name: "Locked workspaces", @@ -54,7 +54,7 @@ export const WorkspacesFilter = ({ return ( Date: Wed, 2 Aug 2023 03:25:55 +0000 Subject: [PATCH 11/15] lint --- .../ImpendingDeletionBadge.tsx | 3 ++- .../ImpendingDeletionBanner.tsx | 3 ++- .../pages/WorkspacesPage/WorkspacesPage.tsx | 27 ++++++++++--------- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/site/src/components/WorkspaceDeletion/ImpendingDeletionBadge.tsx b/site/src/components/WorkspaceDeletion/ImpendingDeletionBadge.tsx index 59bcee81410da..dc09566fc69e9 100644 --- a/site/src/components/WorkspaceDeletion/ImpendingDeletionBadge.tsx +++ b/site/src/components/WorkspaceDeletion/ImpendingDeletionBadge.tsx @@ -8,7 +8,8 @@ export const LockedBadge = ({ }: { workspace: Workspace }): JSX.Element | null => { - if (!workspace.locked_at || !useIsWorkspaceActionsEnabled()) { + const experimentEnabled = useIsWorkspaceActionsEnabled() + if (!workspace.locked_at || !experimentEnabled) { return null } diff --git a/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx b/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx index d506c93160a28..dd79e25332bdd 100644 --- a/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx +++ b/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx @@ -33,12 +33,13 @@ export const LockedWorkspaceBanner = ({ (workspace) => workspace.deleting_at, ) + const experimentEnabled = useIsWorkspaceActionsEnabled() if ( (!hasLockedWorkspaces || // Banners should be redisplayed after dismissal when additional workspaces are newly scheduled for deletion !shouldRedisplayBanner) && // Only show this if the experiment is included. - useIsWorkspaceActionsEnabled() + experimentEnabled ) { return null } diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index ecc74b8709250..907e85e76b54d 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -11,7 +11,7 @@ import { useTemplateFilterMenu, useStatusFilterMenu } from "./filter/menus" import { useSearchParams } from "react-router-dom" import { useFilter } from "components/Filter/filter" import { useUserFilterMenu } from "components/Filter/UserFilter" -import { getWorkspaces, updateWorkspaceVersion } from "api/api" +import { getWorkspaces } from "api/api" const WorkspacesPage: FC = () => { const [lockedWorkspaces, setLockedWorkspaces] = useState([]) @@ -26,17 +26,18 @@ const WorkspacesPage: FC = () => { query: filterProps.filter.query, }) + const experimentEnabled = useIsWorkspaceActionsEnabled() // If workspace actions are enabled we need to fetch the locked // workspaces as well. This lets us determine whether we should // show a banner to the user indicating that some of their workspaces // are at risk of being deleted. - if (useIsWorkspaceActionsEnabled()) { - const includesLocked = filterProps.filter.query.includes("locked_at") - const lockedQuery = includesLocked - ? filterProps.filter.query - : filterProps.filter.query + " locked_at:1970-01-01" + useEffect(() => { + if (experimentEnabled) { + const includesLocked = filterProps.filter.query.includes("locked_at") + const lockedQuery = includesLocked + ? filterProps.filter.query + : filterProps.filter.query + " locked_at:1970-01-01" - useEffect(() => { if (includesLocked && data) { setLockedWorkspaces(data.workspaces) } else { @@ -48,12 +49,12 @@ const WorkspacesPage: FC = () => { // TODO? }) } - }, [includesLocked, data, lockedQuery]) - } else { - // If the experiment isn't included then we'll pretend - // like locked workspaces don't exist. - setLockedWorkspaces([]) - } + } else { + // If the experiment isn't included then we'll pretend + // like locked workspaces don't exist. + setLockedWorkspaces([]) + } + }, [experimentEnabled]) const updateWorkspace = useWorkspaceUpdate(queryKey) From c300fd869b24639aa05b6e24144327193bfd5c0a Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Wed, 2 Aug 2023 03:33:47 +0000 Subject: [PATCH 12/15] fix lint again --- .../components/WorkspaceDeletion/ImpendingDeletionBanner.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx b/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx index dd79e25332bdd..a75bdef0f3fe9 100644 --- a/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx +++ b/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx @@ -21,6 +21,8 @@ export const LockedWorkspaceBanner = ({ shouldRedisplayBanner: boolean count?: Count }): JSX.Element | null => { + const experimentEnabled = useIsWorkspaceActionsEnabled() + if (!workspaces) { return null } @@ -33,7 +35,6 @@ export const LockedWorkspaceBanner = ({ (workspace) => workspace.deleting_at, ) - const experimentEnabled = useIsWorkspaceActionsEnabled() if ( (!hasLockedWorkspaces || // Banners should be redisplayed after dismissal when additional workspaces are newly scheduled for deletion From 152932bbcb131918d244ae0829e7dc530fca72de Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Wed, 2 Aug 2023 03:44:25 +0000 Subject: [PATCH 13/15] linttt --- site/src/pages/WorkspacesPage/WorkspacesPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index 907e85e76b54d..126a0bcdb65f1 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -45,8 +45,8 @@ const WorkspacesPage: FC = () => { .then((resp) => { setLockedWorkspaces(resp.workspaces) }) - .catch((err) => { - // TODO? + .catch(() => { + // TODO }) } } else { From 6873a95da5ae934bd80c4c6adfd93c8923ec3f38 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Wed, 2 Aug 2023 04:00:34 +0000 Subject: [PATCH 14/15] fix a stray banner --- .../WorkspaceDeletion/ImpendingDeletionBanner.tsx | 8 ++++---- site/src/pages/WorkspacesPage/WorkspacesPage.tsx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx b/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx index a75bdef0f3fe9..501dd50dfa95f 100644 --- a/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx +++ b/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx @@ -36,11 +36,11 @@ export const LockedWorkspaceBanner = ({ ) if ( - (!hasLockedWorkspaces || - // Banners should be redisplayed after dismissal when additional workspaces are newly scheduled for deletion - !shouldRedisplayBanner) && // Only show this if the experiment is included. - experimentEnabled + !experimentEnabled || + !hasLockedWorkspaces || + // Banners should be redisplayed after dismissal when additional workspaces are newly scheduled for deletion + !shouldRedisplayBanner ) { return null } diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index 126a0bcdb65f1..6194f1fc4add8 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -54,7 +54,7 @@ const WorkspacesPage: FC = () => { // like locked workspaces don't exist. setLockedWorkspaces([]) } - }, [experimentEnabled]) + }, [experimentEnabled, data, filterProps.filter.query]) const updateWorkspace = useWorkspaceUpdate(queryKey) From 246f3a3255f7b59006bcbec44befd14c825293f8 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 27 Jul 2023 12:54:25 +0000 Subject: [PATCH 15/15] fix tests --- .../TemplateScheduleForm/TemplateScheduleForm.tsx | 3 --- .../TemplateSchedulePage/TemplateSchedulePage.test.tsx | 4 ++++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx index 8bfdeff8a0b27..1efdcec885cfb 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx @@ -341,7 +341,6 @@ export const TemplateScheduleForm: FC = ({ inputProps={{ min: 0, step: "any" }} label="Time until cleanup (days)" type="number" - aria-label="Failure Cleanup" /> @@ -375,7 +374,6 @@ export const TemplateScheduleForm: FC = ({ inputProps={{ min: 0, step: "any" }} label="Time until cleanup (days)" type="number" - aria-label="Inactivity Cleanup" /> @@ -407,7 +405,6 @@ export const TemplateScheduleForm: FC = ({ inputProps={{ min: 0, step: "any" }} label="Time until cleanup (days)" type="number" - aria-label="Locked Cleanup" /> diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx index c1d10a6955a42..15612a544d89e 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx @@ -76,6 +76,10 @@ const fillAndSubmitForm = async ({ FooterFormLanguage.defaultSubmitLabel, ) await user.click(submitButton) + + // User needs to confirm inactivity and locked ttl + const confirmButton = await screen.findByTestId("confirm-button") + await user.click(confirmButton) } describe("TemplateSchedulePage", () => {