Skip to content

Commit 9072238

Browse files
letiescancianocarlonuccio
authored andcommitted
🪟 🔧 Refactor FrequentlyUsedDestinations component (airbytehq#19019)
* 🪟 🔧 Refactor FrequentlyUsedDestinations We need to reuse this component in an experiment to suggest sources. This PR covers the refactor so it's not destination-dependant and can be used later on. Demo: https://www.loom.com/share/c207ab2a53c146bd8e4fe57a57660a6b  * PR comments
1 parent 8015e18 commit 9072238

19 files changed

+262
-158
lines changed

airbyte-webapp/src/locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@
250250
"sources.incrementalDefault": "{value} (default)",
251251
"sources.incrementalSourceCursor": "Incremental - source-defined cursor",
252252
"sources.full_refresh": "Full refresh",
253+
"sources.frequentlyUsed": "Suggested sources",
253254
"sources.incremental": "Incremental - based on...",
254255
"sources.newSource": "New source",
255256
"sources.createFirst": "Connect your first source",

airbyte-webapp/src/pages/DestinationPage/pages/CreateDestinationPage/components/DestinationForm.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import { useLocation } from "react-router-dom";
55
import { ConnectionConfiguration } from "core/domain/connection";
66
import { DestinationDefinitionRead } from "core/request/AirbyteClient";
77
import { LogsRequestError } from "core/request/LogsRequestError";
8+
import { useExperiment } from "hooks/services/Experiment";
89
import { useGetDestinationDefinitionSpecificationAsync } from "services/connector/DestinationDefinitionSpecificationService";
910
import { generateMessageFromError, FormError } from "utils/errorStatusMessage";
1011
import { ConnectorCard } from "views/Connector/ConnectorCard";
11-
import { ConnectorCardValues, FrequentlyUsedDestinations, StartWithDestination } from "views/Connector/ConnectorForm";
12+
import { ConnectorCardValues, FrequentlyUsedConnectors, StartWithDestination } from "views/Connector/ConnectorForm";
1213

1314
import styles from "./DestinationForm.module.scss";
1415

@@ -63,8 +64,17 @@ export const DestinationForm: React.FC<DestinationFormProps> = ({
6364

6465
const errorMessage = error ? generateMessageFromError(error) : null;
6566

67+
const frequentlyUsedDestinationIds = useExperiment("connector.frequentlyUsedDestinationIds", [
68+
"22f6c74f-5699-40ff-833c-4a879ea40133",
69+
"424892c4-daac-4491-b35d-c6688ba547ba",
70+
]);
6671
const frequentlyUsedDestinationsComponent = !isLoading && !destinationDefinitionId && (
67-
<FrequentlyUsedDestinations onDestinationSelect={onDropDownSelect} availableServices={destinationDefinitions} />
72+
<FrequentlyUsedConnectors
73+
connectorType="destination"
74+
onConnectorSelect={onDropDownSelect}
75+
availableServices={destinationDefinitions}
76+
connectorIds={frequentlyUsedDestinationIds}
77+
/>
6878
);
6979
const startWithDestinationComponent = !isLoading && !destinationDefinitionId && (
7080
<div className={styles.startWithDestinationContainer}>

airbyte-webapp/src/test-utils/mock-data/mockFrequentlyUsedDestinations.ts

Lines changed: 45 additions & 1 deletion
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import React from "react";
2+
3+
import { ConnectorDefinition } from "core/domain/connector";
4+
5+
import { FrequentlyUsedConnectorsCard } from "./FrequentlyUsedConnectorsCard";
6+
import { useAnalyticsTrackFunctions } from "./useAnalyticsTrackFunctions";
7+
import { useSuggestedConnectors } from "./useSuggestedConnectors";
8+
9+
interface FrequentlyUsedConnectorsProps {
10+
availableServices: ConnectorDefinition[];
11+
connectorType: "source" | "destination";
12+
connectorIds: string[];
13+
onConnectorSelect: (id: string) => void;
14+
}
15+
16+
export const FrequentlyUsedConnectors: React.FC<FrequentlyUsedConnectorsProps> = ({
17+
availableServices,
18+
connectorType,
19+
connectorIds,
20+
onConnectorSelect,
21+
}) => {
22+
const { trackSelectedSuggestedDestination } = useAnalyticsTrackFunctions();
23+
24+
const suggestedConnectors = useSuggestedConnectors({ availableServices, connectorIds });
25+
const onConnectorCardClick = (id: string, connectorName: string) => {
26+
onConnectorSelect(id);
27+
trackSelectedSuggestedDestination(id, connectorName);
28+
};
29+
30+
return (
31+
<FrequentlyUsedConnectorsCard
32+
connectors={suggestedConnectors}
33+
onConnectorSelect={onConnectorCardClick}
34+
connectorType={connectorType}
35+
/>
36+
);
37+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { fireEvent, render, waitFor } from "@testing-library/react";
2+
import { IntlProvider } from "react-intl";
3+
import { mockDestinationsData } from "test-utils/mock-data/mockFrequentlyUsedDestinations";
4+
5+
import en from "locales/en.json";
6+
7+
import { FrequentlyUsedConnectorsCard, FrequentlyUsedConnectorsCardProps } from "./FrequentlyUsedConnectorsCard";
8+
9+
const renderFrequentlyUsedConnectorsComponent = (props: FrequentlyUsedConnectorsCardProps) =>
10+
render(
11+
<IntlProvider locale="en" messages={en}>
12+
<FrequentlyUsedConnectorsCard {...props} />
13+
</IntlProvider>
14+
);
15+
16+
describe("<mockFrequentlyUsedConnectors />", () => {
17+
it("should renders with mock data without crash", () => {
18+
const component = renderFrequentlyUsedConnectorsComponent({
19+
connectors: mockDestinationsData,
20+
connectorType: "destination",
21+
onConnectorSelect: jest.fn(),
22+
});
23+
24+
expect(component).toMatchSnapshot();
25+
});
26+
27+
it("should call provided handler with right param", async () => {
28+
const handler = jest.fn();
29+
const { getByText } = renderFrequentlyUsedConnectorsComponent({
30+
connectors: mockDestinationsData,
31+
connectorType: "destination",
32+
onConnectorSelect: handler,
33+
});
34+
fireEvent.click(getByText("BigQuery"));
35+
36+
await waitFor(() => {
37+
expect(handler).toHaveBeenCalledTimes(1);
38+
expect(handler).toHaveBeenCalledWith("2", "BigQuery");
39+
});
40+
});
41+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import React from "react";
2+
import { useIntl } from "react-intl";
3+
4+
import { ConnectorCard } from "components";
5+
import { SlickSlider } from "components/ui/SlickSlider";
6+
7+
import { ConnectorCard as ConnectorCardType } from "../../types";
8+
import styles from "./FrequentlyUsedConnectorsCard.module.scss";
9+
10+
export interface FrequentlyUsedConnectorsCardProps {
11+
connectors: ConnectorCardType[];
12+
connectorType: "source" | "destination";
13+
onConnectorSelect: (id: string, connectorName: string) => void;
14+
}
15+
16+
export const FrequentlyUsedConnectorsCard: React.FC<FrequentlyUsedConnectorsCardProps> = ({
17+
connectors,
18+
onConnectorSelect,
19+
connectorType,
20+
}) => {
21+
const { formatMessage } = useIntl();
22+
23+
if (connectors.length === 0) {
24+
return null;
25+
}
26+
27+
return (
28+
<div className={styles.container}>
29+
<SlickSlider
30+
title={formatMessage({
31+
id: `${connectorType}s.frequentlyUsed`,
32+
})}
33+
>
34+
{connectors.map(({ id, name, icon, releaseStage }, index) => (
35+
<button key={index} className={styles.card} onClick={() => onConnectorSelect(id, name)}>
36+
<ConnectorCard connectionName={name} icon={icon} releaseStage={releaseStage} fullWidth />
37+
</button>
38+
))}
39+
</SlickSlider>
40+
</div>
41+
);
42+
};
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`<FrequentlyUsedDestinations /> should renders with mock data without crash 1`] = `
3+
exports[`<mockFrequentlyUsedConnectors /> should renders with mock data without crash 1`] = `
44
<body>
55
<div>
66
<div
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { ComponentStory, ComponentMeta } from "@storybook/react";
2+
import { mockDestinationsData, mockSourcesData } from "test-utils/mock-data/mockFrequentlyUsedDestinations";
3+
4+
import { FrequentlyUsedConnectorsCard } from "./FrequentlyUsedConnectorsCard";
5+
6+
export default {
7+
title: "Views/FrequentlyUsedConnectors",
8+
component: FrequentlyUsedConnectorsCard,
9+
args: {
10+
connectors: mockDestinationsData,
11+
connectorType: "destination",
12+
},
13+
} as ComponentMeta<typeof FrequentlyUsedConnectorsCard>;
14+
15+
const Template: ComponentStory<typeof FrequentlyUsedConnectorsCard> = (args) => (
16+
<div style={{ maxWidth: 560 }}>
17+
<FrequentlyUsedConnectorsCard {...args} />
18+
</div>
19+
);
20+
export const Destinations = Template.bind({});
21+
22+
export const Sources = Template.bind({});
23+
Sources.args = {
24+
...Template.args,
25+
connectors: mockSourcesData,
26+
connectorType: "source",
27+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { FrequentlyUsedConnectors } from "./FrequentlyUsedConnectors";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { useMemo } from "react";
2+
3+
import { ConnectorDefinition } from "core/domain/connector";
4+
import { isDestinationDefinition } from "core/domain/connector/destination";
5+
import { isSourceDefinition } from "core/domain/connector/source";
6+
7+
import { ConnectorCard } from "../../types";
8+
9+
interface Props {
10+
availableServices: ConnectorDefinition[];
11+
connectorIds: string[];
12+
}
13+
export const useSuggestedConnectors = ({ availableServices, connectorIds }: Props): ConnectorCard[] => {
14+
return useMemo(
15+
() =>
16+
availableServices
17+
.filter((service) => {
18+
if (isDestinationDefinition(service)) {
19+
return connectorIds.includes(service.destinationDefinitionId);
20+
}
21+
22+
return isSourceDefinition(service) && connectorIds.includes(service.sourceDefinitionId);
23+
})
24+
.map((service) => {
25+
if (isDestinationDefinition(service)) {
26+
const { destinationDefinitionId, name, icon, releaseStage } = service;
27+
return {
28+
id: destinationDefinitionId,
29+
destinationDefinitionId,
30+
name,
31+
icon,
32+
releaseStage,
33+
};
34+
}
35+
36+
const { sourceDefinitionId, name, icon, releaseStage } = service;
37+
return {
38+
id: sourceDefinitionId,
39+
sourceDefinitionId,
40+
name,
41+
icon,
42+
releaseStage,
43+
};
44+
}),
45+
[availableServices, connectorIds]
46+
);
47+
};

airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedDestinations/FrequentlyUsedDestinations.tsx

Lines changed: 0 additions & 54 deletions
This file was deleted.

airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedDestinations/FrequentlyUsedDestinationsCard.test.tsx

Lines changed: 0 additions & 39 deletions
This file was deleted.

airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedDestinations/FrequentlyUsedDestinationsCard.tsx

Lines changed: 0 additions & 40 deletions
This file was deleted.

0 commit comments

Comments
 (0)