Skip to content

Commit

Permalink
Add new streams table components (#18262)
Browse files Browse the repository at this point in the history
* Add new streams table components

* Update CatalogTree and Catalog section to render new components based on env-based flag
* Add basic Bulk edit panel, stream panel, table row, table headers
* Split the overlay out of the modal and make it its own component. Use in StreamPanel
* Add Fields to en.json

* StreamPanel -> StreamDetailsPanel

* Add new stream fields table components

* Use new stream fiels table props iface on Stream details panel
  • Loading branch information
edmundito authored and nataly committed Nov 3, 2022
1 parent 28c2b78 commit 9b38fa8
Show file tree
Hide file tree
Showing 21 changed files with 859 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { equal, naturalComparatorBy } from "utils/objects";
import { ConnectionFormValues, SUPPORTED_MODES } from "views/Connection/ConnectionForm/formConfig";

import styles from "./CatalogSection.module.scss";
import { CatalogTreeTableRow } from "./next/CatalogTreeTableRow";
import { StreamDetailsPanel } from "./next/StreamDetailsPanel";
import { StreamFieldTable } from "./StreamFieldTable";
import { StreamHeader } from "./StreamHeader";
import { flatten, getPathType } from "./utils";
Expand All @@ -41,6 +43,8 @@ const CatalogSectionInner: React.FC<CatalogSectionInnerProps> = ({
errors,
changedSelected,
}) => {
const isNewStreamsTableEnabled = process.env.REACT_APP_NEW_STREAMS_TABLE ?? false;

const {
destDefinition: { supportedDestinationSyncModes },
} = useConnectionFormService();
Expand Down Expand Up @@ -135,9 +139,11 @@ const CatalogSectionInner: React.FC<CatalogSectionInnerProps> = ({
const hasFields = fields?.length > 0;
const disabled = mode === "readonly";

const StreamComponent = isNewStreamsTableEnabled ? CatalogTreeTableRow : StreamHeader;

return (
<div className={styles.catalogSection}>
<StreamHeader
<StreamComponent
stream={streamNode}
destNamespace={destNamespace}
destName={destName}
Expand All @@ -156,18 +162,33 @@ const CatalogSectionInner: React.FC<CatalogSectionInnerProps> = ({
hasError={hasError}
disabled={disabled}
/>
{isRowExpanded && hasFields && (
<div className={styles.streamFieldTableContainer}>
<StreamFieldTable
{isRowExpanded &&
hasFields &&
(isNewStreamsTableEnabled ? (
<StreamDetailsPanel
config={config}
disabled={mode === "readonly"}
syncSchemaFields={flattenedFields}
onClose={onExpand}
onCursorSelect={onCursorSelect}
onPkSelect={onPkSelect}
onSelectedChange={onSelectStream}
shouldDefinePk={shouldDefinePk}
shouldDefineCursor={shouldDefineCursor}
stream={stream}
/>
</div>
)}
) : (
<div className={styles.streamFieldTableContainer}>
<StreamFieldTable
config={config}
syncSchemaFields={flattenedFields}
onCursorSelect={onCursorSelect}
onPkSelect={onPkSelect}
shouldDefinePk={shouldDefinePk}
shouldDefineCursor={shouldDefineCursor}
/>
</div>
))}
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import { CatalogTreeBody } from "./CatalogTreeBody";
import { CatalogTreeHeader } from "./CatalogTreeHeader";
import { CatalogTreeSearch } from "./CatalogTreeSearch";
import { CatalogTreeSubheader } from "./CatalogTreeSubheader";
import { BulkEditPanel } from "./next/BulkEditPanel";
import { CatalogTreeTableHeader } from "./next/CatalogTreeTableHeader";
import { StreamConnectionHeader } from "./next/StreamConnectionHeader";

interface CatalogTreeProps {
streams: SyncSchemaStream[];
Expand All @@ -24,6 +27,7 @@ const CatalogTreeComponent: React.FC<React.PropsWithChildren<CatalogTreeProps>>
onStreamsChanged,
isLoading,
}) => {
const isNewStreamsTableEnabled = process.env.REACT_APP_NEW_STREAMS_TABLE ?? false;
const { mode } = useConnectionFormService();

const [searchString, setSearchString] = useState("");
Expand Down Expand Up @@ -53,11 +57,21 @@ const CatalogTreeComponent: React.FC<React.PropsWithChildren<CatalogTreeProps>>
<BulkEditServiceProvider nodes={streams} update={onStreamsChanged}>
<LoadingBackdrop loading={isLoading}>
{mode !== "readonly" && <CatalogTreeSearch onSearch={setSearchString} />}
<CatalogTreeHeader />
<CatalogTreeSubheader />
<BulkHeader />
{isNewStreamsTableEnabled ? (
<>
<StreamConnectionHeader />
<CatalogTreeTableHeader />
</>
) : (
<>
<CatalogTreeHeader />
<CatalogTreeSubheader />
<BulkHeader />
</>
)}
<CatalogTreeBody streams={filteredStreams} onStreamChanged={onSingleStreamChanged} />
</LoadingBackdrop>
{isNewStreamsTableEnabled && <BulkEditPanel />}
</BulkEditServiceProvider>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import intersection from "lodash/intersection";
import React, { useMemo } from "react";
import { createPortal } from "react-dom";
import { FormattedMessage } from "react-intl";
import styled from "styled-components";

import { Cell, Header } from "components";
import { Button } from "components/ui/Button";
import { Switch } from "components/ui/Switch";

import { SyncSchemaField, SyncSchemaFieldObject, SyncSchemaStream, traverseSchemaToField } from "core/domain/catalog";
import { DestinationSyncMode, SyncMode } from "core/request/AirbyteClient";
import { useBulkEditService } from "hooks/services/BulkEdit/BulkEditService";
import { useConnectionFormService } from "hooks/services/ConnectionForm/ConnectionFormService";
import { SUPPORTED_MODES } from "views/Connection/ConnectionForm/formConfig";

import { pathDisplayName, PathPopout } from "../PathPopout";
import { HeaderCell } from "../styles";
import { SyncSettingsDropdown } from "../SyncSettingsDropdown";
import { flatten, getPathType } from "../utils";

const SchemaHeader = styled(Header)`
position: fixed;
bottom: 0;
left: 122px;
z-index: 1000;
width: calc(100% - 152px);
bottom: 0;
height: unset;
background: ${({ theme }) => theme.primaryColor};
border-radius: 8px 8px 0 0;
`;

function calculateSharedFields(selectedBatchNodes: SyncSchemaStream[]) {
const primitiveFieldsByStream = selectedBatchNodes.map(({ stream }) => {
const traversedFields = traverseSchemaToField(stream?.jsonSchema, stream?.name);
const flattenedFields = flatten(traversedFields);

return flattenedFields.filter(SyncSchemaFieldObject.isPrimitive);
});

const pathMap = new Map<string, SyncSchemaField>();

// calculate intersection of primitive fields across all selected streams
primitiveFieldsByStream.forEach((fields, index) => {
if (index === 0) {
fields.forEach((field) => pathMap.set(pathDisplayName(field.path), field));
} else {
const fieldMap = new Set(fields.map((f) => pathDisplayName(f.path)));
pathMap.forEach((_, k) => (!fieldMap.has(k) ? pathMap.delete(k) : null));
}
});

return Array.from(pathMap.values());
}

export const BulkEditPanel: React.FC = () => {
const {
destDefinition: { supportedDestinationSyncModes },
} = useConnectionFormService();
const { selectedBatchNodes, options, onChangeOption, onApply, isActive, onCancel } = useBulkEditService();

const availableSyncModes = useMemo(
() =>
SUPPORTED_MODES.filter(([syncMode, destinationSyncMode]) => {
const supportableModes = intersection(selectedBatchNodes.flatMap((n) => n.stream?.supportedSyncModes));
return supportableModes.includes(syncMode) && supportedDestinationSyncModes?.includes(destinationSyncMode);
}).map(([syncMode, destinationSyncMode]) => ({
value: { syncMode, destinationSyncMode },
})),
[selectedBatchNodes, supportedDestinationSyncModes]
);

const primitiveFields: SyncSchemaField[] = useMemo(
() => calculateSharedFields(selectedBatchNodes),
[selectedBatchNodes]
);

if (!isActive) {
return null;
}

const pkRequired = options.destinationSyncMode === DestinationSyncMode.append_dedup;
const shouldDefinePk = selectedBatchNodes.every((n) => n.stream?.sourceDefinedPrimaryKey?.length === 0) && pkRequired;
const cursorRequired = options.syncMode === SyncMode.incremental;
const shouldDefineCursor = selectedBatchNodes.every((n) => !n.stream?.sourceDefinedCursor) && cursorRequired;
const numStreamsSelected = selectedBatchNodes.length;

const pkType = getPathType(pkRequired, shouldDefinePk);
const cursorType = getPathType(cursorRequired, shouldDefineCursor);

const paths = primitiveFields.map((f) => f.path);

return createPortal(
<SchemaHeader>
<HeaderCell>
<div>{numStreamsSelected}</div>
<FormattedMessage id="connection.streams" />
</HeaderCell>
<HeaderCell flex={0.4}>
<div>
<FormattedMessage id="sources.sync" />
</div>
<Switch small checked={options.selected} onChange={() => onChangeOption({ selected: !options.selected })} />
</HeaderCell>
<Cell flex={1.5}>
<div>
<FormattedMessage id="form.syncMode" />
</div>
<SyncSettingsDropdown
value={{
syncMode: options.syncMode,
destinationSyncMode: options.destinationSyncMode,
}}
options={availableSyncModes}
onChange={({ value }) => onChangeOption({ ...value })}
/>
</Cell>
<HeaderCell>
{cursorType && (
<>
<div>
<FormattedMessage id="form.cursorField" />
</div>
<PathPopout
isMulti={false}
onPathChange={(path) => onChangeOption({ cursorField: path })}
pathType={cursorType}
paths={paths}
path={options.cursorField}
/>
</>
)}
</HeaderCell>
<HeaderCell>
{pkType && (
<>
<div>
<FormattedMessage id="form.primaryKey" />
</div>
<PathPopout
isMulti
onPathChange={(path) => onChangeOption({ primaryKey: path })}
pathType={pkType}
paths={paths}
path={options.primaryKey}
/>
</>
)}
</HeaderCell>
<HeaderCell>
<FontAwesomeIcon icon={faArrowRight} />
</HeaderCell>
<HeaderCell>
<div>
<FormattedMessage id="form.syncMode" />
</div>
<SyncSettingsDropdown
value={{
syncMode: options.syncMode,
destinationSyncMode: options.destinationSyncMode,
}}
options={availableSyncModes}
onChange={({ value }) => onChangeOption({ ...value })}
/>
</HeaderCell>
<HeaderCell>
<Button onClick={onCancel}>
<FormattedMessage id="connectionForm.bulkEdit.cancel" />
</Button>
<Button onClick={onApply}>
<FormattedMessage id="connectionForm.bulkEdit.apply" />
</Button>
</HeaderCell>
</SchemaHeader>,
document.body
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { FormattedMessage } from "react-intl";

import { Cell, Header } from "components/SimpleTableComponents";
import { CheckBox } from "components/ui/CheckBox";
import { InfoTooltip, TooltipLearnMoreLink } from "components/ui/Tooltip";

import { useBulkEditService } from "hooks/services/BulkEdit/BulkEditService";
import { useConnectionFormService } from "hooks/services/ConnectionForm/ConnectionFormService";
import { links } from "utils/links";

export const CatalogTreeTableHeader: React.FC = () => {
const { mode } = useConnectionFormService();
const { onCheckAll, selectedBatchNodeIds, allChecked } = useBulkEditService();

return (
<Header>
<Cell>
{mode !== "readonly" && (
<CheckBox
onChange={onCheckAll}
indeterminate={selectedBatchNodeIds.length > 0 && !allChecked}
checked={allChecked}
/>
)}
</Cell>
<Cell>
<FormattedMessage id="sources.sync" />
</Cell>
<Cell>
<FormattedMessage id="form.fields" />
</Cell>
<Cell>
<FormattedMessage id="form.namespace" />
</Cell>
<Cell>
<FormattedMessage id="form.streamName" />
</Cell>
<Cell>
<FormattedMessage id="form.syncMode" />
<InfoTooltip>
<FormattedMessage id="connectionForm.syncType.info" />
<TooltipLearnMoreLink url={links.syncModeLink} />
</InfoTooltip>
</Cell>
<Cell>
<FormattedMessage id="form.cursorField" />
<InfoTooltip>
<FormattedMessage id="connectionForm.cursor.info" />
</InfoTooltip>
</Cell>
<Cell>
<FormattedMessage id="form.primaryKey" />
</Cell>
<Cell />
<Cell>
<FormattedMessage id="form.namespace" />
</Cell>
<Cell>
<FormattedMessage id="form.streamName" />
</Cell>
<Cell>
<FormattedMessage id="form.syncMode" />
<InfoTooltip>
<FormattedMessage id="connectionForm.syncType.info" />
<TooltipLearnMoreLink url={links.syncModeLink} />
</InfoTooltip>
</Cell>
</Header>
);
};
Loading

0 comments on commit 9b38fa8

Please sign in to comment.