Skip to content

Commit

Permalink
🪟 🎉 Add sync mode, primary key, cursor select components to new strea…
Browse files Browse the repository at this point in the history
…ms table (#18627)

* Add SyncModeSelect component
Add button type to pill button and stop propagation
Stop propagation on popoout outside click listener and migrate to scss module

* Update DropDown option to handle click event directly and stop propagation

* Add StreamPathSelect component to select the primary key and cursor paths
Update PillSelect to handle nil values

* Add SyncMode, Primary Key, and Cursor selects to CatalogTreeTableRow

* Replace popup click catcher with Overlay component
Add Overlay color variant and onClick handler option
Fix Overlay import
  • Loading branch information
edmundito authored Oct 31, 2022
1 parent 3b44baf commit db5a149
Show file tree
Hide file tree
Showing 13 changed files with 181 additions and 35 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { faArrowRight, faMinus, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import classnames from "classnames";
import { useMemo } from "react";
import React, { useMemo } from "react";
import { FormattedMessage } from "react-intl";

import { Cell, Row } from "components/SimpleTableComponents";
Expand All @@ -10,18 +10,19 @@ import { Switch } from "components/ui/Switch";

import { useBulkEditSelect } from "hooks/services/BulkEdit/BulkEditService";

import { PathPopout } from "../PathPopout";
import { StreamHeaderProps } from "../StreamHeader";
import { HeaderCell } from "../styles";
import styles from "./CatalogTreeTableRow.module.scss";
import { StreamPathSelect } from "./StreamPathSelect";
import { SyncModeSelect } from "./SyncModeSelect";

export const CatalogTreeTableRow: React.FC<StreamHeaderProps> = ({
stream,
destName,
destNamespace,
// onSelectSyncMode,
onSelectSyncMode,
onSelectStream,
// availableSyncModes,
availableSyncModes,
pkType,
onPrimaryKeyChange,
onCursorChange,
Expand Down Expand Up @@ -96,29 +97,26 @@ export const CatalogTreeTableRow: React.FC<StreamHeaderProps> = ({
{syncSchema.syncMode}
</HeaderCell>
) : (
// TODO: Replace with Dropdown/Popout
syncSchema.syncMode
<SyncModeSelect options={availableSyncModes} onChange={onSelectSyncMode} value={syncSchema} />
)}
</Cell>
<HeaderCell>
{cursorType && (
<PathPopout
<StreamPathSelect
pathType={cursorType}
paths={paths}
path={cursorType === "sourceDefined" ? defaultCursorField : cursorField}
placeholder={<FormattedMessage id="connectionForm.cursor.searchPlaceholder" />}
onPathChange={onCursorChange}
/>
)}
</HeaderCell>
<HeaderCell ellipsis>
{pkType && (
<PathPopout
<StreamPathSelect
pathType={pkType}
paths={paths}
path={primaryKey}
isMulti
placeholder={<FormattedMessage id="connectionForm.primaryKey.searchPlaceholder" />}
onPathChange={onPrimaryKeyChange}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Dialog } from "@headlessui/react";

import { Overlay } from "components/ui/Overlay/Overlay";
import { Overlay } from "components/ui/Overlay";

import { AirbyteStream } from "core/request/AirbyteClient";

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from "react";
import { FormattedMessage } from "react-intl";

import { PillSelect } from "components/ui/PillSelect";
import { Tooltip } from "components/ui/Tooltip";

import { Path } from "core/domain/catalog";

export const pathDisplayName = (path: Path): string => path.join(".");

export type IndexerType = null | "required" | "sourceDefined";

interface StreamPathSelectBaseProps {
paths: Path[];
pathType: "required" | "sourceDefined";
placeholder?: React.ReactNode;
}

interface StreamPathSelectMultiProps {
path?: Path[];
onPathChange: (pkPath: Path[]) => void;
isMulti: true;
}

interface StreamPathSelectProps {
path?: Path;
onPathChange: (pkPath: Path) => void;
isMulti?: false;
}

type PathPopoutProps = StreamPathSelectBaseProps & (StreamPathSelectMultiProps | StreamPathSelectProps);

export const StreamPathSelect: React.FC<PathPopoutProps> = (props) => {
if (props.pathType === "sourceDefined") {
if (props.path && props.path.length > 0) {
const text = props.isMulti ? props.path.map(pathDisplayName).join(", ") : pathDisplayName(props.path);

return (
<Tooltip placement="bottom-start" control={text}>
{text}
</Tooltip>
);
}

return <FormattedMessage id="connection.catalogTree.sourceDefined" />;
}

const options = props.paths.map((path) => ({
value: path,
label: pathDisplayName(path),
}));

return (
<PillSelect
options={options}
value={props.path}
isMulti={props.isMulti}
onChange={(options: PathPopoutProps["isMulti"] extends true ? Array<{ value: Path }> : { value: Path }) => {
const finalValues = Array.isArray(options) ? options.map((op) => op.value) : options.value;
props.onPathChange(finalValues);
}}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.pillSelect {
width: 100%;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useMemo } from "react";
import { FormattedMessage } from "react-intl";

import { DropDownOptionDataItem } from "components/ui/DropDown";
import { PillSelect } from "components/ui/PillSelect";

import { DestinationSyncMode, SyncMode } from "core/request/AirbyteClient";

import styles from "./SyncModeSelect.module.scss";

interface SyncModeValue {
syncMode: SyncMode;
destinationSyncMode: DestinationSyncMode;
}

interface SyncModeOption {
value: SyncModeValue;
}

interface SyncModeSelectProps {
options: SyncModeOption[];
value: Partial<SyncModeValue>;
onChange?: (option: DropDownOptionDataItem<SyncModeValue>) => void;
}

export const SyncModeSelect: React.FC<SyncModeSelectProps> = ({ options, onChange, value }) => {
const pillSelectOptions = useMemo(() => {
return options.map(({ value }) => {
const { syncMode, destinationSyncMode } = value;
return {
label: (
<>
<FormattedMessage id={`syncMode.${syncMode}`} />
{` | `}
<FormattedMessage id={`destinationSyncMode.${destinationSyncMode}`} />
</>
),
value,
};
});
}, [options]);

return <PillSelect options={pillSelectOptions} value={value} onChange={onChange} className={styles.pillSelect} />;
};
15 changes: 10 additions & 5 deletions airbyte-webapp/src/components/ui/DropDown/components/Option.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,16 @@ export type DropDownOptionProps = {
data: { disabled: boolean; index: number; fullText?: boolean } & DropDownOptionDataItem;
} & OptionProps<OptionType, boolean>;

export interface DropDownOptionDataItem {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface DropDownOptionDataItem<Value = any, Config = any> {
label?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value?: any;
value?: Value;
groupValue?: string;
groupValueText?: string;
img?: React.ReactNode;
primary?: boolean;
secondary?: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
config?: any;
config?: Config;
}

export const OptionView = styled.div<{
Expand Down Expand Up @@ -61,6 +60,12 @@ export const DropDownOption: React.FC<DropDownOptionProps> = (props) => {
isSelected={props.isSelected && !props.isMulti}
isDisabled={props.isDisabled}
isFocused={props.isFocused}
onClick={(event) => {
// This custom onClick handler prevents the click event from bubbling up outside of the option
// for cases where the Dropdown is a child of a clickable parent such as a table row.
props.selectOption(props.data);
event.stopPropagation();
}}
>
<DropDownText primary={props.data.primary} secondary={props.data.secondary} fullText={props.data.fullText}>
{props.isMulti && (
Expand Down
2 changes: 1 addition & 1 deletion airbyte-webapp/src/components/ui/Modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import classNames from "classnames";
import React, { useState } from "react";

import { Card } from "../Card";
import { Overlay } from "../Overlay/Overlay";
import { Overlay } from "../Overlay";
import styles from "./Modal.module.scss";

export interface ModalProps {
Expand Down
5 changes: 4 additions & 1 deletion airbyte-webapp/src/components/ui/Overlay/Overlay.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,8 @@
left: 0;
right: 0;
bottom: 0;
background: rgba(colors.$black, 0.5);

&.dark {
background: rgba(colors.$black, 0.5);
}
}
18 changes: 17 additions & 1 deletion airbyte-webapp/src/components/ui/Overlay/Overlay.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
import classNames from "classnames";

import styles from "./Overlay.module.scss";

export const Overlay: React.FC = () => <div className={styles.container} aria-hidden="true" />;
interface OverlayProps {
onClick?: React.MouseEventHandler<HTMLDivElement>;
variant?: "dark" | "transparent";
}

export const Overlay: React.FC<OverlayProps> = ({ variant = "dark", onClick }) => (
<div
className={classNames(styles.container, {
[styles.dark]: variant === "dark",
})}
role={onClick ? "button" : undefined}
onClick={onClick}
aria-hidden="true"
/>
);
1 change: 1 addition & 0 deletions airbyte-webapp/src/components/ui/Overlay/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./Overlay";
2 changes: 1 addition & 1 deletion airbyte-webapp/src/components/ui/PillSelect/PillButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const PillButton: React.FC<React.PropsWithChildren<PillButtonProps>> = ({
);

return (
<button {...buttonProps} className={buttonClassName}>
<button type="button" {...buttonProps} className={buttonClassName}>
<Text as="span" size="xs" className={styles.text}>
{children}
</Text>
Expand Down
22 changes: 17 additions & 5 deletions airbyte-webapp/src/components/ui/PillSelect/PillSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,32 @@ import { Popout, PopoutProps } from "../Popout";
import { Tooltip } from "../Tooltip";
import { PillButton } from "./PillButton";

type PillSelectProps = Pick<PopoutProps, "value" | "options" | "isMulti" | "onChange">;
type PillSelectProps = Pick<PopoutProps, "value" | "options" | "isMulti" | "onChange" | "className">;

export const PillSelect: React.FC<PillSelectProps> = ({ className, ...props }) => {
const { isMulti } = props;

export const PillSelect: React.FC<PillSelectProps> = (props) => {
return (
<Popout
{...props}
targetComponent={({ onOpen, isOpen, value }) => {
const { isMulti } = props;
const label = isMulti ? value.map(({ label }: { label: string }) => label).join(", ") : value.label;
const label = value
? isMulti
? value.map(({ label }: { label: string }) => label).join(", ")
: value.label
: "";

return (
<Tooltip
control={
<PillButton onClick={() => onOpen()} active={isOpen}>
<PillButton
onClick={(event) => {
event.stopPropagation();
onOpen();
}}
active={isOpen}
className={className}
>
{label}
</PillButton>
}
Expand Down
20 changes: 10 additions & 10 deletions airbyte-webapp/src/components/ui/Popout/Popout.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
import React, { ReactNode, useMemo } from "react";
import { ActionMeta, ControlProps, StylesConfig } from "react-select";
import { useToggle } from "react-use";
import styled from "styled-components";

import { DropDown, DropdownProps } from "components/ui/DropDown";

const OutsideClickListener = styled.div`
bottom: 0;
left: 0;
top: 0;
right: 0;
position: fixed;
z-index: 1;
`;
import { Overlay } from "../Overlay";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Value = any;
Expand Down Expand Up @@ -83,7 +75,15 @@ export const Popout: React.FC<PopoutProps> = ({ onChange, targetComponent, ...pr
onChange={onSelectChange}
components={components}
/>
{isOpen ? <OutsideClickListener onClick={toggleOpen} /> : null}
{isOpen && (
<Overlay
variant="transparent"
onClick={(event) => {
event.stopPropagation();
toggleOpen();
}}
/>
)}
</>
);
};

0 comments on commit db5a149

Please sign in to comment.