chore: simplify workspaces data fetching (#17703)

We've been using an abstraction that was not necessary to fetch
workspaces data. I also took sometime to use the new useWorkspaceUpdate
hook in the update workspace tooltip that was missing some important
steps like confirmation.
This commit is contained in:
Bruno Quaresma
2025-05-08 09:42:39 -03:00
committed by GitHub
parent 2695f4e950
commit 4341403346
9 changed files with 105 additions and 231 deletions
+1 -1
View File
@@ -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",
+1 -5
View File
@@ -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;
};
@@ -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<typeof WorkspaceOutdatedTooltip> = {
],
},
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<typeof WorkspaceOutdatedTooltip>;
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(),
);
});
},
@@ -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<TooltipProps> = (props) => {
return (
<HelpTooltip>
<HelpTooltipTrigger
size="small"
aria-label="More info"
hoverEffect={false}
>
<HelpTooltipTrigger size="small" hoverEffect={false}>
<InfoIcon css={styles.icon} />
<span className="sr-only">Outdated info</span>
</HelpTooltipTrigger>
<WorkspaceOutdatedTooltipContent {...props} />
</HelpTooltip>
);
};
const WorkspaceOutdatedTooltipContent: FC<TooltipProps> = ({
organizationName,
templateName,
latestVersionId,
onUpdateVersion,
ariaLabel,
}) => {
const WorkspaceOutdatedTooltipContent: FC<TooltipProps> = ({ 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 (
<HelpTooltipContent>
<HelpTooltipTitle>{Language.outdatedLabel}</HelpTooltipTitle>
<HelpTooltipText>{Language.versionTooltipText}</HelpTooltipText>
<>
<HelpTooltipContent disablePortal={false}>
<HelpTooltipTitle>Outdated</HelpTooltipTitle>
<HelpTooltipText>
This workspace version is outdated and a newer version is available.
</HelpTooltipText>
<div css={styles.container}>
<div css={{ lineHeight: "1.6" }}>
<div css={styles.bold}>New version</div>
<div>
{activeVersion ? (
<Link
href={`${versionLink}/versions/${activeVersion.name}`}
target="_blank"
css={{ color: theme.palette.primary.light }}
>
{activeVersion.name}
</Link>
) : (
<Skeleton variant="text" height={20} width={100} />
)}
<div css={styles.container}>
<div css={{ lineHeight: "1.6" }}>
<div css={styles.bold}>New version</div>
<div>
{activeVersion ? (
<Link
href={`${versionLink}/versions/${activeVersion.name}`}
target="_blank"
css={{ color: theme.palette.primary.light }}
>
{activeVersion.name}
</Link>
) : (
<Skeleton variant="text" height={20} width={100} />
)}
</div>
</div>
<div css={{ lineHeight: "1.6" }}>
<div css={styles.bold}>Message</div>
<div>
{activeVersion ? (
activeVersion.message || "No message"
) : (
<Skeleton variant="text" height={20} width={150} />
)}
</div>
</div>
</div>
<div css={{ lineHeight: "1.6" }}>
<div css={styles.bold}>Message</div>
<div>
{activeVersion ? (
activeVersion.message || "No message"
) : (
<Skeleton variant="text" height={20} width={150} />
)}
</div>
</div>
</div>
<HelpTooltipLinksGroup>
<HelpTooltipAction
icon={RefreshIcon}
onClick={onUpdateVersion}
ariaLabel={ariaLabel}
>
{Language.updateVersionLabel}
</HelpTooltipAction>
</HelpTooltipLinksGroup>
</HelpTooltipContent>
<HelpTooltipLinksGroup>
<HelpTooltipAction
icon={RefreshIcon}
onClick={updateWorkspace.update}
>
Update
</HelpTooltipAction>
</HelpTooltipLinksGroup>
</HelpTooltipContent>
<WorkspaceUpdateDialogs {...updateWorkspace.dialogs} />
</>
);
};
@@ -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;
@@ -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) => {
@@ -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<WorkspacesPageViewProps> = ({
count,
filterProps,
onPageChange,
onUpdateWorkspace,
page,
checkedWorkspaces,
onCheckChange,
@@ -223,7 +221,6 @@ export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({
canCreateTemplate={canCreateTemplate}
workspaces={workspaces}
isUsingFilter={filterProps.filter.used}
onUpdateWorkspace={onUpdateWorkspace}
checkedWorkspaces={checkedWorkspaces}
onCheckChange={onCheckChange}
canCheckWorkspaces={canCheckWorkspaces}
@@ -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<WorkspacesTableProps> = ({
workspaces,
checkedWorkspaces,
isUsingFilter,
onUpdateWorkspace,
onCheckChange,
canCheckWorkspaces,
templates,
@@ -243,16 +241,7 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
{workspace.name}
{workspace.favorite && <Star className="w-4 h-4" />}
{workspace.outdated && (
<WorkspaceOutdatedTooltip
organizationName={workspace.organization_name}
templateName={workspace.template_name}
latestVersionId={
workspace.template_active_version_id
}
onUpdateVersion={() => {
onUpdateWorkspace(workspace);
}}
/>
<WorkspaceOutdatedTooltip workspace={workspace} />
)}
</Stack>
}
-112
View File
@@ -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<WorkspacesResponse>(queryKey, (oldResponse) => {
if (oldResponse) {
return assignPendingStatus(oldResponse, workspace);
}
});
},
onSuccess: (workspaceBuild) => {
queryClient.setQueryData<WorkspacesResponse>(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;
}),
};
};