diff --git a/airbyte-webapp/src/components/connectorBuilder/Builder/Builder.tsx b/airbyte-webapp/src/components/connectorBuilder/Builder/Builder.tsx index 6596005a883c..b042352273ce 100644 --- a/airbyte-webapp/src/components/connectorBuilder/Builder/Builder.tsx +++ b/airbyte-webapp/src/components/connectorBuilder/Builder/Builder.tsx @@ -2,7 +2,7 @@ import { Form } from "formik"; import debounce from "lodash/debounce"; import { useEffect, useMemo } from "react"; -import { BuilderView, useConnectorBuilderState } from "services/connectorBuilder/ConnectorBuilderStateService"; +import { BuilderView, useConnectorBuilderFormState } from "services/connectorBuilder/ConnectorBuilderStateService"; import { builderFormValidationSchema, BuilderFormValues } from "../types"; import styles from "./Builder.module.scss"; @@ -29,7 +29,7 @@ function getView(selectedView: BuilderView, hasMultipleStreams: boolean) { } export const Builder: React.FC = ({ values, toggleYamlEditor, validateForm }) => { - const { setBuilderFormValues, selectedView } = useConnectorBuilderState(); + const { setBuilderFormValues, selectedView } = useConnectorBuilderFormState(); const debouncedSetBuilderFormValues = useMemo( () => debounce((values) => { diff --git a/airbyte-webapp/src/components/connectorBuilder/Builder/BuilderSidebar.tsx b/airbyte-webapp/src/components/connectorBuilder/Builder/BuilderSidebar.tsx index 7bd54599ea6b..d8197ce8ff98 100644 --- a/airbyte-webapp/src/components/connectorBuilder/Builder/BuilderSidebar.tsx +++ b/airbyte-webapp/src/components/connectorBuilder/Builder/BuilderSidebar.tsx @@ -2,6 +2,7 @@ import { faSliders, faUser } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import classnames from "classnames"; import { useFormikContext } from "formik"; +import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; import Indicator from "components/Indicator"; @@ -10,7 +11,7 @@ import { Heading } from "components/ui/Heading"; import { Text } from "components/ui/Text"; import { useConfirmationModalService } from "hooks/services/ConfirmationModal"; -import { BuilderView, useConnectorBuilderState } from "services/connectorBuilder/ConnectorBuilderStateService"; +import { BuilderView, useConnectorBuilderFormState } from "services/connectorBuilder/ConnectorBuilderStateService"; import { DownloadYamlButton } from "../DownloadYamlButton"; import { BuilderFormValues, DEFAULT_BUILDER_FORM_VALUES, getInferredInputs } from "../types"; @@ -52,11 +53,11 @@ interface BuilderSidebarProps { toggleYamlEditor: () => void; } -export const BuilderSidebar: React.FC = ({ className, toggleYamlEditor }) => { +export const BuilderSidebar: React.FC = React.memo(({ className, toggleYamlEditor }) => { const { formatMessage } = useIntl(); const { hasErrors } = useBuilderErrors(); const { openConfirmationModal, closeConfirmationModal } = useConfirmationModalService(); - const { yamlManifest, selectedView, setSelectedView, setTestStreamIndex } = useConnectorBuilderState(); + const { yamlManifest, selectedView, setSelectedView } = useConnectorBuilderFormState(); const { values, setValues } = useFormikContext(); const handleResetForm = () => { openConfirmationModal({ @@ -72,9 +73,6 @@ export const BuilderSidebar: React.FC = ({ className, toggl }; const handleViewSelect = (selectedView: BuilderView) => { setSelectedView(selectedView); - if (selectedView !== "global" && selectedView !== "inputs") { - setTestStreamIndex(selectedView); - } }; return ( @@ -150,4 +148,4 @@ export const BuilderSidebar: React.FC = ({ className, toggl ); -}; +}); diff --git a/airbyte-webapp/src/components/connectorBuilder/Builder/StreamConfigView.tsx b/airbyte-webapp/src/components/connectorBuilder/Builder/StreamConfigView.tsx index 96d344b2a4f2..ee15ef0cae42 100644 --- a/airbyte-webapp/src/components/connectorBuilder/Builder/StreamConfigView.tsx +++ b/airbyte-webapp/src/components/connectorBuilder/Builder/StreamConfigView.tsx @@ -10,7 +10,7 @@ import { CodeEditor } from "components/ui/CodeEditor"; import { Text } from "components/ui/Text"; import { useConfirmationModalService } from "hooks/services/ConfirmationModal"; -import { BuilderView, useConnectorBuilderState } from "services/connectorBuilder/ConnectorBuilderStateService"; +import { BuilderView, useConnectorBuilderFormState } from "services/connectorBuilder/ConnectorBuilderStateService"; import { BuilderStream } from "../types"; import { AddStreamButton } from "./AddStreamButton"; @@ -33,7 +33,7 @@ export const StreamConfigView: React.FC = ({ streamNum, h const [field, , helpers] = useField("streams"); const [selectedTab, setSelectedTab] = useState<"configuration" | "schema">("configuration"); const { openConfirmationModal, closeConfirmationModal } = useConfirmationModalService(); - const { setSelectedView, setTestStreamIndex } = useConnectorBuilderState(); + const { setSelectedView } = useConnectorBuilderFormState(); const streamPath = `streams[${streamNum}]`; const streamFieldPath = (fieldPath: string) => `${streamPath}.${fieldPath}`; @@ -49,7 +49,6 @@ export const StreamConfigView: React.FC = ({ streamNum, h const viewToSelect: BuilderView = updatedStreams.length === 0 ? "global" : streamToSelect; helpers.setValue(updatedStreams); setSelectedView(viewToSelect); - setTestStreamIndex(streamToSelect); closeConfirmationModal(); }, }); @@ -80,7 +79,6 @@ export const StreamConfigView: React.FC = ({ streamNum, h { setSelectedView(addedStreamNum); - setTestStreamIndex(addedStreamNum); }} initialValues={field.value[streamNum]} button={ diff --git a/airbyte-webapp/src/components/connectorBuilder/DownloadYamlButton.tsx b/airbyte-webapp/src/components/connectorBuilder/DownloadYamlButton.tsx index 55a6d1f0170a..ffe02b1fd34c 100644 --- a/airbyte-webapp/src/components/connectorBuilder/DownloadYamlButton.tsx +++ b/airbyte-webapp/src/components/connectorBuilder/DownloadYamlButton.tsx @@ -5,7 +5,7 @@ import { FormattedMessage } from "react-intl"; import { Button } from "components/ui/Button"; import { Tooltip } from "components/ui/Tooltip"; -import { useConnectorBuilderState } from "services/connectorBuilder/ConnectorBuilderStateService"; +import { useConnectorBuilderFormState } from "services/connectorBuilder/ConnectorBuilderStateService"; import { downloadFile } from "utils/file"; import styles from "./DownloadYamlButton.module.scss"; @@ -18,7 +18,7 @@ interface DownloadYamlButtonProps { } export const DownloadYamlButton: React.FC = ({ className, yaml, yamlIsValid }) => { - const { editorView } = useConnectorBuilderState(); + const { editorView } = useConnectorBuilderFormState(); const { hasErrors, validateAndTouch } = useBuilderErrors(); const downloadYaml = () => { diff --git a/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/ConfigMenu.tsx b/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/ConfigMenu.tsx index d9e98fbe4e72..1455e91c3166 100644 --- a/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/ConfigMenu.tsx +++ b/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/ConfigMenu.tsx @@ -12,7 +12,8 @@ import { Tooltip } from "components/ui/Tooltip"; import { SourceDefinitionSpecificationDraft } from "core/domain/connector"; import { StreamReadRequestBodyConfig } from "core/request/ConnectorBuilderClient"; -import { useConnectorBuilderState } from "services/connectorBuilder/ConnectorBuilderStateService"; +import { useConnectorBuilderTestState } from "services/connectorBuilder/ConnectorBuilderStateService"; +import { useConnectorBuilderFormState } from "services/connectorBuilder/ConnectorBuilderStateService"; import { ConnectorForm } from "views/Connector/ConnectorForm"; import styles from "./ConfigMenu.module.scss"; @@ -20,14 +21,16 @@ import { ConfigMenuErrorBoundaryComponent } from "./ConfigMenuErrorBoundary"; interface ConfigMenuProps { className?: string; - configJsonErrors: number; + testInputJsonErrors: number; isOpen: boolean; setIsOpen: (open: boolean) => void; } -export const ConfigMenu: React.FC = ({ className, configJsonErrors, isOpen, setIsOpen }) => { +export const ConfigMenu: React.FC = ({ className, testInputJsonErrors, isOpen, setIsOpen }) => { const { formatMessage } = useIntl(); - const { configJson, setConfigJson, jsonManifest, editorView, setEditorView } = useConnectorBuilderState(); + const { jsonManifest, editorView, setEditorView } = useConnectorBuilderFormState(); + + const { testInputJson, setTestInputJson } = useConnectorBuilderTestState(); const [showInputsWarning, setShowInputsWarning] = useLocalStorage("connectorBuilderInputsWarning", true); @@ -64,8 +67,8 @@ export const ConfigMenu: React.FC = ({ className, configJsonErr > - {configJsonErrors > 0 && ( - + {testInputJsonErrors > 0 && ( + )} } @@ -110,16 +113,16 @@ export const ConfigMenu: React.FC = ({ className, configJsonErr bodyClassName={styles.formContent} footerClassName={styles.inputFormModalFooter} selectedConnectorDefinitionSpecification={connectorDefinitionSpecification} - formValues={{ connectionConfiguration: configJson }} + formValues={{ connectionConfiguration: testInputJson }} onSubmit={async (values) => { - setConfigJson(values.connectionConfiguration as StreamReadRequestBodyConfig); + setTestInputJson(values.connectionConfiguration as StreamReadRequestBodyConfig); setIsOpen(false); }} onCancel={() => { setIsOpen(false); }} onReset={() => { - setConfigJson({}); + setTestInputJson({}); }} submitLabel={formatMessage({ id: "connectorBuilder.saveInputsForm" })} /> diff --git a/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamSelector.tsx b/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamSelector.tsx index 20d3e8189a48..198397229593 100644 --- a/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamSelector.tsx +++ b/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamSelector.tsx @@ -5,7 +5,10 @@ import { useIntl } from "react-intl"; import { Heading } from "components/ui/Heading"; import { ListBox, ListBoxControlButtonProps } from "components/ui/ListBox"; -import { useConnectorBuilderState } from "services/connectorBuilder/ConnectorBuilderStateService"; +import { + useConnectorBuilderTestState, + useConnectorBuilderFormState, +} from "services/connectorBuilder/ConnectorBuilderStateService"; import { ReactComponent as CaretDownIcon } from "../../ui/ListBox/CaretDownIcon.svg"; import styles from "./StreamSelector.module.scss"; @@ -27,7 +30,8 @@ const ControlButton: React.FC> = ({ selectedOp export const StreamSelector: React.FC = ({ className }) => { const { formatMessage } = useIntl(); - const { streams, selectedView, testStreamIndex, setSelectedView, setTestStreamIndex } = useConnectorBuilderState(); + const { selectedView, setSelectedView } = useConnectorBuilderFormState(); + const { streams, testStreamIndex, setTestStreamIndex } = useConnectorBuilderTestState(); const options = streams.map((stream) => { const label = stream.name && stream.name.trim() ? capitalize(stream.name) : formatMessage({ id: "connectorBuilder.emptyName" }); @@ -39,7 +43,7 @@ export const StreamSelector: React.FC = ({ className }) => if (selectedStreamIndex >= 0) { setTestStreamIndex(selectedStreamIndex); - if (selectedView !== "global" && selectedStreamIndex >= 0) { + if (selectedView !== "global") { setSelectedView(selectedStreamIndex); } } diff --git a/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamTestButton.tsx b/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamTestButton.tsx index 30fe955bbbff..a1a2caf051da 100644 --- a/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamTestButton.tsx +++ b/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamTestButton.tsx @@ -7,27 +7,27 @@ 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 { useConnectorBuilderFormState } from "services/connectorBuilder/ConnectorBuilderStateService"; import { useBuilderErrors } from "../useBuilderErrors"; import styles from "./StreamTestButton.module.scss"; interface StreamTestButtonProps { readStream: () => void; - hasConfigJsonErrors: boolean; + hasTestInputJsonErrors: boolean; setTestInputOpen: (open: boolean) => void; } export const StreamTestButton: React.FC = ({ readStream, - hasConfigJsonErrors, + hasTestInputJsonErrors, setTestInputOpen, }) => { - const { editorView, yamlIsValid } = useConnectorBuilderState(); + const { editorView, yamlIsValid } = useConnectorBuilderFormState(); const { hasErrors, validateAndTouch } = useBuilderErrors(); const handleClick = () => { - if (hasConfigJsonErrors) { + if (hasTestInputJsonErrors) { setTestInputOpen(true); return; } @@ -49,7 +49,7 @@ export const StreamTestButton: React.FC = ({ tooltipContent = ; } - if ((editorView === "ui" && hasErrors(false)) || hasConfigJsonErrors) { + if ((editorView === "ui" && hasErrors(false)) || hasTestInputJsonErrors) { showWarningIcon = true; tooltipContent = ; } diff --git a/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamTester.tsx b/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamTester.tsx index dc8027a6d0a9..d91026295938 100644 --- a/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamTester.tsx +++ b/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamTester.tsx @@ -6,7 +6,10 @@ import { Spinner } from "components/ui/Spinner"; import { Text } from "components/ui/Text"; import { useReadStream } from "services/connectorBuilder/ConnectorBuilderApiService"; -import { useConnectorBuilderState } from "services/connectorBuilder/ConnectorBuilderStateService"; +import { + useConnectorBuilderTestState, + useConnectorBuilderFormState, +} from "services/connectorBuilder/ConnectorBuilderStateService"; import { LogsDisplay } from "./LogsDisplay"; import { ResultDisplay } from "./ResultDisplay"; @@ -14,11 +17,12 @@ import { StreamTestButton } from "./StreamTestButton"; import styles from "./StreamTester.module.scss"; export const StreamTester: React.FC<{ - hasConfigJsonErrors: boolean; + hasTestInputJsonErrors: boolean; setTestInputOpen: (open: boolean) => void; -}> = ({ hasConfigJsonErrors, setTestInputOpen }) => { +}> = ({ hasTestInputJsonErrors, setTestInputOpen }) => { const { formatMessage } = useIntl(); - const { jsonManifest, configJson, streams, testStreamIndex } = useConnectorBuilderState(); + const { jsonManifest } = useConnectorBuilderFormState(); + const { streams, testInputJson, testStreamIndex } = useConnectorBuilderTestState(); const { data: streamReadData, refetch: readStream, @@ -28,7 +32,7 @@ export const StreamTester: React.FC<{ } = useReadStream({ manifest: jsonManifest, stream: streams[testStreamIndex]?.name, - config: configJson, + config: testInputJson, }); const [logsFlex, setLogsFlex] = useState(0); @@ -60,7 +64,7 @@ export const StreamTester: React.FC<{ diff --git a/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamTestingPanel.tsx b/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamTestingPanel.tsx index b3bff5d70d38..69bfc96573fc 100644 --- a/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamTestingPanel.tsx +++ b/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamTestingPanel.tsx @@ -10,7 +10,10 @@ import { jsonSchemaToFormBlock } from "core/form/schemaToFormBlock"; import { buildYupFormForJsonSchema } from "core/form/schemaToYup"; import { StreamReadRequestBodyConfig } from "core/request/ConnectorBuilderClient"; import { Spec } from "core/request/ConnectorManifest"; -import { useConnectorBuilderState } from "services/connectorBuilder/ConnectorBuilderStateService"; +import { + useConnectorBuilderTestState, + useConnectorBuilderFormState, +} from "services/connectorBuilder/ConnectorBuilderStateService"; import { links } from "utils/links"; import { ConfigMenu } from "./ConfigMenu"; @@ -20,13 +23,13 @@ import styles from "./StreamTestingPanel.module.scss"; const EMPTY_SCHEMA = {}; -function useConfigJsonErrors(configJson: StreamReadRequestBodyConfig, spec?: Spec): number { +function useTestInputJsonErrors(testInputJson: StreamReadRequestBodyConfig, spec?: Spec): number { return useMemo(() => { try { const jsonSchema = spec && spec.connection_specification ? spec.connection_specification : EMPTY_SCHEMA; const formFields = jsonSchemaToFormBlock(jsonSchema); const validationSchema = buildYupFormForJsonSchema(jsonSchema, formFields); - validationSchema.validateSync(configJson, { abortEarly: false }); + validationSchema.validateSync(testInputJson, { abortEarly: false }); return 0; } catch (e) { if (ValidationError.isError(e)) { @@ -34,14 +37,15 @@ function useConfigJsonErrors(configJson: StreamReadRequestBodyConfig, spec?: Spe } return 1; } - }, [configJson, spec]); + }, [testInputJson, spec]); } export const StreamTestingPanel: React.FC = () => { const [isTestInputOpen, setTestInputOpen] = useState(false); - const { jsonManifest, configJson, streamListErrorMessage, yamlEditorIsMounted } = useConnectorBuilderState(); + const { jsonManifest, yamlEditorIsMounted } = useConnectorBuilderFormState(); + const { testInputJson, streamListErrorMessage } = useConnectorBuilderTestState(); - const configJsonErrors = useConfigJsonErrors(configJson, jsonManifest.spec); + const testInputJsonErrors = useTestInputJsonErrors(testInputJson, jsonManifest.spec); if (!yamlEditorIsMounted) { return ( @@ -57,7 +61,7 @@ export const StreamTestingPanel: React.FC = () => {
@@ -72,7 +76,7 @@ export const StreamTestingPanel: React.FC = () => { {hasStreams && streamListErrorMessage === undefined && (
- 0} setTestInputOpen={setTestInputOpen} /> + 0} setTestInputOpen={setTestInputOpen} />
)} {hasStreams && streamListErrorMessage !== undefined && ( diff --git a/airbyte-webapp/src/components/connectorBuilder/YamlEditor/YamlEditor.tsx b/airbyte-webapp/src/components/connectorBuilder/YamlEditor/YamlEditor.tsx index 39353461c95f..f62b0c1a2514 100644 --- a/airbyte-webapp/src/components/connectorBuilder/YamlEditor/YamlEditor.tsx +++ b/airbyte-webapp/src/components/connectorBuilder/YamlEditor/YamlEditor.tsx @@ -9,7 +9,7 @@ import { CodeEditor } from "components/ui/CodeEditor"; import { ConnectorManifest } from "core/request/ConnectorManifest"; import { useConfirmationModalService } from "hooks/services/ConfirmationModal"; -import { useConnectorBuilderState } from "services/connectorBuilder/ConnectorBuilderStateService"; +import { useConnectorBuilderFormState } from "services/connectorBuilder/ConnectorBuilderStateService"; import { UiYamlToggleButton } from "../Builder/UiYamlToggleButton"; import { DownloadYamlButton } from "../DownloadYamlButton"; @@ -31,7 +31,7 @@ export const YamlEditor: React.FC = ({ toggleYamlEditor }) => { setYamlEditorIsMounted, setYamlIsValid, setJsonManifest, - } = useConnectorBuilderState(); + } = useConnectorBuilderFormState(); const [yamlValue, setYamlValue] = useState(yamlManifest); // debounce the setJsonManifest calls so that it doesnt result in a network call for every keystroke diff --git a/airbyte-webapp/src/components/connectorBuilder/useBuilderErrors.ts b/airbyte-webapp/src/components/connectorBuilder/useBuilderErrors.ts index 88bbf1411e12..5ee008f51c81 100644 --- a/airbyte-webapp/src/components/connectorBuilder/useBuilderErrors.ts +++ b/airbyte-webapp/src/components/connectorBuilder/useBuilderErrors.ts @@ -3,13 +3,13 @@ import { FormikErrors, useFormikContext } from "formik"; import intersection from "lodash/intersection"; import { useCallback } from "react"; -import { BuilderView, useConnectorBuilderState } from "services/connectorBuilder/ConnectorBuilderStateService"; +import { BuilderView, useConnectorBuilderFormState } from "services/connectorBuilder/ConnectorBuilderStateService"; import { BuilderFormValues } from "./types"; export const useBuilderErrors = () => { const { touched, errors, validateForm, setFieldTouched } = useFormikContext(); - const { setSelectedView, setTestStreamIndex } = useConnectorBuilderState(); + const { setSelectedView } = useConnectorBuilderFormState(); const invalidViews = useCallback( (ignoreUntouched: boolean, limitToViews?: BuilderView[], inputErrors?: FormikErrors) => { @@ -87,14 +87,13 @@ export const useBuilderErrors = () => { setSelectedView("global"); } else { setSelectedView(invalidBuilderViews[0]); - setTestStreamIndex(invalidBuilderViews[0] as number); } } else { callback(); } }); }, - [invalidViews, setFieldTouched, setSelectedView, setTestStreamIndex, validateForm] + [invalidViews, setFieldTouched, setSelectedView, validateForm] ); return { hasErrors, validateAndTouch }; diff --git a/airbyte-webapp/src/pages/ConnectorBuilderPage/ConnectorBuilderPage.tsx b/airbyte-webapp/src/pages/ConnectorBuilderPage/ConnectorBuilderPage.tsx index 765efd547b00..be7c38afaced 100644 --- a/airbyte-webapp/src/pages/ConnectorBuilderPage/ConnectorBuilderPage.tsx +++ b/airbyte-webapp/src/pages/ConnectorBuilderPage/ConnectorBuilderPage.tsx @@ -1,77 +1,103 @@ import classnames from "classnames"; import { Formik } from "formik"; +import React, { useCallback, useMemo, useRef } from "react"; import { useIntl } from "react-intl"; import { Builder } from "components/connectorBuilder/Builder/Builder"; import { StreamTestingPanel } from "components/connectorBuilder/StreamTestingPanel"; -import { builderFormValidationSchema } from "components/connectorBuilder/types"; +import { builderFormValidationSchema, BuilderFormValues } from "components/connectorBuilder/types"; import { YamlEditor } from "components/connectorBuilder/YamlEditor"; import { ResizablePanels } from "components/ui/ResizablePanels"; import { - ConnectorBuilderStateProvider, - useConnectorBuilderState, + ConnectorBuilderTestStateProvider, + ConnectorBuilderFormStateProvider, + useConnectorBuilderFormState, } from "services/connectorBuilder/ConnectorBuilderStateService"; import styles from "./ConnectorBuilderPage.module.scss"; -const ConnectorBuilderPageInner: React.FC = () => { - const { formatMessage } = useIntl(); - const { builderFormValues, editorView, setEditorView } = useConnectorBuilderState(); +// eslint-disable-next-line @typescript-eslint/no-empty-function +const noop = function () {}; - return ( - undefined} - validationSchema={builderFormValidationSchema} - validateOnChange={false} - > - {({ values, validateForm }) => { - return ( - - {editorView === "yaml" ? ( - setEditorView("ui")} /> - ) : ( - setEditorView("yaml")} - validateForm={validateForm} - /> - )} - - ), - className: styles.leftPanel, - minWidth: 100, - }} - secondPanel={{ - children: , - className: styles.rightPanel, - flex: 0.33, - minWidth: 60, - overlay: { - displayThreshold: 325, - header: formatMessage({ id: "connectorBuilder.testConnector" }), - rotation: "counter-clockwise", - }, - }} +const ConnectorBuilderPageInner: React.FC = React.memo(() => { + const { builderFormValues, editorView, setEditorView } = useConnectorBuilderFormState(); + + const switchToUI = useCallback(() => setEditorView("ui"), [setEditorView]); + const switchToYaml = useCallback(() => setEditorView("yaml"), [setEditorView]); + + const initialFormValues = useRef(builderFormValues); + return useMemo( + () => ( + + {({ values, validateForm }) => ( + - ); - }} - + )} + + ), + [editorView, switchToUI, switchToYaml] ); -}; +}); export const ConnectorBuilderPage: React.FC = () => ( - - - + + + + + +); + +const Panels = React.memo( + ({ + editorView, + switchToUI, + switchToYaml, + values, + validateForm, + }: { + editorView: string; + switchToUI: () => void; + values: BuilderFormValues; + switchToYaml: () => void; + validateForm: () => void; + }) => { + const { formatMessage } = useIntl(); + return ( + + {editorView === "yaml" ? ( + + ) : ( + + )} + + ), + className: styles.leftPanel, + minWidth: 100, + }} + secondPanel={{ + children: , + className: styles.rightPanel, + flex: 0.33, + minWidth: 60, + overlay: { + displayThreshold: 325, + header: formatMessage({ id: "connectorBuilder.testConnector" }), + rotation: "counter-clockwise", + }, + }} + /> + ); + } ); export default ConnectorBuilderPage; diff --git a/airbyte-webapp/src/services/connectorBuilder/ConnectorBuilderStateService.tsx b/airbyte-webapp/src/services/connectorBuilder/ConnectorBuilderStateService.tsx index 356afdf64dee..6e35db96000b 100644 --- a/airbyte-webapp/src/services/connectorBuilder/ConnectorBuilderStateService.tsx +++ b/airbyte-webapp/src/services/connectorBuilder/ConnectorBuilderStateService.tsx @@ -7,7 +7,7 @@ import { useLocalStorage } from "react-use"; import { BuilderFormValues, convertToManifest, DEFAULT_BUILDER_FORM_VALUES } from "components/connectorBuilder/types"; import { StreamReadRequestBodyConfig, StreamsListReadStreamsItem } from "core/request/ConnectorBuilderClient"; -import { ConnectorManifest } from "core/request/ConnectorManifest"; +import { ConnectorManifest, DeclarativeComponentSchema } from "core/request/ConnectorManifest"; import { useListStreams } from "./ConnectorBuilderApiService"; @@ -24,33 +24,36 @@ const DEFAULT_JSON_MANIFEST_VALUES: ConnectorManifest = { export type EditorView = "ui" | "yaml"; export type BuilderView = "global" | "inputs" | number; -interface Context { +interface FormStateContext { builderFormValues: BuilderFormValues; jsonManifest: ConnectorManifest; + lastValidJsonManifest: DeclarativeComponentSchema | undefined; yamlManifest: string; yamlEditorIsMounted: boolean; yamlIsValid: boolean; - streams: StreamsListReadStreamsItem[]; - streamListErrorMessage: string | undefined; - testStreamIndex: number; selectedView: BuilderView; - configJson: StreamReadRequestBodyConfig; editorView: EditorView; setBuilderFormValues: (values: BuilderFormValues, isInvalid: boolean) => void; setJsonManifest: (jsonValue: ConnectorManifest) => void; setYamlEditorIsMounted: (value: boolean) => void; setYamlIsValid: (value: boolean) => void; - setTestStreamIndex: (streamIndex: number) => void; setSelectedView: (view: BuilderView) => void; - setConfigJson: (value: StreamReadRequestBodyConfig) => void; setEditorView: (editorView: EditorView) => void; } -export const ConnectorBuilderStateContext = React.createContext(null); +interface TestStateContext { + streams: StreamsListReadStreamsItem[]; + streamListErrorMessage: string | undefined; + testInputJson: StreamReadRequestBodyConfig; + setTestInputJson: (value: StreamReadRequestBodyConfig) => void; + setTestStreamIndex: (streamIndex: number) => void; + testStreamIndex: number; +} -export const ConnectorBuilderStateProvider: React.FC> = ({ children }) => { - const { formatMessage } = useIntl(); +export const ConnectorBuilderFormStateContext = React.createContext(null); +export const ConnectorBuilderTestStateContext = React.createContext(null); +export const ConnectorBuilderFormStateProvider: React.FC> = ({ children }) => { // manifest values const [storedBuilderFormValues, setStoredBuilderFormValues] = useLocalStorage( "connectorBuilderFormValues", @@ -61,10 +64,11 @@ export const ConnectorBuilderStateProvider: React.FC { - setStoredBuilderFormValues(values); if (isValid) { + // update ref first because calling setStoredBuilderFormValues might synchronously kick off a react render cycle. lastValidBuilderFormValuesRef.current = values; } + setStoredBuilderFormValues(values); }, [setStoredBuilderFormValues] ); @@ -79,19 +83,31 @@ export const ConnectorBuilderStateProvider: React.FC { - setJsonManifest(convertToManifest(builderFormValues)); - }, [builderFormValues, setJsonManifest]); + const [editorView, rawSetEditorView] = useLocalStorage("connectorBuilderEditorView", "ui"); + + const derivedJsonManifest = useMemo( + () => (editorView === "yaml" ? manifest : convertToManifest(builderFormValues)), + [editorView, builderFormValues, manifest] + ); + + const manifestRef = useRef(derivedJsonManifest); + manifestRef.current = derivedJsonManifest; + + const setEditorView = useCallback( + (view: EditorView) => { + if (view === "yaml") { + // when switching to yaml, store the currently derived json manifest + setJsonManifest(manifestRef.current); + } + rawSetEditorView(view); + }, + [rawSetEditorView, setJsonManifest] + ); const [yamlIsValid, setYamlIsValid] = useState(true); const [yamlEditorIsMounted, setYamlEditorIsMounted] = useState(true); - const [yamlManifest, setYamlManifest] = useState(""); - useEffect(() => { - setYamlManifest(dump(jsonManifest)); - }, [jsonManifest]); - - const [editorView, setEditorView] = useState("ui"); + const yamlManifest = useMemo(() => dump(derivedJsonManifest), [derivedJsonManifest]); const lastValidBuilderFormValues = lastValidBuilderFormValuesRef.current; /** @@ -101,22 +117,50 @@ export const ConnectorBuilderStateProvider: React.FC editorView !== "ui" - ? undefined + ? jsonManifest : builderFormValues === lastValidBuilderFormValues ? jsonManifest : convertToManifest(lastValidBuilderFormValues), [builderFormValues, editorView, jsonManifest, lastValidBuilderFormValues] ); + const [selectedView, setSelectedView] = useState("global"); + + const ctx = { + builderFormValues, + jsonManifest: derivedJsonManifest, + lastValidJsonManifest, + yamlManifest, + yamlEditorIsMounted, + yamlIsValid, + selectedView, + editorView: editorView || "ui", + setBuilderFormValues, + setJsonManifest, + setYamlIsValid, + setYamlEditorIsMounted, + setSelectedView, + setEditorView, + }; + + return {children}; +}; + +export const ConnectorBuilderTestStateProvider: React.FC> = ({ children }) => { + const { formatMessage } = useIntl(); + const { lastValidJsonManifest, selectedView } = useConnectorBuilderFormState(); + + const manifest = lastValidJsonManifest ?? DEFAULT_JSON_MANIFEST_VALUES; + // config - const [configJson, setConfigJson] = useState({}); + const [testInputJson, setTestInputJson] = useState({}); // streams const { data: streamListRead, isError: isStreamListError, error: streamListError, - } = useListStreams({ manifest: lastValidJsonManifest || manifest, config: configJson }); + } = useListStreams({ manifest, config: testInputJson }); const unknownErrorMessage = formatMessage({ id: "connectorBuilder.unknownError" }); const streamListErrorMessage = isStreamListError ? streamListError instanceof Error @@ -129,49 +173,43 @@ export const ConnectorBuilderStateProvider: React.FC { - setTestStreamIndex((prevIndex) => - prevIndex >= streams.length && streams.length > 0 ? streams.length - 1 : prevIndex - ); - }, [streams]); - - const [selectedView, setSelectedView] = useState("global"); + if (typeof selectedView === "number") { + setTestStreamIndex(selectedView); + } + }, [selectedView]); const ctx = { - builderFormValues, - jsonManifest: manifest, - yamlManifest, - yamlEditorIsMounted, - yamlIsValid, streams, streamListErrorMessage, + testInputJson, + setTestInputJson, testStreamIndex, - selectedView, - configJson, - editorView, - setBuilderFormValues, - setJsonManifest, - setYamlIsValid, - setYamlEditorIsMounted, setTestStreamIndex, - setSelectedView, - setConfigJson, - setEditorView, }; - return {children}; + return {children}; +}; + +export const useConnectorBuilderTestState = (): TestStateContext => { + const connectorBuilderState = useContext(ConnectorBuilderTestStateContext); + if (!connectorBuilderState) { + throw new Error("useConnectorBuilderTestStae must be used within a ConnectorBuilderTestStateProvider."); + } + + return connectorBuilderState; }; -export const useConnectorBuilderState = (): Context => { - const connectorBuilderState = useContext(ConnectorBuilderStateContext); +export const useConnectorBuilderFormState = (): FormStateContext => { + const connectorBuilderState = useContext(ConnectorBuilderFormStateContext); if (!connectorBuilderState) { - throw new Error("useConnectorBuilderState must be used within a ConnectorBuilderStateProvider."); + throw new Error("useConnectorBuilderFormState must be used within a ConnectorBuilderFormStateProvider."); } return connectorBuilderState; }; export const useSelectedPageAndSlice = () => { - const { streams, testStreamIndex } = useConnectorBuilderState(); + const { streams, testStreamIndex } = useConnectorBuilderTestState(); const selectedStreamName = streams[testStreamIndex].name;