diff --git a/airbyte-webapp/src/locales/en.json b/airbyte-webapp/src/locales/en.json index 3bb4e3805d6..dd15e6956dd 100644 --- a/airbyte-webapp/src/locales/en.json +++ b/airbyte-webapp/src/locales/en.json @@ -550,7 +550,7 @@ "tables.name": "Name", "tables.connector": "Connector", "tables.connections": "Connections", - "tables.connections.pluralized": "{value, plural, one {connection} other {# connections}}", + "tables.connections.pluralized": "{value, plural, one {# connection} other {# connections}}", "tables.sourceConnectWith": "Destination", "tables.destinationConnectWith": "Source", "tables.sources": "Sources", diff --git a/airbyte-webapp/src/packages/cloud/components/experiments/FreeConnectorProgram/lib/model.test.ts b/airbyte-webapp/src/packages/cloud/components/experiments/FreeConnectorProgram/lib/model.test.ts new file mode 100644 index 00000000000..c0a8972442a --- /dev/null +++ b/airbyte-webapp/src/packages/cloud/components/experiments/FreeConnectorProgram/lib/model.test.ts @@ -0,0 +1,72 @@ +import { mockConnection } from "test-utils"; +import { mockDestinationDefinition } from "test-utils/mock-data/mockDestination"; +import { mockSourceDefinition } from "test-utils/mock-data/mockSource"; + +import { DestinationDefinitionRead, SourceDefinitionRead, WebBackendConnectionRead } from "core/request/AirbyteClient"; + +import { + isConnectionEligibleForFCP, + isDestinationDefinitionEligibleForFCP, + isSourceDefinitionEligibleForFCP, +} from "./model"; + +const deprecatedConnection: WebBackendConnectionRead = { ...mockConnection, status: "deprecated" }; +const alphaSource: SourceDefinitionRead = { ...mockSourceDefinition, releaseStage: "alpha" }; +const betaSource: SourceDefinitionRead = { ...mockSourceDefinition, releaseStage: "beta" }; +const gaSource: SourceDefinitionRead = { + ...mockSourceDefinition, + releaseStage: "generally_available", +}; +const customSource: SourceDefinitionRead = { ...mockSourceDefinition, releaseStage: "custom" }; +const alphaDestination: DestinationDefinitionRead = { ...mockDestinationDefinition, releaseStage: "alpha" }; +const betaDestination: DestinationDefinitionRead = { ...mockDestinationDefinition, releaseStage: "beta" }; +const gaDestination: DestinationDefinitionRead = { + ...mockDestinationDefinition, + releaseStage: "generally_available", +}; +const customDestination: DestinationDefinitionRead = { ...mockDestinationDefinition, releaseStage: "custom" }; + +describe(`${isConnectionEligibleForFCP.name}`, () => { + it("returns true for an alpha source and GA destination", () => { + expect(isConnectionEligibleForFCP(mockConnection, alphaSource, gaDestination)).toBe(true); + }); + it("returns true for a GA source and alpha destination", () => { + expect(isConnectionEligibleForFCP(mockConnection, gaSource, alphaDestination)).toBe(true); + }); + it("returns false for a deprecated connection", () => { + expect(isConnectionEligibleForFCP(deprecatedConnection, alphaSource, alphaDestination)).toBe(false); + }); + it("returns false for a custom source", () => { + expect(isConnectionEligibleForFCP(deprecatedConnection, customSource, alphaDestination)).toBe(false); + }); +}); + +describe(`${isSourceDefinitionEligibleForFCP.name}`, () => { + it("returns true for an alpha destination", () => { + expect(isSourceDefinitionEligibleForFCP(alphaSource)).toBe(true); + }); + it("returns true for an beta destination", () => { + expect(isSourceDefinitionEligibleForFCP(betaSource)).toBe(true); + }); + it("returns false for an GA destination", () => { + expect(isSourceDefinitionEligibleForFCP(gaSource)).toBe(false); + }); + it("returns false for a custom destination", () => { + expect(isSourceDefinitionEligibleForFCP(customSource)).toBe(false); + }); +}); + +describe(`${isDestinationDefinitionEligibleForFCP.name}`, () => { + it("returns true for an alpha destination", () => { + expect(isDestinationDefinitionEligibleForFCP(alphaDestination)).toBe(true); + }); + it("returns true for an beta destination", () => { + expect(isDestinationDefinitionEligibleForFCP(betaDestination)).toBe(true); + }); + it("returns false for an GA destination", () => { + expect(isDestinationDefinitionEligibleForFCP(gaDestination)).toBe(false); + }); + it("returns false for a custom destination", () => { + expect(isDestinationDefinitionEligibleForFCP(customDestination)).toBe(false); + }); +}); diff --git a/airbyte-webapp/src/packages/cloud/components/experiments/FreeConnectorProgram/lib/model.ts b/airbyte-webapp/src/packages/cloud/components/experiments/FreeConnectorProgram/lib/model.ts index 21e96c18ad6..426f5bba878 100644 --- a/airbyte-webapp/src/packages/cloud/components/experiments/FreeConnectorProgram/lib/model.ts +++ b/airbyte-webapp/src/packages/cloud/components/experiments/FreeConnectorProgram/lib/model.ts @@ -1,3 +1,25 @@ -import { ReleaseStage } from "core/request/AirbyteClient"; +import { DestinationDefinitionRead, SourceDefinitionRead, WebBackendConnectionRead } from "core/request/AirbyteClient"; -export const freeReleaseStages: ReleaseStage[] = ["alpha", "beta"]; +export const freeReleaseStages = ["alpha", "beta"]; + +export const isConnectionEligibleForFCP = ( + connection: WebBackendConnectionRead, + sourceDefinition: SourceDefinitionRead, + destinationDefinition: DestinationDefinitionRead +): boolean => { + return !!( + connection.status !== "deprecated" && + sourceDefinition.releaseStage && + destinationDefinition.releaseStage && + (freeReleaseStages.includes(sourceDefinition.releaseStage) || + freeReleaseStages.includes(destinationDefinition.releaseStage)) + ); +}; + +export const isSourceDefinitionEligibleForFCP = (sourceDefinition: SourceDefinitionRead): boolean => { + return !!(sourceDefinition.releaseStage && freeReleaseStages.includes(sourceDefinition.releaseStage)); +}; + +export const isDestinationDefinitionEligibleForFCP = (destinationDefinition: DestinationDefinitionRead): boolean => { + return !!(destinationDefinition.releaseStage && freeReleaseStages.includes(destinationDefinition.releaseStage)); +}; diff --git a/airbyte-webapp/src/pages/connections/ConnectionPage/ConnectionTitleBlock.tsx b/airbyte-webapp/src/pages/connections/ConnectionPage/ConnectionTitleBlock.tsx index 526dfb5ccac..4996d51c807 100644 --- a/airbyte-webapp/src/pages/connections/ConnectionPage/ConnectionTitleBlock.tsx +++ b/airbyte-webapp/src/pages/connections/ConnectionPage/ConnectionTitleBlock.tsx @@ -17,6 +17,7 @@ import { useSchemaChanges } from "hooks/connection/useSchemaChanges"; import { useConnectionEditService } from "hooks/services/ConnectionEdit/ConnectionEditService"; import { useConnectionFormService } from "hooks/services/ConnectionForm/ConnectionFormService"; import { InlineEnrollmentCallout } from "packages/cloud/components/experiments/FreeConnectorProgram/InlineEnrollmentCallout"; +import { isConnectionEligibleForFCP } from "packages/cloud/components/experiments/FreeConnectorProgram/lib/model"; import { RoutePaths } from "pages/routePaths"; import styles from "./ConnectionTitleBlock.module.scss"; @@ -49,9 +50,8 @@ const ConnectorBlock: React.FC = ({ name, icon, id, stage, }; export const ConnectionTitleBlock = () => { - const { - connection: { name, source, destination, schemaChange, status }, - } = useConnectionEditService(); + const { connection } = useConnectionEditService(); + const { name, source, destination, schemaChange, status } = connection; const { sourceDefinition, destDefinition } = useConnectionFormService(); const { hasBreakingSchemaChange } = useSchemaChanges(schemaChange); const fcpEnabled = useFeature(FeatureItem.FreeConnectorProgram); @@ -90,10 +90,9 @@ export const ConnectionTitleBlock = () => { text={} /> )} - {fcpEnabled && - status !== ConnectionStatus.deprecated && - (sourceDefinition.releaseStage !== ReleaseStage.generally_available || - destDefinition.releaseStage !== ReleaseStage.generally_available) && } + {fcpEnabled && isConnectionEligibleForFCP(connection, sourceDefinition, destDefinition) && ( + + )} ); }; diff --git a/airbyte-webapp/src/pages/connections/CreateConnectionPage/CreateConnectionPage.tsx b/airbyte-webapp/src/pages/connections/CreateConnectionPage/CreateConnectionPage.tsx index 8ccd5577bdf..fef3595e689 100644 --- a/airbyte-webapp/src/pages/connections/CreateConnectionPage/CreateConnectionPage.tsx +++ b/airbyte-webapp/src/pages/connections/CreateConnectionPage/CreateConnectionPage.tsx @@ -13,10 +13,8 @@ import { NextPageHeaderWithNavigation } from "components/ui/PageHeader/NextPageH import { StepsIndicator } from "components/ui/StepsIndicator"; import { useTrackPage, PageTrackingCodes } from "core/services/analytics"; -import { FeatureItem, useFeature } from "core/services/features"; import { AppActionCodes, useAppMonitoringService } from "hooks/services/AppMonitoringService"; import { useFormChangeTrackerService } from "hooks/services/FormChangeTracker"; -import { InlineEnrollmentCallout } from "packages/cloud/components/experiments/FreeConnectorProgram/InlineEnrollmentCallout"; import { RoutePaths } from "pages/routePaths"; import { useCurrentWorkspaceId } from "services/workspaces/WorkspacesService"; import { ConnectorDocumentationWrapper } from "views/Connector/ConnectorDocumentationLayout"; @@ -35,7 +33,6 @@ export const CreateConnectionPage: React.FC = () => { useTrackPage(PageTrackingCodes.CONNECTIONS_NEW); const location = useLocation(); const { formatMessage } = useIntl(); - const fcpEnabled = useFeature(FeatureItem.FreeConnectorProgram); const workspaceId = useCurrentWorkspaceId(); const { trackAction } = useAppMonitoringService(); @@ -158,7 +155,7 @@ export const CreateConnectionPage: React.FC = () => { } /> )} - {fcpEnabled && } + {renderStep()} diff --git a/airbyte-webapp/src/pages/destination/DestinationItemPage/DestinationItemPage.tsx b/airbyte-webapp/src/pages/destination/DestinationItemPage/DestinationItemPage.tsx index 0c40fe8d7a3..ff266d689b8 100644 --- a/airbyte-webapp/src/pages/destination/DestinationItemPage/DestinationItemPage.tsx +++ b/airbyte-webapp/src/pages/destination/DestinationItemPage/DestinationItemPage.tsx @@ -11,7 +11,10 @@ import { StepsTypes } from "components/ConnectorBlocks"; import { NextPageHeaderWithNavigation } from "components/ui/PageHeader/NextPageHeaderWithNavigation"; import { useTrackPage, PageTrackingCodes } from "core/services/analytics"; +import { FeatureItem, useFeature } from "core/services/features"; import { useAppMonitoringService } from "hooks/services/AppMonitoringService"; +import InlineEnrollmentCallout from "packages/cloud/components/experiments/FreeConnectorProgram/InlineEnrollmentCallout"; +import { isDestinationDefinitionEligibleForFCP } from "packages/cloud/components/experiments/FreeConnectorProgram/lib/model"; import { RoutePaths } from "pages/routePaths"; import { useDestinationDefinition } from "services/connector/DestinationDefinitionService"; import { ResourceNotFoundErrorBoundary } from "views/common/ResourceNotFoundErrorBoundary"; @@ -26,6 +29,7 @@ export const DestinationItemPage: React.FC = () => { const destination = useGetDestinationFromParams(); const destinationDefinition = useDestinationDefinition(destination.destinationDefinitionId); const { formatMessage } = useIntl(); + const fcpEnabled = useFeature(FeatureItem.FreeConnectorProgram); const { trackError } = useAppMonitoringService(); @@ -45,6 +49,7 @@ export const DestinationItemPage: React.FC = () => { + {fcpEnabled && isDestinationDefinitionEligibleForFCP(destinationDefinition) && } }> diff --git a/airbyte-webapp/src/pages/source/SourceItemPage/SourceItemPage.tsx b/airbyte-webapp/src/pages/source/SourceItemPage/SourceItemPage.tsx index 13e80e76264..798a2b3857f 100644 --- a/airbyte-webapp/src/pages/source/SourceItemPage/SourceItemPage.tsx +++ b/airbyte-webapp/src/pages/source/SourceItemPage/SourceItemPage.tsx @@ -11,7 +11,10 @@ import LoadingPage from "components/LoadingPage"; import { NextPageHeaderWithNavigation } from "components/ui/PageHeader/NextPageHeaderWithNavigation"; import { useTrackPage, PageTrackingCodes } from "core/services/analytics"; +import { FeatureItem, useFeature } from "core/services/features"; import { useAppMonitoringService } from "hooks/services/AppMonitoringService"; +import InlineEnrollmentCallout from "packages/cloud/components/experiments/FreeConnectorProgram/InlineEnrollmentCallout"; +import { isSourceDefinitionEligibleForFCP } from "packages/cloud/components/experiments/FreeConnectorProgram/lib/model"; import { RoutePaths } from "pages/routePaths"; import { useSourceDefinition } from "services/connector/SourceDefinitionService"; import { ResourceNotFoundErrorBoundary } from "views/common/ResourceNotFoundErrorBoundary"; @@ -26,6 +29,7 @@ export const SourceItemPage: React.FC = () => { const source = useGetSourceFromParams(); const sourceDefinition = useSourceDefinition(source.sourceDefinitionId); const { formatMessage } = useIntl(); + const fcpEnabled = useFeature(FeatureItem.FreeConnectorProgram); const breadcrumbBasePath = `/${RoutePaths.Workspaces}/${params.workspaceId}/${RoutePaths.Source}`; @@ -45,6 +49,7 @@ export const SourceItemPage: React.FC = () => { + {fcpEnabled && isSourceDefinitionEligibleForFCP(sourceDefinition) && } }>