mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
+13
-10
@@ -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} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+13
-12
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}),
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user