1
0
mirror of synced 2026-01-06 06:04:16 -05:00

🪟 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:
Teal Larson
2022-08-03 16:56:03 -04:00
committed by GitHub
parent d3d60c1067
commit a4ccaee837
34 changed files with 901 additions and 21 deletions

View File

@@ -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>

View File

@@ -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",

View File

@@ -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",

View File

@@ -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;
}
}

View File

@@ -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>
);
};

View File

@@ -4,4 +4,7 @@
padding: variables.$spacing-lg variables.$spacing-xl;
overflow: auto;
max-width: 100%;
&.paddingNone {
padding: 0;
}
}

View File

@@ -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>
);

View 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>
);
};

View File

@@ -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" })}

View File

@@ -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>>;
}

View File

@@ -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",

View File

@@ -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 = () => {

View File

@@ -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;

View File

@@ -0,0 +1,6 @@
@use "../../../scss/variables";
.modalContent {
display: flex;
flex-direction: column;
}

View File

@@ -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>
</>
);
};

View File

@@ -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;
}

View File

@@ -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>
);
};

View File

@@ -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;
}

View File

@@ -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} />
</>
);
};

View File

@@ -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;
}
}

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -0,0 +1,6 @@
.iconBlock {
display: flex;
flex-direction: row;
gap: 1px;
margin-left: auto;
}

View File

@@ -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>
);
};

View File

@@ -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;
}

View File

@@ -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>
);
};

View File

@@ -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;
}
}
}

View File

@@ -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>
);
};

View File

@@ -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;
}

View File

@@ -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>
);
};

View File

@@ -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;
}
}

View File

@@ -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>
);
};

View File

@@ -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[];
}

View File

@@ -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;
};