Skip to content

Commit

Permalink
🪟🎉 Connector builder: Add copy to/from stream buttons (#20816)
Browse files Browse the repository at this point in the history
Adds buttons to copy pagination and slicing settings between streams
  • Loading branch information
Joe Reuter authored Jan 5, 2023
1 parent 26053ef commit 6c692bc
Show file tree
Hide file tree
Showing 9 changed files with 237 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ interface BuilderProps {
toggleYamlEditor: () => void;
}

function getView(selectedView: BuilderView) {
function getView(selectedView: BuilderView, hasMultipleStreams: boolean) {
switch (selectedView) {
case "global":
return <GlobalConfigView />;
case "inputs":
return <InputsView />;
default:
return <StreamConfigView streamNum={selectedView} />;
return <StreamConfigView streamNum={selectedView} hasMultipleStreams={hasMultipleStreams} />;
}
}

Expand All @@ -47,7 +47,7 @@ export const Builder: React.FC<BuilderProps> = ({ values, toggleYamlEditor, vali
return (
<div className={styles.container}>
<BuilderSidebar className={styles.sidebar} toggleYamlEditor={toggleYamlEditor} />
<Form className={styles.form}>{getView(selectedView)}</Form>
<Form className={styles.form}>{getView(selectedView, values.streams.length > 1)}</Form>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -1,13 +1,45 @@
@use "scss/variables";
@use "scss/colors";

.card {
padding: variables.$spacing-xl;
display: flex;
flex-direction: column;
gap: variables.$spacing-xl;
position: relative;
}

.toggleContainer {
display: flex;
gap: variables.$spacing-md;
}

.copyButtonContainer {
position: absolute;
right: -50px;
top: 0;
display: flex;
flex-direction: column;
gap: variables.$spacing-md;
}

.modalStreamListContainer {
display: flex;
flex-direction: column;
gap: variables.$spacing-md;
align-items: stretch;
}

.streamItem {
border-radius: variables.$border-radius-sm;
padding: variables.$spacing-md;
border: none;
background: none;
display: block;
text-align: left;

&:hover {
cursor: pointer;
background: colors.$grey-50;
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import classNames from "classnames";
import React from "react";
import { useField, useFormikContext } from "formik";
import get from "lodash/get";
import React, { useState } from "react";
import { FormattedMessage } from "react-intl";

import { Button } from "components/ui/Button";
import { Card } from "components/ui/Card";
import { CheckBox } from "components/ui/CheckBox";
import { Modal, ModalBody, ModalFooter } from "components/ui/Modal";

import { BuilderStream } from "../types";
import styles from "./BuilderCard.module.scss";

interface BuilderCardProps {
Expand All @@ -13,13 +21,24 @@ interface BuilderCardProps {
toggledOn: boolean;
onToggle: (newToggleValue: boolean) => void;
};
copyConfig?: {
path: string;
currentStreamIndex: number;
copyToLabel: string;
copyFromLabel: string;
};
}

export const BuilderCard: React.FC<React.PropsWithChildren<BuilderCardProps>> = ({
children,
className,
toggleConfig,
copyConfig,
}) => {
const { setFieldValue, getFieldMeta } = useFormikContext();
const [isCopyToOpen, setCopyToOpen] = useState(false);
const [isCopyFromOpen, setCopyFromOpen] = useState(false);
const streams = getFieldMeta<BuilderStream[]>("streams").value;
return (
<Card className={classNames(className, styles.card)}>
{toggleConfig && (
Expand All @@ -33,7 +52,144 @@ export const BuilderCard: React.FC<React.PropsWithChildren<BuilderCardProps>> =
{toggleConfig.label}
</div>
)}
{copyConfig && streams.length > 1 && (
<div className={styles.copyButtonContainer}>
<Button
variant="secondary"
type="button"
onClick={() => {
setCopyFromOpen(true);
}}
icon={<FontAwesomeIcon icon={faArrowUpRightFromSquare} rotation={180} />}
/>
{get(streams[copyConfig.currentStreamIndex], copyConfig.path) && (
<Button
variant="secondary"
type="button"
onClick={() => {
setCopyToOpen(true);
}}
icon={<FontAwesomeIcon icon={faArrowUpRightFromSquare} />}
/>
)}
{isCopyToOpen && (
<CopyToModal
onCancel={() => {
setCopyToOpen(false);
}}
onApply={(selectedStreamIndices) => {
const sectionToCopy = getFieldMeta(
`streams[${copyConfig.currentStreamIndex}].${copyConfig.path}`
).value;
selectedStreamIndices.forEach((index) => {
setFieldValue(`streams[${index}].${copyConfig.path}`, sectionToCopy, true);
});
setCopyToOpen(false);
}}
currentStreamIndex={copyConfig.currentStreamIndex}
title={copyConfig.copyToLabel}
/>
)}
{isCopyFromOpen && (
<CopyFromModal
onCancel={() => {
setCopyFromOpen(false);
}}
onSelect={(selectedStreamIndex) => {
setFieldValue(
`streams[${copyConfig.currentStreamIndex}].${copyConfig.path}`,
getFieldMeta(`streams[${selectedStreamIndex}].${copyConfig.path}`).value,
true
);
setCopyFromOpen(false);
}}
currentStreamIndex={copyConfig.currentStreamIndex}
title={copyConfig.copyFromLabel}
/>
)}
</div>
)}
{(!toggleConfig || toggleConfig.toggledOn) && children}
</Card>
);
};

function getStreamName(stream: BuilderStream) {
return stream.name || <FormattedMessage id="connectorBuilder.emptyName" />;
}

const CopyToModal: React.FC<{
onCancel: () => void;
onApply: (selectedStreamIndices: number[]) => void;
title: string;
currentStreamIndex: number;
}> = ({ onCancel, onApply, title, currentStreamIndex }) => {
const [streams] = useField<BuilderStream[]>("streams");
const [selectMap, setSelectMap] = useState<Record<string, boolean>>({});
return (
<Modal size="sm" title={title} onClose={onCancel}>
<form
onSubmit={() => {
onApply(
Object.entries(selectMap)
.filter(([, selected]) => selected)
.map(([index]) => Number(index))
);
}}
>
<ModalBody className={styles.modalStreamListContainer}>
{streams.value.map((stream, index) =>
index === currentStreamIndex ? null : (
<label htmlFor={`copy-to-stream-${index}`} key={index} className={styles.toggleContainer}>
<CheckBox
id={`copy-to-stream-${index}`}
checked={selectMap[index] || false}
onChange={() => {
setSelectMap({ ...selectMap, [index]: !selectMap[index] });
}}
/>
{getStreamName(stream)}
</label>
)
)}
</ModalBody>
<ModalFooter>
<Button variant="secondary" onClick={onCancel}>
<FormattedMessage id="form.cancel" />
</Button>
<Button type="submit" disabled={Object.values(selectMap).filter(Boolean).length === 0}>
<FormattedMessage id="form.apply" />
</Button>
</ModalFooter>
</form>
</Modal>
);
};

const CopyFromModal: React.FC<{
onCancel: () => void;
onSelect: (selectedStreamIndex: number) => void;
title: string;
currentStreamIndex: number;
}> = ({ onCancel, onSelect, title, currentStreamIndex }) => {
const [streams] = useField<BuilderStream[]>("streams");
return (
<Modal size="sm" title={title} onClose={onCancel}>
<ModalBody className={styles.modalStreamListContainer}>
{streams.value.map((stream, index) =>
currentStreamIndex === index ? null : (
<button
key={index}
onClick={() => {
onSelect(index);
}}
className={styles.streamItem}
>
{getStreamName(stream)}
</button>
)
)}
</ModalBody>
</Modal>
);
};
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import classNames from "classnames";
import React from "react";

import { Heading } from "components/ui/Heading";
Expand All @@ -6,11 +7,16 @@ import styles from "./BuilderConfigView.module.scss";

interface BuilderConfigViewProps {
heading: string;
className?: string;
}

export const BuilderConfigView: React.FC<React.PropsWithChildren<BuilderConfigViewProps>> = ({ children, heading }) => {
export const BuilderConfigView: React.FC<React.PropsWithChildren<BuilderConfigViewProps>> = ({
children,
heading,
className,
}) => {
return (
<div className={styles.container}>
<div className={classNames(styles.container, className)}>
<Heading className={styles.heading} as="h1">
{heading}
</Heading>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useField } from "formik";
import capitalize from "lodash/capitalize";
import { useIntl } from "react-intl";

import GroupControls from "components/GroupControls";
import { ControlLabels } from "components/LabeledControl";
Expand All @@ -15,9 +16,11 @@ import { ToggleGroupField } from "./ToggleGroupField";

interface PaginationSectionProps {
streamFieldPath: (fieldPath: string) => string;
currentStreamIndex: number;
}

export const PaginationSection: React.FC<PaginationSectionProps> = ({ streamFieldPath }) => {
export const PaginationSection: React.FC<PaginationSectionProps> = ({ streamFieldPath, currentStreamIndex }) => {
const { formatMessage } = useIntl();
const [field, , helpers] = useField<BuilderPaginator | undefined>(streamFieldPath("paginator"));
const [pageSizeField] = useField(streamFieldPath("paginator.strategy.page_size"));
const [, , pageSizeOptionHelpers] = useField(streamFieldPath("paginator.pageSizeOption"));
Expand Down Expand Up @@ -52,6 +55,12 @@ export const PaginationSection: React.FC<PaginationSectionProps> = ({ streamFiel
toggledOn,
onToggle: handleToggle,
}}
copyConfig={{
path: "paginator",
currentStreamIndex,
copyFromLabel: formatMessage({ id: "connectorBuilder.copyFromPaginationTitle" }),
copyToLabel: formatMessage({ id: "connectorBuilder.copyToPaginationTitle" }),
}}
>
<BuilderOneOf
path={streamFieldPath("paginator.strategy")}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,7 @@ $controlButtonWidth: 24px;
background-color: colors.$red;
}
}

.multiStreams {
padding-right: 50px;
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ import { StreamSlicerSection } from "./StreamSlicerSection";

interface StreamConfigViewProps {
streamNum: number;
hasMultipleStreams: boolean;
}

export const StreamConfigView: React.FC<StreamConfigViewProps> = ({ streamNum }) => {
export const StreamConfigView: React.FC<StreamConfigViewProps> = ({ streamNum, hasMultipleStreams }) => {
const { formatMessage } = useIntl();
const [field, , helpers] = useField<BuilderStream[]>("streams");
const { openConfirmationModal, closeConfirmationModal } = useConfirmationModalService();
Expand All @@ -49,7 +50,10 @@ export const StreamConfigView: React.FC<StreamConfigViewProps> = ({ streamNum })
};

return (
<BuilderConfigView heading={formatMessage({ id: "connectorBuilder.stream" })}>
<BuilderConfigView
heading={formatMessage({ id: "connectorBuilder.stream" })}
className={hasMultipleStreams ? styles.multiStreams : undefined}
>
{/* Not using intl for the labels and tooltips in this component in order to keep maintainence simple */}
<BuilderTitle path={streamFieldPath("name")} label="Stream Name" size="md" />
<div className={styles.controls}>
Expand Down Expand Up @@ -97,8 +101,8 @@ export const StreamConfigView: React.FC<StreamConfigViewProps> = ({ streamNum })
optional
/>
</BuilderCard>
<PaginationSection streamFieldPath={streamFieldPath} />
<StreamSlicerSection streamFieldPath={streamFieldPath} />
<PaginationSection streamFieldPath={streamFieldPath} currentStreamIndex={streamNum} />
<StreamSlicerSection streamFieldPath={streamFieldPath} currentStreamIndex={streamNum} />
<BuilderCard>
<KeyValueListField
path={streamFieldPath("requestOptions.requestParameters")}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useField } from "formik";
import { useIntl } from "react-intl";

import { ControlLabels } from "components/LabeledControl";

Expand All @@ -14,9 +15,11 @@ import { ToggleGroupField } from "./ToggleGroupField";

interface StreamSlicerSectionProps {
streamFieldPath: (fieldPath: string) => string;
currentStreamIndex: number;
}

export const StreamSlicerSection: React.FC<StreamSlicerSectionProps> = ({ streamFieldPath }) => {
export const StreamSlicerSection: React.FC<StreamSlicerSectionProps> = ({ streamFieldPath, currentStreamIndex }) => {
const { formatMessage } = useIntl();
const [field, , helpers] = useField<SimpleRetrieverStreamSlicer | undefined>(streamFieldPath("streamSlicer"));

const handleToggle = (newToggleValue: boolean) => {
Expand Down Expand Up @@ -44,6 +47,12 @@ export const StreamSlicerSection: React.FC<StreamSlicerSectionProps> = ({ stream
toggledOn,
onToggle: handleToggle,
}}
copyConfig={{
path: "streamSlicer",
currentStreamIndex,
copyFromLabel: formatMessage({ id: "connectorBuilder.copyFromSlicerTitle" }),
copyToLabel: formatMessage({ id: "connectorBuilder.copyToSlicerTitle" }),
}}
>
<BuilderOneOf
path={streamFieldPath("streamSlicer")}
Expand Down
Loading

0 comments on commit 6c692bc

Please sign in to comment.