From 0ba83723323f1e042f57996ba981313cd03057c4 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 1 May 2025 12:47:01 +0000 Subject: [PATCH 01/17] feat: add inline actions into workspaces table --- site/src/api/queries/workspaces.ts | 6 +- site/src/hooks/usePagination.ts | 6 +- .../workspaces/actions.ts} | 0 .../useWorkspacesToBeDeleted.ts | 9 +- .../WorkspaceActions/WorkspaceActions.tsx | 5 +- .../pages/WorkspacesPage/WorkspacesPage.tsx | 12 +- .../WorkspacesPage/WorkspacesPageView.tsx | 5 +- .../pages/WorkspacesPage/WorkspacesTable.tsx | 228 +++++++++++++++++- site/src/pages/WorkspacesPage/data.ts | 26 +- 9 files changed, 255 insertions(+), 42 deletions(-) rename site/src/{pages/WorkspacePage/WorkspaceActions/constants.ts => modules/workspaces/actions.ts} (100%) diff --git a/site/src/api/queries/workspaces.ts b/site/src/api/queries/workspaces.ts index ee390e542c42c..1c89aeaacdd1c 100644 --- a/site/src/api/queries/workspaces.ts +++ b/site/src/api/queries/workspaces.ts @@ -139,13 +139,9 @@ export function workspacesKey(config: WorkspacesRequest = {}) { } export function workspaces(config: WorkspacesRequest = {}) { - // Duplicates some of the work from workspacesKey, but that felt better than - // letting invisible properties sneak into the query logic - const { q, limit } = config; - return { queryKey: workspacesKey(config), - queryFn: () => API.getWorkspaces({ q, limit }), + queryFn: () => API.getWorkspaces(config), } as const satisfies QueryOptions; } diff --git a/site/src/hooks/usePagination.ts b/site/src/hooks/usePagination.ts index 72ea70868fb30..2ab409e85c9d8 100644 --- a/site/src/hooks/usePagination.ts +++ b/site/src/hooks/usePagination.ts @@ -9,7 +9,7 @@ export const usePagination = ({ const [searchParams, setSearchParams] = searchParamsResult; const page = searchParams.get("page") ? Number(searchParams.get("page")) : 1; const limit = DEFAULT_RECORDS_PER_PAGE; - const offset = page <= 0 ? 0 : (page - 1) * limit; + const offset = calcOffset(page, limit); const goToPage = (page: number) => { searchParams.set("page", page.toString()); @@ -23,3 +23,7 @@ export const usePagination = ({ offset, }; }; + +export const calcOffset = (page: number, limit: number) => { + return page <= 0 ? 0 : (page - 1) * limit; +}; diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/constants.ts b/site/src/modules/workspaces/actions.ts similarity index 100% rename from site/src/pages/WorkspacePage/WorkspaceActions/constants.ts rename to site/src/modules/workspaces/actions.ts diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/useWorkspacesToBeDeleted.ts b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/useWorkspacesToBeDeleted.ts index 338c157f4f791..425e1ad01b5bf 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/useWorkspacesToBeDeleted.ts +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/useWorkspacesToBeDeleted.ts @@ -2,6 +2,7 @@ import type { Template, Workspace } from "api/typesGenerated"; import { compareAsc } from "date-fns"; import { useWorkspacesData } from "pages/WorkspacesPage/data"; import type { TemplateScheduleFormValues } from "./formHelpers"; +import { calcOffset } from "hooks/usePagination"; export const useWorkspacesToGoDormant = ( template: Template, @@ -9,9 +10,9 @@ export const useWorkspacesToGoDormant = ( fromDate: Date, ) => { const { data } = useWorkspacesData({ - page: 0, + offset: calcOffset(0, 0), limit: 0, - query: `template:${template.name}`, + q: `template:${template.name}`, }); return data?.workspaces?.filter((workspace: Workspace) => { @@ -40,9 +41,9 @@ export const useWorkspacesToBeDeleted = ( fromDate: Date, ) => { const { data } = useWorkspacesData({ - page: 0, + offset: calcOffset(0, 0), limit: 0, - query: `template:${template.name} dormant:true`, + q: `template:${template.name} dormant:true`, }); return data?.workspaces?.filter((workspace: Workspace) => { if (!workspace.dormant_at || !formValues.time_til_dormant_autodelete_ms) { diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx index b187167bb4631..b04f5aa721ce8 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx @@ -31,7 +31,10 @@ import { import { DebugButton } from "./DebugButton"; import { DownloadLogsDialog } from "./DownloadLogsDialog"; import { RetryButton } from "./RetryButton"; -import { type ActionType, abilitiesByWorkspaceStatus } from "./constants"; +import { + abilitiesByWorkspaceStatus, + type ActionType, +} from "modules/workspaces/actions"; export interface WorkspaceActionsProps { workspace: Workspace; diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index ba380905adda2..8640077b63e94 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -1,6 +1,6 @@ import { workspacePermissionsByOrganization } from "api/queries/organizations"; import { templates } from "api/queries/templates"; -import type { Workspace } from "api/typesGenerated"; +import type { Workspace, WorkspacesResponse } from "api/typesGenerated"; import { useFilter } from "components/Filter/Filter"; import { useUserFilterMenu } from "components/Filter/UserFilter"; import { useAuthenticated } from "hooks"; @@ -10,7 +10,7 @@ import { useDashboard } from "modules/dashboard/useDashboard"; import { useOrganizationsFilterMenu } from "modules/tableFiltering/options"; import { type FC, useEffect, useMemo, useState } from "react"; import { Helmet } from "react-helmet-async"; -import { useQuery } from "react-query"; +import { useQuery, useQueryClient } from "react-query"; import { useSearchParams } from "react-router-dom"; import { pageTitle } from "utils/page"; import { BatchDeleteConfirmation } from "./BatchDeleteConfirmation"; @@ -35,6 +35,7 @@ function useSafeSearchParams() { } const WorkspacesPage: FC = () => { + const queryClient = useQueryClient(); // 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. @@ -72,7 +73,7 @@ const WorkspacesPage: FC = () => { const { data, error, queryKey, refetch } = useWorkspacesData({ ...pagination, - query: filterProps.filter.query, + q: filterProps.filter.query, }); const updateWorkspace = useWorkspaceUpdate(queryKey); @@ -128,6 +129,11 @@ const WorkspacesPage: FC = () => { onUpdateAll={() => setConfirmingBatchAction("update")} onStartAll={() => batchActions.startAll(checkedWorkspaces)} onStopAll={() => batchActions.stopAll(checkedWorkspaces)} + onActionSuccess={async () => { + await queryClient.invalidateQueries({ + queryKey, + }); + }} /> Promise; } export const WorkspacesPageView: FC = ({ @@ -88,6 +89,7 @@ export const WorkspacesPageView: FC = ({ templatesFetchStatus, canCreateTemplate, canChangeVersions, + onActionSuccess, }) => { // Let's say the user has 5 workspaces, but tried to hit page 100, which does // not exist. In this case, the page is not valid and we want to show a better @@ -221,6 +223,7 @@ export const WorkspacesPageView: FC = ({ onCheckChange={onCheckChange} canCheckWorkspaces={canCheckWorkspaces} templates={templates} + onActionSuccess={onActionSuccess} /> )} diff --git a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx index a9d585fccf58c..526262b77a560 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx @@ -7,6 +7,7 @@ import type { Workspace, WorkspaceAgent, WorkspaceApp, + WorkspaceBuild, } from "api/typesGenerated"; import { Avatar } from "components/Avatar/Avatar"; import { AvatarData } from "components/Avatar/AvatarData"; @@ -47,6 +48,24 @@ import { lastUsedMessage, } from "utils/workspace"; import { WorkspacesEmpty } from "./WorkspacesEmpty"; +import { BanIcon, PlayIcon, RefreshCcwIcon, SquareIcon } from "lucide-react"; +import { Button } from "components/Button/Button"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "components/Tooltip/Tooltip"; +import { useMutation, useQueryClient } from "react-query"; +import { + cancelBuild, + deleteWorkspace, + startWorkspace, + stopWorkspace, + updateWorkspace, +} from "api/queries/workspaces"; +import { Spinner } from "components/Spinner/Spinner"; +import { abilitiesByWorkspaceStatus } from "modules/workspaces/actions"; dayjs.extend(relativeTime); @@ -60,6 +79,7 @@ export interface WorkspacesTableProps { canCheckWorkspaces: boolean; templates?: Template[]; canCreateTemplate: boolean; + onActionSuccess: () => Promise; } export const WorkspacesTable: FC = ({ @@ -71,6 +91,7 @@ export const WorkspacesTable: FC = ({ canCheckWorkspaces, templates, canCreateTemplate, + onActionSuccess, }) => { const dashboard = useDashboard(); const workspaceIDToAppByStatus = useMemo(() => { @@ -260,12 +281,10 @@ export const WorkspacesTable: FC = ({ - - -
- -
-
+ ); })} @@ -289,7 +308,7 @@ const WorkspacesRow: FC = ({ const workspacePageLink = `/@${workspace.owner_name}/${workspace.name}`; const openLinkInNewTab = () => window.open(workspacePageLink, "_blank"); - const clickableProps = useClickableTableRow({ + const { role, hover, ...clickableProps } = useClickableTableRow({ onMiddleClick: openLinkInNewTab, onClick: (event) => { // Order of booleans actually matters here for Windows-Mac compatibility; @@ -399,3 +418,198 @@ const WorkspaceStatusCell: FC = ({ workspace }) => { ); }; + +type WorkspaceActionsCellProps = { + workspace: Workspace; + onActionSuccess: () => Promise; +}; + +const WorkspaceActionsCell: FC = ({ + workspace, + onActionSuccess, +}) => { + const queryClient = useQueryClient(); + const abilities = abilitiesByWorkspaceStatus(workspace, false); + + const startWorkspaceOptions = startWorkspace(workspace, queryClient); + const startWorkspaceMutation = useMutation({ + ...startWorkspaceOptions, + onSuccess: async (build) => { + startWorkspaceOptions.onSuccess(build); + await onActionSuccess(); + }, + }); + + const stopWorkspaceOptions = stopWorkspace(workspace, queryClient); + const stopWorkspaceMutation = useMutation({ + ...stopWorkspaceOptions, + onSuccess: async (build) => { + stopWorkspaceOptions.onSuccess(build); + await onActionSuccess(); + }, + }); + + const cancelJobOptions = cancelBuild(workspace, queryClient); + const cancelBuildMutation = useMutation({ + ...cancelJobOptions, + onSuccess: async () => { + cancelJobOptions.onSuccess(); + await onActionSuccess(); + }, + }); + + const updateWorkspaceOptions = updateWorkspace(workspace, queryClient); + const updateWorkspaceMutation = useMutation({ + ...updateWorkspaceOptions, + onSuccess: async (build) => { + updateWorkspaceOptions.onSuccess(build); + await onActionSuccess(); + }, + }); + + const deleteWorkspaceOptions = deleteWorkspace(workspace, queryClient); + const deleteWorkspaceMutation = useMutation({ + ...deleteWorkspaceOptions, + onSuccess: async (build) => { + deleteWorkspaceOptions.onSuccess(build); + await onActionSuccess(); + }, + }); + + const isRetrying = + updateWorkspaceMutation.isLoading || + deleteWorkspaceMutation.isLoading || + startWorkspaceMutation.isLoading; + const retry = () => { + switch (workspace.latest_build.transition) { + case "start": + startWorkspaceMutation.mutate({}); + break; + case "stop": + stopWorkspaceMutation.mutate({}); + break; + case "delete": + deleteWorkspaceMutation.mutate({}); + break; + } + }; + + return ( + + {abilities.actions.includes("start") && ( + + + + + + Start workspace + + + )} + + {abilities.actions.includes("updateAndStart") && ( + + + + + + Update and start workspace + + + )} + + {abilities.actions.includes("stop") && ( + + + + + + Stop workspace + + + )} + + {abilities.canCancel && ( + + + + + + Cancel current job + + + )} + + {abilities.actions.includes("retry") && ( + + + + + + Retry + + + )} + + ); +}; diff --git a/site/src/pages/WorkspacesPage/data.ts b/site/src/pages/WorkspacesPage/data.ts index 9b46ab7fed05b..764ea218aa96c 100644 --- a/site/src/pages/WorkspacesPage/data.ts +++ b/site/src/pages/WorkspacesPage/data.ts @@ -1,8 +1,10 @@ import { API } from "api/api"; import { getErrorMessage } from "api/errors"; +import { workspaces } from "api/queries/workspaces"; import type { Workspace, WorkspaceBuild, + WorkspacesRequest, WorkspacesResponse, } from "api/typesGenerated"; import { displayError } from "components/GlobalSnackbar/utils"; @@ -14,27 +16,11 @@ import { useQueryClient, } from "react-query"; -type UseWorkspacesDataParams = { - page: number; - limit: number; - query: string; -}; - -export const useWorkspacesData = ({ - page, - limit, - query, -}: UseWorkspacesDataParams) => { - const queryKey = ["workspaces", query, page]; +export const useWorkspacesData = (req: WorkspacesRequest) => { const [shouldRefetch, setShouldRefetch] = useState(true); + const workspacesQueryOptions = workspaces(req); const result = useQuery({ - queryKey, - queryFn: () => - API.getWorkspaces({ - q: query, - limit: limit, - offset: page <= 0 ? 0 : (page - 1) * limit, - }), + ...workspacesQueryOptions, onSuccess: () => { setShouldRefetch(true); }, @@ -46,7 +32,7 @@ export const useWorkspacesData = ({ return { ...result, - queryKey, + queryKey: workspacesQueryOptions.queryKey, }; }; From 25d986ec1364e47d191de32441e9abd83507e1f9 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 1 May 2025 12:58:58 +0000 Subject: [PATCH 02/17] Fix is retrying --- .../pages/WorkspacesPage/WorkspacesTable.tsx | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx index 526262b77a560..6074b07fb2999 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx @@ -160,6 +160,7 @@ export const WorkspacesTable: FC = ({ Template Status + @@ -285,6 +286,11 @@ export const WorkspacesTable: FC = ({ workspace={workspace} onActionSuccess={onActionSuccess} /> + +
+ +
+
); })} @@ -359,8 +365,10 @@ const TableLoader: FC = ({ canCheckWorkspaces }) => { - - + +
+ +
@@ -477,9 +485,10 @@ const WorkspaceActionsCell: FC = ({ }); const isRetrying = - updateWorkspaceMutation.isLoading || - deleteWorkspaceMutation.isLoading || - startWorkspaceMutation.isLoading; + startWorkspaceMutation.isLoading || + stopWorkspaceMutation.isLoading || + deleteWorkspaceMutation.isLoading; + const retry = () => { switch (workspace.latest_build.transition) { case "start": From 50d9d74d695a73711fa049dcebf9ea01cc3118da Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 1 May 2025 13:06:14 +0000 Subject: [PATCH 03/17] Refactor actions --- .../pages/WorkspacesPage/WorkspacesTable.tsx | 180 ++++++++---------- 1 file changed, 79 insertions(+), 101 deletions(-) diff --git a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx index 6074b07fb2999..28bf795ae00ed 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx @@ -38,7 +38,12 @@ import { useDashboard } from "modules/dashboard/useDashboard"; import { WorkspaceAppStatus } from "modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus"; import { WorkspaceDormantBadge } from "modules/workspaces/WorkspaceDormantBadge/WorkspaceDormantBadge"; import { WorkspaceOutdatedTooltip } from "modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip"; -import { type FC, type ReactNode, useMemo } from "react"; +import { + type FC, + type PropsWithChildren, + type ReactNode, + useMemo, +} from "react"; import { useNavigate } from "react-router-dom"; import { cn } from "utils/cn"; import { @@ -506,119 +511,92 @@ const WorkspaceActionsCell: FC = ({ return ( {abilities.actions.includes("start") && ( - - - - - - Start workspace - - + startWorkspaceMutation.mutate({})} + isLoading={startWorkspaceMutation.isLoading} + label="Start workspace" + > + + )} {abilities.actions.includes("updateAndStart") && ( - - - - - - Update and start workspace - - + { + updateWorkspaceMutation.mutate(undefined); + }} + isLoading={updateWorkspaceMutation.isLoading} + label="Update and start workspace" + > + + )} {abilities.actions.includes("stop") && ( - - - - - - Stop workspace - - + { + stopWorkspaceMutation.mutate({}); + }} + isLoading={stopWorkspaceMutation.isLoading} + label="Stop workspace" + > + + )} {abilities.canCancel && ( - - - - - - Cancel current job - - + + + )} {abilities.actions.includes("retry") && ( - - - - - - Retry - - + + + )} ); }; + +type PrimaryActionProps = PropsWithChildren<{ + onClick: () => void; + isLoading: boolean; + label: string; +}>; + +const PrimaryAction: FC = ({ + onClick, + isLoading, + label, + children, +}) => { + return ( + + + + + + {label} + + + ); +}; From a20a1882e12a9cf4ea0738ab8356472db8ff7f86 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 1 May 2025 14:15:28 +0000 Subject: [PATCH 04/17] Simplify permissions check --- site/src/modules/workspaces/actions.ts | 16 ++++++++++++---- site/src/pages/WorkspacePage/Workspace.tsx | 3 --- .../WorkspaceActions.stories.tsx | 15 +++++++++++---- .../WorkspaceActions/WorkspaceActions.tsx | 13 ++++++------- .../pages/WorkspacePage/WorkspaceReadyPage.tsx | 5 ----- site/src/pages/WorkspacePage/WorkspaceTopbar.tsx | 3 --- .../src/pages/WorkspacesPage/WorkspacesTable.tsx | 8 +++++++- 7 files changed, 36 insertions(+), 27 deletions(-) diff --git a/site/src/modules/workspaces/actions.ts b/site/src/modules/workspaces/actions.ts index 9a266c0efbc57..9b1c742d79487 100644 --- a/site/src/modules/workspaces/actions.ts +++ b/site/src/modules/workspaces/actions.ts @@ -34,6 +34,11 @@ export const actionTypes = [ export type ActionType = (typeof actionTypes)[number]; +type ActionPermissions = { + canDebug: boolean; + isOwner: boolean; +}; + type WorkspaceAbilities = { actions: readonly ActionType[]; canCancel: boolean; @@ -42,8 +47,11 @@ type WorkspaceAbilities = { export const abilitiesByWorkspaceStatus = ( workspace: Workspace, - canDebug: boolean, + permissions: ActionPermissions, ): WorkspaceAbilities => { + const hasPermissionToCancel = + workspace.template_allow_user_cancel_workspace_jobs || permissions.isOwner; + if (workspace.dormant_at) { return { actions: ["activate"], @@ -58,7 +66,7 @@ export const abilitiesByWorkspaceStatus = ( case "starting": { return { actions: ["starting"], - canCancel: true, + canCancel: true && hasPermissionToCancel, canAcceptJobs: false, }; } @@ -83,7 +91,7 @@ export const abilitiesByWorkspaceStatus = ( case "stopping": { return { actions: ["stopping"], - canCancel: true, + canCancel: true && hasPermissionToCancel, canAcceptJobs: false, }; } @@ -115,7 +123,7 @@ export const abilitiesByWorkspaceStatus = ( case "failed": { const actions: ActionType[] = ["retry"]; - if (canDebug) { + if (permissions.canDebug) { actions.push("debug"); } diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index 9148c71f32d22..69ce29ed0e7d1 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -53,7 +53,6 @@ export interface WorkspaceProps { buildLogs?: TypesGen.ProvisionerJobLog[]; latestVersion?: TypesGen.TemplateVersion; permissions: WorkspacePermissions; - isOwner: boolean; timings?: TypesGen.WorkspaceBuildTimings; } @@ -86,7 +85,6 @@ export const Workspace: FC = ({ buildLogs, latestVersion, permissions, - isOwner, timings, }) => { const navigate = useNavigate(); @@ -161,7 +159,6 @@ export const Workspace: FC = ({ isUpdating={isUpdating} isRestarting={isRestarting} canUpdateWorkspace={permissions.updateWorkspace} - isOwner={isOwner} template={template} permissions={permissions} latestVersion={latestVersion} diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.stories.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.stories.tsx index 700797c886030..28f52d2507774 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.stories.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.stories.tsx @@ -3,6 +3,7 @@ import { expect, userEvent, within } from "@storybook/test"; import { agentLogsKey, buildLogsKey } from "api/queries/workspaces"; import * as Mocks from "testHelpers/entities"; import { + withAuthProvider, withDashboardProvider, withDesktopViewport, } from "testHelpers/storybook"; @@ -14,7 +15,10 @@ const meta: Meta = { args: { isUpdating: false, }, - decorators: [withDashboardProvider, withDesktopViewport], + decorators: [withDashboardProvider, withDesktopViewport, withAuthProvider], + parameters: { + user: Mocks.MockUser, + }, }; export default meta; @@ -163,14 +167,15 @@ export const CancelShownForOwner: Story = { ...Mocks.MockStartingWorkspace, template_allow_user_cancel_workspace_jobs: false, }, - isOwner: true, }, }; export const CancelShownForUser: Story = { args: { workspace: Mocks.MockStartingWorkspace, - isOwner: false, + }, + parameters: { + user: Mocks.MockUser2, }, }; @@ -180,7 +185,9 @@ export const CancelHiddenForUser: Story = { ...Mocks.MockStartingWorkspace, template_allow_user_cancel_workspace_jobs: false, }, - isOwner: false, + }, + parameters: { + user: Mocks.MockUser2, }, }; diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx index b04f5aa721ce8..d48431cd6ff84 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx @@ -35,6 +35,7 @@ import { abilitiesByWorkspaceStatus, type ActionType, } from "modules/workspaces/actions"; +import { useAuthenticated } from "hooks/useAuthenticated"; export interface WorkspaceActionsProps { workspace: Workspace; @@ -55,7 +56,6 @@ export interface WorkspaceActionsProps { children?: ReactNode; canChangeVersions: boolean; canDebug: boolean; - isOwner: boolean; } export const WorkspaceActions: FC = ({ @@ -76,20 +76,19 @@ export const WorkspaceActions: FC = ({ isRestarting, canChangeVersions, canDebug, - isOwner, }) => { const { duplicateWorkspace, isDuplicationReady } = useWorkspaceDuplication(workspace); const [isDownloadDialogOpen, setIsDownloadDialogOpen] = useState(false); + const { user } = useAuthenticated(); + const isOwner = + user.roles.find((role) => role.name === "owner") !== undefined; const { actions, canCancel, canAcceptJobs } = abilitiesByWorkspaceStatus( workspace, - canDebug, + { canDebug, isOwner }, ); - const showCancel = - canCancel && - (workspace.template_allow_user_cancel_workspace_jobs || isOwner); const mustUpdate = mustUpdateWorkspace(workspace, canChangeVersions); const tooltipText = getTooltipText(workspace, mustUpdate, canChangeVersions); @@ -172,7 +171,7 @@ export const WorkspaceActions: FC = ({ {buttonMapping[action]} ))} - {showCancel && } + {canCancel && } = ({ throw Error("Workspace is undefined"); } - // Owner - const { user: me } = useAuthenticated(); - const isOwner = me.roles.find((role) => role.name === "owner") !== undefined; - // Debug mode const { data: deploymentValues } = useQuery({ ...deploymentConfig(), @@ -280,7 +276,6 @@ export const WorkspaceReadyPage: FC = ({ sshPrefix={sshPrefixQuery.data?.hostname_prefix} template={template} buildLogs={buildLogs} - isOwner={isOwner} timings={timingsQuery.data} /> diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx index d8a5f9d5b345c..d8b63b2a6f7b4 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx @@ -55,7 +55,6 @@ export interface WorkspaceProps { canDebugMode: boolean; handleRetry: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; handleDebug: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; - isOwner: boolean; template: TypesGen.Template; permissions: WorkspacePermissions; latestVersion?: TypesGen.TemplateVersion; @@ -81,7 +80,6 @@ export const WorkspaceTopbar: FC = ({ canDebugMode, handleRetry, handleDebug, - isOwner, template, latestVersion, permissions, @@ -262,7 +260,6 @@ export const WorkspaceTopbar: FC = ({ canChangeVersions={canChangeVersions} isUpdating={isUpdating} isRestarting={isRestarting} - isOwner={isOwner} /> )} diff --git a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx index 28bf795ae00ed..20ced380c5422 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx @@ -71,6 +71,7 @@ import { } from "api/queries/workspaces"; import { Spinner } from "components/Spinner/Spinner"; import { abilitiesByWorkspaceStatus } from "modules/workspaces/actions"; +import { useAuthenticated } from "hooks"; dayjs.extend(relativeTime); @@ -441,8 +442,13 @@ const WorkspaceActionsCell: FC = ({ workspace, onActionSuccess, }) => { + const { user } = useAuthenticated(); + const queryClient = useQueryClient(); - const abilities = abilitiesByWorkspaceStatus(workspace, false); + const abilities = abilitiesByWorkspaceStatus(workspace, { + canDebug: false, + isOwner: user.roles.find((role) => role.name === "owner") !== undefined, + }); const startWorkspaceOptions = startWorkspace(workspace, queryClient); const startWorkspaceMutation = useMutation({ From 6f054b99b0b1b1c70e1bd4fdc16266ece6520665 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 1 May 2025 14:21:59 +0000 Subject: [PATCH 05/17] Handle action errors --- site/src/pages/WorkspacesPage/WorkspacesPage.tsx | 8 ++++++++ site/src/pages/WorkspacesPage/WorkspacesPageView.tsx | 3 +++ site/src/pages/WorkspacesPage/WorkspacesTable.tsx | 10 ++++++++++ 3 files changed, 21 insertions(+) diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index 8640077b63e94..a399ebfc427a9 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -19,6 +19,8 @@ import { WorkspacesPageView } from "./WorkspacesPageView"; import { useBatchActions } from "./batchActions"; import { useWorkspaceUpdate, useWorkspacesData } from "./data"; import { useStatusFilterMenu, useTemplateFilterMenu } from "./filter/menus"; +import { displayError } from "components/GlobalSnackbar/utils"; +import { getErrorDetail, getErrorMessage } from "api/errors"; function useSafeSearchParams() { // Have to wrap setSearchParams because React Router doesn't make sure that @@ -134,6 +136,12 @@ const WorkspacesPage: FC = () => { queryKey, }); }} + onActionError={(error) => { + displayError( + getErrorMessage(error, "Failed to perform action"), + getErrorDetail(error), + ); + }} /> Promise; + onActionError: (error: unknown) => void; } export const WorkspacesPageView: FC = ({ @@ -90,6 +91,7 @@ export const WorkspacesPageView: FC = ({ canCreateTemplate, canChangeVersions, onActionSuccess, + onActionError, }) => { // Let's say the user has 5 workspaces, but tried to hit page 100, which does // not exist. In this case, the page is not valid and we want to show a better @@ -224,6 +226,7 @@ export const WorkspacesPageView: FC = ({ canCheckWorkspaces={canCheckWorkspaces} templates={templates} onActionSuccess={onActionSuccess} + onActionError={onActionError} /> )} diff --git a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx index 20ced380c5422..db1190ee60247 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx @@ -86,6 +86,7 @@ export interface WorkspacesTableProps { templates?: Template[]; canCreateTemplate: boolean; onActionSuccess: () => Promise; + onActionError: (error: unknown) => void; } export const WorkspacesTable: FC = ({ @@ -98,6 +99,7 @@ export const WorkspacesTable: FC = ({ templates, canCreateTemplate, onActionSuccess, + onActionError, }) => { const dashboard = useDashboard(); const workspaceIDToAppByStatus = useMemo(() => { @@ -291,6 +293,7 @@ export const WorkspacesTable: FC = ({
@@ -436,11 +439,13 @@ const WorkspaceStatusCell: FC = ({ workspace }) => { type WorkspaceActionsCellProps = { workspace: Workspace; onActionSuccess: () => Promise; + onActionError: (error: unknown) => void; }; const WorkspaceActionsCell: FC = ({ workspace, onActionSuccess, + onActionError, }) => { const { user } = useAuthenticated(); @@ -457,6 +462,7 @@ const WorkspaceActionsCell: FC = ({ startWorkspaceOptions.onSuccess(build); await onActionSuccess(); }, + onError: onActionError, }); const stopWorkspaceOptions = stopWorkspace(workspace, queryClient); @@ -466,6 +472,7 @@ const WorkspaceActionsCell: FC = ({ stopWorkspaceOptions.onSuccess(build); await onActionSuccess(); }, + onError: onActionError, }); const cancelJobOptions = cancelBuild(workspace, queryClient); @@ -475,6 +482,7 @@ const WorkspaceActionsCell: FC = ({ cancelJobOptions.onSuccess(); await onActionSuccess(); }, + onError: onActionError, }); const updateWorkspaceOptions = updateWorkspace(workspace, queryClient); @@ -484,6 +492,7 @@ const WorkspaceActionsCell: FC = ({ updateWorkspaceOptions.onSuccess(build); await onActionSuccess(); }, + onError: onActionError, }); const deleteWorkspaceOptions = deleteWorkspace(workspace, queryClient); @@ -493,6 +502,7 @@ const WorkspaceActionsCell: FC = ({ deleteWorkspaceOptions.onSuccess(build); await onActionSuccess(); }, + onError: onActionError, }); const isRetrying = From fcc5a27ca464c2fadff9a7e4e7e18fdb41c33e28 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 1 May 2025 16:49:37 +0000 Subject: [PATCH 06/17] Refactor workspace update action --- site/src/components/Dialogs/Dialog.tsx | 9 +- .../modules/workspaces/useWorkspaceUpdate.tsx | 162 ++++++++++++++++++ .../WorkspacePage/WorkspaceReadyPage.tsx | 66 ++----- .../pages/WorkspacesPage/WorkspacesTable.tsx | 43 +++-- 4 files changed, 208 insertions(+), 72 deletions(-) create mode 100644 site/src/modules/workspaces/useWorkspaceUpdate.tsx diff --git a/site/src/components/Dialogs/Dialog.tsx b/site/src/components/Dialogs/Dialog.tsx index cdc271697c680..532b47a1339dc 100644 --- a/site/src/components/Dialogs/Dialog.tsx +++ b/site/src/components/Dialogs/Dialog.tsx @@ -35,7 +35,14 @@ export const DialogActionButtons: FC = ({ return ( <> {onCancel && ( - )} diff --git a/site/src/modules/workspaces/useWorkspaceUpdate.tsx b/site/src/modules/workspaces/useWorkspaceUpdate.tsx new file mode 100644 index 0000000000000..5f3a48dcbfe33 --- /dev/null +++ b/site/src/modules/workspaces/useWorkspaceUpdate.tsx @@ -0,0 +1,162 @@ +import { MissingBuildParameters } from "api/api"; +import { updateWorkspace } from "api/queries/workspaces"; +import type { + TemplateVersion, + Workspace, + WorkspaceBuild, + WorkspaceBuildParameter, +} from "api/typesGenerated"; +import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; +import { MemoizedInlineMarkdown } from "components/Markdown/Markdown"; +import { UpdateBuildParametersDialog } from "pages/WorkspacePage/UpdateBuildParametersDialog"; +import { type FC, useState } from "react"; +import { useMutation, useQueryClient } from "react-query"; + +type UseWorkspaceUpdateOptions = { + workspace: Workspace; + latestVersion: TemplateVersion | undefined; + onSuccess?: (build: WorkspaceBuild) => void; + onError?: (error: unknown) => void; +}; + +type UseWorkspaceUpdateResult = { + update: ( + hasConfirmed?: boolean, + buildParameters?: WorkspaceBuildParameter[], + ) => void; + isUpdating: boolean; + dialogs: { + updateConfirmation: UpdateConfirmationDialogProps; + missingBuildParameters: MissingBuildParametersDialogProps; + }; +}; + +export const useWorkspaceUpdate = ({ + workspace, + latestVersion, + onSuccess, + onError, +}: UseWorkspaceUpdateOptions): UseWorkspaceUpdateResult => { + const queryClient = useQueryClient(); + const [isConfirmingUpdate, setIsConfirmingUpdate] = useState(false); + + const updateWorkspaceOptions = updateWorkspace(workspace, queryClient); + const updateWorkspaceMutation = useMutation({ + ...updateWorkspaceOptions, + onSuccess: (build: WorkspaceBuild) => { + updateWorkspaceOptions.onSuccess(build); + onSuccess?.(build); + }, + onError, + }); + + const update = ( + hasConfirmed = false, + buildParameters: WorkspaceBuildParameter[] = [], + ) => { + if (!hasConfirmed) { + setIsConfirmingUpdate(true); + return; + } + + updateWorkspaceMutation.mutate(buildParameters); + setIsConfirmingUpdate(false); + }; + + return { + update, + isUpdating: updateWorkspaceMutation.isLoading, + dialogs: { + updateConfirmation: { + open: isConfirmingUpdate, + onClose: () => setIsConfirmingUpdate(false), + onConfirm: () => update(true), + latestVersion, + }, + missingBuildParameters: { + error: updateWorkspaceMutation.error, + onClose: () => { + updateWorkspaceMutation.reset(); + }, + onUpdate: (buildParameters: WorkspaceBuildParameter[]) => { + if (updateWorkspaceMutation.error instanceof MissingBuildParameters) { + update(true, buildParameters); + } + }, + }, + }, + }; +}; + +type WorkspaceUpdateDialogsProps = { + updateConfirmation: UpdateConfirmationDialogProps; + missingBuildParameters: MissingBuildParametersDialogProps; +}; + +export const WorkspaceUpdateDialogs: FC = ({ + updateConfirmation, + missingBuildParameters, +}) => { + return ( + <> + + + + ); +}; + +type UpdateConfirmationDialogProps = { + open: boolean; + onClose: () => void; + onConfirm: () => void; + latestVersion?: TemplateVersion; +}; + +const UpdateConfirmationDialog: FC = ({ + latestVersion, + ...dialogProps +}) => { + return ( + +

+ Updating your workspace will start the workspace on the latest + template version. This can{" "} + delete non-persistent data. +

+ {latestVersion?.message && ( + + {latestVersion.message} + + )} +
+ } + /> + ); +}; + +type MissingBuildParametersDialogProps = { + error: unknown; + onClose: () => void; + onUpdate: (buildParameters: WorkspaceBuildParameter[]) => void; +}; + +const MissingBuildParametersDialog: FC = ({ + error, + ...dialogProps +}) => { + return ( + + ); +}; diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index 96e6e92c12b16..e41ccd5bf7a7e 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -12,7 +12,6 @@ import { startWorkspace, stopWorkspace, toggleFavorite, - updateWorkspace, } from "api/queries/workspaces"; import type * as TypesGen from "api/typesGenerated"; import { @@ -20,10 +19,7 @@ import { type ConfirmDialogProps, } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; import { displayError } from "components/GlobalSnackbar/utils"; -import { MemoizedInlineMarkdown } from "components/Markdown/Markdown"; -import { Stack } from "components/Stack/Stack"; import dayjs from "dayjs"; -import { useAuthenticated } from "hooks"; import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata"; import { useWorkspaceBuildLogs } from "hooks/useWorkspaceBuildLogs"; import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; @@ -37,6 +33,10 @@ import { UpdateBuildParametersDialog } from "./UpdateBuildParametersDialog"; import { Workspace } from "./Workspace"; import { WorkspaceDeleteDialog } from "./WorkspaceDeleteDialog"; import type { WorkspacePermissions } from "./permissions"; +import { + useWorkspaceUpdate, + WorkspaceUpdateDialogs, +} from "modules/workspaces/useWorkspaceUpdate"; interface WorkspaceReadyPageProps { template: TypesGen.Template; @@ -116,10 +116,10 @@ export const WorkspaceReadyPage: FC = ({ }); // Update workspace - const [isConfirmingUpdate, setIsConfirmingUpdate] = useState(false); - const updateWorkspaceMutation = useMutation( - updateWorkspace(workspace, queryClient), - ); + const workspaceUpdate = useWorkspaceUpdate({ + workspace, + latestVersion, + }); // If a user can update the template then they can force a delete // (via orphan). @@ -229,7 +229,7 @@ export const WorkspaceReadyPage: FC = ({ { @@ -244,9 +244,7 @@ export const WorkspaceReadyPage: FC = ({ handleRestart={(buildParameters) => { setConfirmingRestart({ open: true, buildParameters }); }} - handleUpdate={() => { - setIsConfirmingUpdate(true); - }} + handleUpdate={workspaceUpdate.update} handleCancel={cancelBuildMutation.mutate} handleSettings={() => navigate("settings")} handleRetry={handleRetry} @@ -313,23 +311,6 @@ export const WorkspaceReadyPage: FC = ({ }} /> - { - updateWorkspaceMutation.reset(); - }} - onUpdate={(buildParameters) => { - if (updateWorkspaceMutation.error instanceof MissingBuildParameters) { - updateWorkspaceMutation.mutate(buildParameters); - } - }} - /> - = ({ }} /> - { - updateWorkspaceMutation.mutate(undefined); - setIsConfirmingUpdate(false); - }} - onClose={() => setIsConfirmingUpdate(false)} - title="Update workspace?" - confirmText="Update" - description={ - -

- Updating your workspace will start the workspace on the latest - template version. This can{" "} - delete non-persistent data. -

- {latestVersion?.message && ( - - {latestVersion.message} - - )} -
- } - /> - { @@ -390,6 +346,8 @@ export const WorkspaceReadyPage: FC = ({ } /> + + ); }; diff --git a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx index db1190ee60247..e8d88104de469 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx @@ -61,7 +61,7 @@ import { TooltipProvider, TooltipTrigger, } from "components/Tooltip/Tooltip"; -import { useMutation, useQueryClient } from "react-query"; +import { useMutation, useQuery, useQueryClient } from "react-query"; import { cancelBuild, deleteWorkspace, @@ -72,6 +72,11 @@ import { import { Spinner } from "components/Spinner/Spinner"; import { abilitiesByWorkspaceStatus } from "modules/workspaces/actions"; import { useAuthenticated } from "hooks"; +import { + useWorkspaceUpdate, + WorkspaceUpdateDialogs, +} from "modules/workspaces/useWorkspaceUpdate"; +import { templateVersion } from "api/queries/templates"; dayjs.extend(relativeTime); @@ -485,13 +490,14 @@ const WorkspaceActionsCell: FC = ({ onError: onActionError, }); - const updateWorkspaceOptions = updateWorkspace(workspace, queryClient); - const updateWorkspaceMutation = useMutation({ - ...updateWorkspaceOptions, - onSuccess: async (build) => { - updateWorkspaceOptions.onSuccess(build); - await onActionSuccess(); - }, + const { data: latestVersion } = useQuery({ + ...templateVersion(workspace.template_active_version_id), + enabled: workspace.outdated, + }); + const workspaceUpdate = useWorkspaceUpdate({ + workspace, + latestVersion, + onSuccess: onActionSuccess, onError: onActionError, }); @@ -537,15 +543,18 @@ const WorkspaceActionsCell: FC = ({ )} {abilities.actions.includes("updateAndStart") && ( - { - updateWorkspaceMutation.mutate(undefined); - }} - isLoading={updateWorkspaceMutation.isLoading} - label="Update and start workspace" - > - - + <> + { + workspaceUpdate.update(false); + }} + isLoading={workspaceUpdate.isUpdating} + label="Update and start workspace" + > + + + + )} {abilities.actions.includes("stop") && ( From 766463d58610b8a0481c0bceb976a19964df9bc0 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 1 May 2025 17:00:11 +0000 Subject: [PATCH 07/17] Run fmt --- .../useWorkspacesToBeDeleted.ts | 2 +- .../WorkspaceActions/WorkspaceActions.tsx | 10 ++-- .../WorkspacePage/WorkspaceReadyPage.tsx | 8 ++-- .../pages/WorkspacesPage/WorkspacesPage.tsx | 6 +-- .../WorkspacesPage/WorkspacesPageView.tsx | 2 +- .../pages/WorkspacesPage/WorkspacesTable.tsx | 48 +++++++++---------- 6 files changed, 37 insertions(+), 39 deletions(-) diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/useWorkspacesToBeDeleted.ts b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/useWorkspacesToBeDeleted.ts index 425e1ad01b5bf..5a974d5d8fe31 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/useWorkspacesToBeDeleted.ts +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/useWorkspacesToBeDeleted.ts @@ -1,8 +1,8 @@ import type { Template, Workspace } from "api/typesGenerated"; import { compareAsc } from "date-fns"; +import { calcOffset } from "hooks/usePagination"; import { useWorkspacesData } from "pages/WorkspacesPage/data"; import type { TemplateScheduleFormValues } from "./formHelpers"; -import { calcOffset } from "hooks/usePagination"; export const useWorkspacesToGoDormant = ( template: Template, diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx index d48431cd6ff84..ac1e556ffebba 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx @@ -13,6 +13,11 @@ import { MoreMenuItem, MoreMenuTrigger, } from "components/MoreMenu/MoreMenu"; +import { useAuthenticated } from "hooks/useAuthenticated"; +import { + type ActionType, + abilitiesByWorkspaceStatus, +} from "modules/workspaces/actions"; import { useWorkspaceDuplication } from "pages/CreateWorkspacePage/useWorkspaceDuplication"; import { type FC, Fragment, type ReactNode, useState } from "react"; import { mustUpdateWorkspace } from "utils/workspace"; @@ -31,11 +36,6 @@ import { import { DebugButton } from "./DebugButton"; import { DownloadLogsDialog } from "./DownloadLogsDialog"; import { RetryButton } from "./RetryButton"; -import { - abilitiesByWorkspaceStatus, - type ActionType, -} from "modules/workspaces/actions"; -import { useAuthenticated } from "hooks/useAuthenticated"; export interface WorkspaceActionsProps { workspace: Workspace; diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index e41ccd5bf7a7e..a6c634082c0eb 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -23,6 +23,10 @@ import dayjs from "dayjs"; import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata"; import { useWorkspaceBuildLogs } from "hooks/useWorkspaceBuildLogs"; import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; +import { + WorkspaceUpdateDialogs, + useWorkspaceUpdate, +} from "modules/workspaces/useWorkspaceUpdate"; import { type FC, useEffect, useState } from "react"; import { Helmet } from "react-helmet-async"; import { useMutation, useQuery, useQueryClient } from "react-query"; @@ -33,10 +37,6 @@ import { UpdateBuildParametersDialog } from "./UpdateBuildParametersDialog"; import { Workspace } from "./Workspace"; import { WorkspaceDeleteDialog } from "./WorkspaceDeleteDialog"; import type { WorkspacePermissions } from "./permissions"; -import { - useWorkspaceUpdate, - WorkspaceUpdateDialogs, -} from "modules/workspaces/useWorkspaceUpdate"; interface WorkspaceReadyPageProps { template: TypesGen.Template; diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index a399ebfc427a9..551c554fd5ee3 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -1,8 +1,10 @@ +import { getErrorDetail, getErrorMessage } from "api/errors"; import { workspacePermissionsByOrganization } from "api/queries/organizations"; import { templates } from "api/queries/templates"; -import type { Workspace, WorkspacesResponse } from "api/typesGenerated"; +import type { Workspace } from "api/typesGenerated"; import { useFilter } from "components/Filter/Filter"; import { useUserFilterMenu } from "components/Filter/UserFilter"; +import { displayError } from "components/GlobalSnackbar/utils"; import { useAuthenticated } from "hooks"; import { useEffectEvent } from "hooks/hookPolyfills"; import { usePagination } from "hooks/usePagination"; @@ -19,8 +21,6 @@ import { WorkspacesPageView } from "./WorkspacesPageView"; import { useBatchActions } from "./batchActions"; import { useWorkspaceUpdate, useWorkspacesData } from "./data"; import { useStatusFilterMenu, useTemplateFilterMenu } from "./filter/menus"; -import { displayError } from "components/GlobalSnackbar/utils"; -import { getErrorDetail, getErrorMessage } from "api/errors"; function useSafeSearchParams() { // Have to wrap setSearchParams because React Router doesn't make sure that diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index b6d015f177b25..b4b885422b627 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -6,7 +6,7 @@ import StopOutlined from "@mui/icons-material/StopOutlined"; import LoadingButton from "@mui/lab/LoadingButton"; import Divider from "@mui/material/Divider"; import { hasError, isApiValidationError } from "api/errors"; -import type { Template, Workspace, WorkspaceBuild } from "api/typesGenerated"; +import type { Template, Workspace } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Button } from "components/Button/Button"; import { EmptyState } from "components/EmptyState/EmptyState"; diff --git a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx index e8d88104de469..bca28aee7f6ac 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx @@ -2,17 +2,25 @@ import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight"; import Star from "@mui/icons-material/Star"; import Checkbox from "@mui/material/Checkbox"; import Skeleton from "@mui/material/Skeleton"; +import { templateVersion } from "api/queries/templates"; +import { + cancelBuild, + deleteWorkspace, + startWorkspace, + stopWorkspace, +} from "api/queries/workspaces"; import type { Template, Workspace, WorkspaceAgent, WorkspaceApp, - WorkspaceBuild, } from "api/typesGenerated"; import { Avatar } from "components/Avatar/Avatar"; import { AvatarData } from "components/Avatar/AvatarData"; import { AvatarDataSkeleton } from "components/Avatar/AvatarDataSkeleton"; +import { Button } from "components/Button/Button"; import { InfoTooltip } from "components/InfoTooltip/InfoTooltip"; +import { Spinner } from "components/Spinner/Spinner"; import { Stack } from "components/Stack/Stack"; import { StatusIndicator, @@ -31,19 +39,33 @@ import { TableLoaderSkeleton, TableRowSkeleton, } from "components/TableLoader/TableLoader"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "components/Tooltip/Tooltip"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; +import { useAuthenticated } from "hooks"; import { useClickableTableRow } from "hooks/useClickableTableRow"; +import { BanIcon, PlayIcon, RefreshCcwIcon, SquareIcon } from "lucide-react"; import { useDashboard } from "modules/dashboard/useDashboard"; import { WorkspaceAppStatus } from "modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus"; import { WorkspaceDormantBadge } from "modules/workspaces/WorkspaceDormantBadge/WorkspaceDormantBadge"; import { WorkspaceOutdatedTooltip } from "modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip"; +import { abilitiesByWorkspaceStatus } from "modules/workspaces/actions"; +import { + WorkspaceUpdateDialogs, + useWorkspaceUpdate, +} from "modules/workspaces/useWorkspaceUpdate"; import { type FC, type PropsWithChildren, type ReactNode, useMemo, } from "react"; +import { useMutation, useQuery, useQueryClient } from "react-query"; import { useNavigate } from "react-router-dom"; import { cn } from "utils/cn"; import { @@ -53,30 +75,6 @@ import { lastUsedMessage, } from "utils/workspace"; import { WorkspacesEmpty } from "./WorkspacesEmpty"; -import { BanIcon, PlayIcon, RefreshCcwIcon, SquareIcon } from "lucide-react"; -import { Button } from "components/Button/Button"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "components/Tooltip/Tooltip"; -import { useMutation, useQuery, useQueryClient } from "react-query"; -import { - cancelBuild, - deleteWorkspace, - startWorkspace, - stopWorkspace, - updateWorkspace, -} from "api/queries/workspaces"; -import { Spinner } from "components/Spinner/Spinner"; -import { abilitiesByWorkspaceStatus } from "modules/workspaces/actions"; -import { useAuthenticated } from "hooks"; -import { - useWorkspaceUpdate, - WorkspaceUpdateDialogs, -} from "modules/workspaces/useWorkspaceUpdate"; -import { templateVersion } from "api/queries/templates"; dayjs.extend(relativeTime); From 281cf1ad1d1eb1306e3d030387a2f691fa4006c5 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 1 May 2025 17:20:21 +0000 Subject: [PATCH 08/17] Fix WorkspacesPage tests --- site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx index 66388eb3f7dd1..b1ad1d887e53c 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx @@ -264,7 +264,7 @@ describe("WorkspacesPage", () => { await user.click(getWorkspaceCheckbox(workspaces[0])); await user.click(getWorkspaceCheckbox(workspaces[1])); await user.click(screen.getByRole("button", { name: /actions/i })); - const stopButton = await screen.findByText(/stop/i); + const stopButton = await screen.findByRole("menuitem", { name: /stop/i }); await user.click(stopButton); await waitFor(() => { @@ -291,7 +291,7 @@ describe("WorkspacesPage", () => { await user.click(getWorkspaceCheckbox(workspaces[0])); await user.click(getWorkspaceCheckbox(workspaces[1])); await user.click(screen.getByRole("button", { name: /actions/i })); - const startButton = await screen.findByText(/start/i); + const startButton = await screen.findByRole("menuitem", { name: /start/i }); await user.click(startButton); await waitFor(() => { From 71c6369c4f5c5a5568622d5ef30f867da6dfa324 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 1 May 2025 17:20:52 +0000 Subject: [PATCH 09/17] Run FMT --- .../WorkspaceActions/WorkspaceActions.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx index e738b928389b7..b65407806ed69 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx @@ -5,11 +5,6 @@ import HistoryIcon from "@mui/icons-material/HistoryOutlined"; import SettingsIcon from "@mui/icons-material/SettingsOutlined"; import type { Workspace, WorkspaceBuildParameter } from "api/typesGenerated"; import { Button } from "components/Button/Button"; -import { useAuthenticated } from "hooks/useAuthenticated"; -import { - type ActionType, - abilitiesByWorkspaceStatus, -} from "modules/workspaces/actions"; import { DropdownMenu, DropdownMenuContent, @@ -17,7 +12,12 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "components/DropdownMenu/DropdownMenu"; +import { useAuthenticated } from "hooks/useAuthenticated"; import { EllipsisVertical } from "lucide-react"; +import { + type ActionType, + abilitiesByWorkspaceStatus, +} from "modules/workspaces/actions"; import { useWorkspaceDuplication } from "pages/CreateWorkspacePage/useWorkspaceDuplication"; import { type FC, Fragment, type ReactNode, useState } from "react"; import { mustUpdateWorkspace } from "utils/workspace"; From 3502a225c236e7257b6d9393d731ee4529983cc6 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 1 May 2025 17:56:30 +0000 Subject: [PATCH 10/17] Fix WorkspacePage tests --- site/src/api/queries/workspaces.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/site/src/api/queries/workspaces.ts b/site/src/api/queries/workspaces.ts index b993186829597..39008f5c712a3 100644 --- a/site/src/api/queries/workspaces.ts +++ b/site/src/api/queries/workspaces.ts @@ -277,7 +277,10 @@ const updateWorkspaceBuild = async ( build.workspace_owner_name, build.workspace_name, ); - const previousData = queryClient.getQueryData(workspaceKey) as Workspace; + const previousData = queryClient.getQueryData(workspaceKey); + if (!previousData) { + return; + } // Check if the build returned is newer than the previous build that could be // updated from web socket From 7d2b8b3457173d787ac267e4909d62c2c8c5ba6c Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 1 May 2025 17:59:06 +0000 Subject: [PATCH 11/17] Fix storybook failed tests --- site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx index ce2ad840a1df0..84af8a518acd8 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx @@ -10,7 +10,7 @@ import { MockUser, MockWorkspace, } from "testHelpers/entities"; -import { withDashboardProvider } from "testHelpers/storybook"; +import { withAuthProvider, withDashboardProvider } from "testHelpers/storybook"; import { WorkspaceTopbar } from "./WorkspaceTopbar"; // We want a workspace without a deadline to not pollute the screenshot. Also @@ -28,7 +28,7 @@ const baseWorkspace: Workspace = { const meta: Meta = { title: "pages/WorkspacePage/WorkspaceTopbar", component: WorkspaceTopbar, - decorators: [withDashboardProvider], + decorators: [withAuthProvider, withDashboardProvider], args: { workspace: baseWorkspace, template: MockTemplate, @@ -41,6 +41,7 @@ const meta: Meta = { chromatic: { diffThreshold: 0.6, }, + user: MockUser, }, }; From 61dc162b053b3fadc748ef928c7912b876f30dd7 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 1 May 2025 18:11:08 +0000 Subject: [PATCH 12/17] Fix one more storybook test --- site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx index aaf23818c8286..47faef89dea0d 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx @@ -26,7 +26,7 @@ import { MockWorkspaceAppStatus, mockApiError, } from "testHelpers/entities"; -import { withDashboardProvider } from "testHelpers/storybook"; +import { withAuthProvider, withDashboardProvider } from "testHelpers/storybook"; import { WorkspacesPageView } from "./WorkspacesPageView"; const createWorkspace = ( @@ -144,8 +144,10 @@ const meta: Meta = { data: MockBuildInfo, }, ], + user: MockUser, }, decorators: [ + withAuthProvider, withDashboardProvider, (Story) => ( Date: Thu, 1 May 2025 18:21:08 +0000 Subject: [PATCH 13/17] Fixes --- .../pages/WorkspacePage/Workspace.stories.tsx | 4 +- .../pages/WorkspacesPage/WorkspacesTable.tsx | 104 +++++++++--------- 2 files changed, 56 insertions(+), 52 deletions(-) diff --git a/site/src/pages/WorkspacePage/Workspace.stories.tsx b/site/src/pages/WorkspacePage/Workspace.stories.tsx index 88198bdb7b09a..7d29b02c11cb6 100644 --- a/site/src/pages/WorkspacePage/Workspace.stories.tsx +++ b/site/src/pages/WorkspacePage/Workspace.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import type { ProvisionerJobLog } from "api/typesGenerated"; import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext"; import * as Mocks from "testHelpers/entities"; -import { withDashboardProvider } from "testHelpers/storybook"; +import { withAuthProvider, withDashboardProvider } from "testHelpers/storybook"; import { Workspace } from "./Workspace"; import type { WorkspacePermissions } from "./permissions"; @@ -40,8 +40,10 @@ const meta: Meta = { data: Mocks.MockListeningPortsResponse, }, ], + user: Mocks.MockUser, }, decorators: [ + withAuthProvider, withDashboardProvider, (Story) => ( = ({ return ( - {abilities.actions.includes("start") && ( - startWorkspaceMutation.mutate({})} - isLoading={startWorkspaceMutation.isLoading} - label="Start workspace" - > - - - )} - - {abilities.actions.includes("updateAndStart") && ( - <> +
+ {abilities.actions.includes("start") && ( + startWorkspaceMutation.mutate({})} + isLoading={startWorkspaceMutation.isLoading} + label="Start workspace" + > + + + )} + + {abilities.actions.includes("updateAndStart") && ( + <> + { + workspaceUpdate.update(false); + }} + isLoading={workspaceUpdate.isUpdating} + label="Update and start workspace" + > + + + + + )} + + {abilities.actions.includes("stop") && ( { - workspaceUpdate.update(false); + stopWorkspaceMutation.mutate({}); }} - isLoading={workspaceUpdate.isUpdating} - label="Update and start workspace" + isLoading={stopWorkspaceMutation.isLoading} + label="Stop workspace" > - + + + )} + + {abilities.canCancel && ( + + - - - )} - - {abilities.actions.includes("stop") && ( - { - stopWorkspaceMutation.mutate({}); - }} - isLoading={stopWorkspaceMutation.isLoading} - label="Stop workspace" - > - - - )} - - {abilities.canCancel && ( - - - - )} - - {abilities.actions.includes("retry") && ( - - - - )} + )} + + {abilities.actions.includes("retry") && ( + + + + )} +
); }; From 53c433214163826aef869ace658d6e51552fbc53 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 1 May 2025 18:24:57 +0000 Subject: [PATCH 14/17] Adjust skeleton --- site/src/pages/WorkspacesPage/WorkspacesTable.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx index 1db3cceade69b..d0af88653a05d 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx @@ -377,6 +377,9 @@ const TableLoader: FC = ({ canCheckWorkspaces }) => { + + +
From 86337e2c644960b784555834b8f17a72cc526b85 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 1 May 2025 19:13:11 +0000 Subject: [PATCH 15/17] Align inline actions to the right --- site/src/pages/WorkspacesPage/WorkspacesTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx index d0af88653a05d..cb51e4379d67e 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx @@ -533,7 +533,7 @@ const WorkspaceActionsCell: FC = ({ return ( -
+
{abilities.actions.includes("start") && ( startWorkspaceMutation.mutate({})} From b70e28f0745e039866211a6d93a470879ea31fc2 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 6 May 2025 14:10:57 +0000 Subject: [PATCH 16/17] Apply improvements from PR review --- ...eUpdate.tsx => WorkspaceUpdateDialogs.tsx} | 21 +++++++------------ site/src/modules/workspaces/actions.ts | 4 ++-- .../WorkspacePage/WorkspaceReadyPage.tsx | 2 +- .../pages/WorkspacesPage/WorkspacesTable.tsx | 4 ++-- 4 files changed, 12 insertions(+), 19 deletions(-) rename site/src/modules/workspaces/{useWorkspaceUpdate.tsx => WorkspaceUpdateDialogs.tsx} (91%) diff --git a/site/src/modules/workspaces/useWorkspaceUpdate.tsx b/site/src/modules/workspaces/WorkspaceUpdateDialogs.tsx similarity index 91% rename from site/src/modules/workspaces/useWorkspaceUpdate.tsx rename to site/src/modules/workspaces/WorkspaceUpdateDialogs.tsx index 5f3a48dcbfe33..741bc12a6539b 100644 --- a/site/src/modules/workspaces/useWorkspaceUpdate.tsx +++ b/site/src/modules/workspaces/WorkspaceUpdateDialogs.tsx @@ -20,10 +20,7 @@ type UseWorkspaceUpdateOptions = { }; type UseWorkspaceUpdateResult = { - update: ( - hasConfirmed?: boolean, - buildParameters?: WorkspaceBuildParameter[], - ) => void; + update: () => void; isUpdating: boolean; dialogs: { updateConfirmation: UpdateConfirmationDialogProps; @@ -50,15 +47,11 @@ export const useWorkspaceUpdate = ({ onError, }); - const update = ( - hasConfirmed = false, - buildParameters: WorkspaceBuildParameter[] = [], - ) => { - if (!hasConfirmed) { - setIsConfirmingUpdate(true); - return; - } + const update = () => { + setIsConfirmingUpdate(true); + }; + const confirmUpdate = (buildParameters: WorkspaceBuildParameter[] = []) => { updateWorkspaceMutation.mutate(buildParameters); setIsConfirmingUpdate(false); }; @@ -70,7 +63,7 @@ export const useWorkspaceUpdate = ({ updateConfirmation: { open: isConfirmingUpdate, onClose: () => setIsConfirmingUpdate(false), - onConfirm: () => update(true), + onConfirm: () => confirmUpdate(), latestVersion, }, missingBuildParameters: { @@ -80,7 +73,7 @@ export const useWorkspaceUpdate = ({ }, onUpdate: (buildParameters: WorkspaceBuildParameter[]) => { if (updateWorkspaceMutation.error instanceof MissingBuildParameters) { - update(true, buildParameters); + confirmUpdate(buildParameters); } }, }, diff --git a/site/src/modules/workspaces/actions.ts b/site/src/modules/workspaces/actions.ts index 0f871b55458ce..6a255e2cd2c88 100644 --- a/site/src/modules/workspaces/actions.ts +++ b/site/src/modules/workspaces/actions.ts @@ -66,7 +66,7 @@ export const abilitiesByWorkspaceStatus = ( case "starting": { return { actions: ["starting"], - canCancel: true && hasPermissionToCancel, + canCancel: hasPermissionToCancel, canAcceptJobs: false, }; } @@ -91,7 +91,7 @@ export const abilitiesByWorkspaceStatus = ( case "stopping": { return { actions: ["stopping"], - canCancel: true && hasPermissionToCancel, + canCancel: hasPermissionToCancel, canAcceptJobs: false, }; } diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index a6c634082c0eb..9ae072e420dd1 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -26,7 +26,7 @@ import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; import { WorkspaceUpdateDialogs, useWorkspaceUpdate, -} from "modules/workspaces/useWorkspaceUpdate"; +} from "modules/workspaces/WorkspaceUpdateDialogs"; import { type FC, useEffect, useState } from "react"; import { Helmet } from "react-helmet-async"; import { useMutation, useQuery, useQueryClient } from "react-query"; diff --git a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx index cb51e4379d67e..4689a175e1b9a 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx @@ -54,11 +54,11 @@ import { useDashboard } from "modules/dashboard/useDashboard"; import { WorkspaceAppStatus } from "modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus"; import { WorkspaceDormantBadge } from "modules/workspaces/WorkspaceDormantBadge/WorkspaceDormantBadge"; import { WorkspaceOutdatedTooltip } from "modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip"; -import { abilitiesByWorkspaceStatus } from "modules/workspaces/actions"; import { WorkspaceUpdateDialogs, useWorkspaceUpdate, -} from "modules/workspaces/useWorkspaceUpdate"; +} from "modules/workspaces/WorkspaceUpdateDialogs"; +import { abilitiesByWorkspaceStatus } from "modules/workspaces/actions"; import { type FC, type PropsWithChildren, From c1d304673177a909dd105f331d771b13955395ca Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 6 May 2025 14:19:58 +0000 Subject: [PATCH 17/17] Fix lint --- site/src/pages/WorkspacesPage/WorkspacesTable.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx index 4689a175e1b9a..2fe94e0260a8f 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx @@ -547,9 +547,7 @@ const WorkspaceActionsCell: FC = ({ {abilities.actions.includes("updateAndStart") && ( <> { - workspaceUpdate.update(false); - }} + onClick={workspaceUpdate.update} isLoading={workspaceUpdate.isUpdating} label="Update and start workspace" >