diff --git a/airbyte-webapp/.storybook/withProvider.tsx b/airbyte-webapp/.storybook/withProvider.tsx index fe0b3523ebafc..bbf184f559a93 100644 --- a/airbyte-webapp/.storybook/withProvider.tsx +++ b/airbyte-webapp/.storybook/withProvider.tsx @@ -13,12 +13,9 @@ import { FeatureService } from "../src/hooks/services/Feature"; import { ConfigServiceProvider, defaultConfig } from "../src/config"; import { DocumentationPanelProvider } from "../src/views/Connector/ConnectorDocumentationLayout/DocumentationPanelContext"; import { ServicesProvider } from "../src/core/servicesProvider"; -import { - analyticsServiceContext, - AnalyticsServiceProviderValue, -} from "../src/hooks/services/Analytics"; +import { analyticsServiceContext, AnalyticsServiceProviderValue } from "../src/hooks/services/Analytics"; -const AnalyticsContextMock: AnalyticsServiceProviderValue = ({ +const AnalyticsContextMock: AnalyticsServiceProviderValue = { analyticsContext: {}, setContext: () => {}, addContextProps: () => {}, @@ -26,7 +23,7 @@ const AnalyticsContextMock: AnalyticsServiceProviderValue = ({ service: { track: () => {}, }, -} as unknown) as AnalyticsServiceProviderValue; +} as unknown as AnalyticsServiceProviderValue; const queryClient = new QueryClient({ defaultOptions: { @@ -45,12 +42,9 @@ export const withProviders = (getStory) => ( - + - + {getStory()} diff --git a/airbyte-webapp/package-lock.json b/airbyte-webapp/package-lock.json index 0b5dbbe30d21a..5d305e831ac66 100644 --- a/airbyte-webapp/package-lock.json +++ b/airbyte-webapp/package-lock.json @@ -14,6 +14,7 @@ "@fortawesome/free-solid-svg-icons": "^6.1.1", "@fortawesome/react-fontawesome": "^0.1.18", "@fullstory/browser": "^1.5.1", + "@headlessui/react": "^1.6.5", "@monaco-editor/react": "^4.4.5", "@sentry/react": "^6.19.6", "@sentry/tracing": "^6.19.6", @@ -3548,6 +3549,18 @@ "node": ">=6" } }, + "node_modules/@headlessui/react": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.6.5.tgz", + "integrity": "sha512-3VkKteDxlxf3fE0KbfO9t60KC1lM7YNpZggLpwzVNg1J/zwL+h+4N7MBlFDVpInZI3rKlZGpNx0PWsG/9c2vQg==", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "react-dom": "^16 || ^17 || ^18" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.3.tgz", @@ -49252,6 +49265,12 @@ "yargs": "^16.2.0" } }, + "@headlessui/react": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.6.5.tgz", + "integrity": "sha512-3VkKteDxlxf3fE0KbfO9t60KC1lM7YNpZggLpwzVNg1J/zwL+h+4N7MBlFDVpInZI3rKlZGpNx0PWsG/9c2vQg==", + "requires": {} + }, "@humanwhocodes/config-array": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.3.tgz", diff --git a/airbyte-webapp/package.json b/airbyte-webapp/package.json index 47683c2b2cd9d..da0d0695f6578 100644 --- a/airbyte-webapp/package.json +++ b/airbyte-webapp/package.json @@ -28,6 +28,7 @@ "@fortawesome/free-solid-svg-icons": "^6.1.1", "@fortawesome/react-fontawesome": "^0.1.18", "@fullstory/browser": "^1.5.1", + "@headlessui/react": "^1.6.5", "@monaco-editor/react": "^4.4.5", "@sentry/react": "^6.19.6", "@sentry/tracing": "^6.19.6", diff --git a/airbyte-webapp/src/components/ImageBlock/ImageBlock.module.scss b/airbyte-webapp/src/components/ImageBlock/ImageBlock.module.scss index 9c50ae6fa8fcc..cf1e1e520dee5 100644 --- a/airbyte-webapp/src/components/ImageBlock/ImageBlock.module.scss +++ b/airbyte-webapp/src/components/ImageBlock/ImageBlock.module.scss @@ -21,6 +21,20 @@ &.darkBlue { background: colors.$dark-blue-100; } + &.green { + background: colors.$green; + color: colors.$white; + } + + &.red { + background: colors.$red; + color: colors.$white; + } + + &.blue { + background: colors.$blue; + color: colors.$white; + } } .small { @@ -39,4 +53,8 @@ font-size: 10px; color: colors.$white; padding: 3px 0 3px; + + &.light { + font-weight: 500; + } } diff --git a/airbyte-webapp/src/components/ImageBlock/ImageBlock.tsx b/airbyte-webapp/src/components/ImageBlock/ImageBlock.tsx index 77dec16e4f0e1..a239e42d715c2 100644 --- a/airbyte-webapp/src/components/ImageBlock/ImageBlock.tsx +++ b/airbyte-webapp/src/components/ImageBlock/ImageBlock.tsx @@ -9,18 +9,28 @@ interface ImageBlockProps { img?: string; num?: number; small?: boolean; + color?: string; + light?: boolean; + ariaLabel?: string; } -export const ImageBlock: React.FC = ({ img, num, small }) => { +export const ImageBlock: React.FC = ({ img, num, small, color, light, ariaLabel }) => { const imageCircleClassnames = classnames({ [styles.circle]: num, [styles.iconContainer]: !num || num === undefined, [styles.small]: small && !num, - [styles.darkBlue]: !small && num, + [styles.darkBlue]: !small && num && !color, + [styles.green]: color === "green", + [styles.red]: color === "red", + [styles.blue]: color === "blue", + [styles.light]: light, }); + + const numberStyles = classnames(styles.number, { [styles.light]: light }); + return ( -
- {num ?
{num}
:
{getIcon(img)}
} +
+ {num ?
{num}
:
{getIcon(img)}
}
); }; diff --git a/airbyte-webapp/src/components/Modal/ModalBody.module.scss b/airbyte-webapp/src/components/Modal/ModalBody.module.scss index 1cb9b8f8c5208..37d77d314c32a 100644 --- a/airbyte-webapp/src/components/Modal/ModalBody.module.scss +++ b/airbyte-webapp/src/components/Modal/ModalBody.module.scss @@ -4,4 +4,7 @@ padding: variables.$spacing-lg variables.$spacing-xl; overflow: auto; max-width: 100%; + &.paddingNone { + padding: 0; + } } diff --git a/airbyte-webapp/src/components/Modal/ModalBody.tsx b/airbyte-webapp/src/components/Modal/ModalBody.tsx index a34a09ed5e3fd..759ebc8833b1a 100644 --- a/airbyte-webapp/src/components/Modal/ModalBody.tsx +++ b/airbyte-webapp/src/components/Modal/ModalBody.tsx @@ -1,12 +1,18 @@ +import classnames from "classnames"; + import styles from "./ModalBody.module.scss"; interface ModalBodyProps { maxHeight?: number | string; + padded?: boolean; } -export const ModalBody: React.FC = ({ children, maxHeight }) => { +export const ModalBody: React.FC = ({ children, maxHeight, padded = true }) => { + const modalStyles = classnames(styles.modalBody, { + [styles.paddingNone]: !padded, + }); return ( -
+
{children}
); diff --git a/airbyte-webapp/src/components/icons/ModificationIcon.tsx b/airbyte-webapp/src/components/icons/ModificationIcon.tsx new file mode 100644 index 0000000000000..0804f327b6c33 --- /dev/null +++ b/airbyte-webapp/src/components/icons/ModificationIcon.tsx @@ -0,0 +1,18 @@ +export const ModificationIcon: React.FC = () => { + return ( + + + + + ); +}; diff --git a/airbyte-webapp/src/hooks/services/Modal/ModalService.tsx b/airbyte-webapp/src/hooks/services/Modal/ModalService.tsx index 33f347b35d842..3983576901871 100644 --- a/airbyte-webapp/src/hooks/services/Modal/ModalService.tsx +++ b/airbyte-webapp/src/hooks/services/Modal/ModalService.tsx @@ -43,7 +43,7 @@ export const ModalServiceProvider: React.FC = ({ children }) => { resultSubjectRef.current?.next({ type: "canceled" })} + onClose={modalOptions.preventCancel ? undefined : () => resultSubjectRef.current?.next({ type: "canceled" })} > resultSubjectRef.current?.next({ type: "canceled" })} diff --git a/airbyte-webapp/src/hooks/services/Modal/types.ts b/airbyte-webapp/src/hooks/services/Modal/types.ts index fe2592fc02f51..6ebd200d50e84 100644 --- a/airbyte-webapp/src/hooks/services/Modal/types.ts +++ b/airbyte-webapp/src/hooks/services/Modal/types.ts @@ -5,6 +5,7 @@ import { ModalProps } from "components/Modal/Modal"; export interface ModalOptions { title: ModalProps["title"]; size?: ModalProps["size"]; + preventCancel?: boolean; content: React.ComponentType>; } diff --git a/airbyte-webapp/src/locales/en.json b/airbyte-webapp/src/locales/en.json index 93c417d86ee2f..8ce44683a9cfa 100644 --- a/airbyte-webapp/src/locales/en.json +++ b/airbyte-webapp/src/locales/en.json @@ -330,6 +330,17 @@ "connection.sourceTestAgain": "Test source connection again", "connection.resetData": "Reset your data", "connection.updateSchema": "Refresh source schema", + "connection.updateSchema.completed": "Refreshed source schema", + "connection.updateSchema.confirm": "Confirm", + "connection.updateSchema.new": "{value} new {item}", + "connection.updateSchema.removed": "{value} removed {item}", + "connection.updateSchema.changed": "{value} {item} with changes", + "connection.updateSchema.stream": "{count, plural, one {table} other {tables}}", + "connection.updateSchema.field": "{count, plural, one {field} other {fields}}", + "connection.updateSchema.streamName": "Stream name", + "connection.updateSchema.namespace": "Namespace", + "connection.updateSchema.dataType": "Data type", + "connection.newConnection": "+ New connection", "connection.newConnectionTitle": "New connection", "connection.noConnections": "Connection list is empty", diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx index 81a6ac9605312..0fd5827bb3303 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx @@ -18,6 +18,7 @@ import { ValuesProps, } from "hooks/services/useConnectionHook"; import { equal } from "utils/objects"; +import { CatalogDiffModal } from "views/Connection/CatalogDiffModal/CatalogDiffModal"; import ConnectionForm from "views/Connection/ConnectionForm"; import { ConnectionFormSubmitResult } from "views/Connection/ConnectionForm/ConnectionForm"; import { FormikConnectionFormValues } from "views/Connection/ConnectionForm/formConfig"; @@ -87,7 +88,6 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch const [saved, setSaved] = useState(false); const [connectionFormValues, setConnectionFormValues] = useState(); const connectionService = useConnectionService(); - const { mutateAsync: updateConnection } = useUpdateConnection(); const { connection: initialConnection, refreshConnectionCatalog } = useConnectionLoad(connectionId); @@ -177,7 +177,16 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch const onRefreshSourceSchema = async () => { setSaved(false); setActiveUpdatingSchemaMode(true); - await refreshCatalog(); + const { catalogDiff, syncCatalog } = await refreshCatalog(); + if (catalogDiff?.transforms && catalogDiff.transforms.length > 0) { + await openModal({ + title: formatMessage({ id: "connection.updateSchema.completed" }), + preventCancel: true, + content: ({ onClose }) => ( + + ), + }); + } }; const onCancelConnectionFormEdit = () => { diff --git a/airbyte-webapp/src/scss/_colors.scss b/airbyte-webapp/src/scss/_colors.scss index 4477d2db49089..bd86228b9c8cb 100644 --- a/airbyte-webapp/src/scss/_colors.scss +++ b/airbyte-webapp/src/scss/_colors.scss @@ -60,7 +60,7 @@ $green-800: #007c84; $green-900: #005959; $green: $green-200; -$red-50: #ffbac6; +$red-50: #ffe4e8; $red-100: #ffbac6; $red-200: #ff8da1; $red-300: #ff5e7b; diff --git a/airbyte-webapp/src/views/Connection/CatalogDiffModal/CatalogDiffModal.module.scss b/airbyte-webapp/src/views/Connection/CatalogDiffModal/CatalogDiffModal.module.scss new file mode 100644 index 0000000000000..4a53ac37cae70 --- /dev/null +++ b/airbyte-webapp/src/views/Connection/CatalogDiffModal/CatalogDiffModal.module.scss @@ -0,0 +1,6 @@ +@use "../../../scss/variables"; + +.modalContent { + display: flex; + flex-direction: column; +} diff --git a/airbyte-webapp/src/views/Connection/CatalogDiffModal/CatalogDiffModal.tsx b/airbyte-webapp/src/views/Connection/CatalogDiffModal/CatalogDiffModal.tsx new file mode 100644 index 0000000000000..a616f9173a12b --- /dev/null +++ b/airbyte-webapp/src/views/Connection/CatalogDiffModal/CatalogDiffModal.tsx @@ -0,0 +1,42 @@ +import { useMemo } from "react"; +import { FormattedMessage } from "react-intl"; + +import { Button } from "components"; + +import { AirbyteCatalog, CatalogDiff } from "core/request/AirbyteClient"; + +import { ModalBody, ModalFooter } from "../../../components/Modal"; +import styles from "./CatalogDiffModal.module.scss"; +import { DiffSection } from "./components/DiffSection"; +import { FieldSection } from "./components/FieldSection"; +import { getSortedDiff } from "./utils"; + +interface CatalogDiffModalProps { + catalogDiff: CatalogDiff; + catalog: AirbyteCatalog; + onClose: () => void; +} + +export const CatalogDiffModal: React.FC = ({ catalogDiff, catalog, onClose }) => { + const { newItems, removedItems, changedItems } = useMemo( + () => getSortedDiff(catalogDiff.transforms), + [catalogDiff.transforms] + ); + + return ( + <> + +
+ {removedItems.length > 0 && } + {newItems.length > 0 && } + {changedItems.length > 0 && } +
+
+ + + + + ); +}; diff --git a/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/DiffAccordion.module.scss b/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/DiffAccordion.module.scss new file mode 100644 index 0000000000000..94bdca9fae0f4 --- /dev/null +++ b/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/DiffAccordion.module.scss @@ -0,0 +1,20 @@ +@use "../../../../scss/variables"; +@forward "../components/StreamRow.module.scss"; +@forward "../components/DiffSection.module.scss"; + +.accordionContainer { + width: 100%; +} + +.accordionButton { + width: 100%; + background: none; + border: none; + height: 40px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + padding: variables.$spacing-sm; + font-weight: 400; +} diff --git a/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/DiffAccordion.tsx b/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/DiffAccordion.tsx new file mode 100644 index 0000000000000..d42a051c03b99 --- /dev/null +++ b/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/DiffAccordion.tsx @@ -0,0 +1,45 @@ +import { Disclosure } from "@headlessui/react"; +import { useMemo } from "react"; + +import { StreamTransform } from "core/request/AirbyteClient"; + +import { getSortedDiff } from "../utils"; +import styles from "./DiffAccordion.module.scss"; +import { DiffAccordionHeader } from "./DiffAccordionHeader"; +import { DiffFieldTable } from "./DiffFieldTable"; + +interface DiffAccordionProps { + streamTransform: StreamTransform; +} + +export const DiffAccordion: React.FC = ({ streamTransform }) => { + const { newItems, removedItems, changedItems } = useMemo( + () => getSortedDiff(streamTransform.updateStream), + [streamTransform.updateStream] + ); + + return ( +
+ + {({ open }) => ( + <> + + + + + {removedItems.length > 0 && } + {newItems.length > 0 && } + {changedItems.length > 0 && } + + + )} + +
+ ); +}; diff --git a/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/DiffAccordionHeader.module.scss b/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/DiffAccordionHeader.module.scss new file mode 100644 index 0000000000000..fb72c08342562 --- /dev/null +++ b/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/DiffAccordionHeader.module.scss @@ -0,0 +1,12 @@ +@forward "../components/StreamRow.module.scss"; +@forward "../components/DiffSection.module.scss"; +@use "../../../../scss/variables"; + +.namespace { + padding-left: variables.$spacing-sm; +} + +.headerAdjust { + padding-left: -10px; + margin-left: -5px; +} diff --git a/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/DiffAccordionHeader.tsx b/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/DiffAccordionHeader.tsx new file mode 100644 index 0000000000000..cca15b62d5cc1 --- /dev/null +++ b/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/DiffAccordionHeader.tsx @@ -0,0 +1,49 @@ +import { faAngleDown, faAngleRight } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import classnames from "classnames"; +import { useIntl } from "react-intl"; + +import { ModificationIcon } from "components/icons/ModificationIcon"; + +import { StreamDescriptor } from "core/request/AirbyteClient"; + +import styles from "./DiffAccordionHeader.module.scss"; +import { DiffIconBlock } from "./DiffIconBlock"; + +interface DiffAccordionHeaderProps { + open: boolean; + + streamDescriptor: StreamDescriptor; + removedCount: number; + newCount: number; + changedCount: number; +} +export const DiffAccordionHeader: React.FC = ({ + open, + streamDescriptor, + removedCount, + newCount, + changedCount, +}) => { + // eslint-disable-next-line css-modules/no-undef-class + const nameCellStyle = classnames(styles.nameCell, styles.row); + + // eslint-disable-next-line css-modules/no-undef-class + const namespaceCellStyles = classnames(styles.nameCell, styles.row, styles.namespace); + + const { formatMessage } = useIntl(); + + return ( + <> + +
+ {open ? : } +
{streamDescriptor.namespace}
+
+
+
{streamDescriptor.name}
+
+ + + ); +}; diff --git a/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/DiffFieldTable.module.scss b/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/DiffFieldTable.module.scss new file mode 100644 index 0000000000000..34d2e94db1adc --- /dev/null +++ b/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/DiffFieldTable.module.scss @@ -0,0 +1,33 @@ +@use "../../../../scss/variables"; +@use "./DiffSection.module.scss"; + +.accordionSubHeader { + @extend .sectionSubHeader; + + & .padLeft { + padding-left: variables.$spacing-lg; + } + + & th { + font-size: 10px; + width: 228px; + padding-left: variables.$spacing-md; + } +} + +.table { + width: 100%; + padding-left: variables.$spacing-xl; + + & tbody > tr:first-of-type > td:nth-of-type(2) { + border-radius: variables.$border-radius-sm variables.$border-radius-sm 0px 0px; + } + + & tbody > tr:last-of-type > td:nth-of-type(2) { + border-radius: 0px 0px variables.$border-radius-sm variables.$border-radius-sm; + } + + & tbody > tr:only-of-type > td:nth-of-type(2) { + border-radius: variables.$border-radius-sm; + } +} diff --git a/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/DiffFieldTable.tsx b/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/DiffFieldTable.tsx new file mode 100644 index 0000000000000..e0cb49a1f5777 --- /dev/null +++ b/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/DiffFieldTable.tsx @@ -0,0 +1,37 @@ +import { FormattedMessage } from "react-intl"; + +import { FieldTransform } from "core/request/AirbyteClient"; + +import { DiffVerb } from "../types"; +import styles from "./DiffFieldTable.module.scss"; +import { DiffHeader } from "./DiffHeader"; +import { FieldRow } from "./FieldRow"; + +interface DiffFieldTableProps { + fieldTransforms: FieldTransform[]; + diffVerb: DiffVerb; +} + +export const DiffFieldTable: React.FC = ({ fieldTransforms, diffVerb }) => { + return ( + + + + + {diffVerb === "changed" && ( + + )} + + + + {fieldTransforms.map((transform) => { + return ; + })} + +
+ + + +
+ ); +}; diff --git a/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/DiffHeader.tsx b/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/DiffHeader.tsx new file mode 100644 index 0000000000000..6b5f543d8ff83 --- /dev/null +++ b/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/DiffHeader.tsx @@ -0,0 +1,25 @@ +import { FormattedMessage } from "react-intl"; + +import { DiffVerb } from "../types"; + +export type DiffType = "field" | "stream"; + +interface DiffHeaderProps { + diffCount: number; + diffVerb: DiffVerb; + diffType: DiffType; +} + +export const DiffHeader: React.FC = ({ diffCount, diffVerb, diffType }) => { + return ( +
+ , + }} + /> +
+ ); +}; diff --git a/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/DiffIconBlock.module.scss b/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/DiffIconBlock.module.scss new file mode 100644 index 0000000000000..7fb834f093b4f --- /dev/null +++ b/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/DiffIconBlock.module.scss @@ -0,0 +1,6 @@ +.iconBlock { + display: flex; + flex-direction: row; + gap: 1px; + margin-left: auto; +} diff --git a/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/DiffIconBlock.tsx b/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/DiffIconBlock.tsx new file mode 100644 index 0000000000000..453f23443dcff --- /dev/null +++ b/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/DiffIconBlock.tsx @@ -0,0 +1,67 @@ +import { useIntl } from "react-intl"; + +import { ImageBlock } from "components"; + +import styles from "./DiffIconBlock.module.scss"; + +interface DiffIconBlockProps { + newCount: number; + removedCount: number; + changedCount: number; +} +export const DiffIconBlock: React.FC = ({ newCount, removedCount, changedCount }) => { + const { formatMessage } = useIntl(); + + return ( +
+ {removedCount > 0 && ( + + )} + {newCount > 0 && ( + + )} + {changedCount > 0 && ( + + )} +
+ ); +}; diff --git a/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/DiffSection.module.scss b/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/DiffSection.module.scss new file mode 100644 index 0000000000000..4dcd0e8d91354 --- /dev/null +++ b/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/DiffSection.module.scss @@ -0,0 +1,37 @@ +@use "../../../../scss/colors"; +@use "../../../../scss/variables"; +@use "../components/StreamRow.module.scss"; + +.sectionContainer { + display: flex; + flex-direction: column; +} + +.sectionSubHeader { + display: flex; + flex-direction: row; + align-items: center; + padding: 0px variables.$spacing-md 0px variables.$spacing-2xl; + width: 100%; + height: 22px; + + & th { + @extend .nameCell; + text-align: left; + font-weight: 400; + font-size: 10px; + color: colors.$grey; + line-height: 12px; + } + + .padLeft { + padding-left: variables.$spacing-md; + } +} + +.sectionHeader { + font-size: 14px; + font-weight: 500; + line-height: 17px; + padding: variables.$spacing-lg variables.$spacing-xl; +} diff --git a/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/DiffSection.tsx b/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/DiffSection.tsx new file mode 100644 index 0000000000000..47981779d2d9b --- /dev/null +++ b/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/DiffSection.tsx @@ -0,0 +1,61 @@ +import { FormattedMessage } from "react-intl"; + +import { AirbyteCatalog, StreamDescriptor, StreamTransform } from "core/request/AirbyteClient"; + +import { DiffVerb } from "../types"; +import { DiffHeader } from "./DiffHeader"; +import styles from "./DiffSection.module.scss"; +import { StreamRow } from "./StreamRow"; + +interface DiffSectionProps { + streams: StreamTransform[]; + catalog?: AirbyteCatalog; + diffVerb: DiffVerb; +} + +const calculateSyncModeString = (catalog: AirbyteCatalog, streamDescriptor: StreamDescriptor) => { + const streamConfig = catalog.streams.find( + (catalogStream) => + catalogStream.stream?.namespace === streamDescriptor.namespace && + catalogStream.stream?.name === streamDescriptor.name + )?.config; + + if (streamConfig?.syncMode && streamConfig.destinationSyncMode) { + return `${streamConfig?.syncMode} | ${streamConfig?.destinationSyncMode}`; + } + return ""; +}; +export const DiffSection: React.FC = ({ streams, catalog, diffVerb }) => { + return ( +
+
+ +
+ + + + + + + + + {streams.map((stream) => { + return ( + + ); + })} + +
+ + + + +
+
+ ); +}; diff --git a/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/FieldRow.module.scss b/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/FieldRow.module.scss new file mode 100644 index 0000000000000..06ccf3e8ade85 --- /dev/null +++ b/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/FieldRow.module.scss @@ -0,0 +1,60 @@ +@use "../../../../scss/variables"; +@use "../../../../scss/colors"; + +.row { + display: flex; + flex-direction: row; + align-items: center; + padding: variables.$spacing-sm variables.$spacing-xl; + gap: variables.$spacing-md; + height: 35px; + font-size: 12px; +} + +.content { + padding-left: 10px; + display: flex; + width: 100%; + height: 35px; + border-bottom: 1px solid colors.$white; + align-items: center; + + &.add { + background-color: colors.$green-50; + } + &.remove { + background-color: colors.$red-50; + } +} + +.icon { + &.plus { + color: colors.$green; + } + &.minus { + color: colors.$red; + } + &.mod { + color: colors.$blue-100; + } +} + +.iconCell { + background: white; + width: 10px; + height: 100%; + display: flex; + align-items: center; +} + +.cell { + width: 228px; + &.update { + border-radius: variables.$border-radius-sm; + & span { + background-color: rgba(98, 94, 255, 0.1); + padding: variables.$spacing-sm; + border-radius: variables.$border-radius-sm; + } + } +} diff --git a/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/FieldRow.tsx b/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/FieldRow.tsx new file mode 100644 index 0000000000000..1c77e9a7281af --- /dev/null +++ b/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/FieldRow.tsx @@ -0,0 +1,66 @@ +import { faArrowRight, faMinus, faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import classnames from "classnames"; + +import { FieldTransform } from "core/request/AirbyteClient"; + +import { ModificationIcon } from "../../../../components/icons/ModificationIcon"; +import styles from "./FieldRow.module.scss"; + +interface FieldRowProps { + transform: FieldTransform; +} + +export const FieldRow: React.FC = ({ transform }) => { + const fieldName = transform.fieldName[transform.fieldName.length - 1]; + const diffType = transform.transformType.includes("add") + ? "add" + : transform.transformType.includes("remove") + ? "remove" + : "update"; + + const oldType = transform.updateFieldSchema?.oldSchema.type; + const newType = transform.updateFieldSchema?.newSchema.type; + + const iconStyle = classnames(styles.icon, { + [styles.plus]: diffType === "add", + [styles.minus]: diffType === "remove", + [styles.mod]: diffType === "update", + }); + + const contentStyle = classnames(styles.content, { + [styles.add]: diffType === "add", + [styles.remove]: diffType === "remove", + [styles.update]: diffType === "update", + }); + + const updateCellStyle = classnames(styles.cell, styles.update); + + return ( + + + {diffType === "add" ? ( + + ) : diffType === "remove" ? ( + + ) : ( + + + + )} + + + + {fieldName} + + + {oldType && newType && ( + + {oldType} {newType} + + )} + + + + ); +}; diff --git a/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/FieldSection.module.scss b/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/FieldSection.module.scss new file mode 100644 index 0000000000000..1c608338fea68 --- /dev/null +++ b/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/FieldSection.module.scss @@ -0,0 +1,41 @@ +@use "../../../../scss/variables"; +@use "./DiffSection.module.scss"; +@use "../../../../scss/colors"; + +ul, +li { + list-style-type: none; + list-style-position: inside; + margin: 0px; + padding: 0px; + height: auto; + font-weight: 400; +} + +.fieldHeader { + @extend .sectionHeader; + padding: variables.$spacing-lg variables.$spacing-xl; +} + +.fieldSubHeader { + @extend .sectionSubHeader; + padding: 0px variables.$spacing-sm 0px 35px; + + .padLeft { + padding-left: variables.$spacing-xl; + } + + > div { + @extend .nameCell; + text-align: left; + font-weight: 400; + font-size: 10px; + color: colors.$grey; + line-height: 12px; + padding-left: variables.$spacing-md; + } +} + +.fieldRowsContainer { + padding-left: variables.$spacing-lg; +} diff --git a/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/FieldSection.tsx b/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/FieldSection.tsx new file mode 100644 index 0000000000000..4c03fcf8a4e7b --- /dev/null +++ b/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/FieldSection.tsx @@ -0,0 +1,48 @@ +import { FormattedMessage, useIntl } from "react-intl"; + +import { StreamTransform } from "core/request/AirbyteClient"; + +import { DiffVerb } from "../types"; +import { DiffAccordion } from "./DiffAccordion"; +import { DiffHeader } from "./DiffHeader"; +import styles from "./FieldSection.module.scss"; + +interface FieldSectionProps { + streams: StreamTransform[]; + diffVerb: DiffVerb; +} + +export const FieldSection: React.FC = ({ streams, diffVerb }) => { + const { formatMessage } = useIntl(); + return ( + // eslint-disable-next-line css-modules/no-undef-class +
+ {/* eslint-disable-next-line css-modules/no-undef-class */} +
+ +
+
+
+ +
+
+ +
+
+
+
+ {streams.length > 0 && ( +
    + {streams.map((stream) => { + return ( +
  • + +
  • + ); + })} +
+ )} +
+
+ ); +}; diff --git a/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/StreamRow.module.scss b/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/StreamRow.module.scss new file mode 100644 index 0000000000000..dfa49bf595e12 --- /dev/null +++ b/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/StreamRow.module.scss @@ -0,0 +1,53 @@ +@use "../../../../scss/colors"; +@use "../../../../scss/variables"; + +.row { + display: flex; + flex-direction: row; + height: 40px; + align-items: center; + padding: variables.$spacing-sm variables.$spacing-xl; + gap: variables.$spacing-md; + border-bottom: 1px solid colors.$white; + width: 100%; + font-size: 12px; + + &.add { + background-color: colors.$green-50; + } + &.remove { + background-color: colors.$red-50; + } +} + +.icon { + margin-top: -1px; + &.plus { + color: colors.$green; + } + &.minus { + color: colors.$red; + } + &.mod { + color: colors.$blue-100; + } +} + +.syncModeBox { + font-size: 11px; + line-height: 12px; + border-radius: variables.$border-radius-sm; + padding: variables.$spacing-sm variables.$spacing-md; + width: 226px; + opacity: 50%; + background: colors.$red-100; +} + +.nameCell { + width: 140px; + text-align: left; + & .row { + display: flex; + flex-direction: row; + } +} diff --git a/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/StreamRow.tsx b/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/StreamRow.tsx new file mode 100644 index 0000000000000..2931ed5ed644b --- /dev/null +++ b/airbyte-webapp/src/views/Connection/CatalogDiffModal/components/StreamRow.tsx @@ -0,0 +1,52 @@ +import { faMinus, faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import classnames from "classnames"; + +import { StreamTransform } from "core/request/AirbyteClient"; + +import { ModificationIcon } from "../../../../components/icons/ModificationIcon"; +import { DiffVerb } from "../types"; +import styles from "./StreamRow.module.scss"; + +interface StreamRowProps { + streamTransform: StreamTransform; + syncMode?: string; + + diffVerb: DiffVerb; +} + +export const SyncModeBox: React.FC<{ syncModeString: string }> = ({ syncModeString }) => { + return
{syncModeString}
; +}; + +export const StreamRow: React.FC = ({ streamTransform, syncMode, diffVerb }) => { + const rowStyle = classnames(styles.row, { + [styles.add]: diffVerb === "new", + [styles.remove]: diffVerb === "removed", + }); + + const iconStyle = classnames(styles.icon, { + [styles.plus]: diffVerb === "new", + [styles.minus]: diffVerb === "removed", + [styles.mod]: diffVerb === "changed", + }); + + const itemName = streamTransform.streamDescriptor.name; + const namespace = streamTransform.streamDescriptor.namespace; + return ( + + + {diffVerb === "new" ? ( + + ) : diffVerb === "removed" ? ( + + ) : ( + + )} + + {namespace} + {itemName}{" "} + {diffVerb === "removed" && syncMode && } + + ); +}; diff --git a/airbyte-webapp/src/views/Connection/CatalogDiffModal/types.tsx b/airbyte-webapp/src/views/Connection/CatalogDiffModal/types.tsx new file mode 100644 index 0000000000000..6c1d38aace4cf --- /dev/null +++ b/airbyte-webapp/src/views/Connection/CatalogDiffModal/types.tsx @@ -0,0 +1,9 @@ +import { FieldTransform, StreamTransform } from "core/request/AirbyteClient"; + +export type DiffVerb = "new" | "removed" | "changed"; + +export interface SortedDiff { + newItems: T[]; + removedItems: T[]; + changedItems: T[]; +} diff --git a/airbyte-webapp/src/views/Connection/CatalogDiffModal/utils.tsx b/airbyte-webapp/src/views/Connection/CatalogDiffModal/utils.tsx new file mode 100644 index 0000000000000..f3ec706aaea0a --- /dev/null +++ b/airbyte-webapp/src/views/Connection/CatalogDiffModal/utils.tsx @@ -0,0 +1,21 @@ +import { FieldTransform, StreamTransform } from "core/request/AirbyteClient"; + +import { SortedDiff } from "./types"; + +export const getSortedDiff = (diffArray?: T[]): SortedDiff => { + const sortedDiff: SortedDiff = { newItems: [], removedItems: [], changedItems: [] }; + diffArray?.forEach((transform) => { + if (transform.transformType.includes("add")) { + sortedDiff.newItems.push(transform); + } + + if (transform.transformType.includes("remove")) { + sortedDiff.removedItems.push(transform); + } + + if (transform.transformType.includes("update")) { + sortedDiff.changedItems.push(transform); + } + }); + return sortedDiff; +};