Skip to content

Commit ecef0c0

Browse files
committed
fix: clean up workspaces page (#16939)
1 parent 224579d commit ecef0c0

16 files changed

+322
-291
lines changed

airbyte-webapp/src/area/layout/SideBar/components/AirbyteOrgPopoverPanel.module.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,7 @@
2424
border: none;
2525
}
2626
}
27+
28+
.footerLoading {
29+
border-top: variables.$border-thin solid colors.$grey-200;
30+
}

airbyte-webapp/src/area/layout/SideBar/components/AirbyteOrgPopoverPanel.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ import { LoadingSpinner } from "components/ui/LoadingSpinner";
1111
import { Text } from "components/ui/Text";
1212

1313
import { useListOrganizationSummaries } from "core/api";
14+
import { OrganizationSummary } from "core/api/types/AirbyteClient";
1415
import { useCurrentUser } from "core/services/auth/AuthContext";
1516
import { useFeature, FeatureItem } from "core/services/features";
16-
import { isNonNullable } from "core/utils/isNonNullable";
1717

1818
import styles from "./AirbyteOrgPopoverPanel.module.scss";
1919
import { OrganizationWithWorkspaces } from "./OrganizationWithWorkspaces";
@@ -36,13 +36,13 @@ export const AirbyteOrgPopoverPanel: React.FC<{ closePopover: () => void }> = ({
3636
},
3737
});
3838

39-
const organizationSummaries =
39+
const organizationSummaries: OrganizationSummary[] =
4040
data?.pages.flatMap((page) => {
4141
if (showOSSWorkspaceName && page.organizationSummaries?.[0]?.workspaces?.[0]) {
4242
page.organizationSummaries[0].workspaces[0].name = formatMessage({ id: "sidebar.myWorkspace" });
4343
}
44-
return page.organizationSummaries;
45-
}) || [];
44+
return page.organizationSummaries ?? [];
45+
}) ?? [];
4646

4747
const handleSearchChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
4848
setSearch(e.target.value);
@@ -115,14 +115,16 @@ export const AirbyteOrgPopoverPanel: React.FC<{ closePopover: () => void }> = ({
115115
<Virtuoso
116116
ref={virtuosoRef}
117117
style={{ height: estimatedHeight }}
118-
data={organizationSummaries?.filter(isNonNullable)}
118+
data={organizationSummaries}
119119
endReached={handleEndReached}
120120
computeItemKey={(index, item) => item.organization.organizationId + index}
121-
itemContent={OrganizationWithWorkspaces}
121+
itemContent={(index, item) => (
122+
<OrganizationWithWorkspaces {...item} lastItem={index === organizationSummaries.length - 1} />
123+
)}
122124
components={{
123125
Footer: isFetchingNextPage
124126
? () => (
125-
<Box pt="md" pb="sm">
127+
<Box pt="md" pb="sm" className={styles.footerLoading}>
126128
<LoadingSpinner />
127129
</Box>
128130
)

airbyte-webapp/src/area/layout/SideBar/components/OrganizationWithWorkspaces.module.scss

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
@use "scss/colors";
22
@use "scss/variables";
33

4-
.container {
5-
border-bottom: 1px solid colors.$grey-200;
6-
}
7-
84
.organizationName {
95
display: flex;
106
align-items: center;
@@ -15,6 +11,7 @@
1511
white-space: nowrap;
1612
overflow: hidden;
1713
text-overflow: ellipsis;
14+
width: 100%;
1815

1916
&:hover {
2017
background-color: colors.$blue-40;
@@ -55,7 +52,7 @@
5552
align-items: center;
5653
justify-content: center;
5754
width: 32px;
58-
height: 50px;
55+
align-self: stretch;
5956

6057
&:hover {
6158
background-color: colors.$blue-40;
@@ -70,6 +67,15 @@
7067
color: colors.$grey-400;
7168
}
7269

70+
.workspaces {
71+
box-sizing: border-box;
72+
border-bottom: 1px solid colors.$grey-200;
73+
74+
&.lastItem {
75+
border-bottom: none;
76+
}
77+
}
78+
7379
.workspaceItem {
7480
padding-left: calc(variables.$spacing-md * 2);
7581
padding-right: variables.$spacing-md;

airbyte-webapp/src/area/layout/SideBar/components/OrganizationWithWorkspaces.tsx

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import classNames from "classnames";
12
import { FormattedMessage } from "react-intl";
23
import { NavLink } from "react-router-dom";
3-
import { ItemContent } from "react-virtuoso";
44

55
import { FlexContainer } from "components/ui/Flex";
66
import { Icon } from "components/ui/Icon";
@@ -12,12 +12,15 @@ import { RoutePaths } from "pages/routePaths";
1212

1313
import styles from "./OrganizationWithWorkspaces.module.scss";
1414

15-
export const OrganizationWithWorkspaces: ItemContent<OrganizationSummary, null> = (
16-
_index,
17-
{ organization, workspaces = [], memberCount, subscription }
18-
) => {
15+
export const OrganizationWithWorkspaces: React.FC<OrganizationSummary & { lastItem: boolean }> = ({
16+
organization,
17+
workspaces = [],
18+
memberCount,
19+
subscription,
20+
lastItem,
21+
}) => {
1922
return (
20-
<FlexContainer direction="column" gap="none" className={styles.container}>
23+
<FlexContainer direction="column" gap="none">
2124
<FlexContainer direction="row" gap="xs" alignItems="center" justifyContent="space-between">
2225
<NavLink
2326
to={`${RoutePaths.Organization}/${organization.organizationId}/${RoutePaths.Workspaces}`}
@@ -28,8 +31,12 @@ export const OrganizationWithWorkspaces: ItemContent<OrganizationSummary, null>
2831
{organization.organizationName}
2932
</Text>
3033
<Text size="sm" color="grey400" className={styles.orgMeta}>
31-
{subscription && <>{subscription.name} &bull; </>}
32-
<FormattedMessage id="organization.members" values={{ count: memberCount || 0 }} />
34+
{subscription && (
35+
<FormattedMessage
36+
id="organization.members"
37+
values={{ subscriptionName: subscription.name, count: memberCount || 0 }}
38+
/>
39+
)}
3340
</Text>
3441
</div>
3542
</NavLink>
@@ -40,7 +47,11 @@ export const OrganizationWithWorkspaces: ItemContent<OrganizationSummary, null>
4047
<Icon type="gear" className={styles.gearIcon} aria-hidden="true" />
4148
</NavLink>
4249
</FlexContainer>
43-
<FlexContainer direction="column" gap="none">
50+
<FlexContainer
51+
direction="column"
52+
gap="none"
53+
className={classNames(styles.workspaces, { [styles.lastItem]: lastItem })}
54+
>
4455
{workspaces.map((workspace) => (
4556
<NavLink
4657
key={workspace.workspaceId}

airbyte-webapp/src/components/ui/ListBox/ListboxOptions.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,16 @@ export const ListboxOptions = React.forwardRef<
1414
HTMLElement,
1515
ExtractProps<typeof HeadlessUIListboxOptions> & ListboxOptionsProps
1616
>((props, ref) => {
17+
const { fullWidth, ...restProps } = props;
1718
const mergedClassNames = classNames(
1819
styles.listboxOptions,
1920
{
20-
[styles["listboxOptions--fullWidth"]]: !!props.fullWidth,
21+
[styles["listboxOptions--fullWidth"]]: !!fullWidth,
2122
},
2223
props.className
2324
);
2425
return (
25-
<HeadlessUIListboxOptions {...props} className={mergedClassNames} ref={ref}>
26+
<HeadlessUIListboxOptions {...restProps} className={mergedClassNames} ref={ref}>
2627
{props.children}
2728
</HeadlessUIListboxOptions>
2829
);

airbyte-webapp/src/core/api/hooks/connections.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
useInfiniteQuery,
44
useIsMutating,
55
useMutation,
6+
useQueries,
67
useQuery,
78
useQueryClient,
89
} from "@tanstack/react-query";
@@ -110,7 +111,7 @@ export const connectionsKeys = {
110111
requestBody.createdAtEnd,
111112
] as const,
112113
event: (eventId: string) => [...connectionsKeys.all, "event", eventId] as const,
113-
statusCounts: () => [...connectionsKeys.all, "statusCounts"] as const,
114+
statusCounts: (workspaceId: string) => [...connectionsKeys.all, "statusCounts", workspaceId] as const,
114115
};
115116

116117
export interface ConnectionValues {
@@ -895,7 +896,24 @@ export const useGetConnectionStatusesCounts = () => {
895896
const workspaceId = useCurrentWorkspaceId();
896897
const requestOptions = useRequestOptions();
897898

898-
return useQuery(connectionsKeys.statusCounts(), () =>
899+
return useQuery(connectionsKeys.statusCounts(workspaceId), () =>
899900
webBackendGetConnectionStatusCounts({ workspaceId }, requestOptions)
900901
);
901902
};
903+
904+
export const useGetWorkspacesStatusesCounts = (workspaceIds: string[], options?: { refetchInterval?: boolean }) => {
905+
const requestOptions = useRequestOptions();
906+
907+
return useQueries({
908+
queries: workspaceIds.map((workspaceId) => ({
909+
queryKey: connectionsKeys.statusCounts(workspaceId),
910+
queryFn: async () => ({
911+
workspaceId,
912+
statusCounts: await webBackendGetConnectionStatusCounts({ workspaceId }, requestOptions),
913+
}),
914+
staleTime: 1000 * 60, // 1 minute
915+
cacheTime: 1000 * 60 * 2, // 2 minutes
916+
refetchInterval: options?.refetchInterval ? 1000 * 60 : undefined, // 1 minute if enabled
917+
})),
918+
});
919+
};

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

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,10 @@ import {
2828
OrganizationRead,
2929
OrganizationTrialStatusRead,
3030
OrganizationUserReadList,
31-
WorkspaceReadList,
3231
OrganizationInfoRead,
3332
ListOrganizationSummariesResponse,
33+
WorkspaceReadList,
34+
ListWorkspacesInOrganizationRequestBody,
3435
} from "../types/AirbyteClient";
3536
import { useRequestOptions } from "../useRequestOptions";
3637
import { useSuspenseQuery } from "../useSuspenseQuery";
@@ -46,7 +47,8 @@ export const organizationKeys = {
4647
trialStatus: (organizationId: string) => [SCOPE_ORGANIZATION, "trial", organizationId] as const,
4748
usage: (organizationId: string, timeWindow: string) =>
4849
[SCOPE_ORGANIZATION, "usage", organizationId, timeWindow] as const,
49-
workspaces: (organizationId: string) => [SCOPE_ORGANIZATION, "workspaces", "list", organizationId] as const,
50+
workspaces: (organizationId: string, pageSize: number, nameContains?: string) =>
51+
[SCOPE_ORGANIZATION, "workspaces", "list", organizationId, pageSize, nameContains] as const,
5052
listByUser: (requestBody: ListOrganizationsByUserRequestBody) =>
5153
[...organizationKeys.all, "byUser", requestBody] as const,
5254
summaries: (requestBody: ListOrganizationSummariesRequestBody) =>
@@ -150,11 +152,43 @@ export const useOrganizationUsage = ({ timeWindow }: { timeWindow: ConsumptionTi
150152
);
151153
};
152154

153-
export const useListWorkspacesInOrganization = ({ organizationId }: { organizationId: string }): WorkspaceReadList => {
155+
export const useListWorkspacesInOrganization = ({
156+
organizationId,
157+
pagination,
158+
nameContains,
159+
}: ListWorkspacesInOrganizationRequestBody): UseInfiniteQueryResult<WorkspaceReadList, unknown> => {
154160
const requestOptions = useRequestOptions();
155-
const queryKey = organizationKeys.workspaces(organizationId);
161+
const pageSize = pagination?.pageSize ?? 10;
162+
const queryKey = organizationKeys.workspaces(organizationId, pageSize, nameContains?.trim());
163+
164+
const listWorkspacesQueryFn = async ({ pageParam = 0 }) => {
165+
const rowOffset = pageParam * pageSize;
166+
return listWorkspacesInOrganization(
167+
{
168+
organizationId,
169+
pagination: pagination ? { pageSize, rowOffset } : undefined,
170+
nameContains,
171+
},
172+
requestOptions
173+
);
174+
};
156175

157-
return useSuspenseQuery(queryKey, () => listWorkspacesInOrganization({ organizationId }, requestOptions));
176+
return useInfiniteQuery({
177+
queryKey,
178+
queryFn: listWorkspacesQueryFn,
179+
staleTime: 1000 * 60 * 5,
180+
cacheTime: 1000 * 60 * 30,
181+
getNextPageParam: (lastPage, allPages) => {
182+
const pageSize = pagination?.pageSize ?? 10;
183+
const workspaces = lastPage.workspaces ?? [];
184+
return workspaces.length < pageSize ? undefined : allPages.length;
185+
},
186+
getPreviousPageParam: (firstPage, allPages) => {
187+
const pageSize = pagination?.pageSize ?? 10;
188+
const workspaces = firstPage.workspaces ?? [];
189+
return workspaces.length < pageSize ? undefined : allPages.length - 1;
190+
},
191+
});
158192
};
159193

160194
export const useListOrganizationsByUser = (requestBody: ListOrganizationsByUserRequestBody) => {

airbyte-webapp/src/locales/en.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@
3434
"workspaces.sourceLimitReached.description": "You have {count} sources in this workspace. If you would like to add more, please change the limits for your instance.",
3535
"workspaces.destinationLimitReached.title": "You've reached the limit for destinations in a workspace.",
3636
"workspaces.destinationLimitReached.description": "You have {count} destinations in this workspace. If you would like to add more, please change the limits for your instance.",
37+
"workspaces.status.runningSyncs": "Running syncs",
38+
"workspaces.status.successfulSyncs": "Successful syncs",
39+
"workspaces.status.failedSyncs": "Failed syncs",
3740

3841
"teamsFeatures.modal.title": "Continue to experience Teams features",
3942
"teamsFeatures.modal.featuresTitle": "You're about to use one of the following Teams features:",
@@ -42,7 +45,7 @@
4245
"teamsFeatures.modal.features.sso": "SSO",
4346
"teamsFeatures.modal.features.rbac": "Role-based access control (RBAC)",
4447
"teamsFeatures.modal.features.mappers": "Mappers",
45-
"teamsFeatures.modal.warningMessage": "Please note you will lost access to these features once your trial ends unless you upgrade to Teams. All syncs using these features will be disabled and all members invited will be deleted.",
48+
"teamsFeatures.modal.warningMessage": "Please note you will lose access to these features once your trial ends unless you upgrade to Teams. All syncs using these features will be disabled and all members invited will be deleted.",
4649
"teamsFeatures.modal.button.continue": "Continue",
4750

4851
"role.admin": "Admin",
@@ -2614,7 +2617,8 @@
26142617
"embedded.onboarding.upsell.description": "Harness the power of Airbyte Embedded and add hundreds of integrations into your product instantly.",
26152618
"embedded.onboarding.talkToSales": "Talk to sales",
26162619

2617-
"organization.members": "{count, plural, one {# member} other {# members}}",
2620+
"organization.members": "{subscriptionName} \u2022 {count, plural, one {# member} other {# members}}",
26182621
"organization.unhealthyWorkspaces": "Unhealthy workspaces",
2619-
"organization.runningSyncs": "Running syncs"
2622+
"organization.runningSyncs": "Running syncs",
2623+
"workspaces.statusFilter.placeholder": "Filter by status"
26202624
}

airbyte-webapp/src/pages/SettingsPage/components/RegionsTable.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import { useListWorkspacesInOrganization, useGetDataplaneGroup } from "core/api"
99

1010
export const RegionsTable = () => {
1111
const organizationId = useCurrentOrganizationId();
12-
const { workspaces } = useListWorkspacesInOrganization({ organizationId });
12+
const { data: workspacesData } = useListWorkspacesInOrganization({ organizationId });
13+
const workspaces = workspacesData?.pages[0].workspaces ?? [];
1314
const { getDataplaneGroup } = useGetDataplaneGroup();
1415
const { formatMessage } = useIntl();
1516

airbyte-webapp/src/pages/workspaces/OrganizationWorkspacesPage.module.scss

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,22 @@
1919
top: calc(variables.$spacing-xl * 3);
2020
}
2121

22+
.loadingPage {
23+
height: 100%;
24+
}
25+
2226
.workspacesPage__loadingSpinner {
2327
margin-left: auto;
2428
margin-right: auto;
2529
text-align: center;
2630
}
2731

32+
.footerLoading {
33+
display: flex;
34+
justify-content: center;
35+
align-items: center;
36+
}
37+
2838
.headerRow {
2939
padding-bottom: variables.$spacing-xl;
3040

@@ -38,6 +48,10 @@
3848
margin-bottom: variables.$spacing-md;
3949
}
4050

51+
.statusFilterButton {
52+
width: 170px;
53+
}
54+
4155
.emptyStateIconBackground {
4256
width: 44px;
4357
height: 44px;

0 commit comments

Comments
 (0)