Skip to content

Commit

Permalink
[Connector Builder] Config UI error handling (#20280)
Browse files Browse the repository at this point in the history
* move connector builder components into the same shared components/connectorBuilder directory

* move diff over from poc branch

* save current progress

* add modal for adding streams

* focus stream after adding and reset button style

* add reset confirm modal and select view on add

* style global config and streams buttons

* styling improvements

* handle long stream names better

* pull in connector manifest schema directly

* add box shadows to resizable panels

* upgrade orval and use connector manifest schema directly

* remove airbyte protocol from connector builder api spec

* generate python models from openapi change

* fix position of yaml toggle

* handle no stream case with better looking message

* group global fields into single object and fix console error

* confirmation modal on toggling dirty form + cleanup

* fix connector name display

* undo change to manifest schema

* remove commented code

* remove unnecessary change

* fix spacing

* use shadow mixin for connector img

* add comment about connector img

* save progress

* change onSubmit to no-op

* remove console log

* clean up styling

* simplify sidebar to remove StreamSelectButton component

* swap colors of toggle

* move FormikPatch to src/core/form

* move types up to connectorBuilder/ level

* use grid display for ui yaml toggle button

* use spread instead of setting array index directly

* add intl in missing places

* pull connector manifest schema in through separate openapi spec

* disable test and download buttons when there are errors and touch erroring fields

* fix comment

* add error indicators to view buttons

* remove commented code

* use correct intl string id

* throttle setting json manifest in yaml editor

* use  button prop instead of manually styling

* consolidate AddStreamButton styles

* fix sidebar flex styles

* use specific flex properties instead of flex

* clean up download and reset button styles

* use row-reverse for yaml editor download button

* fix stream selector styles to remove margins

* give connector setup guide panel same corner and shadow styles

* remove blur from page display

* set view to stream when selected in test panel

* add placeholder when stream name is empty

* switch to index-based stream selection to preserve testing panel selected stream on rename

* handle empty name in stream selector

* remove unnecessary margin

* only show error indicator for touched fields

* focus erroring view on test click

* show warning icon and tooltip when there are form errors

* remove unused parameter
  • Loading branch information
lmossman committed Dec 13, 2022
1 parent 1c3557b commit 3e10c09
Show file tree
Hide file tree
Showing 15 changed files with 280 additions and 87 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useField } from "formik";
import { FormattedMessage } from "react-intl";
import * as yup from "yup";

import { ControlLabels } from "components/LabeledControl";
import { DropDown } from "components/ui/DropDown";
Expand Down Expand Up @@ -53,25 +52,7 @@ const ArrayField: React.FC<ArrayFieldProps> = ({ name, value, setValue, error })
};

export const BuilderField: React.FC<BuilderFieldProps> = ({ path, label, tooltip, optional = false, ...props }) => {
let yupSchema = props.type === "array" ? yup.array().of(yup.string()) : yup.string();
if (!optional) {
yupSchema = yupSchema.required("form.empty.error");
}
const fieldConfig = {
name: path,
validate: (value: string) => {
try {
yupSchema.validateSync(value);
return undefined;
} catch (err) {
if (err instanceof yup.ValidationError) {
return err.errors.join(", ");
}
throw err;
}
},
};
const [field, meta, helpers] = useField(fieldConfig);
const [field, meta, helpers] = useField(path);
const hasError = !!meta.error && meta.touched;

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,19 @@
overflow-y: auto;
}

.viewLabel {
flex: 1;
overflow: hidden;
display: flex;
gap: variables.$spacing-sm;
align-items: center;
}

.errorIndicator {
flex: 0 0 auto;
margin-right: variables.$spacing-xs;
}

.streamViewText {
color: inherit;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import classnames from "classnames";
import { useFormikContext } from "formik";
import { FormattedMessage, useIntl } from "react-intl";

import Indicator from "components/Indicator";
import { Button } from "components/ui/Button";
import { Heading } from "components/ui/Heading";
import { Text } from "components/ui/Text";
Expand All @@ -17,20 +18,23 @@ import {

import { DownloadYamlButton } from "../DownloadYamlButton";
import { BuilderFormValues } from "../types";
import { useBuilderErrors } from "../useBuilderErrors";
import { AddStreamButton } from "./AddStreamButton";
import styles from "./BuilderSidebar.module.scss";
import { UiYamlToggleButton } from "./UiYamlToggleButton";

interface ViewSelectButtonProps {
className?: string;
selected: boolean;
showErrorIndicator: boolean;
onClick: () => void;
}

const ViewSelectButton: React.FC<React.PropsWithChildren<ViewSelectButtonProps>> = ({
children,
className,
selected,
showErrorIndicator,
onClick,
}) => {
return (
Expand All @@ -41,7 +45,8 @@ const ViewSelectButton: React.FC<React.PropsWithChildren<ViewSelectButtonProps>>
})}
onClick={onClick}
>
{children}
<div className={styles.viewLabel}>{children}</div>
{showErrorIndicator && <Indicator className={styles.errorIndicator} />}
</button>
);
};
Expand All @@ -53,6 +58,7 @@ interface BuilderSidebarProps {

export const BuilderSidebar: React.FC<BuilderSidebarProps> = ({ className, toggleYamlEditor }) => {
const { formatMessage } = useIntl();
const { hasErrors } = useBuilderErrors();
const { openConfirmationModal, closeConfirmationModal } = useConfirmationModalService();
const { yamlManifest, selectedView, setSelectedView, setTestStreamIndex } = useConnectorBuilderState();
const { values, setValues } = useFormikContext<BuilderFormValues>();
Expand Down Expand Up @@ -95,6 +101,7 @@ export const BuilderSidebar: React.FC<BuilderSidebarProps> = ({ className, toggl
<ViewSelectButton
className={styles.globalConfigButton}
selected={selectedView === "global"}
showErrorIndicator={hasErrors(true, ["global"])}
onClick={() => handleViewSelect("global")}
>
<FontAwesomeIcon icon={faSliders} />
Expand All @@ -111,7 +118,12 @@ export const BuilderSidebar: React.FC<BuilderSidebarProps> = ({ className, toggl

<div className={styles.streamList}>
{values.streams.map(({ name }, num) => (
<ViewSelectButton key={num} selected={selectedView === num} onClick={() => handleViewSelect(num)}>
<ViewSelectButton
key={num}
selected={selectedView === num}
showErrorIndicator={hasErrors(true, [num])}
onClick={() => handleViewSelect(num)}
>
{name && name.trim() ? (
<Text className={styles.streamViewText}>{name}</Text>
) : (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.tooltipContainer {
display: block;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,40 +5,75 @@ import { FormattedMessage } from "react-intl";
import { Button } from "components/ui/Button";
import { Tooltip } from "components/ui/Tooltip";

import { useConnectorBuilderState } from "services/connectorBuilder/ConnectorBuilderStateService";
import { downloadFile } from "utils/file";

import styles from "./DownloadYamlButton.module.scss";
import { useBuilderErrors } from "./useBuilderErrors";

interface DownloadYamlButtonProps {
className?: string;
yaml: string;
yamlIsValid: boolean;
}

export const DownloadYamlButton: React.FC<DownloadYamlButtonProps> = ({ className, yaml, yamlIsValid }) => {
const { editorView } = useConnectorBuilderState();
const { hasErrors, validateAndTouch } = useBuilderErrors();

const downloadYaml = () => {
const file = new Blob([yaml], { type: "text/plain;charset=utf-8" });
// TODO: pull name from connector name input or generate from yaml contents
downloadFile(file, "connector_builder.yaml");
};

const handleClick = () => {
if (editorView === "yaml") {
downloadYaml();
return;
}

validateAndTouch(downloadYaml);
};

let buttonDisabled = false;
let showWarningIcon = false;
let tooltipContent = undefined;

if (editorView === "yaml" && !yamlIsValid) {
buttonDisabled = true;
showWarningIcon = true;
tooltipContent = <FormattedMessage id="connectorBuilder.invalidYamlDownload" />;
}

if (editorView === "ui" && hasErrors(true)) {
showWarningIcon = true;
tooltipContent = <FormattedMessage id="connectorBuilder.configErrorsDownload" />;
}

const downloadButton = (
<Button
full
onClick={downloadYaml}
disabled={!yamlIsValid}
icon={yamlIsValid ? <FontAwesomeIcon icon={faDownload} /> : <FontAwesomeIcon icon={faWarning} />}
onClick={handleClick}
disabled={buttonDisabled}
icon={showWarningIcon ? <FontAwesomeIcon icon={faWarning} /> : <FontAwesomeIcon icon={faDownload} />}
>
<FormattedMessage id="connectorBuilder.downloadYaml" />
</Button>
);

return (
<div className={className}>
{yamlIsValid ? (
downloadButton
) : (
<Tooltip control={downloadButton} placement="left">
<FormattedMessage id="connectorBuilder.invalidYamlDownload" />
{tooltipContent !== undefined ? (
<Tooltip
control={downloadButton}
placement={editorView === "yaml" ? "left" : "top"}
containerClassName={styles.tooltipContainer}
>
{tooltipContent}
</Tooltip>
) : (
downloadButton
)}
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
@use "scss/variables";
@use "scss/colors";

.testButton {
width: 100%;
}

.testButtonTooltipContainer {
width: 100%;
}

.testButtonText {
color: colors.$white;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { FormattedMessage } from "react-intl";

import { RotateIcon } from "components/icons/RotateIcon";
import { Button } from "components/ui/Button";
import { Text } from "components/ui/Text";
import { Tooltip } from "components/ui/Tooltip";

import { useConnectorBuilderState } from "services/connectorBuilder/ConnectorBuilderStateService";

import { useBuilderErrors } from "../useBuilderErrors";
import styles from "./StreamTestButton.module.scss";

interface StreamTestButtonProps {
readStream: () => void;
}

export const StreamTestButton: React.FC<StreamTestButtonProps> = ({ readStream }) => {
const { editorView, yamlIsValid, testStreamIndex } = useConnectorBuilderState();
const { hasErrors, validateAndTouch } = useBuilderErrors();

const handleClick = () => {
if (editorView === "yaml") {
readStream();
return;
}

validateAndTouch(readStream, ["global", testStreamIndex]);
};

let buttonDisabled = false;
let showWarningIcon = false;
let tooltipContent = undefined;

if (editorView === "yaml" && !yamlIsValid) {
buttonDisabled = true;
showWarningIcon = true;
tooltipContent = <FormattedMessage id="connectorBuilder.invalidYamlTest" />;
}

if (editorView === "ui" && hasErrors(true, ["global", testStreamIndex])) {
showWarningIcon = true;
tooltipContent = <FormattedMessage id="connectorBuilder.configErrorsTest" />;
}

const testButton = (
<Button
className={styles.testButton}
size="sm"
onClick={handleClick}
disabled={buttonDisabled}
icon={
showWarningIcon ? (
<FontAwesomeIcon icon={faWarning} />
) : (
<div>
<RotateIcon width={styles.testIconHeight} height={styles.testIconHeight} />
</div>
)
}
>
<Text className={styles.testButtonText} size="sm" bold>
<FormattedMessage id="connectorBuilder.testButton" />
</Text>
</Button>
);

return tooltipContent !== undefined ? (
<Tooltip control={testButton} containerClassName={styles.testButtonTooltipContainer}>
{tooltipContent}
</Tooltip>
) : (
testButton
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,6 @@
z-index: 0;
}

.testButtonTooltipContainer {
width: 100%;
}

.testButtonText {
color: colors.$white;
}

.url {
color: colors.$blue;
font-weight: 400;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
import { faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useEffect, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { useIntl } from "react-intl";

import { RotateIcon } from "components/icons/RotateIcon";
import { Button } from "components/ui/Button";
import { ResizablePanels } from "components/ui/ResizablePanels";
import { Spinner } from "components/ui/Spinner";
import { Text } from "components/ui/Text";
import { Tooltip } from "components/ui/Tooltip";

import { useReadStream } from "services/connectorBuilder/ConnectorBuilderApiService";
import { useConnectorBuilderState } from "services/connectorBuilder/ConnectorBuilderStateService";

import { LogsDisplay } from "./LogsDisplay";
import { ResultDisplay } from "./ResultDisplay";
import { StreamTestButton } from "./StreamTestButton";
import styles from "./StreamTester.module.scss";

export const StreamTester: React.FC = () => {
const { formatMessage } = useIntl();
const { jsonManifest, configJson, yamlIsValid, streams, testStreamIndex } = useConnectorBuilderState();
const { jsonManifest, configJson, streams, testStreamIndex } = useConnectorBuilderState();
const {
data: streamReadData,
refetch: readStream,
Expand Down Expand Up @@ -53,42 +49,14 @@ export const StreamTester: React.FC = () => {
}
}, [isError]);

const testButton = (
<Button
full
size="sm"
onClick={() => {
readStream();
}}
disabled={!yamlIsValid}
icon={
yamlIsValid ? (
<div>
<RotateIcon width={styles.testIconHeight} height={styles.testIconHeight} />
</div>
) : (
<FontAwesomeIcon icon={faWarning} />
)
}
>
<Text className={styles.testButtonText} size="sm" bold>
<FormattedMessage id="connectorBuilder.testButton" />
</Text>
</Button>
);

return (
<div className={styles.container}>
<Text className={styles.url} size="lg">
{streams[testStreamIndex]?.url}
</Text>
{yamlIsValid ? (
testButton
) : (
<Tooltip control={testButton} containerClassName={styles.testButtonTooltipContainer}>
<FormattedMessage id="connectorBuilder.invalidYamlTest" />
</Tooltip>
)}

<StreamTestButton readStream={readStream} />

{isFetching && (
<div className={styles.fetchingSpinner}>
<Spinner />
Expand Down
Loading

0 comments on commit 3e10c09

Please sign in to comment.