diff --git a/airbyte-webapp/src/components/CreateConnection/__snapshots__/CreateConnectionForm.test.tsx.snap b/airbyte-webapp/src/components/CreateConnection/__snapshots__/CreateConnectionForm.test.tsx.snap index c2406dfd0cd75..b7abe8343415d 100644 --- a/airbyte-webapp/src/components/CreateConnection/__snapshots__/CreateConnectionForm.test.tsx.snap +++ b/airbyte-webapp/src/components/CreateConnection/__snapshots__/CreateConnectionForm.test.tsx.snap @@ -441,13 +441,13 @@ exports[`CreateConnectionForm should render 1`] = ` class="" > Source -
-
-
-
-
-
+ + +
Sync mode -
-
-
-
-
-
+ + +
Cursor field -
-
-
-
-
-
+ + +
Primary key -
-
-
-
-
-
+ + +
Destination -
-
-
-
-
-
+ + +
> = ({ flex, children }) => { @@ -24,7 +40,24 @@ const TextCell: React.FC> = ({ flex, export const CatalogTreeTableHeader: React.FC = () => { const { mode } = useConnectionFormService(); + const { openModal, closeModal } = useModalService(); const { onCheckAll, selectedBatchNodeIds, allChecked } = useBulkEditService(); + const formikProps = useFormikContext(); + + const destinationNamespaceChange = (value: DestinationNamespaceFormValueType) => { + formikProps.setFieldValue("namespaceDefinition", value.namespaceDefinition); + + if (value.namespaceDefinition === NamespaceDefinitionType.customformat) { + formikProps.setFieldValue("namespaceFormat", value.namespaceFormat); + } + }; + + const destinationStreamNamesChange = (value: DestinationStreamNamesFormValueType) => { + formikProps.setFieldValue( + "prefix", + value.streamNameDefinition === StreamNameDefinitionValueType.Prefix ? value.prefix : "" + ); + }; return (
@@ -68,9 +101,52 @@ export const CatalogTreeTableHeader: React.FC = () => {
+ +
); diff --git a/airbyte-webapp/src/components/connection/CatalogTree/next/__snapshots__/BulkEditPanel.test.tsx.snap b/airbyte-webapp/src/components/connection/CatalogTree/next/__snapshots__/BulkEditPanel.test.tsx.snap index 6c8de0e738a7d..12f584eafe8cd 100644 --- a/airbyte-webapp/src/components/connection/CatalogTree/next/__snapshots__/BulkEditPanel.test.tsx.snap +++ b/airbyte-webapp/src/components/connection/CatalogTree/next/__snapshots__/BulkEditPanel.test.tsx.snap @@ -71,7 +71,7 @@ exports[` should render 1`] = ` class="" />
-
-
+
@@ -128,7 +128,7 @@ exports[` should render 1`] = ` class="" />
-
-
+
@@ -186,7 +186,7 @@ exports[` should render 1`] = ` class="" />
-
-
+
diff --git a/airbyte-webapp/src/components/connection/DestinationNamespaceModal/DestinationNamespaceModal.module.scss b/airbyte-webapp/src/components/connection/DestinationNamespaceModal/DestinationNamespaceModal.module.scss new file mode 100644 index 0000000000000..7b145f2bb0b85 --- /dev/null +++ b/airbyte-webapp/src/components/connection/DestinationNamespaceModal/DestinationNamespaceModal.module.scss @@ -0,0 +1,44 @@ +@use "scss/colors"; +@use "scss/variables"; + +.content { + display: flex; + height: 340px; +} + +.actions, .description { + display: flex; + flex-direction: column; + padding: variables.$spacing-xl; +} + +.actions { + flex: 0 0 300px; + + .radioButton + .radioButton { + margin-top: variables.$spacing-xl; + } + + .input { + position: relative; + margin: variables.$spacing-xl 0 0 variables.$spacing-xl; + + .errorMessage { + position: absolute; + bottom: -16px; + left: 0; + color: colors.$red-600; + } + } +} + +.description { + overflow: auto; + flex: 1 0 0; + background-color: colors.$grey-50; + + .generalInfo { + color: colors.$grey; + margin: variables.$spacing-lg 0; + } +} diff --git a/airbyte-webapp/src/components/connection/DestinationNamespaceModal/DestinationNamespaceModal.tsx b/airbyte-webapp/src/components/connection/DestinationNamespaceModal/DestinationNamespaceModal.tsx new file mode 100644 index 0000000000000..48587c2e580fe --- /dev/null +++ b/airbyte-webapp/src/components/connection/DestinationNamespaceModal/DestinationNamespaceModal.tsx @@ -0,0 +1,160 @@ +import { Field, FieldProps, Form, Formik } from "formik"; +import React from "react"; +import { FormattedMessage, useIntl } from "react-intl"; +import * as yup from "yup"; + +import { LabeledRadioButton } from "components"; +import { Button } from "components/ui/Button"; +import { Input } from "components/ui/Input"; +import { ModalBody, ModalFooter } from "components/ui/Modal"; +import { Text } from "components/ui/Text"; + +import { NamespaceDefinitionType } from "core/request/AirbyteClient"; +import { FormikConnectionFormValues } from "views/Connection/ConnectionForm/formConfig"; + +import styles from "./DestinationNamespaceModal.module.scss"; +import { ExampleSettingsTable } from "./ExampleSettingsTable"; + +const destinationNamespaceValidationSchema = yup.object().shape({ + namespaceDefinition: yup + .string() + .oneOf([NamespaceDefinitionType.source, NamespaceDefinitionType.destination, NamespaceDefinitionType.customformat]) + .required("form.empty.error"), + namespaceFormat: yup.string().when("namespaceDefinition", { + is: NamespaceDefinitionType.customformat, + then: yup.string().trim().required("form.empty.error"), + }), +}); + +export interface DestinationNamespaceFormValueType { + namespaceDefinition: NamespaceDefinitionType; + namespaceFormat: string; +} + +interface DestinationNamespaceModalProps { + initialValues: Pick; + onCloseModal: () => void; + onSubmit: (values: DestinationNamespaceFormValueType) => void; +} + +export const DestinationNamespaceModal: React.FC = ({ + initialValues, + onCloseModal, + onSubmit, +}) => { + const { formatMessage } = useIntl(); + + return ( + { + onCloseModal(); + onSubmit(values); + }} + > + {({ dirty, isValid, errors, values }) => ( +
+ +
+ + {({ field }: FieldProps) => ( + + + + } + value={NamespaceDefinitionType.source} + checked={field.value === NamespaceDefinitionType.source} + /> + )} + + + {({ field }: FieldProps) => ( + + + + } + value={NamespaceDefinitionType.destination} + checked={field.value === NamespaceDefinitionType.destination} + /> + )} + + + {({ field }: FieldProps) => ( + + + + } + value={NamespaceDefinitionType.customformat} + checked={field.value === NamespaceDefinitionType.customformat} + /> + )} + +
+ + {({ field, meta }: FieldProps) => ( + + )} + + {!!errors.namespaceFormat && ( + + + + )} +
+
+
+ {values.namespaceDefinition === NamespaceDefinitionType.source && ( + + )} + {(values.namespaceDefinition === NamespaceDefinitionType.destination || + values.namespaceDefinition === NamespaceDefinitionType.customformat) && ( + + )} + + + + +
+
+ + + + +
+ )} +
+ ); +}; diff --git a/airbyte-webapp/src/components/connection/DestinationNamespaceModal/ExampleSettingsTable.module.scss b/airbyte-webapp/src/components/connection/DestinationNamespaceModal/ExampleSettingsTable.module.scss new file mode 100644 index 0000000000000..1991e143b5f30 --- /dev/null +++ b/airbyte-webapp/src/components/connection/DestinationNamespaceModal/ExampleSettingsTable.module.scss @@ -0,0 +1,50 @@ +@use "scss/colors"; +@use "scss/variables"; + +.exampleSettingsTable { + border-spacing: 0; + border-collapse: separate; + + tr { + th, td { + padding: variables.$spacing-md; + border-style: solid; + border-color: colors.$dark-blue-50; + border-width: 0 variables.$border-thin variables.$border-thin 0; + } + + th { + border-top-width: variables.$border-thin; + background-color: colors.$dark-blue-50; + text-align: left; + + .text { + color: colors.$grey-600; + } + } + + td { + background-color: colors.$white; + + &:first-child, th:first-child { + border-left-width: variables.$border-thin; + } + } + + &:first-child th:first-child { + border-top-left-radius: variables.$border-radius-xs; + } + + &:first-child th:last-child { + border-top-right-radius: variables.$border-radius-xs; + } + + &:last-child td:first-child { + border-bottom-left-radius: variables.$border-radius-xs; + } + + &:last-child td:last-child { + border-bottom-right-radius: variables.$border-radius-xs; + } + } +} diff --git a/airbyte-webapp/src/components/connection/DestinationNamespaceModal/ExampleSettingsTable.tsx b/airbyte-webapp/src/components/connection/DestinationNamespaceModal/ExampleSettingsTable.tsx new file mode 100644 index 0000000000000..097b10c19bdcd --- /dev/null +++ b/airbyte-webapp/src/components/connection/DestinationNamespaceModal/ExampleSettingsTable.tsx @@ -0,0 +1,43 @@ +import React from "react"; + +import { Text } from "components/ui/Text"; + +import { NamespaceDefinitionType } from "core/request/AirbyteClient"; + +import styles from "./ExampleSettingsTable.module.scss"; +import { useExampleTableData } from "./useExampleSettingsTable"; + +interface ExampleSettingsTableProps { + namespaceDefinitionType: NamespaceDefinitionType; +} + +export const ExampleSettingsTable: React.FC = ({ namespaceDefinitionType }) => { + const { columns, data } = useExampleTableData(namespaceDefinitionType); + + return ( + + + + {columns.map((column, index) => ( + + ))} + + + + {data.map((row, rowIndex) => ( + + {columns.map((column, dataIndex) => ( + + ))} + + ))} + +
+ + {column.displayName} + +
+ {row[column.id]} +
+ ); +}; diff --git a/airbyte-webapp/src/components/connection/DestinationNamespaceModal/useExampleSettingsTable.ts b/airbyte-webapp/src/components/connection/DestinationNamespaceModal/useExampleSettingsTable.ts new file mode 100644 index 0000000000000..1239b6549f210 --- /dev/null +++ b/airbyte-webapp/src/components/connection/DestinationNamespaceModal/useExampleSettingsTable.ts @@ -0,0 +1,160 @@ +import { useIntl } from "react-intl"; + +import { NamespaceDefinitionType } from "core/request/AirbyteClient"; + +export const useExampleTableData = ( + namespaceDefinition: NamespaceDefinitionType +): { + columns: Array<{ id: string; displayName: string }>; + data: Array>; +} => { + const { formatMessage } = useIntl(); + + switch (namespaceDefinition) { + case NamespaceDefinitionType.source: + return { + columns: [ + { + id: "sourceNamespace", + displayName: formatMessage({ + id: "connectionForm.modal.destinationNamespace.table.header.sourceNamespace", + }), + }, + { + id: "destinationNamespace", + displayName: formatMessage({ + id: "connectionForm.modal.destinationNamespace.table.header.destinationNamespace", + }), + }, + ], + data: [ + { + sourceNamespace: formatMessage({ + id: "connectionForm.modal.destinationNamespace.table.data.public", + }), + destinationNamespace: formatMessage({ + id: "connectionForm.modal.destinationNamespace.table.data.public", + }), + }, + { + sourceNamespace: "", + destinationNamespace: formatMessage({ + id: "connectionForm.modal.destinationNamespace.table.data.mySchema", + }), + }, + ], + }; + case NamespaceDefinitionType.destination: + return { + columns: [ + { + id: "sourceNamespace", + displayName: formatMessage({ + id: "connectionForm.modal.destinationNamespace.table.header.sourceNamespace", + }), + }, + { + id: "destinationNamespace", + displayName: formatMessage({ + id: "connectionForm.modal.destinationNamespace.table.header.destinationNamespace", + }), + }, + ], + data: [ + { + sourceNamespace: formatMessage({ + id: "connectionForm.modal.destinationNamespace.table.data.public", + }), + destinationNamespace: formatMessage({ + id: "connectionForm.modal.destinationNamespace.table.data.mySchema", + }), + }, + { + sourceNamespace: "", + destinationNamespace: formatMessage({ + id: "connectionForm.modal.destinationNamespace.table.data.mySchema", + }), + }, + ], + }; + case NamespaceDefinitionType.customformat: + return { + columns: [ + { + id: "customFormat", + displayName: formatMessage({ + id: "connectionForm.modal.destinationNamespace.table.header.customFormat", + }), + }, + { + id: "sourceNamespace", + displayName: formatMessage({ + id: "connectionForm.modal.destinationNamespace.table.header.sourceNamespace", + }), + }, + { + id: "destinationNamespace", + displayName: formatMessage({ + id: "connectionForm.modal.destinationNamespace.table.header.destinationNamespace", + }), + }, + ], + data: [ + { + customFormat: formatMessage( + { + id: "connectionForm.modal.destinationNamespace.table.data.custom", + }, + { symbol: (node: React.ReactNode) => `"${node}"` } + ), + sourceNamespace: formatMessage({ + id: "connectionForm.modal.destinationNamespace.table.data.public", + }), + destinationNamespace: formatMessage( + { + id: "connectionForm.modal.destinationNamespace.table.data.custom", + }, + { symbol: (node: React.ReactNode) => `${node}` } + ), + }, + { + customFormat: formatMessage( + { + id: "connectionForm.modal.destinationNamespace.table.data.exampleSourceNamespace", + }, + { symbol: (node: React.ReactNode) => `{${node}}` } + ), + sourceNamespace: formatMessage({ + id: "connectionForm.modal.destinationNamespace.table.data.public", + }), + destinationNamespace: formatMessage({ + id: "connectionForm.modal.destinationNamespace.table.data.public", + }), + }, + { + customFormat: formatMessage( + { + id: "connectionForm.modal.destinationNamespace.table.data.exampleMySourceNamespace", + }, + { symbol: (node: React.ReactNode) => `{${node}}` } + ), + sourceNamespace: formatMessage({ + id: "connectionForm.modal.destinationNamespace.table.data.public", + }), + destinationNamespace: formatMessage({ + id: "connectionForm.modal.destinationNamespace.table.data.myPublicSchema", + }), + }, + { + customFormat: "", + sourceNamespace: formatMessage({ + id: "connectionForm.modal.destinationNamespace.table.data.public", + }), + destinationNamespace: formatMessage({ + id: "connectionForm.modal.destinationNamespace.table.data.mySchema", + }), + }, + ], + }; + } +}; diff --git a/airbyte-webapp/src/components/connection/DestinationStreamNamesModal/DestinationStreamNamesModal.module.scss b/airbyte-webapp/src/components/connection/DestinationStreamNamesModal/DestinationStreamNamesModal.module.scss new file mode 100644 index 0000000000000..41718b70754fc --- /dev/null +++ b/airbyte-webapp/src/components/connection/DestinationStreamNamesModal/DestinationStreamNamesModal.module.scss @@ -0,0 +1,29 @@ +@use "scss/colors"; +@use "scss/variables"; + +.content { + display: flex; + flex-direction: column; + margin: variables.$spacing-xl; +} + +.description { + color: colors.$grey; + margin-bottom: variables.$spacing-xl; +} + +.radioButton + .radioButton { + margin-top: variables.$spacing-xl; +} + +.input { + position: relative; + margin: variables.$spacing-xl 0 variables.$spacing-xl variables.$spacing-xl; + + .errorMessage { + position: absolute; + bottom: -16px; + left: 0; + color: colors.$red-600; + } +} diff --git a/airbyte-webapp/src/components/connection/DestinationStreamNamesModal/DestinationStreamNamesModal.tsx b/airbyte-webapp/src/components/connection/DestinationStreamNamesModal/DestinationStreamNamesModal.tsx new file mode 100644 index 0000000000000..5887e2c7e8e26 --- /dev/null +++ b/airbyte-webapp/src/components/connection/DestinationStreamNamesModal/DestinationStreamNamesModal.tsx @@ -0,0 +1,141 @@ +import { Field, FieldProps, Form, Formik } from "formik"; +import React from "react"; +import { FormattedMessage, useIntl } from "react-intl"; +import * as yup from "yup"; + +import { LabeledRadioButton } from "components"; +import { Button } from "components/ui/Button"; +import { Input } from "components/ui/Input"; +import { ModalBody, ModalFooter } from "components/ui/Modal"; +import { Text } from "components/ui/Text"; +import { InfoTooltip } from "components/ui/Tooltip"; + +import { FormikConnectionFormValues } from "views/Connection/ConnectionForm/formConfig"; + +import styles from "./DestinationStreamNamesModal.module.scss"; + +export const enum StreamNameDefinitionValueType { + Mirror = "mirror", + Prefix = "prefix", +} + +export interface DestinationStreamNamesFormValueType { + streamNameDefinition: StreamNameDefinitionValueType; + prefix: string; +} + +const destinationStreamNamesValidationSchema = yup.object().shape({ + streamNameDefinition: yup + .string() + .oneOf([StreamNameDefinitionValueType.Mirror, StreamNameDefinitionValueType.Prefix]) + .required("form.empty.error"), + prefix: yup.string().when("streamNameDefinition", { + is: StreamNameDefinitionValueType.Prefix, + then: yup.string().trim().required("form.empty.error"), + }), +}); + +interface DestinationStreamNamesModalProps { + initialValues: Pick; + onCloseModal: () => void; + onSubmit: (value: DestinationStreamNamesFormValueType) => void; +} + +export const DestinationStreamNamesModal: React.FC = ({ + initialValues, + onCloseModal, + onSubmit, +}) => { + const { formatMessage } = useIntl(); + + return ( + { + onCloseModal(); + onSubmit(values); + }} + > + {({ dirty, isValid, errors, values }) => ( +
+ + + + + + {({ field }: FieldProps) => ( + + + + } + value={StreamNameDefinitionValueType.Mirror} + checked={field.value === StreamNameDefinitionValueType.Mirror} + /> + )} + + + {({ field }: FieldProps) => ( + + + + + + + } + value={StreamNameDefinitionValueType.Prefix} + checked={field.value === StreamNameDefinitionValueType.Prefix} + /> + )} + +
+ + {({ field, meta }: FieldProps) => ( + + )} + + {!!errors.prefix && ( + + + + )} +
+
+ + + + +
+ )} +
+ ); +}; diff --git a/airbyte-webapp/src/components/ui/Tooltip/InfoTooltip.tsx b/airbyte-webapp/src/components/ui/Tooltip/InfoTooltip.tsx index 09e8176254d3c..637672dac2cc9 100644 --- a/airbyte-webapp/src/components/ui/Tooltip/InfoTooltip.tsx +++ b/airbyte-webapp/src/components/ui/Tooltip/InfoTooltip.tsx @@ -11,11 +11,11 @@ export const InfoTooltip: React.FC> = -
+ + -
- + + } > {children} diff --git a/airbyte-webapp/src/components/ui/Tooltip/Tooltip.tsx b/airbyte-webapp/src/components/ui/Tooltip/Tooltip.tsx index 662857acb6113..c2ae41260d6cd 100644 --- a/airbyte-webapp/src/components/ui/Tooltip/Tooltip.tsx +++ b/airbyte-webapp/src/components/ui/Tooltip/Tooltip.tsx @@ -66,7 +66,7 @@ export const Tooltip: React.FC> = (props) return ( <> -
> = (props) onMouseOut={onMouseOut} > {control} -
+ {canShowTooltip && createPortal(
SOURCE_NAMESPACE\"", + "connectionForm.modal.destinationNamespace.table.data.exampleMySourceNamespace": "\"my_$SOURCE_NAMESPACE_schema\"", + "connectionForm.modal.destinationNamespace.table.data.custom": "custom", + + "connectionForm.modal.destinationStreamNames.title": "Destination stream names", + "connectionForm.modal.destinationStreamNames.radioButton.mirror": "Mirror source name", + "connectionForm.modal.destinationStreamNames.radioButton.prefix": "Add a prefix", + "connectionForm.modal.destinationStreamNames.input.placeholder": "prefix", + "connectionForm.modal.destinationStreamNames.description": "Define the final table name in the destination", + "connectionForm.modal.destinationStreamNames.prefix.message": "Add a prefix to stream names (ex. “airbyte_” causes “projects” => “airbyte_projects”)", + "connectorForm.authenticate": "Authenticate your {connector} account", "connectorForm.signInWithGoogle": "Sign in with Google", "connectorForm.authenticate.succeeded": "Authentication succeeded!", diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/__snapshots__/ConnectionReplicationTab.test.tsx.snap b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/__snapshots__/ConnectionReplicationTab.test.tsx.snap index a15c78337bce8..0879fe8c96116 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/__snapshots__/ConnectionReplicationTab.test.tsx.snap +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/__snapshots__/ConnectionReplicationTab.test.tsx.snap @@ -388,13 +388,13 @@ exports[`ConnectionReplicationTab should render 1`] = ` class="" > Source -
-
-
-
-
-
+ + +
Sync mode -
-
-
-
-
-
+ + +
Cursor field -
-
-
-
-
-
+ + +
Primary key -
-
-
-
-
-
+ + +
Destination -
-
-
-
-
-
+ + +
= ({ valu clearFormChange(formId); }); + const isNewStreamsTableEnabled = process.env.REACT_APP_NEW_STREAMS_TABLE ?? false; + return ( <> {/* FormChangeTracker is here as it has access to everything it needs without being repeated */} @@ -54,68 +56,70 @@ export const ConnectionFormFields: React.FC = ({ valu
}>
-
- - - - - - - {values.namespaceDefinition === NamespaceDefinitionType.customformat && ( - - {({ field, meta }: FieldProps) => ( + {!isNewStreamsTableEnabled && ( +
+ + + + + + + {values.namespaceDefinition === NamespaceDefinitionType.customformat && ( + + {({ field, meta }: FieldProps) => ( +
+
+ } + message={} + /> +
+
+ +
+
+ )} +
+ )} + + {({ field }: FieldProps) => (
} - message={} + label={formatMessage({ + id: "form.prefix", + })} + message={formatMessage({ + id: "form.prefix.message", + })} />
-
+
)} - )} - - {({ field }: FieldProps) => ( -
-
- -
-
- -
-
- )} -
-
+
+ )}
should renders with mock data without > Google Sheets
-
should renders with mock data without > Alpha
-
+ @@ -141,7 +141,7 @@ exports[` should renders with mock data without > BigQuery -
should renders with mock data without fill-rule="evenodd" /> -
+ @@ -288,7 +288,7 @@ exports[` should renders with mock data without > Postgres -
should renders with mock data without > Alpha
-
+ diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/components/StartWithDestination/__snapshots__/StartWithDestinationCard.test.tsx.snap b/airbyte-webapp/src/views/Connector/ConnectorForm/components/StartWithDestination/__snapshots__/StartWithDestinationCard.test.tsx.snap index 7ed2232c68c71..747127951891a 100644 --- a/airbyte-webapp/src/views/Connector/ConnectorForm/components/StartWithDestination/__snapshots__/StartWithDestinationCard.test.tsx.snap +++ b/airbyte-webapp/src/views/Connector/ConnectorForm/components/StartWithDestination/__snapshots__/StartWithDestinationCard.test.tsx.snap @@ -35,7 +35,7 @@ exports[` should renders without crash with provided > Don’t have your own destination yet? -
should renders without crash with provided > Alpha
-
+