Skip to content

Commit df6c8c7

Browse files
Handle negative credit in UI (#10814)
* Handle create connection feature * Handle sync feature * Add create and sync connection features to service * Add unit tests * Add allowSync property to deps Co-authored-by: Artem Astapenko <[email protected]>
1 parent 39107bf commit df6c8c7

File tree

13 files changed

+212
-24
lines changed

13 files changed

+212
-24
lines changed

airbyte-webapp/src/components/ConnectorBlocks/TableItemTitle.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Button, DropDownRow, H3, H5 } from "components";
66
import { Popout } from "components/base/Popout/Popout";
77
import { ReleaseStageBadge } from "components/ReleaseStageBadge";
88
import { ReleaseStage } from "core/domain/connector";
9+
import { FeatureItem, useFeatureService } from "hooks/services/Feature";
910

1011
type IProps = {
1112
type: "source" | "destination";
@@ -53,6 +54,8 @@ const TableItemTitle: React.FC<IProps> = ({
5354
entityIcon,
5455
releaseStage,
5556
}) => {
57+
const { hasFeature } = useFeatureService();
58+
const allowCreateConnection = hasFeature(FeatureItem.AllowCreateConnection);
5659
const formatMessage = useIntl().formatMessage;
5760
const options = [
5861
{
@@ -94,7 +97,7 @@ const TableItemTitle: React.FC<IProps> = ({
9497
}}
9598
onChange={onSelect}
9699
targetComponent={({ onOpen }) => (
97-
<Button onClick={onOpen}>
100+
<Button onClick={onOpen} disabled={!allowCreateConnection}>
98101
<FormattedMessage id={`tables.${type}Add`} />
99102
</Button>
100103
)}

airbyte-webapp/src/components/EntityTable/ConnectionTable.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import StatusCell from "./components/StatusCell";
1515
import ConnectionSettingsCell from "./components/ConnectionSettingsCell";
1616
import { ITableDataItem, SortOrderEnum } from "./types";
1717
import useRouter from "hooks/useRouter";
18+
import { FeatureItem, useFeatureService } from "hooks/services/Feature";
1819

1920
const Content = styled.div`
2021
margin: 0 32px 0 27px;
@@ -36,6 +37,8 @@ const ConnectionTable: React.FC<IProps> = ({
3637
onSync,
3738
}) => {
3839
const { query, push } = useRouter();
40+
const { hasFeature } = useFeatureService();
41+
const allowSync = hasFeature(FeatureItem.AllowSync);
3942

4043
const sortBy = query.sortBy || "entity";
4144
const sortOrder = query.order || SortOrderEnum.ASC;
@@ -165,6 +168,7 @@ const ConnectionTable: React.FC<IProps> = ({
165168
isManual={!row.original.schedule}
166169
onChangeStatus={onChangeStatus}
167170
onSync={onSync}
171+
allowSync={allowSync}
168172
/>
169173
),
170174
},
@@ -177,7 +181,7 @@ const ConnectionTable: React.FC<IProps> = ({
177181
),
178182
},
179183
],
180-
[entity, onChangeStatus, onSync, onSortClick, sortBy, sortOrder]
184+
[allowSync, entity, onChangeStatus, onSync, onSortClick, sortBy, sortOrder]
181185
);
182186

183187
return (

airbyte-webapp/src/components/EntityTable/components/StatusCell.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useAsyncFn } from "react-use";
66
import { LoadingButton, Toggle } from "components";
77

88
type IProps = {
9+
allowSync?: boolean;
910
enabled?: boolean;
1011
isSyncing?: boolean;
1112
isManual?: boolean;
@@ -29,6 +30,7 @@ const StatusCell: React.FC<IProps> = ({
2930
onChangeStatus,
3031
isSyncing,
3132
onSync,
33+
allowSync,
3234
}) => {
3335
const [{ loading }, OnLaunch] = useAsyncFn(
3436
async (event: React.SyntheticEvent) => {
@@ -44,7 +46,13 @@ const StatusCell: React.FC<IProps> = ({
4446
};
4547

4648
if (!isManual) {
47-
return <Toggle checked={enabled} onChange={OnToggleClick} />;
49+
return (
50+
<Toggle
51+
checked={enabled}
52+
onChange={OnToggleClick}
53+
disabled={!allowSync}
54+
/>
55+
);
4856
}
4957

5058
if (isSyncing) {
@@ -56,7 +64,7 @@ const StatusCell: React.FC<IProps> = ({
5664
}
5765

5866
return (
59-
<SmallButton onClick={OnLaunch} isLoading={loading}>
67+
<SmallButton onClick={OnLaunch} isLoading={loading} disabled={!allowSync}>
6068
<FormattedMessage id="tables.launch" />
6169
</SmallButton>
6270
);

airbyte-webapp/src/config/ConfigServiceProvider.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { LoadingPage } from "components";
66
import { Config, ValueProvider } from "./types";
77
import { applyProviders } from "./configProviders";
88

9-
type ConfigContext<T extends Config = Config> = {
9+
export type ConfigContext<T extends Config = Config> = {
1010
config: T;
1111
};
1212

airbyte-webapp/src/config/defaultConfig.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ const features: Feature[] = [
1313
{
1414
id: FeatureItem.AllowUpdateConnectors,
1515
},
16+
{
17+
id: FeatureItem.AllowCreateConnection,
18+
},
19+
{
20+
id: FeatureItem.AllowSync,
21+
},
1622
];
1723

1824
const defaultConfig: Config = {
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import React from "react";
2+
import { act, renderHook } from "@testing-library/react-hooks";
3+
import {
4+
FeatureService,
5+
useFeatureRegisterValues,
6+
useFeatureService,
7+
} from "./FeatureService";
8+
import { FeatureItem } from "./types";
9+
import { TestWrapper } from "utils/testutils";
10+
import { ConfigContext, configContext } from "config";
11+
12+
const predefinedFeatures = [
13+
{
14+
id: FeatureItem.AllowCustomDBT,
15+
},
16+
];
17+
18+
const wrapper: React.FC = ({ children }) => (
19+
<TestWrapper>
20+
<configContext.Provider
21+
value={
22+
({
23+
config: { features: predefinedFeatures },
24+
} as unknown) as ConfigContext
25+
}
26+
>
27+
<FeatureService>{children}</FeatureService>
28+
</configContext.Provider>
29+
</TestWrapper>
30+
);
31+
32+
describe("FeatureService", () => {
33+
test("should register and unregister features", async () => {
34+
const { result } = renderHook(() => useFeatureService(), {
35+
wrapper,
36+
});
37+
38+
expect(result.current.features).toEqual(predefinedFeatures);
39+
40+
act(() => {
41+
result.current.registerFeature([
42+
{
43+
id: FeatureItem.AllowCreateConnection,
44+
},
45+
]);
46+
});
47+
48+
expect(result.current.features).toEqual([
49+
...predefinedFeatures,
50+
{
51+
id: FeatureItem.AllowCreateConnection,
52+
},
53+
]);
54+
55+
act(() => {
56+
result.current.unregisterFeature([FeatureItem.AllowCreateConnection]);
57+
});
58+
59+
expect(result.current.features).toEqual(predefinedFeatures);
60+
});
61+
});
62+
63+
describe("useFeatureRegisterValues", () => {
64+
test("should register more than 1 feature", async () => {
65+
const { result } = renderHook(
66+
() => {
67+
useFeatureRegisterValues([{ id: FeatureItem.AllowCreateConnection }]);
68+
useFeatureRegisterValues([{ id: FeatureItem.AllowSync }]);
69+
70+
return useFeatureService();
71+
},
72+
{
73+
initialProps: { initialValue: 0 },
74+
wrapper,
75+
}
76+
);
77+
78+
expect(result.current.features).toEqual([
79+
...predefinedFeatures,
80+
{ id: FeatureItem.AllowCreateConnection },
81+
{ id: FeatureItem.AllowSync },
82+
]);
83+
84+
act(() => {
85+
result.current.unregisterFeature([FeatureItem.AllowCreateConnection]);
86+
});
87+
88+
expect(result.current.features).toEqual([
89+
...predefinedFeatures,
90+
{ id: FeatureItem.AllowSync },
91+
]);
92+
});
93+
});

airbyte-webapp/src/hooks/services/Feature/FeatureService.tsx

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,43 @@
1-
import React, { useContext, useMemo } from "react";
1+
import React, { useContext, useMemo, useState } from "react";
22
import { Feature, FeatureItem, FeatureServiceApi } from "./types";
33
import { useConfig } from "config";
4+
import { useDeepCompareEffect } from "react-use";
45

56
const featureServiceContext = React.createContext<FeatureServiceApi | null>(
67
null
78
);
89

9-
export function FeatureService({
10-
children,
11-
}: {
12-
children: React.ReactNode;
13-
features?: Feature[];
14-
}) {
15-
const { features } = useConfig();
10+
export function FeatureService({ children }: { children: React.ReactNode }) {
11+
const [additionFeatures, setAdditionFeatures] = useState<Feature[]>([]);
12+
const { features: instanceWideFeatures } = useConfig();
13+
14+
const featureMethods = useMemo(() => {
15+
return {
16+
registerFeature: (newFeatures: Feature[]): void =>
17+
setAdditionFeatures((oldFeatures) => [...oldFeatures, ...newFeatures]),
18+
unregisterFeature: (unregisteredFeatures: FeatureItem[]): void => {
19+
setAdditionFeatures((oldFeatures) =>
20+
oldFeatures.filter(
21+
(feature) => !unregisteredFeatures.includes(feature.id)
22+
)
23+
);
24+
},
25+
};
26+
}, []);
27+
28+
const features = useMemo(
29+
() => [...instanceWideFeatures, ...additionFeatures],
30+
[instanceWideFeatures, additionFeatures]
31+
);
32+
1633
const featureService = useMemo(
1734
() => ({
1835
features,
1936
hasFeature: (featureId: FeatureItem): boolean =>
2037
!!features.find((feature) => feature.id === featureId),
38+
...featureMethods,
2139
}),
22-
[features]
40+
[features, featureMethods]
2341
);
2442

2543
return (
@@ -44,3 +62,19 @@ export const WithFeature: React.FC<{ featureId: FeatureItem }> = ({
4462
const { hasFeature } = useFeatureService();
4563
return hasFeature(featureId) ? <>{children}</> : null;
4664
};
65+
66+
export const useFeatureRegisterValues = (props?: Feature[] | null): void => {
67+
const { registerFeature, unregisterFeature } = useFeatureService();
68+
69+
useDeepCompareEffect(() => {
70+
if (props) {
71+
registerFeature(props);
72+
73+
return () =>
74+
unregisterFeature(props.map((feature: Feature) => feature.id));
75+
}
76+
77+
return;
78+
// eslint-disable-next-line react-hooks/exhaustive-deps
79+
}, [props]);
80+
};

airbyte-webapp/src/hooks/services/Feature/types.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ export enum FeatureItem {
33
AllowCustomDBT = "ALLOW_CUSTOM_DBT",
44
AllowUpdateConnectors = "ALLOW_UPDATE_CONNECTORS",
55
AllowOAuthConnector = "ALLOW_OAUTH_CONNECTOR",
6+
AllowCreateConnection = "ALLOW_CREATE_CONNECTION",
7+
AllowSync = "ALLOW_SYNC",
68
}
79

810
type Feature = {
@@ -11,6 +13,8 @@ type Feature = {
1113

1214
type FeatureServiceApi = {
1315
features: Feature[];
16+
registerFeature: (props: Feature[]) => void;
17+
unregisterFeature: (props: FeatureItem[]) => void;
1418
hasFeature: (featureId: FeatureItem) => boolean;
1519
};
1620

airbyte-webapp/src/packages/cloud/cloudRoutes.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ import { storeUtmFromQuery } from "utils/utmStorage";
3636
import { DefaultView } from "./views/DefaultView";
3737
import { hasFromState } from "utils/stateUtils";
3838
import { RoutePaths } from "../../pages/routePaths";
39+
import { FeatureItem, useFeatureRegisterValues } from "hooks/services/Feature";
40+
import { useGetCloudWorkspace } from "./services/workspaces/WorkspacesService";
41+
import { CreditStatus } from "./lib/domain/cloudWorkspaces/types";
3942

4043
export const CloudRoutes = {
4144
Root: "/",
@@ -60,6 +63,7 @@ export const CloudRoutes = {
6063

6164
const MainRoutes: React.FC = () => {
6265
const workspace = useCurrentWorkspace();
66+
const cloudWorkspace = useGetCloudWorkspace(workspace.workspaceId);
6367

6468
const analyticsContext = useMemo(
6569
() => ({
@@ -74,6 +78,21 @@ const MainRoutes: React.FC = () => {
7478
? RoutePaths.Onboarding
7579
: RoutePaths.Connections;
7680

81+
const features = useMemo(
82+
() =>
83+
cloudWorkspace.creditStatus !==
84+
CreditStatus.NEGATIVE_BEYOND_GRACE_PERIOD &&
85+
cloudWorkspace.creditStatus !== CreditStatus.NEGATIVE_MAX_THRESHOLD
86+
? [
87+
{ id: FeatureItem.AllowCreateConnection },
88+
{ id: FeatureItem.AllowSync },
89+
]
90+
: null,
91+
[cloudWorkspace]
92+
);
93+
94+
useFeatureRegisterValues(features);
95+
7796
return (
7897
<Routes>
7998
<Route

airbyte-webapp/src/pages/ConnectionPage/pages/AllConnectionsPage/AllConnectionsPage.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import useRouter from "hooks/useRouter";
99
import HeadTitle from "components/HeadTitle";
1010
import Placeholder, { ResourceTypes } from "components/Placeholder";
1111
import useWorkspace from "hooks/services/useWorkspace";
12+
import { FeatureItem, useFeatureService } from "hooks/services/Feature";
1213
import { RoutePaths } from "../../../routePaths";
1314

1415
const AllConnectionsPage: React.FC = () => {
@@ -18,6 +19,8 @@ const AllConnectionsPage: React.FC = () => {
1819
const { connections } = useResource(ConnectionResource.listShape(), {
1920
workspaceId: workspace.workspaceId,
2021
});
22+
const { hasFeature } = useFeatureService();
23+
const allowCreateConnection = hasFeature(FeatureItem.AllowCreateConnection);
2124

2225
const onClick = () => push(`${RoutePaths.ConnectionNew}`);
2326

@@ -28,7 +31,7 @@ const AllConnectionsPage: React.FC = () => {
2831
<PageTitle
2932
title={<FormattedMessage id="sidebar.connections" />}
3033
endComponent={
31-
<Button onClick={onClick}>
34+
<Button onClick={onClick} disabled={!allowCreateConnection}>
3235
<FormattedMessage id="connection.newConnection" />
3336
</Button>
3437
}

0 commit comments

Comments
 (0)