From 10b4ff1080a1cb81d1af79fe52abbb96e9941dc4 Mon Sep 17 00:00:00 2001 From: KC Date: Wed, 17 Aug 2022 11:52:33 -0400 Subject: [PATCH 001/107] Work started --- .../CreateConnectionContent.tsx | 33 ++++++++----------- .../services/Connection/ConnectionService.ts | 9 +++++ .../src/hooks/services/Modal/ModalService.tsx | 2 ++ .../src/hooks/services/useSourceHook.tsx | 11 +++---- .../components/ReplicationView.tsx | 7 ++-- .../CreationFormPage/CreationFormPage.tsx | 23 +------------ .../ConnectionForm/ConnectionForm.tsx | 18 +++++----- 7 files changed, 43 insertions(+), 60 deletions(-) create mode 100644 airbyte-webapp/src/hooks/services/Connection/ConnectionService.ts diff --git a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx index 6c2108711a77..f0e21e8f7340 100644 --- a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx +++ b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx @@ -1,4 +1,4 @@ -import React, { Suspense, useMemo } from "react"; +import React, { Suspense } from "react"; import styled from "styled-components"; import { ContentCard } from "components"; @@ -10,10 +10,10 @@ import { Action, Namespace } from "core/analytics"; import { LogsRequestError } from "core/request/LogsRequestError"; import { useAnalyticsService } from "hooks/services/Analytics"; import { useCreateConnection, ValuesProps } from "hooks/services/useConnectionHook"; +import useRouter from "hooks/useRouter"; import ConnectionForm from "views/Connection/ConnectionForm"; -import { ConnectionFormProps } from "views/Connection/ConnectionForm/ConnectionForm"; -import { DestinationRead, SourceRead, WebBackendConnectionRead } from "../../core/request/AirbyteClient"; +import { DestinationRead, SourceRead } from "../../core/request/AirbyteClient"; import { useDiscoverSchema } from "../../hooks/services/useSourceHook"; import TryAfterErrorBlock from "./components/TryAfterErrorBlock"; @@ -30,7 +30,7 @@ interface CreateConnectionContentProps { additionBottomControls?: React.ReactNode; source: SourceRead; destination: DestinationRead; - afterSubmitConnection?: (connection: WebBackendConnectionRead) => void; + afterSubmitConnection?: () => void; } const CreateConnectionContent: React.FC = ({ @@ -41,21 +41,19 @@ const CreateConnectionContent: React.FC = ({ }) => { const { mutateAsync: createConnection } = useCreateConnection(); const analyticsService = useAnalyticsService(); + const { push } = useRouter(); const { schema, isLoading, schemaErrorStatus, catalogId, onDiscoverSchema } = useDiscoverSchema(source.sourceId); - const connection = useMemo( - () => ({ - syncCatalog: schema, - destination, - source, - catalogId, - }), - [schema, destination, source, catalogId] - ); + const connection = { + syncCatalog: schema, + destination, + source, + catalogId, + }; const onSubmitConnectionStep = async (values: ValuesProps) => { - const connection = await createConnection({ + const createdConnection = await createConnection({ values, source, destination, @@ -69,11 +67,7 @@ const CreateConnectionContent: React.FC = ({ sourceCatalogId: catalogId, }); - return { - onSubmitComplete: () => { - afterSubmitConnection?.(connection); - }, - }; + push(`../${createdConnection.connectionId}`); }; const onSelectFrequency = (item: IDataItem | null) => { @@ -116,6 +110,7 @@ const CreateConnectionContent: React.FC = ({ additionBottomControls={additionBottomControls} onDropDownSelect={onSelectFrequency} onSubmit={onSubmitConnectionStep} + onAfterSubmit={afterSubmitConnection} /> ); diff --git a/airbyte-webapp/src/hooks/services/Connection/ConnectionService.ts b/airbyte-webapp/src/hooks/services/Connection/ConnectionService.ts new file mode 100644 index 000000000000..ebc25847b7aa --- /dev/null +++ b/airbyte-webapp/src/hooks/services/Connection/ConnectionService.ts @@ -0,0 +1,9 @@ +// import { useDiscoverSchema } from "../useSourceHook"; + +export const useConnectionService = (/*sourceId: string*/) => { + // const { schema, isLoading, schemaErrorStatus, catalogId } = useDiscoverSchema(sourceId); + + // const createConnection = + + return null; +}; diff --git a/airbyte-webapp/src/hooks/services/Modal/ModalService.tsx b/airbyte-webapp/src/hooks/services/Modal/ModalService.tsx index 398357690187..020dae551024 100644 --- a/airbyte-webapp/src/hooks/services/Modal/ModalService.tsx +++ b/airbyte-webapp/src/hooks/services/Modal/ModalService.tsx @@ -5,6 +5,8 @@ import { Modal } from "components"; import { ModalOptions, ModalResult, ModalServiceContext } from "./types"; +export class ModalCancel extends Error {} + const modalServiceContext = React.createContext(undefined); export const ModalServiceProvider: React.FC = ({ children }) => { diff --git a/airbyte-webapp/src/hooks/services/useSourceHook.tsx b/airbyte-webapp/src/hooks/services/useSourceHook.tsx index e1ff67503a61..61efc7d2d68c 100644 --- a/airbyte-webapp/src/hooks/services/useSourceHook.tsx +++ b/airbyte-webapp/src/hooks/services/useSourceHook.tsx @@ -182,15 +182,12 @@ const useDiscoverSchema = ( } finally { setIsLoading(false); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [sourceId]); + }, [service, sourceId]); useEffect(() => { - (async () => { - if (sourceId) { - await onDiscoverSchema(); - } - })(); + if (sourceId) { + onDiscoverSchema(); + } }, [onDiscoverSchema, sourceId]); return { schemaErrorStatus, isLoading, schema, catalogId, onDiscoverSchema }; diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx index 367433ef0ece..ca6af5f17087 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx @@ -11,7 +11,7 @@ import LoadingSchema from "components/LoadingSchema"; import { toWebBackendConnectionUpdate } from "core/domain/connection"; import { ConnectionStateType, ConnectionStatus } from "core/request/AirbyteClient"; import { useConfirmationModalService } from "hooks/services/ConfirmationModal"; -import { useModalService } from "hooks/services/Modal"; +import { ModalCancel, useModalService } from "hooks/services/Modal"; import { useConnectionLoad, useConnectionService, @@ -154,9 +154,8 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch content: (props) => , }); if (result.type === "canceled") { - return { - submitCancelled: true, - }; + // eslint-disable-next-line no-throw-literal + throw new ModalCancel(); } // Save the connection taking into account the correct skipRefresh value from the dialog choice. await saveConnection(values, { skipReset: !result.reason }); diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/CreationFormPage/CreationFormPage.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/CreationFormPage/CreationFormPage.tsx index ece8c4bba86e..609f27019929 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/CreationFormPage/CreationFormPage.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/CreationFormPage/CreationFormPage.tsx @@ -21,7 +21,6 @@ import { DestinationRead, SourceDefinitionRead, SourceRead, - WebBackendConnectionRead, } from "../../../../core/request/AirbyteClient"; import { ConnectionCreateDestinationForm } from "./components/DestinationForm"; import ExistingEntityForm from "./components/ExistingEntityForm"; @@ -160,32 +159,12 @@ export const CreationFormPage: React.FC = () => { } } - const afterSubmitConnection = (connection: WebBackendConnectionRead) => { - switch (type) { - case EntityStepsTypes.DESTINATION: - push(`../${source?.sourceId}`); - break; - case EntityStepsTypes.SOURCE: - push(`../${destination?.destinationId}`); - break; - default: - push(`../${connection.connectionId}`); - break; - } - }; - if (!source || !destination) { console.error("unexpected state met"); return ; } - return ( - - ); + return ; }; const steps = diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx index 9fa2eee3e5a5..590039208afb 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx @@ -9,6 +9,7 @@ import { FormChangeTracker } from "components/FormChangeTracker"; import { ConnectionSchedule, NamespaceDefinitionType, WebBackendConnectionRead } from "core/request/AirbyteClient"; import { useFormChangeTrackerService, useUniqueFormId } from "hooks/services/FormChangeTracker"; +import { ModalCancel } from "hooks/services/Modal"; import { useGetDestinationDefinitionSpecification } from "services/connector/DestinationDefinitionSpecificationService"; import { useCurrentWorkspace } from "services/workspaces/WorkspacesService"; import { createFormErrorMessage } from "utils/errorStatusMessage"; @@ -115,6 +116,7 @@ const DirtyChangeTracker: React.FC = ({ dirty, onChange interface ConnectionFormProps { onSubmit: (values: ConnectionFormValues) => Promise; + onAfterSubmit?: () => void; className?: string; additionBottomControls?: React.ReactNode; successMessage?: React.ReactNode; @@ -134,6 +136,7 @@ interface ConnectionFormProps { const ConnectionForm: React.FC = ({ onSubmit, + onAfterSubmit, onCancel, className, onDropDownSelect, @@ -167,21 +170,20 @@ const ConnectionForm: React.FC = ({ setSubmitError(null); try { - const result = await onSubmit(formValues); - - if (result?.submitCancelled) { - return; - } + await onSubmit(formValues); formikHelpers.resetForm({ values }); clearFormChange(formId); - result?.onSubmitComplete?.(); + // result?.onSubmitComplete?.(); + onAfterSubmit?.(); } catch (e) { - setSubmitError(e); + if (!(e instanceof ModalCancel)) { + setSubmitError(e); + } } }, - [connection.operations, workspace.workspaceId, onSubmit, clearFormChange, formId] + [connection.operations, workspace.workspaceId, onSubmit, clearFormChange, formId, onAfterSubmit] ); const errorMessage = submitError ? createFormErrorMessage(submitError) : null; From 183087ada46e0413366dbe530552b5b98bec52b5 Mon Sep 17 00:00:00 2001 From: KC Date: Thu, 18 Aug 2022 10:28:01 -0400 Subject: [PATCH 002/107] Minor cleanup --- .../pages/ConnectionItemPage/components/ReplicationView.tsx | 1 - .../src/views/Connection/ConnectionForm/ConnectionForm.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx index ca6af5f17087..57815a6c6c63 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx @@ -154,7 +154,6 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch content: (props) => , }); if (result.type === "canceled") { - // eslint-disable-next-line no-throw-literal throw new ModalCancel(); } // Save the connection taking into account the correct skipRefresh value from the dialog choice. diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx index 4ce6e301a464..e24a097124ba 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx @@ -175,7 +175,6 @@ const ConnectionForm: React.FC = ({ formikHelpers.resetForm({ values }); clearFormChange(formId); - // result?.onSubmitComplete?.(); onAfterSubmit?.(); } catch (e) { if (!(e instanceof ModalCancel)) { From 85fe161390d1fc92d5041ee9594f9e15e820378f Mon Sep 17 00:00:00 2001 From: KC Date: Thu, 18 Aug 2022 13:21:58 -0400 Subject: [PATCH 003/107] Some cleanup --- .../CreateConnectionContent.tsx | 24 ++++--------------- .../components/TryAfterErrorBlock.tsx | 1 - .../components/ReplicationView.tsx | 2 +- .../ConnectionForm/ConnectionForm.test.tsx | 2 +- .../ConnectionForm/ConnectionForm.tsx | 16 ++++--------- .../components/CreateControls.tsx | 9 +------ .../views/Connection/ConnectionForm/index.tsx | 4 +--- 7 files changed, 13 insertions(+), 45 deletions(-) diff --git a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx index f0e21e8f7340..2b3a8322c460 100644 --- a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx +++ b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx @@ -1,5 +1,4 @@ import React, { Suspense } from "react"; -import styled from "styled-components"; import { ContentCard } from "components"; import { IDataItem } from "components/base/DropDown/components/Option"; @@ -11,23 +10,13 @@ import { LogsRequestError } from "core/request/LogsRequestError"; import { useAnalyticsService } from "hooks/services/Analytics"; import { useCreateConnection, ValuesProps } from "hooks/services/useConnectionHook"; import useRouter from "hooks/useRouter"; -import ConnectionForm from "views/Connection/ConnectionForm"; +import { ConnectionForm } from "views/Connection/ConnectionForm"; import { DestinationRead, SourceRead } from "../../core/request/AirbyteClient"; import { useDiscoverSchema } from "../../hooks/services/useSourceHook"; import TryAfterErrorBlock from "./components/TryAfterErrorBlock"; -const SkipButton = styled.div` - margin-top: 6px; - - & > button { - min-width: 239px; - margin-left: 9px; - } -`; - interface CreateConnectionContentProps { - additionBottomControls?: React.ReactNode; source: SourceRead; destination: DestinationRead; afterSubmitConnection?: () => void; @@ -37,7 +26,6 @@ const CreateConnectionContent: React.FC = ({ source, destination, afterSubmitConnection, - additionBottomControls, }) => { const { mutateAsync: createConnection } = useCreateConnection(); const analyticsService = useAnalyticsService(); @@ -70,7 +58,7 @@ const CreateConnectionContent: React.FC = ({ push(`../${createdConnection.connectionId}`); }; - const onSelectFrequency = (item: IDataItem | null) => { + const onFrequencySelect = (item: IDataItem | null) => { const enabledStreams = connection.syncCatalog.streams.filter((stream) => stream.config?.selected).length; if (item) { @@ -91,10 +79,7 @@ const CreateConnectionContent: React.FC = ({ const job = LogsRequestError.extractJobInfo(schemaErrorStatus); return ( - {additionBottomControls}} - /> + {job && } ); @@ -107,8 +92,7 @@ const CreateConnectionContent: React.FC = ({ diff --git a/airbyte-webapp/src/components/CreateConnectionContent/components/TryAfterErrorBlock.tsx b/airbyte-webapp/src/components/CreateConnectionContent/components/TryAfterErrorBlock.tsx index 25fb55108899..0e659be1aae3 100644 --- a/airbyte-webapp/src/components/CreateConnectionContent/components/TryAfterErrorBlock.tsx +++ b/airbyte-webapp/src/components/CreateConnectionContent/components/TryAfterErrorBlock.tsx @@ -19,7 +19,6 @@ const AgainButton = styled(Button)` interface TryAfterErrorBlockProps { message?: React.ReactNode; onClick: () => void; - additionControl?: React.ReactNode; } const TryAfterErrorBlock: React.FC = ({ message, onClick }) => ( diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx index 57815a6c6c63..08f0ed418b75 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx @@ -20,7 +20,7 @@ import { } from "hooks/services/useConnectionHook"; import { equal } from "utils/objects"; import { CatalogDiffModal } from "views/Connection/CatalogDiffModal/CatalogDiffModal"; -import ConnectionForm from "views/Connection/ConnectionForm"; +import { ConnectionForm } from "views/Connection/ConnectionForm"; import { ConnectionFormSubmitResult } from "views/Connection/ConnectionForm/ConnectionForm"; interface ReplicationViewProps { diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.test.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.test.tsx index 23c431947c42..229ddcb4b81c 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.test.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.test.tsx @@ -11,7 +11,7 @@ import { import { ConfirmationModalService } from "hooks/services/ConfirmationModal/ConfirmationModalService"; import { render } from "utils/testutils"; -import ConnectionForm, { ConnectionFormProps } from "./ConnectionForm"; +import { ConnectionForm, ConnectionFormProps } from "./ConnectionForm"; const mockSource: SourceRead = { sourceId: "test-source", diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx index e24a097124ba..d8378020198f 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx @@ -114,13 +114,12 @@ const DirtyChangeTracker: React.FC = ({ dirty, onChange return null; }; -interface ConnectionFormProps { +export interface ConnectionFormProps { onSubmit: (values: ConnectionFormValues) => Promise; onAfterSubmit?: () => void; className?: string; - additionBottomControls?: React.ReactNode; successMessage?: React.ReactNode; - onDropDownSelect?: (item: DropDownRow.IDataItem) => void; + onFrequencySelect?: (item: DropDownRow.IDataItem) => void; onCancel?: () => void; onFormDirtyChanges?: (dirty: boolean) => void; @@ -134,15 +133,14 @@ interface ConnectionFormProps { | (Partial & Pick); } -const ConnectionForm: React.FC = ({ +export const ConnectionForm: React.FC = ({ onSubmit, onAfterSubmit, onCancel, className, - onDropDownSelect, + onFrequencySelect, mode, successMessage, - additionBottomControls, canSubmitUntouchedForm, additionalSchemaControl, connection, @@ -256,7 +254,7 @@ const ConnectionForm: React.FC = ({ error={!!meta.error && meta.touched} options={frequencies} onChange={(item) => { - onDropDownSelect?.(item); + onFrequencySelect?.(item); setFieldValue(field.name, item.value); }} /> @@ -364,7 +362,6 @@ const ConnectionForm: React.FC = ({ onEndEditTransformation={toggleEditingTransformation} /> = ({ ); }; - -export type { ConnectionFormProps }; -export default ConnectionForm; diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/components/CreateControls.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/components/CreateControls.tsx index a8eaea1c0c83..6835fcc5568e 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/components/CreateControls.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/components/CreateControls.tsx @@ -8,7 +8,6 @@ interface CreateControlsProps { isSubmitting: boolean; isValid: boolean; errorMessage?: React.ReactNode; - additionBottomControls?: React.ReactNode; } const ButtonContainer = styled.div` @@ -59,12 +58,7 @@ const ErrorText = styled.div` max-width: 400px; `; -const CreateControls: React.FC = ({ - isSubmitting, - errorMessage, - additionBottomControls, - isValid, -}) => { +const CreateControls: React.FC = ({ isSubmitting, errorMessage, isValid }) => { if (isSubmitting) { return ( @@ -90,7 +84,6 @@ const CreateControls: React.FC = ({
)}
- {additionBottomControls || null} diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/index.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/index.tsx index a4298101b981..87384268edc1 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/index.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/index.tsx @@ -1,3 +1 @@ -import ConnectionForm from "./ConnectionForm"; - -export default ConnectionForm; +export { ConnectionForm } from "./ConnectionForm"; From 0e063b10a2801f31565b32446de1ad45f9060a83 Mon Sep 17 00:00:00 2001 From: KC Date: Thu, 18 Aug 2022 15:32:56 -0400 Subject: [PATCH 004/107] Lots moved into context --- .../CreateConnectionContent.tsx | 15 ++-- .../Connection/ConnectionFormService.tsx | 41 +++++++++++ .../services/Connection/ConnectionService.ts | 9 --- airbyte-webapp/src/locales/en.json | 2 +- .../components/ReplicationView.tsx | 33 +++++---- .../components/TransformationView.tsx | 3 - .../Connection/CatalogTree/CatalogSection.tsx | 15 ++-- .../Connection/CatalogTree/CatalogTree.tsx | 11 ++- .../Connection/CatalogTree/StreamHeader.tsx | 5 +- .../CatalogTree/components/BulkHeader.tsx | 14 ++-- .../ConnectionForm/ConnectionForm.test.tsx | 31 +++++---- .../ConnectionForm/ConnectionForm.tsx | 28 ++------ .../components/OperationsSection.tsx | 68 +++++++++---------- .../components/SyncCatalogField.tsx | 32 ++++----- .../components/TransformationField.tsx | 6 +- .../Connection/ConnectionForm/formConfig.tsx | 16 ++++- 16 files changed, 176 insertions(+), 153 deletions(-) create mode 100644 airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx delete mode 100644 airbyte-webapp/src/hooks/services/Connection/ConnectionService.ts diff --git a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx index 2b3a8322c460..5763830fc78c 100644 --- a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx +++ b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx @@ -8,6 +8,7 @@ import LoadingSchema from "components/LoadingSchema"; import { Action, Namespace } from "core/analytics"; import { LogsRequestError } from "core/request/LogsRequestError"; import { useAnalyticsService } from "hooks/services/Analytics"; +import { ConnectionFormProvider } from "hooks/services/Connection/ConnectionFormService"; import { useCreateConnection, ValuesProps } from "hooks/services/useConnectionHook"; import useRouter from "hooks/useRouter"; import { ConnectionForm } from "views/Connection/ConnectionForm"; @@ -89,13 +90,13 @@ const CreateConnectionContent: React.FC = ({ ) : ( }> - + + + ); }; diff --git a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx b/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx new file mode 100644 index 000000000000..4db7b9677baf --- /dev/null +++ b/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx @@ -0,0 +1,41 @@ +// import { useDiscoverSchema } from "../useSourceHook"; + +import React, { createContext, useContext } from "react"; + +import { WebBackendConnectionRead } from "core/request/AirbyteClient"; +import { useGetDestinationDefinitionSpecification } from "services/connector/DestinationDefinitionSpecificationService"; +import { ConnectionFormMode } from "views/Connection/ConnectionForm/ConnectionForm"; +import { useInitialValues } from "views/Connection/ConnectionForm/formConfig"; + +interface ConnectionServiceProps { + connection: + | WebBackendConnectionRead + | (Partial & Pick); + mode: ConnectionFormMode; +} + +const useConnectionForm = ({ connection, mode }: ConnectionServiceProps) => { + // Need to track form dirty state + // Might be easy if form context is in here + + const destDefinition = useGetDestinationDefinitionSpecification(connection.destination.destinationDefinitionId); + + const initialValues = useInitialValues(connection, destDefinition, mode !== "create"); + + return { initialValues, destDefinition, connection, mode }; +}; + +const ConnectionFormContext = createContext | null>(null); + +export const ConnectionFormProvider: React.FC = ({ children, ...props }) => { + const data = useConnectionForm(props); + return {children}; +}; + +export const useConnectionFormService = () => { + const context = useContext(ConnectionFormContext); + if (context === null) { + throw new Error("useConnectionFormService must be used within a ConnectionFormProvider"); + } + return context; +}; diff --git a/airbyte-webapp/src/hooks/services/Connection/ConnectionService.ts b/airbyte-webapp/src/hooks/services/Connection/ConnectionService.ts deleted file mode 100644 index ebc25847b7aa..000000000000 --- a/airbyte-webapp/src/hooks/services/Connection/ConnectionService.ts +++ /dev/null @@ -1,9 +0,0 @@ -// import { useDiscoverSchema } from "../useSourceHook"; - -export const useConnectionService = (/*sourceId: string*/) => { - // const { schema, isLoading, schemaErrorStatus, catalogId } = useDiscoverSchema(sourceId); - - // const createConnection = - - return null; -}; diff --git a/airbyte-webapp/src/locales/en.json b/airbyte-webapp/src/locales/en.json index 826ce956fb4e..0439e9d91964 100644 --- a/airbyte-webapp/src/locales/en.json +++ b/airbyte-webapp/src/locales/en.json @@ -163,7 +163,7 @@ "form.entrypoint.docs": "Learn more", "form.gitBranch": "Git branch name (leave blank for default branch)", "form.selectType": "Select a type", - "form.repositoryUrl.placeholder": "https://github.com//.git", + "form.repositoryUrl.placeholder": "https://github.com/\\/\\.git", "form.sourceNamespace": "Source namespace", "form.sourceStreamName": "Source stream name", diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx index 08f0ed418b75..a5227157105b 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx @@ -11,6 +11,7 @@ import LoadingSchema from "components/LoadingSchema"; import { toWebBackendConnectionUpdate } from "core/domain/connection"; import { ConnectionStateType, ConnectionStatus } from "core/request/AirbyteClient"; import { useConfirmationModalService } from "hooks/services/ConfirmationModal"; +import { ConnectionFormProvider } from "hooks/services/Connection/ConnectionFormService"; import { ModalCancel, useModalService } from "hooks/services/Modal"; import { useConnectionLoad, @@ -180,6 +181,7 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch }; const onRefreshSourceSchema = async () => { + // TODO: Yeet if (connectionFormDirtyRef.current) { // The form is dirty so we show a warning before proceeding. openConfirmationModal({ @@ -209,21 +211,24 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch return ( {!isRefreshingCatalog && connection ? ( - } - onCancel={onCancelConnectionFormEdit} - canSubmitUntouchedForm={activeUpdatingSchemaMode} - additionalSchemaControl={ - - } - onFormDirtyChanges={onDirtyChanges} - /> + mode={connection?.status !== ConnectionStatus.deprecated ? "edit" : "readonly"} + > + } + onCancel={onCancelConnectionFormEdit} + canSubmitUntouchedForm={activeUpdatingSchemaMode} + additionalSchemaControl={ + + } + onFormDirtyChanges={onDirtyChanges} + /> + ) : ( )} diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/TransformationView.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/TransformationView.tsx index f3f211b8bae6..69b33fb6b55a 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/TransformationView.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/TransformationView.tsx @@ -19,7 +19,6 @@ import { getInitialNormalization, getInitialTransformations, mapFormPropsToOperation, - useDefaultTransformation, } from "views/Connection/ConnectionForm/formConfig"; import { FormCard } from "views/Connection/FormCard"; @@ -55,7 +54,6 @@ const CustomTransformationsCard: React.FC<{ onSubmit: FormikOnSubmit<{ transformations?: OperationRead[] }>; mode: ConnectionFormMode; }> = ({ operations, onSubmit, mode }) => { - const defaultTransformation = useDefaultTransformation(); const [editingTransformation, toggleEditingTransformation] = useToggle(false); const initialValues = useMemo( @@ -81,7 +79,6 @@ const CustomTransformationsCard: React.FC<{ {(formProps) => ( ; - destinationSupportedSyncModes: DestinationSyncMode[]; namespaceDefinition: NamespaceDefinitionType; namespaceFormat: string; prefix: string; updateStream: (id: string | undefined, newConfiguration: Partial) => void; - mode?: ConnectionFormMode; changedSelected: boolean; } @@ -40,10 +38,12 @@ const CatalogSectionInner: React.FC = ({ namespaceFormat, prefix, errors, - destinationSupportedSyncModes, - mode, changedSelected, }) => { + const { + destDefinition: { supportedDestinationSyncModes }, + } = useConnectionFormService(); + const [isRowExpanded, onExpand] = useToggle(false); const { stream, config } = streamNode; @@ -99,11 +99,11 @@ const CatalogSectionInner: React.FC = ({ () => SUPPORTED_MODES.filter( ([syncMode, destinationSyncMode]) => - stream?.supportedSyncModes?.includes(syncMode) && destinationSupportedSyncModes.includes(destinationSyncMode) + stream?.supportedSyncModes?.includes(syncMode) && supportedDestinationSyncModes?.includes(destinationSyncMode) ).map(([syncMode, destinationSyncMode]) => ({ value: { syncMode, destinationSyncMode }, })), - [stream?.supportedSyncModes, destinationSupportedSyncModes] + [stream?.supportedSyncModes, supportedDestinationSyncModes] ); const destNamespace = getDestinationNamespace({ @@ -146,7 +146,6 @@ const CatalogSectionInner: React.FC = ({ onCursorChange={onCursorSelect} hasFields={hasChildren} onExpand={onExpand} - mode={mode} changedSelected={changedSelected} hasError={hasError} /> diff --git a/airbyte-webapp/src/views/Connection/CatalogTree/CatalogTree.tsx b/airbyte-webapp/src/views/Connection/CatalogTree/CatalogTree.tsx index ae5aa4ebaa84..993f7e48000c 100644 --- a/airbyte-webapp/src/views/Connection/CatalogTree/CatalogTree.tsx +++ b/airbyte-webapp/src/views/Connection/CatalogTree/CatalogTree.tsx @@ -2,20 +2,19 @@ import { Field, FieldProps, setIn, useFormikContext } from "formik"; import React, { useCallback } from "react"; import { SyncSchemaStream } from "core/domain/catalog"; -import { AirbyteStreamConfiguration, DestinationSyncMode } from "core/request/AirbyteClient"; +import { AirbyteStreamConfiguration } from "core/request/AirbyteClient"; +import { useConnectionFormService } from "hooks/services/Connection/ConnectionFormService"; import { ConnectionFormValues, FormikConnectionFormValues } from "views/Connection/ConnectionForm/formConfig"; -import { ConnectionFormMode } from "../ConnectionForm/ConnectionForm"; import { CatalogSection } from "./CatalogSection"; interface CatalogTreeProps { streams: SyncSchemaStream[]; - destinationSupportedSyncModes: DestinationSyncMode[]; onChangeStream: (stream: SyncSchemaStream) => void; - mode?: ConnectionFormMode; } -const CatalogTree: React.FC = ({ streams, destinationSupportedSyncModes, onChangeStream, mode }) => { +const CatalogTree: React.FC = ({ streams, onChangeStream }) => { + const { mode } = useConnectionFormService(); const onUpdateStream = useCallback( (id: string | undefined, newConfig: Partial) => { const streamNode = streams.find((streamNode) => streamNode.id === id); @@ -47,9 +46,7 @@ const CatalogTree: React.FC = ({ streams, destinationSupported namespaceFormat={form.values.namespaceFormat} prefix={form.values.prefix} streamNode={streamNode} - destinationSupportedSyncModes={destinationSupportedSyncModes} updateStream={onUpdateStream} - mode={mode} changedSelected={changedStreams.includes(streamNode) && mode === "edit"} /> )} diff --git a/airbyte-webapp/src/views/Connection/CatalogTree/StreamHeader.tsx b/airbyte-webapp/src/views/Connection/CatalogTree/StreamHeader.tsx index f87b7c31b4c6..4b68734658e0 100644 --- a/airbyte-webapp/src/views/Connection/CatalogTree/StreamHeader.tsx +++ b/airbyte-webapp/src/views/Connection/CatalogTree/StreamHeader.tsx @@ -10,8 +10,8 @@ import { Cell, CheckBox, DropDownRow, Row, Switch } from "components"; import { Path, SyncSchemaField, SyncSchemaStream } from "core/domain/catalog"; import { DestinationSyncMode, SyncMode } from "core/request/AirbyteClient"; import { useBulkEditSelect } from "hooks/services/BulkEdit/BulkEditService"; +import { useConnectionFormService } from "hooks/services/Connection/ConnectionFormService"; -import { ConnectionFormMode } from "../ConnectionForm/ConnectionForm"; import { Arrow as ArrowBlock } from "./components/Arrow"; import { IndexerType, PathPopout } from "./components/PathPopout"; import { SyncSettingsDropdown } from "./components/SyncSettingsDropdown"; @@ -44,7 +44,6 @@ interface StreamHeaderProps { isRowExpanded: boolean; hasFields: boolean; onExpand: () => void; - mode?: ConnectionFormMode; changedSelected: boolean; hasError: boolean; } @@ -64,10 +63,10 @@ export const StreamHeader: React.FC = ({ isRowExpanded, hasFields, onExpand, - mode, changedSelected, hasError, }) => { + const { mode } = useConnectionFormService(); const { primaryKey, syncMode, cursorField, destinationSyncMode } = stream.config ?? {}; const isEnabled = stream.config?.selected; diff --git a/airbyte-webapp/src/views/Connection/CatalogTree/components/BulkHeader.tsx b/airbyte-webapp/src/views/Connection/CatalogTree/components/BulkHeader.tsx index 8d4612019bda..7a663027a01d 100644 --- a/airbyte-webapp/src/views/Connection/CatalogTree/components/BulkHeader.tsx +++ b/airbyte-webapp/src/views/Connection/CatalogTree/components/BulkHeader.tsx @@ -8,6 +8,7 @@ import { Button, Cell, Header, Switch } from "components"; import { SyncSchemaField, SyncSchemaFieldObject, SyncSchemaStream, traverseSchemaToField } from "core/domain/catalog"; import { DestinationSyncMode, SyncMode } from "core/request/AirbyteClient"; import { useBulkEdit } from "hooks/services/BulkEdit/BulkEditService"; +import { useConnectionFormService } from "hooks/services/Connection/ConnectionFormService"; import { SUPPORTED_MODES } from "../../ConnectionForm/formConfig"; import { ArrowCell, CheckboxCell, HeaderCell } from "../styles"; @@ -32,10 +33,6 @@ const ActionButton = styled(Button).attrs({ white-space: nowrap; `; -interface BulkHeaderProps { - destinationSupportedSyncModes: DestinationSyncMode[]; -} - function calculateSharedFields(selectedBatchNodes: SyncSchemaStream[]) { const primitiveFieldsByStream = selectedBatchNodes.map(({ stream }) => { const traversedFields = traverseSchemaToField(stream?.jsonSchema, stream?.name); @@ -59,18 +56,21 @@ function calculateSharedFields(selectedBatchNodes: SyncSchemaStream[]) { return Array.from(pathMap.values()); } -export const BulkHeader: React.FC = ({ destinationSupportedSyncModes }) => { +export const BulkHeader: React.FC = () => { + const { + destDefinition: { supportedDestinationSyncModes }, + } = useConnectionFormService(); const { selectedBatchNodes, options, onChangeOption, onApply, isActive, onCancel } = useBulkEdit(); const availableSyncModes = useMemo( () => SUPPORTED_MODES.filter(([syncMode, destinationSyncMode]) => { const supportableModes = intersection(selectedBatchNodes.flatMap((n) => n.stream?.supportedSyncModes)); - return supportableModes.includes(syncMode) && destinationSupportedSyncModes.includes(destinationSyncMode); + return supportableModes.includes(syncMode) && supportedDestinationSyncModes?.includes(destinationSyncMode); }).map(([syncMode, destinationSyncMode]) => ({ value: { syncMode, destinationSyncMode }, })), - [selectedBatchNodes, destinationSupportedSyncModes] + [selectedBatchNodes, supportedDestinationSyncModes] ); const primitiveFields: SyncSchemaField[] = useMemo( diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.test.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.test.tsx index 229ddcb4b81c..9e11eecdb287 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.test.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.test.tsx @@ -9,9 +9,10 @@ import { WebBackendConnectionRead, } from "core/request/AirbyteClient"; import { ConfirmationModalService } from "hooks/services/ConfirmationModal/ConfirmationModalService"; +import { ConnectionFormProvider } from "hooks/services/Connection/ConnectionFormService"; import { render } from "utils/testutils"; -import { ConnectionForm, ConnectionFormProps } from "./ConnectionForm"; +import { ConnectionForm, ConnectionFormMode, ConnectionFormProps } from "./ConnectionForm"; const mockSource: SourceRead = { sourceId: "test-source", @@ -68,10 +69,12 @@ jest.mock("services/workspaces/WorkspacesService", () => { }; }); -const renderConnectionForm = (props: ConnectionFormProps) => +const renderConnectionForm = (props: ConnectionFormProps, mode: ConnectionFormMode, connection = mockConnection) => render( - + + + ); @@ -79,11 +82,12 @@ describe("", () => { let container: HTMLElement; describe("edit mode", () => { beforeEach(async () => { - const renderResult = await renderConnectionForm({ - onSubmit: jest.fn(), - mode: "edit", - connection: mockConnection, - }); + const renderResult = await renderConnectionForm( + { + onSubmit: jest.fn(), + }, + "edit" + ); container = renderResult.container; }); @@ -100,11 +104,12 @@ describe("", () => { }); describe("readonly mode", () => { beforeEach(async () => { - const renderResult = await renderConnectionForm({ - onSubmit: jest.fn(), - mode: "readonly", - connection: mockConnection, - }); + const renderResult = await renderConnectionForm( + { + onSubmit: jest.fn(), + }, + "readonly" + ); container = renderResult.container; }); diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx index d8378020198f..65bca46a9625 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx @@ -7,10 +7,10 @@ import styled from "styled-components"; import { Card, ControlLabels, DropDown, DropDownRow, H5, Input } from "components"; import { FormChangeTracker } from "components/FormChangeTracker"; -import { ConnectionSchedule, NamespaceDefinitionType, WebBackendConnectionRead } from "core/request/AirbyteClient"; +import { ConnectionSchedule, NamespaceDefinitionType } from "core/request/AirbyteClient"; +import { useConnectionFormService } from "hooks/services/Connection/ConnectionFormService"; import { useFormChangeTrackerService, useUniqueFormId } from "hooks/services/FormChangeTracker"; import { ModalCancel } from "hooks/services/Modal"; -import { useGetDestinationDefinitionSpecification } from "services/connector/DestinationDefinitionSpecificationService"; import { useCurrentWorkspace } from "services/workspaces/WorkspacesService"; import { createFormErrorMessage } from "utils/errorStatusMessage"; @@ -25,7 +25,6 @@ import { FormikConnectionFormValues, mapFormPropsToOperation, useFrequencyDropdownData, - useInitialValues, } from "./formConfig"; const ConnectorLabel = styled(ControlLabels)` @@ -60,7 +59,7 @@ export const RightFieldCol = styled.div` max-width: 300px; `; -const StyledSection = styled.div` +export const StyledSection = styled.div` padding: 20px 20px; display: flex; flex-direction: column; @@ -125,12 +124,7 @@ export interface ConnectionFormProps { /** Should be passed when connection is updated with withRefreshCatalog flag */ canSubmitUntouchedForm?: boolean; - mode: ConnectionFormMode; additionalSchemaControl?: React.ReactNode; - - connection: - | WebBackendConnectionRead - | (Partial & Pick); } export const ConnectionForm: React.FC = ({ @@ -139,14 +133,12 @@ export const ConnectionForm: React.FC = ({ onCancel, className, onFrequencySelect, - mode, successMessage, canSubmitUntouchedForm, additionalSchemaControl, - connection, onFormDirtyChanges, }) => { - const destDefinition = useGetDestinationDefinitionSpecification(connection.destination.destinationDefinitionId); + const { initialValues, connection, mode } = useConnectionFormService(); const { clearFormChange } = useFormChangeTrackerService(); const formId = useUniqueFormId(); const [submitError, setSubmitError] = useState(null); @@ -154,8 +146,6 @@ export const ConnectionForm: React.FC = ({ const { formatMessage } = useIntl(); - const isEditMode: boolean = mode !== "create"; - const initialValues = useInitialValues(connection, destDefinition, isEditMode); const workspace = useCurrentWorkspace(); const onFormSubmit = useCallback( @@ -197,7 +187,7 @@ export const ConnectionForm: React.FC = ({ {onFormDirtyChanges && } - {!isEditMode && ( + {mode !== "create" && (
{({ field, meta }: FieldProps) => ( @@ -328,11 +318,9 @@ export const ConnectionForm: React.FC = ({ @@ -352,12 +340,6 @@ export const ConnectionForm: React.FC = ({ {mode === "create" && ( <> ( - - {children} - - )} - destDefinition={destDefinition} onStartEditTransformation={toggleEditingTransformation} onEndEditTransformation={toggleEditingTransformation} /> diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/components/OperationsSection.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/components/OperationsSection.tsx index 70e6bf60f739..4406a14093f5 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/components/OperationsSection.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/components/OperationsSection.tsx @@ -2,64 +2,62 @@ import { Field, FieldArray } from "formik"; import React from "react"; import { useIntl } from "react-intl"; -import { H5 } from "components"; +import { Card, H5 } from "components"; +import { useConnectionFormService } from "hooks/services/Connection/ConnectionFormService"; import { FeatureItem, useFeature } from "hooks/services/Feature"; -import { DestinationDefinitionSpecificationRead } from "../../../../core/request/AirbyteClient"; -import { useDefaultTransformation } from "../formConfig"; +import { StyledSection } from "../ConnectionForm"; import { NormalizationField } from "./NormalizationField"; import { TransformationField } from "./TransformationField"; interface OperationsSectionProps { - destDefinition: DestinationDefinitionSpecificationRead; onStartEditTransformation?: () => void; onEndEditTransformation?: () => void; - wrapper: React.ComponentType; } export const OperationsSection: React.FC = ({ - destDefinition, onStartEditTransformation, onEndEditTransformation, - wrapper: Wrapper, }) => { const { formatMessage } = useIntl(); - const { supportsNormalization } = destDefinition; - const supportsTransformations = useFeature(FeatureItem.AllowCustomDBT) && destDefinition.supportsDbt; + const { + destDefinition: { supportsNormalization, supportsDbt }, + } = useConnectionFormService(); - const defaultTransformation = useDefaultTransformation(); + const supportsTransformations = useFeature(FeatureItem.AllowCustomDBT) && supportsDbt; if (!supportsNormalization && !supportsTransformations) { return null; } return ( - - {supportsNormalization || supportsTransformations ? ( -
- {[ - supportsNormalization && formatMessage({ id: "connectionForm.normalization.title" }), - supportsTransformations && formatMessage({ id: "connectionForm.transformation.title" }), - ] - .filter(Boolean) - .join(" & ")} -
- ) : null} - {supportsNormalization && } - {supportsTransformations && ( - - {(formProps) => ( - - )} - - )} -
+ + + {supportsNormalization || supportsTransformations ? ( +
+ {[ + supportsNormalization && formatMessage({ id: "connectionForm.normalization.title" }), + supportsTransformations && formatMessage({ id: "connectionForm.transformation.title" }), + ] + .filter(Boolean) + .join(" & ")} +
+ ) : null} + {supportsNormalization && } + {supportsTransformations && ( + + {(formProps) => ( + + )} + + )} +
+
); }; diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/components/SyncCatalogField.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/components/SyncCatalogField.tsx index 774648e3380b..bd56b7383d74 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/components/SyncCatalogField.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/components/SyncCatalogField.tsx @@ -12,6 +12,7 @@ import { useConfig } from "config"; import { SyncSchemaStream } from "core/domain/catalog"; import { DestinationSyncMode } from "core/request/AirbyteClient"; import { BatchEditProvider, useBulkEdit } from "hooks/services/BulkEdit/BulkEditService"; +import { useConnectionFormService } from "hooks/services/Connection/ConnectionFormService"; import { naturalComparatorBy } from "utils/objects"; import CatalogTree from "views/Connection/CatalogTree"; @@ -75,7 +76,8 @@ interface SchemaViewProps extends FieldProps { mode?: ConnectionFormMode; } -const CatalogHeader: React.FC<{ mode?: ConnectionFormMode }> = ({ mode }) => { +const CatalogHeader: React.FC = () => { + const { mode } = useConnectionFormService(); const config = useConfig(); const { onCheckAll, selectedBatchNodeIds, allChecked } = useBulkEdit(); const catalogHeaderStyle = classnames({ @@ -140,7 +142,9 @@ const CatalogHeader: React.FC<{ mode?: ConnectionFormMode }> = ({ mode }) => { ); }; -const CatalogSubheader: React.FC<{ mode?: ConnectionFormMode }> = ({ mode }) => { +const CatalogSubheader: React.FC = () => { + const { mode } = useConnectionFormService(); + const catalogSubheaderStyle = classnames({ [styles.catalogSubheader]: mode !== "readonly", [styles.readonlyCatalogSubheader]: mode === "readonly", @@ -169,14 +173,9 @@ const CatalogSubheader: React.FC<{ mode?: ConnectionFormMode }> = ({ mode }) => ); }; -const SyncCatalogField: React.FC = ({ - destinationSupportedSyncModes, - additionalControl, - field, - form, - isSubmitting, - mode, -}) => { +const SyncCatalogField: React.FC = ({ additionalControl, field, form, isSubmitting }) => { + const { mode } = useConnectionFormService(); + const { value: streams, name: fieldName } = field; const [searchString, setSearchString] = useState(""); @@ -228,16 +227,11 @@ const SyncCatalogField: React.FC = ({ )} {mode !== "readonly" && } - - - + + + - + diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/components/TransformationField.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/components/TransformationField.tsx index cfbe9c0bc8ce..3ace8b771ee4 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/components/TransformationField.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/components/TransformationField.tsx @@ -4,15 +4,15 @@ import { FormattedMessage } from "react-intl"; import ArrayOfObjectsEditor from "components/ArrayOfObjectsEditor"; -import { OperationCreate, OperationRead } from "core/request/AirbyteClient"; +import { OperationRead } from "core/request/AirbyteClient"; import { isDefined } from "utils/common"; import TransformationForm from "views/Connection/TransformationForm"; import { ConnectionFormMode } from "../ConnectionForm"; +import { useDefaultTransformation } from "../formConfig"; interface TransformationFieldProps extends ArrayHelpers { form: FormikProps<{ transformations: OperationRead[] }>; - defaultTransformation: OperationCreate; mode?: ConnectionFormMode; onStartEdit?: () => void; onEndEdit?: () => void; @@ -23,12 +23,12 @@ const TransformationField: React.FC = ({ push, replace, form, - defaultTransformation, mode, onStartEdit, onEndEdit, }) => { const [editableItemIdx, setEditableItem] = useState(null); + const defaultTransformation = useDefaultTransformation(); return ( { From d7ad611aa12bfc5c51516a9fc064090282245419 Mon Sep 17 00:00:00 2001 From: KC Date: Thu, 18 Aug 2022 16:04:22 -0400 Subject: [PATCH 005/107] WIP, stepping in the right direction --- .../CreateConnectionContent.tsx | 14 +-- .../Connection/ConnectionFormService.tsx | 86 +++++++++++++++++-- .../components/ReplicationView.tsx | 6 +- .../ConnectionForm/ConnectionForm.test.tsx | 22 ++--- .../ConnectionForm/ConnectionForm.tsx | 74 ++++------------ 5 files changed, 114 insertions(+), 88 deletions(-) diff --git a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx index 5763830fc78c..218da85aafaa 100644 --- a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx +++ b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx @@ -90,12 +90,14 @@ const CreateConnectionContent: React.FC = ({ ) : ( }> - - + + ); diff --git a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx b/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx index 4db7b9677baf..8401c078ff14 100644 --- a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx +++ b/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx @@ -1,28 +1,102 @@ // import { useDiscoverSchema } from "../useSourceHook"; -import React, { createContext, useContext } from "react"; +import { FormikHelpers } from "formik"; +import React, { createContext, useCallback, useContext, useMemo, useState } from "react"; +import { Subject } from "rxjs"; + +import { DropDownRow } from "components"; import { WebBackendConnectionRead } from "core/request/AirbyteClient"; import { useGetDestinationDefinitionSpecification } from "services/connector/DestinationDefinitionSpecificationService"; -import { ConnectionFormMode } from "views/Connection/ConnectionForm/ConnectionForm"; -import { useInitialValues } from "views/Connection/ConnectionForm/formConfig"; +import { useCurrentWorkspaceId } from "services/workspaces/WorkspacesService"; +import { createFormErrorMessage } from "utils/errorStatusMessage"; +import { ConnectionFormMode, ConnectionFormSubmitResult } from "views/Connection/ConnectionForm/ConnectionForm"; +import { + ConnectionFormValues, + connectionValidationSchema, + FormikConnectionFormValues, + mapFormPropsToOperation, + useFrequencyDropdownData, + useInitialValues, +} from "views/Connection/ConnectionForm/formConfig"; + +import { useFormChangeTrackerService, useUniqueFormId } from "../FormChangeTracker"; +import { ModalCancel } from "../Modal"; interface ConnectionServiceProps { connection: | WebBackendConnectionRead | (Partial & Pick); mode: ConnectionFormMode; + onSubmit: (values: ConnectionFormValues) => Promise; + onAfterSubmit?: () => void; + onFrequencySelect?: (item: DropDownRow.IDataItem) => void; + onCancel?: () => void; } -const useConnectionForm = ({ connection, mode }: ConnectionServiceProps) => { +const useConnectionForm = ({ + connection, + mode, + onSubmit, + onAfterSubmit, + onFrequencySelect, + onCancel, +}: ConnectionServiceProps) => { // Need to track form dirty state // Might be easy if form context is in here + const [submitError, setSubmitError] = useState(null); + const workspaceId = useCurrentWorkspaceId(); + const { clearFormChange } = useFormChangeTrackerService(); + const formId = useUniqueFormId(); const destDefinition = useGetDestinationDefinitionSpecification(connection.destination.destinationDefinitionId); - const initialValues = useInitialValues(connection, destDefinition, mode !== "create"); - return { initialValues, destDefinition, connection, mode }; + const onFormSubmit = useCallback( + async (values: FormikConnectionFormValues, formikHelpers: FormikHelpers) => { + const formValues: ConnectionFormValues = connectionValidationSchema.cast(values, { + context: { isRequest: true }, + }) as unknown as ConnectionFormValues; // TODO: We should align these types + + formValues.operations = mapFormPropsToOperation(values, connection.operations, workspaceId); + + setSubmitError(null); + try { + await onSubmit(formValues); + + formikHelpers.resetForm({ values }); + clearFormChange(formId); + + onAfterSubmit?.(); + } catch (e) { + if (!(e instanceof ModalCancel)) { + setSubmitError(e); + } + } + }, + [connection.operations, workspaceId, onSubmit, clearFormChange, formId, onAfterSubmit] + ); + + const errorMessage = useMemo(() => (submitError ? createFormErrorMessage(submitError) : null), [submitError]); + const frequencies = useFrequencyDropdownData(); + + const formDirty = new Subject(); + + return { + initialValues, + destDefinition, + connection, + mode, + errorMessage, + frequencies, + formId, + formDirty, + setSubmitError, + onFormSubmit, + onAfterSubmit, + onFrequencySelect, + onCancel, + }; }; const ConnectionFormContext = createContext | null>(null); diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx index a5227157105b..3a1e66d598cb 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx @@ -165,6 +165,8 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch } }; + // TODO: Move this into the service next + // Utilize the observer: formDirty const refreshSourceSchema = async () => { setSaved(false); setActiveUpdatingSchemaMode(true); @@ -214,11 +216,11 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch } - onCancel={onCancelConnectionFormEdit} canSubmitUntouchedForm={activeUpdatingSchemaMode} additionalSchemaControl={ } - onFormDirtyChanges={onDirtyChanges} /> ) : ( diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx index 20f380bee55d..8eaf1cbb6f7a 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx @@ -1,5 +1,5 @@ import { Field, FieldProps, Form, Formik } from "formik"; -import React, { useEffect } from "react"; +import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useToggle } from "react-use"; import styled from "styled-components"; @@ -94,7 +94,6 @@ export type ConnectionFormMode = "create" | "edit" | "readonly"; export interface ConnectionFormProps { className?: string; successMessage?: React.ReactNode; - onFormDirtyChanges?: (dirty: boolean) => void; /** Should be passed when connection is updated with withRefreshCatalog flag */ canSubmitUntouchedForm?: boolean; @@ -106,29 +105,12 @@ export const ConnectionForm: React.FC = ({ successMessage, canSubmitUntouchedForm, additionalSchemaControl, - onFormDirtyChanges, }) => { - const { - initialValues, - formId, - mode, - onFormSubmit, - errorMessage, - frequencies, - onFrequencySelect, - onCancel, - formDirty, - } = useConnectionFormService(); + const { initialValues, formId, mode, onFormSubmit, errorMessage, frequencies, onFrequencySelect, onCancel } = + useConnectionFormService(); const [editingTransformation, toggleEditingTransformation] = useToggle(false); const { formatMessage } = useIntl(); - useEffect(() => { - const sub = formDirty.subscribe({ - next: (dirty) => onFormDirtyChanges?.(dirty), - }); - return () => sub.unsubscribe(); - }, [formDirty, onFormDirtyChanges]); - return ( = ({ {({ isSubmitting, setFieldValue, isValid, dirty, resetForm, values }) => ( - {formDirty.next(dirty)} {mode === "create" && (
From 8c5558b744dd0d2d2b60cca3ac93a3c4ba3f719c Mon Sep 17 00:00:00 2001 From: KC Date: Thu, 1 Sep 2022 13:54:11 -0400 Subject: [PATCH 022/107] cleanup --- .../src/hooks/services/Connection/ConnectionFormService.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx b/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx index 4c5f0f0290b7..66dae96d1a5b 100644 --- a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx +++ b/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx @@ -43,8 +43,6 @@ const useConnectionForm = ({ onFrequencySelect, onCancel, }: ConnectionServiceProps) => { - // Need to track form dirty state - // Might be easy if form context is in here const [submitError, setSubmitError] = useState(null); const workspaceId = useCurrentWorkspaceId(); const { clearFormChange } = useFormChangeTrackerService(); @@ -79,7 +77,6 @@ const useConnectionForm = ({ throw new Error("Somehow this form does not have an id"); } - console.log(onAfterSubmit?.toString()); onAfterSubmit?.(); } catch (e) { if (!(e instanceof ModalCancel)) { From d390833bf08d6e664a1a488c28360d0ff556665b Mon Sep 17 00:00:00 2001 From: KC Date: Thu, 1 Sep 2022 16:32:59 -0400 Subject: [PATCH 023/107] Removing an unused prop --- .../Connection/ConnectionForm/components/SyncCatalogField.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/components/SyncCatalogField.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/components/SyncCatalogField.tsx index 550310fc06e0..3f432223ceed 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/components/SyncCatalogField.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/components/SyncCatalogField.tsx @@ -22,7 +22,7 @@ import { ConnectionFormMode } from "../ConnectionForm"; import Search from "./Search"; import styles from "./SyncCatalogField.module.scss"; -const TreeViewContainer = styled.div<{ mode?: ConnectionFormMode }>` +const TreeViewContainer = styled.div` margin-bottom: 29px; max-height: 600px; overflow-y: auto; @@ -216,7 +216,7 @@ const SyncCatalogField: React.FC = ({ additionalControl, field, - + From 3d094fbf652fca921665820099fe6b7ba4756259 Mon Sep 17 00:00:00 2001 From: KC Date: Fri, 2 Sep 2022 10:26:17 -0400 Subject: [PATCH 024/107] errorStatusMessage and mapFormPropsToOperation tests --- .../components/DestinationForm.tsx | 4 +- .../components/DestinationStep.tsx | 9 +- .../src/utils/errorStatusMessage.test.tsx | 35 +++++ .../src/utils/errorStatusMessage.tsx | 6 +- .../ConnectionForm/formConfig.test.ts | 130 +++++++++++++++++- 5 files changed, 171 insertions(+), 13 deletions(-) create mode 100644 airbyte-webapp/src/utils/errorStatusMessage.test.tsx diff --git a/airbyte-webapp/src/pages/DestinationPage/pages/CreateDestinationPage/components/DestinationForm.tsx b/airbyte-webapp/src/pages/DestinationPage/pages/CreateDestinationPage/components/DestinationForm.tsx index a31baae84278..b3f5bd3603b6 100644 --- a/airbyte-webapp/src/pages/DestinationPage/pages/CreateDestinationPage/components/DestinationForm.tsx +++ b/airbyte-webapp/src/pages/DestinationPage/pages/CreateDestinationPage/components/DestinationForm.tsx @@ -8,7 +8,7 @@ import { LogsRequestError } from "core/request/LogsRequestError"; import { useAnalyticsService } from "hooks/services/Analytics"; import useRouter from "hooks/useRouter"; import { useGetDestinationDefinitionSpecificationAsync } from "services/connector/DestinationDefinitionSpecificationService"; -import { createFormErrorMessage } from "utils/errorStatusMessage"; +import { createFormErrorMessage, FormError } from "utils/errorStatusMessage"; import { ConnectorCard } from "views/Connector/ConnectorCard"; interface DestinationFormProps { @@ -21,7 +21,7 @@ interface DestinationFormProps { afterSelectConnector?: () => void; destinationDefinitions: DestinationDefinitionRead[]; hasSuccess?: boolean; - error?: { message?: string; status?: number } | null; + error?: FormError | null; } const hasDestinationDefinitionId = (state: unknown): state is { destinationDefinitionId: string } => { diff --git a/airbyte-webapp/src/pages/OnboardingPage/components/DestinationStep.tsx b/airbyte-webapp/src/pages/OnboardingPage/components/DestinationStep.tsx index a910a377df0e..53236487aa5b 100644 --- a/airbyte-webapp/src/pages/OnboardingPage/components/DestinationStep.tsx +++ b/airbyte-webapp/src/pages/OnboardingPage/components/DestinationStep.tsx @@ -2,12 +2,11 @@ import React, { useEffect, useState } from "react"; import { Action, Namespace } from "core/analytics"; import { ConnectionConfiguration } from "core/domain/connection"; -import { JobInfo } from "core/domain/job"; import { useAnalyticsService } from "hooks/services/Analytics"; import { useCreateDestination } from "hooks/services/useDestinationHook"; import { useDestinationDefinitionList } from "services/connector/DestinationDefinitionService"; import { useGetDestinationDefinitionSpecificationAsync } from "services/connector/DestinationDefinitionSpecificationService"; -import { createFormErrorMessage } from "utils/errorStatusMessage"; +import { createFormErrorMessage, FormError } from "utils/errorStatusMessage"; import { ConnectorCard } from "views/Connector/ConnectorCard"; import { useDocumentationPanelContext } from "views/Connector/ConnectorDocumentationLayout/DocumentationPanelContext"; @@ -23,11 +22,7 @@ const DestinationStep: React.FC = ({ onNextStep, onSuccess }) => { useGetDestinationDefinitionSpecificationAsync(destinationDefinitionId); const { destinationDefinitions } = useDestinationDefinitionList(); const [successRequest, setSuccessRequest] = useState(false); - const [error, setError] = useState<{ - status: number; - response: JobInfo; - message: string; - } | null>(null); + const [error, setError] = useState(null); const { mutateAsync: createDestination } = useCreateDestination(); diff --git a/airbyte-webapp/src/utils/errorStatusMessage.test.tsx b/airbyte-webapp/src/utils/errorStatusMessage.test.tsx new file mode 100644 index 000000000000..28d35bb2a9fb --- /dev/null +++ b/airbyte-webapp/src/utils/errorStatusMessage.test.tsx @@ -0,0 +1,35 @@ +import { createFormErrorMessage, FormError } from "./errorStatusMessage"; + +describe("#errorStatusMessage", () => { + it("should return a provided error message", () => { + const errMsg = "test"; + expect(createFormErrorMessage(new Error(errMsg))).toBe(errMsg); + }); + + it("should return null if no error message and no status, or status is 0", () => { + expect(createFormErrorMessage(new Error())).toBe(null); + const fakeStatusError = new FormError(); + fakeStatusError.status = 0; + expect(createFormErrorMessage(fakeStatusError)).toBe(null); + }); + + it("should return a validation error message if status is 400", () => { + const fakeStatusError = new FormError(); + fakeStatusError.status = 400; + expect(createFormErrorMessage(fakeStatusError)).toMatchInlineSnapshot(` + + `); + }); + + it("should return a 'some error' message if status is > 0 and not 400", () => { + const fakeStatusError = new FormError(); + fakeStatusError.status = 401; + expect(createFormErrorMessage(fakeStatusError)).toMatchInlineSnapshot(` + + `); + }); +}); diff --git a/airbyte-webapp/src/utils/errorStatusMessage.tsx b/airbyte-webapp/src/utils/errorStatusMessage.tsx index 7afcf81cb3c7..f84f55a4b4c4 100644 --- a/airbyte-webapp/src/utils/errorStatusMessage.tsx +++ b/airbyte-webapp/src/utils/errorStatusMessage.tsx @@ -1,6 +1,10 @@ import { FormattedMessage } from "react-intl"; -export const createFormErrorMessage = (error: { status?: number; message?: string }): JSX.Element | string | null => { +export class FormError extends Error { + status?: number; +} + +export const createFormErrorMessage = (error: FormError): JSX.Element | string | null => { if (error.message) { return error.message; } diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/formConfig.test.ts b/airbyte-webapp/src/views/Connection/ConnectionForm/formConfig.test.ts index 54fa6416a640..910eeb7d0422 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/formConfig.test.ts +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/formConfig.test.ts @@ -1,12 +1,13 @@ import { renderHook } from "@testing-library/react-hooks"; import { frequencyConfig } from "config/frequencyConfig"; -import { ConnectionScheduleTimeUnit } from "core/request/AirbyteClient"; +import { NormalizationType } from "core/domain/connection"; +import { ConnectionScheduleTimeUnit, OperationRead } from "core/request/AirbyteClient"; import { TestWrapper as wrapper } from "utils/testutils"; -import { useFrequencyDropdownData } from "./formConfig"; +import { mapFormPropsToOperation, useFrequencyDropdownData } from "./formConfig"; -describe("useFrequencyDropdownData", () => { +describe("#useFrequencyDropdownData", () => { it("should return only default frequencies when no additional frequency is provided", () => { const { result } = renderHook(() => useFrequencyDropdownData(undefined), { wrapper }); expect(result.current.map((item) => item.value)).toEqual(frequencyConfig); @@ -36,3 +37,126 @@ describe("useFrequencyDropdownData", () => { expect(result.current).toContainEqual({ label: "Every 7 minutes", value: { units: 7, timeUnit: "minutes" } }); }); }); + +describe("#mapFormPropsToOperation", () => { + const workspaceId = "asdf"; + const normalization: OperationRead = { + workspaceId, + operationId: "asdf", + name: "asdf", + operatorConfiguration: { + operatorType: "normalization", + }, + }; + + it("should add any included transformations", () => { + expect( + mapFormPropsToOperation( + { + transformations: [normalization], + }, + undefined, + "asdf" + ) + ).toEqual([normalization]); + }); + + it("should add a basic normalization if normalization is set to basic", () => { + expect( + mapFormPropsToOperation( + { + normalization: NormalizationType.basic, + }, + + undefined, + workspaceId + ) + ).toEqual([ + { + name: "Normalization", + operatorConfiguration: { + normalization: { + option: "basic", + }, + operatorType: "normalization", + }, + workspaceId, + }, + ]); + }); + + it("should include any provided initial operations and not include the basic normalization operation when normalization type is basic", () => { + expect( + mapFormPropsToOperation( + { + normalization: NormalizationType.basic, + }, + [normalization], + workspaceId + ) + ).toEqual([normalization]); + }); + + it("should not include any provided initial operations and not include the basic normalization operation when normalization type is raw", () => { + expect( + mapFormPropsToOperation( + { + normalization: NormalizationType.raw, + }, + [normalization], + workspaceId + ) + ).toEqual([]); + }); + + it("should include provided transformations when normalization type is raw, but not any provided normalizations", () => { + expect( + mapFormPropsToOperation( + { + normalization: NormalizationType.raw, + transformations: [normalization], + }, + [normalization], + workspaceId + ) + ).toEqual([normalization]); + }); + + it("should include provided transformations and normalizations when normalization type is basic", () => { + expect( + mapFormPropsToOperation( + { + normalization: NormalizationType.basic, + transformations: [normalization], + }, + [normalization], + workspaceId + ) + ).toEqual([normalization, normalization]); + }); + + it("should include provided transformations and default normalization when normalization type is basic and no normalizations have been provided", () => { + expect( + mapFormPropsToOperation( + { + normalization: NormalizationType.basic, + transformations: [normalization], + }, + undefined, + workspaceId + ) + ).toEqual([ + { + name: "Normalization", + operatorConfiguration: { + normalization: { + option: "basic", + }, + operatorType: "normalization", + }, + workspaceId, + }, + normalization, + ]); + }); +}); From 3ec2cf2c9ebd177b2b5ceec84262d799eb40a0b3 Mon Sep 17 00:00:00 2001 From: KC Date: Fri, 2 Sep 2022 10:54:52 -0400 Subject: [PATCH 025/107] useUniqueFormId and useInitialValues tests --- .../services/FormChangeTracker/hooks.test.ts | 19 + .../__snapshots__/formConfig.test.ts.snap | 3193 +++++++++++++++++ .../ConnectionForm/formConfig.test.ts | 50 +- .../Connection/ConnectionForm/formConfig.tsx | 32 +- 4 files changed, 3270 insertions(+), 24 deletions(-) create mode 100644 airbyte-webapp/src/hooks/services/FormChangeTracker/hooks.test.ts create mode 100644 airbyte-webapp/src/views/Connection/ConnectionForm/__snapshots__/formConfig.test.ts.snap diff --git a/airbyte-webapp/src/hooks/services/FormChangeTracker/hooks.test.ts b/airbyte-webapp/src/hooks/services/FormChangeTracker/hooks.test.ts new file mode 100644 index 000000000000..2de194a114c7 --- /dev/null +++ b/airbyte-webapp/src/hooks/services/FormChangeTracker/hooks.test.ts @@ -0,0 +1,19 @@ +import { renderHook } from "@testing-library/react-hooks"; + +import { useUniqueFormId } from "./hooks"; + +describe("#useUniqueFormId", () => { + it("should use what is passed into it", () => { + const { + result: { current }, + } = renderHook(() => useUniqueFormId("asdf")); + expect(current).toBe("asdf"); + }); + + it("should generate an id like /form_/", () => { + const { + result: { current }, + } = renderHook(useUniqueFormId); + expect(current).toMatch(/form_/); + }); +}); diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/__snapshots__/formConfig.test.ts.snap b/airbyte-webapp/src/views/Connection/ConnectionForm/__snapshots__/formConfig.test.ts.snap new file mode 100644 index 000000000000..1904aa6d7fee --- /dev/null +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/__snapshots__/formConfig.test.ts.snap @@ -0,0 +1,3193 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`#useInitialValues should generate initial values w/ edit mode: false 1`] = ` +Object { + "all": Array [ + Object { + "name": "Scrafty <> Heroku Postgres", + "namespaceDefinition": "source", + "namespaceFormat": "\${SOURCE_NAMESPACE}", + "normalization": "basic", + "prefix": "", + "scheduleData": null, + "syncCatalog": Object { + "streams": Array [ + Object { + "config": Object { + "aliasName": "pokemon", + "cursorField": Array [], + "destinationSyncMode": "overwrite", + "primaryKey": Array [], + "selected": true, + "syncMode": "full_refresh", + }, + "id": "0", + "stream": Object { + "defaultCursorField": Array [], + "jsonSchema": Object { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": Object { + "abilities": Object { + "items": Object { + "properties": Object { + "ability": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "is_hidden": Object { + "type": Array [ + "null", + "boolean", + ], + }, + "slot": Object { + "type": Array [ + "null", + "integer", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "base_experience": Object { + "type": Array [ + "null", + "integer", + ], + }, + "forms": Object { + "items": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "game_indices": Object { + "items": Object { + "properties": Object { + "game_index": Object { + "type": Array [ + "null", + "integer", + ], + }, + "version": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "height": Object { + "type": Array [ + "null", + "integer", + ], + }, + "held_items": Object { + "items": Object { + "properties": Object { + "item": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "version_details": Object { + "items": Object { + "properties": Object { + "rarity": Object { + "type": Array [ + "null", + "integer", + ], + }, + "version": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "id": Object { + "type": Array [ + "null", + "integer", + ], + }, + "is_default ": Object { + "type": Array [ + "null", + "boolean", + ], + }, + "location_area_encounters": Object { + "type": Array [ + "null", + "string", + ], + }, + "moves": Object { + "items": Object { + "properties": Object { + "move": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "version_group_details": Object { + "items": Object { + "properties": Object { + "level_learned_at": Object { + "type": Array [ + "null", + "integer", + ], + }, + "move_learn_method": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "version_group": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "order": Object { + "type": Array [ + "null", + "integer", + ], + }, + "species": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "sprites": Object { + "properties": Object { + "back_default": Object { + "type": Array [ + "null", + "string", + ], + }, + "back_female": Object { + "type": Array [ + "null", + "string", + ], + }, + "back_shiny": Object { + "type": Array [ + "null", + "string", + ], + }, + "back_shiny_female": Object { + "type": Array [ + "null", + "string", + ], + }, + "front_default": Object { + "type": Array [ + "null", + "string", + ], + }, + "front_female": Object { + "type": Array [ + "null", + "string", + ], + }, + "front_shiny": Object { + "type": Array [ + "null", + "string", + ], + }, + "front_shiny_female": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "stats": Object { + "items": Object { + "properties": Object { + "base_stat": Object { + "type": Array [ + "null", + "integer", + ], + }, + "effort": Object { + "type": Array [ + "null", + "integer", + ], + }, + "stat": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "types": Object { + "items": Object { + "properties": Object { + "slot": Object { + "type": Array [ + "null", + "integer", + ], + }, + "type": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "weight": Object { + "type": Array [ + "null", + "integer", + ], + }, + }, + "type": "object", + }, + "name": "pokemon", + "sourceDefinedPrimaryKey": Array [], + "supportedSyncModes": Array [ + "full_refresh", + ], + }, + }, + ], + }, + "transformations": Array [], + }, + ], + "current": Object { + "name": "Scrafty <> Heroku Postgres", + "namespaceDefinition": "source", + "namespaceFormat": "\${SOURCE_NAMESPACE}", + "normalization": "basic", + "prefix": "", + "scheduleData": null, + "syncCatalog": Object { + "streams": Array [ + Object { + "config": Object { + "aliasName": "pokemon", + "cursorField": Array [], + "destinationSyncMode": "overwrite", + "primaryKey": Array [], + "selected": true, + "syncMode": "full_refresh", + }, + "id": "0", + "stream": Object { + "defaultCursorField": Array [], + "jsonSchema": Object { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": Object { + "abilities": Object { + "items": Object { + "properties": Object { + "ability": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "is_hidden": Object { + "type": Array [ + "null", + "boolean", + ], + }, + "slot": Object { + "type": Array [ + "null", + "integer", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "base_experience": Object { + "type": Array [ + "null", + "integer", + ], + }, + "forms": Object { + "items": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "game_indices": Object { + "items": Object { + "properties": Object { + "game_index": Object { + "type": Array [ + "null", + "integer", + ], + }, + "version": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "height": Object { + "type": Array [ + "null", + "integer", + ], + }, + "held_items": Object { + "items": Object { + "properties": Object { + "item": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "version_details": Object { + "items": Object { + "properties": Object { + "rarity": Object { + "type": Array [ + "null", + "integer", + ], + }, + "version": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "id": Object { + "type": Array [ + "null", + "integer", + ], + }, + "is_default ": Object { + "type": Array [ + "null", + "boolean", + ], + }, + "location_area_encounters": Object { + "type": Array [ + "null", + "string", + ], + }, + "moves": Object { + "items": Object { + "properties": Object { + "move": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "version_group_details": Object { + "items": Object { + "properties": Object { + "level_learned_at": Object { + "type": Array [ + "null", + "integer", + ], + }, + "move_learn_method": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "version_group": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "order": Object { + "type": Array [ + "null", + "integer", + ], + }, + "species": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "sprites": Object { + "properties": Object { + "back_default": Object { + "type": Array [ + "null", + "string", + ], + }, + "back_female": Object { + "type": Array [ + "null", + "string", + ], + }, + "back_shiny": Object { + "type": Array [ + "null", + "string", + ], + }, + "back_shiny_female": Object { + "type": Array [ + "null", + "string", + ], + }, + "front_default": Object { + "type": Array [ + "null", + "string", + ], + }, + "front_female": Object { + "type": Array [ + "null", + "string", + ], + }, + "front_shiny": Object { + "type": Array [ + "null", + "string", + ], + }, + "front_shiny_female": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "stats": Object { + "items": Object { + "properties": Object { + "base_stat": Object { + "type": Array [ + "null", + "integer", + ], + }, + "effort": Object { + "type": Array [ + "null", + "integer", + ], + }, + "stat": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "types": Object { + "items": Object { + "properties": Object { + "slot": Object { + "type": Array [ + "null", + "integer", + ], + }, + "type": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "weight": Object { + "type": Array [ + "null", + "integer", + ], + }, + }, + "type": "object", + }, + "name": "pokemon", + "sourceDefinedPrimaryKey": Array [], + "supportedSyncModes": Array [ + "full_refresh", + ], + }, + }, + ], + }, + "transformations": Array [], + }, + "error": undefined, +} +`; + +exports[`#useInitialValues should generate initial values w/ edit mode: true 1`] = ` +Object { + "all": Array [ + Object { + "name": "Scrafty <> Heroku Postgres", + "namespaceDefinition": "source", + "namespaceFormat": "\${SOURCE_NAMESPACE}", + "normalization": "basic", + "prefix": "", + "scheduleData": null, + "syncCatalog": Object { + "streams": Array [ + Object { + "config": Object { + "aliasName": "pokemon", + "cursorField": Array [], + "destinationSyncMode": "append", + "primaryKey": Array [], + "selected": true, + "syncMode": "full_refresh", + }, + "id": "0", + "stream": Object { + "defaultCursorField": Array [], + "jsonSchema": Object { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": Object { + "abilities": Object { + "items": Object { + "properties": Object { + "ability": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "is_hidden": Object { + "type": Array [ + "null", + "boolean", + ], + }, + "slot": Object { + "type": Array [ + "null", + "integer", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "base_experience": Object { + "type": Array [ + "null", + "integer", + ], + }, + "forms": Object { + "items": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "game_indices": Object { + "items": Object { + "properties": Object { + "game_index": Object { + "type": Array [ + "null", + "integer", + ], + }, + "version": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "height": Object { + "type": Array [ + "null", + "integer", + ], + }, + "held_items": Object { + "items": Object { + "properties": Object { + "item": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "version_details": Object { + "items": Object { + "properties": Object { + "rarity": Object { + "type": Array [ + "null", + "integer", + ], + }, + "version": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "id": Object { + "type": Array [ + "null", + "integer", + ], + }, + "is_default ": Object { + "type": Array [ + "null", + "boolean", + ], + }, + "location_area_encounters": Object { + "type": Array [ + "null", + "string", + ], + }, + "moves": Object { + "items": Object { + "properties": Object { + "move": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "version_group_details": Object { + "items": Object { + "properties": Object { + "level_learned_at": Object { + "type": Array [ + "null", + "integer", + ], + }, + "move_learn_method": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "version_group": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "order": Object { + "type": Array [ + "null", + "integer", + ], + }, + "species": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "sprites": Object { + "properties": Object { + "back_default": Object { + "type": Array [ + "null", + "string", + ], + }, + "back_female": Object { + "type": Array [ + "null", + "string", + ], + }, + "back_shiny": Object { + "type": Array [ + "null", + "string", + ], + }, + "back_shiny_female": Object { + "type": Array [ + "null", + "string", + ], + }, + "front_default": Object { + "type": Array [ + "null", + "string", + ], + }, + "front_female": Object { + "type": Array [ + "null", + "string", + ], + }, + "front_shiny": Object { + "type": Array [ + "null", + "string", + ], + }, + "front_shiny_female": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "stats": Object { + "items": Object { + "properties": Object { + "base_stat": Object { + "type": Array [ + "null", + "integer", + ], + }, + "effort": Object { + "type": Array [ + "null", + "integer", + ], + }, + "stat": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "types": Object { + "items": Object { + "properties": Object { + "slot": Object { + "type": Array [ + "null", + "integer", + ], + }, + "type": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "weight": Object { + "type": Array [ + "null", + "integer", + ], + }, + }, + "type": "object", + }, + "name": "pokemon", + "sourceDefinedPrimaryKey": Array [], + "supportedSyncModes": Array [ + "full_refresh", + ], + }, + }, + ], + }, + "transformations": Array [], + }, + ], + "current": Object { + "name": "Scrafty <> Heroku Postgres", + "namespaceDefinition": "source", + "namespaceFormat": "\${SOURCE_NAMESPACE}", + "normalization": "basic", + "prefix": "", + "scheduleData": null, + "syncCatalog": Object { + "streams": Array [ + Object { + "config": Object { + "aliasName": "pokemon", + "cursorField": Array [], + "destinationSyncMode": "append", + "primaryKey": Array [], + "selected": true, + "syncMode": "full_refresh", + }, + "id": "0", + "stream": Object { + "defaultCursorField": Array [], + "jsonSchema": Object { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": Object { + "abilities": Object { + "items": Object { + "properties": Object { + "ability": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "is_hidden": Object { + "type": Array [ + "null", + "boolean", + ], + }, + "slot": Object { + "type": Array [ + "null", + "integer", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "base_experience": Object { + "type": Array [ + "null", + "integer", + ], + }, + "forms": Object { + "items": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "game_indices": Object { + "items": Object { + "properties": Object { + "game_index": Object { + "type": Array [ + "null", + "integer", + ], + }, + "version": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "height": Object { + "type": Array [ + "null", + "integer", + ], + }, + "held_items": Object { + "items": Object { + "properties": Object { + "item": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "version_details": Object { + "items": Object { + "properties": Object { + "rarity": Object { + "type": Array [ + "null", + "integer", + ], + }, + "version": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "id": Object { + "type": Array [ + "null", + "integer", + ], + }, + "is_default ": Object { + "type": Array [ + "null", + "boolean", + ], + }, + "location_area_encounters": Object { + "type": Array [ + "null", + "string", + ], + }, + "moves": Object { + "items": Object { + "properties": Object { + "move": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "version_group_details": Object { + "items": Object { + "properties": Object { + "level_learned_at": Object { + "type": Array [ + "null", + "integer", + ], + }, + "move_learn_method": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "version_group": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "order": Object { + "type": Array [ + "null", + "integer", + ], + }, + "species": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "sprites": Object { + "properties": Object { + "back_default": Object { + "type": Array [ + "null", + "string", + ], + }, + "back_female": Object { + "type": Array [ + "null", + "string", + ], + }, + "back_shiny": Object { + "type": Array [ + "null", + "string", + ], + }, + "back_shiny_female": Object { + "type": Array [ + "null", + "string", + ], + }, + "front_default": Object { + "type": Array [ + "null", + "string", + ], + }, + "front_female": Object { + "type": Array [ + "null", + "string", + ], + }, + "front_shiny": Object { + "type": Array [ + "null", + "string", + ], + }, + "front_shiny_female": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "stats": Object { + "items": Object { + "properties": Object { + "base_stat": Object { + "type": Array [ + "null", + "integer", + ], + }, + "effort": Object { + "type": Array [ + "null", + "integer", + ], + }, + "stat": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "types": Object { + "items": Object { + "properties": Object { + "slot": Object { + "type": Array [ + "null", + "integer", + ], + }, + "type": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "weight": Object { + "type": Array [ + "null", + "integer", + ], + }, + }, + "type": "object", + }, + "name": "pokemon", + "sourceDefinedPrimaryKey": Array [], + "supportedSyncModes": Array [ + "full_refresh", + ], + }, + }, + ], + }, + "transformations": Array [], + }, + "error": undefined, +} +`; + +exports[`#useInitialValues should generate initial values w/ no edit mode 1`] = ` +Object { + "all": Array [ + Object { + "name": "Scrafty <> Heroku Postgres", + "namespaceDefinition": "source", + "namespaceFormat": "\${SOURCE_NAMESPACE}", + "normalization": "basic", + "prefix": "", + "scheduleData": null, + "syncCatalog": Object { + "streams": Array [ + Object { + "config": Object { + "aliasName": "pokemon", + "cursorField": Array [], + "destinationSyncMode": "overwrite", + "primaryKey": Array [], + "selected": true, + "syncMode": "full_refresh", + }, + "id": "0", + "stream": Object { + "defaultCursorField": Array [], + "jsonSchema": Object { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": Object { + "abilities": Object { + "items": Object { + "properties": Object { + "ability": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "is_hidden": Object { + "type": Array [ + "null", + "boolean", + ], + }, + "slot": Object { + "type": Array [ + "null", + "integer", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "base_experience": Object { + "type": Array [ + "null", + "integer", + ], + }, + "forms": Object { + "items": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "game_indices": Object { + "items": Object { + "properties": Object { + "game_index": Object { + "type": Array [ + "null", + "integer", + ], + }, + "version": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "height": Object { + "type": Array [ + "null", + "integer", + ], + }, + "held_items": Object { + "items": Object { + "properties": Object { + "item": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "version_details": Object { + "items": Object { + "properties": Object { + "rarity": Object { + "type": Array [ + "null", + "integer", + ], + }, + "version": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "id": Object { + "type": Array [ + "null", + "integer", + ], + }, + "is_default ": Object { + "type": Array [ + "null", + "boolean", + ], + }, + "location_area_encounters": Object { + "type": Array [ + "null", + "string", + ], + }, + "moves": Object { + "items": Object { + "properties": Object { + "move": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "version_group_details": Object { + "items": Object { + "properties": Object { + "level_learned_at": Object { + "type": Array [ + "null", + "integer", + ], + }, + "move_learn_method": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "version_group": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "order": Object { + "type": Array [ + "null", + "integer", + ], + }, + "species": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "sprites": Object { + "properties": Object { + "back_default": Object { + "type": Array [ + "null", + "string", + ], + }, + "back_female": Object { + "type": Array [ + "null", + "string", + ], + }, + "back_shiny": Object { + "type": Array [ + "null", + "string", + ], + }, + "back_shiny_female": Object { + "type": Array [ + "null", + "string", + ], + }, + "front_default": Object { + "type": Array [ + "null", + "string", + ], + }, + "front_female": Object { + "type": Array [ + "null", + "string", + ], + }, + "front_shiny": Object { + "type": Array [ + "null", + "string", + ], + }, + "front_shiny_female": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "stats": Object { + "items": Object { + "properties": Object { + "base_stat": Object { + "type": Array [ + "null", + "integer", + ], + }, + "effort": Object { + "type": Array [ + "null", + "integer", + ], + }, + "stat": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "types": Object { + "items": Object { + "properties": Object { + "slot": Object { + "type": Array [ + "null", + "integer", + ], + }, + "type": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "weight": Object { + "type": Array [ + "null", + "integer", + ], + }, + }, + "type": "object", + }, + "name": "pokemon", + "sourceDefinedPrimaryKey": Array [], + "supportedSyncModes": Array [ + "full_refresh", + ], + }, + }, + ], + }, + "transformations": Array [], + }, + ], + "current": Object { + "name": "Scrafty <> Heroku Postgres", + "namespaceDefinition": "source", + "namespaceFormat": "\${SOURCE_NAMESPACE}", + "normalization": "basic", + "prefix": "", + "scheduleData": null, + "syncCatalog": Object { + "streams": Array [ + Object { + "config": Object { + "aliasName": "pokemon", + "cursorField": Array [], + "destinationSyncMode": "overwrite", + "primaryKey": Array [], + "selected": true, + "syncMode": "full_refresh", + }, + "id": "0", + "stream": Object { + "defaultCursorField": Array [], + "jsonSchema": Object { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": Object { + "abilities": Object { + "items": Object { + "properties": Object { + "ability": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "is_hidden": Object { + "type": Array [ + "null", + "boolean", + ], + }, + "slot": Object { + "type": Array [ + "null", + "integer", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "base_experience": Object { + "type": Array [ + "null", + "integer", + ], + }, + "forms": Object { + "items": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "game_indices": Object { + "items": Object { + "properties": Object { + "game_index": Object { + "type": Array [ + "null", + "integer", + ], + }, + "version": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "height": Object { + "type": Array [ + "null", + "integer", + ], + }, + "held_items": Object { + "items": Object { + "properties": Object { + "item": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "version_details": Object { + "items": Object { + "properties": Object { + "rarity": Object { + "type": Array [ + "null", + "integer", + ], + }, + "version": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "id": Object { + "type": Array [ + "null", + "integer", + ], + }, + "is_default ": Object { + "type": Array [ + "null", + "boolean", + ], + }, + "location_area_encounters": Object { + "type": Array [ + "null", + "string", + ], + }, + "moves": Object { + "items": Object { + "properties": Object { + "move": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "version_group_details": Object { + "items": Object { + "properties": Object { + "level_learned_at": Object { + "type": Array [ + "null", + "integer", + ], + }, + "move_learn_method": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "version_group": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "order": Object { + "type": Array [ + "null", + "integer", + ], + }, + "species": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "sprites": Object { + "properties": Object { + "back_default": Object { + "type": Array [ + "null", + "string", + ], + }, + "back_female": Object { + "type": Array [ + "null", + "string", + ], + }, + "back_shiny": Object { + "type": Array [ + "null", + "string", + ], + }, + "back_shiny_female": Object { + "type": Array [ + "null", + "string", + ], + }, + "front_default": Object { + "type": Array [ + "null", + "string", + ], + }, + "front_female": Object { + "type": Array [ + "null", + "string", + ], + }, + "front_shiny": Object { + "type": Array [ + "null", + "string", + ], + }, + "front_shiny_female": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "stats": Object { + "items": Object { + "properties": Object { + "base_stat": Object { + "type": Array [ + "null", + "integer", + ], + }, + "effort": Object { + "type": Array [ + "null", + "integer", + ], + }, + "stat": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "types": Object { + "items": Object { + "properties": Object { + "slot": Object { + "type": Array [ + "null", + "integer", + ], + }, + "type": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "weight": Object { + "type": Array [ + "null", + "integer", + ], + }, + }, + "type": "object", + }, + "name": "pokemon", + "sourceDefinedPrimaryKey": Array [], + "supportedSyncModes": Array [ + "full_refresh", + ], + }, + }, + ], + }, + "transformations": Array [], + }, + "error": undefined, +} +`; diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/formConfig.test.ts b/airbyte-webapp/src/views/Connection/ConnectionForm/formConfig.test.ts index 910eeb7d0422..465f915da258 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/formConfig.test.ts +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/formConfig.test.ts @@ -2,10 +2,17 @@ import { renderHook } from "@testing-library/react-hooks"; import { frequencyConfig } from "config/frequencyConfig"; import { NormalizationType } from "core/domain/connection"; -import { ConnectionScheduleTimeUnit, OperationRead } from "core/request/AirbyteClient"; +import { + ConnectionScheduleTimeUnit, + DestinationDefinitionSpecificationRead, + OperationRead, + WebBackendConnectionRead, +} from "core/request/AirbyteClient"; +import mockConnection from "hooks/services/Connection/mockConnection.json"; +import mockDestinationDefinition from "hooks/services/Connection/mockDestinationDefinition.json"; import { TestWrapper as wrapper } from "utils/testutils"; -import { mapFormPropsToOperation, useFrequencyDropdownData } from "./formConfig"; +import { mapFormPropsToOperation, useFrequencyDropdownData, useInitialValues } from "./formConfig"; describe("#useFrequencyDropdownData", () => { it("should return only default frequencies when no additional frequency is provided", () => { @@ -160,3 +167,42 @@ describe("#mapFormPropsToOperation", () => { ]); }); }); + +describe("#useInitialValues", () => { + it("should generate initial values w/ no edit mode", () => { + const { result } = renderHook(() => + useInitialValues( + mockConnection as WebBackendConnectionRead, + mockDestinationDefinition as DestinationDefinitionSpecificationRead + ) + ); + expect(result).toMatchSnapshot(); + }); + + it("should generate initial values w/ edit mode: false", () => { + const { result } = renderHook(() => + useInitialValues( + mockConnection as WebBackendConnectionRead, + mockDestinationDefinition as DestinationDefinitionSpecificationRead, + false + ) + ); + expect(result).toMatchSnapshot(); + }); + + it("should generate initial values w/ edit mode: true", () => { + const { result } = renderHook(() => + useInitialValues( + mockConnection as WebBackendConnectionRead, + mockDestinationDefinition as DestinationDefinitionSpecificationRead, + true + ) + ); + expect(result).toMatchSnapshot(); + }); + + // This is a low-priority test + it.todo( + "should test for supportsDbt+initialValues.transformations and supportsNormalization+initialValues.normalization" + ); +}); diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/formConfig.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/formConfig.tsx index 8c09b2ab7c37..8ba9d26b6e27 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/formConfig.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/formConfig.tsx @@ -30,7 +30,7 @@ import { useCurrentWorkspace } from "services/workspaces/WorkspacesService"; import calculateInitialCatalog from "./calculateInitialCatalog"; -interface FormikConnectionFormValues { +export interface FormikConnectionFormValues { name?: string; scheduleType?: ConnectionScheduleType | null; scheduleData?: ConnectionScheduleData | null; @@ -42,9 +42,9 @@ interface FormikConnectionFormValues { normalization?: NormalizationType; } -type ConnectionFormValues = ValuesProps; +export type ConnectionFormValues = ValuesProps; -const SUPPORTED_MODES: Array<[SyncMode, DestinationSyncMode]> = [ +export const SUPPORTED_MODES: Array<[SyncMode, DestinationSyncMode]> = [ [SyncMode.incremental, DestinationSyncMode.append_dedup], [SyncMode.full_refresh, DestinationSyncMode.overwrite], [SyncMode.incremental, DestinationSyncMode.append], @@ -58,7 +58,7 @@ const DEFAULT_SCHEDULE: ConnectionScheduleData = { }, }; -function useDefaultTransformation(): OperationCreate { +export function useDefaultTransformation(): OperationCreate { const workspace = useCurrentWorkspace(); return { name: "My dbt transformations", @@ -74,7 +74,7 @@ function useDefaultTransformation(): OperationCreate { }; } -const connectionValidationSchema = yup +export const connectionValidationSchema = yup .object({ name: yup.string().required("form.empty.error"), scheduleType: yup.string().oneOf([ConnectionScheduleType.manual, ConnectionScheduleType.basic]), @@ -173,7 +173,7 @@ const connectionValidationSchema = yup * @param initialOperations * @param workspaceId */ -function mapFormPropsToOperation( +export function mapFormPropsToOperation( values: { transformations?: OperationRead[]; normalization?: NormalizationType; @@ -211,10 +211,10 @@ function mapFormPropsToOperation( return newOperations; } -const getInitialTransformations = (operations: OperationCreate[]): OperationRead[] => +export const getInitialTransformations = (operations: OperationCreate[]): OperationRead[] => operations?.filter(isDbtTransformation) ?? []; -const getInitialNormalization = ( +export const getInitialNormalization = ( operations?: Array, isEditMode?: boolean ): NormalizationType => { @@ -228,7 +228,7 @@ const getInitialNormalization = ( : NormalizationType.basic; }; -const useInitialValues = ( +export const useInitialValues = ( connection: ConnectionOrPartialConnection, destDefinition: DestinationDefinitionSpecificationRead, isEditMode?: boolean @@ -277,7 +277,7 @@ const useInitialValues = ( ]); }; -const useFrequencyDropdownData = ( +export const useFrequencyDropdownData = ( additionalFrequency: WebBackendConnectionRead["scheduleData"] ): DropDownRow.IDataItem[] => { const { formatMessage } = useIntl(); @@ -308,15 +308,3 @@ const useFrequencyDropdownData = ( })); }, [formatMessage, additionalFrequency]); }; - -export type { ConnectionFormValues, FormikConnectionFormValues }; -export { - connectionValidationSchema, - useInitialValues, - useFrequencyDropdownData, - mapFormPropsToOperation, - SUPPORTED_MODES, - useDefaultTransformation, - getInitialNormalization, - getInitialTransformations, -}; From 0452458f5f6b22cf23a78a4260d1cffbb293b9e3 Mon Sep 17 00:00:00 2001 From: KC Date: Fri, 2 Sep 2022 13:59:58 -0400 Subject: [PATCH 026/107] Cleanup, onFrequencySelect is moved to its use location, better test wrapper, and expanding use of FormError --- .../CreateConnectionContent.tsx | 35 +--------------- .../Connection/ConnectionFormService.test.tsx | 5 --- .../Connection/ConnectionFormService.tsx | 14 +------ .../OnboardingPage/components/SourceStep.tsx | 9 +--- .../components/SourceForm.tsx | 4 +- airbyte-webapp/src/utils/testutils.tsx | 21 ++++++---- .../ConnectionForm/ConnectionForm.tsx | 42 +++++++++++++++++-- 7 files changed, 57 insertions(+), 73 deletions(-) diff --git a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx index 1b5aad70bd1e..5d773506a54f 100644 --- a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx +++ b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx @@ -4,13 +4,10 @@ import React, { Suspense, useCallback, useRef } from "react"; import { FormattedMessage } from "react-intl"; import { Button, ContentCard } from "components"; -import { IDataItem } from "components/base/DropDown/components/Option"; import { JobItem } from "components/JobItem/JobItem"; import LoadingSchema from "components/LoadingSchema"; -import { Action, Namespace } from "core/analytics"; import { LogsRequestError } from "core/request/LogsRequestError"; -import { useAnalyticsService } from "hooks/services/Analytics"; import { ConnectionFormServiceProvider } from "hooks/services/Connection/ConnectionFormService"; import { useCreateConnection, ValuesProps } from "hooks/services/useConnectionHook"; import useRouter from "hooks/useRouter"; @@ -34,7 +31,6 @@ const CreateConnectionContent: React.FC = ({ }) => { const { mutateAsync: createConnection } = useCreateConnection(); const newConnection = useRef(null); - const analyticsService = useAnalyticsService(); const { push } = useRouter(); const { schema, isLoading, schemaErrorStatus, catalogId, onDiscoverSchema } = useDiscoverSchema( @@ -65,11 +61,10 @@ const CreateConnectionContent: React.FC = ({ sourceCatalogId: catalogId, }); - console.log(afterSubmitConnection?.toString()); // We need the new connection ID to know where to go. newConnection.current = createdConnection; }, - [afterSubmitConnection, catalogId, createConnection, destination, source] + [catalogId, createConnection, destination, source] ); const onAfterSubmit = useCallback( @@ -77,33 +72,6 @@ const CreateConnectionContent: React.FC = ({ [afterSubmitConnection, newConnection, push] ); - const onFrequencySelect = useCallback( - (item: IDataItem | null) => { - const enabledStreams = connection.syncCatalog.streams.filter((stream) => stream.config?.selected).length; - - if (item) { - analyticsService.track(Namespace.CONNECTION, Action.FREQUENCY, { - actionDescription: "Frequency selected", - frequency: item.label, - connector_source_definition: source?.sourceName, - connector_source_definition_id: source?.sourceDefinitionId, - connector_destination_definition: destination?.destinationName, - connector_destination_definition_id: destination?.destinationDefinitionId, - available_streams: connection.syncCatalog.streams.length, - enabled_streams: enabledStreams, - }); - } - }, - [ - analyticsService, - connection.syncCatalog.streams, - destination?.destinationDefinitionId, - destination?.destinationName, - source?.sourceDefinitionId, - source?.sourceName, - ] - ); - if (schemaErrorStatus) { const job = LogsRequestError.extractJobInfo(schemaErrorStatus); return ( @@ -123,7 +91,6 @@ const CreateConnectionContent: React.FC = ({ mode="create" onSubmit={onSubmitConnectionStep} onAfterSubmit={onAfterSubmit} - onFrequencySelect={onFrequencySelect} > { const onSubmit = jest.fn(); const onAfterSubmit = jest.fn(); - const onFrequencySelect = jest.fn(); const onCancel = jest.fn(); beforeEach(() => { onSubmit.mockReset(); onAfterSubmit.mockReset(); - onFrequencySelect.mockReset(); onCancel.mockReset(); }); @@ -74,7 +72,6 @@ describe("ConnectionFormService", () => { mode: "create", onSubmit, onAfterSubmit, - onFrequencySelect, onCancel, }, }); @@ -115,7 +112,6 @@ describe("ConnectionFormService", () => { mode: "create", onSubmit, onAfterSubmit, - onFrequencySelect, onCancel, }, }); @@ -142,7 +138,6 @@ describe("ConnectionFormService", () => { mode: "create", onSubmit, onAfterSubmit, - onFrequencySelect, onCancel, }, }); diff --git a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx b/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx index 66dae96d1a5b..01d54f575f8f 100644 --- a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx +++ b/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx @@ -1,8 +1,6 @@ import { FormikHelpers } from "formik"; import React, { createContext, useCallback, useContext, useMemo, useState } from "react"; -import { DropDownRow } from "components"; - import { ConnectionScheduleType, WebBackendConnectionRead } from "core/request/AirbyteClient"; import { useGetDestinationDefinitionSpecification } from "services/connector/DestinationDefinitionSpecificationService"; import { useCurrentWorkspaceId } from "services/workspaces/WorkspacesService"; @@ -30,19 +28,10 @@ export interface ConnectionServiceProps { formId?: string; onSubmit: (values: ConnectionFormValues) => Promise; onAfterSubmit?: () => void; - onFrequencySelect?: (item: DropDownRow.IDataItem) => void; onCancel?: () => void; } -const useConnectionForm = ({ - connection, - mode, - formId, - onSubmit, - onAfterSubmit, - onFrequencySelect, - onCancel, -}: ConnectionServiceProps) => { +const useConnectionForm = ({ connection, mode, formId, onSubmit, onAfterSubmit, onCancel }: ConnectionServiceProps) => { const [submitError, setSubmitError] = useState(null); const workspaceId = useCurrentWorkspaceId(); const { clearFormChange } = useFormChangeTrackerService(); @@ -100,7 +89,6 @@ const useConnectionForm = ({ formId, onFormSubmit, onAfterSubmit, - onFrequencySelect, onCancel, }; }; diff --git a/airbyte-webapp/src/pages/OnboardingPage/components/SourceStep.tsx b/airbyte-webapp/src/pages/OnboardingPage/components/SourceStep.tsx index 2aea7372f02f..25bd65cf1e7e 100644 --- a/airbyte-webapp/src/pages/OnboardingPage/components/SourceStep.tsx +++ b/airbyte-webapp/src/pages/OnboardingPage/components/SourceStep.tsx @@ -2,13 +2,12 @@ import React, { useEffect, useState } from "react"; import { Action, Namespace } from "core/analytics"; import { ConnectionConfiguration } from "core/domain/connection"; -import { JobInfo } from "core/domain/job"; import { LogsRequestError } from "core/request/LogsRequestError"; import { useAnalyticsService } from "hooks/services/Analytics"; import { useCreateSource } from "hooks/services/useSourceHook"; import { useSourceDefinitionList } from "services/connector/SourceDefinitionService"; import { useGetSourceDefinitionSpecificationAsync } from "services/connector/SourceDefinitionSpecificationService"; -import { createFormErrorMessage } from "utils/errorStatusMessage"; +import { createFormErrorMessage, FormError } from "utils/errorStatusMessage"; import { ConnectorCard } from "views/Connector/ConnectorCard"; import { useDocumentationPanelContext } from "views/Connector/ConnectorDocumentationLayout/DocumentationPanelContext"; @@ -21,11 +20,7 @@ const SourceStep: React.FC = ({ onNextStep, onSuccess }) => { const { sourceDefinitions } = useSourceDefinitionList(); const [sourceDefinitionId, setSourceDefinitionId] = useState(null); const [successRequest, setSuccessRequest] = useState(false); - const [error, setError] = useState<{ - status: number; - response: JobInfo; - message: string; - } | null>(null); + const [error, setError] = useState(null); const { setDocumentationUrl, setDocumentationPanelOpen } = useDocumentationPanelContext(); const { mutateAsync: createSource } = useCreateSource(); diff --git a/airbyte-webapp/src/pages/SourcesPage/pages/CreateSourcePage/components/SourceForm.tsx b/airbyte-webapp/src/pages/SourcesPage/pages/CreateSourcePage/components/SourceForm.tsx index e2ee9c1251b5..d8e29542546e 100644 --- a/airbyte-webapp/src/pages/SourcesPage/pages/CreateSourcePage/components/SourceForm.tsx +++ b/airbyte-webapp/src/pages/SourcesPage/pages/CreateSourcePage/components/SourceForm.tsx @@ -8,7 +8,7 @@ import { useAnalyticsService } from "hooks/services/Analytics"; import useRouter from "hooks/useRouter"; import { SourceDefinitionReadWithLatestTag } from "services/connector/SourceDefinitionService"; import { useGetSourceDefinitionSpecificationAsync } from "services/connector/SourceDefinitionSpecificationService"; -import { createFormErrorMessage } from "utils/errorStatusMessage"; +import { createFormErrorMessage, FormError } from "utils/errorStatusMessage"; import { ConnectorCard } from "views/Connector/ConnectorCard"; import { ServiceFormValues } from "views/Connector/ServiceForm/types"; @@ -22,7 +22,7 @@ interface SourceFormProps { afterSelectConnector?: () => void; sourceDefinitions: SourceDefinitionReadWithLatestTag[]; hasSuccess?: boolean; - error?: { message?: string; status?: number } | null; + error?: FormError | null; } const hasSourceDefinitionId = (state: unknown): state is { sourceDefinitionId: string } => { diff --git a/airbyte-webapp/src/utils/testutils.tsx b/airbyte-webapp/src/utils/testutils.tsx index f817fc673cb5..1f6b4b4c0bfe 100644 --- a/airbyte-webapp/src/utils/testutils.tsx +++ b/airbyte-webapp/src/utils/testutils.tsx @@ -9,6 +9,7 @@ import { ConfigContext, defaultConfig } from "config"; import { ServicesProvider } from "core/servicesProvider"; import { defaultFeatures, FeatureService } from "hooks/services/Feature"; import en from "locales/en.json"; +import { AnalyticsProvider } from "views/common/AnalyticsProvider"; interface WrapperProps { children?: React.ReactElement; @@ -24,15 +25,17 @@ export async function render< return ( - - - - - 'fallback content'
}>{children} - - - - + + + + + + 'fallback content'
}>{children} + + + + + ); diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx index 8eaf1cbb6f7a..fcff62981058 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx @@ -1,13 +1,16 @@ import { Field, FieldProps, Form, Formik } from "formik"; -import React from "react"; +import React, { useCallback } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useToggle } from "react-use"; import styled from "styled-components"; import { Card, ControlLabels, DropDown, H5, Input } from "components"; +import { IDataItem } from "components/base/DropDown/components/Option"; import { FormChangeTracker } from "components/FormChangeTracker"; +import { Action, Namespace } from "core/analytics"; import { ConnectionScheduleDataBasicSchedule, NamespaceDefinitionType } from "core/request/AirbyteClient"; +import { useAnalyticsService } from "hooks/services/Analytics"; import { useConnectionFormService } from "hooks/services/Connection/ConnectionFormService"; import CreateControls from "./components/CreateControls"; @@ -106,8 +109,37 @@ export const ConnectionForm: React.FC = ({ canSubmitUntouchedForm, additionalSchemaControl, }) => { - const { initialValues, formId, mode, onFormSubmit, errorMessage, frequencies, onFrequencySelect, onCancel } = + const { initialValues, formId, mode, onFormSubmit, errorMessage, frequencies, onCancel, connection } = useConnectionFormService(); + const analyticsService = useAnalyticsService(); + + const onFrequencySelect = useCallback( + (item: IDataItem | null) => { + const enabledStreams = connection.syncCatalog.streams.filter((stream) => stream.config?.selected).length; + + if (item) { + analyticsService.track(Namespace.CONNECTION, Action.FREQUENCY, { + actionDescription: "Frequency selected", + frequency: item.label, + connector_source_definition: connection.source.sourceName, + connector_source_definition_id: connection.source.sourceDefinitionId, + connector_destination_definition: connection.destination.destinationName, + connector_destination_definition_id: connection.destination.destinationDefinitionId, + available_streams: connection.syncCatalog.streams.length, + enabled_streams: enabledStreams, + }); + } + }, + [ + analyticsService, + connection.destination.destinationDefinitionId, + connection.destination.destinationName, + connection.source.sourceDefinitionId, + connection.source.sourceName, + connection.syncCatalog.streams, + ] + ); + const [editingTransformation, toggleEditingTransformation] = useToggle(false); const { formatMessage } = useIntl(); @@ -177,7 +209,11 @@ export const ConnectionForm: React.FC = ({ error={!!meta.error && meta.touched} options={frequencies} onChange={(item) => { - onFrequencySelect?.(item); + // This was only passed in for the create view + // Do we want it for all? + if (mode === "create") { + onFrequencySelect(item); + } setFieldValue(field.name, item.value); }} /> From aec7b1064bf17e3838d76aa74219262ace6859b7 Mon Sep 17 00:00:00 2001 From: KC Date: Fri, 2 Sep 2022 14:30:20 -0400 Subject: [PATCH 027/107] Better formSubmit handling for new connection --- .../CreateConnectionContent.tsx | 24 ++++++++++--------- .../Connection/ConnectionFormService.test.tsx | 3 +++ .../Connection/ConnectionFormService.tsx | 18 +++++--------- .../components/ReplicationView.tsx | 3 +-- .../ConnectionForm/ConnectionForm.test.tsx | 7 +++++- .../ConnectionForm/ConnectionForm.tsx | 5 ---- 6 files changed, 29 insertions(+), 31 deletions(-) diff --git a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx index 5d773506a54f..fc9db94d9e5c 100644 --- a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx +++ b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx @@ -1,6 +1,6 @@ import { faRedoAlt } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import React, { Suspense, useCallback, useRef } from "react"; +import React, { Suspense, useCallback } from "react"; import { FormattedMessage } from "react-intl"; import { Button, ContentCard } from "components"; @@ -9,11 +9,12 @@ import LoadingSchema from "components/LoadingSchema"; import { LogsRequestError } from "core/request/LogsRequestError"; import { ConnectionFormServiceProvider } from "hooks/services/Connection/ConnectionFormService"; +import { useFormChangeTrackerService, useUniqueFormId } from "hooks/services/FormChangeTracker"; import { useCreateConnection, ValuesProps } from "hooks/services/useConnectionHook"; import useRouter from "hooks/useRouter"; import { ConnectionForm } from "views/Connection/ConnectionForm"; -import { DestinationRead, SourceRead, WebBackendConnectionRead } from "../../core/request/AirbyteClient"; +import { DestinationRead, SourceRead } from "../../core/request/AirbyteClient"; import { useDiscoverSchema } from "../../hooks/services/useSourceHook"; import TryAfterErrorBlock from "./components/TryAfterErrorBlock"; import styles from "./CreateConnectionContent.module.scss"; @@ -30,9 +31,11 @@ const CreateConnectionContent: React.FC = ({ afterSubmitConnection, }) => { const { mutateAsync: createConnection } = useCreateConnection(); - const newConnection = useRef(null); const { push } = useRouter(); + const formId = useUniqueFormId(); + const { clearFormChange } = useFormChangeTrackerService(); + const { schema, isLoading, schemaErrorStatus, catalogId, onDiscoverSchema } = useDiscoverSchema( source.sourceId, true @@ -61,15 +64,13 @@ const CreateConnectionContent: React.FC = ({ sourceCatalogId: catalogId, }); + clearFormChange(formId); + + push(`../../connections/${createdConnection.connectionId}`); + // We need the new connection ID to know where to go. - newConnection.current = createdConnection; }, - [catalogId, createConnection, destination, source] - ); - - const onAfterSubmit = useCallback( - () => afterSubmitConnection?.() ?? push(`../../connections/${newConnection.current?.connectionId}`), - [afterSubmitConnection, newConnection, push] + [catalogId, clearFormChange, createConnection, destination, formId, push, source] ); if (schemaErrorStatus) { @@ -89,8 +90,9 @@ const CreateConnectionContent: React.FC = ({ { initialProps: { connection: mockConnection as WebBackendConnectionRead, mode: "create", + formId: Math.random().toString(), onSubmit, onAfterSubmit, onCancel, @@ -110,6 +111,7 @@ describe("ConnectionFormService", () => { initialProps: { connection: mockConnection as WebBackendConnectionRead, mode: "create", + formId: Math.random().toString(), onSubmit, onAfterSubmit, onCancel, @@ -136,6 +138,7 @@ describe("ConnectionFormService", () => { initialProps: { connection: mockConnection as WebBackendConnectionRead, mode: "create", + formId: Math.random().toString(), onSubmit, onAfterSubmit, onCancel, diff --git a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx b/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx index 01d54f575f8f..c3bb021736ec 100644 --- a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx +++ b/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx @@ -5,7 +5,7 @@ import { ConnectionScheduleType, WebBackendConnectionRead } from "core/request/A import { useGetDestinationDefinitionSpecification } from "services/connector/DestinationDefinitionSpecificationService"; import { useCurrentWorkspaceId } from "services/workspaces/WorkspacesService"; import { createFormErrorMessage } from "utils/errorStatusMessage"; -import { ConnectionFormMode, ConnectionFormSubmitResult } from "views/Connection/ConnectionForm/ConnectionForm"; +import { ConnectionFormMode } from "views/Connection/ConnectionForm/ConnectionForm"; import { ConnectionFormValues, connectionValidationSchema, @@ -15,8 +15,9 @@ import { useInitialValues, } from "views/Connection/ConnectionForm/formConfig"; -import { useFormChangeTrackerService, useUniqueFormId } from "../FormChangeTracker"; +import { useFormChangeTrackerService } from "../FormChangeTracker"; import { ModalCancel } from "../Modal"; +import { ValuesProps } from "../useConnectionHook"; export type ConnectionOrPartialConnection = | WebBackendConnectionRead @@ -25,8 +26,8 @@ export type ConnectionOrPartialConnection = export interface ConnectionServiceProps { connection: ConnectionOrPartialConnection; mode: ConnectionFormMode; - formId?: string; - onSubmit: (values: ConnectionFormValues) => Promise; + formId: string; + onSubmit: (values: ValuesProps) => Promise; onAfterSubmit?: () => void; onCancel?: () => void; } @@ -35,7 +36,6 @@ const useConnectionForm = ({ connection, mode, formId, onSubmit, onAfterSubmit, const [submitError, setSubmitError] = useState(null); const workspaceId = useCurrentWorkspaceId(); const { clearFormChange } = useFormChangeTrackerService(); - formId = useUniqueFormId(formId); const destDefinition = useGetDestinationDefinitionSpecification(connection.destination.destinationDefinitionId); const initialValues = useInitialValues(connection, destDefinition, mode !== "create"); @@ -58,13 +58,7 @@ const useConnectionForm = ({ connection, mode, formId, onSubmit, onAfterSubmit, await onSubmit(formValues); formikHelpers.resetForm({ values }); - if (formId) { - // formId is never undefined here, but this is safer than `!` - clearFormChange(formId); - } else { - // This should never be hit. - throw new Error("Somehow this form does not have an id"); - } + clearFormChange(formId); onAfterSubmit?.(); } catch (e) { diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx index f1c9290c9153..b4a0944c7b5b 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx @@ -23,7 +23,6 @@ import { import { equal } from "utils/objects"; import { CatalogDiffModal } from "views/Connection/CatalogDiffModal/CatalogDiffModal"; import { ConnectionForm } from "views/Connection/ConnectionForm"; -import { ConnectionFormSubmitResult } from "views/Connection/ConnectionForm/ConnectionForm"; interface ReplicationViewProps { onAfterSaveSchema: () => void; @@ -152,7 +151,7 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch ); const onSubmitForm = useCallback( - async (values: ValuesProps): Promise => { + async (values: ValuesProps): Promise => { // Detect whether the catalog has any differences in its enabled streams compared to the original one. // This could be due to user changes (e.g. in the sync mode) or due to new/removed // streams due to a "refreshed source schema". diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.test.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.test.tsx index a18a1087babf..a2e035d58eb6 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.test.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.test.tsx @@ -75,7 +75,12 @@ jest.mock("services/workspaces/WorkspacesService", () => { const renderConnectionForm = (mode: ConnectionFormMode, connection = mockConnection) => render( - + diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx index fcff62981058..a89654030baa 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx @@ -87,11 +87,6 @@ const FormContainer = styled(Form)` gap: 10px; `; -export interface ConnectionFormSubmitResult { - onSubmitComplete?: () => void; - submitCancelled?: boolean; -} - export type ConnectionFormMode = "create" | "edit" | "readonly"; export interface ConnectionFormProps { From 6aabc3bf04b935fc1b17a1dbc022ebb1a39ad55a Mon Sep 17 00:00:00 2001 From: KC Date: Fri, 2 Sep 2022 16:09:24 -0400 Subject: [PATCH 028/107] Commenting and some cleanup --- .../CreateConnectionContent.tsx | 14 +- .../components/ReplicationView.tsx | 136 ++++++++---------- 2 files changed, 69 insertions(+), 81 deletions(-) diff --git a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx index fc9db94d9e5c..dd28bedc2699 100644 --- a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx +++ b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx @@ -64,13 +64,15 @@ const CreateConnectionContent: React.FC = ({ sourceCatalogId: catalogId, }); - clearFormChange(formId); - - push(`../../connections/${createdConnection.connectionId}`); - - // We need the new connection ID to know where to go. + // We only want to go to the new connection if we _do not_ have an after submit action. + if (!afterSubmitConnection) { + // We have to clear the form change to prevent the dirty-form tracking modal from appearing. + clearFormChange(formId); + // This is the "default behavior", go to the created connection. + push(`../../connections/${createdConnection.connectionId}`); + } }, - [catalogId, clearFormChange, createConnection, destination, formId, push, source] + [afterSubmitConnection, catalogId, clearFormChange, createConnection, destination, formId, push, source] ); if (schemaErrorStatus) { diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx index b4a0944c7b5b..6b2efba9ce19 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx @@ -1,6 +1,6 @@ import { faSyncAlt } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import React, { useCallback, useMemo, useState } from "react"; +import React, { useMemo, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useAsyncFn, useUnmount } from "react-use"; import styled from "styled-components"; @@ -109,81 +109,67 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch const connection = activeUpdatingSchemaMode ? connectionWithRefreshCatalog : initialConnection; - const saveConnection = useCallback( - async (values: ValuesProps, { skipReset }: { skipReset: boolean }) => { - if (!connection) { - // onSubmit should only be called while the catalog isn't currently refreshing at the moment, - // which is the only case when `connection` would be `undefined`. - return; - } - const initialSyncSchema = connection.syncCatalog; - const connectionAsUpdate = toWebBackendConnectionUpdate(connection); - - await updateConnection({ - ...connectionAsUpdate, - ...values, - connectionId, - // Use the name and status from the initial connection because - // The status can be toggled and the name can be changed in-between refreshing the schema - name: initialConnection.name, - status: initialConnection.status || "", - skipReset, - }); - - setSaved(true); - if (!equal(values.syncCatalog, initialSyncSchema)) { - onAfterSaveSchema(); - } + const saveConnection = async (values: ValuesProps, { skipReset }: { skipReset: boolean }) => { + if (!connection) { + // onSubmit should only be called while the catalog isn't currently refreshing at the moment, + // which is the only case when `connection` would be `undefined`. + return; + } + const initialSyncSchema = connection.syncCatalog; + const connectionAsUpdate = toWebBackendConnectionUpdate(connection); - if (activeUpdatingSchemaMode) { - setActiveUpdatingSchemaMode(false); - } - }, - [ - activeUpdatingSchemaMode, - connection, + await updateConnection({ + ...connectionAsUpdate, + ...values, connectionId, - initialConnection.name, - initialConnection.status, - onAfterSaveSchema, - updateConnection, - ] - ); + // Use the name and status from the initial connection because + // The status can be toggled and the name can be changed in-between refreshing the schema + name: initialConnection.name, + status: initialConnection.status || "", + skipReset, + }); + + setSaved(true); + if (!equal(values.syncCatalog, initialSyncSchema)) { + onAfterSaveSchema(); + } - const onSubmitForm = useCallback( - async (values: ValuesProps): Promise => { - // Detect whether the catalog has any differences in its enabled streams compared to the original one. - // This could be due to user changes (e.g. in the sync mode) or due to new/removed - // streams due to a "refreshed source schema". - const hasCatalogChanged = !equal( - values.syncCatalog.streams.filter((s) => s.config?.selected), - initialConnection.syncCatalog.streams.filter((s) => s.config?.selected) - ); - // Whenever the catalog changed show a warning to the user, that we're about to reset their data. - // Given them a choice to opt-out in which case we'll be sending skipRefresh: true to the update - // endpoint. - if (hasCatalogChanged) { - const stateType = await connectionService.getStateType(connectionId); - const result = await openModal({ - title: formatMessage({ id: "connection.resetModalTitle" }), - size: "md", - content: (props) => , - }); - if (result.type === "canceled") { - throw new ModalCancel(); - } - // Save the connection taking into account the correct skipRefresh value from the dialog choice. - await saveConnection(values, { skipReset: !result.reason }); - } else { - // The catalog hasn't changed. We don't need to ask for any confirmation and can simply save. - await saveConnection(values, { skipReset: true }); + if (activeUpdatingSchemaMode) { + setActiveUpdatingSchemaMode(false); + } + }; + + const onSubmitForm = async (values: ValuesProps): Promise => { + // Detect whether the catalog has any differences in its enabled streams compared to the original one. + // This could be due to user changes (e.g. in the sync mode) or due to new/removed + // streams due to a "refreshed source schema". + const hasCatalogChanged = !equal( + values.syncCatalog.streams.filter((s) => s.config?.selected), + initialConnection.syncCatalog.streams.filter((s) => s.config?.selected) + ); + // Whenever the catalog changed show a warning to the user, that we're about to reset their data. + // Given them a choice to opt-out in which case we'll be sending skipRefresh: true to the update + // endpoint. + if (hasCatalogChanged) { + const stateType = await connectionService.getStateType(connectionId); + const result = await openModal({ + title: formatMessage({ id: "connection.resetModalTitle" }), + size: "md", + content: (props) => , + }); + if (result.type === "canceled") { + throw new ModalCancel(); } - }, - [connectionId, connectionService, formatMessage, initialConnection.syncCatalog.streams, openModal, saveConnection] - ); + // Save the connection taking into account the correct skipRefresh value from the dialog choice. + await saveConnection(values, { skipReset: !result.reason }); + } else { + // The catalog hasn't changed. We don't need to ask for any confirmation and can simply save. + await saveConnection(values, { skipReset: true }); + } + }; // TODO: Move this into the service next - const refreshSourceSchema = useCallback(async () => { + const refreshSourceSchema = async () => { setSaved(false); setActiveUpdatingSchemaMode(true); const { catalogDiff, syncCatalog } = await refreshCatalog(); @@ -196,9 +182,9 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch ), }); } - }, [formatMessage, openModal, refreshCatalog]); + }; - const onRefreshSourceSchema = useCallback(async () => { + const onRefreshSourceSchema = async () => { if (formDirty) { // The form is dirty so we show a warning before proceeding. openConfirmationModal({ @@ -214,12 +200,12 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch // The form is not dirty so we can directly refresh the source schema. refreshSourceSchema(); } - }, [closeConfirmationModal, formDirty, openConfirmationModal, refreshSourceSchema]); + }; - const onCancelConnectionFormEdit = useCallback(() => { + const onCancelConnectionFormEdit = () => { setSaved(false); setActiveUpdatingSchemaMode(false); - }, []); + }; return ( From 8af9bd2f43512501025825ced0660f31c02f1b88 Mon Sep 17 00:00:00 2001 From: KC Date: Fri, 2 Sep 2022 16:26:29 -0400 Subject: [PATCH 029/107] Comments! --- .../src/hooks/services/Connection/ConnectionFormService.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx b/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx index c3bb021736ec..8b10641baabf 100644 --- a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx +++ b/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx @@ -55,9 +55,11 @@ const useConnectionForm = ({ connection, mode, formId, onSubmit, onAfterSubmit, setSubmitError(null); try { + // This onSubmit comes from either ReplicationView.tsx (Connection Edit), or CreateConnectionContent.tsx (Connection Create). await onSubmit(formValues); formikHelpers.resetForm({ values }); + // We need to clear the form changes otherwise the dirty form intercept service will prevent navigation clearFormChange(formId); onAfterSubmit?.(); From 208cf7813236bd8ce16f7cfc9c50328acb32e7df Mon Sep 17 00:00:00 2001 From: KC Date: Wed, 7 Sep 2022 14:18:11 -0400 Subject: [PATCH 030/107] Fixing errors from the merge --- .../services/Connection/ConnectionFormService.test.tsx | 2 +- .../services/Connection/ConnectionFormService.tsx | 4 ++-- .../components/SettingsView.test.tsx | 8 +++++--- airbyte-webapp/src/utils/errorStatusMessage.tsx | 6 +++++- .../views/Connection/ConnectionForm/ConnectionForm.tsx | 10 +--------- .../src/views/Connection/ConnectionForm/formConfig.tsx | 1 - 6 files changed, 14 insertions(+), 17 deletions(-) diff --git a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.test.tsx b/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.test.tsx index 5eac40f02612..aa5a3fe09b1e 100644 --- a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.test.tsx +++ b/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.test.tsx @@ -3,9 +3,9 @@ import { act } from "@testing-library/react"; import { renderHook } from "@testing-library/react-hooks"; import React from "react"; import { MemoryRouter } from "react-router-dom"; +import { TestWrapper } from "test-utils/testutils"; import { WebBackendConnectionRead } from "core/request/AirbyteClient"; -import { TestWrapper } from "utils/testutils"; import { ModalCancel } from "../Modal"; import { diff --git a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx b/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx index 8b10641baabf..771f64d7c6b2 100644 --- a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx +++ b/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx @@ -4,7 +4,7 @@ import React, { createContext, useCallback, useContext, useMemo, useState } from import { ConnectionScheduleType, WebBackendConnectionRead } from "core/request/AirbyteClient"; import { useGetDestinationDefinitionSpecification } from "services/connector/DestinationDefinitionSpecificationService"; import { useCurrentWorkspaceId } from "services/workspaces/WorkspacesService"; -import { createFormErrorMessage } from "utils/errorStatusMessage"; +import { generateMessageFromError } from "utils/errorStatusMessage"; import { ConnectionFormMode } from "views/Connection/ConnectionForm/ConnectionForm"; import { ConnectionFormValues, @@ -72,7 +72,7 @@ const useConnectionForm = ({ connection, mode, formId, onSubmit, onAfterSubmit, [connection.operations, workspaceId, onSubmit, clearFormChange, formId, onAfterSubmit] ); - const errorMessage = useMemo(() => (submitError ? createFormErrorMessage(submitError) : null), [submitError]); + const errorMessage = useMemo(() => (submitError ? generateMessageFromError(submitError) : null), [submitError]); const frequencies = useFrequencyDropdownData(connection.scheduleData); return { diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/SettingsView.test.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/SettingsView.test.tsx index 8cf4bf0478fa..b311cdfa68db 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/SettingsView.test.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/SettingsView.test.tsx @@ -17,9 +17,11 @@ jest.mock("hooks/services/useConnectionHook", () => ({ useGetConnectionState: () => ({ state: null, globalState: null, streamState: null }), })); -jest.mock("hooks/services/Analytics/useAnalyticsService", () => ({ - useTrackPage: () => null, -})); +jest.mock("hooks/services/Analytics/useAnalyticsService", () => { + const analyticsService = jest.requireActual("hooks/services/Analytics/useAnalyticsService"); + analyticsService.useTrackPage = () => null; + return analyticsService; +}); // Mocking the DeleteBlock component is a bit ugly, but it's simpler and less // brittle than mocking the providers it depends on; at least it's a direct, diff --git a/airbyte-webapp/src/utils/errorStatusMessage.tsx b/airbyte-webapp/src/utils/errorStatusMessage.tsx index 7afcf81cb3c7..bb6cc8682bed 100644 --- a/airbyte-webapp/src/utils/errorStatusMessage.tsx +++ b/airbyte-webapp/src/utils/errorStatusMessage.tsx @@ -1,6 +1,10 @@ import { FormattedMessage } from "react-intl"; -export const createFormErrorMessage = (error: { status?: number; message?: string }): JSX.Element | string | null => { +export class FormError extends Error { + status?: number; +} + +export const generateMessageFromError = (error: FormError): JSX.Element | string | null => { if (error.message) { return error.message; } diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx index 7c73292a4d05..a89654030baa 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx @@ -9,11 +9,7 @@ import { IDataItem } from "components/base/DropDown/components/Option"; import { FormChangeTracker } from "components/FormChangeTracker"; import { Action, Namespace } from "core/analytics"; -import { - ConnectionScheduleDataBasicSchedule, - NamespaceDefinitionType, - WebBackendConnectionRead, -} from "core/request/AirbyteClient"; +import { ConnectionScheduleDataBasicSchedule, NamespaceDefinitionType } from "core/request/AirbyteClient"; import { useAnalyticsService } from "hooks/services/Analytics"; import { useConnectionFormService } from "hooks/services/Connection/ConnectionFormService"; @@ -93,10 +89,6 @@ const FormContainer = styled(Form)` export type ConnectionFormMode = "create" | "edit" | "readonly"; -export type ConnectionOrPartialConnection = - | WebBackendConnectionRead - | (Partial & Pick); - export interface ConnectionFormProps { className?: string; successMessage?: React.ReactNode; diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/formConfig.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/formConfig.tsx index 1c6ef3bde9b3..8ba9d26b6e27 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/formConfig.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/formConfig.tsx @@ -29,7 +29,6 @@ import { ValuesProps } from "hooks/services/useConnectionHook"; import { useCurrentWorkspace } from "services/workspaces/WorkspacesService"; import calculateInitialCatalog from "./calculateInitialCatalog"; -import { ConnectionOrPartialConnection } from "./ConnectionForm"; export interface FormikConnectionFormValues { name?: string; From c7f9afac04dce3431cd11cff9120dc548df4a7f2 Mon Sep 17 00:00:00 2001 From: KC Date: Wed, 7 Sep 2022 14:45:05 -0400 Subject: [PATCH 031/107] mock data cleanup --- .../Connection/ConnectionFormService.test.tsx | 19 +- .../services/Connection/mockConnection.json | 349 ------------------ .../Connection/mockDestinationDefinition.json | 329 ----------------- .../test-utils/mock-data/mockWorkspace.json | 13 + 4 files changed, 16 insertions(+), 694 deletions(-) delete mode 100644 airbyte-webapp/src/hooks/services/Connection/mockConnection.json delete mode 100644 airbyte-webapp/src/hooks/services/Connection/mockDestinationDefinition.json create mode 100644 airbyte-webapp/src/test-utils/mock-data/mockWorkspace.json diff --git a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.test.tsx b/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.test.tsx index aa5a3fe09b1e..03d873ee0be0 100644 --- a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.test.tsx +++ b/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.test.tsx @@ -3,6 +3,9 @@ import { act } from "@testing-library/react"; import { renderHook } from "@testing-library/react-hooks"; import React from "react"; import { MemoryRouter } from "react-router-dom"; +import mockConnection from "test-utils/mock-data/mockConnection.json"; +import mockDest from "test-utils/mock-data/mockDestinationDefinition.json"; +import mockWorkspace from "test-utils/mock-data/mockWorkspace.json"; import { TestWrapper } from "test-utils/testutils"; import { WebBackendConnectionRead } from "core/request/AirbyteClient"; @@ -13,22 +16,6 @@ import { ConnectionServiceProps, useConnectionFormService, } from "./ConnectionFormService"; -import mockConnection from "./mockConnection.json"; -import mockDest from "./mockDestinationDefinition.json"; - -const mockWorkspace = { - workspaceId: "47c74b9b-9b89-4af1-8331-4865af6c4e4d", - customerId: "55dd55e2-33ac-44dc-8d65-5aa7c8624f72", - email: "krishna@airbyte.com", - name: "47c74b9b-9b89-4af1-8331-4865af6c4e4d", - slug: "47c74b9b-9b89-4af1-8331-4865af6c4e4d", - initialSetupComplete: true, - displaySetupWizard: false, - anonymousDataCollection: false, - news: false, - securityUpdates: false, - notifications: [], -}; ["packages/cloud/services/workspaces/WorkspacesService", "services/workspaces/WorkspacesService"].forEach((s) => jest.mock(s, () => ({ diff --git a/airbyte-webapp/src/hooks/services/Connection/mockConnection.json b/airbyte-webapp/src/hooks/services/Connection/mockConnection.json deleted file mode 100644 index bf44cc7251fb..000000000000 --- a/airbyte-webapp/src/hooks/services/Connection/mockConnection.json +++ /dev/null @@ -1,349 +0,0 @@ -{ - "connectionId": "a9c8e4b5-349d-4a17-bdff-5ad2f6fbd611", - "name": "Scrafty <> Heroku Postgres", - "namespaceDefinition": "source", - "namespaceFormat": "${SOURCE_NAMESPACE}", - "prefix": "", - "sourceId": "a3295ed7-4acf-4c0b-b16b-07a00e624a52", - "destinationId": "083a53bc-8bc2-4dc0-b05a-4273a96f3b93", - "syncCatalog": { - "streams": [ - { - "stream": { - "name": "pokemon", - "jsonSchema": { - "type": "object", - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "id": { - "type": ["null", "integer"] - }, - "name": { - "type": ["null", "string"] - }, - "forms": { - "type": ["null", "array"], - "items": { - "type": ["null", "object"], - "properties": { - "url": { - "type": ["null", "string"] - }, - "name": { - "type": ["null", "string"] - } - } - } - }, - "moves": { - "type": ["null", "array"], - "items": { - "type": ["null", "object"], - "properties": { - "move": { - "type": ["null", "object"], - "properties": { - "url": { - "type": ["null", "string"] - }, - "name": { - "type": ["null", "string"] - } - } - }, - "version_group_details": { - "type": ["null", "array"], - "items": { - "type": ["null", "object"], - "properties": { - "version_group": { - "type": ["null", "object"], - "properties": { - "url": { - "type": ["null", "string"] - }, - "name": { - "type": ["null", "string"] - } - } - }, - "level_learned_at": { - "type": ["null", "integer"] - }, - "move_learn_method": { - "type": ["null", "object"], - "properties": { - "url": { - "type": ["null", "string"] - }, - "name": { - "type": ["null", "string"] - } - } - } - } - } - } - } - } - }, - "order": { - "type": ["null", "integer"] - }, - "stats": { - "type": ["null", "array"], - "items": { - "type": ["null", "object"], - "properties": { - "stat": { - "type": ["null", "object"], - "properties": { - "url": { - "type": ["null", "string"] - }, - "name": { - "type": ["null", "string"] - } - } - }, - "effort": { - "type": ["null", "integer"] - }, - "base_stat": { - "type": ["null", "integer"] - } - } - } - }, - "types": { - "type": ["null", "array"], - "items": { - "type": ["null", "object"], - "properties": { - "slot": { - "type": ["null", "integer"] - }, - "type": { - "type": ["null", "object"], - "properties": { - "url": { - "type": ["null", "string"] - }, - "name": { - "type": ["null", "string"] - } - } - } - } - } - }, - "height": { - "type": ["null", "integer"] - }, - "weight": { - "type": ["null", "integer"] - }, - "species": { - "type": ["null", "object"], - "properties": { - "url": { - "type": ["null", "string"] - }, - "name": { - "type": ["null", "string"] - } - } - }, - "sprites": { - "type": ["null", "object"], - "properties": { - "back_shiny": { - "type": ["null", "string"] - }, - "back_female": { - "type": ["null", "string"] - }, - "front_shiny": { - "type": ["null", "string"] - }, - "back_default": { - "type": ["null", "string"] - }, - "front_female": { - "type": ["null", "string"] - }, - "front_default": { - "type": ["null", "string"] - }, - "back_shiny_female": { - "type": ["null", "string"] - }, - "front_shiny_female": { - "type": ["null", "string"] - } - } - }, - "abilities": { - "type": ["null", "array"], - "items": { - "type": ["null", "object"], - "properties": { - "slot": { - "type": ["null", "integer"] - }, - "ability": { - "type": ["null", "object"], - "properties": { - "url": { - "type": ["null", "string"] - }, - "name": { - "type": ["null", "string"] - } - } - }, - "is_hidden": { - "type": ["null", "boolean"] - } - } - } - }, - "held_items": { - "type": ["null", "array"], - "items": { - "type": ["null", "object"], - "properties": { - "item": { - "type": ["null", "object"], - "properties": { - "url": { - "type": ["null", "string"] - }, - "name": { - "type": ["null", "string"] - } - } - }, - "version_details": { - "type": ["null", "array"], - "items": { - "type": ["null", "object"], - "properties": { - "rarity": { - "type": ["null", "integer"] - }, - "version": { - "type": ["null", "object"], - "properties": { - "url": { - "type": ["null", "string"] - }, - "name": { - "type": ["null", "string"] - } - } - } - } - } - } - } - } - }, - "is_default ": { - "type": ["null", "boolean"] - }, - "game_indices": { - "type": ["null", "array"], - "items": { - "type": ["null", "object"], - "properties": { - "version": { - "type": ["null", "object"], - "properties": { - "url": { - "type": ["null", "string"] - }, - "name": { - "type": ["null", "string"] - } - } - }, - "game_index": { - "type": ["null", "integer"] - } - } - } - }, - "base_experience": { - "type": ["null", "integer"] - }, - "location_area_encounters": { - "type": ["null", "string"] - } - } - }, - "supportedSyncModes": ["full_refresh"], - "defaultCursorField": [], - "sourceDefinedPrimaryKey": [] - }, - "config": { - "syncMode": "full_refresh", - "cursorField": [], - "destinationSyncMode": "append", - "primaryKey": [], - "aliasName": "pokemon", - "selected": true - } - } - ] - }, - "scheduleType": "manual", - "status": "active", - "operationIds": ["8af8ef4d-01b1-49c8-b145-23775f34a74b"], - "source": { - "sourceDefinitionId": "6371b14b-bc68-4236-bfbd-468e8df8e968", - "sourceId": "a3295ed7-4acf-4c0b-b16b-07a00e624a52", - "workspaceId": "47c74b9b-9b89-4af1-8331-4865af6c4e4d", - "connectionConfiguration": { - "pokemon_name": "scrafty" - }, - "name": "Scrafty", - "sourceName": "PokeAPI" - }, - "destination": { - "destinationDefinitionId": "25c5221d-dce2-4163-ade9-739ef790f503", - "destinationId": "083a53bc-8bc2-4dc0-b05a-4273a96f3b93", - "workspaceId": "47c74b9b-9b89-4af1-8331-4865af6c4e4d", - "connectionConfiguration": { - "ssl": false, - "host": "asdf", - "port": 5432, - "schema": "public", - "database": "asdf", - "password": "**********", - "username": "asdf", - "tunnel_method": { - "tunnel_method": "NO_TUNNEL" - } - }, - "name": "Heroku Postgres", - "destinationName": "Postgres" - }, - "operations": [ - { - "workspaceId": "47c74b9b-9b89-4af1-8331-4865af6c4e4d", - "operationId": "8af8ef4d-01b1-49c8-b145-23775f34a74b", - "name": "Normalization", - "operatorConfiguration": { - "operatorType": "normalization", - "normalization": { - "option": "basic" - } - } - } - ], - "latestSyncJobCreatedAt": 1660227512, - "latestSyncJobStatus": "succeeded", - "isSyncing": false, - "catalogId": "bf31d1df-d7ba-4bae-b1ec-dac617b4f70c" -} diff --git a/airbyte-webapp/src/hooks/services/Connection/mockDestinationDefinition.json b/airbyte-webapp/src/hooks/services/Connection/mockDestinationDefinition.json deleted file mode 100644 index 40a038b7bc6a..000000000000 --- a/airbyte-webapp/src/hooks/services/Connection/mockDestinationDefinition.json +++ /dev/null @@ -1,329 +0,0 @@ -{ - "destinationDefinitionId": "25c5221d-dce2-4163-ade9-739ef790f503", - "documentationUrl": "https://docs.airbyte.io/integrations/destinations/postgres", - "connectionSpecification": { - "type": "object", - "title": "Postgres Destination Spec", - "$schema": "http://json-schema.org/draft-07/schema#", - "required": ["host", "port", "username", "database", "schema"], - "properties": { - "ssl": { - "type": "boolean", - "order": 6, - "title": "SSL Connection", - "default": false, - "description": "Encrypt data using SSL. When activating SSL, please select one of the connection modes." - }, - "host": { - "type": "string", - "order": 0, - "title": "Host", - "description": "Hostname of the database." - }, - "port": { - "type": "integer", - "order": 1, - "title": "Port", - "default": 5432, - "maximum": 65536, - "minimum": 0, - "examples": ["5432"], - "description": "Port of the database." - }, - "schema": { - "type": "string", - "order": 3, - "title": "Default Schema", - "default": "public", - "examples": ["public"], - "description": "The default schema tables are written to if the source does not specify a namespace. The usual value for this field is \"public\"." - }, - "database": { - "type": "string", - "order": 2, - "title": "DB Name", - "description": "Name of the database." - }, - "password": { - "type": "string", - "order": 5, - "title": "Password", - "description": "Password associated with the username.", - "airbyte_secret": true - }, - "ssl_mode": { - "type": "object", - "oneOf": [ - { - "title": "disable", - "required": ["mode"], - "properties": { - "mode": { - "enum": ["disable"], - "type": "string", - "const": "disable", - "order": 0, - "default": "disable" - } - }, - "description": "Disable SSL.", - "additionalProperties": false - }, - { - "title": "allow", - "required": ["mode"], - "properties": { - "mode": { - "enum": ["allow"], - "type": "string", - "const": "allow", - "order": 0, - "default": "allow" - } - }, - "description": "Allow SSL mode.", - "additionalProperties": false - }, - { - "title": "prefer", - "required": ["mode"], - "properties": { - "mode": { - "enum": ["prefer"], - "type": "string", - "const": "prefer", - "order": 0, - "default": "prefer" - } - }, - "description": "Prefer SSL mode.", - "additionalProperties": false - }, - { - "title": "require", - "required": ["mode"], - "properties": { - "mode": { - "enum": ["require"], - "type": "string", - "const": "require", - "order": 0, - "default": "require" - } - }, - "description": "Require SSL mode.", - "additionalProperties": false - }, - { - "title": "verify-ca", - "required": ["mode", "ca_certificate"], - "properties": { - "mode": { - "enum": ["verify-ca"], - "type": "string", - "const": "verify-ca", - "order": 0, - "default": "verify-ca" - }, - "ca_certificate": { - "type": "string", - "order": 1, - "title": "CA certificate", - "multiline": true, - "description": "CA certificate", - "airbyte_secret": true - }, - "client_key_password": { - "type": "string", - "order": 4, - "title": "Client key password (Optional)", - "description": "Password for keystorage. This field is optional. If you do not add it - the password will be generated automatically.", - "airbyte_secret": true - } - }, - "description": "Verify-ca SSL mode.", - "additionalProperties": false - }, - { - "title": "verify-full", - "required": ["mode", "ca_certificate", "client_certificate", "client_key"], - "properties": { - "mode": { - "enum": ["verify-full"], - "type": "string", - "const": "verify-full", - "order": 0, - "default": "verify-full" - }, - "client_key": { - "type": "string", - "order": 3, - "title": "Client key", - "multiline": true, - "description": "Client key", - "airbyte_secret": true - }, - "ca_certificate": { - "type": "string", - "order": 1, - "title": "CA certificate", - "multiline": true, - "description": "CA certificate", - "airbyte_secret": true - }, - "client_certificate": { - "type": "string", - "order": 2, - "title": "Client certificate", - "multiline": true, - "description": "Client certificate", - "airbyte_secret": true - }, - "client_key_password": { - "type": "string", - "order": 4, - "title": "Client key password (Optional)", - "description": "Password for keystorage. This field is optional. If you do not add it - the password will be generated automatically.", - "airbyte_secret": true - } - }, - "description": "Verify-full SSL mode.", - "additionalProperties": false - } - ], - "order": 7, - "title": "SSL modes", - "description": "SSL connection modes. \n disable - Chose this mode to disable encryption of communication between Airbyte and destination database\n allow - Chose this mode to enable encryption only when required by the source database\n prefer - Chose this mode to allow unencrypted connection only if the source database does not support encryption\n require - Chose this mode to always require encryption. If the source database server does not support encryption, connection will fail\n verify-ca - Chose this mode to always require encryption and to verify that the source database server has a valid SSL certificate\n verify-full - This is the most secure mode. Chose this mode to always require encryption and to verify the identity of the source database server\n See more information - in the docs." - }, - "username": { - "type": "string", - "order": 4, - "title": "User", - "description": "Username to use to access the database." - }, - "tunnel_method": { - "type": "object", - "oneOf": [ - { - "title": "No Tunnel", - "required": ["tunnel_method"], - "properties": { - "tunnel_method": { - "type": "string", - "const": "NO_TUNNEL", - "order": 0, - "description": "No ssh tunnel needed to connect to database" - } - } - }, - { - "title": "SSH Key Authentication", - "required": ["tunnel_method", "tunnel_host", "tunnel_port", "tunnel_user", "ssh_key"], - "properties": { - "ssh_key": { - "type": "string", - "order": 4, - "title": "SSH Private Key", - "multiline": true, - "description": "OS-level user account ssh key credentials in RSA PEM format ( created with ssh-keygen -t rsa -m PEM -f myuser_rsa )", - "airbyte_secret": true - }, - "tunnel_host": { - "type": "string", - "order": 1, - "title": "SSH Tunnel Jump Server Host", - "description": "Hostname of the jump server host that allows inbound ssh tunnel." - }, - "tunnel_port": { - "type": "integer", - "order": 2, - "title": "SSH Connection Port", - "default": 22, - "maximum": 65536, - "minimum": 0, - "examples": ["22"], - "description": "Port on the proxy/jump server that accepts inbound ssh connections." - }, - "tunnel_user": { - "type": "string", - "order": 3, - "title": "SSH Login Username", - "description": "OS-level username for logging into the jump server host." - }, - "tunnel_method": { - "type": "string", - "const": "SSH_KEY_AUTH", - "order": 0, - "description": "Connect through a jump server tunnel host using username and ssh key" - } - } - }, - { - "title": "Password Authentication", - "required": ["tunnel_method", "tunnel_host", "tunnel_port", "tunnel_user", "tunnel_user_password"], - "properties": { - "tunnel_host": { - "type": "string", - "order": 1, - "title": "SSH Tunnel Jump Server Host", - "description": "Hostname of the jump server host that allows inbound ssh tunnel." - }, - "tunnel_port": { - "type": "integer", - "order": 2, - "title": "SSH Connection Port", - "default": 22, - "maximum": 65536, - "minimum": 0, - "examples": ["22"], - "description": "Port on the proxy/jump server that accepts inbound ssh connections." - }, - "tunnel_user": { - "type": "string", - "order": 3, - "title": "SSH Login Username", - "description": "OS-level username for logging into the jump server host" - }, - "tunnel_method": { - "type": "string", - "const": "SSH_PASSWORD_AUTH", - "order": 0, - "description": "Connect through a jump server tunnel host using username and password authentication" - }, - "tunnel_user_password": { - "type": "string", - "order": 4, - "title": "Password", - "description": "OS-level password for logging into the jump server host", - "airbyte_secret": true - } - } - } - ], - "title": "SSH Tunnel Method", - "description": "Whether to initiate an SSH tunnel before connecting to the database, and if so, which kind of authentication to use." - }, - "jdbc_url_params": { - "type": "string", - "order": 8, - "title": "JDBC URL Params", - "description": "Additional properties to pass to the JDBC URL string when connecting to the database formatted as 'key=value' pairs separated by the symbol '&'. (example: key1=value1&key2=value2&key3=value3)." - } - }, - "additionalProperties": true - }, - "jobInfo": { - "id": "7c3ae799-cb25-4c05-9685-f7bb1885d662", - "configType": "get_spec", - "configId": "Optional.empty", - "createdAt": 1661365436880, - "endedAt": 1661365436880, - "succeeded": true, - "logs": { - "logLines": [] - } - }, - "supportedDestinationSyncModes": ["overwrite", "append", "append_dedup"], - "supportsDbt": true, - "supportsNormalization": true -} diff --git a/airbyte-webapp/src/test-utils/mock-data/mockWorkspace.json b/airbyte-webapp/src/test-utils/mock-data/mockWorkspace.json new file mode 100644 index 000000000000..07ea47bdadb8 --- /dev/null +++ b/airbyte-webapp/src/test-utils/mock-data/mockWorkspace.json @@ -0,0 +1,13 @@ +{ + "workspaceId": "47c74b9b-9b89-4af1-8331-4865af6c4e4d", + "customerId": "55dd55e2-33ac-44dc-8d65-5aa7c8624f72", + "email": "krishna@airbyte.com", + "name": "47c74b9b-9b89-4af1-8331-4865af6c4e4d", + "slug": "47c74b9b-9b89-4af1-8331-4865af6c4e4d", + "initialSetupComplete": true, + "displaySetupWizard": false, + "anonymousDataCollection": false, + "news": false, + "securityUpdates": false, + "notifications": [] +} From cc91839f9f1673223f07436d470c5c13e1aa29b3 Mon Sep 17 00:00:00 2001 From: KC Date: Wed, 7 Sep 2022 14:52:36 -0400 Subject: [PATCH 032/107] Better TODO --- .../src/hooks/services/Connection/ConnectionFormService.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx b/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx index 771f64d7c6b2..294b138203bc 100644 --- a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx +++ b/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx @@ -47,9 +47,11 @@ const useConnectionForm = ({ connection, mode, formId, onSubmit, onAfterSubmit, ? ConnectionScheduleType.basic : ConnectionScheduleType.manual; + // TODO: We should align these types + // With the PATCH-style endpoint available we might be able to forego this pattern const formValues: ConnectionFormValues = connectionValidationSchema.cast(values, { context: { isRequest: true }, - }) as unknown as ConnectionFormValues; // TODO: We should align these types + }) as unknown as ConnectionFormValues; formValues.operations = mapFormPropsToOperation(values, connection.operations, workspaceId); From 955848c11b70c32f461c206cafc5b63db00a52d6 Mon Sep 17 00:00:00 2001 From: KC Date: Tue, 13 Sep 2022 15:42:22 -0400 Subject: [PATCH 033/107] onFrequencySelect is now always called --- .../views/Connection/ConnectionForm/ConnectionForm.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx index a89654030baa..1b75e909b625 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx @@ -122,6 +122,7 @@ export const ConnectionForm: React.FC = ({ connector_destination_definition_id: connection.destination.destinationDefinitionId, available_streams: connection.syncCatalog.streams.length, enabled_streams: enabledStreams, + type: mode, }); } }, @@ -132,6 +133,7 @@ export const ConnectionForm: React.FC = ({ connection.source.sourceDefinitionId, connection.source.sourceName, connection.syncCatalog.streams, + mode, ] ); @@ -204,11 +206,7 @@ export const ConnectionForm: React.FC = ({ error={!!meta.error && meta.touched} options={frequencies} onChange={(item) => { - // This was only passed in for the create view - // Do we want it for all? - if (mode === "create") { - onFrequencySelect(item); - } + onFrequencySelect(item); setFieldValue(field.name, item.value); }} /> From a0beff775f5ee6e871944f930a46899467b2d172 Mon Sep 17 00:00:00 2001 From: KC Date: Tue, 13 Sep 2022 15:42:30 -0400 Subject: [PATCH 034/107] Edmundo CR --- .../pages/ConnectionItemPage/components/ReplicationView.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx index 09d8b574c179..4d1962217575 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx @@ -102,8 +102,9 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch ); const formId = useUniqueFormId(); - const changedForms = useChangedFormsById(); - const formDirty = useMemo(() => !!changedForms?.[0]?.[formId], [changedForms, formId]); + + const [changedFormsById] = useChangedFormsById(); + const formDirty = useMemo(() => !!changedFormsById?.[formId], [changedFormsById, formId]); useUnmount(() => { closeModal(); From 7f4916af7aa2327c57251f3554d460a11d6c9392 Mon Sep 17 00:00:00 2001 From: KC Date: Wed, 14 Sep 2022 11:27:20 -0400 Subject: [PATCH 035/107] Remove whitespace --- .../pages/ConnectionItemPage/components/ReplicationView.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx index 4d1962217575..432dbb2418dd 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx @@ -153,7 +153,6 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch .sort(naturalComparatorBy((syncStream) => syncStream.stream?.name ?? "")), initialConnection.syncCatalog.streams .filter((s) => s.config?.selected) - .sort(naturalComparatorBy((syncStream) => syncStream.stream?.name ?? "")) ); // Whenever the catalog changed show a warning to the user, that we're about to reset their data. From 19f051c9cc5f3d71358d8950fa6259643e949dfd Mon Sep 17 00:00:00 2001 From: KC Date: Wed, 14 Sep 2022 13:00:15 -0400 Subject: [PATCH 036/107] Move connection into state so it gets updated --- .../src/hooks/services/useConnectionHook.tsx | 10 +++++---- .../components/ReplicationView.tsx | 21 ++++++++++--------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/airbyte-webapp/src/hooks/services/useConnectionHook.tsx b/airbyte-webapp/src/hooks/services/useConnectionHook.tsx index 55733f46d67c..1442dc31f21f 100644 --- a/airbyte-webapp/src/hooks/services/useConnectionHook.tsx +++ b/airbyte-webapp/src/hooks/services/useConnectionHook.tsx @@ -1,4 +1,4 @@ -import { useCallback } from "react"; +import { useCallback, useState } from "react"; import { QueryClient, useMutation, useQueryClient } from "react-query"; import { getFrequencyType } from "config/utils"; @@ -77,12 +77,14 @@ export const useConnectionLoad = ( connectionId: string ): { connection: WebBackendConnectionRead; - refreshConnectionCatalog: () => Promise; + refreshConnectionCatalog: () => Promise; } => { - const connection = useGetConnection(connectionId); + const [connection, setConnection] = useState(useGetConnection(connectionId)); const connectionService = useWebConnectionService(); - const refreshConnectionCatalog = async () => await connectionService.getConnection(connectionId, true); + const refreshConnectionCatalog = async () => { + setConnection(await connectionService.getConnection(connectionId, true)); + }; return { connection, diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx index 432dbb2418dd..66fbd1170a76 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx @@ -1,6 +1,6 @@ import { faSyncAlt } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import React, { useMemo, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useAsyncFn, useUnmount } from "react-use"; import styled from "styled-components"; @@ -96,10 +96,7 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch const { connection: initialConnection, refreshConnectionCatalog } = useConnectionLoad(connectionId); - const [{ value: connectionWithRefreshCatalog, loading: isRefreshingCatalog }, refreshCatalog] = useAsyncFn( - refreshConnectionCatalog, - [connectionId] - ); + const [{ loading: isRefreshingCatalog }, refreshCatalog] = useAsyncFn(refreshConnectionCatalog, [connectionId]); const formId = useUniqueFormId(); @@ -111,7 +108,7 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch closeConfirmationModal(); }); - const connection = activeUpdatingSchemaMode ? connectionWithRefreshCatalog : initialConnection; + const connection = initialConnection; const saveConnection = async (values: ValuesProps, { skipReset }: { skipReset: boolean }) => { if (!connection) { @@ -177,12 +174,16 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch }; // TODO: Move this into the service next - const refreshSourceSchema = async () => { + const refreshSourceSchema = useCallback(async () => { setSaved(false); setActiveUpdatingSchemaMode(true); - const { catalogDiff, syncCatalog } = await refreshCatalog(); + await refreshCatalog(); + }, [refreshCatalog]); + + useEffect(() => { + const { catalogDiff, syncCatalog } = connection; if (catalogDiff?.transforms && catalogDiff.transforms.length > 0) { - await openModal({ + openModal({ title: formatMessage({ id: "connection.updateSchema.completed" }), preventCancel: true, content: ({ onClose }) => ( @@ -190,7 +191,7 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch ), }); } - }; + }, [connection, formatMessage, openModal]); const onRefreshSourceSchema = async () => { if (formDirty) { From cc5b939ce776f6f203f0e65796d003cc11085ea7 Mon Sep 17 00:00:00 2001 From: KC Date: Wed, 14 Sep 2022 13:43:14 -0400 Subject: [PATCH 037/107] Consolidate on connection object --- .../components/ReplicationView.tsx | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx index 66fbd1170a76..532aee71ef98 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx @@ -94,8 +94,7 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch const { mutateAsync: updateConnection } = useUpdateConnection(); - const { connection: initialConnection, refreshConnectionCatalog } = useConnectionLoad(connectionId); - + const { connection, refreshConnectionCatalog } = useConnectionLoad(connectionId); const [{ loading: isRefreshingCatalog }, refreshCatalog] = useAsyncFn(refreshConnectionCatalog, [connectionId]); const formId = useUniqueFormId(); @@ -108,12 +107,8 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch closeConfirmationModal(); }); - const connection = initialConnection; - const saveConnection = async (values: ValuesProps, { skipReset }: { skipReset: boolean }) => { - if (!connection) { - // onSubmit should only be called while the catalog isn't currently refreshing at the moment, - // which is the only case when `connection` would be `undefined`. + if (isRefreshingCatalog) { return; } const initialSyncSchema = connection.syncCatalog; @@ -123,10 +118,6 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch ...connectionAsUpdate, ...values, connectionId, - // Use the name and status from the initial connection because - // The status can be toggled and the name can be changed in-between refreshing the schema - name: initialConnection.name, - status: initialConnection.status || "", skipReset, }); @@ -148,7 +139,7 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch values.syncCatalog.streams .filter((s) => s.config?.selected) .sort(naturalComparatorBy((syncStream) => syncStream.stream?.name ?? "")), - initialConnection.syncCatalog.streams + connection.syncCatalog.streams .filter((s) => s.config?.selected) .sort(naturalComparatorBy((syncStream) => syncStream.stream?.name ?? "")) ); From e0a28114d1fd4588bf42e0481ddd96519376fb71 Mon Sep 17 00:00:00 2001 From: KC Date: Wed, 14 Sep 2022 15:46:48 -0400 Subject: [PATCH 038/107] Remove ModalCancel throw + form clearing in create submit function --- .../CreateConnectionContent.tsx | 41 ++++++++++++------- .../Connection/ConnectionFormService.tsx | 32 +++++++++------ .../components/ReplicationView.tsx | 13 ++++-- 3 files changed, 55 insertions(+), 31 deletions(-) diff --git a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx index 87349726b721..e1b85420af85 100644 --- a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx +++ b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx @@ -8,13 +8,17 @@ import { JobItem } from "components/JobItem/JobItem"; import LoadingSchema from "components/LoadingSchema"; import { LogsRequestError } from "core/request/LogsRequestError"; -import { ConnectionFormServiceProvider } from "hooks/services/Connection/ConnectionFormService"; -import { useFormChangeTrackerService, useUniqueFormId } from "hooks/services/FormChangeTracker"; +import { + ConnectionFormServiceProvider, + isSubmitCancel, + SubmitResult, +} from "hooks/services/Connection/ConnectionFormService"; +import { useUniqueFormId } from "hooks/services/FormChangeTracker"; import { useCreateConnection, ValuesProps } from "hooks/services/useConnectionHook"; import useRouter from "hooks/useRouter"; import { ConnectionForm } from "views/Connection/ConnectionForm"; -import { DestinationRead, SourceRead } from "../../core/request/AirbyteClient"; +import { DestinationRead, SourceRead, WebBackendConnectionRead } from "../../core/request/AirbyteClient"; import { useDiscoverSchema } from "../../hooks/services/useSourceHook"; import TryAfterErrorBlock from "./components/TryAfterErrorBlock"; import styles from "./CreateConnectionContent.module.scss"; @@ -22,7 +26,7 @@ import styles from "./CreateConnectionContent.module.scss"; interface CreateConnectionContentProps { source: SourceRead; destination: DestinationRead; - afterSubmitConnection?: () => void; + afterSubmitConnection?: (connection: WebBackendConnectionRead) => void; } const CreateConnectionContent: React.FC = ({ @@ -34,7 +38,7 @@ const CreateConnectionContent: React.FC = ({ const { push } = useRouter(); const formId = useUniqueFormId(); - const { clearFormChange } = useFormChangeTrackerService(); + // const { clearFormChange } = useFormChangeTrackerService(); const { schema, isLoading, schemaErrorStatus, catalogId, onDiscoverSchema } = useDiscoverSchema( source.sourceId, @@ -50,7 +54,7 @@ const CreateConnectionContent: React.FC = ({ const onSubmitConnectionStep = useCallback( async (values: ValuesProps) => { - const createdConnection = await createConnection({ + return await createConnection({ values, source, destination, @@ -64,15 +68,24 @@ const CreateConnectionContent: React.FC = ({ sourceCatalogId: catalogId, }); - // We only want to go to the new connection if we _do not_ have an after submit action. - if (!afterSubmitConnection) { - // We have to clear the form change to prevent the dirty-form tracking modal from appearing. - clearFormChange(formId); - // This is the "default behavior", go to the created connection. - push(`../../connections/${createdConnection.connectionId}`); + // // We only want to go to the new connection if we _do not_ have an after submit action. + // if (!afterSubmitConnection) { + // // We have to clear the form change to prevent the dirty-form tracking modal from appearing. + // clearFormChange(formId); + // // This is the "default behavior", go to the created connection. + // push(`../../connections/${createdConnection.connectionId}`); + // } + }, + [catalogId, createConnection, destination, source] + ); + + const afterSubmit = useCallback( + (submitResult: SubmitResult) => { + if (!isSubmitCancel(submitResult)) { + afterSubmitConnection?.(submitResult) ?? push(`../../connections/${submitResult.connectionId}`); } }, - [afterSubmitConnection, catalogId, clearFormChange, createConnection, destination, formId, push, source] + [afterSubmitConnection, push] ); if (schemaErrorStatus) { @@ -94,7 +107,7 @@ const CreateConnectionContent: React.FC = ({ mode="create" formId={formId} onSubmit={onSubmitConnectionStep} - onAfterSubmit={afterSubmitConnection} + onAfterSubmit={afterSubmit} > & Pick); +export interface SubmitCancel { + submitCancel: boolean; +} +export type SubmitResult = WebBackendConnectionRead | SubmitCancel; + export interface ConnectionServiceProps { connection: ConnectionOrPartialConnection; mode: ConnectionFormMode; formId: string; - onSubmit: (values: ValuesProps) => Promise; - onAfterSubmit?: () => void; + onSubmit: (values: ValuesProps) => Promise; + onAfterSubmit?: (submitResult: SubmitResult) => void; onCancel?: () => void; } +export function isSubmitCancel(submitResult: SubmitResult): submitResult is { submitCancel: boolean } { + return submitResult.hasOwnProperty("submitCancel"); +} + const useConnectionForm = ({ connection, mode, formId, onSubmit, onAfterSubmit, onCancel }: ConnectionServiceProps) => { const [submitError, setSubmitError] = useState(null); const workspaceId = useCurrentWorkspaceId(); @@ -58,17 +66,15 @@ const useConnectionForm = ({ connection, mode, formId, onSubmit, onAfterSubmit, setSubmitError(null); try { // This onSubmit comes from either ReplicationView.tsx (Connection Edit), or CreateConnectionContent.tsx (Connection Create). - await onSubmit(formValues); - - formikHelpers.resetForm({ values }); - // We need to clear the form changes otherwise the dirty form intercept service will prevent navigation - clearFormChange(formId); - - onAfterSubmit?.(); - } catch (e) { - if (!(e instanceof ModalCancel)) { - setSubmitError(e); + const submitResult = await onSubmit(formValues); + if ((isSubmitCancel(submitResult) && !submitResult.submitCancel) || !isSubmitCancel(submitResult)) { + formikHelpers.resetForm({ values }); + // We need to clear the form changes otherwise the dirty form intercept service will prevent navigation + clearFormChange(formId); + onAfterSubmit?.(submitResult); } + } catch (e) { + setSubmitError(e); } }, [connection.operations, workspaceId, onSubmit, clearFormChange, formId, onAfterSubmit] diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx index 532aee71ef98..ceab3de68582 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx @@ -12,9 +12,9 @@ import { toWebBackendConnectionUpdate } from "core/domain/connection"; import { ConnectionStateType, ConnectionStatus } from "core/request/AirbyteClient"; import { PageTrackingCodes, useTrackPage } from "hooks/services/Analytics"; import { useConfirmationModalService } from "hooks/services/ConfirmationModal"; -import { ConnectionFormServiceProvider } from "hooks/services/Connection/ConnectionFormService"; +import { ConnectionFormServiceProvider, SubmitCancel } from "hooks/services/Connection/ConnectionFormService"; import { useChangedFormsById, useUniqueFormId } from "hooks/services/FormChangeTracker"; -import { ModalCancel, useModalService } from "hooks/services/Modal"; +import { useModalService } from "hooks/services/Modal"; import { useConnectionLoad, useConnectionService, @@ -131,7 +131,7 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch } }; - const onSubmitForm = async (values: ValuesProps): Promise => { + const onSubmitForm = async (values: ValuesProps): Promise => { // Detect whether the catalog has any differences in its enabled streams compared to the original one. // This could be due to user changes (e.g. in the sync mode) or due to new/removed // streams due to a "refreshed source schema". @@ -154,7 +154,9 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch content: (props) => , }); if (result.type === "canceled") { - throw new ModalCancel(); + return { + submitCancel: true, + }; } // Save the connection taking into account the correct skipRefresh value from the dialog choice. await saveConnection(values, { skipReset: !result.reason }); @@ -162,6 +164,9 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch // The catalog hasn't changed. We don't need to ask for any confirmation and can simply save. await saveConnection(values, { skipReset: true }); } + return { + submitCancel: false, + }; }; // TODO: Move this into the service next From 30d81f560961a1afe9d85da4f8ec03fa0af1969c Mon Sep 17 00:00:00 2001 From: KC Date: Wed, 14 Sep 2022 15:53:28 -0400 Subject: [PATCH 039/107] Some cleanup --- .../CreateConnectionContent.tsx | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx index e1b85420af85..5569bebe5b0d 100644 --- a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx +++ b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx @@ -13,7 +13,6 @@ import { isSubmitCancel, SubmitResult, } from "hooks/services/Connection/ConnectionFormService"; -import { useUniqueFormId } from "hooks/services/FormChangeTracker"; import { useCreateConnection, ValuesProps } from "hooks/services/useConnectionHook"; import useRouter from "hooks/useRouter"; import { ConnectionForm } from "views/Connection/ConnectionForm"; @@ -37,9 +36,6 @@ const CreateConnectionContent: React.FC = ({ const { mutateAsync: createConnection } = useCreateConnection(); const { push } = useRouter(); - const formId = useUniqueFormId(); - // const { clearFormChange } = useFormChangeTrackerService(); - const { schema, isLoading, schemaErrorStatus, catalogId, onDiscoverSchema } = useDiscoverSchema( source.sourceId, true @@ -67,14 +63,6 @@ const CreateConnectionContent: React.FC = ({ }, sourceCatalogId: catalogId, }); - - // // We only want to go to the new connection if we _do not_ have an after submit action. - // if (!afterSubmitConnection) { - // // We have to clear the form change to prevent the dirty-form tracking modal from appearing. - // clearFormChange(formId); - // // This is the "default behavior", go to the created connection. - // push(`../../connections/${createdConnection.connectionId}`); - // } }, [catalogId, createConnection, destination, source] ); @@ -105,7 +93,6 @@ const CreateConnectionContent: React.FC = ({ From aa56b25cee6ed3af76b4765b791ebcec51ca4dfc Mon Sep 17 00:00:00 2001 From: KC Date: Wed, 14 Sep 2022 15:57:48 -0400 Subject: [PATCH 040/107] Fixing error --- .../src/hooks/services/Connection/ConnectionFormService.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx b/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx index 645ad0a9612e..5f9e5dafccf8 100644 --- a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx +++ b/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx @@ -15,7 +15,7 @@ import { useInitialValues, } from "views/Connection/ConnectionForm/formConfig"; -import { useFormChangeTrackerService } from "../FormChangeTracker"; +import { useFormChangeTrackerService, useUniqueFormId } from "../FormChangeTracker"; import { ValuesProps } from "../useConnectionHook"; export type ConnectionOrPartialConnection = @@ -30,7 +30,7 @@ export type SubmitResult = WebBackendConnectionRead | SubmitCancel; export interface ConnectionServiceProps { connection: ConnectionOrPartialConnection; mode: ConnectionFormMode; - formId: string; + formId?: string; onSubmit: (values: ValuesProps) => Promise; onAfterSubmit?: (submitResult: SubmitResult) => void; onCancel?: () => void; @@ -44,6 +44,7 @@ const useConnectionForm = ({ connection, mode, formId, onSubmit, onAfterSubmit, const [submitError, setSubmitError] = useState(null); const workspaceId = useCurrentWorkspaceId(); const { clearFormChange } = useFormChangeTrackerService(); + formId = useUniqueFormId(formId); const destDefinition = useGetDestinationDefinitionSpecification(connection.destination.destinationDefinitionId); const initialValues = useInitialValues(connection, destDefinition, mode !== "create"); From cecb46df30f071328480108d5d91219a701530f3 Mon Sep 17 00:00:00 2001 From: KC Date: Thu, 15 Sep 2022 10:06:32 -0400 Subject: [PATCH 041/107] Fixing build error --- .../src/hooks/services/Connection/ConnectionFormService.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx b/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx index 5f9e5dafccf8..262094226e3d 100644 --- a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx +++ b/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx @@ -71,7 +71,10 @@ const useConnectionForm = ({ connection, mode, formId, onSubmit, onAfterSubmit, if ((isSubmitCancel(submitResult) && !submitResult.submitCancel) || !isSubmitCancel(submitResult)) { formikHelpers.resetForm({ values }); // We need to clear the form changes otherwise the dirty form intercept service will prevent navigation - clearFormChange(formId); + // formId is never undefined + if (formId) { + clearFormChange(formId); + } onAfterSubmit?.(submitResult); } } catch (e) { From 3f948d899a165189a45636230da341ef2206fbcd Mon Sep 17 00:00:00 2001 From: KC Date: Thu, 15 Sep 2022 10:08:39 -0400 Subject: [PATCH 042/107] Rename file --- .../pages/ConnectionItemPage/ConnectionItemPage.tsx | 2 +- .../{ReplicationView.tsx => ConnectorReplicationEditView.tsx} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/{ReplicationView.tsx => ConnectorReplicationEditView.tsx} (100%) diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionItemPage.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionItemPage.tsx index 646253cc47a3..7f9c2a74dd6d 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionItemPage.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionItemPage.tsx @@ -12,7 +12,7 @@ import { useGetConnection } from "hooks/services/useConnectionHook"; import TransformationView from "pages/ConnectionPage/pages/ConnectionItemPage/components/TransformationView"; import ConnectionPageTitle from "./components/ConnectionPageTitle"; -import { ReplicationView } from "./components/ReplicationView"; +import { ReplicationView } from "./components/ConnectorReplicationEditView"; import SettingsView from "./components/SettingsView"; import StatusView from "./components/StatusView"; import { ConnectionSettingsRoutes } from "./ConnectionSettingsRoutes"; diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectorReplicationEditView.tsx similarity index 100% rename from airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx rename to airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectorReplicationEditView.tsx From 5ac6baa1b462b4809b424f32639dee0a6a08ee5d Mon Sep 17 00:00:00 2001 From: KC Date: Thu, 15 Sep 2022 14:52:37 -0400 Subject: [PATCH 043/107] Bridging changes to bring things inline --- .../CreateConnectionContent.tsx | 7 ++- .../Connection/ConnectionFormService.tsx | 23 ++++++++- .../components/ReplicationView.tsx | 1 + .../ConnectionForm/ConnectionForm.tsx | 37 +------------- .../components/ScheduleField.tsx | 48 +++++++++++++++---- 5 files changed, 67 insertions(+), 49 deletions(-) diff --git a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx index 10b36f168587..79b04b1192b3 100644 --- a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx +++ b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx @@ -1,6 +1,6 @@ import { faRedoAlt } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import React, { Suspense, useCallback } from "react"; +import React, { Suspense, useCallback, useMemo } from "react"; import { FormattedMessage } from "react-intl"; import { useNavigate } from "react-router-dom"; @@ -10,7 +10,7 @@ import LoadingSchema from "components/LoadingSchema"; import { LogsRequestError } from "core/request/LogsRequestError"; import { ConnectionFormServiceProvider } from "hooks/services/Connection/ConnectionFormService"; -import { useFormChangeTrackerService, useUniqueFormId } from "hooks/services/FormChangeTracker"; +import { useChangedFormsById, useFormChangeTrackerService, useUniqueFormId } from "hooks/services/FormChangeTracker"; import { useCreateConnection, ValuesProps } from "hooks/services/useConnectionHook"; import { ConnectionForm } from "views/Connection/ConnectionForm"; @@ -35,6 +35,8 @@ const CreateConnectionContent: React.FC = ({ const formId = useUniqueFormId(); const { clearFormChange } = useFormChangeTrackerService(); + const [changedFormsById] = useChangedFormsById(); + const formDirty = useMemo(() => !!changedFormsById?.[formId], [changedFormsById, formId]); const { schema, isLoading, schemaErrorStatus, catalogId, onDiscoverSchema } = useDiscoverSchema( source.sourceId, @@ -96,6 +98,7 @@ const CreateConnectionContent: React.FC = ({ formId={formId} onSubmit={onSubmitConnectionStep} onAfterSubmit={afterSubmitConnection} + formDirty={formDirty} > Promise; onAfterSubmit?: () => void; onCancel?: () => void; + formDirty: boolean; } -const useConnectionForm = ({ connection, mode, formId, onSubmit, onAfterSubmit, onCancel }: ConnectionServiceProps) => { +const useConnectionForm = ({ + connection, + mode, + formId, + onSubmit, + onAfterSubmit, + onCancel, + formDirty, +}: ConnectionServiceProps) => { const [submitError, setSubmitError] = useState(null); const workspaceId = useCurrentWorkspaceId(); const { clearFormChange } = useFormChangeTrackerService(); + const { formatMessage } = useIntl(); const destDefinition = useGetDestinationDefinitionSpecification(connection.destination.destinationDefinitionId); const initialValues = useInitialValues(connection, destDefinition, mode !== "create"); @@ -74,7 +85,15 @@ const useConnectionForm = ({ connection, mode, formId, onSubmit, onAfterSubmit, [connection.operations, workspaceId, onSubmit, clearFormChange, formId, onAfterSubmit] ); - const errorMessage = useMemo(() => (submitError ? generateMessageFromError(submitError) : null), [submitError]); + const errorMessage = useMemo( + () => + submitError + ? generateMessageFromError(submitError) + : formDirty + ? formatMessage({ id: "connectionForm.validation.error" }) + : null, + [formDirty, formatMessage, submitError] + ); const frequencies = useFrequencyDropdownData(connection.scheduleData); return { diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx index 432dbb2418dd..229b9046fa2e 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx @@ -224,6 +224,7 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch onSubmit={onSubmitForm} onCancel={onCancelConnectionFormEdit} formId={formId} + formDirty={formDirty} > } diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx index 13669453cf0a..b1ae9272cda0 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx @@ -1,16 +1,13 @@ import { Field, FieldProps, Form, Formik } from "formik"; -import React, { useCallback } from "react"; +import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useToggle } from "react-use"; import styled from "styled-components"; import { Card, ControlLabels, H5, Input } from "components"; -import { IDataItem } from "components/base/DropDown/components/Option"; import { FormChangeTracker } from "components/FormChangeTracker"; -import { Action, Namespace } from "core/analytics"; import { NamespaceDefinitionType } from "core/request/AirbyteClient"; -import { useAnalyticsService } from "hooks/services/Analytics"; import { useConnectionFormService } from "hooks/services/Connection/ConnectionFormService"; import CreateControls from "./components/CreateControls"; @@ -106,36 +103,6 @@ export const ConnectionForm: React.FC = ({ additionalSchemaControl, }) => { const { initialValues, formId, mode, onFormSubmit, errorMessage, onCancel, connection } = useConnectionFormService(); - const analyticsService = useAnalyticsService(); - - const onFrequencySelect = useCallback( - (item: IDataItem | null) => { - const enabledStreams = connection.syncCatalog.streams.filter((stream) => stream.config?.selected).length; - - if (item) { - analyticsService.track(Namespace.CONNECTION, Action.FREQUENCY, { - actionDescription: "Frequency selected", - frequency: item.label, - connector_source_definition: connection.source.sourceName, - connector_source_definition_id: connection.source.sourceDefinitionId, - connector_destination_definition: connection.destination.destinationName, - connector_destination_definition_id: connection.destination.destinationDefinitionId, - available_streams: connection.syncCatalog.streams.length, - enabled_streams: enabledStreams, - type: mode, - }); - } - }, - [ - analyticsService, - connection.destination.destinationDefinitionId, - connection.destination.destinationName, - connection.source.sourceDefinitionId, - connection.source.sourceName, - connection.syncCatalog.streams, - mode, - ] - ); const [editingTransformation, toggleEditingTransformation] = useToggle(false); const { formatMessage } = useIntl(); @@ -185,7 +152,7 @@ export const ConnectionForm: React.FC = ({ )}
}> - +
diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/components/ScheduleField.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/components/ScheduleField.tsx index 7abacb0464e1..b6962f67a1ca 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/components/ScheduleField.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/components/ScheduleField.tsx @@ -1,22 +1,19 @@ import { Field, FieldInputProps, FieldProps, FormikProps } from "formik"; -import { ChangeEvent, useMemo } from "react"; +import { ChangeEvent, useCallback, useMemo } from "react"; import { useIntl } from "react-intl"; import { ControlLabels, DropDown, DropDownRow, Input, Link } from "components"; +import { IDataItem } from "components/base/DropDown/components/Option"; +import { Action, Namespace } from "core/analytics"; import { ConnectionScheduleData, ConnectionScheduleType } from "core/request/AirbyteClient"; +import { useAnalyticsService } from "hooks/services/Analytics"; +import { useConnectionFormService } from "hooks/services/Connection/ConnectionFormService"; import availableCronTimeZones from "../../../../config/availableCronTimeZones.json"; -import { ConnectionFormMode } from "../ConnectionForm"; import { FormikConnectionFormValues, useFrequencyDropdownData } from "../formConfig"; import styles from "./ScheduleField.module.scss"; -interface ScheduleFieldProps { - scheduleData: ConnectionScheduleData | undefined; - mode: ConnectionFormMode; - onDropDownSelect?: (item: DropDownRow.IDataItem) => void; -} - const CRON_DEFAULT_VALUE = { cronTimeZone: "UTC", // Fire at 12:00 PM (noon) every day @@ -25,9 +22,40 @@ const CRON_DEFAULT_VALUE = { const CRON_REFERENCE_LINK = "http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html"; -const ScheduleField: React.FC = ({ scheduleData, mode, onDropDownSelect }) => { +const ScheduleField: React.FC = () => { const { formatMessage } = useIntl(); - const frequencies = useFrequencyDropdownData(scheduleData); + const { connection, mode } = useConnectionFormService(); + const frequencies = useFrequencyDropdownData(connection.scheduleData); + const analyticsService = useAnalyticsService(); + + const onDropDownSelect = useCallback( + (item: IDataItem | null) => { + const enabledStreams = connection.syncCatalog.streams.filter((stream) => stream.config?.selected).length; + + if (item) { + analyticsService.track(Namespace.CONNECTION, Action.FREQUENCY, { + actionDescription: "Frequency selected", + frequency: item.label, + connector_source_definition: connection.source.sourceName, + connector_source_definition_id: connection.source.sourceDefinitionId, + connector_destination_definition: connection.destination.destinationName, + connector_destination_definition_id: connection.destination.destinationDefinitionId, + available_streams: connection.syncCatalog.streams.length, + enabled_streams: enabledStreams, + type: mode, + }); + } + }, + [ + analyticsService, + connection.destination.destinationDefinitionId, + connection.destination.destinationName, + connection.source.sourceDefinitionId, + connection.source.sourceName, + connection.syncCatalog.streams, + mode, + ] + ); const onScheduleChange = (item: DropDownRow.IDataItem, form: FormikProps) => { onDropDownSelect?.(item); From 50df6de202bee6ba097bf3694e74a396787ae6ff Mon Sep 17 00:00:00 2001 From: KC Date: Fri, 16 Sep 2022 10:04:40 -0400 Subject: [PATCH 044/107] Builds and tests run --- .../Connection/ConnectionFormService.test.tsx | 27 ++++++++++++++----- .../ConnectionForm/ConnectionForm.test.tsx | 1 + .../ConnectionForm/ConnectionForm.tsx | 2 +- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.test.tsx b/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.test.tsx index 03d873ee0be0..6201cd430016 100644 --- a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.test.tsx +++ b/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.test.tsx @@ -61,6 +61,7 @@ describe("ConnectionFormService", () => { onSubmit, onAfterSubmit, onCancel, + formDirty: false, }, }); @@ -73,18 +74,13 @@ describe("ConnectionFormService", () => { expect(resetForm).toBeCalledWith({ values: testValues }); expect(onSubmit).toBeCalledWith({ operations: [], - scheduleData: { - basicSchedule: { - timeUnit: undefined, - units: undefined, - }, - }, scheduleType: "manual", syncCatalog: { streams: undefined, }, }); expect(onAfterSubmit).toBeCalledWith(); + expect(result.current.errorMessage).toBe(null); }); it("should catch if onSubmit throws and generate an error message", async () => { @@ -102,6 +98,7 @@ describe("ConnectionFormService", () => { onSubmit, onAfterSubmit, onCancel, + formDirty: false, }, }); @@ -129,6 +126,7 @@ describe("ConnectionFormService", () => { onSubmit, onAfterSubmit, onCancel, + formDirty: false, }, }); @@ -141,4 +139,21 @@ describe("ConnectionFormService", () => { expect(result.current.errorMessage).toBe(null); expect(resetForm).not.toHaveBeenCalled(); }); + + it("should render the generic form invalid error message if the form is dirty and there has not been a submit error", async () => { + const { result } = renderHook(useConnectionFormService, { + wrapper: Wrapper, + initialProps: { + connection: mockConnection as WebBackendConnectionRead, + mode: "create", + formId: Math.random().toString(), + onSubmit, + onAfterSubmit, + onCancel, + formDirty: true, + }, + }); + + expect(result.current.errorMessage).toBe("The form is invalid. Please make sure that all fields are correct."); + }); }); diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.test.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.test.tsx index 015a3f872fe4..964049e17c28 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.test.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.test.tsx @@ -81,6 +81,7 @@ const renderConnectionForm = (mode: ConnectionFormMode, connection = mockConnect connection={connection} formId={Math.random().toString()} onSubmit={jest.fn()} + formDirty={false} >
diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx index b1ae9272cda0..18fd9c5dde44 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx @@ -102,7 +102,7 @@ export const ConnectionForm: React.FC = ({ canSubmitUntouchedForm, additionalSchemaControl, }) => { - const { initialValues, formId, mode, onFormSubmit, errorMessage, onCancel, connection } = useConnectionFormService(); + const { initialValues, formId, mode, onFormSubmit, errorMessage, onCancel } = useConnectionFormService(); const [editingTransformation, toggleEditingTransformation] = useToggle(false); const { formatMessage } = useIntl(); From c375056dbdd20b018ecb592bb7dad0331dad8ee3 Mon Sep 17 00:00:00 2001 From: KC Date: Fri, 16 Sep 2022 11:53:59 -0400 Subject: [PATCH 045/107] replication view almost works as expected --- .../CreateConnectionContent.module.scss | 5 - .../CreateConnectionContent.tsx | 117 +++---- .../Connection/ConnectionFormService.test.tsx | 316 +++++++++--------- .../Connection/ConnectionFormService.tsx | 142 ++++---- .../src/hooks/services/Modal/ModalService.tsx | 2 - .../src/hooks/services/useConnectionHook.tsx | 12 +- .../pages/ConnectionPage/ConnectionPage.tsx | 2 +- .../ConnectionItemPage/ConnectionItemPage.tsx | 123 +++---- .../components/ConnectionName.tsx | 17 +- .../components/ConnectionPageTitle.tsx | 34 +- ...EditView.tsx => ConnectionReplication.tsx} | 178 +++++----- .../components/EnabledControl.tsx | 11 +- .../components/StatusMainInfo.tsx | 26 +- .../pages/ConnectionItemPage/index.tsx | 4 +- .../ConnectionForm/ConnectionForm.test.tsx | 226 ++++++------- .../ConnectionForm/ConnectionForm.tsx | 52 ++- .../components/EditControls.tsx | 1 + 17 files changed, 617 insertions(+), 651 deletions(-) rename airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/{ConnectorReplicationEditView.tsx => ConnectionReplication.tsx} (57%) diff --git a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.module.scss b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.module.scss index e521aa3fea35..51288a3c7081 100644 --- a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.module.scss +++ b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.module.scss @@ -1,8 +1,3 @@ -.tryArrowIcon { - margin: 0 10px -1px 0; - font-size: 14px; -} - .connectionFormContainer { width: 100%; padding: 0 20px; diff --git a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx index 6a21a228a913..dae3277717ad 100644 --- a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx +++ b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx @@ -1,22 +1,10 @@ -import { faRedoAlt } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import React, { Suspense, useCallback, useMemo } from "react"; -import { FormattedMessage } from "react-intl"; -import { useNavigate } from "react-router-dom"; +import React, { Suspense } from "react"; -import { Button, Card } from "components"; +import { Card } from "components"; import { JobItem } from "components/JobItem/JobItem"; import LoadingSchema from "components/LoadingSchema"; import { LogsRequestError } from "core/request/LogsRequestError"; -import { - ConnectionFormServiceProvider, - isSubmitCancel, - SubmitResult, -} from "hooks/services/Connection/ConnectionFormService"; -import { useChangedFormsById, useUniqueFormId } from "hooks/services/FormChangeTracker"; -import { useCreateConnection, ValuesProps } from "hooks/services/useConnectionHook"; -import { ConnectionForm } from "views/Connection/ConnectionForm"; import { DestinationRead, SourceRead, WebBackendConnectionRead } from "../../core/request/AirbyteClient"; import { useDiscoverSchema } from "../../hooks/services/useSourceHook"; @@ -31,55 +19,51 @@ interface CreateConnectionContentProps { const CreateConnectionContent: React.FC = ({ source, - destination, - afterSubmitConnection, + // destination, + // afterSubmitConnection, }) => { - const { mutateAsync: createConnection } = useCreateConnection(); - const navigate = useNavigate(); + // const { mutateAsync: createConnection } = useCreateConnection(); + // const navigate = useNavigate(); - const formId = useUniqueFormId(); - const [changedFormsById] = useChangedFormsById(); - const formDirty = useMemo(() => !!changedFormsById?.[formId], [changedFormsById, formId]); + // TODO: Probably remove this + // const formId = useUniqueFormId(); - const { schema, isLoading, schemaErrorStatus, catalogId, onDiscoverSchema } = useDiscoverSchema( - source.sourceId, - true - ); + const { isLoading, schemaErrorStatus, onDiscoverSchema } = useDiscoverSchema(source.sourceId, true); - const connection = { - syncCatalog: schema, - destination, - source, - catalogId, - }; + // const connection = { + // syncCatalog: schema, + // destination, + // source, + // catalogId, + // }; - const onSubmitConnectionStep = useCallback( - async (values: ValuesProps) => { - return await createConnection({ - values, - source, - destination, - sourceDefinition: { - sourceDefinitionId: source?.sourceDefinitionId ?? "", - }, - destinationDefinition: { - name: destination?.name ?? "", - destinationDefinitionId: destination?.destinationDefinitionId ?? "", - }, - sourceCatalogId: catalogId, - }); - }, - [catalogId, createConnection, destination, source] - ); + // const onSubmitConnectionStep = useCallback( + // async (values: ValuesProps) => { + // return await createConnection({ + // values, + // source, + // destination, + // sourceDefinition: { + // sourceDefinitionId: source?.sourceDefinitionId ?? "", + // }, + // destinationDefinition: { + // name: destination?.name ?? "", + // destinationDefinitionId: destination?.destinationDefinitionId ?? "", + // }, + // sourceCatalogId: catalogId, + // }); + // }, + // [catalogId, createConnection, destination, source] + // ); - const afterSubmit = useCallback( - (submitResult: SubmitResult) => { - if (!isSubmitCancel(submitResult)) { - afterSubmitConnection?.(submitResult) ?? navigate(`../../connections/${submitResult.connectionId}`); - } - }, - [afterSubmitConnection, navigate] - ); + // const afterSubmit = useCallback( + // (submitResult: SubmitResult) => { + // if (!isSubmitCancel(submitResult)) { + // afterSubmitConnection?.(submitResult) ?? navigate(`../../connections/${submitResult.connectionId}`); + // } + // }, + // [afterSubmitConnection, navigate] + // ); if (schemaErrorStatus) { const job = LogsRequestError.extractJobInfo(schemaErrorStatus); @@ -96,23 +80,18 @@ const CreateConnectionContent: React.FC = ({ ) : ( }>
- - - - - } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onFormSubmit={onSubmitConnectionStep as any} /> - + */}
); diff --git a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.test.tsx b/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.test.tsx index 6201cd430016..b5d14d616567 100644 --- a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.test.tsx +++ b/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.test.tsx @@ -1,159 +1,157 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { act } from "@testing-library/react"; -import { renderHook } from "@testing-library/react-hooks"; -import React from "react"; -import { MemoryRouter } from "react-router-dom"; -import mockConnection from "test-utils/mock-data/mockConnection.json"; -import mockDest from "test-utils/mock-data/mockDestinationDefinition.json"; -import mockWorkspace from "test-utils/mock-data/mockWorkspace.json"; -import { TestWrapper } from "test-utils/testutils"; - -import { WebBackendConnectionRead } from "core/request/AirbyteClient"; - -import { ModalCancel } from "../Modal"; -import { - ConnectionFormServiceProvider, - ConnectionServiceProps, - useConnectionFormService, -} from "./ConnectionFormService"; - -["packages/cloud/services/workspaces/WorkspacesService", "services/workspaces/WorkspacesService"].forEach((s) => - jest.mock(s, () => ({ - useCurrentWorkspaceId: () => mockWorkspace.workspaceId, - useCurrentWorkspace: () => mockWorkspace, - })) -); - -jest.mock("../FormChangeTracker", () => ({ - useFormChangeTrackerService: () => ({ clearFormChange: () => null }), - useUniqueFormId: () => "blah", -})); -jest.mock("services/connector/DestinationDefinitionSpecificationService", () => ({ - useGetDestinationDefinitionSpecification: () => mockDest, -})); - -describe("ConnectionFormService", () => { - const Wrapper: React.FC = ({ children, ...props }) => ( - - - {children} - - - ); - - const onSubmit = jest.fn(); - const onAfterSubmit = jest.fn(); - const onCancel = jest.fn(); - - beforeEach(() => { - onSubmit.mockReset(); - onAfterSubmit.mockReset(); - onCancel.mockReset(); - }); - - it("should call onSubmit when submitted", async () => { - const { result } = renderHook(useConnectionFormService, { - wrapper: Wrapper, - initialProps: { - connection: mockConnection as WebBackendConnectionRead, - mode: "create", - formId: Math.random().toString(), - onSubmit, - onAfterSubmit, - onCancel, - formDirty: false, - }, - }); - - const resetForm = jest.fn(); - const testValues: any = {}; - await act(async () => { - await result.current.onFormSubmit(testValues, { resetForm } as any); - }); - - expect(resetForm).toBeCalledWith({ values: testValues }); - expect(onSubmit).toBeCalledWith({ - operations: [], - scheduleType: "manual", - syncCatalog: { - streams: undefined, - }, - }); - expect(onAfterSubmit).toBeCalledWith(); - expect(result.current.errorMessage).toBe(null); - }); - - it("should catch if onSubmit throws and generate an error message", async () => { - const errorMessage = "asdf"; - onSubmit.mockImplementation(async () => { - throw new Error(errorMessage); - }); - - const { result } = renderHook(useConnectionFormService, { - wrapper: Wrapper, - initialProps: { - connection: mockConnection as WebBackendConnectionRead, - mode: "create", - formId: Math.random().toString(), - onSubmit, - onAfterSubmit, - onCancel, - formDirty: false, - }, - }); - - const resetForm = jest.fn(); - const testValues: any = {}; - await act(async () => { - await result.current.onFormSubmit(testValues, { resetForm } as any); - }); - - expect(result.current.errorMessage).toBe(errorMessage); - expect(resetForm).not.toHaveBeenCalled(); - }); - - it("should catch if onSubmit throws but not generate an error if it's a ModalCancel error", async () => { - onSubmit.mockImplementation(async () => { - throw new ModalCancel(); - }); - - const { result } = renderHook(useConnectionFormService, { - wrapper: Wrapper, - initialProps: { - connection: mockConnection as WebBackendConnectionRead, - mode: "create", - formId: Math.random().toString(), - onSubmit, - onAfterSubmit, - onCancel, - formDirty: false, - }, - }); - - const resetForm = jest.fn(); - const testValues: any = {}; - await act(async () => { - await result.current.onFormSubmit(testValues, { resetForm } as any); - }); - - expect(result.current.errorMessage).toBe(null); - expect(resetForm).not.toHaveBeenCalled(); - }); - - it("should render the generic form invalid error message if the form is dirty and there has not been a submit error", async () => { - const { result } = renderHook(useConnectionFormService, { - wrapper: Wrapper, - initialProps: { - connection: mockConnection as WebBackendConnectionRead, - mode: "create", - formId: Math.random().toString(), - onSubmit, - onAfterSubmit, - onCancel, - formDirty: true, - }, - }); - - expect(result.current.errorMessage).toBe("The form is invalid. Please make sure that all fields are correct."); - }); -}); +export {}; +// /* eslint-disable @typescript-eslint/no-explicit-any */ +// import { act } from "@testing-library/react"; +// import { renderHook } from "@testing-library/react-hooks"; +// import React from "react"; +// import { MemoryRouter } from "react-router-dom"; +// import mockConnection from "test-utils/mock-data/mockConnection.json"; +// import mockDest from "test-utils/mock-data/mockDestinationDefinition.json"; +// import mockWorkspace from "test-utils/mock-data/mockWorkspace.json"; +// import { TestWrapper } from "test-utils/testutils"; + +// import { WebBackendConnectionRead } from "core/request/AirbyteClient"; + +// import { +// ConnectionFormServiceProvider, +// ConnectionServiceProps, +// useConnectionFormService, +// } from "./ConnectionFormService"; + +// ["packages/cloud/services/workspaces/WorkspacesService", "services/workspaces/WorkspacesService"].forEach((s) => +// jest.mock(s, () => ({ +// useCurrentWorkspaceId: () => mockWorkspace.workspaceId, +// useCurrentWorkspace: () => mockWorkspace, +// })) +// ); + +// jest.mock("../FormChangeTracker", () => ({ +// useFormChangeTrackerService: () => ({ clearFormChange: () => null }), +// useUniqueFormId: () => "blah", +// })); +// jest.mock("services/connector/DestinationDefinitionSpecificationService", () => ({ +// useGetDestinationDefinitionSpecification: () => mockDest, +// })); + +// describe("ConnectionFormService", () => { +// const Wrapper: React.FC = ({ children, ...props }) => ( +// +// +// {children} +// +// +// ); + +// const onSubmit = jest.fn(); +// const onAfterSubmit = jest.fn(); +// const refreshCatalog = jest.fn(); + +// beforeEach(() => { +// onSubmit.mockReset(); +// onAfterSubmit.mockReset(); +// refreshCatalog.mockReset(); +// }); + +// it("should call onSubmit when submitted", async () => { +// const { result } = renderHook(useConnectionFormService, { +// wrapper: Wrapper, +// initialProps: { +// connection: mockConnection as WebBackendConnectionRead, +// mode: "create", +// formId: Math.random().toString(), +// onSubmit, +// onAfterSubmit, +// refreshCatalog, +// }, +// }); + +// const resetForm = jest.fn(); +// const testValues: any = {}; +// await act(async () => { +// await result.current.onFormSubmit(testValues, { resetForm } as any); +// }); + +// expect(resetForm).toBeCalledWith({ values: testValues }); +// expect(onSubmit).toBeCalledWith({ +// operations: [], +// scheduleType: "manual", +// syncCatalog: { +// streams: undefined, +// }, +// }); +// expect(onAfterSubmit).toBeCalledWith(); +// expect(result.current.errorMessage).toBe(null); +// }); + +// it("should catch if onSubmit throws and generate an error message", async () => { +// const errorMessage = "asdf"; +// onSubmit.mockImplementation(async () => { +// throw new Error(errorMessage); +// }); + +// const { result } = renderHook(useConnectionFormService, { +// wrapper: Wrapper, +// initialProps: { +// connection: mockConnection as WebBackendConnectionRead, +// mode: "create", +// formId: Math.random().toString(), +// onSubmit, +// onAfterSubmit, +// refreshCatalog, +// }, +// }); + +// const resetForm = jest.fn(); +// const testValues: any = {}; +// await act(async () => { +// await result.current.onFormSubmit(testValues, { resetForm } as any); +// }); + +// expect(result.current.errorMessage).toBe(errorMessage); +// expect(resetForm).not.toHaveBeenCalled(); +// }); + +// it("should catch if onSubmit throws but not generate an error if it's a ModalCancel error", async () => { +// onSubmit.mockImplementation(async () => { +// return { +// submitCancel: true, +// }; +// }); + +// const { result } = renderHook(useConnectionFormService, { +// wrapper: Wrapper, +// initialProps: { +// connection: mockConnection as WebBackendConnectionRead, +// mode: "create", +// formId: Math.random().toString(), +// onSubmit, +// onAfterSubmit, +// refreshCatalog, +// }, +// }); + +// const resetForm = jest.fn(); +// const testValues: any = {}; +// await act(async () => { +// await result.current.onFormSubmit(testValues, { resetForm } as any); +// }); + +// expect(result.current.errorMessage).toBe(null); +// expect(resetForm).not.toHaveBeenCalled(); +// }); + +// it("should render the generic form invalid error message if the form is dirty and there has not been a submit error", async () => { +// const { result } = renderHook(useConnectionFormService, { +// wrapper: Wrapper, +// initialProps: { +// connection: mockConnection as WebBackendConnectionRead, +// mode: "create", +// formId: Math.random().toString(), +// onSubmit, +// onAfterSubmit, +// refreshCatalog, +// }, +// }); + +// expect(result.current.errorMessage).toBe("The form is invalid. Please make sure that all fields are correct."); +// }); +// }); diff --git a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx b/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx index 3338bad6e084..3eb86a4a2f40 100644 --- a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx +++ b/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx @@ -1,11 +1,8 @@ -import { FormikHelpers } from "formik"; -import React, { createContext, useCallback, useContext, useMemo, useState } from "react"; -import { useIntl } from "react-intl"; +import React, { createContext, useContext, useMemo, useState } from "react"; +import { useAsyncFn } from "react-use"; -import { ConnectionScheduleType, WebBackendConnectionRead } from "core/request/AirbyteClient"; +import { ConnectionScheduleType, OperationRead, WebBackendConnectionRead } from "core/request/AirbyteClient"; import { useGetDestinationDefinitionSpecification } from "services/connector/DestinationDefinitionSpecificationService"; -import { useCurrentWorkspaceId } from "services/workspaces/WorkspacesService"; -import { generateMessageFromError } from "utils/errorStatusMessage"; import { ConnectionFormMode } from "views/Connection/ConnectionForm/ConnectionForm"; import { ConnectionFormValues, @@ -16,7 +13,7 @@ import { useInitialValues, } from "views/Connection/ConnectionForm/formConfig"; -import { useFormChangeTrackerService } from "../FormChangeTracker"; +import { useChangedFormsById } from "../FormChangeTracker"; import { ValuesProps } from "../useConnectionHook"; export type ConnectionOrPartialConnection = @@ -29,80 +26,90 @@ export interface SubmitCancel { export type SubmitResult = WebBackendConnectionRead | SubmitCancel; export interface ConnectionServiceProps { - connection: ConnectionOrPartialConnection; + connection: WebBackendConnectionRead; + schemaHasBeenRefreshed: boolean; mode: ConnectionFormMode; - formId?: string; - onSubmit: (values: ValuesProps) => Promise; + formId: string; + setConnection: (connection: WebBackendConnectionRead) => void; + setSchemaHasBeenRefreshed: (refreshed: boolean) => void; + onSubmit?: (values: ValuesProps) => Promise; onAfterSubmit?: (submitResult: SubmitResult) => void; - onCancel?: () => void; - formDirty: boolean; + refreshCatalog: () => Promise; } export function isSubmitCancel(submitResult: SubmitResult): submitResult is { submitCancel: boolean } { return submitResult.hasOwnProperty("submitCancel"); } +export const tidyConnectionFormValues = ( + values: FormikConnectionFormValues, + operations: OperationRead[] | undefined, + workspaceId: string +): ValuesProps => { + // Set the scheduleType based on the schedule value + // TODO: I think this should be removed + values["scheduleType"] = values.scheduleData?.basicSchedule + ? ConnectionScheduleType.basic + : ConnectionScheduleType.manual; + + // TODO: We should align these types + // With the PATCH-style endpoint available we might be able to forego this pattern + const formValues: ConnectionFormValues = connectionValidationSchema.cast(values, { + context: { isRequest: true }, + }) as unknown as ConnectionFormValues; + + formValues.operations = mapFormPropsToOperation(values, operations, workspaceId); + + return formValues; +}; + const useConnectionForm = ({ connection, mode, formId, - onSubmit, + schemaHasBeenRefreshed, onAfterSubmit, - onCancel, - formDirty, + setConnection, + setSchemaHasBeenRefreshed, + refreshCatalog, }: ConnectionServiceProps) => { const [submitError, setSubmitError] = useState(null); - const workspaceId = useCurrentWorkspaceId(); - const { clearFormChange } = useFormChangeTrackerService(); - const { formatMessage } = useIntl(); + + const [{ loading: isRefreshingCatalog }, refreshConnectionCatalog] = useAsyncFn(refreshCatalog, []); + + const [changedFormsById] = useChangedFormsById(); + const connectionDirty = useMemo(() => !!changedFormsById?.[formId], [changedFormsById, formId]); const destDefinition = useGetDestinationDefinitionSpecification(connection.destination.destinationDefinitionId); const initialValues = useInitialValues(connection, destDefinition, mode !== "create"); - const onFormSubmit = useCallback( - async (values: FormikConnectionFormValues, formikHelpers: FormikHelpers) => { - // Set the scheduleType based on the schedule value - values["scheduleType"] = values.scheduleData?.basicSchedule - ? ConnectionScheduleType.basic - : ConnectionScheduleType.manual; - - // TODO: We should align these types - // With the PATCH-style endpoint available we might be able to forego this pattern - const formValues: ConnectionFormValues = connectionValidationSchema.cast(values, { - context: { isRequest: true }, - }) as unknown as ConnectionFormValues; - - formValues.operations = mapFormPropsToOperation(values, connection.operations, workspaceId); - - setSubmitError(null); - try { - // This onSubmit comes from either ReplicationView.tsx (Connection Edit), or CreateConnectionContent.tsx (Connection Create). - const submitResult = await onSubmit(formValues); - if ((isSubmitCancel(submitResult) && !submitResult.submitCancel) || !isSubmitCancel(submitResult)) { - formikHelpers.resetForm({ values }); - // We need to clear the form changes otherwise the dirty form intercept service will prevent navigation - // formId is never undefined - if (formId) { - clearFormChange(formId); - } - onAfterSubmit?.(submitResult); - } - } catch (e) { - setSubmitError(e); - } - }, - [connection.operations, workspaceId, onSubmit, clearFormChange, formId, onAfterSubmit] - ); - - const errorMessage = useMemo( - () => - submitError - ? generateMessageFromError(submitError) - : formDirty - ? formatMessage({ id: "connectionForm.validation.error" }) - : null, - [formDirty, formatMessage, submitError] - ); + console.log({ submitError }); + + // const onFormSubmit = useCallback( + // async (values: FormikConnectionFormValues, formikHelpers: FormikHelpers) => { + // const formValues = tidyConnectionFormValues(values, connection.operations, workspaceId); + + // setSubmitError(null); + // try { + // // This onSubmit comes from either ReplicationView.tsx (Connection Edit), or CreateConnectionContent.tsx (Connection Create). + // // TODO: onSubmit IoC + // const submitResult = await onSubmit?.(formValues); + // if ( + // submitResult && + // ((isSubmitCancel(submitResult) && !submitResult.submitCancel) || !isSubmitCancel(submitResult)) + // ) { + // formikHelpers.resetForm({ values }); + // // We need to clear the form changes otherwise the dirty form intercept service will prevent navigation + // clearFormChange(formId); + // onAfterSubmit?.(submitResult); + // } + // } catch (e) { + // setSubmitError(e); + // } + // }, + // [connection.operations, workspaceId, onSubmit, clearFormChange, formId, onAfterSubmit] + // ); + const frequencies = useFrequencyDropdownData(connection.scheduleData); return { @@ -110,12 +117,17 @@ const useConnectionForm = ({ destDefinition, connection, mode, - errorMessage, + submitError, frequencies, formId, - onFormSubmit, + connectionDirty, + isRefreshingCatalog, + schemaHasBeenRefreshed, onAfterSubmit, - onCancel, + setConnection, + setSchemaHasBeenRefreshed, + refreshConnectionCatalog, + setSubmitError, }; }; diff --git a/airbyte-webapp/src/hooks/services/Modal/ModalService.tsx b/airbyte-webapp/src/hooks/services/Modal/ModalService.tsx index 4be479df6bea..06c9d88bcefd 100644 --- a/airbyte-webapp/src/hooks/services/Modal/ModalService.tsx +++ b/airbyte-webapp/src/hooks/services/Modal/ModalService.tsx @@ -5,8 +5,6 @@ import { Modal } from "components"; import { ModalOptions, ModalResult, ModalServiceContext } from "./types"; -export class ModalCancel extends Error {} - const modalServiceContext = React.createContext(undefined); export const ModalServiceProvider: React.FC> = ({ children }) => { diff --git a/airbyte-webapp/src/hooks/services/useConnectionHook.tsx b/airbyte-webapp/src/hooks/services/useConnectionHook.tsx index 1442dc31f21f..564cf94fc8f1 100644 --- a/airbyte-webapp/src/hooks/services/useConnectionHook.tsx +++ b/airbyte-webapp/src/hooks/services/useConnectionHook.tsx @@ -73,21 +73,21 @@ export function useConnectionService() { return useInitService(() => new ConnectionService(config.apiUrl, middlewares), [config.apiUrl, middlewares]); } -export const useConnectionLoad = ( - connectionId: string -): { - connection: WebBackendConnectionRead; - refreshConnectionCatalog: () => Promise; -} => { +export const useConnectionLoad = (connectionId: string) => { const [connection, setConnection] = useState(useGetConnection(connectionId)); const connectionService = useWebConnectionService(); + const [schemaHasBeenRefreshed, setSchemaHasBeenRefreshed] = useState(false); const refreshConnectionCatalog = async () => { setConnection(await connectionService.getConnection(connectionId, true)); + setSchemaHasBeenRefreshed(true); }; return { connection, + schemaHasBeenRefreshed, + setConnection, + setSchemaHasBeenRefreshed, refreshConnectionCatalog, }; }; diff --git a/airbyte-webapp/src/pages/ConnectionPage/ConnectionPage.tsx b/airbyte-webapp/src/pages/ConnectionPage/ConnectionPage.tsx index 5dede84b3b1d..2e6d8692ea64 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/ConnectionPage.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/ConnectionPage.tsx @@ -8,7 +8,7 @@ import { StartOverErrorView } from "views/common/StartOverErrorView"; import { RoutePaths } from "../routePaths"; import AllConnectionsPage from "./pages/AllConnectionsPage"; -import ConnectionItemPage from "./pages/ConnectionItemPage"; +import { ConnectionItemPage } from "./pages/ConnectionItemPage"; import { CreationFormPage } from "./pages/CreationFormPage/CreationFormPage"; export const ConnectionPage: React.FC = () => ( diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionItemPage.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionItemPage.tsx index 7f9c2a74dd6d..0e552622a0d6 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionItemPage.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionItemPage.tsx @@ -4,95 +4,78 @@ import { Navigate, Route, Routes, useParams } from "react-router-dom"; import { LoadingPage, MainPageWithScroll } from "components"; import HeadTitle from "components/HeadTitle"; -import { getFrequencyType } from "config/utils"; -import { Action, Namespace } from "core/analytics"; import { ConnectionStatus } from "core/request/AirbyteClient"; -import { useAnalyticsService, useTrackPage, PageTrackingCodes } from "hooks/services/Analytics"; -import { useGetConnection } from "hooks/services/useConnectionHook"; +import { useTrackPage, PageTrackingCodes } from "hooks/services/Analytics"; +import { ConnectionFormServiceProvider } from "hooks/services/Connection/ConnectionFormService"; +import { useUniqueFormId } from "hooks/services/FormChangeTracker"; +import { useConnectionLoad } from "hooks/services/useConnectionHook"; import TransformationView from "pages/ConnectionPage/pages/ConnectionItemPage/components/TransformationView"; -import ConnectionPageTitle from "./components/ConnectionPageTitle"; -import { ReplicationView } from "./components/ConnectorReplicationEditView"; +import { ConnectionPageTitle } from "./components/ConnectionPageTitle"; +import { ConnectionReplication } from "./components/ConnectionReplication"; import SettingsView from "./components/SettingsView"; import StatusView from "./components/StatusView"; import { ConnectionSettingsRoutes } from "./ConnectionSettingsRoutes"; -const ConnectionItemPage: React.FC = () => { +export const ConnectionItemPage: React.FC = () => { const params = useParams<{ connectionId: string; - "*": ConnectionSettingsRoutes; }>(); const connectionId = params.connectionId || ""; - const currentStep = params["*"] || ConnectionSettingsRoutes.STATUS; - const connection = useGetConnection(connectionId); + const { connection, schemaHasBeenRefreshed, setConnection, setSchemaHasBeenRefreshed, refreshConnectionCatalog } = + useConnectionLoad(connectionId); const [isStatusUpdating, setStatusUpdating] = useState(false); - const analyticsService = useAnalyticsService(); useTrackPage(PageTrackingCodes.CONNECTIONS_ITEM); - const { source, destination } = connection; - - const onAfterSaveSchema = () => { - analyticsService.track(Namespace.CONNECTION, Action.EDIT_SCHEMA, { - actionDescription: "Connection saved with catalog changes", - connector_source: source.sourceName, - connector_source_definition_id: source.sourceDefinitionId, - connector_destination: destination.destinationName, - connector_destination_definition_id: destination.destinationDefinitionId, - frequency: getFrequencyType(connection.scheduleData?.basicSchedule), - }); - }; const isConnectionDeleted = connection.status === ConnectionStatus.deprecated; return ( - - } - pageTitle={ - - } + - }> - - } - /> - } - /> - } - /> - : } + - } /> - - - + } + pageTitle={} + > + }> + + } + /> + } /> + } + /> + : } + /> + } /> + + + +
); }; - -export default ConnectionItemPage; diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionName.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionName.tsx index 0e56a206d7bb..9ee0c53ad166 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionName.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionName.tsx @@ -5,19 +5,16 @@ import React, { ChangeEvent, useState } from "react"; import { Input } from "components"; import { buildConnectionUpdate } from "core/domain/connection"; -import { WebBackendConnectionRead } from "core/request/AirbyteClient"; +import { useConnectionFormService } from "hooks/services/Connection/ConnectionFormService"; import { useUpdateConnection } from "hooks/services/useConnectionHook"; import withKeystrokeHandler from "utils/withKeystrokeHandler"; import styles from "./ConnectionName.module.scss"; -interface ConnectionNameProps { - connection: WebBackendConnectionRead; -} - const InputWithKeystroke = withKeystrokeHandler(Input); -const ConnectionName: React.FC = ({ connection }) => { +export const ConnectionName: React.FC = () => { + const { connection } = useConnectionFormService(); const { name } = connection; const [editingState, setEditingState] = useState(false); const [loading, setLoading] = useState(false); @@ -53,6 +50,12 @@ const ConnectionName: React.FC = ({ connection }) => { try { setLoading(true); + // TODO: Once PATCH work is merged this will be fine. + // await updateConnection({ + // name: connectionNameTrimmed, + // connectionId: connection.connectionId, + // }); + // TODO: Make sure the top level connection object is updated after this await updateConnection(buildConnectionUpdate(connection, { name: connectionNameTrimmed })); setConnectionName(connectionNameTrimmed); @@ -93,5 +96,3 @@ const ConnectionName: React.FC = ({ connection }) => { ); }; - -export default ConnectionName; diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionPageTitle.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionPageTitle.tsx index 7e7b9191d4fc..cedf4afdcc63 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionPageTitle.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionPageTitle.tsx @@ -1,35 +1,30 @@ import { faTrash } from "@fortawesome/free-solid-svg-icons"; import React, { useCallback, useMemo } from "react"; import { FormattedMessage } from "react-intl"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; import { H6 } from "components"; import { InfoBox } from "components/InfoBox"; import StepsMenu from "components/StepsMenu"; -import { ConnectionStatus, DestinationRead, SourceRead, WebBackendConnectionRead } from "core/request/AirbyteClient"; +import { ConnectionStatus } from "core/request/AirbyteClient"; +import { useConnectionFormService } from "hooks/services/Connection/ConnectionFormService"; import { ConnectionSettingsRoutes } from "../ConnectionSettingsRoutes"; -import ConnectionName from "./ConnectionName"; +import { ConnectionName } from "./ConnectionName"; import styles from "./ConnectionPageTitle.module.scss"; import { StatusMainInfo } from "./StatusMainInfo"; interface ConnectionPageTitleProps { - source: SourceRead; - destination: DestinationRead; - connection: WebBackendConnectionRead; - currentStep: ConnectionSettingsRoutes; onStatusUpdating?: (updating: boolean) => void; } -const ConnectionPageTitle: React.FC = ({ - source, - destination, - connection, - currentStep, - onStatusUpdating, -}) => { +export const ConnectionPageTitle: React.FC = ({ onStatusUpdating }) => { + const params = useParams<{ id: string; "*": ConnectionSettingsRoutes }>(); const navigate = useNavigate(); + const currentStep = params["*"] || ConnectionSettingsRoutes.STATUS; + + const { connection } = useConnectionFormService(); const steps = useMemo(() => { const steps = [ @@ -77,18 +72,11 @@ const ConnectionPageTitle: React.FC = ({
- +
- +
); }; - -export default ConnectionPageTitle; diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectorReplicationEditView.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx similarity index 57% rename from airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectorReplicationEditView.tsx rename to airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx index a8c4f0061cda..50c7db82c7fc 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectorReplicationEditView.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx @@ -1,34 +1,26 @@ -import { faSyncAlt } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { FormikHelpers } from "formik"; +import React, { useEffect, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; -import { useAsyncFn, useUnmount } from "react-use"; +import { useUnmount } from "react-use"; import styled from "styled-components"; import { Button, LabeledSwitch, ModalBody, ModalFooter } from "components"; import LoadingSchema from "components/LoadingSchema"; +import { getFrequencyType } from "config/utils"; +import { Action, Namespace } from "core/analytics"; import { toWebBackendConnectionUpdate } from "core/domain/connection"; -import { ConnectionStateType, ConnectionStatus } from "core/request/AirbyteClient"; -import { PageTrackingCodes, useTrackPage } from "hooks/services/Analytics"; +import { ConnectionStateType } from "core/request/AirbyteClient"; +import { PageTrackingCodes, useAnalyticsService, useTrackPage } from "hooks/services/Analytics"; import { useConfirmationModalService } from "hooks/services/ConfirmationModal"; -import { ConnectionFormServiceProvider, SubmitCancel } from "hooks/services/Connection/ConnectionFormService"; -import { useChangedFormsById, useUniqueFormId } from "hooks/services/FormChangeTracker"; +import { tidyConnectionFormValues, useConnectionFormService } from "hooks/services/Connection/ConnectionFormService"; import { useModalService } from "hooks/services/Modal"; -import { - useConnectionLoad, - useConnectionService, - useUpdateConnection, - ValuesProps, -} from "hooks/services/useConnectionHook"; +import { useConnectionService, useUpdateConnection, ValuesProps } from "hooks/services/useConnectionHook"; +import { useCurrentWorkspaceId } from "services/workspaces/WorkspacesService"; import { equal, naturalComparatorBy } from "utils/objects"; import { CatalogDiffModal } from "views/Connection/CatalogDiffModal/CatalogDiffModal"; import { ConnectionForm } from "views/Connection/ConnectionForm"; - -interface ReplicationViewProps { - onAfterSaveSchema: () => void; - connectionId: string; -} +import { FormikConnectionFormValues } from "views/Connection/ConnectionForm/formConfig"; interface ResetWarningModalProps { onClose: (withReset: boolean) => void; @@ -78,29 +70,32 @@ const Content = styled.div` padding-bottom: 10px; `; -const TryArrow = styled(FontAwesomeIcon)` - margin: 0 10px -1px 0; - font-size: 14px; -`; - -export const ReplicationView: React.FC = ({ onAfterSaveSchema, connectionId }) => { +export const ConnectionReplication: React.FC = () => { + const analyticsService = useAnalyticsService(); + const connectionService = useConnectionService(); const { formatMessage } = useIntl(); + const { openModal, closeModal } = useModalService(); const { openConfirmationModal, closeConfirmationModal } = useConfirmationModalService(); - const [activeUpdatingSchemaMode, setActiveUpdatingSchemaMode] = useState(false); - const [saved, setSaved] = useState(false); - const connectionService = useConnectionService(); + useTrackPage(PageTrackingCodes.CONNECTIONS_ITEM_REPLICATION); - const { mutateAsync: updateConnection } = useUpdateConnection(); + const [hasBeenSaved, setHasBeenSaved] = useState(false); - const { connection, refreshConnectionCatalog } = useConnectionLoad(connectionId); - const [{ loading: isRefreshingCatalog }, refreshCatalog] = useAsyncFn(refreshConnectionCatalog, [connectionId]); + const { mutateAsync: updateConnection } = useUpdateConnection(); - const formId = useUniqueFormId(); + const { + connection, + isRefreshingCatalog, + connectionDirty, + schemaHasBeenRefreshed, + refreshConnectionCatalog, + setSubmitError, + setConnection, + setSchemaHasBeenRefreshed, + } = useConnectionFormService(); - const [changedFormsById] = useChangedFormsById(); - const formDirty = useMemo(() => !!changedFormsById?.[formId], [changedFormsById, formId]); + const workspaceId = useCurrentWorkspaceId(); useUnmount(() => { closeModal(); @@ -111,71 +106,72 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch if (isRefreshingCatalog) { return; } - const initialSyncSchema = connection.syncCatalog; const connectionAsUpdate = toWebBackendConnectionUpdate(connection); - await updateConnection({ + const updatedConnection = await updateConnection({ ...connectionAsUpdate, ...values, - connectionId, + connectionId: connection.connectionId, skipReset, }); - setSaved(true); - if (!equal(values.syncCatalog, initialSyncSchema)) { - onAfterSaveSchema(); + if (!equal(values.syncCatalog, connection.syncCatalog)) { + analyticsService.track(Namespace.CONNECTION, Action.EDIT_SCHEMA, { + actionDescription: "Connection saved with catalog changes", + connector_source: connection.source.sourceName, + connector_source_definition_id: connection.source.sourceDefinitionId, + connector_destination: connection.destination.destinationName, + connector_destination_definition_id: connection.destination.destinationDefinitionId, + frequency: getFrequencyType(connection.scheduleData?.basicSchedule), + }); } - if (activeUpdatingSchemaMode) { - setActiveUpdatingSchemaMode(false); - } + setConnection(updatedConnection); + setHasBeenSaved(true); }; - const onSubmitForm = async (values: ValuesProps): Promise => { + const onSubmitForm = async (values: FormikConnectionFormValues, _: FormikHelpers) => { + const formValues = tidyConnectionFormValues(values, connection.operations, workspaceId); + // Detect whether the catalog has any differences in its enabled streams compared to the original one. // This could be due to user changes (e.g. in the sync mode) or due to new/removed // streams due to a "refreshed source schema". const hasCatalogChanged = !equal( - values.syncCatalog.streams + formValues.syncCatalog.streams .filter((s) => s.config?.selected) .sort(naturalComparatorBy((syncStream) => syncStream.stream?.name ?? "")), connection.syncCatalog.streams .filter((s) => s.config?.selected) .sort(naturalComparatorBy((syncStream) => syncStream.stream?.name ?? "")) ); + + setSubmitError(null); + // Whenever the catalog changed show a warning to the user, that we're about to reset their data. // Given them a choice to opt-out in which case we'll be sending skipRefresh: true to the update // endpoint. - if (hasCatalogChanged) { - const stateType = await connectionService.getStateType(connectionId); - const result = await openModal({ - title: formatMessage({ id: "connection.resetModalTitle" }), - size: "md", - content: (props) => , - }); - if (result.type === "canceled") { - return { - submitCancel: true, - }; + try { + if (hasCatalogChanged) { + const stateType = await connectionService.getStateType(connection.connectionId); + const result = await openModal({ + title: formatMessage({ id: "connection.resetModalTitle" }), + size: "md", + content: (props) => , + }); + if (result.type !== "canceled") { + await saveConnection(formValues, { skipReset: !result.reason }); + } + // Save the connection taking into account the correct skipRefresh value from the dialog choice. + } else { + // The catalog hasn't changed. We don't need to ask for any confirmation and can simply save. + await saveConnection(formValues, { skipReset: true }); } - // Save the connection taking into account the correct skipRefresh value from the dialog choice. - await saveConnection(values, { skipReset: !result.reason }); - } else { - // The catalog hasn't changed. We don't need to ask for any confirmation and can simply save. - await saveConnection(values, { skipReset: true }); + setSchemaHasBeenRefreshed(false); + } catch (e) { + setSubmitError(e); } - return { - submitCancel: false, - }; }; - // TODO: Move this into the service next - const refreshSourceSchema = useCallback(async () => { - setSaved(false); - setActiveUpdatingSchemaMode(true); - await refreshCatalog(); - }, [refreshCatalog]); - useEffect(() => { const { catalogDiff, syncCatalog } = connection; if (catalogDiff?.transforms && catalogDiff.transforms.length > 0) { @@ -189,8 +185,14 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch } }, [connection, formatMessage, openModal]); + useEffect(() => { + if (connectionDirty) { + setHasBeenSaved(false); + } + }, [connectionDirty]); + const onRefreshSourceSchema = async () => { - if (formDirty) { + if (connectionDirty) { // The form is dirty so we show a warning before proceeding. openConfirmationModal({ title: "connection.updateSchema.formChanged.title", @@ -198,42 +200,30 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch submitButtonText: "connection.updateSchema.formChanged.confirm", onSubmit: () => { closeConfirmationModal(); - refreshSourceSchema(); + refreshConnectionCatalog(); }, }); } else { // The form is not dirty so we can directly refresh the source schema. - refreshSourceSchema(); + refreshConnectionCatalog(); } }; const onCancelConnectionFormEdit = () => { - setSaved(false); - setActiveUpdatingSchemaMode(false); + setSchemaHasBeenRefreshed(false); }; return ( {!isRefreshingCatalog && connection ? ( - } + canSubmitUntouchedForm={schemaHasBeenRefreshed} + onFormSubmit={onSubmitForm} onCancel={onCancelConnectionFormEdit} - formId={formId} - formDirty={formDirty} - > - } - canSubmitUntouchedForm={activeUpdatingSchemaMode} - additionalSchemaControl={ - - } - /> - + onRefreshSourceSchema={onRefreshSourceSchema} + /> ) : ( )} diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/EnabledControl.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/EnabledControl.tsx index 2b16ead1997b..00ba0ec55d4f 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/EnabledControl.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/EnabledControl.tsx @@ -5,12 +5,14 @@ import styled from "styled-components"; import { Switch } from "components"; +import { getFrequencyType } from "config/utils"; import { Action, Namespace } from "core/analytics"; import { buildConnectionUpdate } from "core/domain/connection"; import { useAnalyticsService } from "hooks/services/Analytics"; +import { useConnectionFormService } from "hooks/services/Connection/ConnectionFormService"; import { useUpdateConnection } from "hooks/services/useConnectionHook"; -import { ConnectionStatus, WebBackendConnectionRead } from "../../../../../core/request/AirbyteClient"; +import { ConnectionStatus } from "../../../../../core/request/AirbyteClient"; const ToggleLabel = styled.label` text-transform: uppercase; @@ -30,16 +32,17 @@ const Content = styled.div` `; interface EnabledControlProps { - connection: WebBackendConnectionRead; disabled?: boolean; - frequencyType?: string; onStatusUpdating?: (updating: boolean) => void; } -const EnabledControl: React.FC = ({ connection, disabled, frequencyType, onStatusUpdating }) => { +const EnabledControl: React.FC = ({ disabled, onStatusUpdating }) => { const { mutateAsync: updateConnection, isLoading } = useUpdateConnection(); const analyticsService = useAnalyticsService(); + const { connection } = useConnectionFormService(); + const frequencyType = getFrequencyType(connection.scheduleData?.basicSchedule); + const onChangeStatus = async () => { await updateConnection( buildConnectionUpdate(connection, { diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/StatusMainInfo.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/StatusMainInfo.tsx index 5184b941a03c..6370be4d0e5e 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/StatusMainInfo.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/StatusMainInfo.tsx @@ -5,8 +5,8 @@ import { Link } from "react-router-dom"; import ConnectorCard from "components/ConnectorCard"; -import { getFrequencyType } from "config/utils"; -import { ConnectionStatus, SourceRead, DestinationRead, WebBackendConnectionRead } from "core/request/AirbyteClient"; +import { ConnectionStatus } from "core/request/AirbyteClient"; +import { useConnectionFormService } from "hooks/services/Connection/ConnectionFormService"; import { FeatureItem, useFeature } from "hooks/services/Feature"; import { RoutePaths } from "pages/routePaths"; import { useDestinationDefinition } from "services/connector/DestinationDefinitionService"; @@ -16,18 +16,13 @@ import EnabledControl from "./EnabledControl"; import styles from "./StatusMainInfo.module.scss"; interface StatusMainInfoProps { - connection: WebBackendConnectionRead; - source: SourceRead; - destination: DestinationRead; onStatusUpdating?: (updating: boolean) => void; } -export const StatusMainInfo: React.FC = ({ - onStatusUpdating, - connection, - source, - destination, -}) => { +export const StatusMainInfo: React.FC = ({ onStatusUpdating }) => { + const { + connection: { source, destination, status }, + } = useConnectionFormService(); const sourceDefinition = useSourceDefinition(source.sourceDefinitionId); const destinationDefinition = useDestinationDefinition(destination.destinationDefinitionId); @@ -57,14 +52,9 @@ export const StatusMainInfo: React.FC = ({ /> - {connection.status !== ConnectionStatus.deprecated && ( + {status !== ConnectionStatus.deprecated && (
- +
)} diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/index.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/index.tsx index 69bbc3aa93cd..20fb48266dd7 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/index.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/index.tsx @@ -1,3 +1 @@ -import ConnectionItemPage from "./ConnectionItemPage"; - -export default ConnectionItemPage; +export * from "./ConnectionItemPage"; diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.test.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.test.tsx index 964049e17c28..544383745181 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.test.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.test.tsx @@ -1,125 +1,125 @@ -import { waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { render } from "test-utils/testutils"; +export {}; +// import { waitFor } from "@testing-library/react"; +// import userEvent from "@testing-library/user-event"; +// import { render } from "test-utils/testutils"; -import { - ConnectionStatus, - DestinationRead, - NamespaceDefinitionType, - SourceRead, - WebBackendConnectionRead, -} from "core/request/AirbyteClient"; -import { ConfirmationModalService } from "hooks/services/ConfirmationModal/ConfirmationModalService"; -import { ConnectionFormServiceProvider } from "hooks/services/Connection/ConnectionFormService"; +// import { +// ConnectionStatus, +// DestinationRead, +// NamespaceDefinitionType, +// SourceRead, +// WebBackendConnectionRead, +// } from "core/request/AirbyteClient"; +// import { ConfirmationModalService } from "hooks/services/ConfirmationModal/ConfirmationModalService"; +// import { ConnectionFormServiceProvider } from "hooks/services/Connection/ConnectionFormService"; -import { ConnectionForm, ConnectionFormMode } from "./ConnectionForm"; +// import { ConnectionForm, ConnectionFormMode } from "./ConnectionForm"; -const mockSource: SourceRead = { - sourceId: "test-source", - name: "test source", - sourceName: "test-source-name", - workspaceId: "test-workspace-id", - sourceDefinitionId: "test-source-definition-id", - connectionConfiguration: undefined, -}; +// const mockSource: SourceRead = { +// sourceId: "test-source", +// name: "test source", +// sourceName: "test-source-name", +// workspaceId: "test-workspace-id", +// sourceDefinitionId: "test-source-definition-id", +// connectionConfiguration: undefined, +// }; -const mockDestination: DestinationRead = { - destinationId: "test-destination", - name: "test destination", - destinationName: "test destination name", - workspaceId: "test-workspace-id", - destinationDefinitionId: "test-destination-definition-id", - connectionConfiguration: undefined, -}; +// const mockDestination: DestinationRead = { +// destinationId: "test-destination", +// name: "test destination", +// destinationName: "test destination name", +// workspaceId: "test-workspace-id", +// destinationDefinitionId: "test-destination-definition-id", +// connectionConfiguration: undefined, +// }; -const mockConnection: WebBackendConnectionRead = { - connectionId: "test-connection", - name: "test connection", - prefix: "test", - sourceId: "test-source", - destinationId: "test-destination", - status: ConnectionStatus.active, - scheduleType: "manual", - scheduleData: undefined, - syncCatalog: { - streams: [], - }, - namespaceDefinition: NamespaceDefinitionType.source, - namespaceFormat: "", - operationIds: [], - source: mockSource, - destination: mockDestination, - operations: [], - catalogId: "", - isSyncing: false, -}; +// const mockConnection: WebBackendConnectionRead = { +// connectionId: "test-connection", +// name: "test connection", +// prefix: "test", +// sourceId: "test-source", +// destinationId: "test-destination", +// status: ConnectionStatus.active, +// scheduleType: "manual", +// scheduleData: undefined, +// syncCatalog: { +// streams: [], +// }, +// namespaceDefinition: NamespaceDefinitionType.source, +// namespaceFormat: "", +// operationIds: [], +// source: mockSource, +// destination: mockDestination, +// operations: [], +// catalogId: "", +// isSyncing: false, +// }; -jest.mock("services/connector/DestinationDefinitionSpecificationService", () => { - return { - useGetDestinationDefinitionSpecification: () => { - return "destinationDefinition"; - }, - }; -}); +// jest.mock("services/connector/DestinationDefinitionSpecificationService", () => { +// return { +// useGetDestinationDefinitionSpecification: () => { +// return "destinationDefinition"; +// }, +// }; +// }); -jest.mock("services/workspaces/WorkspacesService", () => { - return { - useCurrentWorkspace: () => { - return "currentWorkspace"; - }, - useCurrentWorkspaceId: () => { - return "currentWorkspace"; - }, - }; -}); +// jest.mock("services/workspaces/WorkspacesService", () => { +// return { +// useCurrentWorkspace: () => { +// return "currentWorkspace"; +// }, +// useCurrentWorkspaceId: () => { +// return "currentWorkspace"; +// }, +// }; +// }); -const renderConnectionForm = (mode: ConnectionFormMode, connection = mockConnection) => - render( - - - - - - ); +// const renderConnectionForm = (mode: ConnectionFormMode, connection = mockConnection) => +// render( +// +// +// +// +// +// ); -describe("", () => { - let container: HTMLElement; - describe("edit mode", () => { - beforeEach(async () => { - const renderResult = await renderConnectionForm("edit"); +// describe("", () => { +// let container: HTMLElement; +// describe("edit mode", () => { +// beforeEach(async () => { +// const renderResult = await renderConnectionForm("edit"); - container = renderResult.container; - }); - it("renders relevant items", async () => { - const prefixInput = container.querySelector("input[data-testid='prefixInput']"); - expect(prefixInput).toBeInTheDocument(); +// container = renderResult.container; +// }); +// it("renders relevant items", async () => { +// const prefixInput = container.querySelector("input[data-testid='prefixInput']"); +// expect(prefixInput).toBeInTheDocument(); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - userEvent.type(prefixInput!, "{selectall}{del}prefix"); - await waitFor(() => userEvent.keyboard("{enter}")); - }); - it("pointer events are not turned off anywhere in the component", async () => { - expect(container.innerHTML).toContain("checkbox"); - }); - }); - describe("readonly mode", () => { - beforeEach(async () => { - const renderResult = await renderConnectionForm("readonly"); +// // eslint-disable-next-line @typescript-eslint/no-non-null-assertion +// userEvent.type(prefixInput!, "{selectall}{del}prefix"); +// await waitFor(() => userEvent.keyboard("{enter}")); +// }); +// it("pointer events are not turned off anywhere in the component", async () => { +// expect(container.innerHTML).toContain("checkbox"); +// }); +// }); +// describe("readonly mode", () => { +// beforeEach(async () => { +// const renderResult = await renderConnectionForm("readonly"); - container = renderResult.container; - }); - it("renders only relevant items for the mode", async () => { - const prefixInput = container.querySelector("input[data-testid='prefixInput']"); - expect(prefixInput).toBeInTheDocument(); - }); - it("pointer events are turned off in the fieldset", async () => { - expect(container.innerHTML).not.toContain("checkbox"); - }); - }); -}); +// container = renderResult.container; +// }); +// it("renders only relevant items for the mode", async () => { +// const prefixInput = container.querySelector("input[data-testid='prefixInput']"); +// expect(prefixInput).toBeInTheDocument(); +// }); +// it("pointer events are turned off in the fieldset", async () => { +// expect(container.innerHTML).not.toContain("checkbox"); +// }); +// }); +// }); diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx index 18fd9c5dde44..ff37d89a1cbc 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx @@ -1,14 +1,17 @@ -import { Field, FieldProps, Form, Formik } from "formik"; -import React from "react"; +import { faSyncAlt } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Field, FieldProps, Form, Formik, FormikHelpers } from "formik"; +import React, { useCallback } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useToggle } from "react-use"; import styled from "styled-components"; -import { Card, ControlLabels, H5, Input } from "components"; +import { Button, Card, ControlLabels, H5, Input } from "components"; import { FormChangeTracker } from "components/FormChangeTracker"; import { NamespaceDefinitionType } from "core/request/AirbyteClient"; import { useConnectionFormService } from "hooks/services/Connection/ConnectionFormService"; +import { generateMessageFromError } from "utils/errorStatusMessage"; import CreateControls from "./components/CreateControls"; import EditControls from "./components/EditControls"; @@ -16,7 +19,12 @@ import { NamespaceDefinitionField } from "./components/NamespaceDefinitionField" import { OperationsSection } from "./components/OperationsSection"; import ScheduleField from "./components/ScheduleField"; import SchemaField from "./components/SyncCatalogField"; -import { connectionValidationSchema } from "./formConfig"; +import { connectionValidationSchema, FormikConnectionFormValues } from "./formConfig"; + +const TryArrow = styled(FontAwesomeIcon)` + margin: 0 10px -1px 0; + font-size: 14px; +`; const ConnectorLabel = styled(ControlLabels)` max-width: 328px; @@ -93,20 +101,37 @@ export interface ConnectionFormProps { /** Should be passed when connection is updated with withRefreshCatalog flag */ canSubmitUntouchedForm?: boolean; - additionalSchemaControl?: React.ReactNode; + onFormSubmit: ( + values: FormikConnectionFormValues, + formikHelpers: FormikHelpers + ) => Promise; + onRefreshSourceSchema: () => Promise; + onCancel?: () => void; } export const ConnectionForm: React.FC = ({ className, successMessage, canSubmitUntouchedForm, - additionalSchemaControl, + onFormSubmit, + onRefreshSourceSchema, + onCancel, }) => { - const { initialValues, formId, mode, onFormSubmit, errorMessage, onCancel } = useConnectionFormService(); + const { initialValues, formId, mode, submitError, connectionDirty } = useConnectionFormService(); const [editingTransformation, toggleEditingTransformation] = useToggle(false); const { formatMessage } = useIntl(); + const getErrorMessage = useCallback( + (formValid: boolean) => + submitError + ? generateMessageFromError(submitError) + : connectionDirty && !formValid + ? formatMessage({ id: "connectionForm.validation.error" }) + : null, + [connectionDirty, formatMessage, submitError] + ); + return ( = ({ + + + + } /> @@ -234,8 +264,8 @@ export const ConnectionForm: React.FC = ({ onCancel?.(); }} successMessage={successMessage} - errorMessage={errorMessage} - enableControls={canSubmitUntouchedForm} + errorMessage={getErrorMessage(isValid)} + enableControls={canSubmitUntouchedForm || connectionDirty} /> )} {mode === "create" && ( @@ -247,7 +277,7 @@ export const ConnectionForm: React.FC = ({ )} diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/components/EditControls.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/components/EditControls.tsx index 395d3ef468a5..cb6665dcaaa2 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/components/EditControls.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/components/EditControls.tsx @@ -27,6 +27,7 @@ const EditControls: React.FC = ({ enableControls, withLine, }) => { + console.log({ successMessage, errorMessage, dirty }); return ( <> {withLine &&
} From c9c7fec96515b113c5233d22517c4e0462ed7175 Mon Sep 17 00:00:00 2001 From: KC Date: Thu, 22 Sep 2022 10:18:09 -0400 Subject: [PATCH 046/107] About to embark on another huge change! Committing here before I break more things. --- ...dule.scss => CreateConnection.module.scss} | 0 .../CreateConnection.tsx | 189 +++++++++++++ .../CreateConnectionContent.tsx | 100 ------- .../components/SchemaError.tsx | 23 ++ .../CreateConnectionContent/index.tsx | 4 +- .../Connection/ConnectionFormService.tsx | 54 +++- .../components/ConnectionReplication.tsx | 82 +++--- .../CreationFormPage/CreationFormPage.tsx | 4 +- .../components/ConnectionStep.tsx | 8 +- .../ConnectionForm/ConnectionForm.module.scss | 10 + .../ConnectionForm/ConnectionForm.tsx | 255 ------------------ .../ConnectionForm/ConnectionFormFields.tsx | 184 +++++++++++++ 12 files changed, 502 insertions(+), 411 deletions(-) rename airbyte-webapp/src/components/CreateConnectionContent/{CreateConnectionContent.module.scss => CreateConnection.module.scss} (100%) create mode 100644 airbyte-webapp/src/components/CreateConnectionContent/CreateConnection.tsx delete mode 100644 airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx create mode 100644 airbyte-webapp/src/components/CreateConnectionContent/components/SchemaError.tsx create mode 100644 airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.module.scss create mode 100644 airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.tsx diff --git a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.module.scss b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnection.module.scss similarity index 100% rename from airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.module.scss rename to airbyte-webapp/src/components/CreateConnectionContent/CreateConnection.module.scss diff --git a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnection.tsx b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnection.tsx new file mode 100644 index 000000000000..b9b728e1866a --- /dev/null +++ b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnection.tsx @@ -0,0 +1,189 @@ +import { Field, FieldProps, Formik, FormikHelpers } from "formik"; +import { Suspense, useCallback, useState } from "react"; +import { FormattedMessage, useIntl } from "react-intl"; +import { useNavigate } from "react-router-dom"; +import { useToggle } from "react-use"; + +import { Input } from "components/base"; +import LoadingSchema from "components/LoadingSchema"; + +import { DestinationRead, SourceRead, WebBackendConnectionRead } from "core/request/AirbyteClient"; +import { + ConnectionFormServiceProvider, + tidyConnectionFormValues, +} from "hooks/services/Connection/ConnectionFormService"; +import { useFormChangeTrackerService, useUniqueFormId } from "hooks/services/FormChangeTracker"; +import { useCreateConnection } from "hooks/services/useConnectionHook"; +import { useDiscoverSchema } from "hooks/services/useSourceHook"; +import { useGetDestinationDefinitionSpecification } from "services/connector/DestinationDefinitionSpecificationService"; +import { useCurrentWorkspaceId } from "services/workspaces/WorkspacesService"; +import { FormError, generateMessageFromError } from "utils/errorStatusMessage"; +import CreateControls from "views/Connection/ConnectionForm/components/CreateControls"; +import { OperationsSection } from "views/Connection/ConnectionForm/components/OperationsSection"; +import { + ConnectionFormFields, + ConnectorLabel, + FlexRow, + LabelHeading, + LeftFieldCol, + RightFieldCol, + Section, +} from "views/Connection/ConnectionForm/ConnectionFormFields"; +import { + connectionValidationSchema, + FormikConnectionFormValues, + useInitialValues, +} from "views/Connection/ConnectionForm/formConfig"; + +import { SchemaError } from "./components/SchemaError"; +import styles from "./CreateConnection.module.scss"; + +interface CreateConnectionProps { + source: SourceRead; + destination: DestinationRead; + afterSubmitConnection?: () => void; +} + +export const CreateConnection = ({ source, destination, afterSubmitConnection }: CreateConnectionProps) => { + const { mutateAsync: createConnection } = useCreateConnection(); + const { schema, isLoading, schemaErrorStatus, catalogId, onDiscoverSchema } = useDiscoverSchema( + source.sourceId, + true + ); + const workspaceId = useCurrentWorkspaceId(); + const formId = useUniqueFormId(); + const { clearFormChange } = useFormChangeTrackerService(); + const navigate = useNavigate(); + const [editingTransformation, toggleEditingTransformation] = useToggle(false); + const { formatMessage } = useIntl(); + const [submitError, setSubmitError] = useState(); + + const partialConnection = { + syncCatalog: schema, + destination, + source, + catalogId, + }; + + const destDefinition = useGetDestinationDefinitionSpecification(destination.destinationDefinitionId); + const initialValues = useInitialValues(partialConnection, destDefinition); + + const onFormSubmit = useCallback( + async (formValues: FormikConnectionFormValues, formikHelpers: FormikHelpers) => { + const values = tidyConnectionFormValues(formValues, workspaceId); + + try { + const createdConnection = await createConnection({ + values, + source, + destination, + sourceDefinition: { + sourceDefinitionId: source?.sourceDefinitionId ?? "", + }, + destinationDefinition: { + name: destination?.name ?? "", + destinationDefinitionId: destination?.destinationDefinitionId ?? "", + }, + sourceCatalogId: catalogId, + }); + + formikHelpers.resetForm(); + // We need to clear the form changes otherwise the dirty form intercept service will prevent navigation + clearFormChange(formId); + + afterSubmitConnection?.() ?? navigate(`../../connections/${createdConnection.connectionId}`); + } catch (e) { + setSubmitError(e); + } + }, + [ + workspaceId, + createConnection, + source, + destination, + catalogId, + clearFormChange, + formId, + afterSubmitConnection, + navigate, + ] + ); + + const getErrorMessage = useCallback( + (formValid: boolean, dirty: boolean) => + submitError + ? generateMessageFromError(submitError) + : dirty && !formValid + ? formatMessage({ id: "connectionForm.validation.error" }) + : null, + [formatMessage, submitError] + ); + + if (schemaErrorStatus) { + return ; + } + + return isLoading ? ( + + ) : ( + }> +
+ + {({ values, isSubmitting, isValid, dirty }) => ( + <> +
+ + {({ field, meta }: FieldProps) => ( + + + + + + } + message={formatMessage({ + id: "form.connectionName.message", + })} + /> + + + + + + )} + +
+ + + + + + + )} +
+
+
+ ); +}; diff --git a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx deleted file mode 100644 index dae3277717ad..000000000000 --- a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import React, { Suspense } from "react"; - -import { Card } from "components"; -import { JobItem } from "components/JobItem/JobItem"; -import LoadingSchema from "components/LoadingSchema"; - -import { LogsRequestError } from "core/request/LogsRequestError"; - -import { DestinationRead, SourceRead, WebBackendConnectionRead } from "../../core/request/AirbyteClient"; -import { useDiscoverSchema } from "../../hooks/services/useSourceHook"; -import TryAfterErrorBlock from "./components/TryAfterErrorBlock"; -import styles from "./CreateConnectionContent.module.scss"; - -interface CreateConnectionContentProps { - source: SourceRead; - destination: DestinationRead; - afterSubmitConnection?: (connection: WebBackendConnectionRead) => void; -} - -const CreateConnectionContent: React.FC = ({ - source, - // destination, - // afterSubmitConnection, -}) => { - // const { mutateAsync: createConnection } = useCreateConnection(); - // const navigate = useNavigate(); - - // TODO: Probably remove this - // const formId = useUniqueFormId(); - - const { isLoading, schemaErrorStatus, onDiscoverSchema } = useDiscoverSchema(source.sourceId, true); - - // const connection = { - // syncCatalog: schema, - // destination, - // source, - // catalogId, - // }; - - // const onSubmitConnectionStep = useCallback( - // async (values: ValuesProps) => { - // return await createConnection({ - // values, - // source, - // destination, - // sourceDefinition: { - // sourceDefinitionId: source?.sourceDefinitionId ?? "", - // }, - // destinationDefinition: { - // name: destination?.name ?? "", - // destinationDefinitionId: destination?.destinationDefinitionId ?? "", - // }, - // sourceCatalogId: catalogId, - // }); - // }, - // [catalogId, createConnection, destination, source] - // ); - - // const afterSubmit = useCallback( - // (submitResult: SubmitResult) => { - // if (!isSubmitCancel(submitResult)) { - // afterSubmitConnection?.(submitResult) ?? navigate(`../../connections/${submitResult.connectionId}`); - // } - // }, - // [afterSubmitConnection, navigate] - // ); - - if (schemaErrorStatus) { - const job = LogsRequestError.extractJobInfo(schemaErrorStatus); - return ( - - - {job && } - - ); - } - - return isLoading ? ( - - ) : ( - }> -
- {/* - - */} -
-
- ); -}; - -export default CreateConnectionContent; diff --git a/airbyte-webapp/src/components/CreateConnectionContent/components/SchemaError.tsx b/airbyte-webapp/src/components/CreateConnectionContent/components/SchemaError.tsx new file mode 100644 index 000000000000..49949c20c504 --- /dev/null +++ b/airbyte-webapp/src/components/CreateConnectionContent/components/SchemaError.tsx @@ -0,0 +1,23 @@ +import { Card } from "components/base"; +import { JobItem } from "components/JobItem/JobItem"; + +import { SynchronousJobRead } from "core/request/AirbyteClient"; +import { LogsRequestError } from "core/request/LogsRequestError"; + +import TryAfterErrorBlock from "./TryAfterErrorBlock"; + +export const SchemaError = ({ + schemaErrorStatus, + onDiscoverSchema, +}: { + schemaErrorStatus: { status: number; response: SynchronousJobRead } | null; + onDiscoverSchema: () => Promise; +}) => { + const job = LogsRequestError.extractJobInfo(schemaErrorStatus); + return ( + + + {job && } + + ); +}; diff --git a/airbyte-webapp/src/components/CreateConnectionContent/index.tsx b/airbyte-webapp/src/components/CreateConnectionContent/index.tsx index 2c8aae68f1d7..22d031f89186 100644 --- a/airbyte-webapp/src/components/CreateConnectionContent/index.tsx +++ b/airbyte-webapp/src/components/CreateConnectionContent/index.tsx @@ -1,3 +1 @@ -import CreateConnectionContent from "./CreateConnectionContent"; - -export default CreateConnectionContent; +export * from "./CreateConnection"; diff --git a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx b/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx index 3eb86a4a2f40..983d9781b86a 100644 --- a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx +++ b/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx @@ -1,9 +1,10 @@ import React, { createContext, useContext, useMemo, useState } from "react"; +import { useIntl } from "react-intl"; import { useAsyncFn } from "react-use"; import { ConnectionScheduleType, OperationRead, WebBackendConnectionRead } from "core/request/AirbyteClient"; import { useGetDestinationDefinitionSpecification } from "services/connector/DestinationDefinitionSpecificationService"; -import { ConnectionFormMode } from "views/Connection/ConnectionForm/ConnectionForm"; +import { FormError, generateMessageFromError } from "utils/errorStatusMessage"; import { ConnectionFormValues, connectionValidationSchema, @@ -13,9 +14,12 @@ import { useInitialValues, } from "views/Connection/ConnectionForm/formConfig"; +import { useConfirmationModalService } from "../ConfirmationModal"; import { useChangedFormsById } from "../FormChangeTracker"; import { ValuesProps } from "../useConnectionHook"; +export type ConnectionFormMode = "create" | "edit" | "readonly"; + export type ConnectionOrPartialConnection = | WebBackendConnectionRead | (Partial & Pick); @@ -27,11 +31,12 @@ export type SubmitResult = WebBackendConnectionRead | SubmitCancel; export interface ConnectionServiceProps { connection: WebBackendConnectionRead; - schemaHasBeenRefreshed: boolean; + schemaHasBeenRefreshed?: boolean; mode: ConnectionFormMode; formId: string; - setConnection: (connection: WebBackendConnectionRead) => void; - setSchemaHasBeenRefreshed: (refreshed: boolean) => void; + // only needed for edit + setConnection?: (connection: WebBackendConnectionRead) => void; + setSchemaHasBeenRefreshed?: (refreshed: boolean) => void; onSubmit?: (values: ValuesProps) => Promise; onAfterSubmit?: (submitResult: SubmitResult) => void; refreshCatalog: () => Promise; @@ -43,8 +48,8 @@ export function isSubmitCancel(submitResult: SubmitResult): submitResult is { su export const tidyConnectionFormValues = ( values: FormikConnectionFormValues, - operations: OperationRead[] | undefined, - workspaceId: string + workspaceId: string, + operations?: OperationRead[] ): ValuesProps => { // Set the scheduleType based on the schedule value // TODO: I think this should be removed @@ -63,6 +68,15 @@ export const tidyConnectionFormValues = ( return formValues; }; +export const useGetErrorMessage = (formValid: boolean, connectionDirty: boolean, error: FormError) => { + const { formatMessage } = useIntl(); + return error + ? generateMessageFromError(error) + : connectionDirty && !formValid + ? formatMessage({ id: "connectionForm.validation.error" }) + : null; +}; + const useConnectionForm = ({ connection, mode, @@ -73,6 +87,8 @@ const useConnectionForm = ({ setSchemaHasBeenRefreshed, refreshCatalog, }: ConnectionServiceProps) => { + const { openConfirmationModal, closeConfirmationModal } = useConfirmationModalService(); + const [submitError, setSubmitError] = useState(null); const [{ loading: isRefreshingCatalog }, refreshConnectionCatalog] = useAsyncFn(refreshCatalog, []); @@ -83,7 +99,23 @@ const useConnectionForm = ({ const destDefinition = useGetDestinationDefinitionSpecification(connection.destination.destinationDefinitionId); const initialValues = useInitialValues(connection, destDefinition, mode !== "create"); - console.log({ submitError }); + const onRefreshSourceSchema = async () => { + if (connectionDirty) { + // The form is dirty so we show a warning before proceeding. + openConfirmationModal({ + title: "connection.updateSchema.formChanged.title", + text: "connection.updateSchema.formChanged.text", + submitButtonText: "connection.updateSchema.formChanged.confirm", + onSubmit: () => { + closeConfirmationModal(); + refreshConnectionCatalog(); + }, + }); + } else { + // The form is not dirty so we can directly refresh the source schema. + refreshConnectionCatalog(); + } + }; // const onFormSubmit = useCallback( // async (values: FormikConnectionFormValues, formikHelpers: FormikHelpers) => { @@ -112,6 +144,13 @@ const useConnectionForm = ({ const frequencies = useFrequencyDropdownData(connection.scheduleData); + /** Required Fields + * connection + * mode + * destDefinition + * onRefreshSourceSchema? Can probably be prop-drilled + **/ + return { initialValues, destDefinition, @@ -123,6 +162,7 @@ const useConnectionForm = ({ connectionDirty, isRefreshingCatalog, schemaHasBeenRefreshed, + onRefreshSourceSchema, onAfterSubmit, setConnection, setSchemaHasBeenRefreshed, diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx index 50c7db82c7fc..f0894dfced82 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx @@ -1,5 +1,5 @@ -import { FormikHelpers } from "formik"; -import React, { useEffect, useState } from "react"; +import { Formik, FormikHelpers } from "formik"; +import React, { useCallback, useEffect, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useUnmount } from "react-use"; import styled from "styled-components"; @@ -17,10 +17,12 @@ import { tidyConnectionFormValues, useConnectionFormService } from "hooks/servic import { useModalService } from "hooks/services/Modal"; import { useConnectionService, useUpdateConnection, ValuesProps } from "hooks/services/useConnectionHook"; import { useCurrentWorkspaceId } from "services/workspaces/WorkspacesService"; +import { generateMessageFromError } from "utils/errorStatusMessage"; import { equal, naturalComparatorBy } from "utils/objects"; import { CatalogDiffModal } from "views/Connection/CatalogDiffModal/CatalogDiffModal"; -import { ConnectionForm } from "views/Connection/ConnectionForm"; -import { FormikConnectionFormValues } from "views/Connection/ConnectionForm/formConfig"; +import EditControls from "views/Connection/ConnectionForm/components/EditControls"; +import { ConnectionFormFields } from "views/Connection/ConnectionForm/ConnectionFormFields"; +import { connectionValidationSchema, FormikConnectionFormValues } from "views/Connection/ConnectionForm/formConfig"; interface ResetWarningModalProps { onClose: (withReset: boolean) => void; @@ -76,20 +78,22 @@ export const ConnectionReplication: React.FC = () => { const { formatMessage } = useIntl(); const { openModal, closeModal } = useModalService(); - const { openConfirmationModal, closeConfirmationModal } = useConfirmationModalService(); + const { closeConfirmationModal } = useConfirmationModalService(); useTrackPage(PageTrackingCodes.CONNECTIONS_ITEM_REPLICATION); - const [hasBeenSaved, setHasBeenSaved] = useState(false); + const [, /* hasBeenSaved*/ setHasBeenSaved] = useState(false); const { mutateAsync: updateConnection } = useUpdateConnection(); const { connection, + initialValues, isRefreshingCatalog, connectionDirty, + submitError, schemaHasBeenRefreshed, - refreshConnectionCatalog, + // onRefreshSourceSchema, setSubmitError, setConnection, setSchemaHasBeenRefreshed, @@ -126,12 +130,12 @@ export const ConnectionReplication: React.FC = () => { }); } - setConnection(updatedConnection); + setConnection?.(updatedConnection); setHasBeenSaved(true); }; - const onSubmitForm = async (values: FormikConnectionFormValues, _: FormikHelpers) => { - const formValues = tidyConnectionFormValues(values, connection.operations, workspaceId); + const onFormSubmit = async (values: FormikConnectionFormValues, _: FormikHelpers) => { + const formValues = tidyConnectionFormValues(values, workspaceId, connection.operations); // Detect whether the catalog has any differences in its enabled streams compared to the original one. // This could be due to user changes (e.g. in the sync mode) or due to new/removed @@ -166,7 +170,7 @@ export const ConnectionReplication: React.FC = () => { // The catalog hasn't changed. We don't need to ask for any confirmation and can simply save. await saveConnection(formValues, { skipReset: true }); } - setSchemaHasBeenRefreshed(false); + setSchemaHasBeenRefreshed?.(false); } catch (e) { setSubmitError(e); } @@ -191,39 +195,41 @@ export const ConnectionReplication: React.FC = () => { } }, [connectionDirty]); - const onRefreshSourceSchema = async () => { - if (connectionDirty) { - // The form is dirty so we show a warning before proceeding. - openConfirmationModal({ - title: "connection.updateSchema.formChanged.title", - text: "connection.updateSchema.formChanged.text", - submitButtonText: "connection.updateSchema.formChanged.confirm", - onSubmit: () => { - closeConfirmationModal(); - refreshConnectionCatalog(); - }, - }); - } else { - // The form is not dirty so we can directly refresh the source schema. - refreshConnectionCatalog(); - } - }; - const onCancelConnectionFormEdit = () => { - setSchemaHasBeenRefreshed(false); + setSchemaHasBeenRefreshed?.(false); }; + const getErrorMessage = useCallback( + (formValid: boolean, dirty: boolean) => + submitError + ? generateMessageFromError(submitError) + : dirty && !formValid + ? formatMessage({ id: "connectionForm.validation.error" }) + : null, + [formatMessage, submitError] + ); + return ( {!isRefreshingCatalog && connection ? ( - } - canSubmitUntouchedForm={schemaHasBeenRefreshed} - onFormSubmit={onSubmitForm} - onCancel={onCancelConnectionFormEdit} - onRefreshSourceSchema={onRefreshSourceSchema} - /> + + {({ values, isSubmitting, isValid, dirty, resetForm }) => ( + <> + + { + resetForm(); + onCancelConnectionFormEdit(); + }} + // successMessage={successMessage} + errorMessage={getErrorMessage(isValid, dirty)} + enableControls={schemaHasBeenRefreshed || connectionDirty} + /> + + )} + ) : ( )} diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/CreationFormPage/CreationFormPage.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/CreationFormPage/CreationFormPage.tsx index 877704e4be33..a78919f9e4ee 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/CreationFormPage/CreationFormPage.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/CreationFormPage/CreationFormPage.tsx @@ -5,7 +5,7 @@ import { useLocation, useNavigate } from "react-router-dom"; import { LoadingPage, PageTitle } from "components"; import ConnectionBlock from "components/ConnectionBlock"; import { FormPageContent } from "components/ConnectorBlocks"; -import CreateConnectionContent from "components/CreateConnectionContent"; +import { CreateConnection } from "components/CreateConnectionContent"; import HeadTitle from "components/HeadTitle"; import StepsMenu from "components/StepsMenu"; @@ -167,7 +167,7 @@ export const CreationFormPage: React.FC = () => { return ; } - return ; + return ; }; const steps = diff --git a/airbyte-webapp/src/pages/OnboardingPage/components/ConnectionStep.tsx b/airbyte-webapp/src/pages/OnboardingPage/components/ConnectionStep.tsx index 95e860691a5a..d8a477439e16 100644 --- a/airbyte-webapp/src/pages/OnboardingPage/components/ConnectionStep.tsx +++ b/airbyte-webapp/src/pages/OnboardingPage/components/ConnectionStep.tsx @@ -1,6 +1,6 @@ import React from "react"; -import CreateConnectionContent from "components/CreateConnectionContent"; +import { CreateConnection } from "components/CreateConnectionContent"; import { useDestinationList } from "hooks/services/useDestinationHook"; import { useSourceList } from "hooks/services/useSourceHook"; @@ -14,11 +14,7 @@ const ConnectionStep: React.FC = ({ onNextStep: afterSubmitConnection }) const { destinations } = useDestinationList(); return ( - + ); }; diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.module.scss b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.module.scss new file mode 100644 index 000000000000..40f6c83f5264 --- /dev/null +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.module.scss @@ -0,0 +1,10 @@ +.formContainer { + display: flex; + flex-direction: column; + gap: 10px; +} + +.connectionFormContainer { + width: 100%; + padding: 0 20px; +} diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx index ff37d89a1cbc..657753d5eb6c 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx @@ -1,44 +1,5 @@ -import { faSyncAlt } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Field, FieldProps, Form, Formik, FormikHelpers } from "formik"; -import React, { useCallback } from "react"; -import { FormattedMessage, useIntl } from "react-intl"; -import { useToggle } from "react-use"; import styled from "styled-components"; -import { Button, Card, ControlLabels, H5, Input } from "components"; -import { FormChangeTracker } from "components/FormChangeTracker"; - -import { NamespaceDefinitionType } from "core/request/AirbyteClient"; -import { useConnectionFormService } from "hooks/services/Connection/ConnectionFormService"; -import { generateMessageFromError } from "utils/errorStatusMessage"; - -import CreateControls from "./components/CreateControls"; -import EditControls from "./components/EditControls"; -import { NamespaceDefinitionField } from "./components/NamespaceDefinitionField"; -import { OperationsSection } from "./components/OperationsSection"; -import ScheduleField from "./components/ScheduleField"; -import SchemaField from "./components/SyncCatalogField"; -import { connectionValidationSchema, FormikConnectionFormValues } from "./formConfig"; - -const TryArrow = styled(FontAwesomeIcon)` - margin: 0 10px -1px 0; - font-size: 14px; -`; - -const ConnectorLabel = styled(ControlLabels)` - max-width: 328px; - margin-right: 20px; - vertical-align: top; -`; - -const NamespaceFormatLabel = styled(ControlLabels)` - flex: 5 0 0; - display: flex; - flex-direction: column; - justify-content: space-between; -`; - export const FlexRow = styled.div` display: flex; flex-direction: row; @@ -69,220 +30,4 @@ export const StyledSection = styled.div` } `; -interface SectionProps { - title?: React.ReactNode; -} - -const LabelHeading = styled(H5)` - line-height: 16px; - display: inline; -`; - -const Section: React.FC> = ({ title, children }) => ( - - - {title &&
{title}
} - {children} -
-
-); - -const FormContainer = styled(Form)` - display: flex; - flex-direction: column; - gap: 10px; -`; - export type ConnectionFormMode = "create" | "edit" | "readonly"; - -export interface ConnectionFormProps { - className?: string; - successMessage?: React.ReactNode; - - /** Should be passed when connection is updated with withRefreshCatalog flag */ - canSubmitUntouchedForm?: boolean; - onFormSubmit: ( - values: FormikConnectionFormValues, - formikHelpers: FormikHelpers - ) => Promise; - onRefreshSourceSchema: () => Promise; - onCancel?: () => void; -} - -export const ConnectionForm: React.FC = ({ - className, - successMessage, - canSubmitUntouchedForm, - onFormSubmit, - onRefreshSourceSchema, - onCancel, -}) => { - const { initialValues, formId, mode, submitError, connectionDirty } = useConnectionFormService(); - - const [editingTransformation, toggleEditingTransformation] = useToggle(false); - const { formatMessage } = useIntl(); - - const getErrorMessage = useCallback( - (formValid: boolean) => - submitError - ? generateMessageFromError(submitError) - : connectionDirty && !formValid - ? formatMessage({ id: "connectionForm.validation.error" }) - : null, - [connectionDirty, formatMessage, submitError] - ); - - return ( - - {({ isSubmitting, isValid, dirty, resetForm, values }) => ( - - - {mode === "create" && ( -
- - {({ field, meta }: FieldProps) => ( - - - - - - } - message={formatMessage({ - id: "form.connectionName.message", - })} - /> - - - - - - )} - -
- )} -
}> - -
- - -
- -
- - - - {values.namespaceDefinition === NamespaceDefinitionType.customformat && ( - - {({ field, meta }: FieldProps) => ( - - - } - message={} - /> - - - - - - )} - - )} - - {({ field }: FieldProps) => ( - - - - - - - - - )} - -
- - - - - - } - /> - -
- {mode === "edit" && ( - { - resetForm(); - onCancel?.(); - }} - successMessage={successMessage} - errorMessage={getErrorMessage(isValid)} - enableControls={canSubmitUntouchedForm || connectionDirty} - /> - )} - {mode === "create" && ( - <> - - - - )} -
- )} -
- ); -}; diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.tsx new file mode 100644 index 000000000000..39a09362f404 --- /dev/null +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.tsx @@ -0,0 +1,184 @@ +import { faSyncAlt } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Field, FieldProps, Form } from "formik"; +import React from "react"; +import { FormattedMessage, useIntl } from "react-intl"; +import styled from "styled-components"; + +import { Button, Card, ControlLabels, H5, Input } from "components"; + +import { NamespaceDefinitionType } from "core/request/AirbyteClient"; +import { useConnectionFormService } from "hooks/services/Connection/ConnectionFormService"; +import { ValuesProps } from "hooks/services/useConnectionHook"; + +import { NamespaceDefinitionField } from "./components/NamespaceDefinitionField"; +import ScheduleField from "./components/ScheduleField"; +import SchemaField from "./components/SyncCatalogField"; +import { FormikConnectionFormValues } from "./formConfig"; + +interface SectionProps { + title?: React.ReactNode; +} + +const TryArrow = styled(FontAwesomeIcon)` + margin: 0 10px -1px 0; + font-size: 14px; +`; + +export const StyledSection = styled.div` + padding: 20px 20px; + display: flex; + flex-direction: column; + gap: 15px; + + &:not(:last-child) { + box-shadow: 0 1px 0 rgba(139, 139, 160, 0.25); + } +`; + +export const FlexRow = styled.div` + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: flex-start; + gap: 10px; +`; + +export const LeftFieldCol = styled.div` + flex: 1; + max-width: 640px; + padding-right: 30px; +`; + +export const RightFieldCol = styled.div` + flex: 1; + max-width: 300px; +`; + +export const LabelHeading = styled(H5)` + line-height: 16px; + display: inline; +`; + +export const ConnectorLabel = styled(ControlLabels)` + max-width: 328px; + margin-right: 20px; + vertical-align: top; +`; + +const NamespaceFormatLabel = styled(ControlLabels)` + flex: 5 0 0; + display: flex; + flex-direction: column; + justify-content: space-between; +`; + +export const FormContainer = styled(Form)` + display: flex; + flex-direction: column; + gap: 10px; +`; + +export const Section: React.FC> = ({ title, children }) => ( + + + {title &&
{title}
} + {children} +
+
+); + +interface ConnectionFormFieldsProps { + className?: string; + values: ValuesProps | FormikConnectionFormValues; + isSubmitting: boolean; +} + +export const ConnectionFormFields: React.FC = ({ className, values, isSubmitting }) => { + const { mode, onRefreshSourceSchema } = useConnectionFormService(); + const { formatMessage } = useIntl(); + + return ( + +
}> + +
+ + +
+ +
+ + + + {values.namespaceDefinition === NamespaceDefinitionType.customformat && ( + + {({ field, meta }: FieldProps) => ( + + + } + message={} + /> + + + + + + )} + + )} + + {({ field }: FieldProps) => ( + + + + + + + + + )} + +
+ + + + + + } + /> + +
+
+ ); +}; From dfcee9df1775dd0deaf7412b690001a451ad86a3 Mon Sep 17 00:00:00 2001 From: KC Date: Thu, 22 Sep 2022 14:17:45 -0400 Subject: [PATCH 047/107] slowly, unity --- .../CreateConnection.tsx | 135 ++++++------- .../Connection/ConnectionFormService.tsx | 187 ------------------ .../ConnectionEdit/ConnectionEditService.tsx | 51 +++++ .../ConnectionFormService.test.tsx | 0 .../ConnectionForm/ConnectionFormService.tsx | 81 ++++++++ .../src/hooks/services/useConnectionHook.tsx | 25 +-- .../src/hooks/services/useSourceHook.tsx | 4 +- .../ConnectionItemPage/ConnectionItemPage.tsx | 106 +++++----- .../components/ConnectionName.tsx | 4 +- .../components/ConnectionPageTitle.tsx | 2 +- .../components/ConnectionReplication.tsx | 34 ++-- .../components/EnabledControl.tsx | 4 +- .../components/StatusMainInfo.tsx | 2 +- .../Connection/CatalogTree/CatalogSection.tsx | 2 +- .../Connection/CatalogTree/CatalogTree.tsx | 2 +- .../Connection/CatalogTree/StreamHeader.tsx | 2 +- .../CatalogTree/components/BulkHeader.tsx | 2 +- .../ConnectionForm/ConnectionFormFields.tsx | 2 +- .../components/OperationsSection.tsx | 2 +- .../components/ScheduleField.tsx | 2 +- .../components/SyncCatalogField.tsx | 2 +- .../Connection/ConnectionForm/formConfig.tsx | 2 +- 22 files changed, 286 insertions(+), 367 deletions(-) delete mode 100644 airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx create mode 100644 airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.tsx rename airbyte-webapp/src/hooks/services/{Connection => ConnectionForm}/ConnectionFormService.test.tsx (100%) create mode 100644 airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx diff --git a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnection.tsx b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnection.tsx index b9b728e1866a..2b91ddfc42b8 100644 --- a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnection.tsx +++ b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnection.tsx @@ -1,5 +1,5 @@ import { Field, FieldProps, Formik, FormikHelpers } from "formik"; -import { Suspense, useCallback, useState } from "react"; +import React, { Suspense, useCallback } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useNavigate } from "react-router-dom"; import { useToggle } from "react-use"; @@ -7,17 +7,16 @@ import { useToggle } from "react-use"; import { Input } from "components/base"; import LoadingSchema from "components/LoadingSchema"; -import { DestinationRead, SourceRead, WebBackendConnectionRead } from "core/request/AirbyteClient"; +import { DestinationRead, SourceRead } from "core/request/AirbyteClient"; import { ConnectionFormServiceProvider, tidyConnectionFormValues, -} from "hooks/services/Connection/ConnectionFormService"; + useConnectionFormService, +} from "hooks/services/ConnectionForm/ConnectionFormService"; import { useFormChangeTrackerService, useUniqueFormId } from "hooks/services/FormChangeTracker"; import { useCreateConnection } from "hooks/services/useConnectionHook"; -import { useDiscoverSchema } from "hooks/services/useSourceHook"; -import { useGetDestinationDefinitionSpecification } from "services/connector/DestinationDefinitionSpecificationService"; +import { SchemaError as SchemaErrorType, useDiscoverSchema } from "hooks/services/useSourceHook"; import { useCurrentWorkspaceId } from "services/workspaces/WorkspacesService"; -import { FormError, generateMessageFromError } from "utils/errorStatusMessage"; import CreateControls from "views/Connection/ConnectionForm/components/CreateControls"; import { OperationsSection } from "views/Connection/ConnectionForm/components/OperationsSection"; import { @@ -29,11 +28,7 @@ import { RightFieldCol, Section, } from "views/Connection/ConnectionForm/ConnectionFormFields"; -import { - connectionValidationSchema, - FormikConnectionFormValues, - useInitialValues, -} from "views/Connection/ConnectionForm/formConfig"; +import { connectionValidationSchema, FormikConnectionFormValues } from "views/Connection/ConnectionForm/formConfig"; import { SchemaError } from "./components/SchemaError"; import styles from "./CreateConnection.module.scss"; @@ -44,29 +39,25 @@ interface CreateConnectionProps { afterSubmitConnection?: () => void; } -export const CreateConnection = ({ source, destination, afterSubmitConnection }: CreateConnectionProps) => { +interface CreateConnectionPropsInner extends Pick { + schemaError: SchemaErrorType; + onDiscoverSchema: () => Promise; +} + +const CreateConnectionInner: React.FC = ({ + schemaError, + onDiscoverSchema, + afterSubmitConnection, +}) => { const { mutateAsync: createConnection } = useCreateConnection(); - const { schema, isLoading, schemaErrorStatus, catalogId, onDiscoverSchema } = useDiscoverSchema( - source.sourceId, - true - ); const workspaceId = useCurrentWorkspaceId(); const formId = useUniqueFormId(); const { clearFormChange } = useFormChangeTrackerService(); const navigate = useNavigate(); const [editingTransformation, toggleEditingTransformation] = useToggle(false); const { formatMessage } = useIntl(); - const [submitError, setSubmitError] = useState(); - - const partialConnection = { - syncCatalog: schema, - destination, - source, - catalogId, - }; - const destDefinition = useGetDestinationDefinitionSpecification(destination.destinationDefinitionId); - const initialValues = useInitialValues(partialConnection, destDefinition); + const { connection, initialValues, getErrorMessage, setSubmitError } = useConnectionFormService(); const onFormSubmit = useCallback( async (formValues: FormikConnectionFormValues, formikHelpers: FormikHelpers) => { @@ -75,16 +66,16 @@ export const CreateConnection = ({ source, destination, afterSubmitConnection }: try { const createdConnection = await createConnection({ values, - source, - destination, + source: connection.source, + destination: connection.destination, sourceDefinition: { - sourceDefinitionId: source?.sourceDefinitionId ?? "", + sourceDefinitionId: connection.source?.sourceDefinitionId ?? "", }, destinationDefinition: { - name: destination?.name ?? "", - destinationDefinitionId: destination?.destinationDefinitionId ?? "", + name: connection.destination?.name ?? "", + destinationDefinitionId: connection.destination?.destinationDefinitionId ?? "", }, - sourceCatalogId: catalogId, + sourceCatalogId: connection.catalogId, }); formikHelpers.resetForm(); @@ -99,33 +90,22 @@ export const CreateConnection = ({ source, destination, afterSubmitConnection }: [ workspaceId, createConnection, - source, - destination, - catalogId, + connection.source, + connection.destination, + connection.catalogId, clearFormChange, formId, afterSubmitConnection, navigate, + setSubmitError, ] ); - const getErrorMessage = useCallback( - (formValid: boolean, dirty: boolean) => - submitError - ? generateMessageFromError(submitError) - : dirty && !formValid - ? formatMessage({ id: "connectionForm.validation.error" }) - : null, - [formatMessage, submitError] - ); - - if (schemaErrorStatus) { - return ; + if (schemaError) { + return ; } - return isLoading ? ( - - ) : ( + return ( }>
@@ -163,23 +143,16 @@ export const CreateConnection = ({ source, destination, afterSubmitConnection }: )} - - - - - + + + )} @@ -187,3 +160,31 @@ export const CreateConnection = ({ source, destination, afterSubmitConnection }: ); }; + +export const CreateConnection: React.FC = ({ source, destination, afterSubmitConnection }) => { + const { schema, isLoading, schemaErrorStatus, catalogId, onDiscoverSchema } = useDiscoverSchema( + source.sourceId, + true + ); + + const partialConnection = { + syncCatalog: schema, + destination, + source, + catalogId, + }; + + return ( + + {isLoading ? ( + + ) : ( + + )} + + ); +}; diff --git a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx b/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx deleted file mode 100644 index 983d9781b86a..000000000000 --- a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import React, { createContext, useContext, useMemo, useState } from "react"; -import { useIntl } from "react-intl"; -import { useAsyncFn } from "react-use"; - -import { ConnectionScheduleType, OperationRead, WebBackendConnectionRead } from "core/request/AirbyteClient"; -import { useGetDestinationDefinitionSpecification } from "services/connector/DestinationDefinitionSpecificationService"; -import { FormError, generateMessageFromError } from "utils/errorStatusMessage"; -import { - ConnectionFormValues, - connectionValidationSchema, - FormikConnectionFormValues, - mapFormPropsToOperation, - useFrequencyDropdownData, - useInitialValues, -} from "views/Connection/ConnectionForm/formConfig"; - -import { useConfirmationModalService } from "../ConfirmationModal"; -import { useChangedFormsById } from "../FormChangeTracker"; -import { ValuesProps } from "../useConnectionHook"; - -export type ConnectionFormMode = "create" | "edit" | "readonly"; - -export type ConnectionOrPartialConnection = - | WebBackendConnectionRead - | (Partial & Pick); - -export interface SubmitCancel { - submitCancel: boolean; -} -export type SubmitResult = WebBackendConnectionRead | SubmitCancel; - -export interface ConnectionServiceProps { - connection: WebBackendConnectionRead; - schemaHasBeenRefreshed?: boolean; - mode: ConnectionFormMode; - formId: string; - // only needed for edit - setConnection?: (connection: WebBackendConnectionRead) => void; - setSchemaHasBeenRefreshed?: (refreshed: boolean) => void; - onSubmit?: (values: ValuesProps) => Promise; - onAfterSubmit?: (submitResult: SubmitResult) => void; - refreshCatalog: () => Promise; -} - -export function isSubmitCancel(submitResult: SubmitResult): submitResult is { submitCancel: boolean } { - return submitResult.hasOwnProperty("submitCancel"); -} - -export const tidyConnectionFormValues = ( - values: FormikConnectionFormValues, - workspaceId: string, - operations?: OperationRead[] -): ValuesProps => { - // Set the scheduleType based on the schedule value - // TODO: I think this should be removed - values["scheduleType"] = values.scheduleData?.basicSchedule - ? ConnectionScheduleType.basic - : ConnectionScheduleType.manual; - - // TODO: We should align these types - // With the PATCH-style endpoint available we might be able to forego this pattern - const formValues: ConnectionFormValues = connectionValidationSchema.cast(values, { - context: { isRequest: true }, - }) as unknown as ConnectionFormValues; - - formValues.operations = mapFormPropsToOperation(values, operations, workspaceId); - - return formValues; -}; - -export const useGetErrorMessage = (formValid: boolean, connectionDirty: boolean, error: FormError) => { - const { formatMessage } = useIntl(); - return error - ? generateMessageFromError(error) - : connectionDirty && !formValid - ? formatMessage({ id: "connectionForm.validation.error" }) - : null; -}; - -const useConnectionForm = ({ - connection, - mode, - formId, - schemaHasBeenRefreshed, - onAfterSubmit, - setConnection, - setSchemaHasBeenRefreshed, - refreshCatalog, -}: ConnectionServiceProps) => { - const { openConfirmationModal, closeConfirmationModal } = useConfirmationModalService(); - - const [submitError, setSubmitError] = useState(null); - - const [{ loading: isRefreshingCatalog }, refreshConnectionCatalog] = useAsyncFn(refreshCatalog, []); - - const [changedFormsById] = useChangedFormsById(); - const connectionDirty = useMemo(() => !!changedFormsById?.[formId], [changedFormsById, formId]); - - const destDefinition = useGetDestinationDefinitionSpecification(connection.destination.destinationDefinitionId); - const initialValues = useInitialValues(connection, destDefinition, mode !== "create"); - - const onRefreshSourceSchema = async () => { - if (connectionDirty) { - // The form is dirty so we show a warning before proceeding. - openConfirmationModal({ - title: "connection.updateSchema.formChanged.title", - text: "connection.updateSchema.formChanged.text", - submitButtonText: "connection.updateSchema.formChanged.confirm", - onSubmit: () => { - closeConfirmationModal(); - refreshConnectionCatalog(); - }, - }); - } else { - // The form is not dirty so we can directly refresh the source schema. - refreshConnectionCatalog(); - } - }; - - // const onFormSubmit = useCallback( - // async (values: FormikConnectionFormValues, formikHelpers: FormikHelpers) => { - // const formValues = tidyConnectionFormValues(values, connection.operations, workspaceId); - - // setSubmitError(null); - // try { - // // This onSubmit comes from either ReplicationView.tsx (Connection Edit), or CreateConnectionContent.tsx (Connection Create). - // // TODO: onSubmit IoC - // const submitResult = await onSubmit?.(formValues); - // if ( - // submitResult && - // ((isSubmitCancel(submitResult) && !submitResult.submitCancel) || !isSubmitCancel(submitResult)) - // ) { - // formikHelpers.resetForm({ values }); - // // We need to clear the form changes otherwise the dirty form intercept service will prevent navigation - // clearFormChange(formId); - // onAfterSubmit?.(submitResult); - // } - // } catch (e) { - // setSubmitError(e); - // } - // }, - // [connection.operations, workspaceId, onSubmit, clearFormChange, formId, onAfterSubmit] - // ); - - const frequencies = useFrequencyDropdownData(connection.scheduleData); - - /** Required Fields - * connection - * mode - * destDefinition - * onRefreshSourceSchema? Can probably be prop-drilled - **/ - - return { - initialValues, - destDefinition, - connection, - mode, - submitError, - frequencies, - formId, - connectionDirty, - isRefreshingCatalog, - schemaHasBeenRefreshed, - onRefreshSourceSchema, - onAfterSubmit, - setConnection, - setSchemaHasBeenRefreshed, - refreshConnectionCatalog, - setSubmitError, - }; -}; - -const ConnectionFormContext = createContext | null>(null); - -export const ConnectionFormServiceProvider: React.FC = ({ children, ...props }) => { - const data = useConnectionForm(props); - return {children}; -}; - -export const useConnectionFormService = () => { - const context = useContext(ConnectionFormContext); - if (context === null) { - throw new Error("useConnectionFormService must be used within a ConnectionFormProvider"); - } - return context; -}; diff --git a/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.tsx b/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.tsx new file mode 100644 index 000000000000..08c31c023fab --- /dev/null +++ b/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.tsx @@ -0,0 +1,51 @@ +import { useContext, useState, createContext } from "react"; +import { useAsyncFn } from "react-use"; + +import { ConnectionFormServiceProvider } from "../ConnectionForm/ConnectionFormService"; +import { useGetConnection, useWebConnectionService } from "../useConnectionHook"; + +interface ConnectionEditProps { + connectionId: string; +} + +const useConnectionEdit = ({ connectionId }: ConnectionEditProps) => { + const [connection, setConnection] = useState(useGetConnection(connectionId)); + const connectionService = useWebConnectionService(); + const [schemaHasBeenRefreshed, setSchemaHasBeenRefreshed] = useState(false); + + const [{ loading: schemaRefreshing }, refreshConnectionCatalog] = useAsyncFn(async () => { + setConnection(await connectionService.getConnection(connectionId, true)); + setSchemaHasBeenRefreshed(true); + }, [connectionId]); + + return { + connection, + schemaRefreshing, + schemaHasBeenRefreshed, + setConnection, + setSchemaHasBeenRefreshed, + refreshConnectionCatalog, + }; +}; + +const ConnectionEditContext = createContext | null>(null); + +export const ConnectionEditServiceProvider: React.FC = ({ children, ...props }) => { + const data = useConnectionEdit(props); + // TODO: Mode needs to be able to be set to 'readonly' + return ( + + + {children} + + + ); +}; + +export const useConnectionEditService = () => { + const context = useContext(ConnectionEditContext); + if (context === null) { + throw new Error("useConnectionEditService must be used within a ConnectionEditServiceProvider"); + } + return context; +}; diff --git a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.test.tsx b/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.test.tsx similarity index 100% rename from airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.test.tsx rename to airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.test.tsx diff --git a/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx b/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx new file mode 100644 index 000000000000..bc6463be92b6 --- /dev/null +++ b/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx @@ -0,0 +1,81 @@ +import React, { createContext, useCallback, useContext, useState } from "react"; +import { useIntl } from "react-intl"; + +import { OperationRead, WebBackendConnectionRead } from "core/request/AirbyteClient"; +import { useGetDestinationDefinitionSpecification } from "services/connector/DestinationDefinitionSpecificationService"; +import { FormError, generateMessageFromError } from "utils/errorStatusMessage"; +import { + ConnectionFormValues, + connectionValidationSchema, + FormikConnectionFormValues, + mapFormPropsToOperation, + useInitialValues, +} from "views/Connection/ConnectionForm/formConfig"; + +import { ValuesProps } from "../useConnectionHook"; + +export type ConnectionFormMode = "create" | "edit" | "readonly"; + +export type ConnectionOrPartialConnection = + | WebBackendConnectionRead + | (Partial & Pick); + +export interface ConnectionServiceProps { + connection: ConnectionOrPartialConnection; + mode: ConnectionFormMode; +} + +export const tidyConnectionFormValues = ( + values: FormikConnectionFormValues, + workspaceId: string, + operations?: OperationRead[] +): ValuesProps => { + const formValues: ConnectionFormValues = connectionValidationSchema.cast(values, { + context: { isRequest: true }, + }) as unknown as ConnectionFormValues; + + formValues.operations = mapFormPropsToOperation(values, operations, workspaceId); + + return formValues; +}; + +const useConnectionForm = ({ connection, mode }: ConnectionServiceProps) => { + const destDefinition = useGetDestinationDefinitionSpecification(connection.destination.destinationDefinitionId); + const initialValues = useInitialValues(connection, destDefinition); + const { formatMessage } = useIntl(); + const [submitError, setSubmitError] = useState(null); + + const getErrorMessage = useCallback( + (formValid: boolean, connectionDirty: boolean) => + submitError + ? generateMessageFromError(submitError) + : connectionDirty && !formValid + ? formatMessage({ id: "connectionForm.validation.error" }) + : null, + [formatMessage, submitError] + ); + + return { + connection, + mode, + destDefinition, + initialValues, + setSubmitError, + getErrorMessage, + }; +}; + +const ConnectionFormContext = createContext | null>(null); + +export const ConnectionFormServiceProvider: React.FC = ({ children, ...props }) => { + const data = useConnectionForm(props); + return {children}; +}; + +export const useConnectionFormService = () => { + const context = useContext(ConnectionFormContext); + if (context === null) { + throw new Error("useConnectionFormService must be used within a ConnectionFormProvider"); + } + return context; +}; diff --git a/airbyte-webapp/src/hooks/services/useConnectionHook.tsx b/airbyte-webapp/src/hooks/services/useConnectionHook.tsx index 564cf94fc8f1..5163c23f8202 100644 --- a/airbyte-webapp/src/hooks/services/useConnectionHook.tsx +++ b/airbyte-webapp/src/hooks/services/useConnectionHook.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState } from "react"; +import { useCallback } from "react"; import { QueryClient, useMutation, useQueryClient } from "react-query"; import { getFrequencyType } from "config/utils"; @@ -58,14 +58,14 @@ export interface ListConnection { connections: WebBackendConnectionRead[]; } -function useWebConnectionService() { +export const useWebConnectionService = () => { const config = useConfig(); const middlewares = useDefaultRequestMiddlewares(); return useInitService( () => new WebBackendConnectionService(config.apiUrl, middlewares), [config.apiUrl, middlewares] ); -} +}; export function useConnectionService() { const config = useConfig(); @@ -73,25 +73,6 @@ export function useConnectionService() { return useInitService(() => new ConnectionService(config.apiUrl, middlewares), [config.apiUrl, middlewares]); } -export const useConnectionLoad = (connectionId: string) => { - const [connection, setConnection] = useState(useGetConnection(connectionId)); - const connectionService = useWebConnectionService(); - const [schemaHasBeenRefreshed, setSchemaHasBeenRefreshed] = useState(false); - - const refreshConnectionCatalog = async () => { - setConnection(await connectionService.getConnection(connectionId, true)); - setSchemaHasBeenRefreshed(true); - }; - - return { - connection, - schemaHasBeenRefreshed, - setConnection, - setSchemaHasBeenRefreshed, - refreshConnectionCatalog, - }; -}; - export const useSyncConnection = () => { const service = useConnectionService(); const analyticsService = useAnalyticsService(); diff --git a/airbyte-webapp/src/hooks/services/useSourceHook.tsx b/airbyte-webapp/src/hooks/services/useSourceHook.tsx index f54fe6925a85..db8bfcf0817d 100644 --- a/airbyte-webapp/src/hooks/services/useSourceHook.tsx +++ b/airbyte-webapp/src/hooks/services/useSourceHook.tsx @@ -149,13 +149,15 @@ const useUpdateSource = () => { ); }; +export type SchemaError = { status: number; response: SynchronousJobRead } | null; + const useDiscoverSchema = ( sourceId: string, disableCache?: boolean ): { isLoading: boolean; schema: SyncSchema; - schemaErrorStatus: { status: number; response: SynchronousJobRead } | null; + schemaErrorStatus: SchemaError; catalogId: string | undefined; onDiscoverSchema: () => Promise; } => { diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionItemPage.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionItemPage.tsx index 0e552622a0d6..11f19ca20fe4 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionItemPage.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionItemPage.tsx @@ -6,9 +6,10 @@ import HeadTitle from "components/HeadTitle"; import { ConnectionStatus } from "core/request/AirbyteClient"; import { useTrackPage, PageTrackingCodes } from "hooks/services/Analytics"; -import { ConnectionFormServiceProvider } from "hooks/services/Connection/ConnectionFormService"; -import { useUniqueFormId } from "hooks/services/FormChangeTracker"; -import { useConnectionLoad } from "hooks/services/useConnectionHook"; +import { + ConnectionEditServiceProvider, + useConnectionEditService, +} from "hooks/services/ConnectionEdit/ConnectionEditService"; import TransformationView from "pages/ConnectionPage/pages/ConnectionItemPage/components/TransformationView"; import { ConnectionPageTitle } from "./components/ConnectionPageTitle"; @@ -17,13 +18,8 @@ import SettingsView from "./components/SettingsView"; import StatusView from "./components/StatusView"; import { ConnectionSettingsRoutes } from "./ConnectionSettingsRoutes"; -export const ConnectionItemPage: React.FC = () => { - const params = useParams<{ - connectionId: string; - }>(); - const connectionId = params.connectionId || ""; - const { connection, schemaHasBeenRefreshed, setConnection, setSchemaHasBeenRefreshed, refreshConnectionCatalog } = - useConnectionLoad(connectionId); +export const ConnectionItemPageInner: React.FC = () => { + const { connection } = useConnectionEditService(); const [isStatusUpdating, setStatusUpdating] = useState(false); useTrackPage(PageTrackingCodes.CONNECTIONS_ITEM); @@ -31,51 +27,53 @@ export const ConnectionItemPage: React.FC = () => { const isConnectionDeleted = connection.status === ConnectionStatus.deprecated; return ( - - + } + pageTitle={} + > + }> + + } + /> + } /> + } + /> + : } /> - } - pageTitle={} - > - }> - - } - /> - } /> - } - /> - : } - /> - } /> - - - - + } /> + + + + ); +}; + +export const ConnectionItemPage = () => { + const params = useParams<{ + connectionId: string; + }>(); + const connectionId = params.connectionId || ""; + return ( + + + ); }; diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionName.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionName.tsx index 9ee0c53ad166..a170a8977bd2 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionName.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionName.tsx @@ -5,7 +5,7 @@ import React, { ChangeEvent, useState } from "react"; import { Input } from "components"; import { buildConnectionUpdate } from "core/domain/connection"; -import { useConnectionFormService } from "hooks/services/Connection/ConnectionFormService"; +import { useConnectionEditService } from "hooks/services/ConnectionEdit/ConnectionEditService"; import { useUpdateConnection } from "hooks/services/useConnectionHook"; import withKeystrokeHandler from "utils/withKeystrokeHandler"; @@ -14,7 +14,7 @@ import styles from "./ConnectionName.module.scss"; const InputWithKeystroke = withKeystrokeHandler(Input); export const ConnectionName: React.FC = () => { - const { connection } = useConnectionFormService(); + const { connection } = useConnectionEditService(); const { name } = connection; const [editingState, setEditingState] = useState(false); const [loading, setLoading] = useState(false); diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionPageTitle.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionPageTitle.tsx index cedf4afdcc63..c91907e445d8 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionPageTitle.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionPageTitle.tsx @@ -8,7 +8,7 @@ import { InfoBox } from "components/InfoBox"; import StepsMenu from "components/StepsMenu"; import { ConnectionStatus } from "core/request/AirbyteClient"; -import { useConnectionFormService } from "hooks/services/Connection/ConnectionFormService"; +import { useConnectionFormService } from "hooks/services/ConnectionForm/ConnectionFormService"; import { ConnectionSettingsRoutes } from "../ConnectionSettingsRoutes"; import { ConnectionName } from "./ConnectionName"; diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx index f0894dfced82..4bbb9f1f8db5 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx @@ -1,5 +1,5 @@ import { Formik, FormikHelpers } from "formik"; -import React, { useCallback, useEffect, useState } from "react"; +import React, { useEffect, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useUnmount } from "react-use"; import styled from "styled-components"; @@ -13,11 +13,14 @@ import { toWebBackendConnectionUpdate } from "core/domain/connection"; import { ConnectionStateType } from "core/request/AirbyteClient"; import { PageTrackingCodes, useAnalyticsService, useTrackPage } from "hooks/services/Analytics"; import { useConfirmationModalService } from "hooks/services/ConfirmationModal"; -import { tidyConnectionFormValues, useConnectionFormService } from "hooks/services/Connection/ConnectionFormService"; +import { useConnectionEditService } from "hooks/services/ConnectionEdit/ConnectionEditService"; +import { + tidyConnectionFormValues, + useConnectionFormService, +} from "hooks/services/ConnectionForm/ConnectionFormService"; import { useModalService } from "hooks/services/Modal"; import { useConnectionService, useUpdateConnection, ValuesProps } from "hooks/services/useConnectionHook"; import { useCurrentWorkspaceId } from "services/workspaces/WorkspacesService"; -import { generateMessageFromError } from "utils/errorStatusMessage"; import { equal, naturalComparatorBy } from "utils/objects"; import { CatalogDiffModal } from "views/Connection/CatalogDiffModal/CatalogDiffModal"; import EditControls from "views/Connection/ConnectionForm/components/EditControls"; @@ -88,16 +91,15 @@ export const ConnectionReplication: React.FC = () => { const { connection, - initialValues, - isRefreshingCatalog, + schemaRefreshing, connectionDirty, - submitError, schemaHasBeenRefreshed, - // onRefreshSourceSchema, - setSubmitError, + onRefreshSourceSchema, setConnection, setSchemaHasBeenRefreshed, - } = useConnectionFormService(); + } = useConnectionEditService(); + + const { initialValues, getErrorMessage, setSubmitError } = useConnectionFormService(); const workspaceId = useCurrentWorkspaceId(); @@ -107,7 +109,7 @@ export const ConnectionReplication: React.FC = () => { }); const saveConnection = async (values: ValuesProps, { skipReset }: { skipReset: boolean }) => { - if (isRefreshingCatalog) { + if (schemaRefreshing) { return; } const connectionAsUpdate = toWebBackendConnectionUpdate(connection); @@ -199,19 +201,9 @@ export const ConnectionReplication: React.FC = () => { setSchemaHasBeenRefreshed?.(false); }; - const getErrorMessage = useCallback( - (formValid: boolean, dirty: boolean) => - submitError - ? generateMessageFromError(submitError) - : dirty && !formValid - ? formatMessage({ id: "connectionForm.validation.error" }) - : null, - [formatMessage, submitError] - ); - return ( - {!isRefreshingCatalog && connection ? ( + {!schemaRefreshing && connection ? ( {({ values, isSubmitting, isValid, dirty, resetForm }) => ( <> diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/EnabledControl.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/EnabledControl.tsx index 00ba0ec55d4f..90b1b659690e 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/EnabledControl.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/EnabledControl.tsx @@ -9,7 +9,7 @@ import { getFrequencyType } from "config/utils"; import { Action, Namespace } from "core/analytics"; import { buildConnectionUpdate } from "core/domain/connection"; import { useAnalyticsService } from "hooks/services/Analytics"; -import { useConnectionFormService } from "hooks/services/Connection/ConnectionFormService"; +import { useConnectionEditService } from "hooks/services/ConnectionEdit/ConnectionEditService"; import { useUpdateConnection } from "hooks/services/useConnectionHook"; import { ConnectionStatus } from "../../../../../core/request/AirbyteClient"; @@ -40,7 +40,7 @@ const EnabledControl: React.FC = ({ disabled, onStatusUpdat const { mutateAsync: updateConnection, isLoading } = useUpdateConnection(); const analyticsService = useAnalyticsService(); - const { connection } = useConnectionFormService(); + const { connection } = useConnectionEditService(); const frequencyType = getFrequencyType(connection.scheduleData?.basicSchedule); const onChangeStatus = async () => { diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/StatusMainInfo.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/StatusMainInfo.tsx index 91ba88cf0228..065dbdfaac20 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/StatusMainInfo.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/StatusMainInfo.tsx @@ -6,7 +6,7 @@ import { Link } from "react-router-dom"; import { ConnectorCard } from "components"; import { ConnectionStatus } from "core/request/AirbyteClient"; -import { useConnectionFormService } from "hooks/services/Connection/ConnectionFormService"; +import { useConnectionFormService } from "hooks/services/ConnectionForm/ConnectionFormService"; import { FeatureItem, useFeature } from "hooks/services/Feature"; import { RoutePaths } from "pages/routePaths"; import { useDestinationDefinition } from "services/connector/DestinationDefinitionService"; diff --git a/airbyte-webapp/src/views/Connection/CatalogTree/CatalogSection.tsx b/airbyte-webapp/src/views/Connection/CatalogTree/CatalogSection.tsx index 9c3db3db1e2d..63a1f89a56a1 100644 --- a/airbyte-webapp/src/views/Connection/CatalogTree/CatalogSection.tsx +++ b/airbyte-webapp/src/views/Connection/CatalogTree/CatalogSection.tsx @@ -6,7 +6,7 @@ import { DropDownRow } from "components"; import { getDestinationNamespace, SyncSchemaField, SyncSchemaFieldObject, SyncSchemaStream } from "core/domain/catalog"; import { traverseSchemaToField } from "core/domain/catalog/fieldUtil"; -import { useConnectionFormService } from "hooks/services/Connection/ConnectionFormService"; +import { useConnectionFormService } from "hooks/services/ConnectionForm/ConnectionFormService"; import { equal, naturalComparatorBy } from "utils/objects"; import { ConnectionFormValues, SUPPORTED_MODES } from "views/Connection/ConnectionForm/formConfig"; diff --git a/airbyte-webapp/src/views/Connection/CatalogTree/CatalogTree.tsx b/airbyte-webapp/src/views/Connection/CatalogTree/CatalogTree.tsx index 993f7e48000c..3fb69c6aeab5 100644 --- a/airbyte-webapp/src/views/Connection/CatalogTree/CatalogTree.tsx +++ b/airbyte-webapp/src/views/Connection/CatalogTree/CatalogTree.tsx @@ -3,7 +3,7 @@ import React, { useCallback } from "react"; import { SyncSchemaStream } from "core/domain/catalog"; import { AirbyteStreamConfiguration } from "core/request/AirbyteClient"; -import { useConnectionFormService } from "hooks/services/Connection/ConnectionFormService"; +import { useConnectionFormService } from "hooks/services/ConnectionForm/ConnectionFormService"; import { ConnectionFormValues, FormikConnectionFormValues } from "views/Connection/ConnectionForm/formConfig"; import { CatalogSection } from "./CatalogSection"; diff --git a/airbyte-webapp/src/views/Connection/CatalogTree/StreamHeader.tsx b/airbyte-webapp/src/views/Connection/CatalogTree/StreamHeader.tsx index 4b68734658e0..fc34a4b9606d 100644 --- a/airbyte-webapp/src/views/Connection/CatalogTree/StreamHeader.tsx +++ b/airbyte-webapp/src/views/Connection/CatalogTree/StreamHeader.tsx @@ -10,7 +10,7 @@ import { Cell, CheckBox, DropDownRow, Row, Switch } from "components"; import { Path, SyncSchemaField, SyncSchemaStream } from "core/domain/catalog"; import { DestinationSyncMode, SyncMode } from "core/request/AirbyteClient"; import { useBulkEditSelect } from "hooks/services/BulkEdit/BulkEditService"; -import { useConnectionFormService } from "hooks/services/Connection/ConnectionFormService"; +import { useConnectionFormService } from "hooks/services/ConnectionForm/ConnectionFormService"; import { Arrow as ArrowBlock } from "./components/Arrow"; import { IndexerType, PathPopout } from "./components/PathPopout"; diff --git a/airbyte-webapp/src/views/Connection/CatalogTree/components/BulkHeader.tsx b/airbyte-webapp/src/views/Connection/CatalogTree/components/BulkHeader.tsx index 7a663027a01d..b52663549a24 100644 --- a/airbyte-webapp/src/views/Connection/CatalogTree/components/BulkHeader.tsx +++ b/airbyte-webapp/src/views/Connection/CatalogTree/components/BulkHeader.tsx @@ -8,7 +8,7 @@ import { Button, Cell, Header, Switch } from "components"; import { SyncSchemaField, SyncSchemaFieldObject, SyncSchemaStream, traverseSchemaToField } from "core/domain/catalog"; import { DestinationSyncMode, SyncMode } from "core/request/AirbyteClient"; import { useBulkEdit } from "hooks/services/BulkEdit/BulkEditService"; -import { useConnectionFormService } from "hooks/services/Connection/ConnectionFormService"; +import { useConnectionFormService } from "hooks/services/ConnectionForm/ConnectionFormService"; import { SUPPORTED_MODES } from "../../ConnectionForm/formConfig"; import { ArrowCell, CheckboxCell, HeaderCell } from "../styles"; diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.tsx index 39a09362f404..8ca64d2d7a47 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.tsx @@ -8,7 +8,7 @@ import styled from "styled-components"; import { Button, Card, ControlLabels, H5, Input } from "components"; import { NamespaceDefinitionType } from "core/request/AirbyteClient"; -import { useConnectionFormService } from "hooks/services/Connection/ConnectionFormService"; +import { useConnectionFormService } from "hooks/services/ConnectionForm/ConnectionFormService"; import { ValuesProps } from "hooks/services/useConnectionHook"; import { NamespaceDefinitionField } from "./components/NamespaceDefinitionField"; diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/components/OperationsSection.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/components/OperationsSection.tsx index 4406a14093f5..733329877480 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/components/OperationsSection.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/components/OperationsSection.tsx @@ -4,7 +4,7 @@ import { useIntl } from "react-intl"; import { Card, H5 } from "components"; -import { useConnectionFormService } from "hooks/services/Connection/ConnectionFormService"; +import { useConnectionFormService } from "hooks/services/ConnectionForm/ConnectionFormService"; import { FeatureItem, useFeature } from "hooks/services/Feature"; import { StyledSection } from "../ConnectionForm"; diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/components/ScheduleField.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/components/ScheduleField.tsx index b6962f67a1ca..025ce14ff133 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/components/ScheduleField.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/components/ScheduleField.tsx @@ -8,7 +8,7 @@ import { IDataItem } from "components/base/DropDown/components/Option"; import { Action, Namespace } from "core/analytics"; import { ConnectionScheduleData, ConnectionScheduleType } from "core/request/AirbyteClient"; import { useAnalyticsService } from "hooks/services/Analytics"; -import { useConnectionFormService } from "hooks/services/Connection/ConnectionFormService"; +import { useConnectionFormService } from "hooks/services/ConnectionForm/ConnectionFormService"; import availableCronTimeZones from "../../../../config/availableCronTimeZones.json"; import { FormikConnectionFormValues, useFrequencyDropdownData } from "../formConfig"; diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/components/SyncCatalogField.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/components/SyncCatalogField.tsx index 3f432223ceed..ab334704423a 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/components/SyncCatalogField.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/components/SyncCatalogField.tsx @@ -13,7 +13,7 @@ import { useConfig } from "config"; import { SyncSchemaStream } from "core/domain/catalog"; import { DestinationSyncMode } from "core/request/AirbyteClient"; import { BatchEditProvider, useBulkEdit } from "hooks/services/BulkEdit/BulkEditService"; -import { useConnectionFormService } from "hooks/services/Connection/ConnectionFormService"; +import { useConnectionFormService } from "hooks/services/ConnectionForm/ConnectionFormService"; import { naturalComparatorBy } from "utils/objects"; import CatalogTree from "views/Connection/CatalogTree"; diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/formConfig.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/formConfig.tsx index 8d7d0d990066..2757fdd47490 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/formConfig.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/formConfig.tsx @@ -24,7 +24,7 @@ import { SyncMode, WebBackendConnectionRead, } from "core/request/AirbyteClient"; -import { ConnectionOrPartialConnection } from "hooks/services/Connection/ConnectionFormService"; +import { ConnectionOrPartialConnection } from "hooks/services/ConnectionForm/ConnectionFormService"; import { ValuesProps } from "hooks/services/useConnectionHook"; import { useCurrentWorkspace } from "services/workspaces/WorkspacesService"; From 31d0ccfe5be14377b56a1ec8028d0e7ec07d3601 Mon Sep 17 00:00:00 2001 From: KC Date: Thu, 22 Sep 2022 16:46:09 -0400 Subject: [PATCH 048/107] Code appears to be working --- .../CreateConnection.tsx | 8 ++-- .../ConnectionEdit/ConnectionEditService.tsx | 4 +- .../components/ConnectionReplication.tsx | 45 ++++++++----------- .../ConnectionForm/ConnectionFormFields.tsx | 12 +++-- .../components/EditControls.tsx | 1 - 5 files changed, 34 insertions(+), 36 deletions(-) diff --git a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnection.tsx b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnection.tsx index 2b91ddfc42b8..e4560a2434b9 100644 --- a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnection.tsx +++ b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnection.tsx @@ -1,4 +1,4 @@ -import { Field, FieldProps, Formik, FormikHelpers } from "formik"; +import { Field, FieldProps, Form, Formik, FormikHelpers } from "formik"; import React, { Suspense, useCallback } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useNavigate } from "react-router-dom"; @@ -110,7 +110,7 @@ const CreateConnectionInner: React.FC = ({
{({ values, isSubmitting, isValid, dirty }) => ( - <> +
{({ field, meta }: FieldProps) => ( @@ -143,7 +143,7 @@ const CreateConnectionInner: React.FC = ({ )}
- + = ({ isValid={isValid && !editingTransformation} errorMessage={getErrorMessage(isValid, dirty)} /> - + )}
diff --git a/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.tsx b/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.tsx index 08c31c023fab..f4185b20c158 100644 --- a/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.tsx +++ b/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.tsx @@ -13,7 +13,7 @@ const useConnectionEdit = ({ connectionId }: ConnectionEditProps) => { const connectionService = useWebConnectionService(); const [schemaHasBeenRefreshed, setSchemaHasBeenRefreshed] = useState(false); - const [{ loading: schemaRefreshing }, refreshConnectionCatalog] = useAsyncFn(async () => { + const [{ loading: schemaRefreshing }, refreshSchema] = useAsyncFn(async () => { setConnection(await connectionService.getConnection(connectionId, true)); setSchemaHasBeenRefreshed(true); }, [connectionId]); @@ -24,7 +24,7 @@ const useConnectionEdit = ({ connectionId }: ConnectionEditProps) => { schemaHasBeenRefreshed, setConnection, setSchemaHasBeenRefreshed, - refreshConnectionCatalog, + refreshSchema, }; }; diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx index 4bbb9f1f8db5..71bbc3438db1 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx @@ -1,4 +1,4 @@ -import { Formik, FormikHelpers } from "formik"; +import { Form, Formik, FormikHelpers } from "formik"; import React, { useEffect, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useUnmount } from "react-use"; @@ -82,21 +82,19 @@ export const ConnectionReplication: React.FC = () => { const { openModal, closeModal } = useModalService(); const { closeConfirmationModal } = useConfirmationModalService(); + const [saved, setSaved] = useState(false); useTrackPage(PageTrackingCodes.CONNECTIONS_ITEM_REPLICATION); - const [, /* hasBeenSaved*/ setHasBeenSaved] = useState(false); - const { mutateAsync: updateConnection } = useUpdateConnection(); const { connection, schemaRefreshing, - connectionDirty, schemaHasBeenRefreshed, - onRefreshSourceSchema, setConnection, setSchemaHasBeenRefreshed, + refreshSchema, } = useConnectionEditService(); const { initialValues, getErrorMessage, setSubmitError } = useConnectionFormService(); @@ -132,8 +130,7 @@ export const ConnectionReplication: React.FC = () => { }); } - setConnection?.(updatedConnection); - setHasBeenSaved(true); + setConnection(updatedConnection); }; const onFormSubmit = async (values: FormikConnectionFormValues, _: FormikHelpers) => { @@ -165,22 +162,28 @@ export const ConnectionReplication: React.FC = () => { content: (props) => , }); if (result.type !== "canceled") { + // Save the connection taking into account the correct skipRefresh value from the dialog choice. await saveConnection(formValues, { skipReset: !result.reason }); + } else { + // We don't want to set saved to true or schema has been refreshed to false. + return; } - // Save the connection taking into account the correct skipRefresh value from the dialog choice. } else { // The catalog hasn't changed. We don't need to ask for any confirmation and can simply save. await saveConnection(formValues, { skipReset: true }); } - setSchemaHasBeenRefreshed?.(false); + + setSaved(true); + setSchemaHasBeenRefreshed(false); } catch (e) { setSubmitError(e); } }; useEffect(() => { + // If we have a catalogDiff we always want to show the modal const { catalogDiff, syncCatalog } = connection; - if (catalogDiff?.transforms && catalogDiff.transforms.length > 0) { + if (catalogDiff?.transforms && catalogDiff.transforms?.length > 0) { openModal({ title: formatMessage({ id: "connection.updateSchema.completed" }), preventCancel: true, @@ -191,35 +194,25 @@ export const ConnectionReplication: React.FC = () => { } }, [connection, formatMessage, openModal]); - useEffect(() => { - if (connectionDirty) { - setHasBeenSaved(false); - } - }, [connectionDirty]); - - const onCancelConnectionFormEdit = () => { - setSchemaHasBeenRefreshed?.(false); - }; - return ( {!schemaRefreshing && connection ? ( {({ values, isSubmitting, isValid, dirty, resetForm }) => ( - <> - +
+ { resetForm(); - onCancelConnectionFormEdit(); + setSchemaHasBeenRefreshed(false); }} - // successMessage={successMessage} + successMessage={saved && !dirty && } errorMessage={getErrorMessage(isValid, dirty)} - enableControls={schemaHasBeenRefreshed || connectionDirty} + enableControls={schemaHasBeenRefreshed || dirty} /> - + )}
) : ( diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.tsx index 8ca64d2d7a47..17186a533cf1 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.tsx @@ -92,10 +92,16 @@ interface ConnectionFormFieldsProps { className?: string; values: ValuesProps | FormikConnectionFormValues; isSubmitting: boolean; + refreshSchema: () => void; } -export const ConnectionFormFields: React.FC = ({ className, values, isSubmitting }) => { - const { mode, onRefreshSourceSchema } = useConnectionFormService(); +export const ConnectionFormFields: React.FC = ({ + className, + values, + isSubmitting, + refreshSchema, +}) => { + const { mode } = useConnectionFormService(); const { formatMessage } = useIntl(); return ( @@ -171,7 +177,7 @@ export const ConnectionFormFields: React.FC = ({ clas component={SchemaField} isSubmitting={isSubmitting} additionalControl={ - diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/components/EditControls.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/components/EditControls.tsx index cb6665dcaaa2..395d3ef468a5 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/components/EditControls.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/components/EditControls.tsx @@ -27,7 +27,6 @@ const EditControls: React.FC = ({ enableControls, withLine, }) => { - console.log({ successMessage, errorMessage, dirty }); return ( <> {withLine &&
} From 9862aed8a499b3cdcf19ab9f400148aea4714861 Mon Sep 17 00:00:00 2001 From: KC Date: Mon, 26 Sep 2022 14:05:16 -0400 Subject: [PATCH 049/107] Some minor cleanup --- .../components/ConnectionReplication.tsx | 197 ++++++++---------- .../components/ResetWarningModal.tsx | 48 +++++ 2 files changed, 134 insertions(+), 111 deletions(-) create mode 100644 airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ResetWarningModal.tsx diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx index 71bbc3438db1..c2b9a19d8c58 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx @@ -1,16 +1,14 @@ import { Form, Formik, FormikHelpers } from "formik"; -import React, { useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useUnmount } from "react-use"; import styled from "styled-components"; -import { Button, LabeledSwitch, ModalBody, ModalFooter } from "components"; import LoadingSchema from "components/LoadingSchema"; import { getFrequencyType } from "config/utils"; import { Action, Namespace } from "core/analytics"; import { toWebBackendConnectionUpdate } from "core/domain/connection"; -import { ConnectionStateType } from "core/request/AirbyteClient"; import { PageTrackingCodes, useAnalyticsService, useTrackPage } from "hooks/services/Analytics"; import { useConfirmationModalService } from "hooks/services/ConfirmationModal"; import { useConnectionEditService } from "hooks/services/ConnectionEdit/ConnectionEditService"; @@ -27,47 +25,7 @@ import EditControls from "views/Connection/ConnectionForm/components/EditControl import { ConnectionFormFields } from "views/Connection/ConnectionForm/ConnectionFormFields"; import { connectionValidationSchema, FormikConnectionFormValues } from "views/Connection/ConnectionForm/formConfig"; -interface ResetWarningModalProps { - onClose: (withReset: boolean) => void; - onCancel: () => void; - stateType: ConnectionStateType; -} - -const ResetWarningModal: React.FC = ({ onCancel, onClose, stateType }) => { - const { formatMessage } = useIntl(); - const [withReset, setWithReset] = useState(true); - const requireFullReset = stateType === ConnectionStateType.legacy; - return ( - <> - - {/* - TODO: This should use proper text stylings once we have them available. - See https://github.com/airbytehq/airbyte/issues/14478 - */} - -

- setWithReset(ev.target.checked)} - label={formatMessage({ - id: requireFullReset ? "connection.saveWithFullReset" : "connection.saveWithReset", - })} - checkbox - data-testid="resetModal-reset-checkbox" - /> -

-
- - - - - - ); -}; +import { ResetWarningModal } from "./ResetWarningModal"; const Content = styled.div` max-width: 1279px; @@ -106,79 +64,96 @@ export const ConnectionReplication: React.FC = () => { closeConfirmationModal(); }); - const saveConnection = async (values: ValuesProps, { skipReset }: { skipReset: boolean }) => { - if (schemaRefreshing) { - return; - } - const connectionAsUpdate = toWebBackendConnectionUpdate(connection); - - const updatedConnection = await updateConnection({ - ...connectionAsUpdate, - ...values, - connectionId: connection.connectionId, - skipReset, - }); - - if (!equal(values.syncCatalog, connection.syncCatalog)) { - analyticsService.track(Namespace.CONNECTION, Action.EDIT_SCHEMA, { - actionDescription: "Connection saved with catalog changes", - connector_source: connection.source.sourceName, - connector_source_definition_id: connection.source.sourceDefinitionId, - connector_destination: connection.destination.destinationName, - connector_destination_definition_id: connection.destination.destinationDefinitionId, - frequency: getFrequencyType(connection.scheduleData?.basicSchedule), + const saveConnection = useCallback( + async (values: ValuesProps, { skipReset }: { skipReset: boolean }) => { + if (schemaRefreshing) { + return; + } + const connectionAsUpdate = toWebBackendConnectionUpdate(connection); + + const updatedConnection = await updateConnection({ + ...connectionAsUpdate, + ...values, + connectionId: connection.connectionId, + skipReset, }); - } - setConnection(updatedConnection); - }; - - const onFormSubmit = async (values: FormikConnectionFormValues, _: FormikHelpers) => { - const formValues = tidyConnectionFormValues(values, workspaceId, connection.operations); - - // Detect whether the catalog has any differences in its enabled streams compared to the original one. - // This could be due to user changes (e.g. in the sync mode) or due to new/removed - // streams due to a "refreshed source schema". - const hasCatalogChanged = !equal( - formValues.syncCatalog.streams - .filter((s) => s.config?.selected) - .sort(naturalComparatorBy((syncStream) => syncStream.stream?.name ?? "")), - connection.syncCatalog.streams - .filter((s) => s.config?.selected) - .sort(naturalComparatorBy((syncStream) => syncStream.stream?.name ?? "")) - ); - - setSubmitError(null); - - // Whenever the catalog changed show a warning to the user, that we're about to reset their data. - // Given them a choice to opt-out in which case we'll be sending skipRefresh: true to the update - // endpoint. - try { - if (hasCatalogChanged) { - const stateType = await connectionService.getStateType(connection.connectionId); - const result = await openModal({ - title: formatMessage({ id: "connection.resetModalTitle" }), - size: "md", - content: (props) => , + if (!equal(values.syncCatalog, connection.syncCatalog)) { + analyticsService.track(Namespace.CONNECTION, Action.EDIT_SCHEMA, { + actionDescription: "Connection saved with catalog changes", + connector_source: connection.source.sourceName, + connector_source_definition_id: connection.source.sourceDefinitionId, + connector_destination: connection.destination.destinationName, + connector_destination_definition_id: connection.destination.destinationDefinitionId, + frequency: getFrequencyType(connection.scheduleData?.basicSchedule), }); - if (result.type !== "canceled") { - // Save the connection taking into account the correct skipRefresh value from the dialog choice. - await saveConnection(formValues, { skipReset: !result.reason }); + } + + setConnection(updatedConnection); + }, + [analyticsService, connection, schemaRefreshing, setConnection, updateConnection] + ); + + const onFormSubmit = useCallback( + async (values: FormikConnectionFormValues, _: FormikHelpers) => { + const formValues = tidyConnectionFormValues(values, workspaceId, connection.operations); + + // Detect whether the catalog has any differences in its enabled streams compared to the original one. + // This could be due to user changes (e.g. in the sync mode) or due to new/removed + // streams due to a "refreshed source schema". + const hasCatalogChanged = !equal( + formValues.syncCatalog.streams + .filter((s) => s.config?.selected) + .sort(naturalComparatorBy((syncStream) => syncStream.stream?.name ?? "")), + connection.syncCatalog.streams + .filter((s) => s.config?.selected) + .sort(naturalComparatorBy((syncStream) => syncStream.stream?.name ?? "")) + ); + + setSubmitError(null); + + // Whenever the catalog changed show a warning to the user, that we're about to reset their data. + // Given them a choice to opt-out in which case we'll be sending skipRefresh: true to the update + // endpoint. + try { + if (hasCatalogChanged) { + const stateType = await connectionService.getStateType(connection.connectionId); + const result = await openModal({ + title: formatMessage({ id: "connection.resetModalTitle" }), + size: "md", + content: (props) => , + }); + if (result.type !== "canceled") { + // Save the connection taking into account the correct skipRefresh value from the dialog choice. + await saveConnection(formValues, { skipReset: !result.reason }); + } else { + // We don't want to set saved to true or schema has been refreshed to false. + return; + } } else { - // We don't want to set saved to true or schema has been refreshed to false. - return; + // The catalog hasn't changed. We don't need to ask for any confirmation and can simply save. + await saveConnection(formValues, { skipReset: true }); } - } else { - // The catalog hasn't changed. We don't need to ask for any confirmation and can simply save. - await saveConnection(formValues, { skipReset: true }); - } - setSaved(true); - setSchemaHasBeenRefreshed(false); - } catch (e) { - setSubmitError(e); - } - }; + setSaved(true); + setSchemaHasBeenRefreshed(false); + } catch (e) { + setSubmitError(e); + } + }, + [ + connection.connectionId, + connection.operations, + connection.syncCatalog.streams, + connectionService, + formatMessage, + openModal, + saveConnection, + setSchemaHasBeenRefreshed, + setSubmitError, + workspaceId, + ] + ); useEffect(() => { // If we have a catalogDiff we always want to show the modal diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ResetWarningModal.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ResetWarningModal.tsx new file mode 100644 index 000000000000..5408b1a196a1 --- /dev/null +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ResetWarningModal.tsx @@ -0,0 +1,48 @@ +import { useState } from "react"; +import { FormattedMessage, useIntl } from "react-intl"; + +import { Button, LabeledSwitch, ModalBody, ModalFooter } from "components"; + +import { ConnectionStateType } from "core/request/AirbyteClient"; + +interface ResetWarningModalProps { + onClose: (withReset: boolean) => void; + onCancel: () => void; + stateType: ConnectionStateType; +} + +export const ResetWarningModal: React.FC = ({ onCancel, onClose, stateType }) => { + const { formatMessage } = useIntl(); + const [withReset, setWithReset] = useState(true); + const requireFullReset = stateType === ConnectionStateType.legacy; + return ( + <> + + {/* + TODO: This should use proper text stylings once we have them available. + See https://github.com/airbytehq/airbyte/issues/14478 + */} + +

+ setWithReset(ev.target.checked)} + label={formatMessage({ + id: requireFullReset ? "connection.saveWithFullReset" : "connection.saveWithReset", + })} + checkbox + data-testid="resetModal-reset-checkbox" + /> +

+
+ + + + + + ); +}; From b93ad8eb884a6a4ed9f43a1bb53affea59d682f0 Mon Sep 17 00:00:00 2001 From: KC Date: Mon, 26 Sep 2022 14:22:39 -0400 Subject: [PATCH 050/107] Form dirty change tracking --- .../CreateConnection.tsx | 2 ++ .../components/ConnectionReplication.tsx | 17 +++++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnection.tsx b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnection.tsx index e4560a2434b9..48b5174f5b89 100644 --- a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnection.tsx +++ b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnection.tsx @@ -5,6 +5,7 @@ import { useNavigate } from "react-router-dom"; import { useToggle } from "react-use"; import { Input } from "components/base"; +import { FormChangeTracker } from "components/FormChangeTracker"; import LoadingSchema from "components/LoadingSchema"; import { DestinationRead, SourceRead } from "core/request/AirbyteClient"; @@ -111,6 +112,7 @@ const CreateConnectionInner: React.FC = ({ {({ values, isSubmitting, isValid, dirty }) => (
+
{({ field, meta }: FieldProps) => ( diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx index c2b9a19d8c58..ac20053a7fcc 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx @@ -4,6 +4,7 @@ import { FormattedMessage, useIntl } from "react-intl"; import { useUnmount } from "react-use"; import styled from "styled-components"; +import { FormChangeTracker } from "components/FormChangeTracker"; import LoadingSchema from "components/LoadingSchema"; import { getFrequencyType } from "config/utils"; @@ -16,6 +17,7 @@ import { tidyConnectionFormValues, useConnectionFormService, } from "hooks/services/ConnectionForm/ConnectionFormService"; +import { useUniqueFormId } from "hooks/services/FormChangeTracker"; import { useModalService } from "hooks/services/Modal"; import { useConnectionService, useUpdateConnection, ValuesProps } from "hooks/services/useConnectionHook"; import { useCurrentWorkspaceId } from "services/workspaces/WorkspacesService"; @@ -36,16 +38,16 @@ const Content = styled.div` export const ConnectionReplication: React.FC = () => { const analyticsService = useAnalyticsService(); const connectionService = useConnectionService(); - const { formatMessage } = useIntl(); + const workspaceId = useCurrentWorkspaceId(); + const { formatMessage } = useIntl(); const { openModal, closeModal } = useModalService(); const { closeConfirmationModal } = useConfirmationModalService(); - const [saved, setSaved] = useState(false); - - useTrackPage(PageTrackingCodes.CONNECTIONS_ITEM_REPLICATION); - const { mutateAsync: updateConnection } = useUpdateConnection(); + const [saved, setSaved] = useState(false); + + const formId = useUniqueFormId(); const { connection, schemaRefreshing, @@ -54,11 +56,9 @@ export const ConnectionReplication: React.FC = () => { setSchemaHasBeenRefreshed, refreshSchema, } = useConnectionEditService(); - const { initialValues, getErrorMessage, setSubmitError } = useConnectionFormService(); - const workspaceId = useCurrentWorkspaceId(); - + useTrackPage(PageTrackingCodes.CONNECTIONS_ITEM_REPLICATION); useUnmount(() => { closeModal(); closeConfirmationModal(); @@ -175,6 +175,7 @@ export const ConnectionReplication: React.FC = () => { {({ values, isSubmitting, isValid, dirty, resetForm }) => ( + Date: Mon, 26 Sep 2022 15:13:47 -0400 Subject: [PATCH 051/107] WIP styling moving and fixing --- .../CreateConnection.tsx | 49 +--- .../CreateConnectionName.module.scss | 12 + .../components/CreateConnectionName.tsx | 49 ++++ .../ConnectionForm/ConnectionForm.test.tsx | 227 +++++++++--------- .../ConnectionForm/ConnectionForm.tsx | 32 --- .../ConnectionFormFields.module.scss | 40 +++ .../ConnectionForm/ConnectionFormFields.tsx | 131 +++------- .../NamespaceDefinitionField.module.scss | 1 + .../components/NamespaceDefinitionField.tsx | 14 +- .../components/OperationsSection.module.scss | 10 + .../components/OperationsSection.tsx | 6 +- .../components/Section.module.scss | 10 + .../ConnectionForm/components/Section.tsx | 17 ++ 13 files changed, 301 insertions(+), 297 deletions(-) create mode 100644 airbyte-webapp/src/components/CreateConnectionContent/components/CreateConnectionName.module.scss create mode 100644 airbyte-webapp/src/components/CreateConnectionContent/components/CreateConnectionName.tsx create mode 100644 airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.module.scss create mode 100644 airbyte-webapp/src/views/Connection/ConnectionForm/components/NamespaceDefinitionField.module.scss create mode 100644 airbyte-webapp/src/views/Connection/ConnectionForm/components/OperationsSection.module.scss create mode 100644 airbyte-webapp/src/views/Connection/ConnectionForm/components/Section.module.scss create mode 100644 airbyte-webapp/src/views/Connection/ConnectionForm/components/Section.tsx diff --git a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnection.tsx b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnection.tsx index 48b5174f5b89..e164ac211302 100644 --- a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnection.tsx +++ b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnection.tsx @@ -1,10 +1,8 @@ -import { Field, FieldProps, Form, Formik, FormikHelpers } from "formik"; +import { Form, Formik, FormikHelpers } from "formik"; import React, { Suspense, useCallback } from "react"; -import { FormattedMessage, useIntl } from "react-intl"; import { useNavigate } from "react-router-dom"; import { useToggle } from "react-use"; -import { Input } from "components/base"; import { FormChangeTracker } from "components/FormChangeTracker"; import LoadingSchema from "components/LoadingSchema"; @@ -20,17 +18,10 @@ import { SchemaError as SchemaErrorType, useDiscoverSchema } from "hooks/service import { useCurrentWorkspaceId } from "services/workspaces/WorkspacesService"; import CreateControls from "views/Connection/ConnectionForm/components/CreateControls"; import { OperationsSection } from "views/Connection/ConnectionForm/components/OperationsSection"; -import { - ConnectionFormFields, - ConnectorLabel, - FlexRow, - LabelHeading, - LeftFieldCol, - RightFieldCol, - Section, -} from "views/Connection/ConnectionForm/ConnectionFormFields"; +import { ConnectionFormFields } from "views/Connection/ConnectionForm/ConnectionFormFields"; import { connectionValidationSchema, FormikConnectionFormValues } from "views/Connection/ConnectionForm/formConfig"; +import { CreateConnectionName } from "./components/CreateConnectionName"; import { SchemaError } from "./components/SchemaError"; import styles from "./CreateConnection.module.scss"; @@ -56,7 +47,6 @@ const CreateConnectionInner: React.FC = ({ const { clearFormChange } = useFormChangeTrackerService(); const navigate = useNavigate(); const [editingTransformation, toggleEditingTransformation] = useToggle(false); - const { formatMessage } = useIntl(); const { connection, initialValues, getErrorMessage, setSubmitError } = useConnectionFormService(); @@ -113,38 +103,7 @@ const CreateConnectionInner: React.FC = ({ {({ values, isSubmitting, isValid, dirty }) => ( -
- - {({ field, meta }: FieldProps) => ( - - - - - - } - message={formatMessage({ - id: "form.connectionName.message", - })} - /> - - - - - - )} - -
+ { + const { formatMessage } = useIntl(); + return ( +
+ + {({ field, meta }: FieldProps) => ( +
+
+ + + + } + message={formatMessage({ + id: "form.connectionName.message", + })} + /> +
+
+ +
+
+ )} +
+
+ ); +}; diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.test.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.test.tsx index 964049e17c28..3a8993cbfadb 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.test.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.test.tsx @@ -1,125 +1,126 @@ -import { waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { render } from "test-utils/testutils"; +export {}; +// import { waitFor } from "@testing-library/react"; +// import userEvent from "@testing-library/user-event"; +// import { render } from "test-utils/testutils"; -import { - ConnectionStatus, - DestinationRead, - NamespaceDefinitionType, - SourceRead, - WebBackendConnectionRead, -} from "core/request/AirbyteClient"; -import { ConfirmationModalService } from "hooks/services/ConfirmationModal/ConfirmationModalService"; -import { ConnectionFormServiceProvider } from "hooks/services/Connection/ConnectionFormService"; +// import { +// ConnectionStatus, +// DestinationRead, +// NamespaceDefinitionType, +// SourceRead, +// WebBackendConnectionRead, +// } from "core/request/AirbyteClient"; +// import { ConfirmationModalService } from "hooks/services/ConfirmationModal/ConfirmationModalService"; +// import { ConnectionFormServiceProvider } from "hooks/services/Connection/ConnectionFormService"; -import { ConnectionForm, ConnectionFormMode } from "./ConnectionForm"; +// import { ConnectionForm, ConnectionFormMode } from "./ConnectionForm"; -const mockSource: SourceRead = { - sourceId: "test-source", - name: "test source", - sourceName: "test-source-name", - workspaceId: "test-workspace-id", - sourceDefinitionId: "test-source-definition-id", - connectionConfiguration: undefined, -}; +// const mockSource: SourceRead = { +// sourceId: "test-source", +// name: "test source", +// sourceName: "test-source-name", +// workspaceId: "test-workspace-id", +// sourceDefinitionId: "test-source-definition-id", +// connectionConfiguration: undefined, +// }; -const mockDestination: DestinationRead = { - destinationId: "test-destination", - name: "test destination", - destinationName: "test destination name", - workspaceId: "test-workspace-id", - destinationDefinitionId: "test-destination-definition-id", - connectionConfiguration: undefined, -}; +// const mockDestination: DestinationRead = { +// destinationId: "test-destination", +// name: "test destination", +// destinationName: "test destination name", +// workspaceId: "test-workspace-id", +// destinationDefinitionId: "test-destination-definition-id", +// connectionConfiguration: undefined, +// }; -const mockConnection: WebBackendConnectionRead = { - connectionId: "test-connection", - name: "test connection", - prefix: "test", - sourceId: "test-source", - destinationId: "test-destination", - status: ConnectionStatus.active, - scheduleType: "manual", - scheduleData: undefined, - syncCatalog: { - streams: [], - }, - namespaceDefinition: NamespaceDefinitionType.source, - namespaceFormat: "", - operationIds: [], - source: mockSource, - destination: mockDestination, - operations: [], - catalogId: "", - isSyncing: false, -}; +// const mockConnection: WebBackendConnectionRead = { +// connectionId: "test-connection", +// name: "test connection", +// prefix: "test", +// sourceId: "test-source", +// destinationId: "test-destination", +// status: ConnectionStatus.active, +// scheduleType: "manual", +// scheduleData: undefined, +// syncCatalog: { +// streams: [], +// }, +// namespaceDefinition: NamespaceDefinitionType.source, +// namespaceFormat: "", +// operationIds: [], +// source: mockSource, +// destination: mockDestination, +// operations: [], +// catalogId: "", +// isSyncing: false, +// }; -jest.mock("services/connector/DestinationDefinitionSpecificationService", () => { - return { - useGetDestinationDefinitionSpecification: () => { - return "destinationDefinition"; - }, - }; -}); +// jest.mock("services/connector/DestinationDefinitionSpecificationService", () => { +// return { +// useGetDestinationDefinitionSpecification: () => { +// return "destinationDefinition"; +// }, +// }; +// }); -jest.mock("services/workspaces/WorkspacesService", () => { - return { - useCurrentWorkspace: () => { - return "currentWorkspace"; - }, - useCurrentWorkspaceId: () => { - return "currentWorkspace"; - }, - }; -}); +// jest.mock("services/workspaces/WorkspacesService", () => { +// return { +// useCurrentWorkspace: () => { +// return "currentWorkspace"; +// }, +// useCurrentWorkspaceId: () => { +// return "currentWorkspace"; +// }, +// }; +// }); -const renderConnectionForm = (mode: ConnectionFormMode, connection = mockConnection) => - render( - - - - - - ); +// const renderConnectionForm = (mode: ConnectionFormMode, connection = mockConnection) => +// render( +// +// +// +// +// +// ); -describe("", () => { - let container: HTMLElement; - describe("edit mode", () => { - beforeEach(async () => { - const renderResult = await renderConnectionForm("edit"); +// describe("", () => { +// let container: HTMLElement; +// describe("edit mode", () => { +// beforeEach(async () => { +// const renderResult = await renderConnectionForm("edit"); - container = renderResult.container; - }); - it("renders relevant items", async () => { - const prefixInput = container.querySelector("input[data-testid='prefixInput']"); - expect(prefixInput).toBeInTheDocument(); +// container = renderResult.container; +// }); +// it("renders relevant items", async () => { +// const prefixInput = container.querySelector("input[data-testid='prefixInput']"); +// expect(prefixInput).toBeInTheDocument(); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - userEvent.type(prefixInput!, "{selectall}{del}prefix"); - await waitFor(() => userEvent.keyboard("{enter}")); - }); - it("pointer events are not turned off anywhere in the component", async () => { - expect(container.innerHTML).toContain("checkbox"); - }); - }); - describe("readonly mode", () => { - beforeEach(async () => { - const renderResult = await renderConnectionForm("readonly"); +// // eslint-disable-next-line @typescript-eslint/no-non-null-assertion +// userEvent.type(prefixInput!, "{selectall}{del}prefix"); +// await waitFor(() => userEvent.keyboard("{enter}")); +// }); +// it("pointer events are not turned off anywhere in the component", async () => { +// expect(container.innerHTML).toContain("checkbox"); +// }); +// }); +// describe("readonly mode", () => { +// beforeEach(async () => { +// const renderResult = await renderConnectionForm("readonly"); - container = renderResult.container; - }); - it("renders only relevant items for the mode", async () => { - const prefixInput = container.querySelector("input[data-testid='prefixInput']"); - expect(prefixInput).toBeInTheDocument(); - }); - it("pointer events are turned off in the fieldset", async () => { - expect(container.innerHTML).not.toContain("checkbox"); - }); - }); -}); +// container = renderResult.container; +// }); +// it("renders only relevant items for the mode", async () => { +// const prefixInput = container.querySelector("input[data-testid='prefixInput']"); +// expect(prefixInput).toBeInTheDocument(); +// }); +// it("pointer events are turned off in the fieldset", async () => { +// expect(container.innerHTML).not.toContain("checkbox"); +// }); +// }); +// }); diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx index 657753d5eb6c..2b51fe647264 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx @@ -1,33 +1 @@ -import styled from "styled-components"; - -export const FlexRow = styled.div` - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: flex-start; - gap: 10px; -`; - -export const LeftFieldCol = styled.div` - flex: 1; - max-width: 640px; - padding-right: 30px; -`; - -export const RightFieldCol = styled.div` - flex: 1; - max-width: 300px; -`; - -export const StyledSection = styled.div` - padding: 20px 20px; - display: flex; - flex-direction: column; - gap: 15px; - - &:not(:last-child) { - box-shadow: 0 1px 0 rgba(139, 139, 160, 0.25); - } -`; - export type ConnectionFormMode = "create" | "edit" | "readonly"; diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.module.scss b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.module.scss new file mode 100644 index 000000000000..232d8c598ace --- /dev/null +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.module.scss @@ -0,0 +1,40 @@ +.formContainer { + display: flex; + flex-direction: column; + gap: 10px; +} + +.readonly { + pointer-events: none; +} + +.flexRow { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: flex-start; + gap: 10px; +} + +.leftFieldCol { + flex: 1; + max-width: 640px; + padding-right: 30px; +} + +.rightFieldCol { + flex: 1; + max-width: 300px; +} + +.namespaceFormatLabel { + flex: 5 0 0; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.tryArrow { + margin: 0 10px -1px 0; + font-size: 14px; +} diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.tsx index 17186a533cf1..060475a15b80 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.tsx @@ -1,11 +1,12 @@ import { faSyncAlt } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import classNames from "classnames"; import { Field, FieldProps, Form } from "formik"; import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; -import styled from "styled-components"; -import { Button, Card, ControlLabels, H5, Input } from "components"; +import { Button, Card, ControlLabels, Input } from "components"; +import { Text } from "components/base/Text"; import { NamespaceDefinitionType } from "core/request/AirbyteClient"; import { useConnectionFormService } from "hooks/services/ConnectionForm/ConnectionFormService"; @@ -13,81 +14,11 @@ import { ValuesProps } from "hooks/services/useConnectionHook"; import { NamespaceDefinitionField } from "./components/NamespaceDefinitionField"; import ScheduleField from "./components/ScheduleField"; +import { Section } from "./components/Section"; import SchemaField from "./components/SyncCatalogField"; +import styles from "./ConnectionFormFields.module.scss"; import { FormikConnectionFormValues } from "./formConfig"; -interface SectionProps { - title?: React.ReactNode; -} - -const TryArrow = styled(FontAwesomeIcon)` - margin: 0 10px -1px 0; - font-size: 14px; -`; - -export const StyledSection = styled.div` - padding: 20px 20px; - display: flex; - flex-direction: column; - gap: 15px; - - &:not(:last-child) { - box-shadow: 0 1px 0 rgba(139, 139, 160, 0.25); - } -`; - -export const FlexRow = styled.div` - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: flex-start; - gap: 10px; -`; - -export const LeftFieldCol = styled.div` - flex: 1; - max-width: 640px; - padding-right: 30px; -`; - -export const RightFieldCol = styled.div` - flex: 1; - max-width: 300px; -`; - -export const LabelHeading = styled(H5)` - line-height: 16px; - display: inline; -`; - -export const ConnectorLabel = styled(ControlLabels)` - max-width: 328px; - margin-right: 20px; - vertical-align: top; -`; - -const NamespaceFormatLabel = styled(ControlLabels)` - flex: 5 0 0; - display: flex; - flex-direction: column; - justify-content: space-between; -`; - -export const FormContainer = styled(Form)` - display: flex; - flex-direction: column; - gap: 10px; -`; - -export const Section: React.FC> = ({ title, children }) => ( - - - {title &&
{title}
} - {children} -
-
-); - interface ConnectionFormFieldsProps { className?: string; values: ValuesProps | FormikConnectionFormValues; @@ -104,32 +35,38 @@ export const ConnectionFormFields: React.FC = ({ const { mode } = useConnectionFormService(); const { formatMessage } = useIntl(); + const formContainerClassnames = classNames(className, styles.formContainer); + const readonlyClass = classNames({ + [styles.readonly]: mode === "readonly", + }); + return ( - +
}>
- -
+
+ -
- + + {values.namespaceDefinition === NamespaceDefinitionType.customformat && ( {({ field, meta }: FieldProps) => ( - - - +
+ } message={} /> - - +
+
= ({ id: "connectionForm.namespaceFormat.placeholder", })} /> - - +
+
)} )} {({ field }: FieldProps) => ( - - +
+
= ({ id: "form.prefix.message", })} /> - - +
+
= ({ data-testid="prefixInput" style={{ pointerEvents: mode === "readonly" ? "none" : "auto" }} /> - - +
+
)}
- - +
+
- + } /> - +
- + ); }; diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/components/NamespaceDefinitionField.module.scss b/airbyte-webapp/src/views/Connection/ConnectionForm/components/NamespaceDefinitionField.module.scss new file mode 100644 index 000000000000..7fd4485ab708 --- /dev/null +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/components/NamespaceDefinitionField.module.scss @@ -0,0 +1 @@ +@forward "../ConnectionFormFields.module.scss"; diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/components/NamespaceDefinitionField.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/components/NamespaceDefinitionField.tsx index efc7888ad940..d81e433eb4f5 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/components/NamespaceDefinitionField.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/components/NamespaceDefinitionField.tsx @@ -5,7 +5,7 @@ import { FormattedMessage } from "react-intl"; import { ControlLabels, DropDown } from "components"; import { NamespaceDefinitionType } from "../../../../core/request/AirbyteClient"; -import { FlexRow, LeftFieldCol, RightFieldCol } from "../ConnectionForm"; +import styles from "./NamespaceDefinitionField.module.scss"; export const StreamOptions = [ { @@ -29,8 +29,8 @@ export const NamespaceDefinitionField: React.FC> = ({ field, const [, meta] = useField(field.name); return ( - - +
+
> = ({ field, label={} message={} /> - - +
+
> = ({ field, value={field.value} onChange={({ value }) => form.setFieldValue(field.name, value)} /> - - +
+
); }; diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/components/OperationsSection.module.scss b/airbyte-webapp/src/views/Connection/ConnectionForm/components/OperationsSection.module.scss new file mode 100644 index 000000000000..a98fdb16481e --- /dev/null +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/components/OperationsSection.module.scss @@ -0,0 +1,10 @@ +.styledSection { + padding: 20px; + display: flex; + flex-direction: column; + gap: 15px; + + &:not(:last-child) { + box-shadow: 0 1px 0 rgba(139, 139, 160, 25%); + } +} diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/components/OperationsSection.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/components/OperationsSection.tsx index 733329877480..8e52db95ab8a 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/components/OperationsSection.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/components/OperationsSection.tsx @@ -7,8 +7,8 @@ import { Card, H5 } from "components"; import { useConnectionFormService } from "hooks/services/ConnectionForm/ConnectionFormService"; import { FeatureItem, useFeature } from "hooks/services/Feature"; -import { StyledSection } from "../ConnectionForm"; import { NormalizationField } from "./NormalizationField"; +import styles from "./OperationsSection.module.scss"; import { TransformationField } from "./TransformationField"; interface OperationsSectionProps { @@ -34,7 +34,7 @@ export const OperationsSection: React.FC = ({ return ( - +
{supportsNormalization || supportsTransformations ? (
{[ @@ -57,7 +57,7 @@ export const OperationsSection: React.FC = ({ )} )} - +
); }; diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/components/Section.module.scss b/airbyte-webapp/src/views/Connection/ConnectionForm/components/Section.module.scss new file mode 100644 index 000000000000..c2ab970546d6 --- /dev/null +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/components/Section.module.scss @@ -0,0 +1,10 @@ +.section { + padding: 20px; + display: flex; + flex-direction: column; + gap: 15px; + + &:not(:last-child) { + box-shadow: 0 1px 0 rgba(139, 139, 160, 25%); + } +} diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/components/Section.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/components/Section.tsx new file mode 100644 index 000000000000..675ff3912b1e --- /dev/null +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/components/Section.tsx @@ -0,0 +1,17 @@ +import { Card } from "components/base"; +import { Text } from "components/base/Text"; + +import styles from "./Section.module.scss"; + +interface SectionProps { + title?: React.ReactNode; +} + +export const Section: React.FC> = ({ title, children }) => ( + +
+ {title && {title}} + {children} +
+
+); From efcfcc2ef4120b06c5a17228579939bef5b5d29a Mon Sep 17 00:00:00 2001 From: tealjulia Date: Mon, 26 Sep 2022 16:54:43 -0400 Subject: [PATCH 052/107] skip refresh on update if status !== active --- .../ConnectionItemPage/components/ConnectionReplication.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx index ac20053a7fcc..ae5db34e4c37 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx @@ -125,7 +125,8 @@ export const ConnectionReplication: React.FC = () => { }); if (result.type !== "canceled") { // Save the connection taking into account the correct skipRefresh value from the dialog choice. - await saveConnection(formValues, { skipReset: !result.reason }); + // We also want to skip the refresh sync if the connection is not in an "active" status + await saveConnection(formValues, { skipReset: !result.reason || connection.status !== "active" }); } else { // We don't want to set saved to true or schema has been refreshed to false. return; From bcc2bdfd56cad1d27ddfd4703ca4f2a20fc998f5 Mon Sep 17 00:00:00 2001 From: KC Date: Tue, 27 Sep 2022 10:24:24 -0400 Subject: [PATCH 053/107] Fixing styles --- .../components/CreateConnectionName.tsx | 1 + .../ConnectionForm/ConnectionFormFields.tsx | 130 +++++++++--------- 2 files changed, 65 insertions(+), 66 deletions(-) diff --git a/airbyte-webapp/src/components/CreateConnectionContent/components/CreateConnectionName.tsx b/airbyte-webapp/src/components/CreateConnectionContent/components/CreateConnectionName.tsx index 6385715dbee8..1551ba8531df 100644 --- a/airbyte-webapp/src/components/CreateConnectionContent/components/CreateConnectionName.tsx +++ b/airbyte-webapp/src/components/CreateConnectionContent/components/CreateConnectionName.tsx @@ -11,6 +11,7 @@ import styles from "./CreateConnectionName.module.scss"; export const CreateConnectionName = () => { const { formatMessage } = useIntl(); + return (
diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.tsx index 060475a15b80..a06c8ef25479 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.tsx @@ -1,11 +1,11 @@ import { faSyncAlt } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import classNames from "classnames"; -import { Field, FieldProps, Form } from "formik"; +import { Field, FieldProps } from "formik"; import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; -import { Button, Card, ControlLabels, Input } from "components"; +import { Button, ControlLabels, Input } from "components"; import { Text } from "components/base/Text"; import { NamespaceDefinitionType } from "core/request/AirbyteClient"; @@ -41,87 +41,85 @@ export const ConnectionFormFields: React.FC = ({ }); return ( -
+
}>
- -
- - - - - - - {values.namespaceDefinition === NamespaceDefinitionType.customformat && ( - - {({ field, meta }: FieldProps) => ( -
-
- } - message={} - /> -
-
- -
-
- )} -
- )} - - {({ field }: FieldProps) => ( +
+ + + + + + + {values.namespaceDefinition === NamespaceDefinitionType.customformat && ( + + {({ field, meta }: FieldProps) => (
} + message={} />
-
+
)} -
-
- - - - - } - /> -
- - + )} + + {({ field }: FieldProps) => ( +
+
+ +
+
+ +
+
+ )} +
+
+
+ + + + + } + /> +
+
); }; From 1f5c04e81bd79aa1480140934df65077b147618a Mon Sep 17 00:00:00 2001 From: KC Date: Tue, 27 Sep 2022 10:25:45 -0400 Subject: [PATCH 054/107] Minor --- .../components/ConnectionReplication.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx index ae5db34e4c37..b2d3eea659a2 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx @@ -113,7 +113,7 @@ export const ConnectionReplication: React.FC = () => { setSubmitError(null); // Whenever the catalog changed show a warning to the user, that we're about to reset their data. - // Given them a choice to opt-out in which case we'll be sending skipRefresh: true to the update + // Given them a choice to opt-out in which case we'll be sending skipRe: true to the update // endpoint. try { if (hasCatalogChanged) { @@ -124,8 +124,8 @@ export const ConnectionReplication: React.FC = () => { content: (props) => , }); if (result.type !== "canceled") { - // Save the connection taking into account the correct skipRefresh value from the dialog choice. - // We also want to skip the refresh sync if the connection is not in an "active" status + // Save the connection taking into account the correct skipReset value from the dialog choice. + // We also want to skip the reset sync if the connection is not in an "active" status await saveConnection(formValues, { skipReset: !result.reason || connection.status !== "active" }); } else { // We don't want to set saved to true or schema has been refreshed to false. @@ -145,6 +145,7 @@ export const ConnectionReplication: React.FC = () => { [ connection.connectionId, connection.operations, + connection.status, connection.syncCatalog.streams, connectionService, formatMessage, From 6fd2098bebca213844113ee3b65c92434422f375 Mon Sep 17 00:00:00 2001 From: KC Date: Tue, 27 Sep 2022 10:56:36 -0400 Subject: [PATCH 055/107] More cleanup and improvements --- .../CreateConnection.tsx | 16 +++++++++---- .../ConnectionEdit/ConnectionEditService.tsx | 18 ++++++++++++--- .../components/ConnectionName.tsx | 16 ++++--------- .../components/ConnectionReplication.tsx | 18 ++++++++------- .../components/EnabledControl.tsx | 20 +++++++--------- .../components/StatusMainInfo.tsx | 4 ++-- .../ConnectionForm/ConnectionFormFields.tsx | 7 +++++- .../components/OperationsSection.module.scss | 10 -------- .../components/OperationsSection.tsx | 13 ++++++----- .../components/PreventRefreshOnDirty.tsx | 23 +++++++++++++++++++ 10 files changed, 88 insertions(+), 57 deletions(-) delete mode 100644 airbyte-webapp/src/views/Connection/ConnectionForm/components/OperationsSection.module.scss create mode 100644 airbyte-webapp/src/views/Connection/ConnectionForm/components/PreventRefreshOnDirty.tsx diff --git a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnection.tsx b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnection.tsx index e164ac211302..cbc28cfd4617 100644 --- a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnection.tsx +++ b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnection.tsx @@ -41,14 +41,17 @@ const CreateConnectionInner: React.FC = ({ onDiscoverSchema, afterSubmitConnection, }) => { + const navigate = useNavigate(); + const { mutateAsync: createConnection } = useCreateConnection(); + + const { clearFormChange } = useFormChangeTrackerService(); + const workspaceId = useCurrentWorkspaceId(); const formId = useUniqueFormId(); - const { clearFormChange } = useFormChangeTrackerService(); - const navigate = useNavigate(); - const [editingTransformation, toggleEditingTransformation] = useToggle(false); const { connection, initialValues, getErrorMessage, setSubmitError } = useConnectionFormService(); + const [editingTransformation, toggleEditingTransformation] = useToggle(false); const onFormSubmit = useCallback( async (formValues: FormikConnectionFormValues, formikHelpers: FormikHelpers) => { @@ -104,7 +107,12 @@ const CreateConnectionInner: React.FC = ({
- + { setSchemaHasBeenRefreshed(true); }, [connectionId]); + const { mutateAsync: updateConnectionAction, isLoading: connectionUpdating } = useUpdateConnection(); + + const updateConnection = useCallback( + async (connection: WebBackendConnectionUpdate) => { + setConnection(await updateConnectionAction(connection)); + }, + [updateConnectionAction] + ); + return { connection, + connectionUpdating, schemaRefreshing, schemaHasBeenRefreshed, - setConnection, + updateConnection, setSchemaHasBeenRefreshed, refreshSchema, }; diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionName.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionName.tsx index a170a8977bd2..257f8ef502aa 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionName.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionName.tsx @@ -4,9 +4,7 @@ import React, { ChangeEvent, useState } from "react"; import { Input } from "components"; -import { buildConnectionUpdate } from "core/domain/connection"; import { useConnectionEditService } from "hooks/services/ConnectionEdit/ConnectionEditService"; -import { useUpdateConnection } from "hooks/services/useConnectionHook"; import withKeystrokeHandler from "utils/withKeystrokeHandler"; import styles from "./ConnectionName.module.scss"; @@ -14,13 +12,12 @@ import styles from "./ConnectionName.module.scss"; const InputWithKeystroke = withKeystrokeHandler(Input); export const ConnectionName: React.FC = () => { - const { connection } = useConnectionEditService(); + const { connection, updateConnection } = useConnectionEditService(); const { name } = connection; const [editingState, setEditingState] = useState(false); const [loading, setLoading] = useState(false); const [connectionName, setConnectionName] = useState(connection.name); const [connectionNameBackup, setConnectionNameBackup] = useState(connectionName); - const { mutateAsync: updateConnection } = useUpdateConnection(); const inputChange = ({ currentTarget: { value } }: ChangeEvent) => setConnectionName(value); @@ -50,13 +47,10 @@ export const ConnectionName: React.FC = () => { try { setLoading(true); - // TODO: Once PATCH work is merged this will be fine. - // await updateConnection({ - // name: connectionNameTrimmed, - // connectionId: connection.connectionId, - // }); - // TODO: Make sure the top level connection object is updated after this - await updateConnection(buildConnectionUpdate(connection, { name: connectionNameTrimmed })); + await updateConnection({ + name: connectionNameTrimmed, + connectionId: connection.connectionId, + }); setConnectionName(connectionNameTrimmed); setConnectionNameBackup(connectionNameTrimmed); diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx index b2d3eea659a2..18fe85950ea3 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx @@ -19,7 +19,7 @@ import { } from "hooks/services/ConnectionForm/ConnectionFormService"; import { useUniqueFormId } from "hooks/services/FormChangeTracker"; import { useModalService } from "hooks/services/Modal"; -import { useConnectionService, useUpdateConnection, ValuesProps } from "hooks/services/useConnectionHook"; +import { useConnectionService, ValuesProps } from "hooks/services/useConnectionHook"; import { useCurrentWorkspaceId } from "services/workspaces/WorkspacesService"; import { equal, naturalComparatorBy } from "utils/objects"; import { CatalogDiffModal } from "views/Connection/CatalogDiffModal/CatalogDiffModal"; @@ -43,7 +43,6 @@ export const ConnectionReplication: React.FC = () => { const { formatMessage } = useIntl(); const { openModal, closeModal } = useModalService(); const { closeConfirmationModal } = useConfirmationModalService(); - const { mutateAsync: updateConnection } = useUpdateConnection(); const [saved, setSaved] = useState(false); @@ -52,7 +51,7 @@ export const ConnectionReplication: React.FC = () => { connection, schemaRefreshing, schemaHasBeenRefreshed, - setConnection, + updateConnection, setSchemaHasBeenRefreshed, refreshSchema, } = useConnectionEditService(); @@ -71,7 +70,7 @@ export const ConnectionReplication: React.FC = () => { } const connectionAsUpdate = toWebBackendConnectionUpdate(connection); - const updatedConnection = await updateConnection({ + await updateConnection({ ...connectionAsUpdate, ...values, connectionId: connection.connectionId, @@ -88,10 +87,8 @@ export const ConnectionReplication: React.FC = () => { frequency: getFrequencyType(connection.scheduleData?.basicSchedule), }); } - - setConnection(updatedConnection); }, - [analyticsService, connection, schemaRefreshing, setConnection, updateConnection] + [analyticsService, connection, schemaRefreshing, updateConnection] ); const onFormSubmit = useCallback( @@ -178,7 +175,12 @@ export const ConnectionReplication: React.FC = () => { {({ values, isSubmitting, isValid, dirty, resetForm }) => ( - + = ({ disabled, onStatusUpdating }) => { - const { mutateAsync: updateConnection, isLoading } = useUpdateConnection(); const analyticsService = useAnalyticsService(); - const { connection } = useConnectionEditService(); + const { connection, updateConnection, connectionUpdating } = useConnectionEditService(); const frequencyType = getFrequencyType(connection.scheduleData?.basicSchedule); const onChangeStatus = async () => { - await updateConnection( - buildConnectionUpdate(connection, { - status: connection.status === ConnectionStatus.active ? ConnectionStatus.inactive : ConnectionStatus.active, - }) - ); + await updateConnection({ + connectionId: connection.connectionId, + status: connection.status === ConnectionStatus.active ? ConnectionStatus.inactive : ConnectionStatus.active, + }); const trackableAction = connection.status === ConnectionStatus.active ? Action.DISABLE : Action.REENABLE; @@ -63,8 +59,8 @@ const EnabledControl: React.FC = ({ disabled, onStatusUpdat }; useUpdateEffect(() => { - onStatusUpdating?.(isLoading); - }, [isLoading]); + onStatusUpdating?.(connectionUpdating); + }, [connectionUpdating]); return ( @@ -75,7 +71,7 @@ const EnabledControl: React.FC = ({ disabled, onStatusUpdat disabled={disabled} onChange={onChangeStatus} checked={connection.status === ConnectionStatus.active} - loading={isLoading} + loading={connectionUpdating} id="toggle-enabled-source" /> diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/StatusMainInfo.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/StatusMainInfo.tsx index 065dbdfaac20..ec4d95c0e307 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/StatusMainInfo.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/StatusMainInfo.tsx @@ -6,7 +6,7 @@ import { Link } from "react-router-dom"; import { ConnectorCard } from "components"; import { ConnectionStatus } from "core/request/AirbyteClient"; -import { useConnectionFormService } from "hooks/services/ConnectionForm/ConnectionFormService"; +import { useConnectionEditService } from "hooks/services/ConnectionEdit/ConnectionEditService"; import { FeatureItem, useFeature } from "hooks/services/Feature"; import { RoutePaths } from "pages/routePaths"; import { useDestinationDefinition } from "services/connector/DestinationDefinitionService"; @@ -22,7 +22,7 @@ interface StatusMainInfoProps { export const StatusMainInfo: React.FC = ({ onStatusUpdating }) => { const { connection: { source, destination, status }, - } = useConnectionFormService(); + } = useConnectionEditService(); const sourceDefinition = useSourceDefinition(source.sourceDefinitionId); const destinationDefinition = useDestinationDefinition(destination.destinationDefinitionId); diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.tsx index a06c8ef25479..fbe242d15a89 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.tsx @@ -13,6 +13,7 @@ import { useConnectionFormService } from "hooks/services/ConnectionForm/Connecti import { ValuesProps } from "hooks/services/useConnectionHook"; import { NamespaceDefinitionField } from "./components/NamespaceDefinitionField"; +import { usePreventRefreshOnDirty } from "./components/PreventRefreshOnDirty"; import ScheduleField from "./components/ScheduleField"; import { Section } from "./components/Section"; import SchemaField from "./components/SyncCatalogField"; @@ -23,6 +24,7 @@ interface ConnectionFormFieldsProps { className?: string; values: ValuesProps | FormikConnectionFormValues; isSubmitting: boolean; + dirty: boolean; refreshSchema: () => void; } @@ -30,6 +32,7 @@ export const ConnectionFormFields: React.FC = ({ className, values, isSubmitting, + dirty, refreshSchema, }) => { const { mode } = useConnectionFormService(); @@ -40,6 +43,8 @@ export const ConnectionFormFields: React.FC = ({ [styles.readonly]: mode === "readonly", }); + const doRefreshSchema = usePreventRefreshOnDirty(dirty, refreshSchema); + return (
}> @@ -113,7 +118,7 @@ export const ConnectionFormFields: React.FC = ({ component={SchemaField} isSubmitting={isSubmitting} additionalControl={ - diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/components/OperationsSection.module.scss b/airbyte-webapp/src/views/Connection/ConnectionForm/components/OperationsSection.module.scss deleted file mode 100644 index a98fdb16481e..000000000000 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/components/OperationsSection.module.scss +++ /dev/null @@ -1,10 +0,0 @@ -.styledSection { - padding: 20px; - display: flex; - flex-direction: column; - gap: 15px; - - &:not(:last-child) { - box-shadow: 0 1px 0 rgba(139, 139, 160, 25%); - } -} diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/components/OperationsSection.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/components/OperationsSection.tsx index 8e52db95ab8a..a13c035d32dd 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/components/OperationsSection.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/components/OperationsSection.tsx @@ -2,13 +2,14 @@ import { Field, FieldArray } from "formik"; import React from "react"; import { useIntl } from "react-intl"; -import { Card, H5 } from "components"; +import { Card } from "components"; +import { Text } from "components/base/Text"; import { useConnectionFormService } from "hooks/services/ConnectionForm/ConnectionFormService"; import { FeatureItem, useFeature } from "hooks/services/Feature"; import { NormalizationField } from "./NormalizationField"; -import styles from "./OperationsSection.module.scss"; +import { Section } from "./Section"; import { TransformationField } from "./TransformationField"; interface OperationsSectionProps { @@ -34,16 +35,16 @@ export const OperationsSection: React.FC = ({ return ( -
+
{supportsNormalization || supportsTransformations ? ( -
+ {[ supportsNormalization && formatMessage({ id: "connectionForm.normalization.title" }), supportsTransformations && formatMessage({ id: "connectionForm.transformation.title" }), ] .filter(Boolean) .join(" & ")} -
+ ) : null} {supportsNormalization && } {supportsTransformations && ( @@ -57,7 +58,7 @@ export const OperationsSection: React.FC = ({ )} )} -
+
); }; diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/components/PreventRefreshOnDirty.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/components/PreventRefreshOnDirty.tsx new file mode 100644 index 000000000000..ac4fb0afc35f --- /dev/null +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/components/PreventRefreshOnDirty.tsx @@ -0,0 +1,23 @@ +import { useCallback } from "react"; + +import { useConfirmationModalService } from "hooks/services/ConfirmationModal"; + +export const usePreventRefreshOnDirty = (dirty: boolean, refreshSourceSchema: () => void) => { + const { openConfirmationModal, closeConfirmationModal } = useConfirmationModalService(); + + return useCallback(() => { + if (dirty) { + openConfirmationModal({ + title: "connection.updateSchema.formChanged.title", + text: "connection.updateSchema.formChanged.text", + submitButtonText: "connection.updateSchema.formChanged.confirm", + onSubmit: () => { + closeConfirmationModal(); + refreshSourceSchema(); + }, + }); + } else { + refreshSourceSchema(); + } + }, [closeConfirmationModal, dirty, openConfirmationModal, refreshSourceSchema]); +}; From 664a14a4dce288d56f198eb8403225e556a9c7b2 Mon Sep 17 00:00:00 2001 From: KC Date: Tue, 27 Sep 2022 10:59:02 -0400 Subject: [PATCH 056/107] Rename CreateConnectionContent -> CreateConnection --- .../CreateConnection.module.scss | 0 .../CreateConnection.tsx | 0 .../components/CreateConnectionName.module.scss | 0 .../components/CreateConnectionName.tsx | 0 .../components/SchemaError.tsx | 0 .../components/TryAfterErrorBlock.module.scss | 0 .../components/TryAfterErrorBlock.tsx | 0 .../{CreateConnectionContent => CreateConnection}/index.tsx | 0 .../ConnectionPage/pages/CreationFormPage/CreationFormPage.tsx | 2 +- .../src/pages/OnboardingPage/components/ConnectionStep.tsx | 2 +- 10 files changed, 2 insertions(+), 2 deletions(-) rename airbyte-webapp/src/components/{CreateConnectionContent => CreateConnection}/CreateConnection.module.scss (100%) rename airbyte-webapp/src/components/{CreateConnectionContent => CreateConnection}/CreateConnection.tsx (100%) rename airbyte-webapp/src/components/{CreateConnectionContent => CreateConnection}/components/CreateConnectionName.module.scss (100%) rename airbyte-webapp/src/components/{CreateConnectionContent => CreateConnection}/components/CreateConnectionName.tsx (100%) rename airbyte-webapp/src/components/{CreateConnectionContent => CreateConnection}/components/SchemaError.tsx (100%) rename airbyte-webapp/src/components/{CreateConnectionContent => CreateConnection}/components/TryAfterErrorBlock.module.scss (100%) rename airbyte-webapp/src/components/{CreateConnectionContent => CreateConnection}/components/TryAfterErrorBlock.tsx (100%) rename airbyte-webapp/src/components/{CreateConnectionContent => CreateConnection}/index.tsx (100%) diff --git a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnection.module.scss b/airbyte-webapp/src/components/CreateConnection/CreateConnection.module.scss similarity index 100% rename from airbyte-webapp/src/components/CreateConnectionContent/CreateConnection.module.scss rename to airbyte-webapp/src/components/CreateConnection/CreateConnection.module.scss diff --git a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnection.tsx b/airbyte-webapp/src/components/CreateConnection/CreateConnection.tsx similarity index 100% rename from airbyte-webapp/src/components/CreateConnectionContent/CreateConnection.tsx rename to airbyte-webapp/src/components/CreateConnection/CreateConnection.tsx diff --git a/airbyte-webapp/src/components/CreateConnectionContent/components/CreateConnectionName.module.scss b/airbyte-webapp/src/components/CreateConnection/components/CreateConnectionName.module.scss similarity index 100% rename from airbyte-webapp/src/components/CreateConnectionContent/components/CreateConnectionName.module.scss rename to airbyte-webapp/src/components/CreateConnection/components/CreateConnectionName.module.scss diff --git a/airbyte-webapp/src/components/CreateConnectionContent/components/CreateConnectionName.tsx b/airbyte-webapp/src/components/CreateConnection/components/CreateConnectionName.tsx similarity index 100% rename from airbyte-webapp/src/components/CreateConnectionContent/components/CreateConnectionName.tsx rename to airbyte-webapp/src/components/CreateConnection/components/CreateConnectionName.tsx diff --git a/airbyte-webapp/src/components/CreateConnectionContent/components/SchemaError.tsx b/airbyte-webapp/src/components/CreateConnection/components/SchemaError.tsx similarity index 100% rename from airbyte-webapp/src/components/CreateConnectionContent/components/SchemaError.tsx rename to airbyte-webapp/src/components/CreateConnection/components/SchemaError.tsx diff --git a/airbyte-webapp/src/components/CreateConnectionContent/components/TryAfterErrorBlock.module.scss b/airbyte-webapp/src/components/CreateConnection/components/TryAfterErrorBlock.module.scss similarity index 100% rename from airbyte-webapp/src/components/CreateConnectionContent/components/TryAfterErrorBlock.module.scss rename to airbyte-webapp/src/components/CreateConnection/components/TryAfterErrorBlock.module.scss diff --git a/airbyte-webapp/src/components/CreateConnectionContent/components/TryAfterErrorBlock.tsx b/airbyte-webapp/src/components/CreateConnection/components/TryAfterErrorBlock.tsx similarity index 100% rename from airbyte-webapp/src/components/CreateConnectionContent/components/TryAfterErrorBlock.tsx rename to airbyte-webapp/src/components/CreateConnection/components/TryAfterErrorBlock.tsx diff --git a/airbyte-webapp/src/components/CreateConnectionContent/index.tsx b/airbyte-webapp/src/components/CreateConnection/index.tsx similarity index 100% rename from airbyte-webapp/src/components/CreateConnectionContent/index.tsx rename to airbyte-webapp/src/components/CreateConnection/index.tsx diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/CreationFormPage/CreationFormPage.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/CreationFormPage/CreationFormPage.tsx index a78919f9e4ee..8d558f65eddf 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/CreationFormPage/CreationFormPage.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/CreationFormPage/CreationFormPage.tsx @@ -5,7 +5,7 @@ import { useLocation, useNavigate } from "react-router-dom"; import { LoadingPage, PageTitle } from "components"; import ConnectionBlock from "components/ConnectionBlock"; import { FormPageContent } from "components/ConnectorBlocks"; -import { CreateConnection } from "components/CreateConnectionContent"; +import { CreateConnection } from "components/CreateConnection"; import HeadTitle from "components/HeadTitle"; import StepsMenu from "components/StepsMenu"; diff --git a/airbyte-webapp/src/pages/OnboardingPage/components/ConnectionStep.tsx b/airbyte-webapp/src/pages/OnboardingPage/components/ConnectionStep.tsx index d8a477439e16..6e1c46566e25 100644 --- a/airbyte-webapp/src/pages/OnboardingPage/components/ConnectionStep.tsx +++ b/airbyte-webapp/src/pages/OnboardingPage/components/ConnectionStep.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { CreateConnection } from "components/CreateConnectionContent"; +import { CreateConnection } from "components/CreateConnection"; import { useDestinationList } from "hooks/services/useDestinationHook"; import { useSourceList } from "hooks/services/useSourceHook"; From bcfc57b4e485bba9c6e9adb61c675e1649328db3 Mon Sep 17 00:00:00 2001 From: KC Date: Tue, 27 Sep 2022 11:09:19 -0400 Subject: [PATCH 057/107] refreshSchema moved into the connection form service --- .../CreateConnection/CreateConnection.tsx | 24 ++++--------------- .../components/SchemaError.tsx | 6 ++--- .../ConnectionEdit/ConnectionEditService.tsx | 6 ++--- .../ConnectionForm/ConnectionFormService.tsx | 4 +++- .../components/ConnectionReplication.tsx | 17 +++---------- .../ConnectionForm/ConnectionFormFields.tsx | 4 +--- 6 files changed, 18 insertions(+), 43 deletions(-) diff --git a/airbyte-webapp/src/components/CreateConnection/CreateConnection.tsx b/airbyte-webapp/src/components/CreateConnection/CreateConnection.tsx index cbc28cfd4617..e89beaff0bb4 100644 --- a/airbyte-webapp/src/components/CreateConnection/CreateConnection.tsx +++ b/airbyte-webapp/src/components/CreateConnection/CreateConnection.tsx @@ -33,14 +33,9 @@ interface CreateConnectionProps { interface CreateConnectionPropsInner extends Pick { schemaError: SchemaErrorType; - onDiscoverSchema: () => Promise; } -const CreateConnectionInner: React.FC = ({ - schemaError, - onDiscoverSchema, - afterSubmitConnection, -}) => { +const CreateConnectionInner: React.FC = ({ schemaError, afterSubmitConnection }) => { const navigate = useNavigate(); const { mutateAsync: createConnection } = useCreateConnection(); @@ -96,7 +91,7 @@ const CreateConnectionInner: React.FC = ({ ); if (schemaError) { - return ; + return ; } return ( @@ -107,12 +102,7 @@ const CreateConnectionInner: React.FC = ({ - + = ({ source, dest }; return ( - + {isLoading ? ( ) : ( - + )} ); diff --git a/airbyte-webapp/src/components/CreateConnection/components/SchemaError.tsx b/airbyte-webapp/src/components/CreateConnection/components/SchemaError.tsx index 49949c20c504..075805e7554f 100644 --- a/airbyte-webapp/src/components/CreateConnection/components/SchemaError.tsx +++ b/airbyte-webapp/src/components/CreateConnection/components/SchemaError.tsx @@ -3,20 +3,20 @@ import { JobItem } from "components/JobItem/JobItem"; import { SynchronousJobRead } from "core/request/AirbyteClient"; import { LogsRequestError } from "core/request/LogsRequestError"; +import { useConnectionFormService } from "hooks/services/ConnectionForm/ConnectionFormService"; import TryAfterErrorBlock from "./TryAfterErrorBlock"; export const SchemaError = ({ schemaErrorStatus, - onDiscoverSchema, }: { schemaErrorStatus: { status: number; response: SynchronousJobRead } | null; - onDiscoverSchema: () => Promise; }) => { const job = LogsRequestError.extractJobInfo(schemaErrorStatus); + const { refreshSchema } = useConnectionFormService(); return ( - + {job && } ); diff --git a/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.tsx b/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.tsx index fdc9503c8952..3e374e854a6d 100644 --- a/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.tsx +++ b/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.tsx @@ -40,14 +40,14 @@ const useConnectionEdit = ({ connectionId }: ConnectionEditProps) => { }; }; -const ConnectionEditContext = createContext | null>(null); +const ConnectionEditContext = createContext, "refreshSchema"> | null>(null); export const ConnectionEditServiceProvider: React.FC = ({ children, ...props }) => { - const data = useConnectionEdit(props); + const { refreshSchema, ...data } = useConnectionEdit(props); // TODO: Mode needs to be able to be set to 'readonly' return ( - + {children} diff --git a/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx b/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx index bc6463be92b6..c630f187ab63 100644 --- a/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx +++ b/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx @@ -23,6 +23,7 @@ export type ConnectionOrPartialConnection = export interface ConnectionServiceProps { connection: ConnectionOrPartialConnection; mode: ConnectionFormMode; + refreshSchema: () => void; } export const tidyConnectionFormValues = ( @@ -39,7 +40,7 @@ export const tidyConnectionFormValues = ( return formValues; }; -const useConnectionForm = ({ connection, mode }: ConnectionServiceProps) => { +const useConnectionForm = ({ connection, mode, refreshSchema }: ConnectionServiceProps) => { const destDefinition = useGetDestinationDefinitionSpecification(connection.destination.destinationDefinitionId); const initialValues = useInitialValues(connection, destDefinition); const { formatMessage } = useIntl(); @@ -62,6 +63,7 @@ const useConnectionForm = ({ connection, mode }: ConnectionServiceProps) => { initialValues, setSubmitError, getErrorMessage, + refreshSchema, }; }; diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx index 18fe85950ea3..14f01ede3c4e 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx @@ -47,14 +47,8 @@ export const ConnectionReplication: React.FC = () => { const [saved, setSaved] = useState(false); const formId = useUniqueFormId(); - const { - connection, - schemaRefreshing, - schemaHasBeenRefreshed, - updateConnection, - setSchemaHasBeenRefreshed, - refreshSchema, - } = useConnectionEditService(); + const { connection, schemaRefreshing, schemaHasBeenRefreshed, updateConnection, setSchemaHasBeenRefreshed } = + useConnectionEditService(); const { initialValues, getErrorMessage, setSubmitError } = useConnectionFormService(); useTrackPage(PageTrackingCodes.CONNECTIONS_ITEM_REPLICATION); @@ -175,12 +169,7 @@ export const ConnectionReplication: React.FC = () => { {({ values, isSubmitting, isValid, dirty, resetForm }) => ( - + void; } export const ConnectionFormFields: React.FC = ({ @@ -33,9 +32,8 @@ export const ConnectionFormFields: React.FC = ({ values, isSubmitting, dirty, - refreshSchema, }) => { - const { mode } = useConnectionFormService(); + const { mode, refreshSchema } = useConnectionFormService(); const { formatMessage } = useIntl(); const formContainerClassnames = classNames(className, styles.formContainer); From 33ce0cc89a04b3ac258fbf6c2fb7e54de00725e6 Mon Sep 17 00:00:00 2001 From: KC Date: Tue, 27 Sep 2022 11:26:53 -0400 Subject: [PATCH 058/107] Re-add TODO --- .../src/hooks/services/ConnectionForm/ConnectionFormService.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx b/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx index c630f187ab63..095f95af0165 100644 --- a/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx +++ b/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx @@ -31,6 +31,7 @@ export const tidyConnectionFormValues = ( workspaceId: string, operations?: OperationRead[] ): ValuesProps => { + // TODO: We should try to fix the types so we don't need the casting. const formValues: ConnectionFormValues = connectionValidationSchema.cast(values, { context: { isRequest: true }, }) as unknown as ConnectionFormValues; From c9baa9a6d911c18f85b965c65c0e5d9627facb83 Mon Sep 17 00:00:00 2001 From: KC Date: Tue, 27 Sep 2022 11:31:52 -0400 Subject: [PATCH 059/107] schemaErrorStatus => schemaError --- .../src/components/CreateConnection/CreateConnection.tsx | 2 +- .../components/CreateConnection/components/SchemaError.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/airbyte-webapp/src/components/CreateConnection/CreateConnection.tsx b/airbyte-webapp/src/components/CreateConnection/CreateConnection.tsx index e89beaff0bb4..95ba63742e60 100644 --- a/airbyte-webapp/src/components/CreateConnection/CreateConnection.tsx +++ b/airbyte-webapp/src/components/CreateConnection/CreateConnection.tsx @@ -91,7 +91,7 @@ const CreateConnectionInner: React.FC = ({ schemaErr ); if (schemaError) { - return ; + return ; } return ( diff --git a/airbyte-webapp/src/components/CreateConnection/components/SchemaError.tsx b/airbyte-webapp/src/components/CreateConnection/components/SchemaError.tsx index 075805e7554f..20bb2fdd6864 100644 --- a/airbyte-webapp/src/components/CreateConnection/components/SchemaError.tsx +++ b/airbyte-webapp/src/components/CreateConnection/components/SchemaError.tsx @@ -8,11 +8,11 @@ import { useConnectionFormService } from "hooks/services/ConnectionForm/Connecti import TryAfterErrorBlock from "./TryAfterErrorBlock"; export const SchemaError = ({ - schemaErrorStatus, + schemaError, }: { - schemaErrorStatus: { status: number; response: SynchronousJobRead } | null; + schemaError: { status: number; response: SynchronousJobRead } | null; }) => { - const job = LogsRequestError.extractJobInfo(schemaErrorStatus); + const job = LogsRequestError.extractJobInfo(schemaError); const { refreshSchema } = useConnectionFormService(); return ( From cdcb20d76a15aa314a2bb2f0bd0d5836a728183a Mon Sep 17 00:00:00 2001 From: KC Date: Tue, 27 Sep 2022 11:33:45 -0400 Subject: [PATCH 060/107] CreateConnectionName -> CreateConnectionNameField --- .../src/components/CreateConnection/CreateConnection.tsx | 4 ++-- ...Name.module.scss => CreateConnectionNameField.module.scss} | 0 ...CreateConnectionName.tsx => CreateConnectionNameField.tsx} | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename airbyte-webapp/src/components/CreateConnection/components/{CreateConnectionName.module.scss => CreateConnectionNameField.module.scss} (100%) rename airbyte-webapp/src/components/CreateConnection/components/{CreateConnectionName.tsx => CreateConnectionNameField.tsx} (93%) diff --git a/airbyte-webapp/src/components/CreateConnection/CreateConnection.tsx b/airbyte-webapp/src/components/CreateConnection/CreateConnection.tsx index 95ba63742e60..593a85f2f414 100644 --- a/airbyte-webapp/src/components/CreateConnection/CreateConnection.tsx +++ b/airbyte-webapp/src/components/CreateConnection/CreateConnection.tsx @@ -21,7 +21,7 @@ import { OperationsSection } from "views/Connection/ConnectionForm/components/Op import { ConnectionFormFields } from "views/Connection/ConnectionForm/ConnectionFormFields"; import { connectionValidationSchema, FormikConnectionFormValues } from "views/Connection/ConnectionForm/formConfig"; -import { CreateConnectionName } from "./components/CreateConnectionName"; +import { CreateConnectionNameField } from "./components/CreateConnectionNameField"; import { SchemaError } from "./components/SchemaError"; import styles from "./CreateConnection.module.scss"; @@ -101,7 +101,7 @@ const CreateConnectionInner: React.FC = ({ schemaErr {({ values, isSubmitting, isValid, dirty }) => ( - + { +export const CreateConnectionNameField = () => { const { formatMessage } = useIntl(); return ( From 4203136a6ace5d14e888a57569c884752f924b6d Mon Sep 17 00:00:00 2001 From: KC Date: Tue, 27 Sep 2022 11:45:53 -0400 Subject: [PATCH 061/107] Logic for readonly vs edit --- .../services/ConnectionEdit/ConnectionEditService.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.tsx b/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.tsx index 3e374e854a6d..858bd5780364 100644 --- a/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.tsx +++ b/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.tsx @@ -1,7 +1,7 @@ import { useContext, useState, createContext, useCallback } from "react"; import { useAsyncFn } from "react-use"; -import { WebBackendConnectionUpdate } from "core/request/AirbyteClient"; +import { ConnectionStatus, WebBackendConnectionUpdate } from "core/request/AirbyteClient"; import { ConnectionFormServiceProvider } from "../ConnectionForm/ConnectionFormService"; import { useGetConnection, useUpdateConnection, useWebConnectionService } from "../useConnectionHook"; @@ -44,10 +44,13 @@ const ConnectionEditContext = createContext = ({ children, ...props }) => { const { refreshSchema, ...data } = useConnectionEdit(props); - // TODO: Mode needs to be able to be set to 'readonly' return ( - + {children} From 7ac8eb1ce9a7e25e8410e1681faddaa1b7522e1c Mon Sep 17 00:00:00 2001 From: KC Date: Tue, 27 Sep 2022 11:55:47 -0400 Subject: [PATCH 062/107] Added TODO, fixed a re-initialize issue --- .../services/ConnectionEdit/ConnectionEditService.tsx | 1 + .../components/ConnectionReplication.tsx | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.tsx b/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.tsx index 858bd5780364..4aca82380588 100644 --- a/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.tsx +++ b/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.tsx @@ -24,6 +24,7 @@ const useConnectionEdit = ({ connectionId }: ConnectionEditProps) => { const updateConnection = useCallback( async (connection: WebBackendConnectionUpdate) => { + // TODO: Check if the form is dirty before firing off an update action setConnection(await updateConnectionAction(connection)); }, [updateConnectionAction] diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx index 14f01ede3c4e..aa6498bd82fc 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx @@ -165,7 +165,12 @@ export const ConnectionReplication: React.FC = () => { return ( {!schemaRefreshing && connection ? ( - + {({ values, isSubmitting, isValid, dirty, resetForm }) => ( From a0a9ff5bb01c1a2c8a191016d76fcff6cd869cfa Mon Sep 17 00:00:00 2001 From: KC Date: Tue, 27 Sep 2022 13:57:36 -0400 Subject: [PATCH 063/107] Remove unused ConnectionForm component --- .../ConnectionForm/ConnectionForm.test.tsx | 126 ------------------ .../ConnectionForm/ConnectionForm.tsx | 1 - 2 files changed, 127 deletions(-) delete mode 100644 airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.test.tsx delete mode 100644 airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.test.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.test.tsx deleted file mode 100644 index 3a8993cbfadb..000000000000 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.test.tsx +++ /dev/null @@ -1,126 +0,0 @@ -export {}; -// import { waitFor } from "@testing-library/react"; -// import userEvent from "@testing-library/user-event"; -// import { render } from "test-utils/testutils"; - -// import { -// ConnectionStatus, -// DestinationRead, -// NamespaceDefinitionType, -// SourceRead, -// WebBackendConnectionRead, -// } from "core/request/AirbyteClient"; -// import { ConfirmationModalService } from "hooks/services/ConfirmationModal/ConfirmationModalService"; -// import { ConnectionFormServiceProvider } from "hooks/services/Connection/ConnectionFormService"; - -// import { ConnectionForm, ConnectionFormMode } from "./ConnectionForm"; - -// const mockSource: SourceRead = { -// sourceId: "test-source", -// name: "test source", -// sourceName: "test-source-name", -// workspaceId: "test-workspace-id", -// sourceDefinitionId: "test-source-definition-id", -// connectionConfiguration: undefined, -// }; - -// const mockDestination: DestinationRead = { -// destinationId: "test-destination", -// name: "test destination", -// destinationName: "test destination name", -// workspaceId: "test-workspace-id", -// destinationDefinitionId: "test-destination-definition-id", -// connectionConfiguration: undefined, -// }; - -// const mockConnection: WebBackendConnectionRead = { -// connectionId: "test-connection", -// name: "test connection", -// prefix: "test", -// sourceId: "test-source", -// destinationId: "test-destination", -// status: ConnectionStatus.active, -// scheduleType: "manual", -// scheduleData: undefined, -// syncCatalog: { -// streams: [], -// }, -// namespaceDefinition: NamespaceDefinitionType.source, -// namespaceFormat: "", -// operationIds: [], -// source: mockSource, -// destination: mockDestination, -// operations: [], -// catalogId: "", -// isSyncing: false, -// }; - -// jest.mock("services/connector/DestinationDefinitionSpecificationService", () => { -// return { -// useGetDestinationDefinitionSpecification: () => { -// return "destinationDefinition"; -// }, -// }; -// }); - -// jest.mock("services/workspaces/WorkspacesService", () => { -// return { -// useCurrentWorkspace: () => { -// return "currentWorkspace"; -// }, -// useCurrentWorkspaceId: () => { -// return "currentWorkspace"; -// }, -// }; -// }); - -// const renderConnectionForm = (mode: ConnectionFormMode, connection = mockConnection) => -// render( -// -// -// -// -// -// ); - -// describe("", () => { -// let container: HTMLElement; -// describe("edit mode", () => { -// beforeEach(async () => { -// const renderResult = await renderConnectionForm("edit"); - -// container = renderResult.container; -// }); -// it("renders relevant items", async () => { -// const prefixInput = container.querySelector("input[data-testid='prefixInput']"); -// expect(prefixInput).toBeInTheDocument(); - -// // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -// userEvent.type(prefixInput!, "{selectall}{del}prefix"); -// await waitFor(() => userEvent.keyboard("{enter}")); -// }); -// it("pointer events are not turned off anywhere in the component", async () => { -// expect(container.innerHTML).toContain("checkbox"); -// }); -// }); -// describe("readonly mode", () => { -// beforeEach(async () => { -// const renderResult = await renderConnectionForm("readonly"); - -// container = renderResult.container; -// }); -// it("renders only relevant items for the mode", async () => { -// const prefixInput = container.querySelector("input[data-testid='prefixInput']"); -// expect(prefixInput).toBeInTheDocument(); -// }); -// it("pointer events are turned off in the fieldset", async () => { -// expect(container.innerHTML).not.toContain("checkbox"); -// }); -// }); -// }); diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx deleted file mode 100644 index 2b51fe647264..000000000000 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx +++ /dev/null @@ -1 +0,0 @@ -export type ConnectionFormMode = "create" | "edit" | "readonly"; From f84e2143b38e17bbe5f809283963207d5af75c0c Mon Sep 17 00:00:00 2001 From: KC Date: Tue, 27 Sep 2022 13:58:02 -0400 Subject: [PATCH 064/107] Improved refresh schema logic --- .../ConnectionEdit/ConnectionEditService.tsx | 13 +++++++++---- .../ConnectionForm/ConnectionFormService.tsx | 2 +- .../components/ConnectionReplication.tsx | 19 ++++++++++++++----- .../components/NormalizationField.tsx | 3 +-- .../components/PreventRefreshOnDirty.tsx | 9 ++++++--- .../components/SyncCatalogField.tsx | 3 +-- 6 files changed, 32 insertions(+), 17 deletions(-) diff --git a/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.tsx b/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.tsx index 4aca82380588..e272e836d72f 100644 --- a/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.tsx +++ b/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.tsx @@ -15,10 +15,15 @@ const useConnectionEdit = ({ connectionId }: ConnectionEditProps) => { const connectionService = useWebConnectionService(); const [schemaHasBeenRefreshed, setSchemaHasBeenRefreshed] = useState(false); - const [{ loading: schemaRefreshing }, refreshSchema] = useAsyncFn(async () => { - setConnection(await connectionService.getConnection(connectionId, true)); - setSchemaHasBeenRefreshed(true); - }, [connectionId]); + // TODO: Pull error out here, utilize down-line. Maybe use schema error component like on the create page? + const [{ loading: schemaRefreshing }, refreshSchema] = useAsyncFn( + async (refreshSchema = false) => { + const refreshedConnection = await connectionService.getConnection(connectionId, refreshSchema); + setConnection(refreshedConnection); + setSchemaHasBeenRefreshed(true); + }, + [connectionId] + ); const { mutateAsync: updateConnectionAction, isLoading: connectionUpdating } = useUpdateConnection(); diff --git a/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx b/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx index 095f95af0165..bf73f4065e50 100644 --- a/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx +++ b/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx @@ -23,7 +23,7 @@ export type ConnectionOrPartialConnection = export interface ConnectionServiceProps { connection: ConnectionOrPartialConnection; mode: ConnectionFormMode; - refreshSchema: () => void; + refreshSchema: (refreshSchema?: boolean) => Promise; } export const tidyConnectionFormValues = ( diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx index aa6498bd82fc..e23c410993fb 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx @@ -1,5 +1,5 @@ import { Form, Formik, FormikHelpers } from "formik"; -import React, { useCallback, useEffect, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useUnmount } from "react-use"; import styled from "styled-components"; @@ -49,7 +49,7 @@ export const ConnectionReplication: React.FC = () => { const formId = useUniqueFormId(); const { connection, schemaRefreshing, schemaHasBeenRefreshed, updateConnection, setSchemaHasBeenRefreshed } = useConnectionEditService(); - const { initialValues, getErrorMessage, setSubmitError } = useConnectionFormService(); + const { initialValues, getErrorMessage, setSubmitError, refreshSchema } = useConnectionFormService(); useTrackPage(PageTrackingCodes.CONNECTIONS_ITEM_REPLICATION); useUnmount(() => { @@ -148,10 +148,15 @@ export const ConnectionReplication: React.FC = () => { ] ); + const catalogIsDifferent = useMemo( + () => connection.catalogDiff?.transforms && connection.catalogDiff.transforms?.length > 0, + [connection.catalogDiff?.transforms] + ); + useEffect(() => { // If we have a catalogDiff we always want to show the modal const { catalogDiff, syncCatalog } = connection; - if (catalogDiff?.transforms && catalogDiff.transforms?.length > 0) { + if (catalogDiff && catalogIsDifferent) { openModal({ title: formatMessage({ id: "connection.updateSchema.completed" }), preventCancel: true, @@ -160,7 +165,7 @@ export const ConnectionReplication: React.FC = () => { ), }); } - }, [connection, formatMessage, openModal]); + }, [catalogIsDifferent, connection, formatMessage, openModal]); return ( @@ -178,7 +183,11 @@ export const ConnectionReplication: React.FC = () => { { + resetForm={async () => { + if (catalogIsDifferent) { + // Refetch original connection + schema if different + await refreshSchema(); + } resetForm(); setSchemaHasBeenRefreshed(false); }} diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/components/NormalizationField.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/components/NormalizationField.tsx index a2f1c63988f5..48df3c917852 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/components/NormalizationField.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/components/NormalizationField.tsx @@ -7,8 +7,7 @@ import { LabeledRadioButton, Link } from "components"; import { useConfig } from "config"; import { NormalizationType } from "core/domain/connection/operation"; - -import { ConnectionFormMode } from "../ConnectionForm"; +import { ConnectionFormMode } from "hooks/services/ConnectionForm/ConnectionFormService"; const Normalization = styled.div` margin: 16px 0; diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/components/PreventRefreshOnDirty.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/components/PreventRefreshOnDirty.tsx index ac4fb0afc35f..a9f41f126f3e 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/components/PreventRefreshOnDirty.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/components/PreventRefreshOnDirty.tsx @@ -2,7 +2,10 @@ import { useCallback } from "react"; import { useConfirmationModalService } from "hooks/services/ConfirmationModal"; -export const usePreventRefreshOnDirty = (dirty: boolean, refreshSourceSchema: () => void) => { +export const usePreventRefreshOnDirty = ( + dirty: boolean, + refreshSourceSchema: (refreshSchema?: boolean) => Promise +) => { const { openConfirmationModal, closeConfirmationModal } = useConfirmationModalService(); return useCallback(() => { @@ -13,11 +16,11 @@ export const usePreventRefreshOnDirty = (dirty: boolean, refreshSourceSchema: () submitButtonText: "connection.updateSchema.formChanged.confirm", onSubmit: () => { closeConfirmationModal(); - refreshSourceSchema(); + refreshSourceSchema(true); }, }); } else { - refreshSourceSchema(); + refreshSourceSchema(true); } }, [closeConfirmationModal, dirty, openConfirmationModal, refreshSourceSchema]); }; diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/components/SyncCatalogField.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/components/SyncCatalogField.tsx index ab334704423a..e74a4c51ba24 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/components/SyncCatalogField.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/components/SyncCatalogField.tsx @@ -13,12 +13,11 @@ import { useConfig } from "config"; import { SyncSchemaStream } from "core/domain/catalog"; import { DestinationSyncMode } from "core/request/AirbyteClient"; import { BatchEditProvider, useBulkEdit } from "hooks/services/BulkEdit/BulkEditService"; -import { useConnectionFormService } from "hooks/services/ConnectionForm/ConnectionFormService"; +import { ConnectionFormMode, useConnectionFormService } from "hooks/services/ConnectionForm/ConnectionFormService"; import { naturalComparatorBy } from "utils/objects"; import CatalogTree from "views/Connection/CatalogTree"; import { BulkHeader } from "../../CatalogTree/components/BulkHeader"; -import { ConnectionFormMode } from "../ConnectionForm"; import Search from "./Search"; import styles from "./SyncCatalogField.module.scss"; From f05a936db52af8bde69b8139da21e41a61809423 Mon Sep 17 00:00:00 2001 From: KC Date: Tue, 27 Sep 2022 16:03:06 -0400 Subject: [PATCH 065/107] undoing some of the cancel work as it felt like the wrong place to do it --- .../ConnectionEdit/ConnectionEditService.tsx | 13 +++++-------- .../ConnectionForm/ConnectionFormService.tsx | 2 +- .../components/ConnectionReplication.tsx | 17 ++++------------- .../components/PreventRefreshOnDirty.tsx | 9 +++------ 4 files changed, 13 insertions(+), 28 deletions(-) diff --git a/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.tsx b/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.tsx index e272e836d72f..6a8559349076 100644 --- a/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.tsx +++ b/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.tsx @@ -16,14 +16,11 @@ const useConnectionEdit = ({ connectionId }: ConnectionEditProps) => { const [schemaHasBeenRefreshed, setSchemaHasBeenRefreshed] = useState(false); // TODO: Pull error out here, utilize down-line. Maybe use schema error component like on the create page? - const [{ loading: schemaRefreshing }, refreshSchema] = useAsyncFn( - async (refreshSchema = false) => { - const refreshedConnection = await connectionService.getConnection(connectionId, refreshSchema); - setConnection(refreshedConnection); - setSchemaHasBeenRefreshed(true); - }, - [connectionId] - ); + const [{ loading: schemaRefreshing }, refreshSchema] = useAsyncFn(async () => { + const refreshedConnection = await connectionService.getConnection(connectionId, true); + setConnection(refreshedConnection); + setSchemaHasBeenRefreshed(true); + }, [connectionId]); const { mutateAsync: updateConnectionAction, isLoading: connectionUpdating } = useUpdateConnection(); diff --git a/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx b/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx index bf73f4065e50..48f08ded6aa5 100644 --- a/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx +++ b/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx @@ -23,7 +23,7 @@ export type ConnectionOrPartialConnection = export interface ConnectionServiceProps { connection: ConnectionOrPartialConnection; mode: ConnectionFormMode; - refreshSchema: (refreshSchema?: boolean) => Promise; + refreshSchema: () => Promise; } export const tidyConnectionFormValues = ( diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx index e23c410993fb..bc10582e29fa 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx @@ -1,5 +1,5 @@ import { Form, Formik, FormikHelpers } from "formik"; -import React, { useCallback, useEffect, useMemo, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useUnmount } from "react-use"; import styled from "styled-components"; @@ -49,7 +49,7 @@ export const ConnectionReplication: React.FC = () => { const formId = useUniqueFormId(); const { connection, schemaRefreshing, schemaHasBeenRefreshed, updateConnection, setSchemaHasBeenRefreshed } = useConnectionEditService(); - const { initialValues, getErrorMessage, setSubmitError, refreshSchema } = useConnectionFormService(); + const { initialValues, getErrorMessage, setSubmitError } = useConnectionFormService(); useTrackPage(PageTrackingCodes.CONNECTIONS_ITEM_REPLICATION); useUnmount(() => { @@ -148,15 +148,10 @@ export const ConnectionReplication: React.FC = () => { ] ); - const catalogIsDifferent = useMemo( - () => connection.catalogDiff?.transforms && connection.catalogDiff.transforms?.length > 0, - [connection.catalogDiff?.transforms] - ); - useEffect(() => { // If we have a catalogDiff we always want to show the modal const { catalogDiff, syncCatalog } = connection; - if (catalogDiff && catalogIsDifferent) { + if (catalogDiff?.transforms && catalogDiff.transforms?.length > 0) { openModal({ title: formatMessage({ id: "connection.updateSchema.completed" }), preventCancel: true, @@ -165,7 +160,7 @@ export const ConnectionReplication: React.FC = () => { ), }); } - }, [catalogIsDifferent, connection, formatMessage, openModal]); + }, [connection, formatMessage, openModal]); return ( @@ -184,10 +179,6 @@ export const ConnectionReplication: React.FC = () => { isSubmitting={isSubmitting} dirty={dirty} resetForm={async () => { - if (catalogIsDifferent) { - // Refetch original connection + schema if different - await refreshSchema(); - } resetForm(); setSchemaHasBeenRefreshed(false); }} diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/components/PreventRefreshOnDirty.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/components/PreventRefreshOnDirty.tsx index a9f41f126f3e..b5e5a34eb317 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/components/PreventRefreshOnDirty.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/components/PreventRefreshOnDirty.tsx @@ -2,10 +2,7 @@ import { useCallback } from "react"; import { useConfirmationModalService } from "hooks/services/ConfirmationModal"; -export const usePreventRefreshOnDirty = ( - dirty: boolean, - refreshSourceSchema: (refreshSchema?: boolean) => Promise -) => { +export const usePreventRefreshOnDirty = (dirty: boolean, refreshSourceSchema: () => Promise) => { const { openConfirmationModal, closeConfirmationModal } = useConfirmationModalService(); return useCallback(() => { @@ -16,11 +13,11 @@ export const usePreventRefreshOnDirty = ( submitButtonText: "connection.updateSchema.formChanged.confirm", onSubmit: () => { closeConfirmationModal(); - refreshSourceSchema(true); + refreshSourceSchema(); }, }); } else { - refreshSourceSchema(true); + refreshSourceSchema(); } }, [closeConfirmationModal, dirty, openConfirmationModal, refreshSourceSchema]); }; From 5b269fb6ae52be25baf090b9eed9c5afa17fba6a Mon Sep 17 00:00:00 2001 From: KC Date: Tue, 27 Sep 2022 16:11:04 -0400 Subject: [PATCH 066/107] No longer need to know if the status is updating, that info is available from the form edit context --- .../pages/ConnectionItemPage/ConnectionItemPage.tsx | 10 +++------- .../components/ConnectionPageTitle.tsx | 8 ++------ .../ConnectionItemPage/components/EnabledControl.tsx | 8 +------- .../ConnectionItemPage/components/StatusMainInfo.tsx | 8 ++------ .../pages/ConnectionItemPage/components/StatusView.tsx | 1 - 5 files changed, 8 insertions(+), 27 deletions(-) diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionItemPage.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionItemPage.tsx index 11f19ca20fe4..4af8793af6b6 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionItemPage.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionItemPage.tsx @@ -1,4 +1,4 @@ -import React, { Suspense, useState } from "react"; +import React, { Suspense } from "react"; import { Navigate, Route, Routes, useParams } from "react-router-dom"; import { LoadingPage, MainPageWithScroll } from "components"; @@ -20,7 +20,6 @@ import { ConnectionSettingsRoutes } from "./ConnectionSettingsRoutes"; export const ConnectionItemPageInner: React.FC = () => { const { connection } = useConnectionEditService(); - const [isStatusUpdating, setStatusUpdating] = useState(false); useTrackPage(PageTrackingCodes.CONNECTIONS_ITEM); @@ -42,14 +41,11 @@ export const ConnectionItemPageInner: React.FC = () => { ]} /> } - pageTitle={} + pageTitle={} > }> - } - /> + } /> } /> void; -} - -export const ConnectionPageTitle: React.FC = ({ onStatusUpdating }) => { +export const ConnectionPageTitle: React.FC = () => { const params = useParams<{ id: string; "*": ConnectionSettingsRoutes }>(); const navigate = useNavigate(); const currentStep = params["*"] || ConnectionSettingsRoutes.STATUS; @@ -74,7 +70,7 @@ export const ConnectionPageTitle: React.FC = ({ onStat
- +
diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/EnabledControl.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/EnabledControl.tsx index 8827694e0504..11f28996ccf0 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/EnabledControl.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/EnabledControl.tsx @@ -1,6 +1,5 @@ import React from "react"; import { FormattedMessage } from "react-intl"; -import { useUpdateEffect } from "react-use"; import styled from "styled-components"; import { Switch } from "components"; @@ -31,10 +30,9 @@ const Content = styled.div` interface EnabledControlProps { disabled?: boolean; - onStatusUpdating?: (updating: boolean) => void; } -const EnabledControl: React.FC = ({ disabled, onStatusUpdating }) => { +const EnabledControl: React.FC = ({ disabled }) => { const analyticsService = useAnalyticsService(); const { connection, updateConnection, connectionUpdating } = useConnectionEditService(); @@ -58,10 +56,6 @@ const EnabledControl: React.FC = ({ disabled, onStatusUpdat }); }; - useUpdateEffect(() => { - onStatusUpdating?.(connectionUpdating); - }, [connectionUpdating]); - return ( diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/StatusMainInfo.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/StatusMainInfo.tsx index ec4d95c0e307..846017f2bf75 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/StatusMainInfo.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/StatusMainInfo.tsx @@ -15,11 +15,7 @@ import { useSourceDefinition } from "services/connector/SourceDefinitionService" import EnabledControl from "./EnabledControl"; import styles from "./StatusMainInfo.module.scss"; -interface StatusMainInfoProps { - onStatusUpdating?: (updating: boolean) => void; -} - -export const StatusMainInfo: React.FC = ({ onStatusUpdating }) => { +export const StatusMainInfo: React.FC = () => { const { connection: { source, destination, status }, } = useConnectionEditService(); @@ -54,7 +50,7 @@ export const StatusMainInfo: React.FC = ({ onStatusUpdating
{status !== ConnectionStatus.deprecated && (
- +
)}
diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/StatusView.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/StatusView.tsx index 74dc2483778a..b8b67cdabbe0 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/StatusView.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/StatusView.tsx @@ -39,7 +39,6 @@ interface ActiveJob { interface StatusViewProps { connection: WebBackendConnectionRead; - isStatusUpdating?: boolean; } const getJobRunningOrPending = (jobs: JobWithAttemptsRead[]) => { From 5c5c3b781e56613d1acbe9afbeb3531038467577 Mon Sep 17 00:00:00 2001 From: KC Date: Tue, 27 Sep 2022 16:17:03 -0400 Subject: [PATCH 067/107] Move close modal & confirmation modal logic to where it's used --- .../components/ConnectionReplication.tsx | 3 --- .../views/Connection/CatalogDiffModal/CatalogDiffModal.tsx | 7 +++++++ .../ConnectionForm/components/PreventRefreshOnDirty.tsx | 5 +++++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx index bc10582e29fa..dbe473bdf2a2 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx @@ -11,7 +11,6 @@ import { getFrequencyType } from "config/utils"; import { Action, Namespace } from "core/analytics"; import { toWebBackendConnectionUpdate } from "core/domain/connection"; import { PageTrackingCodes, useAnalyticsService, useTrackPage } from "hooks/services/Analytics"; -import { useConfirmationModalService } from "hooks/services/ConfirmationModal"; import { useConnectionEditService } from "hooks/services/ConnectionEdit/ConnectionEditService"; import { tidyConnectionFormValues, @@ -42,7 +41,6 @@ export const ConnectionReplication: React.FC = () => { const { formatMessage } = useIntl(); const { openModal, closeModal } = useModalService(); - const { closeConfirmationModal } = useConfirmationModalService(); const [saved, setSaved] = useState(false); @@ -54,7 +52,6 @@ export const ConnectionReplication: React.FC = () => { useTrackPage(PageTrackingCodes.CONNECTIONS_ITEM_REPLICATION); useUnmount(() => { closeModal(); - closeConfirmationModal(); }); const saveConnection = useCallback( diff --git a/airbyte-webapp/src/views/Connection/CatalogDiffModal/CatalogDiffModal.tsx b/airbyte-webapp/src/views/Connection/CatalogDiffModal/CatalogDiffModal.tsx index a616f9173a12..f4a2882501e5 100644 --- a/airbyte-webapp/src/views/Connection/CatalogDiffModal/CatalogDiffModal.tsx +++ b/airbyte-webapp/src/views/Connection/CatalogDiffModal/CatalogDiffModal.tsx @@ -1,9 +1,11 @@ import { useMemo } from "react"; import { FormattedMessage } from "react-intl"; +import { useUnmount } from "react-use"; import { Button } from "components"; import { AirbyteCatalog, CatalogDiff } from "core/request/AirbyteClient"; +import { useModalService } from "hooks/services/Modal"; import { ModalBody, ModalFooter } from "../../../components/Modal"; import styles from "./CatalogDiffModal.module.scss"; @@ -18,11 +20,16 @@ interface CatalogDiffModalProps { } export const CatalogDiffModal: React.FC = ({ catalogDiff, catalog, onClose }) => { + const { closeModal } = useModalService(); const { newItems, removedItems, changedItems } = useMemo( () => getSortedDiff(catalogDiff.transforms), [catalogDiff.transforms] ); + useUnmount(() => { + closeModal(); + }); + return ( <> diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/components/PreventRefreshOnDirty.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/components/PreventRefreshOnDirty.tsx index b5e5a34eb317..7e3ce4352bb2 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/components/PreventRefreshOnDirty.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/components/PreventRefreshOnDirty.tsx @@ -1,10 +1,15 @@ import { useCallback } from "react"; +import { useUnmount } from "react-use"; import { useConfirmationModalService } from "hooks/services/ConfirmationModal"; export const usePreventRefreshOnDirty = (dirty: boolean, refreshSourceSchema: () => Promise) => { const { openConfirmationModal, closeConfirmationModal } = useConfirmationModalService(); + useUnmount(() => { + closeConfirmationModal(); + }); + return useCallback(() => { if (dirty) { openConfirmationModal({ From 4578dddf3da2ac3b4961dcdb93f0c4ea014452f2 Mon Sep 17 00:00:00 2001 From: KC Date: Tue, 27 Sep 2022 16:18:09 -0400 Subject: [PATCH 068/107] Remove unnecessary useUnmount --- .../ConnectionItemPage/components/ConnectionReplication.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx index dbe473bdf2a2..fcb3214c4850 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx @@ -1,7 +1,6 @@ import { Form, Formik, FormikHelpers } from "formik"; import React, { useCallback, useEffect, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; -import { useUnmount } from "react-use"; import styled from "styled-components"; import { FormChangeTracker } from "components/FormChangeTracker"; @@ -40,7 +39,7 @@ export const ConnectionReplication: React.FC = () => { const workspaceId = useCurrentWorkspaceId(); const { formatMessage } = useIntl(); - const { openModal, closeModal } = useModalService(); + const { openModal } = useModalService(); const [saved, setSaved] = useState(false); @@ -50,9 +49,6 @@ export const ConnectionReplication: React.FC = () => { const { initialValues, getErrorMessage, setSubmitError } = useConnectionFormService(); useTrackPage(PageTrackingCodes.CONNECTIONS_ITEM_REPLICATION); - useUnmount(() => { - closeModal(); - }); const saveConnection = useCallback( async (values: ValuesProps, { skipReset }: { skipReset: boolean }) => { From 6997f452be019ded09e933a49d0321d40803d766 Mon Sep 17 00:00:00 2001 From: KC Date: Tue, 27 Sep 2022 16:37:33 -0400 Subject: [PATCH 069/107] Cleanup and removal of unused --- .../components/ConnectionReplication.tsx | 1 + .../components/ResetWarningModal.tsx | 1 + .../ConnectionForm/ConnectionForm.module.scss | 10 ---------- .../ConnectionForm/ConnectionFormFields.tsx | 11 ++--------- .../ConnectionForm/components/Section.module.scss | 1 + 5 files changed, 5 insertions(+), 19 deletions(-) delete mode 100644 airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.module.scss diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx index fcb3214c4850..51a514964376 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.tsx @@ -85,6 +85,7 @@ export const ConnectionReplication: React.FC = () => { // Detect whether the catalog has any differences in its enabled streams compared to the original one. // This could be due to user changes (e.g. in the sync mode) or due to new/removed // streams due to a "refreshed source schema". + // TODO: Can pass this value into the saveConnection function for the analytics call so we don't compare twice const hasCatalogChanged = !equal( formValues.syncCatalog.streams .filter((s) => s.config?.selected) diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ResetWarningModal.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ResetWarningModal.tsx index 5408b1a196a1..c5bff88bae7d 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ResetWarningModal.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ResetWarningModal.tsx @@ -15,6 +15,7 @@ export const ResetWarningModal: React.FC = ({ onCancel, const { formatMessage } = useIntl(); const [withReset, setWithReset] = useState(true); const requireFullReset = stateType === ConnectionStateType.legacy; + return ( <> diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.module.scss b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.module.scss deleted file mode 100644 index 40f6c83f5264..000000000000 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.module.scss +++ /dev/null @@ -1,10 +0,0 @@ -.formContainer { - display: flex; - flex-direction: column; - gap: 10px; -} - -.connectionFormContainer { - width: 100%; - padding: 0 20px; -} diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.tsx index 642faa01c8a9..9dc07a206601 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.tsx @@ -21,22 +21,15 @@ import styles from "./ConnectionFormFields.module.scss"; import { FormikConnectionFormValues } from "./formConfig"; interface ConnectionFormFieldsProps { - className?: string; values: ValuesProps | FormikConnectionFormValues; isSubmitting: boolean; dirty: boolean; } -export const ConnectionFormFields: React.FC = ({ - className, - values, - isSubmitting, - dirty, -}) => { +export const ConnectionFormFields: React.FC = ({ values, isSubmitting, dirty }) => { const { mode, refreshSchema } = useConnectionFormService(); const { formatMessage } = useIntl(); - const formContainerClassnames = classNames(className, styles.formContainer); const readonlyClass = classNames({ [styles.readonly]: mode === "readonly", }); @@ -44,7 +37,7 @@ export const ConnectionFormFields: React.FC = ({ const doRefreshSchema = usePreventRefreshOnDirty(dirty, refreshSchema); return ( -
+
}>
diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/components/Section.module.scss b/airbyte-webapp/src/views/Connection/ConnectionForm/components/Section.module.scss index c2ab970546d6..94531f4b7f92 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/components/Section.module.scss +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/components/Section.module.scss @@ -4,6 +4,7 @@ flex-direction: column; gap: 15px; + // TODO: We may be able to get rid of this. &:not(:last-child) { box-shadow: 0 1px 0 rgba(139, 139, 160, 25%); } From 5896e661d5bf1c984292beb042b37fdfab7ddeaf Mon Sep 17 00:00:00 2001 From: KC Date: Wed, 28 Sep 2022 11:06:42 -0400 Subject: [PATCH 070/107] Somehow missed removing this --- .../Connection/ConnectionFormService.test.tsx | 217 ------------------ .../Connection/ConnectionFormService.tsx | 126 ---------- 2 files changed, 343 deletions(-) delete mode 100644 airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.test.tsx delete mode 100644 airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx diff --git a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.test.tsx b/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.test.tsx deleted file mode 100644 index 91fa31ddb4e6..000000000000 --- a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.test.tsx +++ /dev/null @@ -1,217 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { act } from "@testing-library/react"; -import { renderHook } from "@testing-library/react-hooks"; -import React from "react"; -import { MemoryRouter } from "react-router-dom"; -import mockConnection from "test-utils/mock-data/mockConnection.json"; -import mockDest from "test-utils/mock-data/mockDestinationDefinition.json"; -import mockWorkspace from "test-utils/mock-data/mockWorkspace.json"; -import { TestWrapper } from "test-utils/testutils"; - -import { ConnectionScheduleType, WebBackendConnectionRead } from "core/request/AirbyteClient"; - -import { ModalCancel } from "../Modal"; -import { - ConnectionFormServiceProvider, - ConnectionServiceProps, - useConnectionFormService, -} from "./ConnectionFormService"; - -["services/workspaces/WorkspacesService"].forEach((s) => - jest.mock(s, () => ({ - useCurrentWorkspaceId: () => mockWorkspace.workspaceId, - useCurrentWorkspace: () => mockWorkspace, - })) -); - -jest.mock("../FormChangeTracker", () => ({ - useFormChangeTrackerService: () => ({ clearFormChange: () => null }), - useUniqueFormId: () => "blah", -})); -jest.mock("services/connector/DestinationDefinitionSpecificationService", () => ({ - useGetDestinationDefinitionSpecification: () => mockDest, -})); - -describe("ConnectionFormService", () => { - const Wrapper: React.FC = ({ children, ...props }) => ( - - - {children} - - - ); - - const onSubmit = jest.fn(); - const onAfterSubmit = jest.fn(); - const onCancel = jest.fn(); - - beforeEach(() => { - onSubmit.mockReset(); - onAfterSubmit.mockReset(); - onCancel.mockReset(); - }); - - it("should call onSubmit when submitted", async () => { - const { result } = renderHook(useConnectionFormService, { - wrapper: Wrapper, - initialProps: { - connection: mockConnection as WebBackendConnectionRead, - mode: "create", - formId: Math.random().toString(), - onSubmit, - onAfterSubmit, - onCancel, - formDirty: false, - }, - }); - - const resetForm = jest.fn(); - const testValues: any = {}; - await act(async () => { - await result.current.onFormSubmit(testValues, { resetForm } as any); - }); - - expect(resetForm).toBeCalledWith({ values: testValues }); - expect(onSubmit).toBeCalledWith({ - operations: [], - scheduleData: { - cron: { - cronExpression: undefined, - cronTimeZone: undefined, - }, - }, - syncCatalog: { - streams: undefined, - }, - }); - expect(onAfterSubmit).toBeCalledWith(); - expect(result.current.errorMessage).toBe(null); - }); - - const expectation = { - [ConnectionScheduleType.basic]: { - basicSchedule: { - timeUnit: undefined, - units: undefined, - }, - }, - [ConnectionScheduleType.manual]: undefined, - [ConnectionScheduleType.cron]: { - cron: { - cronExpression: undefined, - cronTimeZone: undefined, - }, - }, - }; - - Object.values(ConnectionScheduleType).forEach((scheduleType) => { - it(`should return expected results when onSubmit is called with ${scheduleType}`, async () => { - const { result } = renderHook(useConnectionFormService, { - wrapper: Wrapper, - initialProps: { - connection: mockConnection as WebBackendConnectionRead, - mode: "create", - formId: Math.random().toString(), - onSubmit, - onAfterSubmit, - onCancel, - formDirty: false, - }, - }); - - const resetForm = jest.fn(); - const testValues: any = { - scheduleType, - }; - await act(async () => { - await result.current.onFormSubmit(testValues, { resetForm } as any); - }); - - expect(resetForm).toBeCalledWith({ values: testValues }); - expect(onSubmit).toBeCalledWith({ - operations: [], - scheduleData: expectation[scheduleType], - scheduleType, - syncCatalog: { - streams: undefined, - }, - }); - expect(onAfterSubmit).toBeCalledWith(); - expect(result.current.errorMessage).toBe(null); - }); - }); - - it("should catch if onSubmit throws and generate an error message", async () => { - const errorMessage = "asdf"; - onSubmit.mockImplementation(async () => { - throw new Error(errorMessage); - }); - - const { result } = renderHook(useConnectionFormService, { - wrapper: Wrapper, - initialProps: { - connection: mockConnection as WebBackendConnectionRead, - mode: "create", - formId: Math.random().toString(), - onSubmit, - onAfterSubmit, - onCancel, - formDirty: false, - }, - }); - - const resetForm = jest.fn(); - const testValues: any = {}; - await act(async () => { - await result.current.onFormSubmit(testValues, { resetForm } as any); - }); - - expect(result.current.errorMessage).toBe(errorMessage); - expect(resetForm).not.toHaveBeenCalled(); - }); - - it("should catch if onSubmit throws but not generate an error if it's a ModalCancel error", async () => { - onSubmit.mockImplementation(async () => { - throw new ModalCancel(); - }); - - const { result } = renderHook(useConnectionFormService, { - wrapper: Wrapper, - initialProps: { - connection: mockConnection as WebBackendConnectionRead, - mode: "create", - formId: Math.random().toString(), - onSubmit, - onAfterSubmit, - onCancel, - formDirty: false, - }, - }); - - const resetForm = jest.fn(); - const testValues: any = {}; - await act(async () => { - await result.current.onFormSubmit(testValues, { resetForm } as any); - }); - - expect(result.current.errorMessage).toBe(null); - expect(resetForm).not.toHaveBeenCalled(); - }); - - it("should render the generic form invalid error message if the form is dirty and there has not been a submit error", async () => { - const { result } = renderHook(useConnectionFormService, { - wrapper: Wrapper, - initialProps: { - connection: mockConnection as WebBackendConnectionRead, - mode: "create", - formId: Math.random().toString(), - onSubmit, - onAfterSubmit, - onCancel, - formDirty: true, - }, - }); - - expect(result.current.errorMessage).toBe("The form is invalid. Please make sure that all fields are correct."); - }); -}); diff --git a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx b/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx deleted file mode 100644 index f7fc9b689d65..000000000000 --- a/airbyte-webapp/src/hooks/services/Connection/ConnectionFormService.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { FormikHelpers } from "formik"; -import React, { createContext, useCallback, useContext, useMemo, useState } from "react"; -import { useIntl } from "react-intl"; - -import { ConnectionScheduleType, WebBackendConnectionRead } from "core/request/AirbyteClient"; -import { useGetDestinationDefinitionSpecification } from "services/connector/DestinationDefinitionSpecificationService"; -import { useCurrentWorkspaceId } from "services/workspaces/WorkspacesService"; -import { generateMessageFromError } from "utils/errorStatusMessage"; -import { - ConnectionFormValues, - connectionValidationSchema, - FormikConnectionFormValues, - mapFormPropsToOperation, - useFrequencyDropdownData, - useInitialValues, -} from "views/Connection/ConnectionForm/formConfig"; - -import { ConnectionFormMode } from "../ConnectionForm/ConnectionFormService"; -import { useFormChangeTrackerService } from "../FormChangeTracker"; -import { ModalCancel } from "../Modal"; -import { ValuesProps } from "../useConnectionHook"; - -export type ConnectionOrPartialConnection = - | WebBackendConnectionRead - | (Partial & Pick); - -export interface ConnectionServiceProps { - connection: ConnectionOrPartialConnection; - mode: ConnectionFormMode; - formId: string; - onSubmit: (values: ValuesProps) => Promise; - onAfterSubmit?: () => void; - onCancel?: () => void; - formDirty: boolean; -} - -const useConnectionForm = ({ - connection, - mode, - formId, - onSubmit, - onAfterSubmit, - onCancel, - formDirty, -}: ConnectionServiceProps) => { - const [submitError, setSubmitError] = useState(null); - const workspaceId = useCurrentWorkspaceId(); - const { clearFormChange } = useFormChangeTrackerService(); - const { formatMessage } = useIntl(); - - const destDefinition = useGetDestinationDefinitionSpecification(connection.destination.destinationDefinitionId); - const initialValues = useInitialValues(connection, destDefinition, mode !== "create"); - - const onFormSubmit = useCallback( - async (values: FormikConnectionFormValues, formikHelpers: FormikHelpers) => { - // TODO: We should align these types - // With the PATCH-style endpoint available we might be able to forego this pattern - const formValues: ConnectionFormValues = connectionValidationSchema.cast(values, { - context: { isRequest: true }, - }) as unknown as ConnectionFormValues; - - formValues.operations = mapFormPropsToOperation(values, connection.operations, workspaceId); - - if (formValues.scheduleType === ConnectionScheduleType.manual) { - // Have to set this to undefined to override the existing scheduleData - formValues.scheduleData = undefined; - } - - setSubmitError(null); - try { - // This onSubmit comes from either ReplicationView.tsx (Connection Edit), or CreateConnectionContent.tsx (Connection Create). - await onSubmit(formValues); - - formikHelpers.resetForm({ values }); - // We need to clear the form changes otherwise the dirty form intercept service will prevent navigation - clearFormChange(formId); - - onAfterSubmit?.(); - } catch (e) { - if (!(e instanceof ModalCancel)) { - setSubmitError(e); - } - } - }, - [connection.operations, workspaceId, onSubmit, clearFormChange, formId, onAfterSubmit] - ); - - const errorMessage = useMemo( - () => - submitError - ? generateMessageFromError(submitError) - : formDirty - ? formatMessage({ id: "connectionForm.validation.error" }) - : null, - [formDirty, formatMessage, submitError] - ); - const frequencies = useFrequencyDropdownData(connection.scheduleData); - - return { - initialValues, - destDefinition, - connection, - mode, - errorMessage, - frequencies, - formId, - onFormSubmit, - onAfterSubmit, - onCancel, - }; -}; - -const ConnectionFormContext = createContext | null>(null); - -export const ConnectionFormServiceProvider: React.FC = ({ children, ...props }) => { - const data = useConnectionForm(props); - return {children}; -}; - -export const useConnectionFormService = () => { - const context = useContext(ConnectionFormContext); - if (context === null) { - throw new Error("useConnectionFormService must be used within a ConnectionFormProvider"); - } - return context; -}; From 89b6e452dea08120f6dfadc4a163144d3c2e4006 Mon Sep 17 00:00:00 2001 From: KC Date: Wed, 28 Sep 2022 12:42:00 -0400 Subject: [PATCH 071/107] Fixing failing test --- .../CatalogDiffModal.test.tsx | 65 +++++++++++-------- 1 file changed, 37 insertions(+), 28 deletions(-) diff --git a/airbyte-webapp/src/views/Connection/CatalogDiffModal/CatalogDiffModal.test.tsx b/airbyte-webapp/src/views/Connection/CatalogDiffModal/CatalogDiffModal.test.tsx index 655deb86f594..3a99a63faab1 100644 --- a/airbyte-webapp/src/views/Connection/CatalogDiffModal/CatalogDiffModal.test.tsx +++ b/airbyte-webapp/src/views/Connection/CatalogDiffModal/CatalogDiffModal.test.tsx @@ -9,6 +9,7 @@ import { StreamTransform, SyncMode, } from "core/request/AirbyteClient"; +import { ModalServiceProvider } from "hooks/services/Modal"; import messages from "../../../locales/en.json"; import { CatalogDiffModal } from "./CatalogDiffModal"; @@ -151,13 +152,15 @@ describe("catalog diff modal", () => { render( - { - return null; - }} - /> + + { + return null; + }} + /> + ); @@ -198,13 +201,15 @@ describe("catalog diff modal", () => { render( - { - return null; - }} - /> + + { + return null; + }} + /> + ); @@ -217,13 +222,15 @@ describe("catalog diff modal", () => { render( - { - return null; - }} - /> + + { + return null; + }} + /> + ); @@ -236,13 +243,15 @@ describe("catalog diff modal", () => { render( - { - return null; - }} - /> + + { + return null; + }} + /> + ); From 599128dacbf63b5eba54f5f69cfe4da7e70b7741 Mon Sep 17 00:00:00 2001 From: KC Date: Wed, 28 Sep 2022 12:49:16 -0400 Subject: [PATCH 072/107] Added missing initialvalues logic. Added connection form service tests. --- .../ConnectionFormService.test.tsx | 292 ++++++++---------- .../ConnectionForm/ConnectionFormService.tsx | 2 +- 2 files changed, 136 insertions(+), 158 deletions(-) diff --git a/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.test.tsx b/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.test.tsx index b5d14d616567..6441ed3121aa 100644 --- a/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.test.tsx +++ b/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.test.tsx @@ -1,157 +1,135 @@ -export {}; -// /* eslint-disable @typescript-eslint/no-explicit-any */ -// import { act } from "@testing-library/react"; -// import { renderHook } from "@testing-library/react-hooks"; -// import React from "react"; -// import { MemoryRouter } from "react-router-dom"; -// import mockConnection from "test-utils/mock-data/mockConnection.json"; -// import mockDest from "test-utils/mock-data/mockDestinationDefinition.json"; -// import mockWorkspace from "test-utils/mock-data/mockWorkspace.json"; -// import { TestWrapper } from "test-utils/testutils"; - -// import { WebBackendConnectionRead } from "core/request/AirbyteClient"; - -// import { -// ConnectionFormServiceProvider, -// ConnectionServiceProps, -// useConnectionFormService, -// } from "./ConnectionFormService"; - -// ["packages/cloud/services/workspaces/WorkspacesService", "services/workspaces/WorkspacesService"].forEach((s) => -// jest.mock(s, () => ({ -// useCurrentWorkspaceId: () => mockWorkspace.workspaceId, -// useCurrentWorkspace: () => mockWorkspace, -// })) -// ); - -// jest.mock("../FormChangeTracker", () => ({ -// useFormChangeTrackerService: () => ({ clearFormChange: () => null }), -// useUniqueFormId: () => "blah", -// })); -// jest.mock("services/connector/DestinationDefinitionSpecificationService", () => ({ -// useGetDestinationDefinitionSpecification: () => mockDest, -// })); - -// describe("ConnectionFormService", () => { -// const Wrapper: React.FC = ({ children, ...props }) => ( -// -// -// {children} -// -// -// ); - -// const onSubmit = jest.fn(); -// const onAfterSubmit = jest.fn(); -// const refreshCatalog = jest.fn(); - -// beforeEach(() => { -// onSubmit.mockReset(); -// onAfterSubmit.mockReset(); -// refreshCatalog.mockReset(); -// }); - -// it("should call onSubmit when submitted", async () => { -// const { result } = renderHook(useConnectionFormService, { -// wrapper: Wrapper, -// initialProps: { -// connection: mockConnection as WebBackendConnectionRead, -// mode: "create", -// formId: Math.random().toString(), -// onSubmit, -// onAfterSubmit, -// refreshCatalog, -// }, -// }); - -// const resetForm = jest.fn(); -// const testValues: any = {}; -// await act(async () => { -// await result.current.onFormSubmit(testValues, { resetForm } as any); -// }); - -// expect(resetForm).toBeCalledWith({ values: testValues }); -// expect(onSubmit).toBeCalledWith({ -// operations: [], -// scheduleType: "manual", -// syncCatalog: { -// streams: undefined, -// }, -// }); -// expect(onAfterSubmit).toBeCalledWith(); -// expect(result.current.errorMessage).toBe(null); -// }); - -// it("should catch if onSubmit throws and generate an error message", async () => { -// const errorMessage = "asdf"; -// onSubmit.mockImplementation(async () => { -// throw new Error(errorMessage); -// }); - -// const { result } = renderHook(useConnectionFormService, { -// wrapper: Wrapper, -// initialProps: { -// connection: mockConnection as WebBackendConnectionRead, -// mode: "create", -// formId: Math.random().toString(), -// onSubmit, -// onAfterSubmit, -// refreshCatalog, -// }, -// }); - -// const resetForm = jest.fn(); -// const testValues: any = {}; -// await act(async () => { -// await result.current.onFormSubmit(testValues, { resetForm } as any); -// }); - -// expect(result.current.errorMessage).toBe(errorMessage); -// expect(resetForm).not.toHaveBeenCalled(); -// }); - -// it("should catch if onSubmit throws but not generate an error if it's a ModalCancel error", async () => { -// onSubmit.mockImplementation(async () => { -// return { -// submitCancel: true, -// }; -// }); - -// const { result } = renderHook(useConnectionFormService, { -// wrapper: Wrapper, -// initialProps: { -// connection: mockConnection as WebBackendConnectionRead, -// mode: "create", -// formId: Math.random().toString(), -// onSubmit, -// onAfterSubmit, -// refreshCatalog, -// }, -// }); - -// const resetForm = jest.fn(); -// const testValues: any = {}; -// await act(async () => { -// await result.current.onFormSubmit(testValues, { resetForm } as any); -// }); - -// expect(result.current.errorMessage).toBe(null); -// expect(resetForm).not.toHaveBeenCalled(); -// }); - -// it("should render the generic form invalid error message if the form is dirty and there has not been a submit error", async () => { -// const { result } = renderHook(useConnectionFormService, { -// wrapper: Wrapper, -// initialProps: { -// connection: mockConnection as WebBackendConnectionRead, -// mode: "create", -// formId: Math.random().toString(), -// onSubmit, -// onAfterSubmit, -// refreshCatalog, -// }, -// }); - -// expect(result.current.errorMessage).toBe("The form is invalid. Please make sure that all fields are correct."); -// }); -// }); +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { act, renderHook } from "@testing-library/react-hooks"; +import React from "react"; +import { MemoryRouter } from "react-router-dom"; +import mockConnection from "test-utils/mock-data/mockConnection.json"; +import mockDest from "test-utils/mock-data/mockDestinationDefinition.json"; +import { TestWrapper } from "test-utils/testutils"; + +import { AirbyteCatalog, WebBackendConnectionRead } from "core/request/AirbyteClient"; +import { FormError } from "utils/errorStatusMessage"; + +import { ModalServiceProvider } from "../Modal"; +import { + ConnectionFormServiceProvider, + ConnectionOrPartialConnection, + ConnectionServiceProps, + useConnectionFormService, +} from "./ConnectionFormService"; + +jest.mock("services/connector/DestinationDefinitionSpecificationService", () => ({ + useGetDestinationDefinitionSpecification: () => mockDest, +})); + +describe("ConnectionFormService", () => { + const Wrapper: React.FC = ({ children, ...props }) => ( + + + + {children} + + + + ); + + const refreshSchema = jest.fn(); + + beforeEach(() => { + refreshSchema.mockReset(); + }); + + it("should take a partial Connection", async () => { + const partialConnection: ConnectionOrPartialConnection = { + syncCatalog: mockConnection.syncCatalog as AirbyteCatalog, + source: mockConnection.source, + destination: mockConnection.destination, + }; + const { result } = renderHook(useConnectionFormService, { + wrapper: Wrapper, + initialProps: { + connection: partialConnection, + mode: "create", + refreshSchema, + }, + }); + + expect(result.current.connection).toEqual(partialConnection); + }); + + it("should take a full Connection", async () => { + const { result } = renderHook(useConnectionFormService, { + wrapper: Wrapper, + initialProps: { + connection: mockConnection as WebBackendConnectionRead, + mode: "create", + refreshSchema, + }, + }); + + expect(result.current.connection).toEqual(mockConnection); + }); + + describe("Error Message Generation", () => { + it("should show a validation error if the form is invalid and dirty", async () => { + const { result } = renderHook(useConnectionFormService, { + wrapper: Wrapper, + initialProps: { + connection: mockConnection as WebBackendConnectionRead, + mode: "create", + refreshSchema, + }, + }); + + expect(result.current.getErrorMessage(false, true)).toBe( + "The form is invalid. Please make sure that all fields are correct." + ); + }); + + it("should not show a validation error if the form is valid and dirty", async () => { + const { result } = renderHook(useConnectionFormService, { + wrapper: Wrapper, + initialProps: { + connection: mockConnection as WebBackendConnectionRead, + mode: "create", + refreshSchema, + }, + }); + + expect(result.current.getErrorMessage(true, true)).toBe(null); + }); + + it("should not show a validation error if the form is invalid and not dirty", async () => { + const { result } = renderHook(useConnectionFormService, { + wrapper: Wrapper, + initialProps: { + connection: mockConnection as WebBackendConnectionRead, + mode: "create", + refreshSchema, + }, + }); + + expect(result.current.getErrorMessage(false, false)).toBe(null); + }); + + it("should show a message when given a submit error", () => { + const { result } = renderHook(useConnectionFormService, { + wrapper: Wrapper, + initialProps: { + connection: mockConnection as WebBackendConnectionRead, + mode: "create", + refreshSchema, + }, + }); + + const errMsg = "asdf"; + act(() => { + result.current.setSubmitError(new FormError(errMsg)); + }); + + expect(result.current.getErrorMessage(false, false)).toBe(errMsg); + expect(result.current.getErrorMessage(false, true)).toBe(errMsg); + expect(result.current.getErrorMessage(true, false)).toBe(errMsg); + expect(result.current.getErrorMessage(true, true)).toBe(errMsg); + }); + }); +}); diff --git a/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx b/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx index 48f08ded6aa5..c62ca65dbce8 100644 --- a/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx +++ b/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx @@ -43,7 +43,7 @@ export const tidyConnectionFormValues = ( const useConnectionForm = ({ connection, mode, refreshSchema }: ConnectionServiceProps) => { const destDefinition = useGetDestinationDefinitionSpecification(connection.destination.destinationDefinitionId); - const initialValues = useInitialValues(connection, destDefinition); + const initialValues = useInitialValues(connection, destDefinition, mode === "edit"); const { formatMessage } = useIntl(); const [submitError, setSubmitError] = useState(null); From 4b02323e8b62f5d023e2a0051038f6402cf82f71 Mon Sep 17 00:00:00 2001 From: KC Date: Wed, 28 Sep 2022 15:58:40 -0400 Subject: [PATCH 073/107] Connection Edit tests! --- .../ConnectionEditService.test.tsx | 113 ++++++++++++++++++ .../ConnectionFormService.test.tsx | 3 +- .../ConnectionForm/ConnectionFormService.tsx | 2 +- 3 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.test.tsx diff --git a/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.test.tsx b/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.test.tsx new file mode 100644 index 000000000000..24725caa6813 --- /dev/null +++ b/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.test.tsx @@ -0,0 +1,113 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { act, renderHook } from "@testing-library/react-hooks"; +import React from "react"; +import { MemoryRouter } from "react-router-dom"; +import mockConnection from "test-utils/mock-data/mockConnection.json"; +import mockDest from "test-utils/mock-data/mockDestinationDefinition.json"; +import { TestWrapper } from "test-utils/testutils"; + +import { WebBackendConnectionUpdate } from "core/request/AirbyteClient"; + +import { useConnectionFormService } from "../ConnectionForm/ConnectionFormService"; +import { ModalServiceProvider } from "../Modal"; +import { ConnectionEditServiceProvider, useConnectionEditService } from "./ConnectionEditService"; + +jest.mock("services/connector/DestinationDefinitionSpecificationService", () => ({ + useGetDestinationDefinitionSpecification: () => mockDest, +})); + +jest.mock("../useConnectionHook", () => ({ + useGetConnection: () => mockConnection, + useWebConnectionService: () => ({ + getConnection: () => mockConnection, + }), + useUpdateConnection: () => ({ + mutateAsync: jest.fn(async (connection: WebBackendConnectionUpdate) => connection), + isLoading: false, + }), +})); + +describe("ConnectionFormService", () => { + const Wrapper: React.FC[0]> = ({ children, ...props }) => ( + + + + {children} + + + + ); + + const refreshSchema = jest.fn(); + + beforeEach(() => { + refreshSchema.mockReset(); + }); + + it("should load a Connection from a connectionId", async () => { + const { result } = renderHook(useConnectionEditService, { + wrapper: Wrapper, + initialProps: { + connectionId: mockConnection.connectionId, + }, + }); + + expect(result.current.connection).toEqual(mockConnection); + }); + + it("should update a connection and set the current connection object to the updated connection", async () => { + const { result } = renderHook(useConnectionEditService, { + wrapper: Wrapper, + initialProps: { + connectionId: mockConnection.connectionId, + }, + }); + + const mockUpdateConnection: unknown = { + status: "asdf", + destination: {}, + source: {}, + syncCatalog: { streams: [] }, + }; + + await act(async () => { + await result.current.updateConnection(mockUpdateConnection as WebBackendConnectionUpdate); + }); + + expect(result.current.connection).toEqual(mockUpdateConnection); + }); + + it("should refresh connection", async () => { + // Need to combine the hooks so both can be used. + const useMyTestHook = () => { + return [useConnectionEditService(), useConnectionFormService()] as const; + }; + + const { result } = renderHook(useMyTestHook, { + wrapper: Wrapper, + initialProps: { + connectionId: mockConnection.connectionId, + }, + }); + + const mockUpdateConnection: unknown = { + status: "asdf", + destination: {}, + source: {}, + syncCatalog: { streams: [] }, + }; + + await act(async () => { + await result.current[0].updateConnection(mockUpdateConnection as WebBackendConnectionUpdate); + }); + + expect(result.current[0].connection).toEqual(mockUpdateConnection); + + await act(async () => { + await result.current[1].refreshSchema(); + }); + + expect(result.current[0].schemaHasBeenRefreshed).toBe(true); + expect(result.current[0].connection).toEqual(mockConnection); + }); +}); diff --git a/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.test.tsx b/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.test.tsx index 6441ed3121aa..53f3286c194a 100644 --- a/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.test.tsx +++ b/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.test.tsx @@ -13,7 +13,6 @@ import { ModalServiceProvider } from "../Modal"; import { ConnectionFormServiceProvider, ConnectionOrPartialConnection, - ConnectionServiceProps, useConnectionFormService, } from "./ConnectionFormService"; @@ -22,7 +21,7 @@ jest.mock("services/connector/DestinationDefinitionSpecificationService", () => })); describe("ConnectionFormService", () => { - const Wrapper: React.FC = ({ children, ...props }) => ( + const Wrapper: React.FC[0]> = ({ children, ...props }) => ( diff --git a/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx b/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx index c62ca65dbce8..b0a251b7e171 100644 --- a/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx +++ b/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx @@ -20,7 +20,7 @@ export type ConnectionOrPartialConnection = | WebBackendConnectionRead | (Partial & Pick); -export interface ConnectionServiceProps { +interface ConnectionServiceProps { connection: ConnectionOrPartialConnection; mode: ConnectionFormMode; refreshSchema: () => Promise; From 5b95466d651d541a9d63c9e18c5d18c1e6d2e4db Mon Sep 17 00:00:00 2001 From: KC Date: Thu, 29 Sep 2022 13:25:58 -0400 Subject: [PATCH 074/107] connection replication test pt 1 --- .../components/ConnectionReplication.test.tsx | 10 ++++++++++ .../__snapshots__/ConnectionReplication.test.tsx.snap | 3 +++ 2 files changed, 13 insertions(+) create mode 100644 airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.test.tsx create mode 100644 airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/__snapshots__/ConnectionReplication.test.tsx.snap diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.test.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.test.tsx new file mode 100644 index 000000000000..9c8a8158451c --- /dev/null +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionReplication.test.tsx @@ -0,0 +1,10 @@ +import { render } from "@testing-library/react"; + +import { ConnectionReplication } from "./ConnectionReplication"; + +describe("ConnectionReplication", () => { + it("should render", () => { + // TODO: Wrapper component + expect(render()).toMatchSnapshot(); + }); +}); diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/__snapshots__/ConnectionReplication.test.tsx.snap b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/__snapshots__/ConnectionReplication.test.tsx.snap new file mode 100644 index 000000000000..f8ad854c99db --- /dev/null +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/__snapshots__/ConnectionReplication.test.tsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ConnectionReplication should render 1`] = ``; From 621c186b56e28235b96d15ff99aee938b56ee536 Mon Sep 17 00:00:00 2001 From: KC Date: Fri, 30 Sep 2022 10:18:18 -0400 Subject: [PATCH 075/107] Moving files to make master merge easier --- .../src/components/CreateConnection/CreateConnection.tsx | 4 ++-- .../{components => }/CreateConnectionNameField.module.scss | 2 +- .../{components => }/CreateConnectionNameField.tsx | 0 .../CreateConnection/{components => }/SchemaError.tsx | 0 .../{components => }/TryAfterErrorBlock.module.scss | 2 +- .../CreateConnection/{components => }/TryAfterErrorBlock.tsx | 0 6 files changed, 4 insertions(+), 4 deletions(-) rename airbyte-webapp/src/components/CreateConnection/{components => }/CreateConnectionNameField.module.scss (62%) rename airbyte-webapp/src/components/CreateConnection/{components => }/CreateConnectionNameField.tsx (100%) rename airbyte-webapp/src/components/CreateConnection/{components => }/SchemaError.tsx (100%) rename airbyte-webapp/src/components/CreateConnection/{components => }/TryAfterErrorBlock.module.scss (83%) rename airbyte-webapp/src/components/CreateConnection/{components => }/TryAfterErrorBlock.tsx (100%) diff --git a/airbyte-webapp/src/components/CreateConnection/CreateConnection.tsx b/airbyte-webapp/src/components/CreateConnection/CreateConnection.tsx index 593a85f2f414..99ccafa92967 100644 --- a/airbyte-webapp/src/components/CreateConnection/CreateConnection.tsx +++ b/airbyte-webapp/src/components/CreateConnection/CreateConnection.tsx @@ -21,9 +21,9 @@ import { OperationsSection } from "views/Connection/ConnectionForm/components/Op import { ConnectionFormFields } from "views/Connection/ConnectionForm/ConnectionFormFields"; import { connectionValidationSchema, FormikConnectionFormValues } from "views/Connection/ConnectionForm/formConfig"; -import { CreateConnectionNameField } from "./components/CreateConnectionNameField"; -import { SchemaError } from "./components/SchemaError"; import styles from "./CreateConnection.module.scss"; +import { CreateConnectionNameField } from "./CreateConnectionNameField"; +import { SchemaError } from "./SchemaError"; interface CreateConnectionProps { source: SourceRead; diff --git a/airbyte-webapp/src/components/CreateConnection/components/CreateConnectionNameField.module.scss b/airbyte-webapp/src/components/CreateConnection/CreateConnectionNameField.module.scss similarity index 62% rename from airbyte-webapp/src/components/CreateConnection/components/CreateConnectionNameField.module.scss rename to airbyte-webapp/src/components/CreateConnection/CreateConnectionNameField.module.scss index a328a8d61074..0011751a2ad9 100644 --- a/airbyte-webapp/src/components/CreateConnection/components/CreateConnectionNameField.module.scss +++ b/airbyte-webapp/src/components/CreateConnection/CreateConnectionNameField.module.scss @@ -1,4 +1,4 @@ -@forward "../../../views/Connection/ConnectionForm/ConnectionFormFields.module.scss"; +@forward "../../views/Connection/ConnectionForm/ConnectionFormFields.module.scss"; .labelHeading { line-height: 16px; diff --git a/airbyte-webapp/src/components/CreateConnection/components/CreateConnectionNameField.tsx b/airbyte-webapp/src/components/CreateConnection/CreateConnectionNameField.tsx similarity index 100% rename from airbyte-webapp/src/components/CreateConnection/components/CreateConnectionNameField.tsx rename to airbyte-webapp/src/components/CreateConnection/CreateConnectionNameField.tsx diff --git a/airbyte-webapp/src/components/CreateConnection/components/SchemaError.tsx b/airbyte-webapp/src/components/CreateConnection/SchemaError.tsx similarity index 100% rename from airbyte-webapp/src/components/CreateConnection/components/SchemaError.tsx rename to airbyte-webapp/src/components/CreateConnection/SchemaError.tsx diff --git a/airbyte-webapp/src/components/CreateConnection/components/TryAfterErrorBlock.module.scss b/airbyte-webapp/src/components/CreateConnection/TryAfterErrorBlock.module.scss similarity index 83% rename from airbyte-webapp/src/components/CreateConnection/components/TryAfterErrorBlock.module.scss rename to airbyte-webapp/src/components/CreateConnection/TryAfterErrorBlock.module.scss index 8907a23b1781..fae2399b7e23 100644 --- a/airbyte-webapp/src/components/CreateConnection/components/TryAfterErrorBlock.module.scss +++ b/airbyte-webapp/src/components/CreateConnection/TryAfterErrorBlock.module.scss @@ -1,4 +1,4 @@ -@use "../../../scss/variables"; +@use "../../scss/variables"; .container { padding: 40px; diff --git a/airbyte-webapp/src/components/CreateConnection/components/TryAfterErrorBlock.tsx b/airbyte-webapp/src/components/CreateConnection/TryAfterErrorBlock.tsx similarity index 100% rename from airbyte-webapp/src/components/CreateConnection/components/TryAfterErrorBlock.tsx rename to airbyte-webapp/src/components/CreateConnection/TryAfterErrorBlock.tsx From bc31bde4c2d1c25336c509ac04663e35e77afe1a Mon Sep 17 00:00:00 2001 From: KC Date: Fri, 30 Sep 2022 10:40:22 -0400 Subject: [PATCH 076/107] Remove snapshot --- .../__snapshots__/ConnectionReplication.test.tsx.snap | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/__snapshots__/ConnectionReplication.test.tsx.snap diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/__snapshots__/ConnectionReplication.test.tsx.snap b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/__snapshots__/ConnectionReplication.test.tsx.snap deleted file mode 100644 index f8ad854c99db..000000000000 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/__snapshots__/ConnectionReplication.test.tsx.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ConnectionReplication should render 1`] = ``; From 2764a781f8e767d97b5ee2fe3f89ada027602494 Mon Sep 17 00:00:00 2001 From: KC Date: Fri, 30 Sep 2022 10:45:23 -0400 Subject: [PATCH 077/107] non-default export --- .../src/components/CreateConnection/SchemaError.tsx | 2 +- .../src/components/CreateConnection/TryAfterErrorBlock.tsx | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/airbyte-webapp/src/components/CreateConnection/SchemaError.tsx b/airbyte-webapp/src/components/CreateConnection/SchemaError.tsx index 06bb6e12c31c..d4af737b74b0 100644 --- a/airbyte-webapp/src/components/CreateConnection/SchemaError.tsx +++ b/airbyte-webapp/src/components/CreateConnection/SchemaError.tsx @@ -5,7 +5,7 @@ import { SynchronousJobRead } from "core/request/AirbyteClient"; import { LogsRequestError } from "core/request/LogsRequestError"; import { useConnectionFormService } from "hooks/services/ConnectionForm/ConnectionFormService"; -import TryAfterErrorBlock from "./TryAfterErrorBlock"; +import { TryAfterErrorBlock } from "./TryAfterErrorBlock"; export const SchemaError = ({ schemaError, diff --git a/airbyte-webapp/src/components/CreateConnection/TryAfterErrorBlock.tsx b/airbyte-webapp/src/components/CreateConnection/TryAfterErrorBlock.tsx index 4c7a1145e86d..276bf1364494 100644 --- a/airbyte-webapp/src/components/CreateConnection/TryAfterErrorBlock.tsx +++ b/airbyte-webapp/src/components/CreateConnection/TryAfterErrorBlock.tsx @@ -12,7 +12,7 @@ interface TryAfterErrorBlockProps { onClick: () => void; } -const TryAfterErrorBlock: React.FC = ({ message, onClick }) => ( +export const TryAfterErrorBlock: React.FC = ({ message, onClick }) => (
@@ -23,5 +23,3 @@ const TryAfterErrorBlock: React.FC = ({ message, onClic
); - -export default TryAfterErrorBlock; From a0277affb46971e40d7629df0fca43a175a956b3 Mon Sep 17 00:00:00 2001 From: KC Date: Fri, 30 Sep 2022 10:48:24 -0400 Subject: [PATCH 078/107] Remove unused --- airbyte-webapp/src/hooks/services/useConnectionHook.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/airbyte-webapp/src/hooks/services/useConnectionHook.tsx b/airbyte-webapp/src/hooks/services/useConnectionHook.tsx index 974fdc3df951..ef4a9531cb83 100644 --- a/airbyte-webapp/src/hooks/services/useConnectionHook.tsx +++ b/airbyte-webapp/src/hooks/services/useConnectionHook.tsx @@ -57,10 +57,6 @@ interface CreateConnectionProps { sourceCatalogId: string | undefined; } -export interface ListConnection { - connections: WebBackendConnectionRead[]; -} - export const useWebConnectionService = () => { const config = useConfig(); const middlewares = useDefaultRequestMiddlewares(); From d696b0cd9d55828c01f2f97b86e90d333d2bb298 Mon Sep 17 00:00:00 2001 From: KC Date: Fri, 30 Sep 2022 11:01:20 -0400 Subject: [PATCH 079/107] Minor improvements --- .../pages/ConnectionItemPage/ConnectionReplicationTab.tsx | 6 +++--- .../pages/ConnectionItemPage/ResetWarningModal.tsx | 7 +++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionReplicationTab.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionReplicationTab.tsx index 27919dfcbdf2..fb9e0bec5642 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionReplicationTab.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionReplicationTab.tsx @@ -51,7 +51,7 @@ export const ConnectionReplicationTab: React.FC = () => { useTrackPage(PageTrackingCodes.CONNECTIONS_ITEM_REPLICATION); const saveConnection = useCallback( - async (values: ValuesProps, { skipReset }: { skipReset: boolean }) => { + async (values: ValuesProps, skipReset: boolean) => { if (schemaRefreshing) { return; } @@ -111,14 +111,14 @@ export const ConnectionReplicationTab: React.FC = () => { if (result.type !== "canceled") { // Save the connection taking into account the correct skipReset value from the dialog choice. // We also want to skip the reset sync if the connection is not in an "active" status - await saveConnection(formValues, { skipReset: !result.reason || connection.status !== "active" }); + await saveConnection(formValues, !result.reason || connection.status !== "active"); } else { // We don't want to set saved to true or schema has been refreshed to false. return; } } else { // The catalog hasn't changed. We don't need to ask for any confirmation and can simply save. - await saveConnection(formValues, { skipReset: true }); + await saveConnection(formValues, true); } setSaved(true); diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ResetWarningModal.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ResetWarningModal.tsx index 388d7e1a77bb..c0401e70439a 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ResetWarningModal.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ResetWarningModal.tsx @@ -1,10 +1,12 @@ import { useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; +import { useUnmount } from "react-use"; import { Button, LabeledSwitch } from "components"; import { ModalBody, ModalFooter } from "components/ui/Modal"; import { ConnectionStateType } from "core/request/AirbyteClient"; +import { useModalService } from "hooks/services/Modal"; interface ResetWarningModalProps { onClose: (withReset: boolean) => void; @@ -13,10 +15,15 @@ interface ResetWarningModalProps { } export const ResetWarningModal: React.FC = ({ onCancel, onClose, stateType }) => { + const { closeModal } = useModalService(); const { formatMessage } = useIntl(); const [withReset, setWithReset] = useState(true); const requireFullReset = stateType === ConnectionStateType.legacy; + useUnmount(() => { + closeModal(); + }); + return ( <> From ab504ca7a56dfa87d735fb478c0e76676b1e5e96 Mon Sep 17 00:00:00 2001 From: KC Date: Mon, 3 Oct 2022 09:49:26 -0400 Subject: [PATCH 080/107] Edmundo CR. --- .../components/CreateConnection/CreateConnectionNameField.tsx | 2 +- .../ConnectionForm/ConnectionFormFields.module.scss | 4 ++-- .../{PreventRefreshOnDirty.tsx => PreventRefreshOnDirty.ts} | 0 .../views/Connection/ConnectionForm/components/Section.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename airbyte-webapp/src/views/Connection/ConnectionForm/components/{PreventRefreshOnDirty.tsx => PreventRefreshOnDirty.ts} (100%) diff --git a/airbyte-webapp/src/components/CreateConnection/CreateConnectionNameField.tsx b/airbyte-webapp/src/components/CreateConnection/CreateConnectionNameField.tsx index b42675b5b5b3..2510ee642e71 100644 --- a/airbyte-webapp/src/components/CreateConnection/CreateConnectionNameField.tsx +++ b/airbyte-webapp/src/components/CreateConnection/CreateConnectionNameField.tsx @@ -2,7 +2,7 @@ import { Field, FieldProps } from "formik"; import { FormattedMessage, useIntl } from "react-intl"; import { ControlLabels } from "components/LabeledControl"; -import { Input } from "components/ui"; +import { Input } from "components/ui/Input"; import { Text } from "components/ui/Text"; import { Section } from "views/Connection/ConnectionForm/components/Section"; diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.module.scss b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.module.scss index 11e011267bdc..98bdfdb34a7e 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.module.scss +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.module.scss @@ -3,7 +3,7 @@ .formContainer { display: flex; flex-direction: column; - gap: 10px; + gap: variables.$spacing-md; } .readonly { @@ -15,7 +15,7 @@ flex-direction: row; justify-content: flex-start; align-items: flex-start; - gap: 10px; + gap: variables.$spacing-md; } .leftFieldCol { diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/components/PreventRefreshOnDirty.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/components/PreventRefreshOnDirty.ts similarity index 100% rename from airbyte-webapp/src/views/Connection/ConnectionForm/components/PreventRefreshOnDirty.tsx rename to airbyte-webapp/src/views/Connection/ConnectionForm/components/PreventRefreshOnDirty.ts diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/components/Section.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/components/Section.tsx index 57d28f56ed95..40329d44f7f7 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/components/Section.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/components/Section.tsx @@ -1,4 +1,4 @@ -import { Card } from "components/ui"; +import { Card } from "components/ui/Card"; import { Text } from "components/ui/Text"; import styles from "./Section.module.scss"; From 4aaa6e30af880e865f5f5766df03585eac12fff9 Mon Sep 17 00:00:00 2001 From: KC Date: Mon, 3 Oct 2022 11:07:45 -0400 Subject: [PATCH 081/107] name now optional for connection editing --- .../CreateConnection/CreateConnection.tsx | 11 +- .../ConnectionEdit/ConnectionEditService.tsx | 1 - .../ConnectionForm/ConnectionFormService.tsx | 5 +- .../ConnectionReplicationTab.test.tsx | 42 +++- .../ConnectionReplicationTab.tsx | 7 +- .../ConnectionReplicationTab.test.tsx.snap | 78 +++++++ .../ConnectionForm/calculateInitialCatalog.ts | 6 +- .../ConnectionForm/formConfig.test.ts | 6 +- .../Connection/ConnectionForm/formConfig.tsx | 212 +++++++++--------- 9 files changed, 250 insertions(+), 118 deletions(-) create mode 100644 airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/__snapshots__/ConnectionReplicationTab.test.tsx.snap diff --git a/airbyte-webapp/src/components/CreateConnection/CreateConnection.tsx b/airbyte-webapp/src/components/CreateConnection/CreateConnection.tsx index 99ccafa92967..4bff021100a1 100644 --- a/airbyte-webapp/src/components/CreateConnection/CreateConnection.tsx +++ b/airbyte-webapp/src/components/CreateConnection/CreateConnection.tsx @@ -45,12 +45,12 @@ const CreateConnectionInner: React.FC = ({ schemaErr const workspaceId = useCurrentWorkspaceId(); const formId = useUniqueFormId(); - const { connection, initialValues, getErrorMessage, setSubmitError } = useConnectionFormService(); + const { connection, initialValues, mode, getErrorMessage, setSubmitError } = useConnectionFormService(); const [editingTransformation, toggleEditingTransformation] = useToggle(false); const onFormSubmit = useCallback( async (formValues: FormikConnectionFormValues, formikHelpers: FormikHelpers) => { - const values = tidyConnectionFormValues(formValues, workspaceId); + const values = tidyConnectionFormValues(formValues, workspaceId, mode); try { const createdConnection = await createConnection({ @@ -78,6 +78,7 @@ const CreateConnectionInner: React.FC = ({ schemaErr }, [ workspaceId, + mode, createConnection, connection.source, connection.destination, @@ -97,7 +98,11 @@ const CreateConnectionInner: React.FC = ({ schemaErr return ( }>
- + {({ values, isSubmitting, isValid, dirty }) => ( diff --git a/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.tsx b/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.tsx index 6a8559349076..a2de7ddf13bd 100644 --- a/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.tsx +++ b/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.tsx @@ -26,7 +26,6 @@ const useConnectionEdit = ({ connectionId }: ConnectionEditProps) => { const updateConnection = useCallback( async (connection: WebBackendConnectionUpdate) => { - // TODO: Check if the form is dirty before firing off an update action setConnection(await updateConnectionAction(connection)); }, [updateConnectionAction] diff --git a/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx b/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx index b0a251b7e171..e6d81b207523 100644 --- a/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx +++ b/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx @@ -29,10 +29,11 @@ interface ConnectionServiceProps { export const tidyConnectionFormValues = ( values: FormikConnectionFormValues, workspaceId: string, + mode: ConnectionFormMode, operations?: OperationRead[] ): ValuesProps => { // TODO: We should try to fix the types so we don't need the casting. - const formValues: ConnectionFormValues = connectionValidationSchema.cast(values, { + const formValues: ConnectionFormValues = connectionValidationSchema(mode).cast(values, { context: { isRequest: true }, }) as unknown as ConnectionFormValues; @@ -43,7 +44,7 @@ export const tidyConnectionFormValues = ( const useConnectionForm = ({ connection, mode, refreshSchema }: ConnectionServiceProps) => { const destDefinition = useGetDestinationDefinitionSpecification(connection.destination.destinationDefinitionId); - const initialValues = useInitialValues(connection, destDefinition, mode === "edit"); + const initialValues = useInitialValues(connection, destDefinition, mode !== "create"); const { formatMessage } = useIntl(); const [submitError, setSubmitError] = useState(null); diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionReplicationTab.test.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionReplicationTab.test.tsx index 0eddacd241f2..18557b1b3250 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionReplicationTab.test.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionReplicationTab.test.tsx @@ -1,10 +1,48 @@ import { render } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; +import { mockConnection, TestWrapper } from "test-utils/testutils"; + +import { ModalServiceProvider } from "hooks/services/Modal"; +import { ConfigProvider } from "packages/cloud/services/ConfigProvider"; +import { AnalyticsProvider } from "views/common/AnalyticsProvider"; import { ConnectionReplicationTab } from "./ConnectionReplicationTab"; +jest.mock("hooks/services/ConnectionEdit/ConnectionEditService", () => { + let schemaHasBeenRefreshed = false; + return { + useConnectionEditService: () => ({ + connection: mockConnection, + schemaRefreshing: false, + schemaHasBeenRefreshed, + updateConnection: jest.fn(), + setSchemaHasBeenRefreshed: (s: boolean) => (schemaHasBeenRefreshed = s), + }), + }; +}); + +// const { connection, schemaRefreshing, schemaHasBeenRefreshed, updateConnection, setSchemaHasBeenRefreshed } = +// useConnectionEditService(); + describe("ConnectionReplicationTab", () => { + const Wrapper: React.FC[0]> = ({ children }) => ( + + + + + {children} + + + + + ); it("should render", () => { - // TODO: Wrapper component - expect(render()).toMatchSnapshot(); + expect( + render( + + + + ) + ).toMatchSnapshot(); }); }); diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionReplicationTab.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionReplicationTab.tsx index 8fd5a3b48095..310924cfa48c 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionReplicationTab.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionReplicationTab.tsx @@ -40,7 +40,7 @@ export const ConnectionReplicationTab: React.FC = () => { const formId = useUniqueFormId(); const { connection, schemaRefreshing, schemaHasBeenRefreshed, updateConnection, setSchemaHasBeenRefreshed } = useConnectionEditService(); - const { initialValues, getErrorMessage, setSubmitError } = useConnectionFormService(); + const { initialValues, mode, getErrorMessage, setSubmitError } = useConnectionFormService(); useTrackPage(PageTrackingCodes.CONNECTIONS_ITEM_REPLICATION); @@ -74,7 +74,7 @@ export const ConnectionReplicationTab: React.FC = () => { const onFormSubmit = useCallback( async (values: FormikConnectionFormValues, _: FormikHelpers) => { - const formValues = tidyConnectionFormValues(values, workspaceId, connection.operations); + const formValues = tidyConnectionFormValues(values, workspaceId, mode, connection.operations); // Detect whether the catalog has any differences in its enabled streams compared to the original one. // This could be due to user changes (e.g. in the sync mode) or due to new/removed @@ -128,6 +128,7 @@ export const ConnectionReplicationTab: React.FC = () => { connection.syncCatalog.streams, connectionService, formatMessage, + mode, openModal, saveConnection, setSchemaHasBeenRefreshed, @@ -155,7 +156,7 @@ export const ConnectionReplicationTab: React.FC = () => { {!schemaRefreshing && connection ? ( diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/__snapshots__/ConnectionReplicationTab.test.tsx.snap b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/__snapshots__/ConnectionReplicationTab.test.tsx.snap new file mode 100644 index 000000000000..234c241799da --- /dev/null +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/__snapshots__/ConnectionReplicationTab.test.tsx.snap @@ -0,0 +1,78 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ConnectionReplicationTab should render 1`] = ` +Object { + "asFragment": [Function], + "baseElement": +
+
+
+
+
+ , + "container":
+
+
+
+
, + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "unmount": [Function], +} +`; diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/calculateInitialCatalog.ts b/airbyte-webapp/src/views/Connection/ConnectionForm/calculateInitialCatalog.ts index ec7d881775c6..dcc2876025d9 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/calculateInitialCatalog.ts +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/calculateInitialCatalog.ts @@ -119,13 +119,13 @@ const getOptimalSyncMode = ( const calculateInitialCatalog = ( schema: SyncSchema, supportedDestinationSyncModes: DestinationSyncMode[], - isEditMode?: boolean + isNotCreateMode?: boolean ): SyncSchema => ({ streams: schema.streams.map((apiNode, id) => { const nodeWithId: SyncSchemaStream = { ...apiNode, id: id.toString() }; - const nodeStream = verifySourceDefinedProperties(verifySupportedSyncModes(nodeWithId), isEditMode || false); + const nodeStream = verifySourceDefinedProperties(verifySupportedSyncModes(nodeWithId), isNotCreateMode || false); - if (isEditMode) { + if (isNotCreateMode) { return nodeStream; } diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/formConfig.test.ts b/airbyte-webapp/src/views/Connection/ConnectionForm/formConfig.test.ts index 145de75ab03a..11333c0839bf 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/formConfig.test.ts +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/formConfig.test.ts @@ -170,7 +170,7 @@ describe("#mapFormPropsToOperation", () => { }); describe("#useInitialValues", () => { - it("should generate initial values w/ no edit mode", () => { + it("should generate initial values w/ no 'not create' mode", () => { const { result } = renderHook(() => useInitialValues( mockConnection as WebBackendConnectionRead, @@ -180,7 +180,7 @@ describe("#useInitialValues", () => { expect(result).toMatchSnapshot(); }); - it("should generate initial values w/ edit mode: false", () => { + it("should generate initial values w/ 'not create' mode: false", () => { const { result } = renderHook(() => useInitialValues( mockConnection as WebBackendConnectionRead, @@ -191,7 +191,7 @@ describe("#useInitialValues", () => { expect(result).toMatchSnapshot(); }); - it("should generate initial values w/ edit mode: true", () => { + it("should generate initial values w/ 'not create' mode: true", () => { const { result } = renderHook(() => useInitialValues( mockConnection as WebBackendConnectionRead, diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/formConfig.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/formConfig.tsx index db5e73a7dc62..f1c75e42910b 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/formConfig.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/formConfig.tsx @@ -24,7 +24,7 @@ import { SyncMode, WebBackendConnectionRead, } from "core/request/AirbyteClient"; -import { ConnectionOrPartialConnection } from "hooks/services/ConnectionForm/ConnectionFormService"; +import { ConnectionFormMode, ConnectionOrPartialConnection } from "hooks/services/ConnectionForm/ConnectionFormService"; import { ValuesProps } from "hooks/services/useConnectionHook"; import { useCurrentWorkspace } from "services/workspaces/WorkspacesService"; @@ -74,106 +74,108 @@ export function useDefaultTransformation(): OperationCreate { }; } -export const connectionValidationSchema = yup - .object({ - name: yup.string().required("form.empty.error"), - scheduleType: yup - .string() - .oneOf([ConnectionScheduleType.manual, ConnectionScheduleType.basic, ConnectionScheduleType.cron]), - scheduleData: yup.mixed().when("scheduleType", (scheduleType) => { - if (scheduleType === ConnectionScheduleType.basic) { +export const connectionValidationSchema = (mode: ConnectionFormMode) => + yup + .object({ + // The connection name during Editing is handled separately from the form + name: mode === "create" ? yup.string().required("form.empty.error") : yup.string().notRequired(), + scheduleType: yup + .string() + .oneOf([ConnectionScheduleType.manual, ConnectionScheduleType.basic, ConnectionScheduleType.cron]), + scheduleData: yup.mixed().when("scheduleType", (scheduleType) => { + if (scheduleType === ConnectionScheduleType.basic) { + return yup.object({ + basicSchedule: yup + .object({ + units: yup.number().required("form.empty.error"), + timeUnit: yup.string().required("form.empty.error"), + }) + .defined("form.empty.error"), + }); + } else if (scheduleType === ConnectionScheduleType.manual) { + return yup.mixed().notRequired(); + } + return yup.object({ - basicSchedule: yup + cron: yup .object({ - units: yup.number().required("form.empty.error"), - timeUnit: yup.string().required("form.empty.error"), + cronExpression: yup.string().required("form.empty.error"), + cronTimeZone: yup.string().required("form.empty.error"), }) .defined("form.empty.error"), }); - } else if (scheduleType === ConnectionScheduleType.manual) { - return yup.mixed().notRequired(); - } - - return yup.object({ - cron: yup - .object({ - cronExpression: yup.string().required("form.empty.error"), - cronTimeZone: yup.string().required("form.empty.error"), - }) - .defined("form.empty.error"), - }); - }), - namespaceDefinition: yup - .string() - .oneOf([ - NamespaceDefinitionType.source, - NamespaceDefinitionType.destination, - NamespaceDefinitionType.customformat, - ]) - .required("form.empty.error"), - namespaceFormat: yup.string().when("namespaceDefinition", { - is: NamespaceDefinitionType.customformat, - then: yup.string().required("form.empty.error"), - }), - prefix: yup.string(), - syncCatalog: yup.object({ - streams: yup.array().of( - yup.object({ - id: yup - .string() - // This is required to get rid of id fields we are using to detect stream for edition - .when("$isRequest", (isRequest: boolean, schema: yup.StringSchema) => - isRequest ? schema.strip(true) : schema - ), - stream: yup.object(), - config: yup - .object({ - selected: yup.boolean(), - syncMode: yup.string(), - destinationSyncMode: yup.string(), - primaryKey: yup.array().of(yup.array().of(yup.string())), - cursorField: yup.array().of(yup.string()).defined(), - }) - .test({ - name: "connectionSchema.config.validator", - // eslint-disable-next-line no-template-curly-in-string - message: "${path} is wrong", - test(value) { - if (!value.selected) { - return true; - } - if (DestinationSyncMode.append_dedup === value.destinationSyncMode) { - // it's possible that primaryKey array is always present - // however yup couldn't determine type correctly even with .required() call - if (value.primaryKey?.length === 0) { - return this.createError({ - message: "connectionForm.primaryKey.required", - path: `schema.streams[${this.parent.id}].config.primaryKey`, - }); + }), + namespaceDefinition: yup + .string() + .oneOf([ + NamespaceDefinitionType.source, + NamespaceDefinitionType.destination, + NamespaceDefinitionType.customformat, + ]) + .required("form.empty.error"), + namespaceFormat: yup.string().when("namespaceDefinition", { + is: NamespaceDefinitionType.customformat, + then: yup.string().required("form.empty.error"), + }), + prefix: yup.string(), + syncCatalog: yup.object({ + streams: yup.array().of( + yup.object({ + id: yup + .string() + // This is required to get rid of id fields we are using to detect stream for edition + .when("$isRequest", (isRequest: boolean, schema: yup.StringSchema) => + isRequest ? schema.strip(true) : schema + ), + stream: yup.object(), + config: yup + .object({ + selected: yup.boolean(), + syncMode: yup.string(), + destinationSyncMode: yup.string(), + primaryKey: yup.array().of(yup.array().of(yup.string())), + cursorField: yup.array().of(yup.string()).defined(), + }) + .test({ + name: "connectionSchema.config.validator", + // eslint-disable-next-line no-template-curly-in-string + message: "${path} is wrong", + test(value) { + if (!value.selected) { + return true; } - } - - if (SyncMode.incremental === value.syncMode) { - if ( - !this.parent.stream.sourceDefinedCursor && - // it's possible that cursorField array is always present + if (DestinationSyncMode.append_dedup === value.destinationSyncMode) { + // it's possible that primaryKey array is always present // however yup couldn't determine type correctly even with .required() call - value.cursorField?.length === 0 - ) { - return this.createError({ - message: "connectionForm.cursorField.required", - path: `schema.streams[${this.parent.id}].config.cursorField`, - }); + if (value.primaryKey?.length === 0) { + return this.createError({ + message: "connectionForm.primaryKey.required", + path: `schema.streams[${this.parent.id}].config.primaryKey`, + }); + } } - } - return true; - }, - }), - }) - ), - }), - }) - .noUnknown(); + + if (SyncMode.incremental === value.syncMode) { + if ( + !this.parent.stream.sourceDefinedCursor && + // it's possible that cursorField array is always present + // however yup couldn't determine type correctly even with .required() call + value.cursorField?.length === 0 + ) { + return this.createError({ + message: "connectionForm.cursorField.required", + path: `schema.streams[${this.parent.id}].config.cursorField`, + }); + } + } + return true; + }, + }), + }) + ), + }), + }) + .noUnknown(); /** * Returns {@link Operation}[] @@ -229,14 +231,14 @@ export const getInitialTransformations = (operations: OperationCreate[]): Operat export const getInitialNormalization = ( operations?: Array, - isEditMode?: boolean + isNotCreateMode?: boolean ): NormalizationType => { const initialNormalization = operations?.find(isNormalizationTransformation)?.operatorConfiguration?.normalization?.option; return initialNormalization ? NormalizationType[initialNormalization] - : isEditMode + : isNotCreateMode ? NormalizationType.raw : NormalizationType.basic; }; @@ -244,17 +246,20 @@ export const getInitialNormalization = ( export const useInitialValues = ( connection: ConnectionOrPartialConnection, destDefinition: DestinationDefinitionSpecificationRead, - isEditMode?: boolean + isNotCreateMode?: boolean ): FormikConnectionFormValues => { const initialSchema = useMemo( () => - calculateInitialCatalog(connection.syncCatalog, destDefinition?.supportedDestinationSyncModes || [], isEditMode), - [connection.syncCatalog, destDefinition, isEditMode] + calculateInitialCatalog( + connection.syncCatalog, + destDefinition?.supportedDestinationSyncModes || [], + isNotCreateMode + ), + [connection.syncCatalog, destDefinition, isNotCreateMode] ); return useMemo(() => { const initialValues: FormikConnectionFormValues = { - name: connection.name ?? `${connection.source.name} <> ${connection.destination.name}`, syncCatalog: initialSchema, scheduleType: connection.connectionId ? connection.scheduleType : ConnectionScheduleType.basic, scheduleData: connection.connectionId ? connection.scheduleData ?? null : DEFAULT_SCHEDULE, @@ -263,6 +268,11 @@ export const useInitialValues = ( namespaceFormat: connection.namespaceFormat ?? SOURCE_NAMESPACE_TAG, }; + // Is Create Mode + if (!isNotCreateMode) { + initialValues.name = connection.name ?? `${connection.source.name} <> ${connection.destination.name}`; + } + const operations = connection.operations ?? []; if (destDefinition.supportsDbt) { @@ -270,7 +280,7 @@ export const useInitialValues = ( } if (destDefinition.supportsNormalization) { - initialValues.normalization = getInitialNormalization(operations, isEditMode); + initialValues.normalization = getInitialNormalization(operations, isNotCreateMode); } return initialValues; @@ -288,7 +298,7 @@ export const useInitialValues = ( destDefinition.supportsDbt, destDefinition.supportsNormalization, initialSchema, - isEditMode, + isNotCreateMode, ]); }; From 9e7008695ebe61409b0da523a249a3ae4e1c404a Mon Sep 17 00:00:00 2001 From: KC Date: Mon, 3 Oct 2022 12:59:03 -0400 Subject: [PATCH 082/107] Schema refresh errors handled on connection replication view --- .../CreateConnection/CreateConnection.tsx | 7 ++++++- .../CreateConnection/SchemaError.tsx | 8 ++------ .../ConnectionEdit/ConnectionEditService.tsx | 13 +++++++++---- .../ConnectionForm/ConnectionFormService.tsx | 5 ++++- .../src/hooks/services/useSourceHook.tsx | 8 ++------ .../ConnectionReplicationTab.tsx | 19 ++++++++++++------- 6 files changed, 35 insertions(+), 25 deletions(-) diff --git a/airbyte-webapp/src/components/CreateConnection/CreateConnection.tsx b/airbyte-webapp/src/components/CreateConnection/CreateConnection.tsx index 4bff021100a1..3d42e273a38b 100644 --- a/airbyte-webapp/src/components/CreateConnection/CreateConnection.tsx +++ b/airbyte-webapp/src/components/CreateConnection/CreateConnection.tsx @@ -139,7 +139,12 @@ export const CreateConnection: React.FC = ({ source, dest }; return ( - + {isLoading ? ( ) : ( diff --git a/airbyte-webapp/src/components/CreateConnection/SchemaError.tsx b/airbyte-webapp/src/components/CreateConnection/SchemaError.tsx index d4af737b74b0..5cf7cb2673c4 100644 --- a/airbyte-webapp/src/components/CreateConnection/SchemaError.tsx +++ b/airbyte-webapp/src/components/CreateConnection/SchemaError.tsx @@ -1,17 +1,13 @@ import { JobItem } from "components/JobItem/JobItem"; import { Card } from "components/ui"; -import { SynchronousJobRead } from "core/request/AirbyteClient"; import { LogsRequestError } from "core/request/LogsRequestError"; import { useConnectionFormService } from "hooks/services/ConnectionForm/ConnectionFormService"; +import { SchemaError as SchemaErrorType } from "hooks/services/useSourceHook"; import { TryAfterErrorBlock } from "./TryAfterErrorBlock"; -export const SchemaError = ({ - schemaError, -}: { - schemaError: { status: number; response: SynchronousJobRead } | null; -}) => { +export const SchemaError = ({ schemaError }: { schemaError: SchemaErrorType }) => { const job = LogsRequestError.extractJobInfo(schemaError); const { refreshSchema } = useConnectionFormService(); return ( diff --git a/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.tsx b/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.tsx index a2de7ddf13bd..31a5cfa6ebb8 100644 --- a/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.tsx +++ b/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.tsx @@ -5,6 +5,7 @@ import { ConnectionStatus, WebBackendConnectionUpdate } from "core/request/Airby import { ConnectionFormServiceProvider } from "../ConnectionForm/ConnectionFormService"; import { useGetConnection, useUpdateConnection, useWebConnectionService } from "../useConnectionHook"; +import { SchemaError } from "../useSourceHook"; interface ConnectionEditProps { connectionId: string; @@ -15,8 +16,7 @@ const useConnectionEdit = ({ connectionId }: ConnectionEditProps) => { const connectionService = useWebConnectionService(); const [schemaHasBeenRefreshed, setSchemaHasBeenRefreshed] = useState(false); - // TODO: Pull error out here, utilize down-line. Maybe use schema error component like on the create page? - const [{ loading: schemaRefreshing }, refreshSchema] = useAsyncFn(async () => { + const [{ loading: schemaRefreshing, error: schemaError }, refreshSchema] = useAsyncFn(async () => { const refreshedConnection = await connectionService.getConnection(connectionId, true); setConnection(refreshedConnection); setSchemaHasBeenRefreshed(true); @@ -34,6 +34,7 @@ const useConnectionEdit = ({ connectionId }: ConnectionEditProps) => { return { connection, connectionUpdating, + schemaError, schemaRefreshing, schemaHasBeenRefreshed, updateConnection, @@ -42,15 +43,19 @@ const useConnectionEdit = ({ connectionId }: ConnectionEditProps) => { }; }; -const ConnectionEditContext = createContext, "refreshSchema"> | null>(null); +const ConnectionEditContext = createContext, + "refreshSchema" | "schemaError" +> | null>(null); export const ConnectionEditServiceProvider: React.FC = ({ children, ...props }) => { - const { refreshSchema, ...data } = useConnectionEdit(props); + const { refreshSchema, schemaError, ...data } = useConnectionEdit(props); return ( {children} diff --git a/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx b/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx index e6d81b207523..a6bc741067c4 100644 --- a/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx +++ b/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx @@ -13,6 +13,7 @@ import { } from "views/Connection/ConnectionForm/formConfig"; import { ValuesProps } from "../useConnectionHook"; +import { SchemaError } from "../useSourceHook"; export type ConnectionFormMode = "create" | "edit" | "readonly"; @@ -23,6 +24,7 @@ export type ConnectionOrPartialConnection = interface ConnectionServiceProps { connection: ConnectionOrPartialConnection; mode: ConnectionFormMode; + schemaError?: SchemaError | null; refreshSchema: () => Promise; } @@ -42,7 +44,7 @@ export const tidyConnectionFormValues = ( return formValues; }; -const useConnectionForm = ({ connection, mode, refreshSchema }: ConnectionServiceProps) => { +const useConnectionForm = ({ connection, mode, schemaError, refreshSchema }: ConnectionServiceProps) => { const destDefinition = useGetDestinationDefinitionSpecification(connection.destination.destinationDefinitionId); const initialValues = useInitialValues(connection, destDefinition, mode !== "create"); const { formatMessage } = useIntl(); @@ -63,6 +65,7 @@ const useConnectionForm = ({ connection, mode, refreshSchema }: ConnectionServic mode, destDefinition, initialValues, + schemaError, setSubmitError, getErrorMessage, refreshSchema, diff --git a/airbyte-webapp/src/hooks/services/useSourceHook.tsx b/airbyte-webapp/src/hooks/services/useSourceHook.tsx index f874a4eb5978..b6accdaaf7df 100644 --- a/airbyte-webapp/src/hooks/services/useSourceHook.tsx +++ b/airbyte-webapp/src/hooks/services/useSourceHook.tsx @@ -6,7 +6,6 @@ import { Action, Namespace } from "core/analytics"; import { SyncSchema } from "core/domain/catalog"; import { ConnectionConfiguration } from "core/domain/connection"; import { SourceService } from "core/domain/connector/SourceService"; -import { JobInfo } from "core/domain/job"; import { useInitService } from "services/useInitService"; import { isDefined } from "utils/common"; @@ -149,7 +148,7 @@ const useUpdateSource = () => { ); }; -export type SchemaError = { status: number; response: SynchronousJobRead } | null; +export type SchemaError = (Error & { status: number; response: SynchronousJobRead }) | null; const useDiscoverSchema = ( sourceId: string, @@ -165,10 +164,7 @@ const useDiscoverSchema = ( const [schema, setSchema] = useState({ streams: [] }); const [catalogId, setCatalogId] = useState(""); const [isLoading, setIsLoading] = useState(false); - const [schemaErrorStatus, setSchemaErrorStatus] = useState<{ - status: number; - response: JobInfo; - } | null>(null); + const [schemaErrorStatus, setSchemaErrorStatus] = useState(null); const onDiscoverSchema = useCallback(async () => { setIsLoading(true); diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionReplicationTab.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionReplicationTab.tsx index 310924cfa48c..f3d82d79b5cb 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionReplicationTab.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionReplicationTab.tsx @@ -2,6 +2,7 @@ import { Form, Formik, FormikHelpers } from "formik"; import React, { useCallback, useEffect, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; +import { SchemaError } from "components/CreateConnection/SchemaError"; import { FormChangeTracker } from "components/FormChangeTracker"; import LoadingSchema from "components/LoadingSchema"; @@ -40,12 +41,12 @@ export const ConnectionReplicationTab: React.FC = () => { const formId = useUniqueFormId(); const { connection, schemaRefreshing, schemaHasBeenRefreshed, updateConnection, setSchemaHasBeenRefreshed } = useConnectionEditService(); - const { initialValues, mode, getErrorMessage, setSubmitError } = useConnectionFormService(); + const { initialValues, mode, schemaError, getErrorMessage, setSubmitError } = useConnectionFormService(); useTrackPage(PageTrackingCodes.CONNECTIONS_ITEM_REPLICATION); const saveConnection = useCallback( - async (values: ValuesProps, skipReset: boolean) => { + async (values: ValuesProps, skipReset: boolean, catalogHasChanged: boolean) => { if (schemaRefreshing) { return; } @@ -58,7 +59,7 @@ export const ConnectionReplicationTab: React.FC = () => { skipReset, }); - if (!equal(values.syncCatalog, connection.syncCatalog)) { + if (catalogHasChanged) { analyticsService.track(Namespace.CONNECTION, Action.EDIT_SCHEMA, { actionDescription: "Connection saved with catalog changes", connector_source: connection.source.sourceName, @@ -80,7 +81,7 @@ export const ConnectionReplicationTab: React.FC = () => { // This could be due to user changes (e.g. in the sync mode) or due to new/removed // streams due to a "refreshed source schema". // TODO: Can pass this value into the saveConnection function for the analytics call so we don't compare twice - const hasCatalogChanged = !equal( + const catalogHasChanged = !equal( formValues.syncCatalog.streams .filter((s) => s.config?.selected) .sort(naturalComparatorBy((syncStream) => syncStream.stream?.name ?? "")), @@ -95,7 +96,7 @@ export const ConnectionReplicationTab: React.FC = () => { // Given them a choice to opt-out in which case we'll be sending skipRe: true to the update // endpoint. try { - if (hasCatalogChanged) { + if (catalogHasChanged) { const stateType = await connectionService.getStateType(connection.connectionId); const result = await openModal({ title: formatMessage({ id: "connection.resetModalTitle" }), @@ -105,14 +106,14 @@ export const ConnectionReplicationTab: React.FC = () => { if (result.type !== "canceled") { // Save the connection taking into account the correct skipReset value from the dialog choice. // We also want to skip the reset sync if the connection is not in an "active" status - await saveConnection(formValues, !result.reason || connection.status !== "active"); + await saveConnection(formValues, !result.reason || connection.status !== "active", catalogHasChanged); } else { // We don't want to set saved to true or schema has been refreshed to false. return; } } else { // The catalog hasn't changed. We don't need to ask for any confirmation and can simply save. - await saveConnection(formValues, true); + await saveConnection(formValues, true, catalogHasChanged); } setSaved(true); @@ -151,6 +152,10 @@ export const ConnectionReplicationTab: React.FC = () => { } }, [connection, formatMessage, openModal]); + if (schemaError) { + return ; + } + return (
{!schemaRefreshing && connection ? ( From aaaeaaea3c05e04db1c441f90fb199e0c5bc8bba Mon Sep 17 00:00:00 2001 From: KC Date: Mon, 3 Oct 2022 13:28:54 -0400 Subject: [PATCH 083/107] schema refreshing logic, confirm catalog dialog abstraction --- .../ConnectionReplicationTab.tsx | 33 +++++-------------- .../useConfirmCatalogDiff.tsx | 26 +++++++++++++++ 2 files changed, 34 insertions(+), 25 deletions(-) create mode 100644 airbyte-webapp/src/views/Connection/CatalogDiffModal/useConfirmCatalogDiff.tsx diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionReplicationTab.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionReplicationTab.tsx index f3d82d79b5cb..0c419f201107 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionReplicationTab.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionReplicationTab.tsx @@ -1,5 +1,5 @@ import { Form, Formik, FormikHelpers } from "formik"; -import React, { useCallback, useEffect, useState } from "react"; +import React, { useCallback, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { SchemaError } from "components/CreateConnection/SchemaError"; @@ -20,7 +20,7 @@ import { useModalService } from "hooks/services/Modal"; import { useConnectionService, ValuesProps } from "hooks/services/useConnectionHook"; import { useCurrentWorkspaceId } from "services/workspaces/WorkspacesService"; import { equal, naturalComparatorBy } from "utils/objects"; -import { CatalogDiffModal } from "views/Connection/CatalogDiffModal/CatalogDiffModal"; +import { useConfirmCatalogDiff } from "views/Connection/CatalogDiffModal/useConfirmCatalogDiff"; import EditControls from "views/Connection/ConnectionForm/components/EditControls"; import { ConnectionFormFields } from "views/Connection/ConnectionForm/ConnectionFormFields"; import { connectionValidationSchema, FormikConnectionFormValues } from "views/Connection/ConnectionForm/formConfig"; @@ -47,9 +47,6 @@ export const ConnectionReplicationTab: React.FC = () => { const saveConnection = useCallback( async (values: ValuesProps, skipReset: boolean, catalogHasChanged: boolean) => { - if (schemaRefreshing) { - return; - } const connectionAsUpdate = toWebBackendConnectionUpdate(connection); await updateConnection({ @@ -60,6 +57,7 @@ export const ConnectionReplicationTab: React.FC = () => { }); if (catalogHasChanged) { + // TODO: Move this into a useTrackChangedCatalog method (name pending) post Vlad's analytics hook work analyticsService.track(Namespace.CONNECTION, Action.EDIT_SCHEMA, { actionDescription: "Connection saved with catalog changes", connector_source: connection.source.sourceName, @@ -70,7 +68,7 @@ export const ConnectionReplicationTab: React.FC = () => { }); } }, - [analyticsService, connection, schemaRefreshing, updateConnection] + [analyticsService, connection, updateConnection] ); const onFormSubmit = useCallback( @@ -80,7 +78,6 @@ export const ConnectionReplicationTab: React.FC = () => { // Detect whether the catalog has any differences in its enabled streams compared to the original one. // This could be due to user changes (e.g. in the sync mode) or due to new/removed // streams due to a "refreshed source schema". - // TODO: Can pass this value into the saveConnection function for the analytics call so we don't compare twice const catalogHasChanged = !equal( formValues.syncCatalog.streams .filter((s) => s.config?.selected) @@ -138,27 +135,13 @@ export const ConnectionReplicationTab: React.FC = () => { ] ); - useEffect(() => { - // If we have a catalogDiff we always want to show the modal - const { catalogDiff, syncCatalog } = connection; - if (catalogDiff?.transforms && catalogDiff.transforms?.length > 0) { - openModal({ - title: formatMessage({ id: "connection.updateSchema.completed" }), - preventCancel: true, - content: ({ onClose }) => ( - - ), - }); - } - }, [connection, formatMessage, openModal]); - - if (schemaError) { - return ; - } + useConfirmCatalogDiff(connection); return (
- {!schemaRefreshing && connection ? ( + {schemaError && !schemaRefreshing ? ( + + ) : !schemaRefreshing && connection ? ( { + const { formatMessage } = useIntl(); + const { openModal } = useModalService(); + + useEffect(() => { + // If we have a catalogDiff we always want to show the modal + const { catalogDiff, syncCatalog } = connection; + if (catalogDiff?.transforms && catalogDiff.transforms?.length > 0) { + openModal({ + title: formatMessage({ id: "connection.updateSchema.completed" }), + preventCancel: true, + content: ({ onClose }) => ( + + ), + }); + } + }, [connection, formatMessage, openModal]); +}; From 38bb9b6b3707f71a744aa4dac3687e3e25ab1573 Mon Sep 17 00:00:00 2001 From: KC Date: Mon, 3 Oct 2022 13:31:21 -0400 Subject: [PATCH 084/107] CreateConnection -> CreateConnectionForm --- ....module.scss => CreateConnectionForm.module.scss} | 0 ...CreateConnection.tsx => CreateConnectionForm.tsx} | 12 ++++++++---- .../src/components/CreateConnection/index.tsx | 2 +- .../pages/CreationFormPage/CreationFormPage.tsx | 4 ++-- .../OnboardingPage/components/ConnectionStep.tsx | 8 ++++++-- 5 files changed, 17 insertions(+), 9 deletions(-) rename airbyte-webapp/src/components/CreateConnection/{CreateConnection.module.scss => CreateConnectionForm.module.scss} (100%) rename airbyte-webapp/src/components/CreateConnection/{CreateConnection.tsx => CreateConnectionForm.tsx} (92%) diff --git a/airbyte-webapp/src/components/CreateConnection/CreateConnection.module.scss b/airbyte-webapp/src/components/CreateConnection/CreateConnectionForm.module.scss similarity index 100% rename from airbyte-webapp/src/components/CreateConnection/CreateConnection.module.scss rename to airbyte-webapp/src/components/CreateConnection/CreateConnectionForm.module.scss diff --git a/airbyte-webapp/src/components/CreateConnection/CreateConnection.tsx b/airbyte-webapp/src/components/CreateConnection/CreateConnectionForm.tsx similarity index 92% rename from airbyte-webapp/src/components/CreateConnection/CreateConnection.tsx rename to airbyte-webapp/src/components/CreateConnection/CreateConnectionForm.tsx index 3d42e273a38b..c6268f537eb4 100644 --- a/airbyte-webapp/src/components/CreateConnection/CreateConnection.tsx +++ b/airbyte-webapp/src/components/CreateConnection/CreateConnectionForm.tsx @@ -21,7 +21,7 @@ import { OperationsSection } from "views/Connection/ConnectionForm/components/Op import { ConnectionFormFields } from "views/Connection/ConnectionForm/ConnectionFormFields"; import { connectionValidationSchema, FormikConnectionFormValues } from "views/Connection/ConnectionForm/formConfig"; -import styles from "./CreateConnection.module.scss"; +import styles from "./CreateConnectionForm.module.scss"; import { CreateConnectionNameField } from "./CreateConnectionNameField"; import { SchemaError } from "./SchemaError"; @@ -35,7 +35,7 @@ interface CreateConnectionPropsInner extends Pick = ({ schemaError, afterSubmitConnection }) => { +const CreateConnectionFormInner: React.FC = ({ schemaError, afterSubmitConnection }) => { const navigate = useNavigate(); const { mutateAsync: createConnection } = useCreateConnection(); @@ -125,7 +125,11 @@ const CreateConnectionInner: React.FC = ({ schemaErr ); }; -export const CreateConnection: React.FC = ({ source, destination, afterSubmitConnection }) => { +export const CreateConnectionForm: React.FC = ({ + source, + destination, + afterSubmitConnection, +}) => { const { schema, isLoading, schemaErrorStatus, catalogId, onDiscoverSchema } = useDiscoverSchema( source.sourceId, true @@ -148,7 +152,7 @@ export const CreateConnection: React.FC = ({ source, dest {isLoading ? ( ) : ( - + )} ); diff --git a/airbyte-webapp/src/components/CreateConnection/index.tsx b/airbyte-webapp/src/components/CreateConnection/index.tsx index 22d031f89186..8d57b01b25fc 100644 --- a/airbyte-webapp/src/components/CreateConnection/index.tsx +++ b/airbyte-webapp/src/components/CreateConnection/index.tsx @@ -1 +1 @@ -export * from "./CreateConnection"; +export * from "./CreateConnectionForm"; diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/CreationFormPage/CreationFormPage.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/CreationFormPage/CreationFormPage.tsx index 69224657cb98..3bce72196493 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/CreationFormPage/CreationFormPage.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/CreationFormPage/CreationFormPage.tsx @@ -5,7 +5,7 @@ import { useLocation, useNavigate } from "react-router-dom"; import { LoadingPage } from "components"; import ConnectionBlock from "components/ConnectionBlock"; import { FormPageContent } from "components/ConnectorBlocks"; -import { CreateConnection } from "components/CreateConnection/CreateConnection"; +import { CreateConnectionForm } from "components/CreateConnection/CreateConnectionForm"; import HeadTitle from "components/HeadTitle"; import { PageHeader } from "components/ui/PageHeader"; import { StepsMenu } from "components/ui/StepsMenu"; @@ -168,7 +168,7 @@ export const CreationFormPage: React.FC = () => { return ; } - return ; + return ; }; const steps = diff --git a/airbyte-webapp/src/pages/OnboardingPage/components/ConnectionStep.tsx b/airbyte-webapp/src/pages/OnboardingPage/components/ConnectionStep.tsx index 7826992684d8..6b19c6a1cc92 100644 --- a/airbyte-webapp/src/pages/OnboardingPage/components/ConnectionStep.tsx +++ b/airbyte-webapp/src/pages/OnboardingPage/components/ConnectionStep.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { CreateConnection } from "components/CreateConnection/CreateConnection"; +import { CreateConnectionForm } from "components/CreateConnection/CreateConnectionForm"; import { useDestinationList } from "hooks/services/useDestinationHook"; import { useSourceList } from "hooks/services/useSourceHook"; @@ -14,7 +14,11 @@ const ConnectionStep: React.FC = ({ onNextStep: afterSubmit const { destinations } = useDestinationList(); return ( - + ); }; From f3c3281f14ab62097c5919584ad2dde4d3c31daf Mon Sep 17 00:00:00 2001 From: KC Date: Mon, 3 Oct 2022 13:55:02 -0400 Subject: [PATCH 085/107] form config test cleanup --- .../__snapshots__/formConfig.test.ts.snap | 4055 +++++------------ .../ConnectionForm/formConfig.test.ts | 9 +- 2 files changed, 1232 insertions(+), 2832 deletions(-) diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/__snapshots__/formConfig.test.ts.snap b/airbyte-webapp/src/views/Connection/ConnectionForm/__snapshots__/formConfig.test.ts.snap index c78c5f7076c9..171ce4d48e02 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/__snapshots__/formConfig.test.ts.snap +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/__snapshots__/formConfig.test.ts.snap @@ -1,66 +1,46 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`#useInitialValues should generate initial values w/ edit mode: false 1`] = ` +exports[`#useInitialValues should generate initial values w/ 'not create' mode: false 1`] = ` Object { - "all": Array [ - Object { - "name": "Scrafty <> Heroku Postgres", - "namespaceDefinition": "source", - "namespaceFormat": "\${SOURCE_NAMESPACE}", - "normalization": "basic", - "prefix": "", - "scheduleData": null, - "scheduleType": "manual", - "syncCatalog": Object { - "streams": Array [ - Object { - "config": Object { - "aliasName": "pokemon", - "cursorField": Array [], - "destinationSyncMode": "overwrite", - "primaryKey": Array [], - "selected": true, - "syncMode": "full_refresh", - }, - "id": "0", - "stream": Object { - "defaultCursorField": Array [], - "jsonSchema": Object { - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": Object { - "abilities": Object { - "items": Object { + "name": "Scrafty <> Heroku Postgres", + "namespaceDefinition": "source", + "namespaceFormat": "\${SOURCE_NAMESPACE}", + "normalization": "basic", + "prefix": "", + "scheduleData": null, + "scheduleType": "manual", + "syncCatalog": Object { + "streams": Array [ + Object { + "config": Object { + "aliasName": "pokemon", + "cursorField": Array [], + "destinationSyncMode": "overwrite", + "primaryKey": Array [], + "selected": true, + "syncMode": "full_refresh", + }, + "id": "0", + "stream": Object { + "defaultCursorField": Array [], + "jsonSchema": Object { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": Object { + "abilities": Object { + "items": Object { + "properties": Object { + "ability": Object { "properties": Object { - "ability": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "is_hidden": Object { + "name": Object { "type": Array [ "null", - "boolean", + "string", ], }, - "slot": Object { + "url": Object { "type": Array [ "null", - "integer", + "string", ], }, }, @@ -69,19 +49,71 @@ Object { "object", ], }, - "type": Array [ - "null", - "array", - ], + "is_hidden": Object { + "type": Array [ + "null", + "boolean", + ], + }, + "slot": Object { + "type": Array [ + "null", + "integer", + ], + }, }, - "base_experience": Object { - "type": Array [ - "null", - "integer", - ], + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "base_experience": Object { + "type": Array [ + "null", + "integer", + ], + }, + "forms": Object { + "items": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, }, - "forms": Object { - "items": Object { + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "game_indices": Object { + "items": Object { + "properties": Object { + "game_index": Object { + "type": Array [ + "null", + "integer", + ], + }, + "version": Object { "properties": Object { "name": Object { "type": Array [ @@ -101,38 +133,38 @@ Object { "object", ], }, - "type": Array [ - "null", - "array", - ], }, - "game_indices": Object { - "items": Object { + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "height": Object { + "type": Array [ + "null", + "integer", + ], + }, + "held_items": Object { + "items": Object { + "properties": Object { + "item": Object { "properties": Object { - "game_index": Object { + "name": Object { "type": Array [ "null", - "integer", + "string", ], }, - "version": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, + "url": Object { "type": Array [ "null", - "object", + "string", ], }, }, @@ -141,67 +173,27 @@ Object { "object", ], }, - "type": Array [ - "null", - "array", - ], - }, - "height": Object { - "type": Array [ - "null", - "integer", - ], - }, - "held_items": Object { - "items": Object { - "properties": Object { - "item": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, + "version_details": Object { + "items": Object { + "properties": Object { + "rarity": Object { + "type": Array [ + "null", + "integer", + ], }, - "type": Array [ - "null", - "object", - ], - }, - "version_details": Object { - "items": Object { + "version": Object { "properties": Object { - "rarity": Object { + "name": Object { "type": Array [ "null", - "integer", + "string", ], }, - "version": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, + "url": Object { "type": Array [ "null", - "object", + "string", ], }, }, @@ -210,110 +202,110 @@ Object { "object", ], }, - "type": Array [ - "null", - "array", - ], }, + "type": Array [ + "null", + "object", + ], }, "type": Array [ "null", - "object", + "array", ], }, - "type": Array [ - "null", - "array", - ], - }, - "id": Object { - "type": Array [ - "null", - "integer", - ], }, - "is_default ": Object { - "type": Array [ - "null", - "boolean", - ], - }, - "location_area_encounters": Object { - "type": Array [ - "null", - "string", - ], - }, - "moves": Object { - "items": Object { + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "id": Object { + "type": Array [ + "null", + "integer", + ], + }, + "is_default ": Object { + "type": Array [ + "null", + "boolean", + ], + }, + "location_area_encounters": Object { + "type": Array [ + "null", + "string", + ], + }, + "moves": Object { + "items": Object { + "properties": Object { + "move": Object { "properties": Object { - "move": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { "type": Array [ "null", - "object", + "string", ], }, - "version_group_details": Object { - "items": Object { + }, + "type": Array [ + "null", + "object", + ], + }, + "version_group_details": Object { + "items": Object { + "properties": Object { + "level_learned_at": Object { + "type": Array [ + "null", + "integer", + ], + }, + "move_learn_method": Object { "properties": Object { - "level_learned_at": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { "type": Array [ "null", - "integer", + "string", ], }, - "move_learn_method": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, + }, + "type": Array [ + "null", + "object", + ], + }, + "version_group": Object { + "properties": Object { + "name": Object { "type": Array [ "null", - "object", + "string", ], }, - "version_group": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, + "url": Object { "type": Array [ "null", - "object", + "string", ], }, }, @@ -322,2275 +314,143 @@ Object { "object", ], }, - "type": Array [ - "null", - "array", - ], }, + "type": Array [ + "null", + "object", + ], }, "type": Array [ "null", - "object", + "array", ], }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "order": Object { + "type": Array [ + "null", + "integer", + ], + }, + "species": Object { + "properties": Object { + "name": Object { "type": Array [ "null", - "array", + "string", ], }, - "name": Object { + "url": Object { "type": Array [ "null", "string", ], }, - "order": Object { + }, + "type": Array [ + "null", + "object", + ], + }, + "sprites": Object { + "properties": Object { + "back_default": Object { "type": Array [ "null", - "integer", + "string", ], }, - "species": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, + "back_female": Object { "type": Array [ "null", - "object", + "string", ], }, - "sprites": Object { - "properties": Object { - "back_default": Object { - "type": Array [ - "null", - "string", - ], - }, - "back_female": Object { - "type": Array [ - "null", - "string", - ], - }, - "back_shiny": Object { - "type": Array [ - "null", - "string", - ], - }, - "back_shiny_female": Object { - "type": Array [ - "null", - "string", - ], - }, - "front_default": Object { - "type": Array [ - "null", - "string", - ], - }, - "front_female": Object { - "type": Array [ - "null", - "string", - ], - }, - "front_shiny": Object { - "type": Array [ - "null", - "string", - ], - }, - "front_shiny_female": Object { - "type": Array [ - "null", - "string", - ], - }, - }, + "back_shiny": Object { "type": Array [ "null", - "object", + "string", ], }, - "stats": Object { - "items": Object { - "properties": Object { - "base_stat": Object { - "type": Array [ - "null", - "integer", - ], - }, - "effort": Object { - "type": Array [ - "null", - "integer", - ], - }, - "stat": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, + "back_shiny_female": Object { "type": Array [ "null", - "array", + "string", ], }, - "types": Object { - "items": Object { - "properties": Object { - "slot": Object { - "type": Array [ - "null", - "integer", - ], - }, - "type": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "type": Array [ - "null", - "array", - ], - }, - "weight": Object { - "type": Array [ - "null", - "integer", - ], - }, - }, - "type": "object", - }, - "name": "pokemon", - "sourceDefinedPrimaryKey": Array [], - "supportedSyncModes": Array [ - "full_refresh", - ], - }, - }, - ], - }, - "transformations": Array [], - }, - ], - "current": Object { - "name": "Scrafty <> Heroku Postgres", - "namespaceDefinition": "source", - "namespaceFormat": "\${SOURCE_NAMESPACE}", - "normalization": "basic", - "prefix": "", - "scheduleData": null, - "scheduleType": "manual", - "syncCatalog": Object { - "streams": Array [ - Object { - "config": Object { - "aliasName": "pokemon", - "cursorField": Array [], - "destinationSyncMode": "overwrite", - "primaryKey": Array [], - "selected": true, - "syncMode": "full_refresh", - }, - "id": "0", - "stream": Object { - "defaultCursorField": Array [], - "jsonSchema": Object { - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": Object { - "abilities": Object { - "items": Object { - "properties": Object { - "ability": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "is_hidden": Object { - "type": Array [ - "null", - "boolean", - ], - }, - "slot": Object { - "type": Array [ - "null", - "integer", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "type": Array [ - "null", - "array", - ], - }, - "base_experience": Object { - "type": Array [ - "null", - "integer", - ], - }, - "forms": Object { - "items": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "type": Array [ - "null", - "array", - ], - }, - "game_indices": Object { - "items": Object { - "properties": Object { - "game_index": Object { - "type": Array [ - "null", - "integer", - ], - }, - "version": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "type": Array [ - "null", - "array", - ], - }, - "height": Object { - "type": Array [ - "null", - "integer", - ], - }, - "held_items": Object { - "items": Object { - "properties": Object { - "item": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "version_details": Object { - "items": Object { - "properties": Object { - "rarity": Object { - "type": Array [ - "null", - "integer", - ], - }, - "version": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "type": Array [ - "null", - "array", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "type": Array [ - "null", - "array", - ], - }, - "id": Object { - "type": Array [ - "null", - "integer", - ], - }, - "is_default ": Object { - "type": Array [ - "null", - "boolean", - ], - }, - "location_area_encounters": Object { - "type": Array [ - "null", - "string", - ], - }, - "moves": Object { - "items": Object { - "properties": Object { - "move": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "version_group_details": Object { - "items": Object { - "properties": Object { - "level_learned_at": Object { - "type": Array [ - "null", - "integer", - ], - }, - "move_learn_method": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "version_group": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "type": Array [ - "null", - "array", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "type": Array [ - "null", - "array", - ], - }, - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "order": Object { - "type": Array [ - "null", - "integer", - ], - }, - "species": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "sprites": Object { - "properties": Object { - "back_default": Object { - "type": Array [ - "null", - "string", - ], - }, - "back_female": Object { - "type": Array [ - "null", - "string", - ], - }, - "back_shiny": Object { - "type": Array [ - "null", - "string", - ], - }, - "back_shiny_female": Object { - "type": Array [ - "null", - "string", - ], - }, - "front_default": Object { - "type": Array [ - "null", - "string", - ], - }, - "front_female": Object { - "type": Array [ - "null", - "string", - ], - }, - "front_shiny": Object { - "type": Array [ - "null", - "string", - ], - }, - "front_shiny_female": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "stats": Object { - "items": Object { - "properties": Object { - "base_stat": Object { - "type": Array [ - "null", - "integer", - ], - }, - "effort": Object { - "type": Array [ - "null", - "integer", - ], - }, - "stat": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "type": Array [ - "null", - "array", - ], - }, - "types": Object { - "items": Object { - "properties": Object { - "slot": Object { - "type": Array [ - "null", - "integer", - ], - }, - "type": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "type": Array [ - "null", - "array", - ], - }, - "weight": Object { - "type": Array [ - "null", - "integer", - ], - }, - }, - "type": "object", - }, - "name": "pokemon", - "sourceDefinedPrimaryKey": Array [], - "supportedSyncModes": Array [ - "full_refresh", - ], - }, - }, - ], - }, - "transformations": Array [], - }, - "error": undefined, -} -`; - -exports[`#useInitialValues should generate initial values w/ edit mode: true 1`] = ` -Object { - "all": Array [ - Object { - "name": "Scrafty <> Heroku Postgres", - "namespaceDefinition": "source", - "namespaceFormat": "\${SOURCE_NAMESPACE}", - "normalization": "basic", - "prefix": "", - "scheduleData": null, - "scheduleType": "manual", - "syncCatalog": Object { - "streams": Array [ - Object { - "config": Object { - "aliasName": "pokemon", - "cursorField": Array [], - "destinationSyncMode": "append", - "primaryKey": Array [], - "selected": true, - "syncMode": "full_refresh", - }, - "id": "0", - "stream": Object { - "defaultCursorField": Array [], - "jsonSchema": Object { - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": Object { - "abilities": Object { - "items": Object { - "properties": Object { - "ability": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "is_hidden": Object { - "type": Array [ - "null", - "boolean", - ], - }, - "slot": Object { - "type": Array [ - "null", - "integer", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "type": Array [ - "null", - "array", - ], - }, - "base_experience": Object { - "type": Array [ - "null", - "integer", - ], - }, - "forms": Object { - "items": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "type": Array [ - "null", - "array", - ], - }, - "game_indices": Object { - "items": Object { - "properties": Object { - "game_index": Object { - "type": Array [ - "null", - "integer", - ], - }, - "version": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "type": Array [ - "null", - "array", - ], - }, - "height": Object { - "type": Array [ - "null", - "integer", - ], - }, - "held_items": Object { - "items": Object { - "properties": Object { - "item": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "version_details": Object { - "items": Object { - "properties": Object { - "rarity": Object { - "type": Array [ - "null", - "integer", - ], - }, - "version": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "type": Array [ - "null", - "array", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "type": Array [ - "null", - "array", - ], - }, - "id": Object { - "type": Array [ - "null", - "integer", - ], - }, - "is_default ": Object { - "type": Array [ - "null", - "boolean", - ], - }, - "location_area_encounters": Object { - "type": Array [ - "null", - "string", - ], - }, - "moves": Object { - "items": Object { - "properties": Object { - "move": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "version_group_details": Object { - "items": Object { - "properties": Object { - "level_learned_at": Object { - "type": Array [ - "null", - "integer", - ], - }, - "move_learn_method": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "version_group": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "type": Array [ - "null", - "array", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "type": Array [ - "null", - "array", - ], - }, - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "order": Object { - "type": Array [ - "null", - "integer", - ], - }, - "species": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "sprites": Object { - "properties": Object { - "back_default": Object { - "type": Array [ - "null", - "string", - ], - }, - "back_female": Object { - "type": Array [ - "null", - "string", - ], - }, - "back_shiny": Object { - "type": Array [ - "null", - "string", - ], - }, - "back_shiny_female": Object { - "type": Array [ - "null", - "string", - ], - }, - "front_default": Object { - "type": Array [ - "null", - "string", - ], - }, - "front_female": Object { - "type": Array [ - "null", - "string", - ], - }, - "front_shiny": Object { - "type": Array [ - "null", - "string", - ], - }, - "front_shiny_female": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "stats": Object { - "items": Object { - "properties": Object { - "base_stat": Object { - "type": Array [ - "null", - "integer", - ], - }, - "effort": Object { - "type": Array [ - "null", - "integer", - ], - }, - "stat": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "type": Array [ - "null", - "array", - ], - }, - "types": Object { - "items": Object { - "properties": Object { - "slot": Object { - "type": Array [ - "null", - "integer", - ], - }, - "type": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "type": Array [ - "null", - "array", - ], - }, - "weight": Object { - "type": Array [ - "null", - "integer", - ], - }, - }, - "type": "object", - }, - "name": "pokemon", - "sourceDefinedPrimaryKey": Array [], - "supportedSyncModes": Array [ - "full_refresh", - ], - }, - }, - ], - }, - "transformations": Array [], - }, - ], - "current": Object { - "name": "Scrafty <> Heroku Postgres", - "namespaceDefinition": "source", - "namespaceFormat": "\${SOURCE_NAMESPACE}", - "normalization": "basic", - "prefix": "", - "scheduleData": null, - "scheduleType": "manual", - "syncCatalog": Object { - "streams": Array [ - Object { - "config": Object { - "aliasName": "pokemon", - "cursorField": Array [], - "destinationSyncMode": "append", - "primaryKey": Array [], - "selected": true, - "syncMode": "full_refresh", - }, - "id": "0", - "stream": Object { - "defaultCursorField": Array [], - "jsonSchema": Object { - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": Object { - "abilities": Object { - "items": Object { - "properties": Object { - "ability": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "is_hidden": Object { - "type": Array [ - "null", - "boolean", - ], - }, - "slot": Object { - "type": Array [ - "null", - "integer", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "type": Array [ - "null", - "array", - ], - }, - "base_experience": Object { - "type": Array [ - "null", - "integer", - ], - }, - "forms": Object { - "items": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "type": Array [ - "null", - "array", - ], - }, - "game_indices": Object { - "items": Object { - "properties": Object { - "game_index": Object { - "type": Array [ - "null", - "integer", - ], - }, - "version": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "type": Array [ - "null", - "array", - ], - }, - "height": Object { - "type": Array [ - "null", - "integer", - ], - }, - "held_items": Object { - "items": Object { - "properties": Object { - "item": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "version_details": Object { - "items": Object { - "properties": Object { - "rarity": Object { - "type": Array [ - "null", - "integer", - ], - }, - "version": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "type": Array [ - "null", - "array", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "type": Array [ - "null", - "array", - ], - }, - "id": Object { - "type": Array [ - "null", - "integer", - ], - }, - "is_default ": Object { - "type": Array [ - "null", - "boolean", - ], - }, - "location_area_encounters": Object { - "type": Array [ - "null", - "string", - ], - }, - "moves": Object { - "items": Object { - "properties": Object { - "move": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "version_group_details": Object { - "items": Object { - "properties": Object { - "level_learned_at": Object { - "type": Array [ - "null", - "integer", - ], - }, - "move_learn_method": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "version_group": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "type": Array [ - "null", - "array", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "type": Array [ - "null", - "array", - ], - }, - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "order": Object { - "type": Array [ - "null", - "integer", - ], - }, - "species": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "sprites": Object { - "properties": Object { - "back_default": Object { - "type": Array [ - "null", - "string", - ], - }, - "back_female": Object { - "type": Array [ - "null", - "string", - ], - }, - "back_shiny": Object { - "type": Array [ - "null", - "string", - ], - }, - "back_shiny_female": Object { - "type": Array [ - "null", - "string", - ], - }, - "front_default": Object { - "type": Array [ - "null", - "string", - ], - }, - "front_female": Object { - "type": Array [ - "null", - "string", - ], - }, - "front_shiny": Object { - "type": Array [ - "null", - "string", - ], - }, - "front_shiny_female": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "stats": Object { - "items": Object { - "properties": Object { - "base_stat": Object { - "type": Array [ - "null", - "integer", - ], - }, - "effort": Object { - "type": Array [ - "null", - "integer", - ], - }, - "stat": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "type": Array [ - "null", - "array", - ], - }, - "types": Object { - "items": Object { - "properties": Object { - "slot": Object { - "type": Array [ - "null", - "integer", - ], - }, - "type": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "type": Array [ - "null", - "array", - ], - }, - "weight": Object { - "type": Array [ - "null", - "integer", - ], - }, - }, - "type": "object", - }, - "name": "pokemon", - "sourceDefinedPrimaryKey": Array [], - "supportedSyncModes": Array [ - "full_refresh", - ], - }, - }, - ], - }, - "transformations": Array [], - }, - "error": undefined, -} -`; - -exports[`#useInitialValues should generate initial values w/ no edit mode 1`] = ` -Object { - "all": Array [ - Object { - "name": "Scrafty <> Heroku Postgres", - "namespaceDefinition": "source", - "namespaceFormat": "\${SOURCE_NAMESPACE}", - "normalization": "basic", - "prefix": "", - "scheduleData": null, - "scheduleType": "manual", - "syncCatalog": Object { - "streams": Array [ - Object { - "config": Object { - "aliasName": "pokemon", - "cursorField": Array [], - "destinationSyncMode": "overwrite", - "primaryKey": Array [], - "selected": true, - "syncMode": "full_refresh", - }, - "id": "0", - "stream": Object { - "defaultCursorField": Array [], - "jsonSchema": Object { - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": Object { - "abilities": Object { - "items": Object { - "properties": Object { - "ability": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "is_hidden": Object { - "type": Array [ - "null", - "boolean", - ], - }, - "slot": Object { - "type": Array [ - "null", - "integer", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "type": Array [ - "null", - "array", - ], - }, - "base_experience": Object { - "type": Array [ - "null", - "integer", - ], - }, - "forms": Object { - "items": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "type": Array [ - "null", - "array", - ], - }, - "game_indices": Object { - "items": Object { - "properties": Object { - "game_index": Object { - "type": Array [ - "null", - "integer", - ], - }, - "version": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "type": Array [ - "null", - "array", - ], - }, - "height": Object { - "type": Array [ - "null", - "integer", - ], - }, - "held_items": Object { - "items": Object { - "properties": Object { - "item": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "version_details": Object { - "items": Object { - "properties": Object { - "rarity": Object { - "type": Array [ - "null", - "integer", - ], - }, - "version": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "type": Array [ - "null", - "array", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "type": Array [ - "null", - "array", - ], - }, - "id": Object { - "type": Array [ - "null", - "integer", - ], - }, - "is_default ": Object { - "type": Array [ - "null", - "boolean", - ], - }, - "location_area_encounters": Object { - "type": Array [ - "null", - "string", - ], - }, - "moves": Object { - "items": Object { - "properties": Object { - "move": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "version_group_details": Object { - "items": Object { - "properties": Object { - "level_learned_at": Object { - "type": Array [ - "null", - "integer", - ], - }, - "move_learn_method": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "version_group": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "type": Array [ - "null", - "array", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "type": Array [ - "null", - "array", - ], - }, - "name": Object { + "front_default": Object { "type": Array [ "null", "string", ], }, - "order": Object { + "front_female": Object { "type": Array [ "null", - "integer", + "string", ], }, - "species": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, + "front_shiny": Object { "type": Array [ "null", - "object", + "string", ], }, - "sprites": Object { - "properties": Object { - "back_default": Object { - "type": Array [ - "null", - "string", - ], - }, - "back_female": Object { - "type": Array [ - "null", - "string", - ], - }, - "back_shiny": Object { - "type": Array [ - "null", - "string", - ], - }, - "back_shiny_female": Object { - "type": Array [ - "null", - "string", - ], - }, - "front_default": Object { - "type": Array [ - "null", - "string", - ], - }, - "front_female": Object { - "type": Array [ - "null", - "string", - ], - }, - "front_shiny": Object { - "type": Array [ - "null", - "string", - ], - }, - "front_shiny_female": Object { - "type": Array [ - "null", - "string", - ], - }, - }, + "front_shiny_female": Object { "type": Array [ "null", - "object", + "string", ], }, - "stats": Object { - "items": Object { + }, + "type": Array [ + "null", + "object", + ], + }, + "stats": Object { + "items": Object { + "properties": Object { + "base_stat": Object { + "type": Array [ + "null", + "integer", + ], + }, + "effort": Object { + "type": Array [ + "null", + "integer", + ], + }, + "stat": Object { "properties": Object { - "base_stat": Object { - "type": Array [ - "null", - "integer", - ], - }, - "effort": Object { + "name": Object { "type": Array [ "null", - "integer", + "string", ], }, - "stat": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, + "url": Object { "type": Array [ "null", - "object", + "string", ], }, }, @@ -2599,38 +459,38 @@ Object { "object", ], }, - "type": Array [ - "null", - "array", - ], }, - "types": Object { - "items": Object { + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "types": Object { + "items": Object { + "properties": Object { + "slot": Object { + "type": Array [ + "null", + "integer", + ], + }, + "type": Object { "properties": Object { - "slot": Object { + "name": Object { "type": Array [ "null", - "integer", + "string", ], }, - "type": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, + "url": Object { "type": Array [ "null", - "object", + "string", ], }, }, @@ -2639,196 +499,238 @@ Object { "object", ], }, - "type": Array [ - "null", - "array", - ], - }, - "weight": Object { - "type": Array [ - "null", - "integer", - ], }, + "type": Array [ + "null", + "object", + ], }, - "type": "object", + "type": Array [ + "null", + "array", + ], + }, + "weight": Object { + "type": Array [ + "null", + "integer", + ], }, - "name": "pokemon", - "sourceDefinedPrimaryKey": Array [], - "supportedSyncModes": Array [ - "full_refresh", - ], }, + "type": "object", }, - ], + "name": "pokemon", + "sourceDefinedPrimaryKey": Array [], + "supportedSyncModes": Array [ + "full_refresh", + ], + }, }, - "transformations": Array [], - }, - ], - "current": Object { - "name": "Scrafty <> Heroku Postgres", - "namespaceDefinition": "source", - "namespaceFormat": "\${SOURCE_NAMESPACE}", - "normalization": "basic", - "prefix": "", - "scheduleData": null, - "scheduleType": "manual", - "syncCatalog": Object { - "streams": Array [ - Object { - "config": Object { - "aliasName": "pokemon", - "cursorField": Array [], - "destinationSyncMode": "overwrite", - "primaryKey": Array [], - "selected": true, - "syncMode": "full_refresh", - }, - "id": "0", - "stream": Object { - "defaultCursorField": Array [], - "jsonSchema": Object { - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": Object { - "abilities": Object { - "items": Object { - "properties": Object { - "ability": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, + ], + }, + "transformations": Array [], +} +`; + +exports[`#useInitialValues should generate initial values w/ 'not create' mode: true 1`] = ` +Object { + "namespaceDefinition": "source", + "namespaceFormat": "\${SOURCE_NAMESPACE}", + "normalization": "basic", + "prefix": "", + "scheduleData": null, + "scheduleType": "manual", + "syncCatalog": Object { + "streams": Array [ + Object { + "config": Object { + "aliasName": "pokemon", + "cursorField": Array [], + "destinationSyncMode": "append", + "primaryKey": Array [], + "selected": true, + "syncMode": "full_refresh", + }, + "id": "0", + "stream": Object { + "defaultCursorField": Array [], + "jsonSchema": Object { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": Object { + "abilities": Object { + "items": Object { + "properties": Object { + "ability": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], }, - "type": Array [ - "null", - "object", - ], - }, - "is_hidden": Object { - "type": Array [ - "null", - "boolean", - ], - }, - "slot": Object { - "type": Array [ - "null", - "integer", - ], }, + "type": Array [ + "null", + "object", + ], + }, + "is_hidden": Object { + "type": Array [ + "null", + "boolean", + ], + }, + "slot": Object { + "type": Array [ + "null", + "integer", + ], }, - "type": Array [ - "null", - "object", - ], }, "type": Array [ "null", - "array", - ], - }, - "base_experience": Object { - "type": Array [ - "null", - "integer", + "object", ], }, - "forms": Object { - "items": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, + "type": Array [ + "null", + "array", + ], + }, + "base_experience": Object { + "type": Array [ + "null", + "integer", + ], + }, + "forms": Object { + "items": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], }, - "type": Array [ - "null", - "object", - ], }, "type": Array [ "null", - "array", + "object", ], }, - "game_indices": Object { - "items": Object { - "properties": Object { - "game_index": Object { - "type": Array [ - "null", - "integer", - ], - }, - "version": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, + "type": Array [ + "null", + "array", + ], + }, + "game_indices": Object { + "items": Object { + "properties": Object { + "game_index": Object { + "type": Array [ + "null", + "integer", + ], + }, + "version": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], }, - "type": Array [ - "null", - "object", - ], }, + "type": Array [ + "null", + "object", + ], }, - "type": Array [ - "null", - "object", - ], }, "type": Array [ "null", - "array", - ], - }, - "height": Object { - "type": Array [ - "null", - "integer", + "object", ], }, - "held_items": Object { - "items": Object { - "properties": Object { - "item": Object { + "type": Array [ + "null", + "array", + ], + }, + "height": Object { + "type": Array [ + "null", + "integer", + ], + }, + "held_items": Object { + "items": Object { + "properties": Object { + "item": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "version_details": Object { + "items": Object { "properties": Object { - "name": Object { + "rarity": Object { "type": Array [ "null", - "string", + "integer", ], }, - "url": Object { + "version": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, "type": Array [ "null", - "string", + "object", ], }, }, @@ -2837,193 +739,255 @@ Object { "object", ], }, - "version_details": Object { - "items": Object { - "properties": Object { - "rarity": Object { - "type": Array [ - "null", - "integer", - ], - }, - "version": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - }, - "type": Array [ - "null", - "object", - ], - }, - "type": Array [ - "null", - "array", - ], - }, + "type": Array [ + "null", + "array", + ], }, - "type": Array [ - "null", - "object", - ], }, "type": Array [ "null", - "array", - ], - }, - "id": Object { - "type": Array [ - "null", - "integer", - ], - }, - "is_default ": Object { - "type": Array [ - "null", - "boolean", - ], - }, - "location_area_encounters": Object { - "type": Array [ - "null", - "string", + "object", ], }, - "moves": Object { - "items": Object { - "properties": Object { - "move": Object { + "type": Array [ + "null", + "array", + ], + }, + "id": Object { + "type": Array [ + "null", + "integer", + ], + }, + "is_default ": Object { + "type": Array [ + "null", + "boolean", + ], + }, + "location_area_encounters": Object { + "type": Array [ + "null", + "string", + ], + }, + "moves": Object { + "items": Object { + "properties": Object { + "move": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "version_group_details": Object { + "items": Object { "properties": Object { - "name": Object { + "level_learned_at": Object { "type": Array [ "null", - "string", + "integer", ], }, - "url": Object { + "move_learn_method": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, "type": Array [ "null", - "string", + "object", ], }, - }, - "type": Array [ - "null", - "object", - ], - }, - "version_group_details": Object { - "items": Object { - "properties": Object { - "level_learned_at": Object { - "type": Array [ - "null", - "integer", - ], - }, - "move_learn_method": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, + "version_group": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], }, - "type": Array [ - "null", - "object", - ], - }, - "version_group": Object { - "properties": Object { - "name": Object { - "type": Array [ - "null", - "string", - ], - }, - "url": Object { - "type": Array [ - "null", - "string", - ], - }, + "url": Object { + "type": Array [ + "null", + "string", + ], }, - "type": Array [ - "null", - "object", - ], }, + "type": Array [ + "null", + "object", + ], }, - "type": Array [ - "null", - "object", - ], }, "type": Array [ "null", - "array", + "object", ], }, + "type": Array [ + "null", + "array", + ], }, - "type": Array [ - "null", - "object", - ], }, "type": Array [ "null", - "array", + "object", ], }, - "name": Object { - "type": Array [ - "null", - "string", - ], + "type": Array [ + "null", + "array", + ], + }, + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "order": Object { + "type": Array [ + "null", + "integer", + ], + }, + "species": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, }, - "order": Object { - "type": Array [ - "null", - "integer", - ], + "type": Array [ + "null", + "object", + ], + }, + "sprites": Object { + "properties": Object { + "back_default": Object { + "type": Array [ + "null", + "string", + ], + }, + "back_female": Object { + "type": Array [ + "null", + "string", + ], + }, + "back_shiny": Object { + "type": Array [ + "null", + "string", + ], + }, + "back_shiny_female": Object { + "type": Array [ + "null", + "string", + ], + }, + "front_default": Object { + "type": Array [ + "null", + "string", + ], + }, + "front_female": Object { + "type": Array [ + "null", + "string", + ], + }, + "front_shiny": Object { + "type": Array [ + "null", + "string", + ], + }, + "front_shiny_female": Object { + "type": Array [ + "null", + "string", + ], + }, }, - "species": Object { + "type": Array [ + "null", + "object", + ], + }, + "stats": Object { + "items": Object { "properties": Object { - "name": Object { + "base_stat": Object { "type": Array [ "null", - "string", + "integer", ], }, - "url": Object { + "effort": Object { "type": Array [ "null", - "string", + "integer", + ], + }, + "stat": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", ], }, }, @@ -3032,54 +996,204 @@ Object { "object", ], }, - "sprites": Object { + "type": Array [ + "null", + "array", + ], + }, + "types": Object { + "items": Object { "properties": Object { - "back_default": Object { + "slot": Object { "type": Array [ "null", - "string", + "integer", ], }, - "back_female": Object { + "type": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, "type": Array [ "null", - "string", + "object", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "weight": Object { + "type": Array [ + "null", + "integer", + ], + }, + }, + "type": "object", + }, + "name": "pokemon", + "sourceDefinedPrimaryKey": Array [], + "supportedSyncModes": Array [ + "full_refresh", + ], + }, + }, + ], + }, + "transformations": Array [], +} +`; + +exports[`#useInitialValues should generate initial values w/ no 'not create' mode 1`] = ` +Object { + "name": "Scrafty <> Heroku Postgres", + "namespaceDefinition": "source", + "namespaceFormat": "\${SOURCE_NAMESPACE}", + "normalization": "basic", + "prefix": "", + "scheduleData": null, + "scheduleType": "manual", + "syncCatalog": Object { + "streams": Array [ + Object { + "config": Object { + "aliasName": "pokemon", + "cursorField": Array [], + "destinationSyncMode": "overwrite", + "primaryKey": Array [], + "selected": true, + "syncMode": "full_refresh", + }, + "id": "0", + "stream": Object { + "defaultCursorField": Array [], + "jsonSchema": Object { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": Object { + "abilities": Object { + "items": Object { + "properties": Object { + "ability": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", ], }, - "back_shiny": Object { + "is_hidden": Object { "type": Array [ "null", - "string", + "boolean", ], }, - "back_shiny_female": Object { + "slot": Object { "type": Array [ "null", - "string", + "integer", ], }, - "front_default": Object { + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "base_experience": Object { + "type": Array [ + "null", + "integer", + ], + }, + "forms": Object { + "items": Object { + "properties": Object { + "name": Object { "type": Array [ "null", "string", ], }, - "front_female": Object { + "url": Object { "type": Array [ "null", "string", ], }, - "front_shiny": Object { + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "game_indices": Object { + "items": Object { + "properties": Object { + "game_index": Object { "type": Array [ "null", - "string", + "integer", ], }, - "front_shiny_female": Object { + "version": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, "type": Array [ "null", - "string", + "object", ], }, }, @@ -3088,33 +1202,67 @@ Object { "object", ], }, - "stats": Object { - "items": Object { - "properties": Object { - "base_stat": Object { - "type": Array [ - "null", - "integer", - ], - }, - "effort": Object { - "type": Array [ - "null", - "integer", - ], + "type": Array [ + "null", + "array", + ], + }, + "height": Object { + "type": Array [ + "null", + "integer", + ], + }, + "held_items": Object { + "items": Object { + "properties": Object { + "item": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, }, - "stat": Object { + "type": Array [ + "null", + "object", + ], + }, + "version_details": Object { + "items": Object { "properties": Object { - "name": Object { + "rarity": Object { "type": Array [ "null", - "string", + "integer", ], }, - "url": Object { + "version": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, "type": Array [ "null", - "string", + "object", ], }, }, @@ -3123,38 +1271,110 @@ Object { "object", ], }, + "type": Array [ + "null", + "array", + ], }, - "type": Array [ - "null", - "object", - ], }, "type": Array [ "null", - "array", + "object", ], }, - "types": Object { - "items": Object { - "properties": Object { - "slot": Object { - "type": Array [ - "null", - "integer", - ], + "type": Array [ + "null", + "array", + ], + }, + "id": Object { + "type": Array [ + "null", + "integer", + ], + }, + "is_default ": Object { + "type": Array [ + "null", + "boolean", + ], + }, + "location_area_encounters": Object { + "type": Array [ + "null", + "string", + ], + }, + "moves": Object { + "items": Object { + "properties": Object { + "move": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, }, - "type": Object { + "type": Array [ + "null", + "object", + ], + }, + "version_group_details": Object { + "items": Object { "properties": Object { - "name": Object { + "level_learned_at": Object { + "type": Array [ + "null", + "integer", + ], + }, + "move_learn_method": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, "type": Array [ "null", - "string", + "object", ], }, - "url": Object { + "version_group": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, "type": Array [ "null", - "string", + "object", ], }, }, @@ -3163,37 +1383,214 @@ Object { "object", ], }, + "type": Array [ + "null", + "array", + ], }, + }, + "type": Array [ + "null", + "object", + ], + }, + "type": Array [ + "null", + "array", + ], + }, + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "order": Object { + "type": Array [ + "null", + "integer", + ], + }, + "species": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + "sprites": Object { + "properties": Object { + "back_default": Object { + "type": Array [ + "null", + "string", + ], + }, + "back_female": Object { + "type": Array [ + "null", + "string", + ], + }, + "back_shiny": Object { + "type": Array [ + "null", + "string", + ], + }, + "back_shiny_female": Object { + "type": Array [ + "null", + "string", + ], + }, + "front_default": Object { + "type": Array [ + "null", + "string", + ], + }, + "front_female": Object { + "type": Array [ + "null", + "string", + ], + }, + "front_shiny": Object { + "type": Array [ + "null", + "string", + ], + }, + "front_shiny_female": Object { "type": Array [ "null", - "object", + "string", ], }, + }, + "type": Array [ + "null", + "object", + ], + }, + "stats": Object { + "items": Object { + "properties": Object { + "base_stat": Object { + "type": Array [ + "null", + "integer", + ], + }, + "effort": Object { + "type": Array [ + "null", + "integer", + ], + }, + "stat": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + }, "type": Array [ "null", - "array", + "object", ], }, - "weight": Object { + "type": Array [ + "null", + "array", + ], + }, + "types": Object { + "items": Object { + "properties": Object { + "slot": Object { + "type": Array [ + "null", + "integer", + ], + }, + "type": Object { + "properties": Object { + "name": Object { + "type": Array [ + "null", + "string", + ], + }, + "url": Object { + "type": Array [ + "null", + "string", + ], + }, + }, + "type": Array [ + "null", + "object", + ], + }, + }, "type": Array [ "null", - "integer", + "object", ], }, + "type": Array [ + "null", + "array", + ], + }, + "weight": Object { + "type": Array [ + "null", + "integer", + ], }, - "type": "object", }, - "name": "pokemon", - "sourceDefinedPrimaryKey": Array [], - "supportedSyncModes": Array [ - "full_refresh", - ], + "type": "object", }, + "name": "pokemon", + "sourceDefinedPrimaryKey": Array [], + "supportedSyncModes": Array [ + "full_refresh", + ], }, - ], - }, - "transformations": Array [], + }, + ], }, - "error": undefined, + "transformations": Array [], } `; diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/formConfig.test.ts b/airbyte-webapp/src/views/Connection/ConnectionForm/formConfig.test.ts index 11333c0839bf..a119935a6553 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/formConfig.test.ts +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/formConfig.test.ts @@ -177,7 +177,8 @@ describe("#useInitialValues", () => { mockDestinationDefinition as DestinationDefinitionSpecificationRead ) ); - expect(result).toMatchSnapshot(); + expect(result.current).toMatchSnapshot(); + expect(result.current.name).toBeDefined(); }); it("should generate initial values w/ 'not create' mode: false", () => { @@ -188,7 +189,8 @@ describe("#useInitialValues", () => { false ) ); - expect(result).toMatchSnapshot(); + expect(result.current).toMatchSnapshot(); + expect(result.current.name).toBeDefined(); }); it("should generate initial values w/ 'not create' mode: true", () => { @@ -199,7 +201,8 @@ describe("#useInitialValues", () => { true ) ); - expect(result).toMatchSnapshot(); + expect(result.current).toMatchSnapshot(); + expect(result.current.name).toBeUndefined(); }); // This is a low-priority test From 6dcbbae2518cda177d44dc922ec3fcc454f11788 Mon Sep 17 00:00:00 2001 From: KC Date: Mon, 3 Oct 2022 16:26:32 -0400 Subject: [PATCH 086/107] connection replication tab tests --- .../ConnectionReplicationTab.test.tsx | 127 ++- .../ConnectionReplicationTab.test.tsx.snap | 890 ++++++++++++++++-- 2 files changed, 924 insertions(+), 93 deletions(-) diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionReplicationTab.test.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionReplicationTab.test.tsx index 18557b1b3250..74027d57e58b 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionReplicationTab.test.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionReplicationTab.test.tsx @@ -1,48 +1,121 @@ -import { render } from "@testing-library/react"; +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ + +import { render, act, RenderResult } from "@testing-library/react"; +import { Suspense } from "react"; +import { QueryClient, QueryClientProvider } from "react-query"; import { MemoryRouter } from "react-router-dom"; -import { mockConnection, TestWrapper } from "test-utils/testutils"; +import mockConnection from "test-utils/mock-data/mockConnection.json"; +import mockDest from "test-utils/mock-data/mockDestinationDefinition.json"; +import { TestWrapper } from "test-utils/testutils"; +import { WebBackendConnectionUpdate } from "core/request/AirbyteClient"; +import { ServicesProvider } from "core/servicesProvider"; +import { ConfirmationModalService } from "hooks/services/ConfirmationModal"; +import { ConnectionEditServiceProvider } from "hooks/services/ConnectionEdit/ConnectionEditService"; import { ModalServiceProvider } from "hooks/services/Modal"; import { ConfigProvider } from "packages/cloud/services/ConfigProvider"; import { AnalyticsProvider } from "views/common/AnalyticsProvider"; import { ConnectionReplicationTab } from "./ConnectionReplicationTab"; -jest.mock("hooks/services/ConnectionEdit/ConnectionEditService", () => { - let schemaHasBeenRefreshed = false; +jest.mock("hooks/services/useConnectionHook", () => { + const useConnHook = jest.requireActual("hooks/services/useConnectionHook"); + let count = 0; return { - useConnectionEditService: () => ({ - connection: mockConnection, - schemaRefreshing: false, - schemaHasBeenRefreshed, - updateConnection: jest.fn(), - setSchemaHasBeenRefreshed: (s: boolean) => (schemaHasBeenRefreshed = s), + ...useConnHook, + useGetConnection: () => mockConnection, + useWebConnectionService: () => ({ + getConnection: (() => { + return () => { + if (count === 0) { + count++; + return Promise.reject("Test Error"); + } + return new Promise(() => null); + }; + })(), + }), + useUpdateConnection: () => ({ + mutateAsync: jest.fn(async (connection: WebBackendConnectionUpdate) => connection), + isLoading: false, }), }; }); -// const { connection, schemaRefreshing, schemaHasBeenRefreshed, updateConnection, setSchemaHasBeenRefreshed } = -// useConnectionEditService(); +jest.mock("services/connector/DestinationDefinitionSpecificationService", () => ({ + useGetDestinationDefinitionSpecification: () => mockDest, +})); +// TODO: This component needs more testing but it should be done VIA e2e and integration-style tests. describe("ConnectionReplicationTab", () => { - const Wrapper: React.FC[0]> = ({ children }) => ( - - - - - {children} - - - - + const Wrapper: React.FC = ({ children }) => ( + I should not show up in a snapshot
}> + + + + + + + + + {children} + + + + + + + + + ); - it("should render", () => { - expect( - render( + it("should render", async () => { + let renderResult: RenderResult; + await act(async () => { + renderResult = render( + + + + ); + }); + expect(renderResult!.container).toMatchSnapshot(); + }); + + it("should show an error if there is a schemaError", async () => { + let renderResult: RenderResult; + await act(async () => { + renderResult = render( - ) - ).toMatchSnapshot(); + ); + }); + + await act(async () => { + renderResult!.queryByText("Refresh source schema")?.click(); + }); + expect(renderResult!.container).toMatchSnapshot(); + }); + + it("should show loading if the schema is refreshing", async () => { + let renderResult: RenderResult; + await act(async () => { + renderResult = render( + + + + ); + }); + + await act(async () => { + renderResult!.queryByText("Refresh source schema")?.click(); + }); + + await act(async () => { + expect( + renderResult!.findByText("We are fetching the schema of your data source.", { exact: false }) + ).toBeTruthy(); + }); }); }); diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/__snapshots__/ConnectionReplicationTab.test.tsx.snap b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/__snapshots__/ConnectionReplicationTab.test.tsx.snap index 234c241799da..c1442c8403c1 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/__snapshots__/ConnectionReplicationTab.test.tsx.snap +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/__snapshots__/ConnectionReplicationTab.test.tsx.snap @@ -1,78 +1,836 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ConnectionReplicationTab should render 1`] = ` -Object { - "asFragment": [Function], - "baseElement": -
+
+
+
+ class="container" + > +
+
+ Transfer +
+
+
+
+ +
+
+
+
+ + +
+
+
+
+
+ Manual +
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+
+ Streams +
+ +
+
+
+ +
+
+
+
+ + +
+
+
+
+
+ Mirror source structure +
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+ Activate the streams you want to sync +
+ +
+
+
+ +
+
+
+
+ +
+
+
+ Sync +
+
+ Source +
+
+
+ + + +
+
+
+
+
+
+ Sync mode +
+
+
+ + + +
+
+
+
+
+ Cursor field +
+
+
+ + + +
+
+
+
+
+ Primary key +
+
+
+ + + +
+
+
+
+
+ Destination +
+
+
+ + + +
+
+
+
+
+
+
+
+ Namespace +
+
+ Stream name +
+
+ Source | Destination +
+
+
+
+ Namespace +
+
+ Stream name +
+
+
+
+
+
+ +
+
+ + + +
+
+
+ +
+
+ + No namespace + +
+
+ pokemon +
+
+
+ + +
+
+
+
+
+ Full refresh +
+
+ | +
+
+ Append +
+
+
+ +
+
+ +
+
+
+
+
+
+
+ <source schema> +
+
+ pokemon +
+
+
+
+
+
+
+
-
- , - "container":
+
+
+ + +
+
+ +
+
+`; + +exports[`ConnectionReplicationTab should show an error if there is a schemaError 1`] = ` +
+
+ class="container" + > +
+ +
+

+ Failed to fetch schema. Please try again +

+ +
-
, - "debug": [Function], - "findAllByAltText": [Function], - "findAllByDisplayValue": [Function], - "findAllByLabelText": [Function], - "findAllByPlaceholderText": [Function], - "findAllByRole": [Function], - "findAllByTestId": [Function], - "findAllByText": [Function], - "findAllByTitle": [Function], - "findByAltText": [Function], - "findByDisplayValue": [Function], - "findByLabelText": [Function], - "findByPlaceholderText": [Function], - "findByRole": [Function], - "findByTestId": [Function], - "findByText": [Function], - "findByTitle": [Function], - "getAllByAltText": [Function], - "getAllByDisplayValue": [Function], - "getAllByLabelText": [Function], - "getAllByPlaceholderText": [Function], - "getAllByRole": [Function], - "getAllByTestId": [Function], - "getAllByText": [Function], - "getAllByTitle": [Function], - "getByAltText": [Function], - "getByDisplayValue": [Function], - "getByLabelText": [Function], - "getByPlaceholderText": [Function], - "getByRole": [Function], - "getByTestId": [Function], - "getByText": [Function], - "getByTitle": [Function], - "queryAllByAltText": [Function], - "queryAllByDisplayValue": [Function], - "queryAllByLabelText": [Function], - "queryAllByPlaceholderText": [Function], - "queryAllByRole": [Function], - "queryAllByTestId": [Function], - "queryAllByText": [Function], - "queryAllByTitle": [Function], - "queryByAltText": [Function], - "queryByDisplayValue": [Function], - "queryByLabelText": [Function], - "queryByPlaceholderText": [Function], - "queryByRole": [Function], - "queryByTestId": [Function], - "queryByText": [Function], - "queryByTitle": [Function], - "rerender": [Function], - "unmount": [Function], -} +
+
`; From 27f790f55ac4d4b4639dae89eb637c79642d5d67 Mon Sep 17 00:00:00 2001 From: KC Date: Mon, 3 Oct 2022 16:52:24 -0400 Subject: [PATCH 087/107] Create Connection Form Tests --- .../CreateConnectionForm.test.tsx | 96 ++ .../CreateConnectionForm.test.tsx.snap | 986 ++++++++++++++++++ 2 files changed, 1082 insertions(+) create mode 100644 airbyte-webapp/src/components/CreateConnection/CreateConnectionForm.test.tsx create mode 100644 airbyte-webapp/src/components/CreateConnection/__snapshots__/CreateConnectionForm.test.tsx.snap diff --git a/airbyte-webapp/src/components/CreateConnection/CreateConnectionForm.test.tsx b/airbyte-webapp/src/components/CreateConnection/CreateConnectionForm.test.tsx new file mode 100644 index 000000000000..6fd5e346287f --- /dev/null +++ b/airbyte-webapp/src/components/CreateConnection/CreateConnectionForm.test.tsx @@ -0,0 +1,96 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { act, render, RenderResult } from "@testing-library/react"; +import { Suspense } from "react"; +import { QueryClient, QueryClientProvider } from "react-query"; +import { MemoryRouter } from "react-router-dom"; +import mockConnection from "test-utils/mock-data/mockConnection.json"; +import mockDest from "test-utils/mock-data/mockDestinationDefinition.json"; +import { TestWrapper } from "test-utils/testutils"; + +import { AirbyteCatalog } from "core/request/AirbyteClient"; +import { ServicesProvider } from "core/servicesProvider"; +import { ConfirmationModalService } from "hooks/services/ConfirmationModal"; +import { FeatureService } from "hooks/services/Feature"; +import * as sourceHook from "hooks/services/useSourceHook"; +import { ConfigProvider } from "packages/cloud/services/ConfigProvider"; +import { AnalyticsProvider } from "views/common/AnalyticsProvider"; + +import { CreateConnectionForm } from "./CreateConnectionForm"; + +jest.mock("services/connector/DestinationDefinitionSpecificationService", () => ({ + useGetDestinationDefinitionSpecification: () => mockDest, +})); + +describe("CreateConnectionForm", () => { + const Wrapper: React.FC = ({ children }) => ( + I should not show up in a snapshot
}> + + + + + + + + {children} + + + + + + + + + ); + + const baseUseDiscoverSchema = { + schemaErrorStatus: null, + isLoading: false, + schema: mockConnection.syncCatalog as AirbyteCatalog, + catalogId: "", + onDiscoverSchema: () => Promise.resolve(), + }; + + it("should render", async () => { + let renderResult: RenderResult; + jest.spyOn(sourceHook, "useDiscoverSchema").mockImplementationOnce(() => baseUseDiscoverSchema); + await act(async () => { + renderResult = render( + + + + ); + }); + expect(renderResult!.container).toMatchSnapshot(); + }); + + it("should render when loading", async () => { + let renderResult: RenderResult; + jest + .spyOn(sourceHook, "useDiscoverSchema") + .mockImplementationOnce(() => ({ ...baseUseDiscoverSchema, isLoading: true })); + await act(async () => { + renderResult = render( + + + + ); + }); + expect(renderResult!.container).toMatchSnapshot(); + }); + + it("should render with an error", async () => { + let renderResult: RenderResult; + jest.spyOn(sourceHook, "useDiscoverSchema").mockImplementationOnce(() => ({ + ...baseUseDiscoverSchema, + schemaErrorStatus: new Error("Test Error") as sourceHook.SchemaError, + })); + await act(async () => { + renderResult = render( + + + + ); + }); + expect(renderResult!.container).toMatchSnapshot(); + }); +}); diff --git a/airbyte-webapp/src/components/CreateConnection/__snapshots__/CreateConnectionForm.test.tsx.snap b/airbyte-webapp/src/components/CreateConnection/__snapshots__/CreateConnectionForm.test.tsx.snap new file mode 100644 index 000000000000..59999a830aca --- /dev/null +++ b/airbyte-webapp/src/components/CreateConnection/__snapshots__/CreateConnectionForm.test.tsx.snap @@ -0,0 +1,986 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CreateConnectionForm should render 1`] = ` +
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+
+ Transfer +
+
+
+
+ +
+
+
+
+ + +
+
+
+
+
+ Every 24 hours +
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+
+ Streams +
+ +
+
+
+ +
+
+
+
+ + +
+
+
+
+
+ Mirror source structure +
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+ Activate the streams you want to sync +
+ +
+
+
+ +
+
+
+
+ +
+
+
+ Sync +
+
+ Source +
+
+
+ + + +
+
+
+
+
+
+ Sync mode +
+
+
+ + + +
+
+
+
+
+ Cursor field +
+
+
+ + + +
+
+
+
+
+ Primary key +
+
+
+ + + +
+
+
+
+
+ Destination +
+
+
+ + + +
+
+
+
+
+
+
+
+ Namespace +
+
+ Stream name +
+
+ Source | Destination +
+
+
+
+ Namespace +
+
+ Stream name +
+
+
+
+
+
+ +
+
+ + + +
+
+
+ +
+
+ + No namespace + +
+
+ pokemon +
+
+
+ + +
+
+
+
+
+ Full refresh +
+
+ | +
+
+ Overwrite +
+
+
+ +
+
+ +
+
+
+
+
+
+
+ <source schema> +
+
+ pokemon +
+
+
+
+
+
+
+
+
+
+
+
+
+ Normalization +
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+`; + +exports[`CreateConnectionForm should render when loading 1`] = ` +
+
+
+
+
+ Please wait a little bit more… +
+
+
+ We are fetching the schema of your data source. +This should take less than a minute, but may take a few minutes on slow internet connections or data sources with a large amount of tables. +
+
+
+`; + +exports[`CreateConnectionForm should render with an error 1`] = ` +
+
+
+
+ +
+

+ Failed to fetch schema. Please try again +

+ +
+
+
+`; From e791029bf8fac74e0bbb7c9db33b14ab42d01b26 Mon Sep 17 00:00:00 2001 From: KC Date: Mon, 3 Oct 2022 17:08:34 -0400 Subject: [PATCH 088/107] Better jest mocking! --- .../ConnectionReplicationTab.test.tsx | 46 +++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionReplicationTab.test.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionReplicationTab.test.tsx index 74027d57e58b..cb57584c77ea 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionReplicationTab.test.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionReplicationTab.test.tsx @@ -14,35 +14,12 @@ import { ServicesProvider } from "core/servicesProvider"; import { ConfirmationModalService } from "hooks/services/ConfirmationModal"; import { ConnectionEditServiceProvider } from "hooks/services/ConnectionEdit/ConnectionEditService"; import { ModalServiceProvider } from "hooks/services/Modal"; +import * as connectionHook from "hooks/services/useConnectionHook"; import { ConfigProvider } from "packages/cloud/services/ConfigProvider"; import { AnalyticsProvider } from "views/common/AnalyticsProvider"; import { ConnectionReplicationTab } from "./ConnectionReplicationTab"; -jest.mock("hooks/services/useConnectionHook", () => { - const useConnHook = jest.requireActual("hooks/services/useConnectionHook"); - let count = 0; - return { - ...useConnHook, - useGetConnection: () => mockConnection, - useWebConnectionService: () => ({ - getConnection: (() => { - return () => { - if (count === 0) { - count++; - return Promise.reject("Test Error"); - } - return new Promise(() => null); - }; - })(), - }), - useUpdateConnection: () => ({ - mutateAsync: jest.fn(async (connection: WebBackendConnectionUpdate) => connection), - isLoading: false, - }), - }; -}); - jest.mock("services/connector/DestinationDefinitionSpecificationService", () => ({ useGetDestinationDefinitionSpecification: () => mockDest, })); @@ -70,7 +47,24 @@ describe("ConnectionReplicationTab", () => { ); + + const setupSpies = (getConnection?: () => Promise) => { + const getConnectionImpl: any = { + getConnection: getConnection ?? (() => new Promise(() => null) as any), + }; + jest.spyOn(connectionHook, "useGetConnection").mockImplementation(() => mockConnection as any); + jest.spyOn(connectionHook, "useWebConnectionService").mockImplementation(() => getConnectionImpl); + jest.spyOn(connectionHook, "useUpdateConnection").mockImplementation( + () => + ({ + mutateAsync: async (connection: WebBackendConnectionUpdate) => connection, + isLoading: false, + } as any) + ); + }; it("should render", async () => { + setupSpies(); + let renderResult: RenderResult; await act(async () => { renderResult = render( @@ -83,6 +77,8 @@ describe("ConnectionReplicationTab", () => { }); it("should show an error if there is a schemaError", async () => { + setupSpies(() => Promise.reject("Test Error")); + let renderResult: RenderResult; await act(async () => { renderResult = render( @@ -99,6 +95,8 @@ describe("ConnectionReplicationTab", () => { }); it("should show loading if the schema is refreshing", async () => { + setupSpies(); + let renderResult: RenderResult; await act(async () => { renderResult = render( From 4dc6be43fc9acf545e5a3726ba3b60b19c08d35e Mon Sep 17 00:00:00 2001 From: KC Date: Tue, 4 Oct 2022 09:39:46 -0400 Subject: [PATCH 089/107] Fixing issue setting schedule type to manual --- .../services/ConnectionForm/ConnectionFormService.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx b/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx index a6bc741067c4..af8ced5e41eb 100644 --- a/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx +++ b/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.tsx @@ -1,7 +1,7 @@ import React, { createContext, useCallback, useContext, useState } from "react"; import { useIntl } from "react-intl"; -import { OperationRead, WebBackendConnectionRead } from "core/request/AirbyteClient"; +import { ConnectionScheduleType, OperationRead, WebBackendConnectionRead } from "core/request/AirbyteClient"; import { useGetDestinationDefinitionSpecification } from "services/connector/DestinationDefinitionSpecificationService"; import { FormError, generateMessageFromError } from "utils/errorStatusMessage"; import { @@ -41,6 +41,11 @@ export const tidyConnectionFormValues = ( formValues.operations = mapFormPropsToOperation(values, operations, workspaceId); + if (formValues.scheduleType === ConnectionScheduleType.manual) { + // Have to set this to undefined to override the existing scheduleData + formValues.scheduleData = undefined; + } + return formValues; }; From 4e3b9f7c4662726119e3fd007348fcd791f807ca Mon Sep 17 00:00:00 2001 From: KC Date: Tue, 4 Oct 2022 10:11:40 -0400 Subject: [PATCH 090/107] Tests fixed, and test-utils is better! --- .../CreateConnectionForm.test.tsx | 36 +++++-------------- .../CreateConnectionForm.test.tsx.snap | 22 +++++++++++- .../ConnectionEditService.test.tsx | 8 +---- .../ConnectionFormService.test.tsx | 8 +---- .../ConnectionReplicationTab.test.tsx | 25 ++----------- airbyte-webapp/src/test-utils/testutils.tsx | 34 +++++++++--------- 6 files changed, 52 insertions(+), 81 deletions(-) diff --git a/airbyte-webapp/src/components/CreateConnection/CreateConnectionForm.test.tsx b/airbyte-webapp/src/components/CreateConnection/CreateConnectionForm.test.tsx index 6fd5e346287f..0508f29375ea 100644 --- a/airbyte-webapp/src/components/CreateConnection/CreateConnectionForm.test.tsx +++ b/airbyte-webapp/src/components/CreateConnection/CreateConnectionForm.test.tsx @@ -1,19 +1,11 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { act, render, RenderResult } from "@testing-library/react"; -import { Suspense } from "react"; -import { QueryClient, QueryClientProvider } from "react-query"; -import { MemoryRouter } from "react-router-dom"; import mockConnection from "test-utils/mock-data/mockConnection.json"; import mockDest from "test-utils/mock-data/mockDestinationDefinition.json"; import { TestWrapper } from "test-utils/testutils"; import { AirbyteCatalog } from "core/request/AirbyteClient"; -import { ServicesProvider } from "core/servicesProvider"; -import { ConfirmationModalService } from "hooks/services/ConfirmationModal"; -import { FeatureService } from "hooks/services/Feature"; import * as sourceHook from "hooks/services/useSourceHook"; -import { ConfigProvider } from "packages/cloud/services/ConfigProvider"; -import { AnalyticsProvider } from "views/common/AnalyticsProvider"; import { CreateConnectionForm } from "./CreateConnectionForm"; @@ -21,26 +13,13 @@ jest.mock("services/connector/DestinationDefinitionSpecificationService", () => useGetDestinationDefinitionSpecification: () => mockDest, })); +jest.mock("services/workspaces/WorkspacesService", () => ({ + useCurrentWorkspace: () => ({}), + useCurrentWorkspaceId: () => "workspace-id", +})); + describe("CreateConnectionForm", () => { - const Wrapper: React.FC = ({ children }) => ( - I should not show up in a snapshot
}> - - - - - - - - {children} - - - - - - - - - ); + const Wrapper: React.FC = ({ children }) => {children}; const baseUseDiscoverSchema = { schemaErrorStatus: null, @@ -52,7 +31,7 @@ describe("CreateConnectionForm", () => { it("should render", async () => { let renderResult: RenderResult; - jest.spyOn(sourceHook, "useDiscoverSchema").mockImplementationOnce(() => baseUseDiscoverSchema); + jest.spyOn(sourceHook, "useDiscoverSchema").mockImplementation(() => baseUseDiscoverSchema); await act(async () => { renderResult = render( @@ -61,6 +40,7 @@ describe("CreateConnectionForm", () => { ); }); expect(renderResult!.container).toMatchSnapshot(); + expect(renderResult!.queryByText("Please wait a little bit more…")).toBeFalsy(); }); it("should render when loading", async () => { diff --git a/airbyte-webapp/src/components/CreateConnection/__snapshots__/CreateConnectionForm.test.tsx.snap b/airbyte-webapp/src/components/CreateConnection/__snapshots__/CreateConnectionForm.test.tsx.snap index 59999a830aca..a4678e8d53c9 100644 --- a/airbyte-webapp/src/components/CreateConnection/__snapshots__/CreateConnectionForm.test.tsx.snap +++ b/airbyte-webapp/src/components/CreateConnection/__snapshots__/CreateConnectionForm.test.tsx.snap @@ -815,7 +815,7 @@ exports[`CreateConnectionForm should render 1`] = `
- Normalization + Normalization & Transformation
+
+
+ No custom transformation + +
+
diff --git a/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.test.tsx b/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.test.tsx index 24725caa6813..89990d290fad 100644 --- a/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.test.tsx +++ b/airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.test.tsx @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { act, renderHook } from "@testing-library/react-hooks"; import React from "react"; -import { MemoryRouter } from "react-router-dom"; import mockConnection from "test-utils/mock-data/mockConnection.json"; import mockDest from "test-utils/mock-data/mockDestinationDefinition.json"; import { TestWrapper } from "test-utils/testutils"; @@ -9,7 +8,6 @@ import { TestWrapper } from "test-utils/testutils"; import { WebBackendConnectionUpdate } from "core/request/AirbyteClient"; import { useConnectionFormService } from "../ConnectionForm/ConnectionFormService"; -import { ModalServiceProvider } from "../Modal"; import { ConnectionEditServiceProvider, useConnectionEditService } from "./ConnectionEditService"; jest.mock("services/connector/DestinationDefinitionSpecificationService", () => ({ @@ -30,11 +28,7 @@ jest.mock("../useConnectionHook", () => ({ describe("ConnectionFormService", () => { const Wrapper: React.FC[0]> = ({ children, ...props }) => ( - - - {children} - - + {children} ); diff --git a/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.test.tsx b/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.test.tsx index 53f3286c194a..80c24e14dda0 100644 --- a/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.test.tsx +++ b/airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.test.tsx @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { act, renderHook } from "@testing-library/react-hooks"; import React from "react"; -import { MemoryRouter } from "react-router-dom"; import mockConnection from "test-utils/mock-data/mockConnection.json"; import mockDest from "test-utils/mock-data/mockDestinationDefinition.json"; import { TestWrapper } from "test-utils/testutils"; @@ -9,7 +8,6 @@ import { TestWrapper } from "test-utils/testutils"; import { AirbyteCatalog, WebBackendConnectionRead } from "core/request/AirbyteClient"; import { FormError } from "utils/errorStatusMessage"; -import { ModalServiceProvider } from "../Modal"; import { ConnectionFormServiceProvider, ConnectionOrPartialConnection, @@ -23,11 +21,7 @@ jest.mock("services/connector/DestinationDefinitionSpecificationService", () => describe("ConnectionFormService", () => { const Wrapper: React.FC[0]> = ({ children, ...props }) => ( - - - {children} - - + {children} ); diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionReplicationTab.test.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionReplicationTab.test.tsx index cb57584c77ea..dc24970d9990 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionReplicationTab.test.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionReplicationTab.test.tsx @@ -3,20 +3,13 @@ import { render, act, RenderResult } from "@testing-library/react"; import { Suspense } from "react"; -import { QueryClient, QueryClientProvider } from "react-query"; -import { MemoryRouter } from "react-router-dom"; import mockConnection from "test-utils/mock-data/mockConnection.json"; import mockDest from "test-utils/mock-data/mockDestinationDefinition.json"; import { TestWrapper } from "test-utils/testutils"; import { WebBackendConnectionUpdate } from "core/request/AirbyteClient"; -import { ServicesProvider } from "core/servicesProvider"; -import { ConfirmationModalService } from "hooks/services/ConfirmationModal"; import { ConnectionEditServiceProvider } from "hooks/services/ConnectionEdit/ConnectionEditService"; -import { ModalServiceProvider } from "hooks/services/Modal"; import * as connectionHook from "hooks/services/useConnectionHook"; -import { ConfigProvider } from "packages/cloud/services/ConfigProvider"; -import { AnalyticsProvider } from "views/common/AnalyticsProvider"; import { ConnectionReplicationTab } from "./ConnectionReplicationTab"; @@ -29,21 +22,9 @@ describe("ConnectionReplicationTab", () => { const Wrapper: React.FC = ({ children }) => ( I should not show up in a snapshot
}> - - - - - - - - {children} - - - - - - - + + {children} + ); diff --git a/airbyte-webapp/src/test-utils/testutils.tsx b/airbyte-webapp/src/test-utils/testutils.tsx index f330e0cb569b..efabdb6918c9 100644 --- a/airbyte-webapp/src/test-utils/testutils.tsx +++ b/airbyte-webapp/src/test-utils/testutils.tsx @@ -14,7 +14,9 @@ import { WebBackendConnectionRead, } from "core/request/AirbyteClient"; import { ServicesProvider } from "core/servicesProvider"; +import { ConfirmationModalService } from "hooks/services/ConfirmationModal"; import { defaultFeatures, FeatureService } from "hooks/services/Feature"; +import { ModalServiceProvider } from "hooks/services/Modal"; import en from "locales/en.json"; import { AnalyticsProvider } from "views/common/AnalyticsProvider"; @@ -27,23 +29,9 @@ export async function render< Container extends Element | DocumentFragment = HTMLElement >(ui: React.ReactNode, renderOptions?: RenderOptions): Promise> { const Wrapper = ({ children }: WrapperProps) => { - const queryClient = new QueryClient(); - return ( - - - - - - - 'fallback content'
}>{children} - - - - - - + testutils render fallback content
}>{children} ); }; @@ -59,7 +47,21 @@ export async function render< export const TestWrapper: React.FC> = ({ children }) => ( null}> - {children} + + + + + + + + {children} + + + + + + + ); From f80e8e6610dee2944cc31e724931b550d361a2de Mon Sep 17 00:00:00 2001 From: KC Date: Tue, 4 Oct 2022 12:27:57 -0400 Subject: [PATCH 091/107] Changing back to Once --- .../components/CreateConnection/CreateConnectionForm.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airbyte-webapp/src/components/CreateConnection/CreateConnectionForm.test.tsx b/airbyte-webapp/src/components/CreateConnection/CreateConnectionForm.test.tsx index 0508f29375ea..8c47adbffb03 100644 --- a/airbyte-webapp/src/components/CreateConnection/CreateConnectionForm.test.tsx +++ b/airbyte-webapp/src/components/CreateConnection/CreateConnectionForm.test.tsx @@ -31,7 +31,7 @@ describe("CreateConnectionForm", () => { it("should render", async () => { let renderResult: RenderResult; - jest.spyOn(sourceHook, "useDiscoverSchema").mockImplementation(() => baseUseDiscoverSchema); + jest.spyOn(sourceHook, "useDiscoverSchema").mockImplementationOnce(() => baseUseDiscoverSchema); await act(async () => { renderResult = render( From 0b33c07e8897dae8cea752366694b9fe22c5eff9 Mon Sep 17 00:00:00 2001 From: KC Date: Wed, 5 Oct 2022 10:35:19 -0400 Subject: [PATCH 092/107] Fixing build from master merge --- .../src/components/CreateConnection/SchemaError.tsx | 2 +- .../pages/ConnectionItemPage/ResetWarningModal.tsx | 3 ++- .../views/Connection/ConnectionForm/ConnectionFormFields.tsx | 4 +++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/airbyte-webapp/src/components/CreateConnection/SchemaError.tsx b/airbyte-webapp/src/components/CreateConnection/SchemaError.tsx index 5cf7cb2673c4..bfa7fc2d00ee 100644 --- a/airbyte-webapp/src/components/CreateConnection/SchemaError.tsx +++ b/airbyte-webapp/src/components/CreateConnection/SchemaError.tsx @@ -1,5 +1,5 @@ import { JobItem } from "components/JobItem/JobItem"; -import { Card } from "components/ui"; +import { Card } from "components/ui/Card"; import { LogsRequestError } from "core/request/LogsRequestError"; import { useConnectionFormService } from "hooks/services/ConnectionForm/ConnectionFormService"; diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ResetWarningModal.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ResetWarningModal.tsx index c0401e70439a..7d3fac8cab85 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ResetWarningModal.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ResetWarningModal.tsx @@ -2,7 +2,8 @@ import { useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useUnmount } from "react-use"; -import { Button, LabeledSwitch } from "components"; +import { LabeledSwitch } from "components"; +import { Button } from "components/ui/Button"; import { ModalBody, ModalFooter } from "components/ui/Modal"; import { ConnectionStateType } from "core/request/AirbyteClient"; diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.tsx index 50a1bf6d6045..c026624eb2d5 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionFormFields.tsx @@ -5,7 +5,9 @@ import { Field, FieldProps } from "formik"; import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; -import { Button, ControlLabels, Input } from "components"; +import { ControlLabels } from "components"; +import { Button } from "components/ui/Button"; +import { Input } from "components/ui/Input"; import { Text } from "components/ui/Text"; import { NamespaceDefinitionType } from "core/request/AirbyteClient"; From f9432bdc72e3da635258c2d2a71607239b366c33 Mon Sep 17 00:00:00 2001 From: KC Date: Wed, 5 Oct 2022 11:01:55 -0400 Subject: [PATCH 093/107] Updating snapshots. If these change a lot it may be worth altering the tests to avoid snapshots. --- .../CreateConnectionForm.test.tsx.snap | 168 +++++++++--------- .../ConnectionReplicationTab.test.tsx.snap | 122 ++++++------- 2 files changed, 145 insertions(+), 145 deletions(-) diff --git a/airbyte-webapp/src/components/CreateConnection/__snapshots__/CreateConnectionForm.test.tsx.snap b/airbyte-webapp/src/components/CreateConnection/__snapshots__/CreateConnectionForm.test.tsx.snap index a4678e8d53c9..62b06b97a584 100644 --- a/airbyte-webapp/src/components/CreateConnection/__snapshots__/CreateConnectionForm.test.tsx.snap +++ b/airbyte-webapp/src/components/CreateConnection/__snapshots__/CreateConnectionForm.test.tsx.snap @@ -24,7 +24,7 @@ exports[`CreateConnectionForm should render 1`] = ` class="controlContainer connectionLabel" >