diff --git a/airbyte-webapp/src/locales/en.json b/airbyte-webapp/src/locales/en.json index 680639c8f9dd3..3eeb625c4719e 100644 --- a/airbyte-webapp/src/locales/en.json +++ b/airbyte-webapp/src/locales/en.json @@ -250,6 +250,7 @@ "sources.incrementalDefault": "{value} (default)", "sources.incrementalSourceCursor": "Incremental - source-defined cursor", "sources.full_refresh": "Full refresh", + "sources.frequentlyUsed": "Suggested sources", "sources.incremental": "Incremental - based on...", "sources.newSource": "New source", "sources.createFirst": "Connect your first source", diff --git a/airbyte-webapp/src/pages/DestinationPage/pages/CreateDestinationPage/components/DestinationForm.tsx b/airbyte-webapp/src/pages/DestinationPage/pages/CreateDestinationPage/components/DestinationForm.tsx index e3925d8b06405..5d7f38a85392f 100644 --- a/airbyte-webapp/src/pages/DestinationPage/pages/CreateDestinationPage/components/DestinationForm.tsx +++ b/airbyte-webapp/src/pages/DestinationPage/pages/CreateDestinationPage/components/DestinationForm.tsx @@ -5,10 +5,11 @@ import { useLocation } from "react-router-dom"; import { ConnectionConfiguration } from "core/domain/connection"; import { DestinationDefinitionRead } from "core/request/AirbyteClient"; import { LogsRequestError } from "core/request/LogsRequestError"; +import { useExperiment } from "hooks/services/Experiment"; import { useGetDestinationDefinitionSpecificationAsync } from "services/connector/DestinationDefinitionSpecificationService"; import { generateMessageFromError, FormError } from "utils/errorStatusMessage"; import { ConnectorCard } from "views/Connector/ConnectorCard"; -import { ConnectorCardValues, FrequentlyUsedDestinations, StartWithDestination } from "views/Connector/ConnectorForm"; +import { ConnectorCardValues, FrequentlyUsedConnectors, StartWithDestination } from "views/Connector/ConnectorForm"; import styles from "./DestinationForm.module.scss"; @@ -63,8 +64,17 @@ export const DestinationForm: React.FC = ({ const errorMessage = error ? generateMessageFromError(error) : null; + const frequentlyUsedDestinationIds = useExperiment("connector.frequentlyUsedDestinationIds", [ + "22f6c74f-5699-40ff-833c-4a879ea40133", + "424892c4-daac-4491-b35d-c6688ba547ba", + ]); const frequentlyUsedDestinationsComponent = !isLoading && !destinationDefinitionId && ( - + ); const startWithDestinationComponent = !isLoading && !destinationDefinitionId && (
diff --git a/airbyte-webapp/src/test-utils/mock-data/mockFrequentlyUsedDestinations.ts b/airbyte-webapp/src/test-utils/mock-data/mockFrequentlyUsedDestinations.ts index 017ae9110a865..a4ffaa7dc4465 100644 --- a/airbyte-webapp/src/test-utils/mock-data/mockFrequentlyUsedDestinations.ts +++ b/airbyte-webapp/src/test-utils/mock-data/mockFrequentlyUsedDestinations.ts @@ -1,31 +1,75 @@ +import { ConnectorCard } from "views/Connector/ConnectorForm/types"; + import { ReleaseStage } from "../../core/request/AirbyteClient"; -export const mockData = [ +export const mockDestinationsData: ConnectorCard[] = [ { destinationDefinitionId: "1", + id: "1", name: "Google Sheets", icon: '\n\n\n\n\t\n\t\n\t\n\n\n', releaseStage: ReleaseStage.alpha, }, { destinationDefinitionId: "2", + id: "2", name: "BigQuery", icon: '', releaseStage: ReleaseStage.generally_available, }, { destinationDefinitionId: "3", + id: "3", name: "DynamoDB", icon: '', }, { destinationDefinitionId: "4", + id: "4", name: "MongoDB", icon: '\n\n', releaseStage: ReleaseStage.custom, }, { destinationDefinitionId: "5", + id: "5", + name: "Postgres", + icon: '\r\n\r\n\r\n\r\n\r\n\t\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n', + releaseStage: ReleaseStage.alpha, + }, +]; + +export const mockSourcesData: ConnectorCard[] = [ + { + sourceDefinitionId: "1", + id: "1", + name: "Google Sheets", + icon: '\n\n\n\n\t\n\t\n\t\n\n\n', + releaseStage: ReleaseStage.alpha, + }, + { + sourceDefinitionId: "2", + id: "2", + name: "BigQuery", + icon: '', + releaseStage: ReleaseStage.generally_available, + }, + { + sourceDefinitionId: "3", + id: "3", + name: "DynamoDB", + icon: '', + }, + { + sourceDefinitionId: "4", + id: "4", + name: "MongoDB", + icon: '\n\n', + releaseStage: ReleaseStage.custom, + }, + { + sourceDefinitionId: "5", + id: "5", name: "Postgres", icon: '\r\n\r\n\r\n\r\n\r\n\t\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n', releaseStage: ReleaseStage.alpha, diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedConnectors/FrequentlyUsedConnectors.tsx b/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedConnectors/FrequentlyUsedConnectors.tsx new file mode 100644 index 0000000000000..4a40c75ead774 --- /dev/null +++ b/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedConnectors/FrequentlyUsedConnectors.tsx @@ -0,0 +1,37 @@ +import React from "react"; + +import { ConnectorDefinition } from "core/domain/connector"; + +import { FrequentlyUsedConnectorsCard } from "./FrequentlyUsedConnectorsCard"; +import { useAnalyticsTrackFunctions } from "./useAnalyticsTrackFunctions"; +import { useSuggestedConnectors } from "./useSuggestedConnectors"; + +interface FrequentlyUsedConnectorsProps { + availableServices: ConnectorDefinition[]; + connectorType: "source" | "destination"; + connectorIds: string[]; + onConnectorSelect: (id: string) => void; +} + +export const FrequentlyUsedConnectors: React.FC = ({ + availableServices, + connectorType, + connectorIds, + onConnectorSelect, +}) => { + const { trackSelectedSuggestedDestination } = useAnalyticsTrackFunctions(); + + const suggestedConnectors = useSuggestedConnectors({ availableServices, connectorIds }); + const onConnectorCardClick = (id: string, connectorName: string) => { + onConnectorSelect(id); + trackSelectedSuggestedDestination(id, connectorName); + }; + + return ( + + ); +}; diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedDestinations/FrequentlyUsedDestinationsCard.module.scss b/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedConnectors/FrequentlyUsedConnectorsCard.module.scss similarity index 100% rename from airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedDestinations/FrequentlyUsedDestinationsCard.module.scss rename to airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedConnectors/FrequentlyUsedConnectorsCard.module.scss diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedConnectors/FrequentlyUsedConnectorsCard.test.tsx b/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedConnectors/FrequentlyUsedConnectorsCard.test.tsx new file mode 100644 index 0000000000000..77a1df94157f3 --- /dev/null +++ b/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedConnectors/FrequentlyUsedConnectorsCard.test.tsx @@ -0,0 +1,41 @@ +import { fireEvent, render, waitFor } from "@testing-library/react"; +import { IntlProvider } from "react-intl"; +import { mockDestinationsData } from "test-utils/mock-data/mockFrequentlyUsedDestinations"; + +import en from "locales/en.json"; + +import { FrequentlyUsedConnectorsCard, FrequentlyUsedConnectorsCardProps } from "./FrequentlyUsedConnectorsCard"; + +const renderFrequentlyUsedConnectorsComponent = (props: FrequentlyUsedConnectorsCardProps) => + render( + + + + ); + +describe("", () => { + it("should renders with mock data without crash", () => { + const component = renderFrequentlyUsedConnectorsComponent({ + connectors: mockDestinationsData, + connectorType: "destination", + onConnectorSelect: jest.fn(), + }); + + expect(component).toMatchSnapshot(); + }); + + it("should call provided handler with right param", async () => { + const handler = jest.fn(); + const { getByText } = renderFrequentlyUsedConnectorsComponent({ + connectors: mockDestinationsData, + connectorType: "destination", + onConnectorSelect: handler, + }); + fireEvent.click(getByText("BigQuery")); + + await waitFor(() => { + expect(handler).toHaveBeenCalledTimes(1); + expect(handler).toHaveBeenCalledWith("2", "BigQuery"); + }); + }); +}); diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedConnectors/FrequentlyUsedConnectorsCard.tsx b/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedConnectors/FrequentlyUsedConnectorsCard.tsx new file mode 100644 index 0000000000000..97c77b77c5726 --- /dev/null +++ b/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedConnectors/FrequentlyUsedConnectorsCard.tsx @@ -0,0 +1,42 @@ +import React from "react"; +import { useIntl } from "react-intl"; + +import { ConnectorCard } from "components"; +import { SlickSlider } from "components/ui/SlickSlider"; + +import { ConnectorCard as ConnectorCardType } from "../../types"; +import styles from "./FrequentlyUsedConnectorsCard.module.scss"; + +export interface FrequentlyUsedConnectorsCardProps { + connectors: ConnectorCardType[]; + connectorType: "source" | "destination"; + onConnectorSelect: (id: string, connectorName: string) => void; +} + +export const FrequentlyUsedConnectorsCard: React.FC = ({ + connectors, + onConnectorSelect, + connectorType, +}) => { + const { formatMessage } = useIntl(); + + if (connectors.length === 0) { + return null; + } + + return ( +
+ + {connectors.map(({ id, name, icon, releaseStage }, index) => ( + + ))} + +
+ ); +}; diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedDestinations/__snapshots__/FrequentlyUsedDestinationsCard.test.tsx.snap b/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedConnectors/__snapshots__/FrequentlyUsedConnectorsCard.test.tsx.snap similarity index 99% rename from airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedDestinations/__snapshots__/FrequentlyUsedDestinationsCard.test.tsx.snap rename to airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedConnectors/__snapshots__/FrequentlyUsedConnectorsCard.test.tsx.snap index 7eac3efa49d18..156cf2188e81a 100644 --- a/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedDestinations/__snapshots__/FrequentlyUsedDestinationsCard.test.tsx.snap +++ b/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedConnectors/__snapshots__/FrequentlyUsedConnectorsCard.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` should renders with mock data without crash 1`] = ` +exports[` should renders with mock data without crash 1`] = `
; + +const Template: ComponentStory = (args) => ( +
+ +
+); +export const Destinations = Template.bind({}); + +export const Sources = Template.bind({}); +Sources.args = { + ...Template.args, + connectors: mockSourcesData, + connectorType: "source", +}; diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedConnectors/index.ts b/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedConnectors/index.ts new file mode 100644 index 0000000000000..64b1515cdfe92 --- /dev/null +++ b/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedConnectors/index.ts @@ -0,0 +1 @@ +export { FrequentlyUsedConnectors } from "./FrequentlyUsedConnectors"; diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedDestinations/useAnalyticsTrackFunctions.tsx b/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedConnectors/useAnalyticsTrackFunctions.tsx similarity index 100% rename from airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedDestinations/useAnalyticsTrackFunctions.tsx rename to airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedConnectors/useAnalyticsTrackFunctions.tsx diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedConnectors/useSuggestedConnectors.ts b/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedConnectors/useSuggestedConnectors.ts new file mode 100644 index 0000000000000..85708b04fc6c1 --- /dev/null +++ b/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedConnectors/useSuggestedConnectors.ts @@ -0,0 +1,47 @@ +import { useMemo } from "react"; + +import { ConnectorDefinition } from "core/domain/connector"; +import { isDestinationDefinition } from "core/domain/connector/destination"; +import { isSourceDefinition } from "core/domain/connector/source"; + +import { ConnectorCard } from "../../types"; + +interface Props { + availableServices: ConnectorDefinition[]; + connectorIds: string[]; +} +export const useSuggestedConnectors = ({ availableServices, connectorIds }: Props): ConnectorCard[] => { + return useMemo( + () => + availableServices + .filter((service) => { + if (isDestinationDefinition(service)) { + return connectorIds.includes(service.destinationDefinitionId); + } + + return isSourceDefinition(service) && connectorIds.includes(service.sourceDefinitionId); + }) + .map((service) => { + if (isDestinationDefinition(service)) { + const { destinationDefinitionId, name, icon, releaseStage } = service; + return { + id: destinationDefinitionId, + destinationDefinitionId, + name, + icon, + releaseStage, + }; + } + + const { sourceDefinitionId, name, icon, releaseStage } = service; + return { + id: sourceDefinitionId, + sourceDefinitionId, + name, + icon, + releaseStage, + }; + }), + [availableServices, connectorIds] + ); +}; diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedDestinations/FrequentlyUsedDestinations.tsx b/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedDestinations/FrequentlyUsedDestinations.tsx deleted file mode 100644 index 46b0cf3d9c8c7..0000000000000 --- a/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedDestinations/FrequentlyUsedDestinations.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React, { useMemo } from "react"; - -import { ConnectorDefinition } from "core/domain/connector"; -import { isDestinationDefinition } from "core/domain/connector/destination"; -import { useExperiment } from "hooks/services/Experiment"; -import { DestinationDefinitionReadWithLatestTag } from "services/connector/DestinationDefinitionService"; - -import { DestinationConnectorCard } from "../../types"; -import { FrequentlyUsedDestinationsCard } from "./FrequentlyUsedDestinationsCard"; -import { useAnalyticsTrackFunctions } from "./useAnalyticsTrackFunctions"; - -interface FrequentlyUsedDestinationsProps { - availableServices: ConnectorDefinition[]; - onDestinationSelect: (id: string) => void; -} - -export const FrequentlyUsedDestinations: React.FC = ({ - availableServices, - onDestinationSelect, -}) => { - const frequentlyUsedDestinationIds = useExperiment("connector.frequentlyUsedDestinationIds", [ - "22f6c74f-5699-40ff-833c-4a879ea40133", - "424892c4-daac-4491-b35d-c6688ba547ba", - ]); - const { trackSelectedSuggestedDestination } = useAnalyticsTrackFunctions(); - - const frequentlyUsedDestinations: DestinationConnectorCard[] = useMemo( - () => - availableServices - .filter( - (service): service is DestinationDefinitionReadWithLatestTag => - isDestinationDefinition(service) && frequentlyUsedDestinationIds.includes(service.destinationDefinitionId) - ) - .map(({ destinationDefinitionId, name, icon, releaseStage }) => ({ - destinationDefinitionId, - name, - icon, - releaseStage, - })), - [availableServices, frequentlyUsedDestinationIds] - ); - - const onDestinationCardClick = (id: string, connectorName: string) => { - onDestinationSelect(id); - trackSelectedSuggestedDestination(id, connectorName); - }; - - return ( - - ); -}; diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedDestinations/FrequentlyUsedDestinationsCard.test.tsx b/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedDestinations/FrequentlyUsedDestinationsCard.test.tsx deleted file mode 100644 index 448acc3a61da4..0000000000000 --- a/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedDestinations/FrequentlyUsedDestinationsCard.test.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { fireEvent, render, waitFor } from "@testing-library/react"; -import { IntlProvider } from "react-intl"; - -import en from "locales/en.json"; - -import { mockData } from "../../../../../test-utils/mock-data/mockFrequentlyUsedDestinations"; -import { FrequentlyUsedDestinationsCard, FrequentlyUsedDestinationsCardProps } from "./FrequentlyUsedDestinationsCard"; - -const renderFrequentlyUsedDestinationsComponent = (props: FrequentlyUsedDestinationsCardProps) => - render( - - - - ); - -describe("", () => { - it("should renders with mock data without crash", () => { - const component = renderFrequentlyUsedDestinationsComponent({ - destinations: mockData, - onDestinationSelect: jest.fn(), - }); - - expect(component).toMatchSnapshot(); - }); - - it("should call provided handler with right param", async () => { - const handler = jest.fn(); - const { getByText } = renderFrequentlyUsedDestinationsComponent({ - destinations: mockData, - onDestinationSelect: handler, - }); - fireEvent.click(getByText("BigQuery")); - - await waitFor(() => { - expect(handler).toHaveBeenCalledTimes(1); - expect(handler).toHaveBeenCalledWith("2", "BigQuery"); - }); - }); -}); diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedDestinations/FrequentlyUsedDestinationsCard.tsx b/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedDestinations/FrequentlyUsedDestinationsCard.tsx deleted file mode 100644 index 98e9287084cb3..0000000000000 --- a/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedDestinations/FrequentlyUsedDestinationsCard.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from "react"; -import { useIntl } from "react-intl"; - -import { ConnectorCard } from "components"; -import { SlickSlider } from "components/ui/SlickSlider"; - -import { DestinationConnectorCard } from "../../types"; -import styles from "./FrequentlyUsedDestinationsCard.module.scss"; - -export interface FrequentlyUsedDestinationsCardProps { - destinations: DestinationConnectorCard[]; - onDestinationSelect: (id: string, connectorName: string) => void; -} - -export const FrequentlyUsedDestinationsCard: React.FC = ({ - destinations, - onDestinationSelect, -}) => { - const { formatMessage } = useIntl(); - - if (!destinations?.length) { - return null; - } - - return ( -
- - {destinations.map(({ destinationDefinitionId, name, icon, releaseStage }, index) => ( - - ))} - -
- ); -}; diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedDestinations/index.stories.tsx b/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedDestinations/index.stories.tsx deleted file mode 100644 index d9f6de88de213..0000000000000 --- a/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedDestinations/index.stories.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { ComponentStory, ComponentMeta } from "@storybook/react"; - -import { mockData } from "../../../../../test-utils/mock-data/mockFrequentlyUsedDestinations"; -import { FrequentlyUsedDestinationsCard } from "./FrequentlyUsedDestinationsCard"; - -export default { - title: "Views/FrequentlyUsedDestinations", - component: FrequentlyUsedDestinationsCard, - args: { - destinations: mockData, - propertyPath: "serviceType", - }, -} as ComponentMeta; - -export const Template: ComponentStory = (args) => ( -
- -
-); diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedDestinations/index.ts b/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedDestinations/index.ts deleted file mode 100644 index 6f1794559211e..0000000000000 --- a/airbyte-webapp/src/views/Connector/ConnectorForm/components/FrequentlyUsedDestinations/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { FrequentlyUsedDestinations } from "./FrequentlyUsedDestinations"; diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/index.tsx b/airbyte-webapp/src/views/Connector/ConnectorForm/index.tsx index 07b4aa404f4b5..38f44049f6375 100644 --- a/airbyte-webapp/src/views/Connector/ConnectorForm/index.tsx +++ b/airbyte-webapp/src/views/Connector/ConnectorForm/index.tsx @@ -2,4 +2,4 @@ export * from "./types"; export * from "./ConnectorForm"; export { StartWithDestination } from "./components/StartWithDestination"; -export { FrequentlyUsedDestinations } from "./components/FrequentlyUsedDestinations"; +export { FrequentlyUsedConnectors } from "./components/FrequentlyUsedConnectors"; diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/types.ts b/airbyte-webapp/src/views/Connector/ConnectorForm/types.ts index 485b1013dc2b9..f40b5409975fc 100644 --- a/airbyte-webapp/src/views/Connector/ConnectorForm/types.ts +++ b/airbyte-webapp/src/views/Connector/ConnectorForm/types.ts @@ -1,4 +1,5 @@ import { DestinationDefinitionReadWithLatestTag } from "services/connector/DestinationDefinitionService"; +import { SourceDefinitionReadWithLatestTag } from "services/connector/SourceDefinitionService"; // TODO: This needs to be converted to interface, but has int he current state a problem with index signatures // eslint-disable-next-line @typescript-eslint/consistent-type-definitions @@ -14,3 +15,9 @@ export type DestinationConnectorCard = Pick< DestinationDefinitionReadWithLatestTag, "destinationDefinitionId" | "name" | "icon" | "releaseStage" >; +export type SourceConnectorCard = Pick< + SourceDefinitionReadWithLatestTag, + "sourceDefinitionId" | "name" | "icon" | "releaseStage" +>; + +export type ConnectorCard = (DestinationConnectorCard | SourceConnectorCard) & { id: string };