Skip to content

Commit

Permalink
🪟🎉 Connector builder: Integrate connector form for test input (#20385)
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

* 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

* 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

* make connector form work in connector builder

* fix small stuff

* add warning label

* review comments

* adjust translation

Co-authored-by: lmossman <lake@airbyte.io>
  • Loading branch information
Joe Reuter and lmossman authored Dec 20, 2022
1 parent 02c8096 commit 7d49233
Show file tree
Hide file tree
Showing 21 changed files with 412 additions and 128 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,31 @@
@use "scss/colors";
@use "scss/variables";

.modalContent {
height: 60vh;
overflow: visible;
background-color: colors.$grey-100;
.formContent {
max-height: 60vh;
overflow: auto;
}

.inputFormModalFooter {
border-top: variables.$border-thin solid colors.$grey-100;
gap: variables.$spacing-md;
padding: 0 variables.$spacing-xl;
margin: 0 -1 * variables.$spacing-xl;
}

.inputFormModalFooter > * {
// need to overwrite the margin of the button wrapper used within create controls
// TODO refactor so this isn't necessary
margin-top: variables.$spacing-lg !important;
}

.warningBox {
margin-bottom: variables.$spacing-lg;
background-color: colors.$blue-50;
}

.warningBoxContainer {
display: flex;
gap: variables.$spacing-md;
align-items: center;
}
Original file line number Diff line number Diff line change
@@ -1,54 +1,106 @@
import { faGear } from "@fortawesome/free-solid-svg-icons";
import { faClose, faGear } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useState } from "react";
import { FormattedMessage } from "react-intl";
import { useMemo, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { useLocalStorage } from "react-use";

import { Button } from "components/ui/Button";
import { CodeEditor } from "components/ui/CodeEditor";
import { Modal, ModalBody, ModalFooter } from "components/ui/Modal";
import { InfoBox } from "components/ui/InfoBox";
import { Modal, ModalBody } from "components/ui/Modal";
import { Tooltip } from "components/ui/Tooltip";

import { useConnectorBuilderState } from "services/connectorBuilder/ConnectorBuilderStateService";
import { ConnectorForm } from "views/Connector/ConnectorForm";

import styles from "./ConfigMenu.module.scss";
import { ConfigMenuErrorBoundaryComponent } from "./ConfigMenuErrorBoundary";

interface ConfigMenuProps {
className?: string;
}

export const ConfigMenu: React.FC<ConfigMenuProps> = ({ className }) => {
const [isOpen, setIsOpen] = useState(false);
const { configString, setConfigString } = useConnectorBuilderState();
const { formatMessage } = useIntl();
const { configString, setConfigString, jsonManifest, editorView, setEditorView } = useConnectorBuilderState();

const [showInputsWarning, setShowInputsWarning] = useLocalStorage<boolean>("connectorBuilderInputsWarning", true);

const formValues = useMemo(() => {
return { connectionConfiguration: JSON.parse(configString) };
}, [configString]);

const switchToYaml = () => {
setEditorView("yaml");
setIsOpen(false);
};

return (
<>
<Button
className={className}
size="sm"
variant="secondary"
onClick={() => setIsOpen(true)}
icon={<FontAwesomeIcon className={styles.icon} icon={faGear} />}
/>
{isOpen && (
<Tooltip
control={
<Button
size="sm"
variant="secondary"
onClick={() => setIsOpen(true)}
disabled={!jsonManifest.spec}
icon={<FontAwesomeIcon className={styles.icon} icon={faGear} />}
/>
}
placement={editorView === "yaml" ? "left" : "top"}
containerClassName={className}
>
{jsonManifest.spec ? (
<FormattedMessage id="connectorBuilder.inputsTooltip" />
) : editorView === "ui" ? (
<FormattedMessage id="connectorBuilder.inputsNoSpecUITooltip" />
) : (
<FormattedMessage id="connectorBuilder.inputsNoSpecYAMLTooltip" />
)}
</Tooltip>
{isOpen && jsonManifest.spec && (
<Modal
size="lg"
onClose={() => setIsOpen(false)}
title={<FormattedMessage id="connectorBuilder.configMenuTitle" />}
>
<ModalBody className={styles.modalContent}>
<CodeEditor
value={configString}
language="json"
theme="airbyte-light"
onChange={(val: string | undefined) => {
setConfigString(val ?? "");
}}
/>
<ModalBody>
<ConfigMenuErrorBoundaryComponent currentView={editorView} closeAndSwitchToYaml={switchToYaml}>
<>
{showInputsWarning && (
<InfoBox className={styles.warningBox}>
<div className={styles.warningBoxContainer}>
<div>
<FormattedMessage id="connectorBuilder.inputsFormWarning" />
</div>
<Button
onClick={() => {
setShowInputsWarning(false);
}}
variant="clear"
icon={<FontAwesomeIcon icon={faClose} />}
/>
</div>
</InfoBox>
)}
<ConnectorForm
formType="source"
bodyClassName={styles.formContent}
footerClassName={styles.inputFormModalFooter}
selectedConnectorDefinitionSpecification={jsonManifest.spec}
formValues={formValues}
onSubmit={async (values) => {
setConfigString(JSON.stringify(values.connectionConfiguration, null, 2) ?? "");
setIsOpen(false);
}}
onCancel={() => {
setIsOpen(false);
}}
submitLabel={formatMessage({ id: "connectorBuilder.saveInputsForm" })}
/>
</>
</ConfigMenuErrorBoundaryComponent>
</ModalBody>
<ModalFooter>
<Button onClick={() => setIsOpen(false)}>
<FormattedMessage id="connectorBuilder.configMenuConfirm" />
</Button>
</ModalFooter>
</Modal>
)}
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@use "scss/colors";
@use "scss/variables";

.errorContent {
display: flex;
flex-direction: column;
gap: variables.$spacing-lg;
align-items: flex-end;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React from "react";
import { FormattedMessage } from "react-intl";

import { Button } from "components/ui/Button";
import { InfoBox } from "components/ui/InfoBox";

import { FormBuildError, isFormBuildError } from "core/form/FormBuildError";
import { EditorView } from "services/connectorBuilder/ConnectorBuilderStateService";

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

interface ApiErrorBoundaryState {
error?: string | FormBuildError;
}

interface ApiErrorBoundaryProps {
closeAndSwitchToYaml: () => void;
currentView: EditorView;
}

export class ConfigMenuErrorBoundaryComponent extends React.Component<
React.PropsWithChildren<ApiErrorBoundaryProps>,
ApiErrorBoundaryState
> {
state: ApiErrorBoundaryState = {};

static getDerivedStateFromError(error: { message: string; __type?: string }): ApiErrorBoundaryState {
if (isFormBuildError(error)) {
return { error };
}

return { error: error.message };
}
render(): React.ReactNode {
const { children, currentView, closeAndSwitchToYaml } = this.props;
const { error } = this.state;

if (!error) {
return children;
}
return (
<div className={styles.errorContent}>
<InfoBox>
<FormattedMessage
id="connectorBuilder.inputsError"
values={{ error: typeof error === "string" ? error : <FormattedMessage id={error.message} /> }}
/>{" "}
<a
target="_blank"
href="https://docs.airbyte.com/connector-development/connector-specification-reference"
rel="noreferrer"
>
<FormattedMessage id="connectorBuilder.inputsErrorDocumentation" />
</a>
</InfoBox>
<Button onClick={closeAndSwitchToYaml}>
{currentView === "ui" ? (
<FormattedMessage id="connectorBuilder.goToYaml" />
) : (
<FormattedMessage id="connectorBuilder.close" />
)}
</Button>
</div>
);
}
}
6 changes: 4 additions & 2 deletions airbyte-webapp/src/core/domain/connector/connector.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { DestinationDefinitionSpecificationRead, SourceDefinitionSpecificationRead } from "core/request/AirbyteClient";

import { DEV_IMAGE_TAG } from "./constants";
import { isSource, isSourceDefinition, isSourceDefinitionSpecification } from "./source";
import { ConnectorDefinition, ConnectorDefinitionSpecification, ConnectorT } from "./types";
import { ConnectorDefinition, ConnectorT } from "./types";

export class Connector {
static id(connector: ConnectorDefinition): string {
Expand All @@ -26,7 +28,7 @@ export class ConnectorHelper {
}

export class ConnectorSpecification {
static id(connector: ConnectorDefinitionSpecification): string {
static id(connector: DestinationDefinitionSpecificationRead | SourceDefinitionSpecificationRead): string {
return isSourceDefinitionSpecification(connector)
? connector.sourceDefinitionId
: connector.destinationDefinitionId;
Expand Down
23 changes: 21 additions & 2 deletions airbyte-webapp/src/core/domain/connector/source.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import { SourceDefinitionRead, SourceDefinitionSpecificationRead, SourceRead } from "../../request/AirbyteClient";
import { ConnectorDefinition, ConnectorDefinitionSpecification, ConnectorT } from "./types";
import {
DestinationDefinitionSpecificationRead,
SourceDefinitionRead,
SourceDefinitionSpecificationRead,
SourceRead,
} from "../../request/AirbyteClient";
import {
ConnectorDefinition,
ConnectorDefinitionSpecification,
ConnectorT,
SourceDefinitionSpecificationDraft,
} from "./types";

export function isSource(connector: ConnectorT): connector is SourceRead {
return "sourceId" in connector;
Expand All @@ -15,5 +25,14 @@ export function isSourceDefinitionSpecification(
return (connector as SourceDefinitionSpecificationRead).sourceDefinitionId !== undefined;
}

export function isSourceDefinitionSpecificationDraft(
connector: ConnectorDefinitionSpecification | SourceDefinitionSpecificationDraft
): connector is SourceDefinitionSpecificationDraft {
return (
(connector as SourceDefinitionSpecificationRead).sourceDefinitionId === undefined &&
(connector as DestinationDefinitionSpecificationRead).destinationDefinitionId === undefined
);
}

// eslint-disable-next-line no-template-curly-in-string
export const SOURCE_NAMESPACE_TAG = "${SOURCE_NAMESPACE}";
5 changes: 5 additions & 0 deletions airbyte-webapp/src/core/domain/connector/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import {

export type ConnectorDefinition = SourceDefinitionReadWithLatestTag | DestinationDefinitionReadWithLatestTag;

export type SourceDefinitionSpecificationDraft = Pick<
SourceDefinitionSpecificationRead,
"documentationUrl" | "connectionSpecification" | "authSpecification" | "advancedAuth"
>;

export type ConnectorDefinitionSpecification =
| DestinationDefinitionSpecificationRead
| SourceDefinitionSpecificationRead;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { SourceDefinitionSpecificationDraft } from "core/domain/connector";

import { ConnectorManifest } from "../../request/ConnectorManifest";

// Patching this type as required until the upstream schema is updated
export interface PatchedConnectorManifest extends ConnectorManifest {
spec?: SourceDefinitionSpecificationDraft;
}
11 changes: 10 additions & 1 deletion airbyte-webapp/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -638,7 +638,7 @@

"connectorBuilder.downloadYaml": "Download Config",
"connectorBuilder.testButton": "Test",
"connectorBuilder.configMenuTitle": "Configure Test Input",
"connectorBuilder.configMenuTitle": "User Inputs",
"connectorBuilder.configMenuConfirm": "Confirm",
"connectorBuilder.recordsTab": "Records",
"connectorBuilder.requestTab": "Request",
Expand Down Expand Up @@ -680,6 +680,15 @@
"connectorBuilder.key": "key",
"connectorBuilder.value": "value",
"connectorBuilder.addKeyValue": "Add",
"connectorBuilder.saveInputsForm": "Save",
"connectorBuilder.inputsFormWarning": "User inputs are not saved with the connector. They are required in order to test your streams, and will be asked to the end user in order to setup this connector",
"connectorBuilder.inputsError": "User inputs form could not be rendered: <b>{error}</b>. Make sure the spec in the YAML conforms to the specified standard.",
"connectorBuilder.inputsErrorDocumentation": "Check out the documentation",
"connectorBuilder.goToYaml": "Switch to YAML view",
"connectorBuilder.close": "Close",
"connectorBuilder.inputsTooltip": "Define test inputs to check whether the connector configuration works",
"connectorBuilder.inputsNoSpecUITooltip": "Add User Input fields to allow setting test inputs",
"connectorBuilder.inputsNoSpecYAMLTooltip": "Add a spec to your manifest to allow setting test inputs",

"jobs.noAttemptsFailure": "Failed to start job.",

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useLocalStorage } from "react-use";

import { BuilderFormValues, convertToManifest } from "components/connectorBuilder/types";

import { PatchedConnectorManifest } from "core/domain/connectorBuilder/PatchedConnectorManifest";
import { StreamReadRequestBodyConfig, StreamsListReadStreamsItem } from "core/request/ConnectorBuilderClient";
import { ConnectorManifest } from "core/request/ConnectorManifest";

Expand All @@ -26,12 +27,12 @@ const DEFAULT_JSON_MANIFEST_VALUES: ConnectorManifest = {
streams: [],
};

type EditorView = "ui" | "yaml";
export type EditorView = "ui" | "yaml";
export type BuilderView = "global" | number;

interface Context {
builderFormValues: BuilderFormValues;
jsonManifest: ConnectorManifest;
jsonManifest: PatchedConnectorManifest;
yamlManifest: string;
yamlEditorIsMounted: boolean;
yamlIsValid: boolean;
Expand Down
Loading

0 comments on commit 7d49233

Please sign in to comment.