diff --git a/airbyte-webapp/src/components/connectorBuilder/Builder/BuilderField.tsx b/airbyte-webapp/src/components/connectorBuilder/Builder/BuilderField.tsx index ae7bf7fa1909b..6a529c71730f5 100644 --- a/airbyte-webapp/src/components/connectorBuilder/Builder/BuilderField.tsx +++ b/airbyte-webapp/src/components/connectorBuilder/Builder/BuilderField.tsx @@ -117,6 +117,7 @@ const InnerBuilderField: React.FC> = readOnly={readOnly} adornment={adornment} onBlur={(e) => { + field.onBlur(e); props.onBlur?.(e.target.value); }} /> diff --git a/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamSelector.tsx b/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamSelector.tsx index 9e27979c035d2..411716fa9c252 100644 --- a/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamSelector.tsx +++ b/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamSelector.tsx @@ -34,7 +34,10 @@ export const StreamSelector: React.FC = ({ className }) => const analyticsService = useAnalyticsService(); const { formatMessage } = useIntl(); const { selectedView, setSelectedView } = useConnectorBuilderFormState(); - const { streams, testStreamIndex, setTestStreamIndex } = useConnectorBuilderTestState(); + const { testStreamIndex, setTestStreamIndex } = useConnectorBuilderTestState(); + + const streams = useStreamNames(); + const options = streams.map((stream) => { const label = stream.name && stream.name.trim() ? capitalize(stream.name) : formatMessage({ id: "connectorBuilder.emptyName" }); @@ -60,10 +63,25 @@ export const StreamSelector: React.FC = ({ className }) => ); }; + +function useStreamNames() { + const { builderFormValues, editorView, formValuesValid } = useConnectorBuilderFormState(); + const { streams: testStreams, isFetchingStreamList, streamListErrorMessage } = useConnectorBuilderTestState(); + + let streams: Array<{ name: string }> = editorView === "ui" ? builderFormValues.streams : testStreams; + + const testStreamListUpToDate = formValuesValid && !isFetchingStreamList && !streamListErrorMessage; + + if (editorView === "ui" && testStreamListUpToDate) { + streams = streams.map((stream, index) => ({ name: testStreams[index]?.name || stream.name })); + } + + return streams; +} diff --git a/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamTestButton.tsx b/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamTestButton.tsx index d0c8d16960d2e..fc1536bf37ef7 100644 --- a/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamTestButton.tsx +++ b/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamTestButton.tsx @@ -15,12 +15,14 @@ import { useBuilderErrors } from "../useBuilderErrors"; interface StreamTestButtonProps { readStream: () => void; hasTestInputJsonErrors: boolean; + hasStreamListErrors: boolean; setTestInputOpen: (open: boolean) => void; } export const StreamTestButton: React.FC = ({ readStream, hasTestInputJsonErrors, + hasStreamListErrors, setTestInputOpen, }) => { const { editorView, yamlIsValid } = useConnectorBuilderFormState(); @@ -52,6 +54,9 @@ export const StreamTestButton: React.FC = ({ if ((editorView === "ui" && hasErrors(false)) || hasTestInputJsonErrors) { showWarningIcon = true; tooltipContent = ; + } else if (hasStreamListErrors) { + // only disable the button on stream list errors if there are no user-fixable errors + buttonDisabled = true; } const testButton = ( diff --git a/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamTester.module.scss b/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamTester.module.scss index 7cbf936ac5369..cfdbf363650f4 100644 --- a/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamTester.module.scss +++ b/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamTester.module.scss @@ -5,7 +5,6 @@ flex: 1; display: flex; flex-direction: column; - align-items: center; gap: variables.$spacing-lg; min-height: 0; width: 100%; @@ -31,3 +30,12 @@ .fetchingSpinner { margin: auto; } + +.listErrorDisplay { + padding: variables.$spacing-lg; + display: flex; + flex-direction: column; + gap: variables.$spacing-md; + background-color: colors.$blue-50; + border-radius: variables.$border-radius-sm; +} diff --git a/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamTester.tsx b/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamTester.tsx index a2459150e457e..cdfdc04a132b9 100644 --- a/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamTester.tsx +++ b/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamTester.tsx @@ -1,13 +1,15 @@ import { useEffect, useState } from "react"; -import { useIntl } from "react-intl"; +import { FormattedMessage, useIntl } from "react-intl"; import { ResizablePanels } from "components/ui/ResizablePanels"; import { Spinner } from "components/ui/Spinner"; import { Text } from "components/ui/Text"; import { Action, Namespace } from "core/analytics"; +import { StreamsListReadStreamsItem } from "core/request/ConnectorBuilderClient"; import { useAnalyticsService } from "hooks/services/Analytics"; import { useConnectorBuilderTestState } from "services/connectorBuilder/ConnectorBuilderStateService"; +import { links } from "utils/links"; import { LogsDisplay } from "./LogsDisplay"; import { ResultDisplay } from "./ResultDisplay"; @@ -22,6 +24,8 @@ export const StreamTester: React.FC<{ const { streams, testStreamIndex, + isFetchingStreamList, + streamListErrorMessage, streamRead: { data: streamReadData, refetch: readStream, @@ -78,11 +82,24 @@ export const StreamTester: React.FC<{ } }, [analyticsService, errorMessage, isFetchedAfterMount, streamName, dataUpdatedAt, errorUpdatedAt]); + const currentStream = streams[testStreamIndex] as StreamsListReadStreamsItem | undefined; return (
- - {streams[testStreamIndex]?.url} - + {currentStream && ( + + {currentStream.url} + + )} + {!currentStream && isFetchingStreamList && ( + + + + )} + {!currentStream && streamListErrorMessage && ( + + + + )} { @@ -93,9 +110,30 @@ export const StreamTester: React.FC<{ }); }} hasTestInputJsonErrors={hasTestInputJsonErrors} + hasStreamListErrors={Boolean(streamListErrorMessage)} setTestInputOpen={setTestInputOpen} /> + {streamListErrorMessage !== undefined && ( +
+ + + + {streamListErrorMessage} + + ( + + {node} + + ), + }} + /> + +
+ )} {isFetching && (
diff --git a/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamTestingPanel.module.scss b/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamTestingPanel.module.scss index efd4b057ba055..87d6feea98cf2 100644 --- a/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamTestingPanel.module.scss +++ b/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamTestingPanel.module.scss @@ -28,18 +28,6 @@ align-items: center; } -.listErrorDisplay { - padding: variables.$spacing-lg; - display: flex; - flex-direction: column; - gap: variables.$spacing-md; - background-color: colors.$blue-50; - border-radius: variables.$border-radius-sm; - - // leave room for config button - margin-top: 50px; -} - .loadingSpinner { height: 100%; width: 100%; diff --git a/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamTestingPanel.tsx b/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamTestingPanel.tsx index 69bfc96573fcb..d947808ac285c 100644 --- a/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamTestingPanel.tsx +++ b/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamTestingPanel.tsx @@ -4,7 +4,6 @@ import { ValidationError } from "yup"; import { Heading } from "components/ui/Heading"; import { Spinner } from "components/ui/Spinner"; -import { Text } from "components/ui/Text"; import { jsonSchemaToFormBlock } from "core/form/schemaToFormBlock"; import { buildYupFormForJsonSchema } from "core/form/schemaToYup"; @@ -14,7 +13,6 @@ import { useConnectorBuilderTestState, useConnectorBuilderFormState, } from "services/connectorBuilder/ConnectorBuilderStateService"; -import { links } from "utils/links"; import { ConfigMenu } from "./ConfigMenu"; import { StreamSelector } from "./StreamSelector"; @@ -43,7 +41,7 @@ function useTestInputJsonErrors(testInputJson: StreamReadRequestBodyConfig, spec export const StreamTestingPanel: React.FC = () => { const [isTestInputOpen, setTestInputOpen] = useState(false); const { jsonManifest, yamlEditorIsMounted } = useConnectorBuilderFormState(); - const { testInputJson, streamListErrorMessage } = useConnectorBuilderTestState(); + const { testInputJson } = useConnectorBuilderTestState(); const testInputJsonErrors = useTestInputJsonErrors(testInputJson, jsonManifest.spec); @@ -73,32 +71,12 @@ export const StreamTestingPanel: React.FC = () => {
)} - {hasStreams && streamListErrorMessage === undefined && ( + {hasStreams && (
0} setTestInputOpen={setTestInputOpen} />
)} - {hasStreams && streamListErrorMessage !== undefined && ( -
- - - - {streamListErrorMessage} - - ( - - {node} - - ), - }} - /> - -
- )}
); }; diff --git a/airbyte-webapp/src/components/connectorBuilder/useBuilderErrors.ts b/airbyte-webapp/src/components/connectorBuilder/useBuilderErrors.ts index 5ee008f51c81e..843b3ab205c5f 100644 --- a/airbyte-webapp/src/components/connectorBuilder/useBuilderErrors.ts +++ b/airbyte-webapp/src/components/connectorBuilder/useBuilderErrors.ts @@ -14,7 +14,9 @@ export const useBuilderErrors = () => { const invalidViews = useCallback( (ignoreUntouched: boolean, limitToViews?: BuilderView[], inputErrors?: FormikErrors) => { const errorsToCheck = inputErrors !== undefined ? inputErrors : errors; - const errorKeys = Object.keys(errorsToCheck); + const errorKeys = Object.keys(errorsToCheck).filter((errorKey) => + Boolean(errorsToCheck[errorKey as keyof BuilderFormValues]) + ); const invalidViews: BuilderView[] = []; @@ -33,7 +35,9 @@ export const useBuilderErrors = () => { } if (errorKeys.includes("streams")) { - const errorStreamNums = Object.keys(errorsToCheck.streams ?? {}); + const errorStreamNums = Object.keys(errorsToCheck.streams ?? {}).filter((errorKey) => + Boolean(errorsToCheck.streams?.[Number(errorKey)]) + ); if (ignoreUntouched) { if (errorsToCheck.streams && touched.streams) { diff --git a/airbyte-webapp/src/locales/en.json b/airbyte-webapp/src/locales/en.json index 38c0e428e8eb1..09177f8bc9238 100644 --- a/airbyte-webapp/src/locales/en.json +++ b/airbyte-webapp/src/locales/en.json @@ -668,7 +668,7 @@ "connectorBuilder.invalidYamlTest": "Cannot test stream while YAML content is invalid.", "connectorBuilder.unknownError": "An unknown error has occurred", "connectorBuilder.testConnector": "TEST YOUR CONNECTOR", - "connectorBuilder.couldNotDetectStreams": "Could not detect streams in the YAML editor:", + "connectorBuilder.couldNotDetectStreams": "Could not verify streams:", "connectorBuilder.ensureProperYaml": "In order to test a stream, ensure that the YAML is structured as described in the docs.", "connectorBuilder.addStreamModal.title": "New stream", "connectorBuilder.addStreamModal.streamNameLabel": "Stream name", @@ -747,6 +747,9 @@ "connectorBuilder.copyFromSlicerTitle": "Import slicing settings from...", "connectorBuilder.inputsButton": "Inputs", "connectorBuilder.interUserInputValue": "Insert a user input value", + "connectorBuilder.loadingStreamList": "Loading", + "connectorBuilder.noStreamSelected": "No stream selected", + "connectorBuilder.streamListUrlError": "Could not load URL", "jobs.noAttemptsFailure": "Failed to start job.", diff --git a/airbyte-webapp/src/pages/ConnectorBuilderPage/ConnectorBuilderPage.tsx b/airbyte-webapp/src/pages/ConnectorBuilderPage/ConnectorBuilderPage.tsx index 41e1931f0118f..7777194d67952 100644 --- a/airbyte-webapp/src/pages/ConnectorBuilderPage/ConnectorBuilderPage.tsx +++ b/airbyte-webapp/src/pages/ConnectorBuilderPage/ConnectorBuilderPage.tsx @@ -36,6 +36,7 @@ const ConnectorBuilderPageInner: React.FC = React.memo(() => { const switchToYaml = useCallback(() => setEditorView("yaml"), [setEditorView]); const initialFormValues = useRef(builderFormValues); + return useMemo( () => ( void; testStreamIndex: number; streamRead: UseQueryResult; + isFetchingStreamList: boolean; } export const ConnectorBuilderFormStateContext = React.createContext(null); @@ -65,6 +67,8 @@ export const ConnectorBuilderFormStateProvider: React.FC(storedBuilderFormValues as BuilderFormValues); const currentBuilderFormValuesRef = useRef(storedBuilderFormValues as BuilderFormValues); @@ -75,6 +79,7 @@ export const ConnectorBuilderFormStateProvider: React.FC{children};