🪟 Add catalog changes modal on schema refresh (#14074)
* WIP - types, props, components * logic tweaks * moving around * begin styling and content * modal formatting, section header * client update, add/removed streams works * theme tweaks * WIP -- adding accordion * hook for modal display logic * display logic, row/accordion progress * fix atrocities of table rendering, move header to own component * headers cleanup * headers cleanup * imageblock more flexible * progress on table todo: consolidate, complete * styling good, animation TODO * self review pt. 1 * cleanup * note * note * accessibility and i18n improvements * fix typo in scss * missig i18l things * move icon to /icons * Update airbyte-webapp/src/views/Connection/CatalogDiffModal/CatalogDiffModal.tsx Co-authored-by: Edmundo Ruiz Ghanem <168664+edmundito@users.noreply.github.com> * Update airbyte-webapp/src/views/Connection/CatalogDiffModal/components/DiffAccordion.tsx Co-authored-by: Edmundo Ruiz Ghanem <168664+edmundito@users.noreply.github.com> * spacing, use ModalFooter * Update airbyte-webapp/src/views/Connection/CatalogDiffModal/components/StreamRow.tsx Co-authored-by: Edmundo Ruiz Ghanem <168664+edmundito@users.noreply.github.com> * begin moving to memoized reducer function * memoize diff sorter and remove extra divs * cleanup * modal body padding * up0date to use modal service * move calculated string mode out of component * respond to review * add accordionheader component * catalog can be undefined * cleanup cell rendering * cleanup and make storybook work again * move table styles within a parent class * subheading alignment consistency * more padding/spacing adjustments * cleanup from review * mixup from rebase * set width on modal level not content level * Update airbyte-webapp/src/views/Connection/CatalogDiffModal/utils.tsx Co-authored-by: Edmundo Ruiz Ghanem <168664+edmundito@users.noreply.github.com> * Update airbyte-webapp/src/views/Connection/CatalogDiffModal/utils.tsx Co-authored-by: Edmundo Ruiz Ghanem <168664+edmundito@users.noreply.github.com> * linting and unused class cleanup Co-authored-by: Edmundo Ruiz Ghanem <168664+edmundito@users.noreply.github.com>
This commit is contained in:
@@ -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) => (
|
||||
<MemoryRouter>
|
||||
<IntlProvider messages={messages} locale={"en"}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<ConfigServiceProvider
|
||||
defaultConfig={defaultConfig}
|
||||
providers={[]}
|
||||
>
|
||||
<ConfigServiceProvider defaultConfig={defaultConfig} providers={[]}>
|
||||
<DocumentationPanelProvider>
|
||||
<FeatureService>
|
||||
<FeatureService features={[]}>
|
||||
<GlobalStyle />
|
||||
{getStory()}
|
||||
</FeatureService>
|
||||
|
||||
19
airbyte-webapp/package-lock.json
generated
19
airbyte-webapp/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,18 +9,28 @@ interface ImageBlockProps {
|
||||
img?: string;
|
||||
num?: number;
|
||||
small?: boolean;
|
||||
color?: string;
|
||||
light?: boolean;
|
||||
ariaLabel?: string;
|
||||
}
|
||||
|
||||
export const ImageBlock: React.FC<ImageBlockProps> = ({ img, num, small }) => {
|
||||
export const ImageBlock: React.FC<ImageBlockProps> = ({ 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 (
|
||||
<div className={imageCircleClassnames}>
|
||||
{num ? <div className={styles.number}>{num}</div> : <div className={styles.icon}>{getIcon(img)}</div>}
|
||||
<div className={imageCircleClassnames} aria-label={ariaLabel}>
|
||||
{num ? <div className={numberStyles}>{num}</div> : <div className={styles.icon}>{getIcon(img)}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,4 +4,7 @@
|
||||
padding: variables.$spacing-lg variables.$spacing-xl;
|
||||
overflow: auto;
|
||||
max-width: 100%;
|
||||
&.paddingNone {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ModalBodyProps> = ({ children, maxHeight }) => {
|
||||
export const ModalBody: React.FC<ModalBodyProps> = ({ children, maxHeight, padded = true }) => {
|
||||
const modalStyles = classnames(styles.modalBody, {
|
||||
[styles.paddingNone]: !padded,
|
||||
});
|
||||
return (
|
||||
<div className={styles.modalBody} style={{ maxHeight }}>
|
||||
<div className={modalStyles} style={{ maxHeight }}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
18
airbyte-webapp/src/components/icons/ModificationIcon.tsx
Normal file
18
airbyte-webapp/src/components/icons/ModificationIcon.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
export const ModificationIcon: React.FC = () => {
|
||||
return (
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10.8332 9.16663L14.1665 6.66663L10.8332 4.16663V5.83329H4.1665V7.49996H10.8332V9.16663Z"
|
||||
fill="#CBC8FF"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9.16683 15.8334L5.8335 13.3334L9.16683 10.8334V12.5H15.8335V14.1667H9.16683V15.8334Z"
|
||||
fill="#CBC8FF"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -43,7 +43,7 @@ export const ModalServiceProvider: React.FC = ({ children }) => {
|
||||
<Modal
|
||||
title={modalOptions.title}
|
||||
size={modalOptions.size}
|
||||
onClose={() => resultSubjectRef.current?.next({ type: "canceled" })}
|
||||
onClose={modalOptions.preventCancel ? undefined : () => resultSubjectRef.current?.next({ type: "canceled" })}
|
||||
>
|
||||
<modalOptions.content
|
||||
onCancel={() => resultSubjectRef.current?.next({ type: "canceled" })}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ModalProps } from "components/Modal/Modal";
|
||||
export interface ModalOptions<T> {
|
||||
title: ModalProps["title"];
|
||||
size?: ModalProps["size"];
|
||||
preventCancel?: boolean;
|
||||
content: React.ComponentType<ModalContentProps<T>>;
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<ReplicationViewProps> = ({ onAfterSaveSch
|
||||
const [saved, setSaved] = useState(false);
|
||||
const [connectionFormValues, setConnectionFormValues] = useState<FormikConnectionFormValues>();
|
||||
const connectionService = useConnectionService();
|
||||
|
||||
const { mutateAsync: updateConnection } = useUpdateConnection();
|
||||
|
||||
const { connection: initialConnection, refreshConnectionCatalog } = useConnectionLoad(connectionId);
|
||||
@@ -177,7 +177,16 @@ export const ReplicationView: React.FC<ReplicationViewProps> = ({ onAfterSaveSch
|
||||
const onRefreshSourceSchema = async () => {
|
||||
setSaved(false);
|
||||
setActiveUpdatingSchemaMode(true);
|
||||
await refreshCatalog();
|
||||
const { catalogDiff, syncCatalog } = await refreshCatalog();
|
||||
if (catalogDiff?.transforms && catalogDiff.transforms.length > 0) {
|
||||
await openModal<void>({
|
||||
title: formatMessage({ id: "connection.updateSchema.completed" }),
|
||||
preventCancel: true,
|
||||
content: ({ onClose }) => (
|
||||
<CatalogDiffModal catalogDiff={catalogDiff} catalog={syncCatalog} onClose={onClose} />
|
||||
),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onCancelConnectionFormEdit = () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
@use "../../../scss/variables";
|
||||
|
||||
.modalContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -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<CatalogDiffModalProps> = ({ catalogDiff, catalog, onClose }) => {
|
||||
const { newItems, removedItems, changedItems } = useMemo(
|
||||
() => getSortedDiff(catalogDiff.transforms),
|
||||
[catalogDiff.transforms]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalBody maxHeight={400} padded={false}>
|
||||
<div className={styles.modalContent}>
|
||||
{removedItems.length > 0 && <DiffSection streams={removedItems} diffVerb="removed" catalog={catalog} />}
|
||||
{newItems.length > 0 && <DiffSection streams={newItems} diffVerb="new" />}
|
||||
{changedItems.length > 0 && <FieldSection streams={changedItems} diffVerb="changed" />}
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button onClick={() => onClose()}>
|
||||
<FormattedMessage id="connection.updateSchema.confirm" />
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<DiffAccordionProps> = ({ streamTransform }) => {
|
||||
const { newItems, removedItems, changedItems } = useMemo(
|
||||
() => getSortedDiff(streamTransform.updateStream),
|
||||
[streamTransform.updateStream]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.accordionContainer}>
|
||||
<Disclosure>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Disclosure.Button className={styles.accordionButton}>
|
||||
<DiffAccordionHeader
|
||||
streamDescriptor={streamTransform.streamDescriptor}
|
||||
removedCount={removedItems.length}
|
||||
newCount={newItems.length}
|
||||
changedCount={changedItems.length}
|
||||
open={open}
|
||||
/>
|
||||
</Disclosure.Button>
|
||||
<Disclosure.Panel>
|
||||
{removedItems.length > 0 && <DiffFieldTable fieldTransforms={removedItems} diffVerb="removed" />}
|
||||
{newItems.length > 0 && <DiffFieldTable fieldTransforms={newItems} diffVerb="new" />}
|
||||
{changedItems.length > 0 && <DiffFieldTable fieldTransforms={changedItems} diffVerb="changed" />}
|
||||
</Disclosure.Panel>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<DiffAccordionHeaderProps> = ({
|
||||
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 (
|
||||
<>
|
||||
<ModificationIcon />
|
||||
<div className={namespaceCellStyles} aria-labelledby={formatMessage({ id: "connection.updateSchema.namespace" })}>
|
||||
{open ? <FontAwesomeIcon icon={faAngleDown} /> : <FontAwesomeIcon icon={faAngleRight} />}
|
||||
<div className={styles.headerAdjust}>{streamDescriptor.namespace}</div>
|
||||
</div>
|
||||
<div className={nameCellStyle} aria-labelledby={formatMessage({ id: "connection.updateSchema.streamName" })}>
|
||||
<div className={styles.headerAdjust}>{streamDescriptor.name}</div>
|
||||
</div>
|
||||
<DiffIconBlock removedCount={removedCount} newCount={newCount} changedCount={changedCount} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<DiffFieldTableProps> = ({ fieldTransforms, diffVerb }) => {
|
||||
return (
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr className={styles.accordionSubHeader}>
|
||||
<th>
|
||||
<DiffHeader diffCount={fieldTransforms.length} diffVerb={diffVerb} diffType="field" />
|
||||
</th>
|
||||
{diffVerb === "changed" && (
|
||||
<th className={styles.padLeft}>
|
||||
<FormattedMessage id="connection.updateSchema.dataType" />
|
||||
</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{fieldTransforms.map((transform) => {
|
||||
return <FieldRow transform={transform} key={`${transform.fieldName}.${transform.transformType}`} />;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
@@ -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<DiffHeaderProps> = ({ diffCount, diffVerb, diffType }) => {
|
||||
return (
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id={`connection.updateSchema.${diffVerb}`}
|
||||
values={{
|
||||
value: diffCount,
|
||||
item: <FormattedMessage id={`connection.updateSchema.${diffType}`} values={{ count: diffCount }} />,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
.iconBlock {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1px;
|
||||
margin-left: auto;
|
||||
}
|
||||
@@ -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<DiffIconBlockProps> = ({ newCount, removedCount, changedCount }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<div className={styles.iconBlock}>
|
||||
{removedCount > 0 && (
|
||||
<ImageBlock
|
||||
num={removedCount}
|
||||
color="red"
|
||||
light
|
||||
ariaLabel={`${removedCount} ${formatMessage(
|
||||
{
|
||||
id: "connection.updateSchema.removed",
|
||||
},
|
||||
{
|
||||
value: removedCount,
|
||||
item: formatMessage({ id: "field" }, { values: { count: removedCount } }),
|
||||
}
|
||||
)}`}
|
||||
/>
|
||||
)}
|
||||
{newCount > 0 && (
|
||||
<ImageBlock
|
||||
num={newCount}
|
||||
color="green"
|
||||
light
|
||||
ariaLabel={`${newCount} ${formatMessage(
|
||||
{
|
||||
id: "connection.updateSchema.new",
|
||||
},
|
||||
{
|
||||
value: newCount,
|
||||
item: formatMessage({ id: "field" }, { values: { count: newCount } }),
|
||||
}
|
||||
)}`}
|
||||
/>
|
||||
)}
|
||||
{changedCount > 0 && (
|
||||
<ImageBlock
|
||||
num={changedCount}
|
||||
color="blue"
|
||||
light
|
||||
ariaLabel={`${changedCount} ${formatMessage(
|
||||
{
|
||||
id: "connection.updateSchema.changed",
|
||||
},
|
||||
{
|
||||
value: changedCount,
|
||||
item: formatMessage({ id: "field" }, { values: { count: changedCount } }),
|
||||
}
|
||||
)}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<DiffSectionProps> = ({ streams, catalog, diffVerb }) => {
|
||||
return (
|
||||
<div className={styles.sectionContainer}>
|
||||
<div className={styles.sectionHeader}>
|
||||
<DiffHeader diffCount={streams.length} diffVerb={diffVerb} diffType="stream" />
|
||||
</div>
|
||||
<table>
|
||||
<thead className={styles.sectionSubHeader}>
|
||||
<tr>
|
||||
<th>
|
||||
<FormattedMessage id="connection.updateSchema.namespace" />
|
||||
</th>
|
||||
<th className={styles.padLeft}>
|
||||
<FormattedMessage id="connection.updateSchema.streamName" />
|
||||
</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{streams.map((stream) => {
|
||||
return (
|
||||
<StreamRow
|
||||
streamTransform={stream}
|
||||
syncMode={!catalog ? undefined : calculateSyncModeString(catalog, stream.streamDescriptor)}
|
||||
diffVerb={diffVerb}
|
||||
key={`${stream.streamDescriptor.namespace}.${stream.streamDescriptor.name}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<FieldRowProps> = ({ 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 (
|
||||
<tr className={styles.row}>
|
||||
<td className={styles.iconCell}>
|
||||
{diffType === "add" ? (
|
||||
<FontAwesomeIcon icon={faPlus} size="1x" className={iconStyle} />
|
||||
) : diffType === "remove" ? (
|
||||
<FontAwesomeIcon icon={faMinus} size="1x" className={iconStyle} />
|
||||
) : (
|
||||
<span className="mod">
|
||||
<ModificationIcon />
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className={contentStyle}>
|
||||
<td className={styles.cell}>
|
||||
<span>{fieldName}</span>
|
||||
</td>
|
||||
<td className={updateCellStyle}>
|
||||
{oldType && newType && (
|
||||
<span>
|
||||
{oldType} <FontAwesomeIcon icon={faArrowRight} /> {newType}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<FieldSectionProps> = ({ streams, diffVerb }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
// eslint-disable-next-line css-modules/no-undef-class
|
||||
<div className={styles.sectionContainer}>
|
||||
{/* eslint-disable-next-line css-modules/no-undef-class */}
|
||||
<div className={styles.fieldHeader}>
|
||||
<DiffHeader diffCount={streams.length} diffVerb={diffVerb} diffType="stream" />
|
||||
</div>
|
||||
<div className={styles.fieldSubHeader}>
|
||||
<div id={formatMessage({ id: "connection.updateSchema.namespace" })}>
|
||||
<FormattedMessage id="connection.updateSchema.namespace" />
|
||||
</div>
|
||||
<div className={styles.padLeft} id={formatMessage({ id: "connection.updateSchema.streamName" })}>
|
||||
<FormattedMessage id="connection.updateSchema.streamName" />
|
||||
</div>
|
||||
<div />
|
||||
</div>
|
||||
<div className={styles.fieldRowsContainer}>
|
||||
{streams.length > 0 && (
|
||||
<ul>
|
||||
{streams.map((stream) => {
|
||||
return (
|
||||
<li key={`${stream.streamDescriptor.namespace}.${stream.streamDescriptor.name}`}>
|
||||
<DiffAccordion streamTransform={stream} />
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 <div className={styles.syncModeBox}> {syncModeString} </div>;
|
||||
};
|
||||
|
||||
export const StreamRow: React.FC<StreamRowProps> = ({ 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 (
|
||||
<tr className={rowStyle}>
|
||||
<td>
|
||||
{diffVerb === "new" ? (
|
||||
<FontAwesomeIcon icon={faPlus} size="1x" className={iconStyle} />
|
||||
) : diffVerb === "removed" ? (
|
||||
<FontAwesomeIcon icon={faMinus} size="1x" className={iconStyle} />
|
||||
) : (
|
||||
<ModificationIcon />
|
||||
)}
|
||||
</td>
|
||||
<td className={styles.nameCell}>{namespace}</td>
|
||||
<td className={styles.nameCell}>{itemName}</td>{" "}
|
||||
<td>{diffVerb === "removed" && syncMode && <SyncModeBox syncModeString={syncMode} />} </td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import { FieldTransform, StreamTransform } from "core/request/AirbyteClient";
|
||||
|
||||
export type DiffVerb = "new" | "removed" | "changed";
|
||||
|
||||
export interface SortedDiff<T extends StreamTransform | FieldTransform> {
|
||||
newItems: T[];
|
||||
removedItems: T[];
|
||||
changedItems: T[];
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { FieldTransform, StreamTransform } from "core/request/AirbyteClient";
|
||||
|
||||
import { SortedDiff } from "./types";
|
||||
|
||||
export const getSortedDiff = <T extends StreamTransform | FieldTransform>(diffArray?: T[]): SortedDiff<T> => {
|
||||
const sortedDiff: SortedDiff<T> = { 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;
|
||||
};
|
||||
Reference in New Issue
Block a user