Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🪟🎉 Connector builder authentication #20645

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useEffect } from "react";

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

import { BuilderFormValues } from "../types";
import { builderFormValidationSchema, BuilderFormValues } from "../types";
import styles from "./Builder.module.scss";
import { BuilderSidebar } from "./BuilderSidebar";
import { GlobalConfigView } from "./GlobalConfigView";
Expand All @@ -29,7 +29,7 @@ function getView(selectedView: BuilderView) {
export const Builder: React.FC<BuilderProps> = ({ values, toggleYamlEditor }) => {
const { setBuilderFormValues, selectedView } = useConnectorBuilderState();
useEffect(() => {
setBuilderFormValues(values);
setBuilderFormValues(values, builderFormValidationSchema.isValidSync(values));
}, [values, setBuilderFormValues]);

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { useField } from "formik";
import React from "react";

import GroupControls from "components/GroupControls";
import { ControlLabels } from "components/LabeledControl";
import { DropDown } from "components/ui/DropDown";

interface Option {
label: string;
value: string;
default?: object;
}

interface OneOfOption {
label: string; // label shown in the dropdown menu
typeValue: string; // value to set on the `type` field for this component - should match the oneOf type definition
default?: object; // default values for the path
children?: React.ReactNode;
}

interface BuilderOneOfProps {
options: OneOfOption[];
path: string; // path to the oneOf component in the json schema
label: string;
tooltip: string;
}

export const BuilderOneOf: React.FC<BuilderOneOfProps> = ({ options, path, label, tooltip }) => {
const [, , oneOfPathHelpers] = useField(path);
const typePath = `${path}.type`;
const [typePathField] = useField(typePath);
const value = typePathField.value;

const selectedOption = options.find((option) => option.typeValue === value);

return (
<GroupControls
label={<ControlLabels label={label} infoTooltipContent={tooltip} />}
dropdown={
<DropDown
{...typePathField}
options={options.map((option) => {
return { label: option.label, value: option.typeValue, default: option.default };
})}
value={value ?? options[0].typeValue}
onChange={(selectedOption: Option) => {
if (selectedOption.value === value) {
return;
}
// clear all values for this oneOf and set selected option and default values
oneOfPathHelpers.setValue({
type: selectedOption.value,
...selectedOption.default,
});
}}
/>
}
>
{selectedOption?.children}
</GroupControls>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
} from "services/connectorBuilder/ConnectorBuilderStateService";

import { DownloadYamlButton } from "../DownloadYamlButton";
import { BuilderFormValues } from "../types";
import { BuilderFormValues, getInferredInputs } from "../types";
import { useBuilderErrors } from "../useBuilderErrors";
import { AddStreamButton } from "./AddStreamButton";
import styles from "./BuilderSidebar.module.scss";
Expand Down Expand Up @@ -115,7 +115,10 @@ export const BuilderSidebar: React.FC<BuilderSidebarProps> = ({ className, toggl
onClick={() => handleViewSelect("inputs")}
>
<FontAwesomeIcon icon={faUser} />
<FormattedMessage id="connectorBuilder.userInputs" values={{ number: values.inputs.length }} />
<FormattedMessage
id="connectorBuilder.userInputs"
values={{ number: values.inputs.length + getInferredInputs(values).length }}
/>
</ViewSelectButton>

<div className={styles.streamsHeader}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import { useIntl } from "react-intl";
import { BuilderCard } from "./BuilderCard";
import { BuilderConfigView } from "./BuilderConfigView";
import { BuilderField } from "./BuilderField";
import { BuilderOneOf } from "./BuilderOneOf";
import { BuilderTitle } from "./BuilderTitle";
import styles from "./GlobalConfigView.module.scss";
import { UserInputField } from "./UserInputField";

export const GlobalConfigView: React.FC = () => {
const { formatMessage } = useIntl();
Expand All @@ -16,6 +18,70 @@ export const GlobalConfigView: React.FC = () => {
<BuilderCard className={styles.content}>
<BuilderField type="string" path="global.urlBase" label="API URL" tooltip="Base URL of the source API" />
</BuilderCard>
<BuilderCard>
<BuilderOneOf
path="global.authenticator"
label="Authentication"
tooltip="Authentication method to use for requests sent to the API"
options={[
{ label: "No Auth", typeValue: "NoAuth" },
{
label: "API Key",
typeValue: "ApiKeyAuthenticator",
default: {
api_token: "{{ config['api_key'] }}",
},
children: (
<>
<BuilderField
type="string"
path="global.authenticator.header"
label="Header"
tooltip="HTTP header which should be set to the API Key"
/>
<UserInputField
label="API Key"
tooltip="The API key issued by the service. Fill it in in the user inputs"
/>
</>
),
},
{
label: "Bearer",
typeValue: "BearerAuthenticator",
default: {
api_token: "{{ config['api_key'] }}",
},
children: (
<UserInputField
label="API Key"
tooltip="The API key issued by the service. Fill it in in the user inputs"
/>
),
},
{
label: "Basic HTTP",
typeValue: "BasicHttpAuthenticator",
default: {
username: "{{ config['username'] }}",
password: "{{ config['password'] }}",
},
children: (
<>
<UserInputField
label="Username"
tooltip="The username for the login. Fill it in in the user inputs"
/>
<UserInputField
label="Password"
tooltip="The password for the login. Fill it in in the user inputs"
/>
</>
),
},
]}
/>
</BuilderCard>
</BuilderConfigView>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { FormattedMessage, useIntl } from "react-intl";
import { useEffectOnce } from "react-use";
import * as yup from "yup";

import Label from "components/Label";
import { Button } from "components/ui/Button";
import { Card } from "components/ui/Card";
import { InfoBox } from "components/ui/InfoBox";
Expand All @@ -16,7 +15,7 @@ import { Text } from "components/ui/Text";

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

import { BuilderFormInput } from "../types";
import { BuilderFormInput, BuilderFormValues, getInferredInputs } from "../types";
import { BuilderConfigView } from "./BuilderConfigView";
import { BuilderField } from "./BuilderField";
import styles from "./InputsView.module.scss";
Expand All @@ -30,6 +29,7 @@ interface InputInEditing {
isNew?: boolean;
showDefaultValueField: boolean;
type: typeof supportedTypes[number];
isInferredInputOverride: boolean;
}

function sluggify(str: string) {
Expand All @@ -44,10 +44,14 @@ function newInputInEditing(): InputInEditing {
isNew: true,
showDefaultValueField: false,
type: "string",
isInferredInputOverride: false,
};
}

function formInputToInputInEditing({ key, definition, required }: BuilderFormInput): InputInEditing {
function formInputToInputInEditing(
{ key, definition, required }: BuilderFormInput,
isInferredInputOverride: boolean
): InputInEditing {
const supportedType = supportedTypes.find((type) => type === definition.type) || "unknown";
return {
key,
Expand All @@ -56,6 +60,7 @@ function formInputToInputInEditing({ key, definition, required }: BuilderFormInp
isNew: false,
showDefaultValueField: Boolean(definition.default),
type: supportedType !== "unknown" && definition.enum ? "enum" : supportedType,
isInferredInputOverride,
};
}

Expand All @@ -79,9 +84,14 @@ function inputInEditingToFormInput({

export const InputsView: React.FC = () => {
const { formatMessage } = useIntl();
const { values, setFieldValue } = useFormikContext<BuilderFormValues>();
const [inputs, , helpers] = useField<BuilderFormInput[]>("inputs");
const [inputInEditing, setInputInEditing] = useState<InputInEditing | undefined>(undefined);
const usedKeys = useMemo(() => inputs.value.map((input) => input.key), [inputs.value]);
const inferredInputs = useMemo(() => getInferredInputs(values), [values]);
const usedKeys = useMemo(
() => [...inputs.value, ...inferredInputs].map((input) => input.key),
[inputs.value, inferredInputs]
);
const inputInEditValidation = useMemo(
() =>
yup.object().shape({
Expand All @@ -99,29 +109,20 @@ export const InputsView: React.FC = () => {
}),
[inputInEditing?.isNew, inputInEditing?.key, usedKeys]
);

return (
<BuilderConfigView heading={formatMessage({ id: "connectorBuilder.inputsTitle" })}>
<Text centered className={styles.inputsDescription}>
<FormattedMessage id="connectorBuilder.inputsDescription" />
</Text>
{inputs.value.length > 0 && (
{(inputs.value.length > 0 || inferredInputs.length > 0) && (
<Card withPadding className={styles.inputsCard}>
<ol className={styles.list}>
{inferredInputs.map((input) => (
<InputItem key={input.key} input={input} setInputInEditing={setInputInEditing} isInferredInput />
))}
{inputs.value.map((input) => (
<li className={styles.listItem} key={input.key}>
<Label className={styles.itemLabel}>{input.definition.title || input.key}</Label>
<Button
className={styles.itemButton}
size="sm"
variant="secondary"
aria-label="Edit"
onClick={() => {
setInputInEditing(formInputToInputInEditing(input));
}}
>
<FontAwesomeIcon className={styles.icon} icon={faGear} />
</Button>
</li>
<InputItem key={input.key} input={input} setInputInEditing={setInputInEditing} isInferredInput={false} />
))}
</ol>
</Card>
Expand All @@ -142,12 +143,16 @@ export const InputsView: React.FC = () => {
initialValues={inputInEditing}
validationSchema={inputInEditValidation}
onSubmit={(values: InputInEditing) => {
const newInput = inputInEditingToFormInput(values);
helpers.setValue(
inputInEditing.isNew
? [...inputs.value, newInput]
: inputs.value.map((input) => (input.key === inputInEditing.key ? newInput : input))
);
if (values.isInferredInputOverride) {
setFieldValue(`inferredInputOverrides.${values.key}`, values.definition);
} else {
const newInput = inputInEditingToFormInput(values);
helpers.setValue(
inputInEditing.isNew
? [...inputs.value, newInput]
: inputs.value.map((input) => (input.key === inputInEditing.key ? newInput : input))
);
}
setInputInEditing(undefined);
}}
>
Expand Down Expand Up @@ -179,7 +184,9 @@ const InputModal = ({
onDelete: () => void;
onClose: () => void;
}) => {
const isInferredInputOverride = inputInEditing.isInferredInputOverride;
const { isValid, values, setFieldValue, setTouched } = useFormikContext<InputInEditing>();

const { formatMessage } = useIntl();
useEffectOnce(() => {
// key input is always touched so errors are shown right away as it will be auto-set by the user changing the title
Expand All @@ -202,7 +209,9 @@ const InputModal = ({
path="definition.title"
type="string"
onChange={(newValue) => {
setFieldValue("key", sluggify(newValue || ""), true);
if (!isInferredInputOverride) {
setFieldValue("key", sluggify(newValue || ""), true);
}
}}
label={formatMessage({ id: "connectorBuilder.inputModal.inputName" })}
tooltip={formatMessage({ id: "connectorBuilder.inputModal.inputNameTooltip" })}
Expand All @@ -226,7 +235,7 @@ const InputModal = ({
label={formatMessage({ id: "connectorBuilder.inputModal.description" })}
tooltip={formatMessage({ id: "connectorBuilder.inputModal.descriptionTooltip" })}
/>
{values.type !== "unknown" ? (
{values.type !== "unknown" && !isInferredInputOverride ? (
<>
<BuilderField
path="type"
Expand Down Expand Up @@ -287,12 +296,16 @@ const InputModal = ({
</>
) : (
<InfoBox>
<FormattedMessage id="connectorBuilder.inputModal.unsupportedInput" />
{isInferredInputOverride ? (
<FormattedMessage id="connectorBuilder.inputModal.inferredInputMessage" />
) : (
<FormattedMessage id="connectorBuilder.inputModal.unsupportedInput" />
)}
</InfoBox>
)}
</ModalBody>
<ModalFooter>
{!inputInEditing.isNew && (
{!inputInEditing.isNew && !inputInEditing.isInferredInputOverride && (
<div className={styles.deleteButtonContainer}>
<Button variant="danger" type="button" onClick={onDelete}>
<FormattedMessage id="form.delete" />
Expand All @@ -310,3 +323,30 @@ const InputModal = ({
</Modal>
);
};

const InputItem = ({
input,
setInputInEditing,
isInferredInput,
}: {
input: BuilderFormInput;
setInputInEditing: (inputInEditing: InputInEditing) => void;
isInferredInput: boolean;
}): JSX.Element => {
return (
<li className={styles.listItem}>
<div className={styles.itemLabel}>{input.definition.title || input.key}</div>
<Button
className={styles.itemButton}
size="sm"
variant="secondary"
aria-label="Edit"
onClick={() => {
setInputInEditing(formInputToInputInEditing(input, isInferredInput));
}}
>
<FontAwesomeIcon className={styles.icon} icon={faGear} />
</Button>
</li>
);
};
Loading