Skip to content

Commit e25e7fe

Browse files
committed
🪟 🎉 Paginated querying for workspaces picker (#9580)
1 parent b204cc1 commit e25e7fe

File tree

8 files changed

+83
-60
lines changed

8 files changed

+83
-60
lines changed

airbyte-webapp/src/components/workspace/WorkspacesPicker.tsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,9 @@ import { Icon } from "components/ui/Icon";
88
import { Text } from "components/ui/Text";
99

1010
import { useCurrentWorkspace } from "core/api";
11-
import { CloudWorkspaceReadList } from "core/api/types/CloudApi";
12-
import { WorkspaceReadList } from "core/request/AirbyteClient";
1311

1412
import styles from "./WorkspacesPicker.module.scss";
15-
import { WorkspacesPickerList } from "./WorkspacesPickerList";
13+
import { WorkspaceFetcher, WorkspacesPickerList } from "./WorkspacesPickerList";
1614

1715
const WorkspaceButton = React.forwardRef<HTMLButtonElement | null, React.ButtonHTMLAttributes<HTMLButtonElement>>(
1816
({ children, ...props }, ref) => {
@@ -26,11 +24,7 @@ const WorkspaceButton = React.forwardRef<HTMLButtonElement | null, React.ButtonH
2624

2725
WorkspaceButton.displayName = "WorkspaceButton";
2826

29-
interface WorkspacePickerProps {
30-
workspaces?: WorkspaceReadList | CloudWorkspaceReadList;
31-
loading: boolean;
32-
}
33-
export const WorkspacesPicker: React.FC<WorkspacePickerProps> = ({ workspaces, loading }) => {
27+
export const WorkspacesPicker: React.FC<{ useFetchWorkspaces: WorkspaceFetcher }> = ({ useFetchWorkspaces }) => {
3428
const currentWorkspace = useCurrentWorkspace();
3529

3630
const { x, y, reference, floating, strategy } = useFloating({
@@ -70,7 +64,7 @@ export const WorkspacesPicker: React.FC<WorkspacePickerProps> = ({ workspaces, l
7064
{currentWorkspace.name}
7165
</Text>
7266
</Box>
73-
<WorkspacesPickerList loading={loading} workspaces={workspaces} closePicker={close} />
67+
<WorkspacesPickerList useFetchWorkspaces={useFetchWorkspaces} closePicker={close} />
7468
</div>
7569
</Popover.Panel>
7670
</>

airbyte-webapp/src/components/workspace/WorkspacesPickerList.tsx

Lines changed: 57 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { HTMLAttributes, useEffect, useMemo, useRef, useState, forwardRef, Ref } from "react";
1+
import { UseInfiniteQueryResult } from "@tanstack/react-query";
2+
import { HTMLAttributes, useRef, forwardRef, Ref, useState } from "react";
23
import { FormattedMessage } from "react-intl";
3-
import { useLocation, useUpdateEffect } from "react-use";
4+
import { useDebounce, useLocation, useUpdateEffect } from "react-use";
45
import { Virtuoso, ItemContent, ComputeItemKey, VirtuosoHandle } from "react-virtuoso";
56

67
import { Box } from "components/ui/Box";
@@ -13,13 +14,24 @@ import { Text } from "components/ui/Text";
1314
import { CloudWorkspaceRead, CloudWorkspaceReadList } from "core/api/types/CloudApi";
1415
import { WorkspaceRead, WorkspaceReadList } from "core/request/AirbyteClient";
1516
import { RoutePaths } from "pages/routePaths";
17+
import { WORKSPACE_LIST_LENGTH } from "pages/workspaces/WorkspacesPage";
1618

1719
import styles from "./WorkspacesPickerList.module.scss";
1820

21+
export type WorkspaceFetcher = (
22+
pageSize: number,
23+
nameContains: string
24+
) => UseInfiniteQueryResult<
25+
{
26+
data: CloudWorkspaceReadList | WorkspaceReadList;
27+
pageParam: number;
28+
},
29+
unknown
30+
>;
31+
1932
interface WorkspacePickerListProps {
20-
loading: boolean;
21-
workspaces?: CloudWorkspaceReadList | WorkspaceReadList;
2233
closePicker: () => void;
34+
useFetchWorkspaces: WorkspaceFetcher;
2335
}
2436

2537
const ListRow: ItemContent<CloudWorkspaceRead | WorkspaceRead, null> = (_index, workspace) => {
@@ -49,39 +61,52 @@ const UlList = forwardRef<HTMLDivElement>((props, ref) => (
4961
));
5062
UlList.displayName = "UlList";
5163

52-
export const WorkspacesPickerList: React.FC<WorkspacePickerListProps> = ({ loading, closePicker, workspaces }) => {
53-
const [workspaceFilter, setWorkspaceFilter] = useState("");
64+
export const WorkspacesPickerList: React.FC<WorkspacePickerListProps> = ({ closePicker, useFetchWorkspaces }) => {
5465
const location = useLocation();
5566

67+
const [searchValue, setSearchValue] = useState("");
68+
const [debouncedSearchValue, setDebouncedSearchValue] = useState("");
69+
70+
const {
71+
data: workspacesData,
72+
hasNextPage,
73+
fetchNextPage,
74+
isLoading,
75+
isFetchingNextPage,
76+
} = useFetchWorkspaces(WORKSPACE_LIST_LENGTH, debouncedSearchValue);
77+
78+
const workspaces =
79+
workspacesData?.pages.flatMap<CloudWorkspaceRead | WorkspaceRead>((page) => page.data.workspaces) ?? [];
80+
81+
const handleEndReached = () => {
82+
if (hasNextPage) {
83+
fetchNextPage();
84+
}
85+
};
86+
87+
useDebounce(
88+
() => {
89+
setDebouncedSearchValue(searchValue);
90+
virtuosoRef.current?.scrollTo({ top: 0 });
91+
},
92+
250,
93+
[searchValue]
94+
);
95+
5696
useUpdateEffect(() => {
5797
closePicker();
5898
}, [closePicker, location.pathname, location.search]);
5999

60-
const filteredWorkspaces = useMemo(() => {
61-
const filterableWorkspaces = workspaces?.workspaces as Array<WorkspaceRead | CloudWorkspaceRead>;
62-
63-
return (
64-
filterableWorkspaces.filter((workspace) => {
65-
return workspace.name?.toLowerCase().includes(workspaceFilter.toLowerCase());
66-
}) ?? []
67-
);
68-
}, [workspaceFilter, workspaces]);
69-
70100
const virtuosoRef = useRef<VirtuosoHandle | null>(null);
71-
useEffect(() => {
72-
virtuosoRef.current?.scrollTo({ top: 0 });
73-
}, [filteredWorkspaces]);
74101

75-
return loading ? (
102+
return isLoading ? (
76103
<Box p="lg">
77104
<LoadingSpinner />
78105
</Box>
79106
) : (
80107
<div>
81-
<div className={styles.workspaceSearch}>
82-
<SearchInput value={workspaceFilter} onChange={(e) => setWorkspaceFilter(e.target.value)} inline />
83-
</div>
84-
{!filteredWorkspaces.length ? (
108+
<SearchInput value={searchValue} onChange={(e) => setSearchValue(e.target.value)} inline />
109+
{!workspaces || !workspaces.length ? (
85110
<Box p="md">
86111
<FormattedMessage id="workspaces.noWorkspaces" />
87112
</Box>
@@ -95,7 +120,9 @@ export const WorkspacesPickerList: React.FC<WorkspacePickerListProps> = ({ loadi
95120
),
96121
width: "100%",
97122
}}
98-
data={filteredWorkspaces}
123+
data={workspaces}
124+
endReached={handleEndReached}
125+
increaseViewportBy={5}
99126
defaultItemHeight={37 /* single-line workspaces are 36.59 pixels in Chrome */}
100127
computeItemKey={computeItemKey}
101128
components={{
@@ -109,7 +136,11 @@ export const WorkspacesPickerList: React.FC<WorkspacePickerListProps> = ({ loadi
109136
itemContent={ListRow}
110137
/>
111138
)}
112-
139+
{isFetchingNextPage && (
140+
<Box pt="sm">
141+
<LoadingSpinner />
142+
</Box>
143+
)}
113144
<Box py="lg">
114145
<Link variant="primary" to={`/${RoutePaths.Workspaces}`}>
115146
<Text color="blue" size="md" bold align="center">

airbyte-webapp/src/core/api/hooks/cloud/cloudWorkspaces.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { useCurrentWorkspace } from "../workspaces";
2727
export const workspaceKeys = {
2828
all: [SCOPE_USER, "cloud_workspaces"] as const,
2929
lists: () => [...workspaceKeys.all, "list"] as const,
30-
list: (filters: string) => [...workspaceKeys.lists(), { filters }] as const,
30+
list: (filters: string | Record<string, string>) => [...workspaceKeys.lists(), { filters }] as const,
3131
detail: (id: number | string) => [...workspaceKeys.all, "detail", id] as const,
3232
usage: (id: number | string, timeWindow: string) => [...workspaceKeys.all, id, timeWindow, "usage"] as const,
3333
};
@@ -77,12 +77,12 @@ export function useCreateCloudWorkspace() {
7777
);
7878
}
7979

80-
export const useListCloudWorkspacesInfinite = (pageSize: number, nameContains?: string) => {
80+
export const useListCloudWorkspacesInfinite = (pageSize: number, nameContains: string) => {
8181
const { userId } = useCurrentUser();
8282
const requestOptions = useRequestOptions();
8383

8484
return useInfiniteQuery(
85-
workspaceKeys.list(`paginated`),
85+
workspaceKeys.list({ pageSize: pageSize.toString(), nameContains }),
8686
async ({ pageParam = 0 }: { pageParam?: number }) => {
8787
return {
8888
data: await webBackendListWorkspacesByUserPaginated(
@@ -96,6 +96,7 @@ export const useListCloudWorkspacesInfinite = (pageSize: number, nameContains?:
9696
suspense: true,
9797
getPreviousPageParam: (firstPage) => (firstPage.pageParam > 0 ? firstPage.pageParam - 1 : undefined),
9898
getNextPageParam: (lastPage) => (lastPage.data.workspaces.length < pageSize ? undefined : lastPage.pageParam + 1),
99+
cacheTime: 10000,
99100
}
100101
);
101102
};

airbyte-webapp/src/core/api/hooks/workspaces.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import { useSuspenseQuery } from "../useSuspenseQuery";
2929
export const workspaceKeys = {
3030
all: [SCOPE_USER, "workspaces"] as const,
3131
lists: () => [...workspaceKeys.all, "list"] as const,
32-
list: (filters: string) => [...workspaceKeys.lists(), { filters }] as const,
32+
list: (filters: string | Record<string, string>) => [...workspaceKeys.lists(), { filters }] as const,
3333
allListUsers: [SCOPE_WORKSPACE, "users", "list"] as const,
3434
listUsers: (workspaceId: string) => [SCOPE_WORKSPACE, "users", "list", workspaceId] as const,
3535
detail: (workspaceId: string) => [...workspaceKeys.all, "details", workspaceId] as const,
@@ -149,25 +149,24 @@ export const useListUsersInWorkspace = (workspaceId: string) => {
149149
return useSuspenseQuery(queryKey, () => listUsersInWorkspace({ workspaceId }, requestOptions));
150150
};
151151

152-
export const useListWorkspacesInfinite = (pageSize: number, nameContains?: string) => {
152+
export const useListWorkspacesInfinite = (pageSize: number, nameContains: string) => {
153153
const { userId } = useCurrentUser();
154154
const requestOptions = useRequestOptions();
155155

156156
return useInfiniteQuery(
157-
workspaceKeys.list(`paginated`),
158-
async ({ pageParam = 0 }: { pageParam?: number }) => {
157+
workspaceKeys.list({ pageSize: pageSize.toString(), nameContains }),
158+
async ({ pageParam = 0 }) => {
159+
const rowOffset = pageParam * pageSize;
159160
return {
160-
data: await listWorkspacesByUser(
161-
{ userId, pagination: { pageSize, rowOffset: pageParam * pageSize }, nameContains },
162-
requestOptions
163-
),
161+
data: await listWorkspacesByUser({ userId, pagination: { pageSize, rowOffset }, nameContains }, requestOptions),
164162
pageParam,
165163
};
166164
},
167165
{
168166
suspense: true,
169167
getPreviousPageParam: (firstPage) => (firstPage.pageParam > 0 ? firstPage.pageParam - 1 : undefined),
170168
getNextPageParam: (lastPage) => (lastPage.data.workspaces.length < pageSize ? undefined : lastPage.pageParam + 1),
169+
cacheTime: 10000,
171170
}
172171
);
173172
};

airbyte-webapp/src/packages/cloud/views/layout/CloudMainView/CloudMainView.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { ThemeToggle } from "components/ui/ThemeToggle";
1111
import { WorkspacesPicker } from "components/workspace/WorkspacesPicker";
1212

1313
import { useCurrentOrganizationInfo, useCurrentWorkspace } from "core/api";
14-
import { useGetCloudWorkspaceAsync, useListCloudWorkspacesAsync } from "core/api/cloud";
14+
import { useGetCloudWorkspaceAsync, useListCloudWorkspacesInfinite } from "core/api/cloud";
1515
import { CloudWorkspaceReadWorkspaceTrialStatus as WorkspaceTrialStatus } from "core/api/types/CloudApi";
1616
import { useAuthService } from "core/services/auth";
1717
import { FeatureItem, useFeature } from "core/services/features";
@@ -41,7 +41,6 @@ const CloudMainView: React.FC<React.PropsWithChildren<unknown>> = (props) => {
4141
const workspace = useCurrentWorkspace();
4242
const organization = useCurrentOrganizationInfo();
4343
const cloudWorkspace = useGetCloudWorkspaceAsync(workspace.workspaceId);
44-
const { data: workspaces, isLoading } = useListCloudWorkspacesAsync();
4544

4645
const isShowAdminWarningEnabled = useFeature(FeatureItem.ShowAdminWarningInWorkspace);
4746
const isNewTrialPolicy = useExperiment("billing.newTrialPolicy", false);
@@ -65,7 +64,7 @@ const CloudMainView: React.FC<React.PropsWithChildren<unknown>> = (props) => {
6564
<SideBar>
6665
<AirbyteHomeLink />
6766
{isShowAdminWarningEnabled && <AdminWorkspaceWarning />}
68-
<WorkspacesPicker loading={isLoading} workspaces={workspaces} />
67+
<WorkspacesPicker useFetchWorkspaces={useListCloudWorkspacesInfinite} />
6968
<MenuContent>
7069
<MainNavItems />
7170
<MenuContent>

airbyte-webapp/src/packages/cloud/views/workspaces/WorkspacesPage/CloudWorkspacesPage.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,16 @@ export const CloudWorkspacesPage: React.FC = () => {
2929
const { isLoading, mutateAsync: handleLogout } = useMutation(() => logout?.() ?? Promise.resolve());
3030
useTrackPage(PageTrackingCodes.WORKSPACES);
3131
const [searchValue, setSearchValue] = useState("");
32+
const [debouncedSearchValue, setDebouncedSearchValue] = useState("");
3233
const [isSearchEmpty, setIsSearchEmpty] = useState(true);
3334

3435
const {
3536
data: workspacesData,
36-
refetch,
3737
hasNextPage,
3838
fetchNextPage,
3939
isFetchingNextPage,
4040
isFetching,
41-
} = useListCloudWorkspacesInfinite(WORKSPACE_LIST_LENGTH, searchValue);
41+
} = useListCloudWorkspacesInfinite(WORKSPACE_LIST_LENGTH, debouncedSearchValue);
4242

4343
const { organizationsMemberOnly, organizationsToCreateIn } = useOrganizationsToCreateWorkspaces();
4444

@@ -55,8 +55,8 @@ export const CloudWorkspacesPage: React.FC = () => {
5555

5656
useDebounce(
5757
() => {
58-
refetch();
59-
setIsSearchEmpty(searchValue === "");
58+
setDebouncedSearchValue(searchValue);
59+
setIsSearchEmpty(debouncedSearchValue === "");
6060
},
6161
250,
6262
[searchValue]

airbyte-webapp/src/pages/workspaces/WorkspacesPage.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,14 @@ const WorkspacesPage: React.FC = () => {
2929
const { isLoading, mutateAsync: handleLogout } = useMutation(() => logout?.() ?? Promise.resolve());
3030
useTrackPage(PageTrackingCodes.WORKSPACES);
3131
const [searchValue, setSearchValue] = useState("");
32+
const [debouncedSearchValue, setDebouncedSearchValue] = useState("");
3233

3334
const {
3435
data: workspacesData,
35-
refetch,
3636
hasNextPage,
3737
fetchNextPage,
3838
isFetchingNextPage,
39-
} = useListWorkspacesInfinite(WORKSPACE_LIST_LENGTH, searchValue);
39+
} = useListWorkspacesInfinite(WORKSPACE_LIST_LENGTH, debouncedSearchValue);
4040

4141
const workspaces = workspacesData?.pages.flatMap((page) => page.data.workspaces) ?? [];
4242

@@ -45,7 +45,7 @@ const WorkspacesPage: React.FC = () => {
4545

4646
useDebounce(
4747
() => {
48-
refetch();
48+
setDebouncedSearchValue(searchValue);
4949
},
5050
250,
5151
[searchValue]
@@ -89,7 +89,7 @@ const WorkspacesPage: React.FC = () => {
8989
</Box>
9090
<WorkspacesList workspaces={workspaces} fetchNextPage={fetchNextPage} hasNextPage={hasNextPage} />
9191
{isFetchingNextPage && (
92-
<Box py="2xl" className={styles.loadingSpinner}>
92+
<Box py="2xl">
9393
<LoadingSpinner />
9494
</Box>
9595
)}

airbyte-webapp/src/views/layout/MainView/MainView.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { ThemeToggle } from "components/ui/ThemeToggle";
1111
import { WorkspacesPicker } from "components/workspace/WorkspacesPicker";
1212

1313
import { useConfig } from "config";
14-
import { useListWorkspacesAsync } from "core/api";
14+
import { useListWorkspacesInfinite } from "core/api";
1515
import { FeatureItem, useFeature } from "core/services/features";
1616
import { links } from "core/utils/links";
1717
import { useAppMonitoringService } from "hooks/services/AppMonitoringService";
@@ -34,13 +34,12 @@ const MainView: React.FC<React.PropsWithChildren<unknown>> = (props) => {
3434
const { trackError } = useAppMonitoringService();
3535
const { hasNewVersions } = useGetConnectorsOutOfDate();
3636
const newWorkspacesUI = useFeature(FeatureItem.MultiWorkspaceUI);
37-
const { data: workspaces, isLoading } = useListWorkspacesAsync();
3837

3938
return (
4039
<FlexContainer className={classNames(styles.mainViewContainer)} gap="none">
4140
<SideBar>
4241
<AirbyteHomeLink />
43-
{newWorkspacesUI && <WorkspacesPicker loading={isLoading} workspaces={workspaces} />}
42+
{newWorkspacesUI && <WorkspacesPicker useFetchWorkspaces={useListWorkspacesInfinite} />}
4443
<MenuContent>
4544
<MainNavItems />
4645
<MenuContent>

0 commit comments

Comments
 (0)