From 458bb8d7abc71ebbe12cd6e3d72dd4dc9a66b4da Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 23 Jan 2023 12:37:33 +0100 Subject: [PATCH 1/5] refactor --- .../common/DeleteBlock/DeleteBlock.tsx | 22 +-- .../StreamTestingPanel/ConfigMenu.tsx | 25 +++- airbyte-webapp/src/hooks/useDeleteModal.tsx | 23 ++++ airbyte-webapp/src/locales/en.json | 5 +- .../components/SourceSettings.tsx | 13 +- .../DestinationSettingsPage.tsx | 12 +- .../ConnectorCard/ConnectorCard.module.scss | 8 -- .../Connector/ConnectorCard/ConnectorCard.tsx | 125 +++++++++--------- .../ConnectorCard/components/Controls.tsx | 73 ++++++++++ .../components/TestCard.module.scss | 5 + .../ConnectorCard/components/TestCard.tsx | 119 +++++++++++++++++ .../TestingConnectionSpinner.module.scss | 0 .../components/TestingConnectionSpinner.tsx | 0 .../components/TestingConnectionSuccess.tsx | 17 +++ .../ConnectorForm/ConnectorForm.test.tsx | 42 +----- .../Connector/ConnectorForm/ConnectorForm.tsx | 31 +++-- .../ConnectorForm/FormRoot.module.scss | 5 + .../Connector/ConnectorForm/FormRoot.tsx | 98 +++++++------- .../components/CreateControls.module.scss | 19 --- .../components/CreateControls.tsx | 73 ---------- .../components/EditControls.module.scss | 16 --- .../ConnectorForm/components/EditControls.tsx | 78 ----------- .../components/Sections/auth/AuthSection.tsx | 5 +- .../components/TestingConnectionError.tsx | 44 ++---- .../components/TestingConnectionSuccess.tsx | 33 ----- .../ConnectorForm/connectorFormContext.tsx | 4 +- .../Connector/ConnectorForm/useBuildForm.tsx | 24 +++- 27 files changed, 463 insertions(+), 456 deletions(-) create mode 100644 airbyte-webapp/src/hooks/useDeleteModal.tsx create mode 100644 airbyte-webapp/src/views/Connector/ConnectorCard/components/Controls.tsx create mode 100644 airbyte-webapp/src/views/Connector/ConnectorCard/components/TestCard.module.scss create mode 100644 airbyte-webapp/src/views/Connector/ConnectorCard/components/TestCard.tsx rename airbyte-webapp/src/views/Connector/{ConnectorForm => ConnectorCard}/components/TestingConnectionSpinner.module.scss (100%) rename airbyte-webapp/src/views/Connector/{ConnectorForm => ConnectorCard}/components/TestingConnectionSpinner.tsx (100%) create mode 100644 airbyte-webapp/src/views/Connector/ConnectorCard/components/TestingConnectionSuccess.tsx create mode 100644 airbyte-webapp/src/views/Connector/ConnectorForm/FormRoot.module.scss delete mode 100644 airbyte-webapp/src/views/Connector/ConnectorForm/components/CreateControls.module.scss delete mode 100644 airbyte-webapp/src/views/Connector/ConnectorForm/components/CreateControls.tsx delete mode 100644 airbyte-webapp/src/views/Connector/ConnectorForm/components/EditControls.module.scss delete mode 100644 airbyte-webapp/src/views/Connector/ConnectorForm/components/EditControls.tsx delete mode 100644 airbyte-webapp/src/views/Connector/ConnectorForm/components/TestingConnectionSuccess.tsx diff --git a/airbyte-webapp/src/components/common/DeleteBlock/DeleteBlock.tsx b/airbyte-webapp/src/components/common/DeleteBlock/DeleteBlock.tsx index 548ad268ffae..684d6dcbf92d 100644 --- a/airbyte-webapp/src/components/common/DeleteBlock/DeleteBlock.tsx +++ b/airbyte-webapp/src/components/common/DeleteBlock/DeleteBlock.tsx @@ -1,12 +1,11 @@ -import React, { useCallback } from "react"; +import React from "react"; import { FormattedMessage } from "react-intl"; -import { useNavigate } from "react-router-dom"; import { H5 } from "components/base/Titles"; import { Button } from "components/ui/Button"; import { Card } from "components/ui/Card"; -import { useConfirmationModalService } from "hooks/services/ConfirmationModal"; +import { useDeleteModal } from "hooks/useDeleteModal"; import styles from "./DeleteBlock.module.scss"; @@ -16,22 +15,7 @@ interface IProps { } export const DeleteBlock: React.FC = ({ type, onDelete }) => { - const { openConfirmationModal, closeConfirmationModal } = useConfirmationModalService(); - const navigate = useNavigate(); - - const onDeleteButtonClick = useCallback(() => { - openConfirmationModal({ - text: `tables.${type}DeleteModalText`, - title: `tables.${type}DeleteConfirm`, - submitButtonText: "form.delete", - onSubmit: async () => { - await onDelete(); - closeConfirmationModal(); - navigate("../.."); - }, - submitButtonDataId: "delete", - }); - }, [closeConfirmationModal, onDelete, openConfirmationModal, navigate, type]); + const onDeleteButtonClick = useDeleteModal(type, onDelete); return ( diff --git a/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/ConfigMenu.tsx b/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/ConfigMenu.tsx index 0596304949d8..ebbb5ed354e2 100644 --- a/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/ConfigMenu.tsx +++ b/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/ConfigMenu.tsx @@ -6,6 +6,7 @@ import { useLocalStorage } from "react-use"; import { Button } from "components/ui/Button"; import { Callout } from "components/ui/Callout"; +import { FlexContainer, FlexItem } from "components/ui/Flex"; import { Modal, ModalBody } from "components/ui/Modal"; import { NumberBadge } from "components/ui/NumberBadge"; import { Tooltip } from "components/ui/Tooltip"; @@ -112,13 +113,35 @@ export const ConfigMenu: React.FC = ({ className, testInputJson { setTestInputJson(values.connectionConfiguration as StreamReadRequestBodyConfig); setIsOpen(false); }} + renderFooter={({ dirty, isSubmitting }) => ( +
+ + + + + + + +
+ )} onCancel={() => { setIsOpen(false); }} diff --git a/airbyte-webapp/src/hooks/useDeleteModal.tsx b/airbyte-webapp/src/hooks/useDeleteModal.tsx new file mode 100644 index 000000000000..17f1d87c1e41 --- /dev/null +++ b/airbyte-webapp/src/hooks/useDeleteModal.tsx @@ -0,0 +1,23 @@ +import { useCallback } from "react"; +import { useNavigate } from "react-router-dom"; + +import { useConfirmationModalService } from "./services/ConfirmationModal"; + +export function useDeleteModal(type: "source" | "destination" | "connection", onDelete: () => Promise) { + const { openConfirmationModal, closeConfirmationModal } = useConfirmationModalService(); + const navigate = useNavigate(); + + return useCallback(() => { + openConfirmationModal({ + text: `tables.${type}DeleteModalText`, + title: `tables.${type}DeleteConfirm`, + submitButtonText: "form.delete", + onSubmit: async () => { + await onDelete(); + closeConfirmationModal(); + navigate("../.."); + }, + submitButtonDataId: "delete", + }); + }, [closeConfirmationModal, onDelete, openConfirmationModal, navigate, type]); +} diff --git a/airbyte-webapp/src/locales/en.json b/airbyte-webapp/src/locales/en.json index 32d224d27f1d..f9a8d2c89a8c 100644 --- a/airbyte-webapp/src/locales/en.json +++ b/airbyte-webapp/src/locales/en.json @@ -62,9 +62,12 @@ "form.saveChanges": "Save changes", "form.openDatepicker": "Open datepicker", "form.datepickerTimeCaption": "Time (UTC)", - "form.saveChangesAndTest": "Save changes and test", + "form.saveChangesAndTest": "Test and save", "form.sourceRetest": "Retest source", "form.destinationRetest": "Retest destination", + "form.test": "Test", + "form.sourceRetestTitle": "Test the source", + "form.destinationRetestTitle": "Test the destination", "form.discardChanges": "Discard changes", "form.discardChangesConfirmation": "There are unsaved changes. Are you sure you want to discard your changes?", "form.every.minutes": "Every {value, plural, one {minute} other {# minutes}}", diff --git a/airbyte-webapp/src/pages/SourcesPage/pages/SourceItemPage/components/SourceSettings.tsx b/airbyte-webapp/src/pages/SourcesPage/pages/SourceItemPage/components/SourceSettings.tsx index f1b21954adb9..54f55047a132 100644 --- a/airbyte-webapp/src/pages/SourcesPage/pages/SourceItemPage/components/SourceSettings.tsx +++ b/airbyte-webapp/src/pages/SourcesPage/pages/SourceItemPage/components/SourceSettings.tsx @@ -1,13 +1,12 @@ -import React, { useEffect } from "react"; +import React, { useCallback, useEffect } from "react"; import { FormattedMessage } from "react-intl"; -import { DeleteBlock } from "components/common/DeleteBlock"; - import { ConnectionConfiguration } from "core/domain/connection"; import { SourceRead, WebBackendConnectionListItem } from "core/request/AirbyteClient"; import { useTrackPage, PageTrackingCodes } from "hooks/services/Analytics"; import { useFormChangeTrackerService, useUniqueFormId } from "hooks/services/FormChangeTracker"; import { useDeleteSource, useUpdateSource } from "hooks/services/useSourceHook"; +import { useDeleteModal } from "hooks/useDeleteModal"; import { useSourceDefinition } from "services/connector/SourceDefinitionService"; import { useGetSourceDefinitionSpecification } from "services/connector/SourceDefinitionSpecificationService"; import { ConnectorCard } from "views/Connector/ConnectorCard"; @@ -49,10 +48,12 @@ const SourceSettings: React.FC = ({ currentSource, connecti }); }; - const onDelete = async () => { + const onDelete = useCallback(async () => { clearFormChange(formId); await deleteSource({ connectionsWithSource, source: currentSource }); - }; + }, [clearFormChange, connectionsWithSource, currentSource, deleteSource, formId]); + + const onDeleteClick = useDeleteModal("source", onDelete); return (
@@ -66,8 +67,8 @@ const SourceSettings: React.FC = ({ currentSource, connecti selectedConnectorDefinitionId={sourceDefinitionSpecification.sourceDefinitionId} connector={currentSource} onSubmit={onSubmit} + onDeleteClick={onDeleteClick} /> -
); }; diff --git a/airbyte-webapp/src/pages/destination/DestinationSettingsPage/DestinationSettingsPage.tsx b/airbyte-webapp/src/pages/destination/DestinationSettingsPage/DestinationSettingsPage.tsx index 33cd33acb419..9e46d6bed34e 100644 --- a/airbyte-webapp/src/pages/destination/DestinationSettingsPage/DestinationSettingsPage.tsx +++ b/airbyte-webapp/src/pages/destination/DestinationSettingsPage/DestinationSettingsPage.tsx @@ -1,14 +1,14 @@ -import React from "react"; +import React, { useCallback } from "react"; import { FormattedMessage } from "react-intl"; import { useParams } from "react-router-dom"; -import { DeleteBlock } from "components/common/DeleteBlock"; import { StepsTypes } from "components/ConnectorBlocks"; import { useTrackPage, PageTrackingCodes } from "hooks/services/Analytics"; import { useFormChangeTrackerService, useUniqueFormId } from "hooks/services/FormChangeTracker"; import { useConnectionList } from "hooks/services/useConnectionHook"; import { useDeleteDestination, useGetDestination, useUpdateDestination } from "hooks/services/useDestinationHook"; +import { useDeleteModal } from "hooks/useDeleteModal"; import { useDestinationDefinition } from "services/connector/DestinationDefinitionService"; import { useGetDestinationDefinitionSpecification } from "services/connector/DestinationDefinitionSpecificationService"; import { ConnectorCard } from "views/Connector/ConnectorCard"; @@ -36,13 +36,15 @@ export const DestinationSettingsPage: React.FC = () => { }); }; - const onDelete = async () => { + const onDelete = useCallback(async () => { clearFormChange(formId); await deleteDestination({ connectionsWithDestination, destination, }); - }; + }, [clearFormChange, connectionsWithDestination, deleteDestination, destination, formId]); + + const onDeleteClick = useDeleteModal("destination", onDelete); return (
@@ -56,8 +58,8 @@ export const DestinationSettingsPage: React.FC = () => { selectedConnectorDefinitionId={destinationSpecification.destinationDefinitionId} connector={destination} onSubmit={onSubmitForm} + onDeleteClick={onDeleteClick} /> -
); }; diff --git a/airbyte-webapp/src/views/Connector/ConnectorCard/ConnectorCard.module.scss b/airbyte-webapp/src/views/Connector/ConnectorCard/ConnectorCard.module.scss index 24d8851b643e..ca3da58cf15f 100644 --- a/airbyte-webapp/src/views/Connector/ConnectorCard/ConnectorCard.module.scss +++ b/airbyte-webapp/src/views/Connector/ConnectorCard/ConnectorCard.module.scss @@ -1,9 +1,5 @@ @use "scss/variables"; -.cardForm { - padding: 22px 27px 23px 24px; -} - .connectorSelectControl { margin-bottom: variables.$spacing-xl; } @@ -19,7 +15,3 @@ margin-top: variables.$spacing-md; margin-left: variables.$spacing-lg; } - -.connectionTestLogs { - margin-top: variables.$spacing-lg; -} diff --git a/airbyte-webapp/src/views/Connector/ConnectorCard/ConnectorCard.tsx b/airbyte-webapp/src/views/Connector/ConnectorCard/ConnectorCard.tsx index 72dd4f107e7b..d047d3b32ac6 100644 --- a/airbyte-webapp/src/views/Connector/ConnectorCard/ConnectorCard.tsx +++ b/airbyte-webapp/src/views/Connector/ConnectorCard/ConnectorCard.tsx @@ -1,11 +1,5 @@ -import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import React, { useEffect, useMemo, useState } from "react"; -import { FormattedMessage } from "react-intl"; -import { JobLogs } from "components/JobItem/components/JobLogs"; -import { Button } from "components/ui/Button"; -import { Card } from "components/ui/Card"; import { Spinner } from "components/ui/Spinner"; import { @@ -28,6 +22,7 @@ import { import { useDocumentationPanelContext } from "../ConnectorDocumentationLayout/DocumentationPanelContext"; import { ConnectorDefinitionTypeControl } from "../ConnectorForm/components/Controls/ConnectorServiceTypeControl"; import { FetchingConnectorError } from "../ConnectorForm/components/TestingConnectionError"; +import { Controls } from "./components/Controls"; import ShowLoadingMessage from "./components/ShowLoadingMessage"; import styles from "./ConnectorCard.module.scss"; import { useAnalyticsTrackFunctions } from "./useAnalyticsTrackFunctions"; @@ -43,6 +38,7 @@ interface ConnectorCardBaseProps { jobInfo?: SynchronousJobRead | null; additionalSelectorComponent?: React.ReactNode; onSubmit: (values: ConnectorCardValues) => Promise | void; + onDeleteClick?: () => void; onConnectorDefinitionSelect?: (id: string) => void; availableConnectorDefinitions: ConnectorDefinition[]; @@ -75,20 +71,16 @@ const getConnectorId = (connectorRead: DestinationRead | SourceRead) => { }; export const ConnectorCard: React.FC = ({ - title, - description, - full, jobInfo, onSubmit, + onDeleteClick, additionalSelectorComponent, selectedConnectorDefinitionId, fetchingConnectorError, ...props }) => { - const [saved, setSaved] = useState(false); const [errorStatusRequest, setErrorStatusRequest] = useState(null); const [isFormSubmitting, setIsFormSubmitting] = useState(false); - const [logsVisible, setLogsVisible] = useState(false); const { setDocumentationUrl, setDocumentationPanelOpen } = useDocumentationPanelContext(); const { @@ -171,7 +163,6 @@ export const ConnectorCard: React.FC -
-
- -
- {additionalSelectorComponent} -
+ +
+ +
+ {additionalSelectorComponent} {props.isLoading && (
@@ -215,42 +206,48 @@ export const ConnectorCard: React.FC )} {fetchingConnectorError && } - {selectedConnectorDefinition && selectedConnectorDefinitionSpecification && ( - } - connectorId={isEditMode ? getConnectorId(props.connector) : undefined} - /> - )} - {job && ( -
- - {logsVisible && } -
- )} -
-
- + + } + // Causes the whole ConnectorForm to be unmounted and a new instance mounted whenever the connector type changes. + // That way we carry less state around inside it, preventing any state from one connector type from affecting another + // connector type's form in any way. + key={selectedConnectorDefinition && Connector.id(selectedConnectorDefinition)} + {...props} + selectedConnectorDefinition={selectedConnectorDefinition} + selectedConnectorDefinitionSpecification={selectedConnectorDefinitionSpecification} + isTestConnectionInProgress={isTestConnectionInProgress} + connectionTestSuccess={connectionTestSuccess} + onStopTesting={onStopTesting} + testConnector={handleTestConnector} + onSubmit={onHandleSubmit} + formValues={formValues} + connectorId={isEditMode ? getConnectorId(props.connector) : undefined} + renderFooter={({ dirty, isSubmitting, isValid, resetConnectorForm, getValues }) => ( + { + if (!selectedConnectorDefinitionId) { + return; + } + handleTestConnector({ ...getValues(), serviceType: selectedConnectorDefinitionId }); + }} + onDeleteClick={onDeleteClick} + isValid={isValid} + dirty={dirty} + job={job ? job : undefined} + onCancelClick={() => { + resetConnectorForm(); + }} + connectionTestSuccess={connectionTestSuccess} + /> + )} + renderWithCard + /> ); }; diff --git a/airbyte-webapp/src/views/Connector/ConnectorCard/components/Controls.tsx b/airbyte-webapp/src/views/Connector/ConnectorCard/components/Controls.tsx new file mode 100644 index 000000000000..18ba1317fe05 --- /dev/null +++ b/airbyte-webapp/src/views/Connector/ConnectorCard/components/Controls.tsx @@ -0,0 +1,73 @@ +import React from "react"; +import { FormattedMessage } from "react-intl"; + +import { Button } from "components/ui/Button"; +import { FlexContainer, FlexItem } from "components/ui/Flex"; + +import { SynchronousJobRead } from "core/request/AirbyteClient"; + +import { TestCard } from "./TestCard"; + +interface IProps { + formType: "source" | "destination"; + isSubmitting: boolean; + isValid: boolean; + dirty: boolean; + onCancelClick: () => void; + onDeleteClick?: () => void; + onRetestClick: () => void; + onCancelTesting: () => void; + isTestConnectionInProgress?: boolean; + errorMessage?: React.ReactNode; + job?: SynchronousJobRead; + connectionTestSuccess: boolean; + hasDefinition: boolean; + isEditMode: boolean; +} + +export const Controls: React.FC = ({ + isTestConnectionInProgress, + isSubmitting, + formType, + hasDefinition, + isEditMode, + dirty, + onDeleteClick, + onCancelClick, + ...restProps +}) => { + return ( + <> + {hasDefinition && ( + + )} + + + {isEditMode && ( + + )} + + {isEditMode && ( + + )} + + + + ); +}; diff --git a/airbyte-webapp/src/views/Connector/ConnectorCard/components/TestCard.module.scss b/airbyte-webapp/src/views/Connector/ConnectorCard/components/TestCard.module.scss new file mode 100644 index 000000000000..9c08b01bf157 --- /dev/null +++ b/airbyte-webapp/src/views/Connector/ConnectorCard/components/TestCard.module.scss @@ -0,0 +1,5 @@ +@use "scss/variables"; + +.cardTest { + padding: variables.$spacing-xl; +} diff --git a/airbyte-webapp/src/views/Connector/ConnectorCard/components/TestCard.tsx b/airbyte-webapp/src/views/Connector/ConnectorCard/components/TestCard.tsx new file mode 100644 index 000000000000..b3dcea3303e6 --- /dev/null +++ b/airbyte-webapp/src/views/Connector/ConnectorCard/components/TestCard.tsx @@ -0,0 +1,119 @@ +import { faChevronDown, faChevronRight, faClose, faRefresh } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import React, { useState } from "react"; +import { FormattedMessage } from "react-intl"; + +import { JobLogs } from "components/JobItem/components/JobLogs"; +import { Button } from "components/ui/Button"; +import { Card } from "components/ui/Card"; +import { FlexContainer, FlexItem } from "components/ui/Flex"; +import { ProgressBar } from "components/ui/ProgressBar"; +import { Text } from "components/ui/Text"; + +import { SynchronousJobRead } from "core/request/AirbyteClient"; + +import { TestingConnectionError } from "../../ConnectorForm/components/TestingConnectionError"; +import styles from "./TestCard.module.scss"; +import TestingConnectionSuccess from "./TestingConnectionSuccess"; + +interface IProps { + formType: "source" | "destination"; + isValid: boolean; + dirty: boolean; + onRetestClick: () => void; + onCancelTesting: () => void; + isTestConnectionInProgress?: boolean; + successMessage?: React.ReactNode; + errorMessage?: React.ReactNode; + job?: SynchronousJobRead; + isEditMode?: boolean; + connectionTestSuccess: boolean; +} + +const PROGRESS_BAR_TIME = 60 * 2; + +export const TestCard: React.FC = ({ + isTestConnectionInProgress, + isValid, + formType, + onRetestClick, + connectionTestSuccess, + errorMessage, + onCancelTesting, + job, + dirty, + isEditMode, +}) => { + const [logsVisible, setLogsVisible] = useState(false); + + const renderStatusMessage = () => { + if (errorMessage) { + return ( + + + {job && ( +
+ + {logsVisible && } +
+ )} +
+ ); + } + if (connectionTestSuccess) { + return ; + } + return null; + }; + + return ( + + + + + + + + + {isTestConnectionInProgress ? ( + + ) : ( + + )} + + {isTestConnectionInProgress ? ( + + + + ) : ( + renderStatusMessage() + )} + + + ); +}; diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/components/TestingConnectionSpinner.module.scss b/airbyte-webapp/src/views/Connector/ConnectorCard/components/TestingConnectionSpinner.module.scss similarity index 100% rename from airbyte-webapp/src/views/Connector/ConnectorForm/components/TestingConnectionSpinner.module.scss rename to airbyte-webapp/src/views/Connector/ConnectorCard/components/TestingConnectionSpinner.module.scss diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/components/TestingConnectionSpinner.tsx b/airbyte-webapp/src/views/Connector/ConnectorCard/components/TestingConnectionSpinner.tsx similarity index 100% rename from airbyte-webapp/src/views/Connector/ConnectorForm/components/TestingConnectionSpinner.tsx rename to airbyte-webapp/src/views/Connector/ConnectorCard/components/TestingConnectionSpinner.tsx diff --git a/airbyte-webapp/src/views/Connector/ConnectorCard/components/TestingConnectionSuccess.tsx b/airbyte-webapp/src/views/Connector/ConnectorCard/components/TestingConnectionSuccess.tsx new file mode 100644 index 000000000000..2f364342f160 --- /dev/null +++ b/airbyte-webapp/src/views/Connector/ConnectorCard/components/TestingConnectionSuccess.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import { FormattedMessage } from "react-intl"; + +import { FlexContainer } from "components/ui/Flex"; +import { StatusIcon } from "components/ui/StatusIcon"; +import { Text } from "components/ui/Text"; + +const TestingConnectionSuccess: React.FC = () => ( + + + + + + +); + +export default TestingConnectionSuccess; diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/ConnectorForm.test.tsx b/airbyte-webapp/src/views/Connector/ConnectorForm/ConnectorForm.test.tsx index c39270c4b493..d79346a66b07 100644 --- a/airbyte-webapp/src/views/Connector/ConnectorForm/ConnectorForm.test.tsx +++ b/airbyte-webapp/src/views/Connector/ConnectorForm/ConnectorForm.test.tsx @@ -8,7 +8,7 @@ import { render, useMockIntersectionObserver } from "test-utils/testutils"; import { ConnectorDefinition } from "core/domain/connector"; import { AirbyteJSONSchema } from "core/jsonSchema/types"; import { DestinationDefinitionSpecificationRead } from "core/request/AirbyteClient"; -import { ConnectorForm, ConnectorFormProps } from "views/Connector/ConnectorForm"; +import { ConnectorForm } from "views/Connector/ConnectorForm"; import { DocumentationPanelContext } from "../ConnectorDocumentationLayout/DocumentationPanelContext"; import { ConnectorFormValues } from "./types"; @@ -179,6 +179,7 @@ describe("Service Form", () => { formType="source" onSubmit={handleSubmit} selectedConnectorDefinition={connectorDefinition} + renderFooter={() => } selectedConnectorDefinitionSpecification={ // @ts-expect-error Partial objects for testing { @@ -260,6 +261,7 @@ describe("Service Form", () => { onSubmit={async (values) => { result = values; }} + renderFooter={() => } selectedConnectorDefinition={connectorDefinition} selectedConnectorDefinitionSpecification={ // @ts-expect-error Partial objects for testing @@ -390,42 +392,4 @@ describe("Service Form", () => { ]); }); }); - - describe("conditionally render form submit button", () => { - const renderConnectorForm = (props: ConnectorFormProps) => - render(); - // eslint-disable-next-line @typescript-eslint/no-empty-function - const onSubmitClb = async () => {}; - const connectorDefSpec = { - connectionSpecification: schema, - sourceDefinitionId: "test-service-type", - documentationUrl: "", - }; - - it("should render if connector is selected", async () => { - const { getByText } = await renderConnectorForm({ - selectedConnectorDefinition: connectorDefinition, - selectedConnectorDefinitionSpecification: - // @ts-expect-error Partial objects for testing - connectorDefSpec as DestinationDefinitionSpecificationRead, - formType: "destination", - onSubmit: onSubmitClb, - }); - expect(getByText(/Set up destination/)).toBeInTheDocument(); - }); - - it("should render if connector is selected", async () => { - const { getByText } = await renderConnectorForm({ - selectedConnectorDefinition: connectorDefinition, - selectedConnectorDefinitionSpecification: - // @ts-expect-error Partial objects for testing - connectorDefSpec as DestinationDefinitionSpecificationRead, - formType: "destination", - onSubmit: onSubmitClb, - isEditMode: true, - }); - - expect(getByText(/Save changes and test/)).toBeInTheDocument(); - }); - }); }); diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/ConnectorForm.tsx b/airbyte-webapp/src/views/Connector/ConnectorForm/ConnectorForm.tsx index f193a3f41e8c..b0c5cc98d1d7 100644 --- a/airbyte-webapp/src/views/Connector/ConnectorForm/ConnectorForm.tsx +++ b/airbyte-webapp/src/views/Connector/ConnectorForm/ConnectorForm.tsx @@ -1,5 +1,5 @@ import { Formik } from "formik"; -import React, { useCallback } from "react"; +import React, { ReactNode, useCallback } from "react"; import { FormChangeTracker } from "components/common/FormChangeTracker"; @@ -24,17 +24,31 @@ export interface ConnectorFormProps { * Definition of the connector might not be available if it's not released but only exists in frontend heap */ selectedConnectorDefinition?: ConnectorDefinition; - selectedConnectorDefinitionSpecification: ConnectorDefinitionSpecification | SourceDefinitionSpecificationDraft; + selectedConnectorDefinitionSpecification?: ConnectorDefinitionSpecification | SourceDefinitionSpecificationDraft; onSubmit: (values: ConnectorFormValues) => Promise; isEditMode?: boolean; formValues?: Partial; connectionTestSuccess?: boolean; errorMessage?: React.ReactNode; - successMessage?: React.ReactNode; connectorId?: string; footerClassName?: string; bodyClassName?: string; submitLabel?: string; + title?: React.ReactNode; + description?: React.ReactNode; + full?: boolean; + headerBlock?: ReactNode; + footerComponent?: React.FC; + renderFooter?: (formProps: { + dirty: boolean; + isSubmitting: boolean; + isValid: boolean; + resetConnectorForm: () => void; + isEditMode?: boolean; + formType: "source" | "destination"; + getValues: () => ConnectorFormValues; + }) => ReactNode; + renderWithCard?: boolean; /** * Called in case the user cancels the form - if not provided, no cancel button is rendered */ @@ -62,7 +76,6 @@ export const ConnectorForm: React.FC = (props) => { testConnector, selectedConnectorDefinition, selectedConnectorDefinitionSpecification, - errorMessage, connectorId, onReset, } = props; @@ -74,7 +87,7 @@ export const ConnectorForm: React.FC = (props) => { formValues ); - const getValues = useCallback( + const castValues = useCallback( (values: ConnectorFormValues) => validationSchema.cast(values, { stripUnknown: true, @@ -84,11 +97,11 @@ export const ConnectorForm: React.FC = (props) => { const onFormSubmit = useCallback( async (values: ConnectorFormValues) => { - const valuesToSend = getValues(values); + const valuesToSend = castValues(values); await onSubmit(valuesToSend); clearFormChange(formId); }, - [clearFormChange, formId, getValues, onSubmit] + [clearFormChange, formId, castValues, onSubmit] ); return ( @@ -103,7 +116,7 @@ export const ConnectorForm: React.FC = (props) => { {({ dirty, resetForm }) => ( = (props) => { { diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/FormRoot.module.scss b/airbyte-webapp/src/views/Connector/ConnectorForm/FormRoot.module.scss new file mode 100644 index 000000000000..dbb8d75cb985 --- /dev/null +++ b/airbyte-webapp/src/views/Connector/ConnectorForm/FormRoot.module.scss @@ -0,0 +1,5 @@ +@use "scss/variables"; + +.cardForm { + padding: variables.$spacing-xl; +} diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/FormRoot.tsx b/airbyte-webapp/src/views/Connector/ConnectorForm/FormRoot.tsx index fbc00004eafe..27a936af87e9 100644 --- a/airbyte-webapp/src/views/Connector/ConnectorForm/FormRoot.tsx +++ b/airbyte-webapp/src/views/Connector/ConnectorForm/FormRoot.tsx @@ -1,25 +1,40 @@ import { Form, useFormikContext } from "formik"; -import React from "react"; +import React, { ReactNode } from "react"; + +import { Card } from "components/ui/Card"; +import { FlexContainer } from "components/ui/Flex"; import { FormBlock } from "core/form/types"; -import CreateControls from "./components/CreateControls"; -import EditControls from "./components/EditControls"; import { FormSection } from "./components/Sections/FormSection"; import { useConnectorForm } from "./connectorFormContext"; +import styles from "./FormRoot.module.scss"; import { ConnectorFormValues } from "./types"; interface FormRootProps { + title?: React.ReactNode; + description?: React.ReactNode; + full?: boolean; formFields: FormBlock; connectionTestSuccess?: boolean; isTestConnectionInProgress?: boolean; - errorMessage?: React.ReactNode; - successMessage?: React.ReactNode; onRetest?: () => void; onStopTestingConnector?: () => void; submitLabel?: string; footerClassName?: string; bodyClassName?: string; + headerBlock?: ReactNode; + castValues: (values: ConnectorFormValues) => ConnectorFormValues; + renderFooter?: (formProps: { + dirty: boolean; + isSubmitting: boolean; + isValid: boolean; + resetConnectorForm: () => void; + isEditMode?: boolean; + formType: "source" | "destination"; + getValues: () => ConnectorFormValues; + }) => ReactNode; + renderWithCard?: boolean; /** * Called in case the user cancels the form - if not provided, no cancel button is rendered */ @@ -32,56 +47,49 @@ interface FormRootProps { export const FormRoot: React.FC = ({ isTestConnectionInProgress = false, - onRetest, formFields, - successMessage, - errorMessage, - connectionTestSuccess, - onStopTestingConnector, - submitLabel, - footerClassName, bodyClassName, - onCancel, - onReset, + headerBlock, + renderFooter, + title, + description, + renderWithCard, + castValues, + full, }) => { - const { dirty, isSubmitting, isValid } = useFormikContext(); + const { dirty, isSubmitting, isValid, values } = useFormikContext(); const { resetConnectorForm, isEditMode, formType } = useConnectorForm(); - return ( -
+ const formBody = ( + <> + {headerBlock}
-
- {isEditMode ? ( - { - resetConnectorForm(); - }} - successMessage={successMessage} - /> + + ); + + return ( + + + {renderWithCard ? ( + +
{formBody}
+
) : ( - + formBody )} -
+ {renderFooter && + renderFooter({ + dirty, + isSubmitting, + isValid, + resetConnectorForm, + isEditMode, + formType, + getValues: () => castValues(values), + })} +
); }; diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/components/CreateControls.module.scss b/airbyte-webapp/src/views/Connector/ConnectorForm/components/CreateControls.module.scss deleted file mode 100644 index d93a86168241..000000000000 --- a/airbyte-webapp/src/views/Connector/ConnectorForm/components/CreateControls.module.scss +++ /dev/null @@ -1,19 +0,0 @@ -@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; -} - -.deleteButtonContainer { - flex-grow: 1; -} diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/components/CreateControls.tsx b/airbyte-webapp/src/views/Connector/ConnectorForm/components/CreateControls.tsx deleted file mode 100644 index 425157a83cc7..000000000000 --- a/airbyte-webapp/src/views/Connector/ConnectorForm/components/CreateControls.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React from "react"; -import { FormattedMessage } from "react-intl"; - -import { Button } from "components/ui/Button"; - -import styles from "./CreateControls.module.scss"; -import { TestingConnectionError } from "./TestingConnectionError"; -import { TestingConnectionSpinner } from "./TestingConnectionSpinner"; -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; - /** - * Called in case the user reset the form - if not provided, no reset button is rendered - */ - onReset?: () => void; - submitLabel?: string; - isSubmitting: boolean; - errorMessage?: React.ReactNode; - connectionTestSuccess?: boolean; - - isTestConnectionInProgress: boolean; - onCancelTesting?: () => void; -} - -const CreateControls: React.FC = ({ - isTestConnectionInProgress, - isSubmitting, - formType, - connectionTestSuccess, - errorMessage, - onCancelTesting, - onCancel, - onReset, - submitLabel, -}) => { - if (isSubmitting) { - return ; - } - - if (connectionTestSuccess) { - return ; - } - - return ( -
- {errorMessage && } - {onReset && ( -
- -
- )} -
- {onCancel && ( - - )} - -
-
- ); -}; - -export default CreateControls; diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/components/EditControls.module.scss b/airbyte-webapp/src/views/Connector/ConnectorForm/components/EditControls.module.scss deleted file mode 100644 index 0976e17e7de4..000000000000 --- a/airbyte-webapp/src/views/Connector/ConnectorForm/components/EditControls.module.scss +++ /dev/null @@ -1,16 +0,0 @@ -@use "scss/variables"; - -.controlsContainer { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: variables.$spacing-lg; -} - -.buttonsContainer { - display: flex; -} - -.cancelButton { - margin-left: 10px; -} diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/components/EditControls.tsx b/airbyte-webapp/src/views/Connector/ConnectorForm/components/EditControls.tsx deleted file mode 100644 index ba6bd0246089..000000000000 --- a/airbyte-webapp/src/views/Connector/ConnectorForm/components/EditControls.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React from "react"; -import { FormattedMessage } from "react-intl"; - -import { Button } from "components/ui/Button"; - -import styles from "./EditControls.module.scss"; -import { TestingConnectionError } from "./TestingConnectionError"; -import { TestingConnectionSpinner } from "./TestingConnectionSpinner"; -import TestingConnectionSuccess from "./TestingConnectionSuccess"; - -interface IProps { - formType: "source" | "destination"; - isSubmitting: boolean; - isValid: boolean; - dirty: boolean; - onCancelClick: () => void; - onRetestClick?: () => void; - onCancelTesting?: () => void; - isTestConnectionInProgress?: boolean; - successMessage?: React.ReactNode; - errorMessage?: React.ReactNode; -} - -const EditControls: React.FC = ({ - isSubmitting, - isTestConnectionInProgress, - isValid, - dirty, - onCancelClick, - formType, - onRetestClick, - successMessage, - errorMessage, - onCancelTesting, -}) => { - if (isSubmitting) { - return ; - } - - const renderStatusMessage = () => { - if (errorMessage) { - return ; - } - if (successMessage) { - return ; - } - return null; - }; - - return ( - <> -
-
- - -
- {onRetestClick && ( - - )} -
- {renderStatusMessage()} - - ); -}; - -export default EditControls; 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 12b38123404d..db4a059f33e5 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 @@ -9,7 +9,10 @@ import { AuthButton } from "./AuthButton"; export const AuthSection: React.FC = () => { const { selectedConnectorDefinitionSpecification } = useConnectorForm(); - if (isSourceDefinitionSpecificationDraft(selectedConnectorDefinitionSpecification)) { + if ( + !selectedConnectorDefinitionSpecification || + isSourceDefinitionSpecificationDraft(selectedConnectorDefinitionSpecification) + ) { return null; } return ( diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/components/TestingConnectionError.tsx b/airbyte-webapp/src/views/Connector/ConnectorForm/components/TestingConnectionError.tsx index f88f3e9ef602..e9538929578e 100644 --- a/airbyte-webapp/src/views/Connector/ConnectorForm/components/TestingConnectionError.tsx +++ b/airbyte-webapp/src/views/Connector/ConnectorForm/components/TestingConnectionError.tsx @@ -1,44 +1,24 @@ import React from "react"; import { FormattedMessage } from "react-intl"; -import styled from "styled-components"; +import { Callout } from "components/ui/Callout"; +import { FlexContainer } from "components/ui/Flex"; import { StatusIcon } from "components/ui/StatusIcon"; - -const Error = styled(StatusIcon)` - padding-left: 1px; - width: 26px; - min-width: 26px; - height: 26px; - padding-top: 5px; - font-size: 17px; -`; - -const ErrorBlock = styled.div` - display: flex; - align-items: center; - font-weight: 600; - font-size: 12px; - line-height: 18px; - color: ${({ theme }) => theme.darkPrimaryColor}; -`; - -const ErrorText = styled.div` - font-weight: normal; - color: ${({ theme }) => theme.dangerColor}; - max-width: 400px; -`; +import { Text } from "components/ui/Text"; const ErrorSection: React.FC<{ errorTitle: React.ReactNode; errorMessage: React.ReactNode; }> = ({ errorMessage, errorTitle }) => ( - - -
- {errorTitle} - {errorMessage} -
-
+ + + + + {errorTitle} + {errorMessage} + + + ); const TestingConnectionError: React.FC<{ errorMessage: React.ReactNode }> = ({ errorMessage }) => ( diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/components/TestingConnectionSuccess.tsx b/airbyte-webapp/src/views/Connector/ConnectorForm/components/TestingConnectionSuccess.tsx deleted file mode 100644 index ec07619a3590..000000000000 --- a/airbyte-webapp/src/views/Connector/ConnectorForm/components/TestingConnectionSuccess.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from "react"; -import { FormattedMessage } from "react-intl"; -import styled from "styled-components"; - -import { StatusIcon } from "components/ui/StatusIcon"; - -const LoadingContainer = styled.div` - font-weight: 600; - font-size: 14px; - line-height: 17px; - color: ${({ theme }) => theme.darkPrimaryColor}; - margin-top: 34px; - display: flex; - align-items: center; - justify-content: center; -`; - -const Success = styled(StatusIcon)` - width: 26px; - min-width: 26px; - height: 26px; - padding-top: 5px; - font-size: 17px; -`; - -const TestingConnectionSuccess: React.FC = () => ( - - - - -); - -export default TestingConnectionSuccess; diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/connectorFormContext.tsx b/airbyte-webapp/src/views/Connector/ConnectorForm/connectorFormContext.tsx index a222e14c59c9..4389e49fcf74 100644 --- a/airbyte-webapp/src/views/Connector/ConnectorForm/connectorFormContext.tsx +++ b/airbyte-webapp/src/views/Connector/ConnectorForm/connectorFormContext.tsx @@ -15,7 +15,7 @@ interface ConnectorFormContext { getValues: (values: ConnectorFormValues) => ConnectorFormValues; resetConnectorForm: () => void; selectedConnectorDefinition?: ConnectorDefinition; - selectedConnectorDefinitionSpecification: ConnectorDefinitionSpecification | SourceDefinitionSpecificationDraft; + selectedConnectorDefinitionSpecification?: ConnectorDefinitionSpecification | SourceDefinitionSpecificationDraft; isEditMode?: boolean; validationSchema: AnySchema; connectorId?: string; @@ -36,7 +36,7 @@ interface ConnectorFormContextProviderProps { formType: "source" | "destination"; isEditMode?: boolean; getValues: (values: ConnectorFormValues) => ConnectorFormValues; - selectedConnectorDefinitionSpecification: ConnectorDefinitionSpecification | SourceDefinitionSpecificationDraft; + selectedConnectorDefinitionSpecification?: ConnectorDefinitionSpecification | SourceDefinitionSpecificationDraft; validationSchema: AnySchema; connectorId?: string; } diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/useBuildForm.tsx b/airbyte-webapp/src/views/Connector/ConnectorForm/useBuildForm.tsx index cb4e76b6bde3..6a009380795e 100644 --- a/airbyte-webapp/src/views/Connector/ConnectorForm/useBuildForm.tsx +++ b/airbyte-webapp/src/views/Connector/ConnectorForm/useBuildForm.tsx @@ -61,14 +61,26 @@ export function setDefaultValues( export function useBuildForm( isEditMode: boolean, formType: "source" | "destination", - selectedConnectorDefinitionSpecification: ConnectorDefinitionSpecification | SourceDefinitionSpecificationDraft, + selectedConnectorDefinitionSpecification: + | ConnectorDefinitionSpecification + | SourceDefinitionSpecificationDraft + | undefined, initialValues?: Partial ): BuildFormHook { const { formatMessage } = useIntl(); - const isDraft = isSourceDefinitionSpecificationDraft(selectedConnectorDefinitionSpecification); + + const isDraft = + selectedConnectorDefinitionSpecification && + isSourceDefinitionSpecificationDraft(selectedConnectorDefinitionSpecification); try { const jsonSchema: JSONSchema7 = useMemo(() => { + if (!selectedConnectorDefinitionSpecification) { + return { + type: "object", + properties: {}, + }; + } const schema: JSONSchema7 = { type: "object", properties: { @@ -89,7 +101,7 @@ export function useBuildForm( }; schema.required = ["name"]; return schema; - }, [formType, formatMessage, isDraft, selectedConnectorDefinitionSpecification.connectionSpecification]); + }, [formType, formatMessage, isDraft, selectedConnectorDefinitionSpecification]); const formFields = useMemo(() => jsonSchemaToFormBlock(jsonSchema), [jsonSchema]); @@ -119,7 +131,7 @@ export function useBuildForm( return baseValues; } - setDefaultValues(formFields, baseValues as Record, { respectExistingValues: isDraft }); + setDefaultValues(formFields, baseValues as Record, { respectExistingValues: Boolean(isDraft) }); return baseValues; }, [formFields, initialValues, isDraft, isEditMode, validationSchema]); @@ -134,7 +146,9 @@ export function useBuildForm( if (isFormBuildError(e)) { throw new FormBuildError( e.message, - isDraft ? undefined : ConnectorSpecification.id(selectedConnectorDefinitionSpecification) + isDraft || !selectedConnectorDefinitionSpecification + ? undefined + : ConnectorSpecification.id(selectedConnectorDefinitionSpecification) ); } throw e; From 5feec240064b5ae3feb579686d9feffacaa565df Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 25 Jan 2023 13:31:13 +0100 Subject: [PATCH 2/5] review comments --- .../StreamTestingPanel/ConfigMenu.tsx | 13 +--- .../Connector/ConnectorCard/ConnectorCard.tsx | 42 ++++++----- .../ConnectorCard/components/Controls.tsx | 3 +- .../ConnectorCard/components/TestCard.tsx | 4 +- .../Connector/ConnectorForm/ConnectorForm.tsx | 72 ++++++------------- .../Connector/ConnectorForm/FormRoot.tsx | 40 +++++------ 6 files changed, 62 insertions(+), 112 deletions(-) diff --git a/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/ConfigMenu.tsx b/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/ConfigMenu.tsx index ebbb5ed354e2..696e02fc6125 100644 --- a/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/ConfigMenu.tsx +++ b/airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/ConfigMenu.tsx @@ -1,7 +1,7 @@ import { faClose, faUser } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useMemo } from "react"; -import { FormattedMessage, useIntl } from "react-intl"; +import { FormattedMessage } from "react-intl"; import { useLocalStorage } from "react-use"; import { Button } from "components/ui/Button"; @@ -28,7 +28,6 @@ interface ConfigMenuProps { } export const ConfigMenu: React.FC = ({ className, testInputJsonErrors, isOpen, setIsOpen }) => { - const { formatMessage } = useIntl(); const { jsonManifest, editorView, setEditorView } = useConnectorBuilderFormState(); const { testInputJson, setTestInputJson } = useConnectorBuilderTestState(); @@ -119,12 +118,13 @@ export const ConfigMenu: React.FC = ({ className, testInputJson setTestInputJson(values.connectionConfiguration as StreamReadRequestBodyConfig); setIsOpen(false); }} - renderFooter={({ dirty, isSubmitting }) => ( + renderFooter={({ dirty, isSubmitting, resetConnectorForm }) => (
)} - onCancel={() => { - setIsOpen(false); - }} - onReset={() => { - setTestInputJson({}); - }} - submitLabel={formatMessage({ id: "connectorBuilder.saveInputsForm" })} /> diff --git a/airbyte-webapp/src/views/Connector/ConnectorCard/ConnectorCard.tsx b/airbyte-webapp/src/views/Connector/ConnectorCard/ConnectorCard.tsx index d047d3b32ac6..2c8802727703 100644 --- a/airbyte-webapp/src/views/Connector/ConnectorCard/ConnectorCard.tsx +++ b/airbyte-webapp/src/views/Connector/ConnectorCard/ConnectorCard.tsx @@ -12,12 +12,7 @@ import { import { DestinationRead, SourceRead, SynchronousJobRead } from "core/request/AirbyteClient"; import { LogsRequestError } from "core/request/LogsRequestError"; import { generateMessageFromError } from "utils/errorStatusMessage"; -import { - ConnectorCardValues, - ConnectorForm, - ConnectorFormProps, - ConnectorFormValues, -} from "views/Connector/ConnectorForm"; +import { ConnectorCardValues, ConnectorForm, ConnectorFormValues } from "views/Connector/ConnectorForm"; import { useDocumentationPanelContext } from "../ConnectorDocumentationLayout/DocumentationPanelContext"; import { ConnectorDefinitionTypeControl } from "../ConnectorForm/components/Controls/ConnectorServiceTypeControl"; @@ -131,9 +126,25 @@ export const ConnectorCard: React.FC { + const testConnectorWithTracking = async (connectorCardValues: ConnectorCardValues) => { + trackTestConnectorStarted(selectedConnectorDefinition); + try { + await testConnector(connectorCardValues); + trackTestConnectorSuccess(selectedConnectorDefinition); + } catch (e) { + trackTestConnectorFailure(selectedConnectorDefinition); + throw e; + } + }; + + const handleTestConnector = async (values: ConnectorCardValues) => { setErrorStatusRequest(null); - return testConnector(v); + try { + await testConnectorWithTracking(values); + } catch (e) { + setErrorStatusRequest(e); + throw e; + } }; const onHandleSubmit = async (values: ConnectorFormValues) => { @@ -149,19 +160,8 @@ export const ConnectorCard: React.FC { - trackTestConnectorStarted(selectedConnectorDefinition); - try { - await testConnector(connectorCardValues); - trackTestConnectorSuccess(selectedConnectorDefinition); - } catch (e) { - trackTestConnectorFailure(selectedConnectorDefinition); - throw e; - } - }; - try { - await testConnectorWithTracking(); + await testConnectorWithTracking(connectorCardValues); onSubmit(connectorCardValues); } catch (e) { setErrorStatusRequest(e); @@ -217,8 +217,6 @@ export const ConnectorCard: React.FC = ({ formType={formType} isTestConnectionInProgress={isTestConnectionInProgress} isEditMode={isEditMode} - dirty={dirty} /> )} @@ -60,7 +59,7 @@ export const Controls: React.FC = ({ )} - diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/ConnectorForm.tsx b/airbyte-webapp/src/views/Connector/ConnectorForm/ConnectorForm.tsx index b0c5cc98d1d7..a5c14439c997 100644 --- a/airbyte-webapp/src/views/Connector/ConnectorForm/ConnectorForm.tsx +++ b/airbyte-webapp/src/views/Connector/ConnectorForm/ConnectorForm.tsx @@ -1,5 +1,5 @@ import { Formik } from "formik"; -import React, { ReactNode, useCallback } from "react"; +import React, { useCallback, useMemo } from "react"; import { FormChangeTracker } from "components/common/FormChangeTracker"; @@ -9,15 +9,14 @@ import { SourceDefinitionSpecificationDraft, } from "core/domain/connector"; import { FormikPatch } from "core/form/FormikPatch"; -import { CheckConnectionRead } from "core/request/AirbyteClient"; import { useFormChangeTrackerService, useUniqueFormId } from "hooks/services/FormChangeTracker"; import { ConnectorFormContextProvider } from "./connectorFormContext"; -import { FormRoot } from "./FormRoot"; -import { ConnectorCardValues, ConnectorFormValues } from "./types"; +import { BaseFormRootProps, FormRoot } from "./FormRoot"; +import { ConnectorFormValues } from "./types"; import { useBuildForm } from "./useBuildForm"; -export interface ConnectorFormProps { +interface BaseConnectorFormProps extends Omit { formType: "source" | "destination"; formId?: string; /** @@ -28,41 +27,22 @@ export interface ConnectorFormProps { onSubmit: (values: ConnectorFormValues) => Promise; isEditMode?: boolean; formValues?: Partial; - connectionTestSuccess?: boolean; - errorMessage?: React.ReactNode; connectorId?: string; - footerClassName?: string; - bodyClassName?: string; - submitLabel?: string; +} + +interface CardConnectorFormProps extends BaseConnectorFormProps { + renderWithCard: true; title?: React.ReactNode; description?: React.ReactNode; full?: boolean; - headerBlock?: ReactNode; - footerComponent?: React.FC; - renderFooter?: (formProps: { - dirty: boolean; - isSubmitting: boolean; - isValid: boolean; - resetConnectorForm: () => void; - isEditMode?: boolean; - formType: "source" | "destination"; - getValues: () => ConnectorFormValues; - }) => ReactNode; - renderWithCard?: boolean; - /** - * Called in case the user cancels the form - if not provided, no cancel button is rendered - */ - onCancel?: () => void; - /** - * Called in case the user reset the form - if not provided, no reset button is rendered - */ - onReset?: () => void; +} - isTestConnectionInProgress?: boolean; - onStopTesting?: () => void; - testConnector?: (v?: ConnectorCardValues) => Promise; +interface BareConnectorFormProps extends BaseConnectorFormProps { + renderWithCard?: false; } +export type ConnectorFormProps = CardConnectorFormProps | BareConnectorFormProps; + export const ConnectorForm: React.FC = (props) => { const formId = useUniqueFormId(props.formId); const { clearFormChange } = useFormChangeTrackerService(); @@ -72,12 +52,9 @@ export const ConnectorForm: React.FC = (props) => { formValues, onSubmit, isEditMode, - onStopTesting, - testConnector, selectedConnectorDefinition, selectedConnectorDefinitionSpecification, connectorId, - onReset, } = props; const { formFields, initialValues, validationSchema } = useBuildForm( @@ -104,16 +81,22 @@ export const ConnectorForm: React.FC = (props) => { [clearFormChange, formId, castValues, onSubmit] ); + const isInitialValid = useMemo( + () => Boolean(validationSchema.isValidSync(initialValues)), + [initialValues, validationSchema] + ); + return ( - {({ dirty, resetForm }) => ( + {({ dirty }) => ( = (props) => { > - { - onReset?.(); - resetForm(); - }) - } - onStopTestingConnector={onStopTesting ? () => onStopTesting() : undefined} - onRetest={testConnector ? async () => await testConnector() : undefined} - /> + )} diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/FormRoot.tsx b/airbyte-webapp/src/views/Connector/ConnectorForm/FormRoot.tsx index 27a936af87e9..8d5a2b8c0c8f 100644 --- a/airbyte-webapp/src/views/Connector/ConnectorForm/FormRoot.tsx +++ b/airbyte-webapp/src/views/Connector/ConnectorForm/FormRoot.tsx @@ -11,17 +11,10 @@ import { useConnectorForm } from "./connectorFormContext"; import styles from "./FormRoot.module.scss"; import { ConnectorFormValues } from "./types"; -interface FormRootProps { - title?: React.ReactNode; - description?: React.ReactNode; - full?: boolean; +export interface BaseFormRootProps { formFields: FormBlock; connectionTestSuccess?: boolean; isTestConnectionInProgress?: boolean; - onRetest?: () => void; - onStopTestingConnector?: () => void; - submitLabel?: string; - footerClassName?: string; bodyClassName?: string; headerBlock?: ReactNode; castValues: (values: ConnectorFormValues) => ConnectorFormValues; @@ -34,28 +27,27 @@ interface FormRootProps { formType: "source" | "destination"; getValues: () => ConnectorFormValues; }) => ReactNode; - renderWithCard?: boolean; - /** - * Called in case the user cancels the form - if not provided, no cancel button is rendered - */ - onCancel?: () => void; - /** - * Called in case the user reset the form - if not provided, no reset button is rendered - */ - onReset?: () => void; } -export const FormRoot: React.FC = ({ +interface CardFormRootProps extends BaseFormRootProps { + renderWithCard: true; + title?: React.ReactNode; + description?: React.ReactNode; + full?: boolean; +} + +interface BareFormRootProps extends BaseFormRootProps { + renderWithCard?: false; +} + +export const FormRoot: React.FC = ({ isTestConnectionInProgress = false, formFields, bodyClassName, headerBlock, renderFooter, - title, - description, - renderWithCard, castValues, - full, + ...props }) => { const { dirty, isSubmitting, isValid, values } = useFormikContext(); const { resetConnectorForm, isEditMode, formType } = useConnectorForm(); @@ -72,8 +64,8 @@ export const FormRoot: React.FC = ({ return (
- {renderWithCard ? ( - + {props.renderWithCard ? ( +
{formBody}
) : ( From 3c5def48731f5b3253f77a1551a5a17472e9798d Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 26 Jan 2023 12:24:24 +0100 Subject: [PATCH 3/5] do not pass changes to retest source --- airbyte-webapp/src/locales/en.json | 4 ++-- .../src/views/Connector/ConnectorCard/ConnectorCard.tsx | 8 +++++--- .../views/Connector/ConnectorCard/components/Controls.tsx | 1 + .../views/Connector/ConnectorCard/components/TestCard.tsx | 5 ++++- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/airbyte-webapp/src/locales/en.json b/airbyte-webapp/src/locales/en.json index 64ca79d63ebc..7eeb523a2b8a 100644 --- a/airbyte-webapp/src/locales/en.json +++ b/airbyte-webapp/src/locales/en.json @@ -63,8 +63,8 @@ "form.openDatepicker": "Open datepicker", "form.datepickerTimeCaption": "Time (UTC)", "form.saveChangesAndTest": "Test and save", - "form.sourceRetest": "Retest source", - "form.destinationRetest": "Retest destination", + "form.sourceRetest": "Retest saved source", + "form.destinationRetest": "Retest saved destination", "form.test": "Test", "form.sourceRetestTitle": "Test the source", "form.destinationRetestTitle": "Test the destination", diff --git a/airbyte-webapp/src/views/Connector/ConnectorCard/ConnectorCard.tsx b/airbyte-webapp/src/views/Connector/ConnectorCard/ConnectorCard.tsx index 2c8802727703..12b5b4ea463e 100644 --- a/airbyte-webapp/src/views/Connector/ConnectorCard/ConnectorCard.tsx +++ b/airbyte-webapp/src/views/Connector/ConnectorCard/ConnectorCard.tsx @@ -126,7 +126,7 @@ export const ConnectorCard: React.FC { + const testConnectorWithTracking = async (connectorCardValues?: ConnectorCardValues) => { trackTestConnectorStarted(selectedConnectorDefinition); try { await testConnector(connectorCardValues); @@ -137,7 +137,7 @@ export const ConnectorCard: React.FC { + const handleTestConnector = async (values?: ConnectorCardValues) => { setErrorStatusRequest(null); try { await testConnectorWithTracking(values); @@ -233,7 +233,9 @@ export const ConnectorCard: React.FC = ({ {hasDefinition && ( = ({ onCancelTesting, job, isEditMode, + dirty, }) => { const [logsVisible, setLogsVisible] = useState(false); @@ -98,7 +100,8 @@ export const TestCard: React.FC = ({ onClick={onRetestClick} variant="secondary" icon={} - disabled={!isValid} + // disable if there are changes in edit mode because the retest API can currently only test the saved state + disabled={!isValid || (isEditMode && dirty)} > From 9207ec173c19fa0b3d53aa2726862d29ec25704f Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 27 Jan 2023 12:06:25 +0100 Subject: [PATCH 4/5] reorder imports --- .../src/views/Connector/ConnectorCard/ConnectorCard.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/airbyte-webapp/src/views/Connector/ConnectorCard/ConnectorCard.tsx b/airbyte-webapp/src/views/Connector/ConnectorCard/ConnectorCard.tsx index 12b5b4ea463e..55a474a0c17d 100644 --- a/airbyte-webapp/src/views/Connector/ConnectorCard/ConnectorCard.tsx +++ b/airbyte-webapp/src/views/Connector/ConnectorCard/ConnectorCard.tsx @@ -14,14 +14,14 @@ import { LogsRequestError } from "core/request/LogsRequestError"; import { generateMessageFromError } from "utils/errorStatusMessage"; import { ConnectorCardValues, ConnectorForm, ConnectorFormValues } from "views/Connector/ConnectorForm"; -import { useDocumentationPanelContext } from "../ConnectorDocumentationLayout/DocumentationPanelContext"; -import { ConnectorDefinitionTypeControl } from "../ConnectorForm/components/Controls/ConnectorServiceTypeControl"; -import { FetchingConnectorError } from "../ConnectorForm/components/TestingConnectionError"; import { Controls } from "./components/Controls"; import ShowLoadingMessage from "./components/ShowLoadingMessage"; import styles from "./ConnectorCard.module.scss"; import { useAnalyticsTrackFunctions } from "./useAnalyticsTrackFunctions"; import { useTestConnector } from "./useTestConnector"; +import { useDocumentationPanelContext } from "../ConnectorDocumentationLayout/DocumentationPanelContext"; +import { ConnectorDefinitionTypeControl } from "../ConnectorForm/components/Controls/ConnectorServiceTypeControl"; +import { FetchingConnectorError } from "../ConnectorForm/components/TestingConnectionError"; // TODO: need to clean up the ConnectorCard and ConnectorForm props, // since some of props are used in both components, and some of them used just as a prop-drill From 1ff8752748c3a3cce4680f5c7482bad1ebb80b42 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 27 Jan 2023 12:32:29 +0100 Subject: [PATCH 5/5] fix order of imports --- .../src/views/Connector/ConnectorCard/components/TestCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airbyte-webapp/src/views/Connector/ConnectorCard/components/TestCard.tsx b/airbyte-webapp/src/views/Connector/ConnectorCard/components/TestCard.tsx index 24f2eb5ff114..75dac5a0e05b 100644 --- a/airbyte-webapp/src/views/Connector/ConnectorCard/components/TestCard.tsx +++ b/airbyte-webapp/src/views/Connector/ConnectorCard/components/TestCard.tsx @@ -12,9 +12,9 @@ import { Text } from "components/ui/Text"; import { SynchronousJobRead } from "core/request/AirbyteClient"; -import { TestingConnectionError } from "../../ConnectorForm/components/TestingConnectionError"; import styles from "./TestCard.module.scss"; import TestingConnectionSuccess from "./TestingConnectionSuccess"; +import { TestingConnectionError } from "../../ConnectorForm/components/TestingConnectionError"; interface IProps { formType: "source" | "destination";