diff --git a/site/src/components/Badge/Badge.tsx b/site/src/components/Badge/Badge.tsx index 8995222027ed0..e6b23b8a4dd94 100644 --- a/site/src/components/Badge/Badge.tsx +++ b/site/src/components/Badge/Badge.tsx @@ -19,7 +19,7 @@ const badgeVariants = cva( warning: "border border-solid border-border-warning bg-surface-orange text-content-warning shadow", destructive: - "border border-solid border-border-destructive bg-surface-red text-content-highlight-red shadow", + "border border-solid border-border-destructive bg-surface-red text-highlight-red shadow", }, size: { xs: "text-2xs font-regular h-5 [&_svg]:hidden rounded px-1.5", diff --git a/site/src/hooks/usePagination.ts b/site/src/hooks/usePagination.ts index 2ab409e85c9d8..72ea70868fb30 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 = calcOffset(page, limit); + const offset = page <= 0 ? 0 : (page - 1) * limit; const goToPage = (page: number) => { searchParams.set("page", page.toString()); @@ -23,7 +23,3 @@ export const usePagination = ({ offset, }; }; - -export const calcOffset = (page: number, limit: number) => { - return page <= 0 ? 0 : (page - 1) * limit; -}; diff --git a/site/src/modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.stories.tsx b/site/src/modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.stories.tsx index bb0c93e74c8c6..843f8131b793f 100644 --- a/site/src/modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.stories.tsx +++ b/site/src/modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.stories.tsx @@ -1,7 +1,10 @@ -import { action } from "@storybook/addon-actions"; import type { Meta, StoryObj } from "@storybook/react"; import { expect, userEvent, waitFor, within } from "@storybook/test"; -import { MockTemplate, MockTemplateVersion } from "testHelpers/entities"; +import { + MockTemplate, + MockTemplateVersion, + MockWorkspace, +} from "testHelpers/entities"; import { withDashboardProvider } from "testHelpers/storybook"; import { WorkspaceOutdatedTooltip } from "./WorkspaceOutdatedTooltip"; @@ -18,9 +21,11 @@ const meta: Meta = { ], }, args: { - onUpdateVersion: action("onUpdateVersion"), - templateName: MockTemplate.display_name, - latestVersionId: MockTemplateVersion.id, + workspace: { + ...MockWorkspace, + template_name: MockTemplate.display_name, + template_active_version_id: MockTemplateVersion.id, + }, }, }; @@ -29,14 +34,12 @@ type Story = StoryObj; const Example: Story = { play: async ({ canvasElement, step }) => { - const screen = within(canvasElement); + const body = within(canvasElement.ownerDocument.body); await step("activate hover trigger", async () => { - await userEvent.hover(screen.getByRole("button")); + await userEvent.hover(body.getByRole("button")); await waitFor(() => - expect( - screen.getByText(MockTemplateVersion.message), - ).toBeInTheDocument(), + expect(body.getByText(MockTemplateVersion.message)).toBeInTheDocument(), ); }); }, diff --git a/site/src/modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.tsx b/site/src/modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.tsx index ada4c63d9a0bb..9615560840c59 100644 --- a/site/src/modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.tsx +++ b/site/src/modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.tsx @@ -3,7 +3,10 @@ import InfoIcon from "@mui/icons-material/InfoOutlined"; import RefreshIcon from "@mui/icons-material/Refresh"; import Link from "@mui/material/Link"; import Skeleton from "@mui/material/Skeleton"; +import { getErrorDetail, getErrorMessage } from "api/errors"; import { templateVersion } from "api/queries/templates"; +import type { Workspace } from "api/typesGenerated"; +import { displayError } from "components/GlobalSnackbar/utils"; import { HelpTooltip, HelpTooltipAction, @@ -17,102 +20,99 @@ import { usePopover } from "components/deprecated/Popover/Popover"; import { linkToTemplate, useLinks } from "modules/navigation"; import type { FC } from "react"; import { useQuery } from "react-query"; - -const Language = { - outdatedLabel: "Outdated", - versionTooltipText: - "This workspace version is outdated and a newer version is available.", - updateVersionLabel: "Update", -}; +import { + WorkspaceUpdateDialogs, + useWorkspaceUpdate, +} from "../WorkspaceUpdateDialogs"; interface TooltipProps { - organizationName: string; - templateName: string; - latestVersionId: string; - onUpdateVersion: () => void; - ariaLabel?: string; + workspace: Workspace; } export const WorkspaceOutdatedTooltip: FC = (props) => { return ( - + + Outdated info - ); }; -const WorkspaceOutdatedTooltipContent: FC = ({ - organizationName, - templateName, - latestVersionId, - onUpdateVersion, - ariaLabel, -}) => { +const WorkspaceOutdatedTooltipContent: FC = ({ workspace }) => { const getLink = useLinks(); const theme = useTheme(); const popover = usePopover(); const { data: activeVersion } = useQuery({ - ...templateVersion(latestVersionId), + ...templateVersion(workspace.template_active_version_id), enabled: popover.open, }); + const updateWorkspace = useWorkspaceUpdate({ + workspace, + latestVersion: activeVersion, + onError: (error) => { + displayError( + getErrorMessage(error, "Error updating workspace"), + getErrorDetail(error), + ); + }, + }); const versionLink = `${getLink( - linkToTemplate(organizationName, templateName), + linkToTemplate(workspace.organization_name, workspace.template_name), )}`; return ( - - {Language.outdatedLabel} - {Language.versionTooltipText} + <> + + Outdated + + This workspace version is outdated and a newer version is available. + -
-
-
New version
-
- {activeVersion ? ( - - {activeVersion.name} - - ) : ( - - )} +
+
+
New version
+
+ {activeVersion ? ( + + {activeVersion.name} + + ) : ( + + )} +
-
-
-
Message
-
- {activeVersion ? ( - activeVersion.message || "No message" - ) : ( - - )} +
+
Message
+
+ {activeVersion ? ( + activeVersion.message || "No message" + ) : ( + + )} +
-
- - - {Language.updateVersionLabel} - - - + + + Update + + + + + ); }; diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/useWorkspacesToBeDeleted.ts b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/useWorkspacesToBeDeleted.ts index 5a974d5d8fe31..d55f0b6c54fff 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/useWorkspacesToBeDeleted.ts +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/useWorkspacesToBeDeleted.ts @@ -1,7 +1,7 @@ +import { workspaces } from "api/queries/workspaces"; import type { Template, Workspace } from "api/typesGenerated"; import { compareAsc } from "date-fns"; -import { calcOffset } from "hooks/usePagination"; -import { useWorkspacesData } from "pages/WorkspacesPage/data"; +import { useQuery } from "react-query"; import type { TemplateScheduleFormValues } from "./formHelpers"; export const useWorkspacesToGoDormant = ( @@ -9,11 +9,11 @@ export const useWorkspacesToGoDormant = ( formValues: TemplateScheduleFormValues, fromDate: Date, ) => { - const { data } = useWorkspacesData({ - offset: calcOffset(0, 0), - limit: 0, - q: `template:${template.name}`, - }); + const { data } = useQuery( + workspaces({ + q: `template:${template.name}`, + }), + ); return data?.workspaces?.filter((workspace: Workspace) => { if (!formValues.time_til_dormant_ms) { @@ -40,11 +40,12 @@ export const useWorkspacesToBeDeleted = ( formValues: TemplateScheduleFormValues, fromDate: Date, ) => { - const { data } = useWorkspacesData({ - offset: calcOffset(0, 0), - limit: 0, - q: `template:${template.name} dormant:true`, - }); + const { data } = useQuery( + workspaces({ + q: `template:${template.name} dormant:true`, + }), + ); + return data?.workspaces?.filter((workspace: Workspace) => { if (!workspace.dormant_at || !formValues.time_til_dormant_autodelete_ms) { return false; diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index 551c554fd5ee3..d0439be0f0462 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -1,6 +1,7 @@ import { getErrorDetail, getErrorMessage } from "api/errors"; import { workspacePermissionsByOrganization } from "api/queries/organizations"; import { templates } from "api/queries/templates"; +import { workspaces } from "api/queries/workspaces"; import type { Workspace } from "api/typesGenerated"; import { useFilter } from "components/Filter/Filter"; import { useUserFilterMenu } from "components/Filter/UserFilter"; @@ -19,7 +20,6 @@ import { BatchDeleteConfirmation } from "./BatchDeleteConfirmation"; import { BatchUpdateConfirmation } from "./BatchUpdateConfirmation"; import { WorkspacesPageView } from "./WorkspacesPageView"; import { useBatchActions } from "./batchActions"; -import { useWorkspaceUpdate, useWorkspacesData } from "./data"; import { useStatusFilterMenu, useTemplateFilterMenu } from "./filter/menus"; function useSafeSearchParams() { @@ -45,9 +45,7 @@ const WorkspacesPage: FC = () => { const pagination = usePagination({ searchParamsResult }); const { permissions, user: me } = useAuthenticated(); const { entitlements } = useDashboard(); - const templatesQuery = useQuery(templates()); - const workspacePermissionsQuery = useQuery( workspacePermissionsByOrganization( templatesQuery.data?.map((template) => template.organization_id), @@ -73,12 +71,17 @@ const WorkspacesPage: FC = () => { onFilterChange: () => pagination.goToPage(1), }); - const { data, error, queryKey, refetch } = useWorkspacesData({ + const workspacesQueryOptions = workspaces({ ...pagination, q: filterProps.filter.query, }); + const { data, error, refetch } = useQuery({ + ...workspacesQueryOptions, + refetchInterval: (_, query) => { + return query.state.error ? false : 5_000; + }, + }); - const updateWorkspace = useWorkspaceUpdate(queryKey); const [checkedWorkspaces, setCheckedWorkspaces] = useState< readonly Workspace[] >([]); @@ -123,9 +126,6 @@ const WorkspacesPage: FC = () => { limit={pagination.limit} onPageChange={pagination.goToPage} filterProps={filterProps} - onUpdateWorkspace={(workspace) => { - updateWorkspace.mutate(workspace); - }} isRunningBatchAction={batchActions.isLoading} onDeleteAll={() => setConfirmingBatchAction("delete")} onUpdateAll={() => setConfirmingBatchAction("update")} @@ -133,7 +133,7 @@ const WorkspacesPage: FC = () => { onStopAll={() => batchActions.stopAll(checkedWorkspaces)} onActionSuccess={async () => { await queryClient.invalidateQueries({ - queryKey, + queryKey: workspacesQueryOptions.queryKey, }); }} onActionError={(error) => { diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index 6db41fef8fa6c..6633f884e1263 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -53,7 +53,6 @@ export interface WorkspacesPageViewProps { page: number; limit: number; onPageChange: (page: number) => void; - onUpdateWorkspace: (workspace: Workspace) => void; onCheckChange: (checkedWorkspaces: readonly Workspace[]) => void; isRunningBatchAction: boolean; onDeleteAll: () => void; @@ -76,7 +75,6 @@ export const WorkspacesPageView: FC = ({ count, filterProps, onPageChange, - onUpdateWorkspace, page, checkedWorkspaces, onCheckChange, @@ -223,7 +221,6 @@ export const WorkspacesPageView: FC = ({ canCreateTemplate={canCreateTemplate} workspaces={workspaces} isUsingFilter={filterProps.filter.used} - onUpdateWorkspace={onUpdateWorkspace} checkedWorkspaces={checkedWorkspaces} onCheckChange={onCheckChange} canCheckWorkspaces={canCheckWorkspaces} diff --git a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx index 92dcee60dec96..b4f1c98b27261 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx @@ -97,7 +97,6 @@ export interface WorkspacesTableProps { checkedWorkspaces: readonly Workspace[]; error?: unknown; isUsingFilter: boolean; - onUpdateWorkspace: (workspace: Workspace) => void; onCheckChange: (checkedWorkspaces: readonly Workspace[]) => void; canCheckWorkspaces: boolean; templates?: Template[]; @@ -110,7 +109,6 @@ export const WorkspacesTable: FC = ({ workspaces, checkedWorkspaces, isUsingFilter, - onUpdateWorkspace, onCheckChange, canCheckWorkspaces, templates, @@ -243,16 +241,7 @@ export const WorkspacesTable: FC = ({ {workspace.name} {workspace.favorite && } {workspace.outdated && ( - { - onUpdateWorkspace(workspace); - }} - /> + )} } diff --git a/site/src/pages/WorkspacesPage/data.ts b/site/src/pages/WorkspacesPage/data.ts deleted file mode 100644 index 764ea218aa96c..0000000000000 --- a/site/src/pages/WorkspacesPage/data.ts +++ /dev/null @@ -1,112 +0,0 @@ -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"; -import { useState } from "react"; -import { - type QueryKey, - useMutation, - useQuery, - useQueryClient, -} from "react-query"; - -export const useWorkspacesData = (req: WorkspacesRequest) => { - const [shouldRefetch, setShouldRefetch] = useState(true); - const workspacesQueryOptions = workspaces(req); - const result = useQuery({ - ...workspacesQueryOptions, - onSuccess: () => { - setShouldRefetch(true); - }, - onError: () => { - setShouldRefetch(false); - }, - refetchInterval: shouldRefetch ? 5_000 : undefined, - }); - - return { - ...result, - queryKey: workspacesQueryOptions.queryKey, - }; -}; - -export const useWorkspaceUpdate = (queryKey: QueryKey) => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: API.updateWorkspaceVersion, - onMutate: async (workspace) => { - await queryClient.cancelQueries({ queryKey }); - queryClient.setQueryData(queryKey, (oldResponse) => { - if (oldResponse) { - return assignPendingStatus(oldResponse, workspace); - } - }); - }, - onSuccess: (workspaceBuild) => { - queryClient.setQueryData(queryKey, (oldResponse) => { - if (oldResponse) { - return assignLatestBuild(oldResponse, workspaceBuild); - } - }); - }, - onError: (error) => { - const message = getErrorMessage( - error, - "Error updating workspace version", - ); - displayError(message); - }, - }); -}; - -const assignLatestBuild = ( - oldResponse: WorkspacesResponse, - build: WorkspaceBuild, -): WorkspacesResponse => { - return { - ...oldResponse, - workspaces: oldResponse.workspaces.map((workspace) => { - if (workspace.id === build.workspace_id) { - return { - ...workspace, - latest_build: build, - }; - } - - return workspace; - }), - }; -}; - -const assignPendingStatus = ( - oldResponse: WorkspacesResponse, - workspace: Workspace, -): WorkspacesResponse => { - return { - ...oldResponse, - workspaces: oldResponse.workspaces.map((workspaceItem) => { - if (workspaceItem.id === workspace.id) { - return { - ...workspace, - latest_build: { - ...workspace.latest_build, - status: "pending", - job: { - ...workspace.latest_build.job, - status: "pending", - }, - }, - } as Workspace; - } - - return workspaceItem; - }), - }; -};