Skip to content

Commit

Permalink
[Connector Builder] Configuration UI MVP (#20008)
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
  • Loading branch information
lmossman committed Dec 12, 2022
1 parent 2f306ea commit 7afc689
Show file tree
Hide file tree
Showing 42 changed files with 1,132 additions and 176 deletions.
1 change: 1 addition & 0 deletions airbyte-webapp/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ storybook-static/
# Ignore generated API clients, since they're automatically generated
/src/core/request/AirbyteClient.ts
/src/core/request/ConnectorBuilderClient.ts
/src/core/request/ConnectorManifest.ts
17 changes: 17 additions & 0 deletions airbyte-webapp/orval.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,21 @@ export default defineConfig({
},
},
},
connectorManifest: {
input: "./src/services/connectorBuilder/connector_manifest_openapi.yaml",
output: {
target: "./src/core/request/ConnectorManifest.ts",
prettier: true,
override: {
header: (info) => [
`eslint-disable`,
`Generated by orval 🍺`,
`Do not edit manually. Run "npm run generate-client" instead.`,
...(info.title ? [info.title] : []),
...(info.description ? [info.description] : []),
...(info.version ? [`OpenAPI spec version: ${info.version}`] : []),
],
},
},
},
});
52 changes: 52 additions & 0 deletions airbyte-webapp/public/images/octavia/pointing.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
@use "scss/variables";
@use "scss/colors";

$buttonWidth: 26px;

.body {
display: flex;
flex-direction: column;
gap: variables.$spacing-xl;
}

.addButton {
width: $buttonWidth;
height: $buttonWidth !important;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
padding: 9px !important;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { Form, Formik, useField } from "formik";
import { useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";

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

import { FormikPatch } from "core/form/FormikPatch";

import { ReactComponent as PlusIcon } from "../../connection/ConnectionOnboarding/plusIcon.svg";
import { BuilderStream } from "../types";
import styles from "./AddStreamButton.module.scss";
import { BuilderField } from "./BuilderField";

interface AddStreamValues {
streamName: string;
urlPath: string;
}

interface AddStreamButtonProps {
onAddStream: (addedStreamNum: number) => void;
}

export const AddStreamButton: React.FC<AddStreamButtonProps> = ({ onAddStream }) => {
const { formatMessage } = useIntl();
const [isOpen, setIsOpen] = useState(false);
const [streamsField, , helpers] = useField<BuilderStream[]>("streams");
const numStreams = streamsField.value.length;

return (
<>
<Button
className={styles.addButton}
onClick={() => {
setIsOpen(true);
}}
icon={<PlusIcon />}
/>
{isOpen && (
<Formik
initialValues={{ streamName: "", urlPath: "" }}
onSubmit={(values: AddStreamValues) => {
helpers.setValue([
...streamsField.value,
{
name: values.streamName,
urlPath: values.urlPath,
fieldPointer: [],
httpMethod: "GET",
},
]);
setIsOpen(false);
onAddStream(numStreams);
}}
>
<>
<FormikPatch />
<Modal
size="sm"
title={<FormattedMessage id="connectorBuilder.addStreamModal.title" />}
onClose={() => {
setIsOpen(false);
}}
>
<Form>
<ModalBody className={styles.body}>
<BuilderField
path="streamName"
type="text"
label={formatMessage({ id: "connectorBuilder.addStreamModal.streamNameLabel" })}
tooltip={formatMessage({ id: "connectorBuilder.addStreamModal.streamNameTooltip" })}
/>
<BuilderField
path="urlPath"
type="text"
label={formatMessage({ id: "connectorBuilder.addStreamModal.urlPathLabel" })}
tooltip={formatMessage({ id: "connectorBuilder.addStreamModal.urlPathTooltip" })}
/>
</ModalBody>
<ModalFooter>
<Button
variant="secondary"
type="reset"
onClick={() => {
setIsOpen(false);
}}
>
<FormattedMessage id="form.cancel" />
</Button>
<Button type="submit">
<FormattedMessage id="form.create" />
</Button>
</ModalFooter>
</Form>
</Modal>
</>
</Formik>
)}
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
@use "scss/variables";
@use "scss/colors";
@use "scss/mixins";

.container {
height: 100%;
display: flex;
}

.sidebar {
@include mixins.right-shadow;

width: 200px;
flex: 0 0 auto;
background-color: colors.$white;
}

.form {
flex: 1;
padding: variables.$spacing-xl;
display: flex;
flex-direction: column;
gap: variables.$spacing-xl;
}
31 changes: 31 additions & 0 deletions airbyte-webapp/src/components/connectorBuilder/Builder/Builder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Form } from "formik";
import { useEffect } from "react";

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

import { BuilderFormValues } from "../types";
import styles from "./Builder.module.scss";
import { BuilderSidebar } from "./BuilderSidebar";
import { GlobalConfigView } from "./GlobalConfigView";
import { StreamConfigView } from "./StreamConfigView";

interface BuilderProps {
values: BuilderFormValues;
toggleYamlEditor: () => void;
}

export const Builder: React.FC<BuilderProps> = ({ values, toggleYamlEditor }) => {
const { setBuilderFormValues, selectedView } = useConnectorBuilderState();
useEffect(() => {
setBuilderFormValues(values);
}, [values, setBuilderFormValues]);

return (
<div className={styles.container}>
<BuilderSidebar className={styles.sidebar} toggleYamlEditor={toggleYamlEditor} />
<Form className={styles.form}>
{selectedView === "global" ? <GlobalConfigView /> : <StreamConfigView streamNum={selectedView} />}
</Form>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@use "scss/variables";

.card {
padding: variables.$spacing-xl;
display: flex;
flex-direction: column;
gap: variables.$spacing-xl;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from "react";

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

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

export const BuilderCard: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
return <Card className={styles.card}>{children}</Card>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
@use "scss/variables";
@use "scss/colors";

.error {
margin-top: variables.$spacing-sm;
color: colors.$red;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
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";
import { Input } from "components/ui/Input";
import { TagInput } from "components/ui/TagInput";
import { Text } from "components/ui/Text";

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

interface EnumFieldProps {
options: string[];
value: string;
setValue: (value: string) => void;
error: boolean;
}

interface ArrayFieldProps {
name: string;
value: string[];
setValue: (value: string[]) => void;
error: boolean;
}

interface BaseFieldProps {
// path to the location in the Connector Manifest schema which should be set by this component
path: string;
label: string;
tooltip?: string;
optional?: boolean;
}

type BuilderFieldProps = BaseFieldProps & ({ type: "text" } | { type: "array" } | { type: "enum"; options: string[] });

const EnumField: React.FC<EnumFieldProps> = ({ options, value, setValue, error, ...props }) => {
return (
<DropDown
{...props}
options={options.map((option) => {
return { label: option, value: option };
})}
onChange={(selected) => selected && setValue(selected.value)}
value={value}
error={error}
/>
);
};

const ArrayField: React.FC<ArrayFieldProps> = ({ name, value, setValue, error }) => {
return <TagInput name={name} fieldValue={value} onChange={(value) => setValue(value)} error={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 hasError = !!meta.error && meta.touched;

return (
<ControlLabels className={styles.container} label={label} infoTooltipContent={tooltip} optional={optional}>
{props.type === "text" && <Input {...field} type={props.type} value={field.value ?? ""} error={hasError} />}
{props.type === "array" && (
<ArrayField name={path} value={field.value ?? []} setValue={helpers.setValue} error={hasError} />
)}
{props.type === "enum" && (
<EnumField
options={props.options}
value={field.value ?? props.options[0]}
setValue={helpers.setValue}
error={hasError}
/>
)}
{hasError && (
<Text className={styles.error}>
<FormattedMessage id={meta.error} />
</Text>
)}
</ControlLabels>
);
};
Loading

0 comments on commit 7afc689

Please sign in to comment.