From 7d49233c871fa80ae060add8c3462b7df7af85d1 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 20 Dec 2022 10:28:58 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=AA=9F=F0=9F=8E=89=20Connector=20builder:?= =?UTF-8?q?=20Integrate=20connector=20form=20for=20test=20input=20(#20385)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- .../StreamTestingPanel/ConfigMenu.module.scss | 32 +++++- .../StreamTestingPanel/ConfigMenu.tsx | 108 +++++++++++++----- .../ConfigMenuErrorBoundary.module.scss | 9 ++ .../ConfigMenuErrorBoundary.tsx | 66 +++++++++++ .../src/core/domain/connector/connector.ts | 6 +- .../src/core/domain/connector/source.ts | 23 +++- .../src/core/domain/connector/types.ts | 5 + .../PatchedConnectorManifest.ts | 8 ++ airbyte-webapp/src/locales/en.json | 11 +- .../ConnectorBuilderStateService.tsx | 5 +- .../Connector/ConnectorForm/ConnectorForm.tsx | 20 +++- .../Connector/ConnectorForm/FormRoot.tsx | 69 ++++++----- .../components/CreateControls.module.scss | 16 ++- .../components/CreateControls.tsx | 32 +++--- .../Sections/auth/AuthButton.test.tsx | 30 +++-- .../components/Sections/auth/AuthButton.tsx | 12 +- .../components/Sections/auth/AuthSection.tsx | 8 +- .../ConnectorForm/connectorFormContext.tsx | 14 ++- .../Connector/ConnectorForm/index.stories.tsx | 12 +- .../ConnectorForm/useAuthentication.tsx | 8 +- .../Connector/ConnectorForm/useBuildForm.tsx | 46 +++++--- 21 files changed, 412 insertions(+), 128 deletions(-) create mode 100644 airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/ConfigMenuErrorBoundary.module.scss create mode 100644 airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/ConfigMenuErrorBoundary.tsx create mode 100644 airbyte-webapp/src/core/domain/connectorBuilder/PatchedConnectorManifest.ts diff --git a/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/ConfigMenu.module.scss b/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/ConfigMenu.module.scss index b9017a3447a5..d55c44c2758c 100644 --- a/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/ConfigMenu.module.scss +++ b/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/ConfigMenu.module.scss @@ -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; } diff --git a/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/ConfigMenu.tsx b/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/ConfigMenu.tsx index b9685aad35b6..21f45bb9e26a 100644 --- a/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/ConfigMenu.tsx +++ b/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/ConfigMenu.tsx @@ -1,15 +1,19 @@ -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; @@ -17,38 +21,86 @@ interface ConfigMenuProps { export const ConfigMenu: React.FC = ({ className }) => { const [isOpen, setIsOpen] = useState(false); - const { configString, setConfigString } = useConnectorBuilderState(); + const { formatMessage } = useIntl(); + const { configString, setConfigString, jsonManifest, editorView, setEditorView } = useConnectorBuilderState(); + + const [showInputsWarning, setShowInputsWarning] = useLocalStorage("connectorBuilderInputsWarning", true); + + const formValues = useMemo(() => { + return { connectionConfiguration: JSON.parse(configString) }; + }, [configString]); + + const switchToYaml = () => { + setEditorView("yaml"); + setIsOpen(false); + }; return ( <> - - )} diff --git a/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/ConfigMenuErrorBoundary.module.scss b/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/ConfigMenuErrorBoundary.module.scss new file mode 100644 index 000000000000..0cc75c2324e9 --- /dev/null +++ b/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/ConfigMenuErrorBoundary.module.scss @@ -0,0 +1,9 @@ +@use "scss/colors"; +@use "scss/variables"; + +.errorContent { + display: flex; + flex-direction: column; + gap: variables.$spacing-lg; + align-items: flex-end; +} diff --git a/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/ConfigMenuErrorBoundary.tsx b/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/ConfigMenuErrorBoundary.tsx new file mode 100644 index 000000000000..972c92c90a2e --- /dev/null +++ b/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/ConfigMenuErrorBoundary.tsx @@ -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, + 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 ( +
+ + }} + />{" "} + + + + + +
+ ); + } +} diff --git a/airbyte-webapp/src/core/domain/connector/connector.ts b/airbyte-webapp/src/core/domain/connector/connector.ts index 6f0749c43952..d9452f34c211 100644 --- a/airbyte-webapp/src/core/domain/connector/connector.ts +++ b/airbyte-webapp/src/core/domain/connector/connector.ts @@ -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 { @@ -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; diff --git a/airbyte-webapp/src/core/domain/connector/source.ts b/airbyte-webapp/src/core/domain/connector/source.ts index 9ecb47a49962..ae43b1df16e4 100644 --- a/airbyte-webapp/src/core/domain/connector/source.ts +++ b/airbyte-webapp/src/core/domain/connector/source.ts @@ -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; @@ -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}"; diff --git a/airbyte-webapp/src/core/domain/connector/types.ts b/airbyte-webapp/src/core/domain/connector/types.ts index d52ec5feb3c2..444af7d0e98b 100644 --- a/airbyte-webapp/src/core/domain/connector/types.ts +++ b/airbyte-webapp/src/core/domain/connector/types.ts @@ -10,6 +10,11 @@ import { export type ConnectorDefinition = SourceDefinitionReadWithLatestTag | DestinationDefinitionReadWithLatestTag; +export type SourceDefinitionSpecificationDraft = Pick< + SourceDefinitionSpecificationRead, + "documentationUrl" | "connectionSpecification" | "authSpecification" | "advancedAuth" +>; + export type ConnectorDefinitionSpecification = | DestinationDefinitionSpecificationRead | SourceDefinitionSpecificationRead; diff --git a/airbyte-webapp/src/core/domain/connectorBuilder/PatchedConnectorManifest.ts b/airbyte-webapp/src/core/domain/connectorBuilder/PatchedConnectorManifest.ts new file mode 100644 index 000000000000..2e8dd79b575f --- /dev/null +++ b/airbyte-webapp/src/core/domain/connectorBuilder/PatchedConnectorManifest.ts @@ -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; +} diff --git a/airbyte-webapp/src/locales/en.json b/airbyte-webapp/src/locales/en.json index f34580fab3ae..022869453100 100644 --- a/airbyte-webapp/src/locales/en.json +++ b/airbyte-webapp/src/locales/en.json @@ -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", @@ -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: {error}. 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.", diff --git a/airbyte-webapp/src/services/connectorBuilder/ConnectorBuilderStateService.tsx b/airbyte-webapp/src/services/connectorBuilder/ConnectorBuilderStateService.tsx index 69b01d4e4013..0f4ec2c67548 100644 --- a/airbyte-webapp/src/services/connectorBuilder/ConnectorBuilderStateService.tsx +++ b/airbyte-webapp/src/services/connectorBuilder/ConnectorBuilderStateService.tsx @@ -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"; @@ -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; diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/ConnectorForm.tsx b/airbyte-webapp/src/views/Connector/ConnectorForm/ConnectorForm.tsx index 5e40157abfd7..4db39a5fdb47 100644 --- a/airbyte-webapp/src/views/Connector/ConnectorForm/ConnectorForm.tsx +++ b/airbyte-webapp/src/views/Connector/ConnectorForm/ConnectorForm.tsx @@ -3,7 +3,11 @@ import React, { useCallback } from "react"; import { FormChangeTracker } from "components/common/FormChangeTracker"; -import { ConnectorDefinition, ConnectorDefinitionSpecification } from "core/domain/connector"; +import { + ConnectorDefinition, + ConnectorDefinitionSpecification, + SourceDefinitionSpecificationDraft, +} from "core/domain/connector"; import { FormikPatch } from "core/form/FormikPatch"; import { CheckConnectionRead } from "core/request/AirbyteClient"; import { useFormChangeTrackerService, useUniqueFormId } from "hooks/services/FormChangeTracker"; @@ -16,8 +20,11 @@ import { useBuildForm } from "./useBuildForm"; export interface ConnectorFormProps { formType: "source" | "destination"; formId?: string; - selectedConnectorDefinition: ConnectorDefinition; - selectedConnectorDefinitionSpecification: ConnectorDefinitionSpecification; + /** + * Definition of the connector might not be available if it's not released but only exists in frontend heap + */ + selectedConnectorDefinition?: ConnectorDefinition; + selectedConnectorDefinitionSpecification: ConnectorDefinitionSpecification | SourceDefinitionSpecificationDraft; onSubmit: (values: ConnectorFormValues) => Promise; isEditMode?: boolean; formValues?: Partial; @@ -25,6 +32,13 @@ export interface ConnectorFormProps { errorMessage?: React.ReactNode; successMessage?: React.ReactNode; connectorId?: string; + footerClassName?: string; + bodyClassName?: string; + submitLabel?: string; + /** + * Called in case the user cancels the form - if not provided, no cancel button is rendered + */ + onCancel?: () => void; isTestConnectionInProgress?: boolean; onStopTesting?: () => void; diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/FormRoot.tsx b/airbyte-webapp/src/views/Connector/ConnectorForm/FormRoot.tsx index d11cb3684cd0..8c471a0bb268 100644 --- a/airbyte-webapp/src/views/Connector/ConnectorForm/FormRoot.tsx +++ b/airbyte-webapp/src/views/Connector/ConnectorForm/FormRoot.tsx @@ -17,6 +17,13 @@ interface FormRootProps { successMessage?: React.ReactNode; onRetest?: () => void; onStopTestingConnector?: () => void; + submitLabel?: string; + footerClassName?: string; + bodyClassName?: string; + /** + * Called in case the user cancels the form - if not provided, no cancel button is rendered + */ + onCancel?: () => void; } export const FormRoot: React.FC = ({ @@ -27,38 +34,48 @@ export const FormRoot: React.FC = ({ errorMessage, connectionTestSuccess, onStopTestingConnector, + submitLabel, + footerClassName, + bodyClassName, + onCancel, }) => { const { dirty, isSubmitting, isValid } = useFormikContext(); const { resetConnectorForm, isEditMode, formType } = useConnectorForm(); return (
- - {isEditMode ? ( - { - resetConnectorForm(); - }} - successMessage={successMessage} - /> - ) : ( - - )} +
+ +
+
+ {isEditMode ? ( + { + resetConnectorForm(); + }} + successMessage={successMessage} + /> + ) : ( + + )} +
); }; diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/components/CreateControls.module.scss b/airbyte-webapp/src/views/Connector/ConnectorForm/components/CreateControls.module.scss index e6049bbfa295..8b96a3953685 100644 --- a/airbyte-webapp/src/views/Connector/ConnectorForm/components/CreateControls.module.scss +++ b/airbyte-webapp/src/views/Connector/ConnectorForm/components/CreateControls.module.scss @@ -1,3 +1,15 @@ -.submitButton { - margin-left: auto; +@use "scss/variables"; + +.controlContainer { + margin-top: 34px; + display: flex; + align-items: center; + justify-content: flex-end; +} + +.buttonContainer { + display: flex; + flex: 0 0 auto; + align-self: flex-end; + gap: variables.$spacing-sm; } diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/components/CreateControls.tsx b/airbyte-webapp/src/views/Connector/ConnectorForm/components/CreateControls.tsx index 689bf4fdca4f..c4eda4898499 100644 --- a/airbyte-webapp/src/views/Connector/ConnectorForm/components/CreateControls.tsx +++ b/airbyte-webapp/src/views/Connector/ConnectorForm/components/CreateControls.tsx @@ -1,6 +1,5 @@ import React from "react"; import { FormattedMessage } from "react-intl"; -import styled from "styled-components"; import { Button } from "components/ui/Button"; @@ -11,6 +10,11 @@ import TestingConnectionSuccess from "./TestingConnectionSuccess"; interface CreateControlProps { formType: "source" | "destination"; + /** + * Called in case the user cancels the form - if not provided, no cancel button is rendered + */ + onCancel?: () => void; + submitLabel?: string; isSubmitting: boolean; errorMessage?: React.ReactNode; connectionTestSuccess?: boolean; @@ -19,13 +23,6 @@ interface CreateControlProps { onCancelTesting?: () => void; } -const ButtonContainer = styled.div` - margin-top: 34px; - display: flex; - align-items: center; - justify-content: space-between; -`; - const CreateControls: React.FC = ({ isTestConnectionInProgress, isSubmitting, @@ -33,6 +30,8 @@ const CreateControls: React.FC = ({ connectionTestSuccess, errorMessage, onCancelTesting, + onCancel, + submitLabel, }) => { if (isSubmitting) { return ; @@ -43,12 +42,19 @@ const CreateControls: React.FC = ({ } return ( - +
{errorMessage && } - - +
+ {onCancel && ( + + )} + +
+
); }; diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/components/Sections/auth/AuthButton.test.tsx b/airbyte-webapp/src/views/Connector/ConnectorForm/components/Sections/auth/AuthButton.test.tsx index 7ad988453e15..0d35ea01ee9d 100644 --- a/airbyte-webapp/src/views/Connector/ConnectorForm/components/Sections/auth/AuthButton.test.tsx +++ b/airbyte-webapp/src/views/Connector/ConnectorForm/components/Sections/auth/AuthButton.test.tsx @@ -55,9 +55,9 @@ describe("auth button", () => { it("initially renders with correct message and no status message", () => { // no auth errors mockUseConnectorForm.mockImplementationOnce(() => { - const { selectedConnectorDefinitionSpecification, selectedConnectorDefinition } = baseUseConnectorFormValues; + const { selectedConnectorDefinition } = baseUseConnectorFormValues; - return { selectedConnectorDefinitionSpecification, selectedConnectorDefinition }; + return { selectedConnectorDefinition }; }); // not done @@ -70,7 +70,11 @@ describe("auth button", () => { render( - + ); @@ -90,9 +94,9 @@ describe("auth button", () => { it("after successful authentication, it renders with correct message and success message", () => { // no auth errors mockUseConnectorForm.mockImplementationOnce(() => { - const { selectedConnectorDefinitionSpecification, selectedConnectorDefinition } = baseUseConnectorFormValues; + const { selectedConnectorDefinition } = baseUseConnectorFormValues; - return { selectedConnectorDefinitionSpecification, selectedConnectorDefinition }; + return { selectedConnectorDefinition }; }); // done @@ -105,7 +109,11 @@ describe("auth button", () => { render( - + ); @@ -123,9 +131,9 @@ describe("auth button", () => { mockUseAuthentication.mockReturnValue({ hiddenAuthFieldErrors: { field: "form.empty.error" } }); mockUseConnectorForm.mockImplementationOnce(() => { - const { selectedConnectorDefinitionSpecification, selectedConnectorDefinition } = baseUseConnectorFormValues; + const { selectedConnectorDefinition } = baseUseConnectorFormValues; - return { selectedConnectorDefinitionSpecification, selectedConnectorDefinition }; + return { selectedConnectorDefinition }; }); // not done @@ -138,7 +146,11 @@ describe("auth button", () => { render( - + ); diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/components/Sections/auth/AuthButton.tsx b/airbyte-webapp/src/views/Connector/ConnectorForm/components/Sections/auth/AuthButton.tsx index ed93c3bff8d8..df7b33ab31e5 100644 --- a/airbyte-webapp/src/views/Connector/ConnectorForm/components/Sections/auth/AuthButton.tsx +++ b/airbyte-webapp/src/views/Connector/ConnectorForm/components/Sections/auth/AuthButton.tsx @@ -5,7 +5,7 @@ import { FormattedMessage } from "react-intl"; import { Button } from "components/ui/Button"; import { Text } from "components/ui/Text"; -import { ConnectorSpecification } from "core/domain/connector"; +import { ConnectorDefinitionSpecification, ConnectorSpecification } from "core/domain/connector"; import { ConnectorIds } from "utils/connectors"; import { useConnectorForm } from "../../../connectorFormContext"; @@ -46,16 +46,18 @@ function getAuthenticateMessageId(connectorDefinitionId: string): string { return "connectorForm.authenticate"; } -export const AuthButton: React.FC = () => { - const { selectedConnectorDefinition, selectedConnectorDefinitionSpecification } = useConnectorForm(); +export const AuthButton: React.FC<{ + selectedConnectorDefinitionSpecification: ConnectorDefinitionSpecification; +}> = ({ selectedConnectorDefinitionSpecification }) => { + const { selectedConnectorDefinition } = useConnectorForm(); const { hiddenAuthFieldErrors } = useAuthentication(); const authRequiredError = Object.values(hiddenAuthFieldErrors).includes("form.empty.error"); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const { loading, done, run, hasRun } = useFormikOauthAdapter(selectedConnectorDefinitionSpecification); - if (!selectedConnectorDefinitionSpecification) { - console.error("Entered non-auth flow while no connector is selected"); + if (!selectedConnectorDefinition) { + console.error("Entered non-auth flow while no supported connector is selected"); return null; } diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/components/Sections/auth/AuthSection.tsx b/airbyte-webapp/src/views/Connector/ConnectorForm/components/Sections/auth/AuthSection.tsx index 1c89cf2b1e62..12b38123404d 100644 --- a/airbyte-webapp/src/views/Connector/ConnectorForm/components/Sections/auth/AuthSection.tsx +++ b/airbyte-webapp/src/views/Connector/ConnectorForm/components/Sections/auth/AuthSection.tsx @@ -1,15 +1,21 @@ import React from "react"; +import { isSourceDefinitionSpecificationDraft } from "core/domain/connector/source"; import { FeatureItem, IfFeatureEnabled } from "hooks/services/Feature"; +import { useConnectorForm } from "views/Connector/ConnectorForm/connectorFormContext"; import { SectionContainer } from "../SectionContainer"; import { AuthButton } from "./AuthButton"; export const AuthSection: React.FC = () => { + const { selectedConnectorDefinitionSpecification } = useConnectorForm(); + if (isSourceDefinitionSpecificationDraft(selectedConnectorDefinitionSpecification)) { + return null; + } return ( - + ); diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/connectorFormContext.tsx b/airbyte-webapp/src/views/Connector/ConnectorForm/connectorFormContext.tsx index 11d2f766536f..a222e14c59c9 100644 --- a/airbyte-webapp/src/views/Connector/ConnectorForm/connectorFormContext.tsx +++ b/airbyte-webapp/src/views/Connector/ConnectorForm/connectorFormContext.tsx @@ -2,7 +2,11 @@ import { useFormikContext } from "formik"; import React, { useContext, useMemo } from "react"; import { AnySchema } from "yup"; -import { ConnectorDefinition, ConnectorDefinitionSpecification } from "core/domain/connector"; +import { + ConnectorDefinition, + ConnectorDefinitionSpecification, + SourceDefinitionSpecificationDraft, +} from "core/domain/connector"; import { ConnectorFormValues } from "./types"; @@ -10,8 +14,8 @@ interface ConnectorFormContext { formType: "source" | "destination"; getValues: (values: ConnectorFormValues) => ConnectorFormValues; resetConnectorForm: () => void; - selectedConnectorDefinition: ConnectorDefinition; - selectedConnectorDefinitionSpecification: ConnectorDefinitionSpecification; + selectedConnectorDefinition?: ConnectorDefinition; + selectedConnectorDefinitionSpecification: ConnectorDefinitionSpecification | SourceDefinitionSpecificationDraft; isEditMode?: boolean; validationSchema: AnySchema; connectorId?: string; @@ -28,11 +32,11 @@ export const useConnectorForm = (): ConnectorFormContext => { }; interface ConnectorFormContextProviderProps { - selectedConnectorDefinition: ConnectorDefinition; + selectedConnectorDefinition?: ConnectorDefinition; formType: "source" | "destination"; isEditMode?: boolean; getValues: (values: ConnectorFormValues) => ConnectorFormValues; - selectedConnectorDefinitionSpecification: ConnectorDefinitionSpecification; + selectedConnectorDefinitionSpecification: ConnectorDefinitionSpecification | SourceDefinitionSpecificationDraft; validationSchema: AnySchema; connectorId?: string; } diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/index.stories.tsx b/airbyte-webapp/src/views/Connector/ConnectorForm/index.stories.tsx index 9ca0171a17b6..d391f35ee0a2 100644 --- a/airbyte-webapp/src/views/Connector/ConnectorForm/index.stories.tsx +++ b/airbyte-webapp/src/views/Connector/ConnectorForm/index.stories.tsx @@ -3,7 +3,7 @@ import withMock from "storybook-addon-mock"; import { Card } from "components/ui/Card"; -import { ConnectorSpecification } from "core/domain/connector"; +import { ConnectorDefinitionSpecification, ConnectorSpecification } from "core/domain/connector"; import { isSourceDefinitionSpecification } from "core/domain/connector/source"; import { ConnectorForm } from "./ConnectorForm"; @@ -46,13 +46,11 @@ export default { } as ComponentMeta; const Template: ComponentStory = (args) => { + const selectedSpecification = args.selectedConnectorDefinitionSpecification as ConnectorDefinitionSpecification; // Hack to allow devs to not specify sourceDefinitionId - if ( - args.selectedConnectorDefinitionSpecification && - !ConnectorSpecification.id(args.selectedConnectorDefinitionSpecification) - ) { - if (isSourceDefinitionSpecification(args.selectedConnectorDefinitionSpecification)) { - args.selectedConnectorDefinitionSpecification.sourceDefinitionId = TempConnector.sourceDefinitionId; + if (!ConnectorSpecification.id(selectedSpecification)) { + if (isSourceDefinitionSpecification(selectedSpecification)) { + selectedSpecification.sourceDefinitionId = TempConnector.sourceDefinitionId; } } diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/useAuthentication.tsx b/airbyte-webapp/src/views/Connector/ConnectorForm/useAuthentication.tsx index 7ec5a265e1b6..9610895dcf42 100644 --- a/airbyte-webapp/src/views/Connector/ConnectorForm/useAuthentication.tsx +++ b/airbyte-webapp/src/views/Connector/ConnectorForm/useAuthentication.tsx @@ -3,6 +3,7 @@ import { JSONSchema7 } from "json-schema"; import { useCallback, useMemo } from "react"; import { ConnectorSpecification } from "core/domain/connector"; +import { isSourceDefinitionSpecificationDraft } from "core/domain/connector/source"; import { useAppMonitoringService } from "hooks/services/AppMonitoringService"; import { FeatureItem, useFeature } from "hooks/services/Feature"; @@ -155,7 +156,10 @@ export const useAuthentication = (): AuthenticationHook => { console.error(`getValues in useAuthentication failed.`, e); trackError(e, { id: "useAuthentication.getValues", - connector: connectorSpec ? ConnectorSpecification.id(connectorSpec) : null, + connector: + connectorSpec && !isSourceDefinitionSpecificationDraft(connectorSpec) + ? ConnectorSpecification.id(connectorSpec) + : null, }); return values; } @@ -178,7 +182,7 @@ export const useAuthentication = (): AuthenticationHook => { const implicitAuthFieldPaths = useMemo( () => [ // Fields from `advancedAuth` connectors - ...(advancedAuth + ...(advancedAuth && !isSourceDefinitionSpecificationDraft(connectorSpec) ? Object.values(serverProvidedOauthPaths(connectorSpec)).map((f) => makeConnectionConfigurationPath(f.path_in_connector_config) ) diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/useBuildForm.tsx b/airbyte-webapp/src/views/Connector/ConnectorForm/useBuildForm.tsx index fdaaa82cbbb6..e2804de6cc05 100644 --- a/airbyte-webapp/src/views/Connector/ConnectorForm/useBuildForm.tsx +++ b/airbyte-webapp/src/views/Connector/ConnectorForm/useBuildForm.tsx @@ -3,7 +3,12 @@ import { useMemo } from "react"; import { useIntl } from "react-intl"; import { AnySchema } from "yup"; -import { ConnectorDefinitionSpecification, ConnectorSpecification } from "core/domain/connector"; +import { + ConnectorDefinitionSpecification, + ConnectorSpecification, + SourceDefinitionSpecificationDraft, +} from "core/domain/connector"; +import { isSourceDefinitionSpecificationDraft } from "core/domain/connector/source"; import { FormBuildError, isFormBuildError } from "core/form/FormBuildError"; import { jsonSchemaToFormBlock } from "core/form/schemaToFormBlock"; import { buildYupFormForJsonSchema } from "core/form/schemaToYup"; @@ -41,28 +46,34 @@ function setDefaultValues(formGroup: FormGroupItem, values: Record ): BuildFormHook { const { formatMessage } = useIntl(); + const isDraft = isSourceDefinitionSpecificationDraft(selectedConnectorDefinitionSpecification); try { - const jsonSchema: JSONSchema7 = useMemo( - () => ({ + const jsonSchema: JSONSchema7 = useMemo(() => { + const schema: JSONSchema7 = { type: "object", properties: { - name: { - type: "string", - title: formatMessage({ id: `form.${formType}Name` }), - description: formatMessage({ id: `form.${formType}Name.message` }), - }, connectionConfiguration: selectedConnectorDefinitionSpecification.connectionSpecification as JSONSchema7Definition, }, - required: ["name"], - }), - [formType, formatMessage, selectedConnectorDefinitionSpecification.connectionSpecification] - ); + }; + if (isDraft) { + return schema; + } + // schema.properties gets defined right above + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + schema.properties!.name = { + type: "string", + title: formatMessage({ id: `form.${formType}Name` }), + description: formatMessage({ id: `form.${formType}Name.message` }), + }; + schema.required = ["name"]; + return schema; + }, [formType, formatMessage, isDraft, selectedConnectorDefinitionSpecification.connectionSpecification]); const formFields = useMemo(() => jsonSchemaToFormBlock(jsonSchema), [jsonSchema]); @@ -71,7 +82,7 @@ export function useBuildForm( } const startValues = useMemo(() => { - if (isEditMode) { + if (isEditMode || isDraft) { return { name: "", connectionConfiguration: {}, @@ -87,7 +98,7 @@ export function useBuildForm( setDefaultValues(formFields, baseValues as Record); return baseValues; - }, [formFields, initialValues, isEditMode]); + }, [formFields, initialValues, isDraft, isEditMode]); const validationSchema = useMemo(() => buildYupFormForJsonSchema(jsonSchema, formFields), [formFields, jsonSchema]); return { @@ -98,7 +109,10 @@ export function useBuildForm( } catch (e) { // catch and re-throw form-build errors to enrich them with the connector id if (isFormBuildError(e)) { - throw new FormBuildError(e.message, ConnectorSpecification.id(selectedConnectorDefinitionSpecification)); + throw new FormBuildError( + e.message, + isDraft ? undefined : ConnectorSpecification.id(selectedConnectorDefinitionSpecification) + ); } throw e; }