From 78c9d6b83700b1aa213e42e2f0badc66836c0d3e Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Tue, 12 Jul 2022 15:41:13 +0200 Subject: [PATCH 01/20] [WIP] new per stream flow --- .../ConfirmationModal/ConfirmationModal.tsx | 20 ++- .../domain/connection/ConnectionService.ts | 6 +- .../ConfirmationModalService.tsx | 2 + .../src/hooks/services/useConnectionHook.tsx | 2 +- airbyte-webapp/src/locales/en.json | 5 +- .../components/ReplicationView.tsx | 126 +++++++++--------- .../ConnectionForm/ConnectionForm.tsx | 64 ++++----- .../components/EditControls.tsx | 25 +--- 8 files changed, 120 insertions(+), 130 deletions(-) diff --git a/airbyte-webapp/src/components/ConfirmationModal/ConfirmationModal.tsx b/airbyte-webapp/src/components/ConfirmationModal/ConfirmationModal.tsx index ab48e88fc16c..48d0309a9da3 100644 --- a/airbyte-webapp/src/components/ConfirmationModal/ConfirmationModal.tsx +++ b/airbyte-webapp/src/components/ConfirmationModal/ConfirmationModal.tsx @@ -31,7 +31,9 @@ export interface ConfirmationModalProps { title: string; text: string; submitButtonText: string; - onSubmit: () => void; + secondaryButtonText?: string; + secondaryButtonDataId?: string; + onSubmit: (button: "primary" | "secondary") => void; submitButtonDataId?: string; cancelButtonText?: string; } @@ -43,10 +45,13 @@ export const ConfirmationModal: React.FC = ({ onSubmit, submitButtonText, submitButtonDataId, + secondaryButtonText, + secondaryButtonDataId, cancelButtonText, }) => { const { isLoading, startAction } = useLoadingState(); - const onSubmitBtnClick = () => startAction({ action: () => onSubmit() }); + const onSubmitBtnClick = () => startAction({ action: () => onSubmit("primary") }); + const onSecondaryBtnClick = () => startAction({ action: () => onSubmit("secondary") }); return ( }> @@ -56,6 +61,17 @@ export const ConfirmationModal: React.FC = ({ + {secondaryButtonText && ( + + + + )} diff --git a/airbyte-webapp/src/core/domain/connection/ConnectionService.ts b/airbyte-webapp/src/core/domain/connection/ConnectionService.ts index a4616be27e56..5a3ae970e69f 100644 --- a/airbyte-webapp/src/core/domain/connection/ConnectionService.ts +++ b/airbyte-webapp/src/core/domain/connection/ConnectionService.ts @@ -1,4 +1,4 @@ -import { deleteConnection, resetConnection, syncConnection, getState } from "../../request/AirbyteClient"; +import { deleteConnection, resetConnection, syncConnection, getState, getStateType } from "../../request/AirbyteClient"; import { AirbyteRequestService } from "../../request/AirbyteRequestService"; export class ConnectionService extends AirbyteRequestService { @@ -17,4 +17,8 @@ export class ConnectionService extends AirbyteRequestService { public getState(connectionId: string) { return getState({ connectionId }, this.requestOptions); } + + public getStateType(connectionId: string) { + return getStateType({ connectionId }, this.requestOptions); + } } diff --git a/airbyte-webapp/src/hooks/services/ConfirmationModal/ConfirmationModalService.tsx b/airbyte-webapp/src/hooks/services/ConfirmationModal/ConfirmationModalService.tsx index 52b3a50a9b01..c810d0f34afb 100644 --- a/airbyte-webapp/src/hooks/services/ConfirmationModal/ConfirmationModalService.tsx +++ b/airbyte-webapp/src/hooks/services/ConfirmationModal/ConfirmationModalService.tsx @@ -66,6 +66,8 @@ export const ConfirmationModalService = ({ children }: { children: React.ReactNo submitButtonText={state.confirmationModal.submitButtonText} submitButtonDataId={state.confirmationModal.submitButtonDataId} cancelButtonText={state.confirmationModal.cancelButtonText} + secondaryButtonText={state.confirmationModal.secondaryButtonText} + secondaryButtonDataId={state.confirmationModal.secondaryButtonDataId} /> ) : null} diff --git a/airbyte-webapp/src/hooks/services/useConnectionHook.tsx b/airbyte-webapp/src/hooks/services/useConnectionHook.tsx index 749a4fb98774..a925f10f650f 100644 --- a/airbyte-webapp/src/hooks/services/useConnectionHook.tsx +++ b/airbyte-webapp/src/hooks/services/useConnectionHook.tsx @@ -63,7 +63,7 @@ function useWebConnectionService() { ); } -function useConnectionService() { +export function useConnectionService() { const config = useConfig(); const middlewares = useDefaultRequestMiddlewares(); return useInitService(() => new ConnectionService(config.apiUrl, middlewares), [config.apiUrl, middlewares]); diff --git a/airbyte-webapp/src/locales/en.json b/airbyte-webapp/src/locales/en.json index 15470e7f5381..ef2c9f58abc2 100644 --- a/airbyte-webapp/src/locales/en.json +++ b/airbyte-webapp/src/locales/en.json @@ -106,8 +106,6 @@ "form.destinationConnector": "Destination connector", "form.addItems": "Add", "form.items": "Items", - "form.toSaveSchema": "To save the new schema, click on Save changes.", - "form.noteStartSync": "Note that it will delete all the data in your destination and start syncing from scratch. ", "form.pkSelected": "{count, plural, =0 { } one {{items}} other {# keys selected}}", "form.url.error": "field must be a valid URL", "form.setupGuide": "Setup Guide", @@ -317,7 +315,7 @@ "syncMode.full_refresh": "Full refresh", "syncMode.incremental": "Incremental", - "connection.warningUpdateSchema": "WARNING! Updating the schema will delete all the data for this connection in your destination and start syncing from scratch.", + "connection.updateSchemaWithoutReset": "Save without reset", "connection.title": "Connection", "connection.description": "Connections link Sources to Destinations.", "connection.fromTo": "{source} → {destination}", @@ -326,7 +324,6 @@ "connection.testsFailed": "The connection tests failed.", "connection.destinationCheckSettings": "Check destination settings", "connection.sourceCheckSettings": "Check source settings", - "connection.saveAndReset": "Save changes & reset data", "connection.destinationTestAgain": "Test destination connection again", "connection.sourceTestAgain": "Test source connection again", "connection.resetData": "Reset your data", 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 6eb75031cc3f..13769d794d54 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,5 @@ import { faSyncAlt } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { FormikHelpers } from "formik"; import React, { useMemo, useState } from "react"; import { FormattedMessage } from "react-intl"; import { useAsyncFn } from "react-use"; @@ -10,11 +9,11 @@ import { Button, Card } from "components"; import LoadingSchema from "components/LoadingSchema"; import { toWebBackendConnectionUpdate } from "core/domain/connection"; -import { ConnectionStatus } from "core/request/AirbyteClient"; +import { ConnectionStateType, ConnectionStatus } from "core/request/AirbyteClient"; import { useConfirmationModalService } from "hooks/services/ConfirmationModal"; import { useConnectionLoad, - useResetConnection, + useConnectionService, useUpdateConnection, ValuesProps, } from "hooks/services/useConnectionHook"; @@ -38,27 +37,16 @@ const TryArrow = styled(FontAwesomeIcon)` font-size: 14px; `; -const Message = styled.div` - font-weight: 500; - font-size: 12px; - line-height: 15px; - color: ${({ theme }) => theme.greyColor40}; -`; - -const Note = styled.span` - color: ${({ theme }) => theme.dangerColor}; -`; - export const ReplicationView: React.FC = ({ onAfterSaveSchema, connectionId }) => { const { openConfirmationModal, closeConfirmationModal } = useConfirmationModalService(); const [activeUpdatingSchemaMode, setActiveUpdatingSchemaMode] = useState(false); const [saved, setSaved] = useState(false); const [connectionFormValues, setConnectionFormValues] = useState(); + const connectionService = useConnectionService(); - const { mutateAsync: updateConnection } = useUpdateConnection(); - const { mutateAsync: resetConnection } = useResetConnection(); + console.log("saved", saved); - const onReset = () => resetConnection(connectionId); + const { mutateAsync: updateConnection } = useUpdateConnection(); const { connection: initialConnection, refreshConnectionCatalog } = useConnectionLoad(connectionId); @@ -86,15 +74,12 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch return initialConnection; }, [activeUpdatingSchemaMode, connectionWithRefreshCatalog, initialConnection, connectionFormValues]); - const onSubmit = async (values: ValuesProps, formikHelpers?: FormikHelpers) => { - if (!connection) { - // onSubmit should only be called when a connection object exists. - return; - } - + const saveConnection = async (values: ValuesProps, skipReset = false) => { const initialSyncSchema = connection.syncCatalog; const connectionAsUpdate = toWebBackendConnectionUpdate(connection); + console.log("skipReset", skipReset); + await updateConnection({ ...connectionAsUpdate, ...values, @@ -103,7 +88,8 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch // The status can be toggled and the name can be changed in-between refreshing the schema name: initialConnection.name, status: initialConnection.status || "", - withRefreshedCatalog: activeUpdatingSchemaMode, + // TODO: skipRefresh: true/false + // skipReset, }); setSaved(true); @@ -114,59 +100,65 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch if (activeUpdatingSchemaMode) { setActiveUpdatingSchemaMode(false); } - - formikHelpers?.resetForm({ values }); - }; - - const openResetDataModal = (values: ValuesProps) => { - openConfirmationModal({ - title: "connection.updateSchema", - text: "connection.updateSchemaText", - submitButtonText: "connection.updateSchema", - submitButtonDataId: "refresh", - onSubmit: async () => { - await onSubmit(values); - closeConfirmationModal(); - }, - }); }; const onSubmitForm = async (values: ValuesProps) => { - if (activeUpdatingSchemaMode) { - openResetDataModal(values); + console.log("values.syncCatalog", values.syncCatalog); + console.log("initialConnection", initialConnection.syncCatalog); + const hasCatalogChanged = !equal(values.syncCatalog, initialConnection.syncCatalog); + + console.log("hasCatalogChanged?", hasCatalogChanged); + if (hasCatalogChanged) { + const stateType = await connectionService.getStateType(connectionId); + console.log("ConnectionStateType", stateType); + if (stateType === ConnectionStateType.legacy) { + // The state type is legacy so the server will do a full reset after saving + // TODO: Show confirm dialog with full reset option + openConfirmationModal({ + title: "connection.updateSchema", + text: "connection.updateSchemaText", + submitButtonText: "connection.updateSchema", + submitButtonDataId: "refresh", + secondaryButtonText: "connection.updateSchemaWithoutReset", + onSubmit: async (type) => { + await saveConnection(values, type === "secondary"); + closeConfirmationModal(); + }, + }); + } else { + // TODO: Show confirm dialog with partial reset option + openConfirmationModal({ + title: "connection.updateSchema", + text: "connection.updateSchemaText", + submitButtonText: "connection.updateSchema", + submitButtonDataId: "refresh", + secondaryButtonText: "connection.updateSchemaWithoutReset", + onSubmit: async (type) => { + await saveConnection(values, type === "secondary"); + closeConfirmationModal(); + }, + }); + } + // const skipRefresh = true; + // await saveConnection(values, skipRefresh); } else { - await onSubmit(values); + // The catalog hasn't changed. We don't need to ask for any confirmation and can simply save + // TODO: Clarify if we want to have `skipRefresh` true or false in this case with BE. + await saveConnection(values, true); } }; - const onEnterRefreshCatalogMode = async () => { + const onRefreshSourceSchema = async () => { + setSaved(false); setActiveUpdatingSchemaMode(true); await refreshCatalog(); }; const onCancelConnectionFormEdit = () => { + setSaved(false); setActiveUpdatingSchemaMode(false); }; - const renderUpdateSchemaButton = () => { - if (!activeUpdatingSchemaMode) { - return ( - - ); - } - return ( - - {" "} - - - - - ); - }; - return ( @@ -175,11 +167,15 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch mode={connection?.status !== ConnectionStatus.deprecated ? "edit" : "readonly"} connection={connection} onSubmit={onSubmitForm} - onReset={onReset} successMessage={saved && } onCancel={onCancelConnectionFormEdit} - editSchemeMode={activeUpdatingSchemaMode} - additionalSchemaControl={renderUpdateSchemaButton()} + allowSavingUntouchedForm={activeUpdatingSchemaMode} + additionalSchemaControl={ + + } onChangeValues={setConnectionFormValues} /> ) : ( diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx index 0c11955e3599..ca2eee9cfb07 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx @@ -9,12 +9,10 @@ import { ControlLabels, DropDown, DropDownRow, H5, Input, Label } from "componen import { FormChangeTracker } from "components/FormChangeTracker"; import { ConnectionSchedule, NamespaceDefinitionType, WebBackendConnectionRead } from "core/request/AirbyteClient"; -import { useConfirmationModalService } from "hooks/services/ConfirmationModal"; import { useFormChangeTrackerService, useUniqueFormId } from "hooks/services/FormChangeTracker"; import { useGetDestinationDefinitionSpecification } from "services/connector/DestinationDefinitionSpecificationService"; import { useCurrentWorkspace } from "services/workspaces/WorkspacesService"; import { createFormErrorMessage } from "utils/errorStatusMessage"; -import { equal } from "utils/objects"; import CreateControls from "./components/CreateControls"; import EditControls from "./components/EditControls"; @@ -97,7 +95,10 @@ export type ConnectionFormMode = "create" | "edit" | "readonly"; // eslint-disable-next-line react/function-component-definition function FormValuesChangeTracker({ onChangeValues }: { onChangeValues?: (values: T) => void }) { // Grab values from context - const { values } = useFormikContext(); + const { values, errors, dirty } = useFormikContext(); + console.log("values", values); + console.log("errors", errors); + console.log("dirty", dirty); useDebounce( () => { onChangeValues?.(values); @@ -113,13 +114,12 @@ interface ConnectionFormProps { className?: string; additionBottomControls?: React.ReactNode; successMessage?: React.ReactNode; - onReset?: (connectionId?: string) => void; onDropDownSelect?: (item: DropDownRow.IDataItem) => void; onCancel?: () => void; onChangeValues?: (values: FormikConnectionFormValues) => void; /** Should be passed when connection is updated with withRefreshCatalog flag */ - editSchemeMode?: boolean; + allowSavingUntouchedForm?: boolean; mode: ConnectionFormMode; additionalSchemaControl?: React.ReactNode; @@ -130,19 +130,18 @@ interface ConnectionFormProps { const ConnectionForm: React.FC = ({ onSubmit, - onReset, onCancel, className, onDropDownSelect, mode, successMessage, additionBottomControls, - editSchemeMode, + allowSavingUntouchedForm, additionalSchemaControl, connection, onChangeValues, }) => { - const { openConfirmationModal, closeConfirmationModal } = useConfirmationModalService(); + // const { openConfirmationModal, closeConfirmationModal } = useConfirmationModalService(); const destDefinition = useGetDestinationDefinitionSpecification(connection.destination.destinationDefinitionId); const { clearFormChange } = useFormChangeTrackerService(); const formId = useUniqueFormId(); @@ -155,18 +154,19 @@ const ConnectionForm: React.FC = ({ const initialValues = useInitialValues(connection, destDefinition, isEditMode); const workspace = useCurrentWorkspace(); - const openResetDataModal = useCallback(() => { - openConfirmationModal({ - title: "form.resetData", - text: "form.changedColumns", - submitButtonText: "form.reset", - cancelButtonText: "form.noNeed", - onSubmit: async () => { - await onReset?.(); - closeConfirmationModal(); - }, - }); - }, [closeConfirmationModal, onReset, openConfirmationModal]); + // TODO: Remove this + // const openResetDataModal = useCallback(() => { + // openConfirmationModal({ + // title: "form.resetData", + // text: "form.changedColumns", + // submitButtonText: "form.reset", + // cancelButtonText: "form.noNeed", + // onSubmit: async () => { + // await onReset?.(); + // closeConfirmationModal(); + // }, + // }); + // }, [closeConfirmationModal, onReset, openConfirmationModal]); const onFormSubmit = useCallback( async (values: FormikConnectionFormValues, formikHelpers: FormikHelpers) => { @@ -183,29 +183,19 @@ const ConnectionForm: React.FC = ({ formikHelpers.resetForm({ values }); clearFormChange(formId); - const requiresReset = - mode === "edit" && !equal(initialValues.syncCatalog, values.syncCatalog) && !editSchemeMode; + // // TODO: Remove this + // const requiresReset = mode === "edit" && !equal(initialValues.syncCatalog, values.syncCatalog); - if (requiresReset) { - openResetDataModal(); - } + // if (requiresReset) { + // openResetDataModal(); + // } result?.onSubmitComplete?.(); } catch (e) { setSubmitError(e); } }, - [ - connection.operations, - workspace.workspaceId, - onSubmit, - clearFormChange, - formId, - mode, - initialValues.syncCatalog, - editSchemeMode, - openResetDataModal, - ] + [connection.operations, workspace.workspaceId, onSubmit, clearFormChange, formId] ); const errorMessage = submitError ? createFormErrorMessage(submitError) : null; @@ -362,7 +352,7 @@ const ConnectionForm: React.FC = ({ errorMessage={ errorMessage || !isValid ? formatMessage({ id: "connectionForm.validation.error" }) : null } - editSchemeMode={editSchemeMode} + enableControls={allowSavingUntouchedForm} /> )} {mode === "create" && ( diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/components/EditControls.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/components/EditControls.tsx index c8366c3d3dc9..8fb8b4d83eb9 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/components/EditControls.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/components/EditControls.tsx @@ -11,16 +11,10 @@ interface EditControlProps { resetForm: () => void; successMessage?: React.ReactNode; errorMessage?: React.ReactNode; - editSchemeMode?: boolean; + enableControls?: boolean; withLine?: boolean; } -const Warning = styled.div` - margin-bottom: 10px; - font-size: 12px; - font-weight: bold; -`; - const Buttons = styled.div` display: flex; justify-content: space-between; @@ -57,7 +51,7 @@ const EditControls: React.FC = ({ resetForm, successMessage, errorMessage, - editSchemeMode, + enableControls, withLine, }) => { const showStatusMessage = () => { @@ -72,28 +66,19 @@ const EditControls: React.FC = ({ return ( <> - {editSchemeMode && ( - - - - )} {withLine && }
{showStatusMessage()}
- - {editSchemeMode ? ( - - ) : ( - - )} +
From efa5c982bae9c2b63e2379798476b73fbe6d5140 Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Tue, 12 Jul 2022 19:22:17 +0200 Subject: [PATCH 02/20] Start work on ModalSerice --- airbyte-webapp/src/App.tsx | 9 ++-- .../src/hooks/services/Modal/ModalService.tsx | 54 +++++++++++++++++++ .../src/hooks/services/Modal/index.ts | 1 + .../src/hooks/services/Modal/types.ts | 18 +++++++ airbyte-webapp/src/packages/cloud/App.tsx | 21 ++++---- .../components/ReplicationView.tsx | 52 +++++++++--------- 6 files changed, 115 insertions(+), 40 deletions(-) create mode 100644 airbyte-webapp/src/hooks/services/Modal/ModalService.tsx create mode 100644 airbyte-webapp/src/hooks/services/Modal/index.ts create mode 100644 airbyte-webapp/src/hooks/services/Modal/types.ts diff --git a/airbyte-webapp/src/App.tsx b/airbyte-webapp/src/App.tsx index 6536704c38a2..f3533dc0ef85 100644 --- a/airbyte-webapp/src/App.tsx +++ b/airbyte-webapp/src/App.tsx @@ -8,6 +8,7 @@ import { ServicesProvider } from "core/servicesProvider"; import { ConfirmationModalService } from "hooks/services/ConfirmationModal"; import { FeatureService } from "hooks/services/Feature"; import { FormChangeTrackerService } from "hooks/services/FormChangeTracker"; +import { ModalServiceProvider } from "hooks/services/Modal"; import NotificationService from "hooks/services/Notification"; import { AnalyticsProvider } from "views/common/AnalyticsProvider"; import { StoreProvider } from "views/common/StoreProvider"; @@ -44,9 +45,11 @@ const Services: React.FC = ({ children }) => ( - - {children} - + + + {children} + + diff --git a/airbyte-webapp/src/hooks/services/Modal/ModalService.tsx b/airbyte-webapp/src/hooks/services/Modal/ModalService.tsx new file mode 100644 index 000000000000..e1b9c1f688b2 --- /dev/null +++ b/airbyte-webapp/src/hooks/services/Modal/ModalService.tsx @@ -0,0 +1,54 @@ +import React, { useContext, useMemo, useRef, useState } from "react"; +import { firstValueFrom, Subject } from "rxjs"; + +import { Modal } from "components"; + +import { ModalOptions, ModalResult, ModalServiceContextType } from "./types"; + +const ModalServiceContext = React.createContext(undefined); + +export const ModalServiceProvider: React.FC = ({ children }) => { + const [modalOptions, setModalOptions] = useState(); + const promiseRef = useRef>(); + + const service: ModalServiceContextType = useMemo( + () => ({ + openModal: (options) => { + promiseRef.current = new Subject(); + setModalOptions(options); + + return firstValueFrom(promiseRef.current).then((reason) => { + setModalOptions(undefined); + promiseRef.current = undefined; + return reason; + }); + }, + closeModal: () => { + promiseRef.current?.next({ type: "canceled" }); + }, + }), + [] + ); + + return ( + + {children} + {modalOptions && ( + promiseRef.current?.next({ type: "canceled" })}> + promiseRef.current?.next({ type: "canceled" })} + onClose={(reason: unknown) => promiseRef.current?.next({ type: "closed", reason })} + /> + + )} + + ); +}; + +export const useModalService = () => { + const context = useContext(ModalServiceContext); + if (!context) { + throw new Error("Can't use ModalService outside ModalServiceProvider"); + } + return context; +}; diff --git a/airbyte-webapp/src/hooks/services/Modal/index.ts b/airbyte-webapp/src/hooks/services/Modal/index.ts new file mode 100644 index 000000000000..2a39bbce1986 --- /dev/null +++ b/airbyte-webapp/src/hooks/services/Modal/index.ts @@ -0,0 +1 @@ +export * from "./ModalService"; diff --git a/airbyte-webapp/src/hooks/services/Modal/types.ts b/airbyte-webapp/src/hooks/services/Modal/types.ts new file mode 100644 index 000000000000..e33574118eac --- /dev/null +++ b/airbyte-webapp/src/hooks/services/Modal/types.ts @@ -0,0 +1,18 @@ +import React from "react"; + +export interface ModalOptions { + title: React.ReactNode; + content: React.ComponentType; +} + +export type ModalResult = { type: "canceled" } | { type: "closed"; reason: unknown }; + +interface ModalContentProps { + onClose: (reason: unknown) => void; + onCancel: () => void; +} + +export interface ModalServiceContextType { + openModal: (options: ModalOptions) => Promise; + closeModal: () => void; +} diff --git a/airbyte-webapp/src/packages/cloud/App.tsx b/airbyte-webapp/src/packages/cloud/App.tsx index efcd8a3d263b..c064dd7b28a2 100644 --- a/airbyte-webapp/src/packages/cloud/App.tsx +++ b/airbyte-webapp/src/packages/cloud/App.tsx @@ -10,6 +10,7 @@ import { I18nProvider } from "core/i18n"; import { ConfirmationModalService } from "hooks/services/ConfirmationModal"; import { FeatureService } from "hooks/services/Feature"; import { FormChangeTrackerService } from "hooks/services/FormChangeTracker"; +import { ModalServiceProvider } from "hooks/services/Modal"; import NotificationServiceProvider from "hooks/services/Notification"; import en from "locales/en.json"; import { Routing } from "packages/cloud/cloudRoutes"; @@ -37,15 +38,17 @@ const Services: React.FC = ({ children }) => ( - - - - - {children} - - - - + + + + + + {children} + + + + + 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 13769d794d54..25968b19e93f 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx @@ -10,7 +10,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 { useConnectionLoad, useConnectionService, @@ -38,14 +38,13 @@ const TryArrow = styled(FontAwesomeIcon)` `; export const ReplicationView: React.FC = ({ onAfterSaveSchema, connectionId }) => { - const { openConfirmationModal, closeConfirmationModal } = useConfirmationModalService(); + // const { openConfirmationModal, closeConfirmationModal } = useConfirmationModalService(); + const { openModal, closeModal } = useModalService(); const [activeUpdatingSchemaMode, setActiveUpdatingSchemaMode] = useState(false); const [saved, setSaved] = useState(false); const [connectionFormValues, setConnectionFormValues] = useState(); const connectionService = useConnectionService(); - console.log("saved", saved); - const { mutateAsync: updateConnection } = useUpdateConnection(); const { connection: initialConnection, refreshConnectionCatalog } = useConnectionLoad(connectionId); @@ -114,33 +113,30 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch if (stateType === ConnectionStateType.legacy) { // The state type is legacy so the server will do a full reset after saving // TODO: Show confirm dialog with full reset option - openConfirmationModal({ - title: "connection.updateSchema", - text: "connection.updateSchemaText", - submitButtonText: "connection.updateSchema", - submitButtonDataId: "refresh", - secondaryButtonText: "connection.updateSchemaWithoutReset", - onSubmit: async (type) => { - await saveConnection(values, type === "secondary"); - closeConfirmationModal(); - }, - }); + // TODO: This should render a nicer modal with a checkbox inside, once we have a proper modal service + const result = await openModal({ title: "test", content: () =>

props

}); + console.log("result", result); + if (result.type !== "canceled") { + // TODO: right skipRefresh + await saveConnection(values, false); + } + // openConfirmationModal({ + // title: "connection.updateSchema", + // text: "connection.updateSchemaText", + // submitButtonText: "connection.updateSchema", + // submitButtonDataId: "refresh", + // secondaryButtonText: "connection.updateSchemaWithoutReset", + // onSubmit: async (type) => { + // await saveConnection(values, type === "secondary"); + // closeConfirmationModal(); + // }, + // }); } else { // TODO: Show confirm dialog with partial reset option - openConfirmationModal({ - title: "connection.updateSchema", - text: "connection.updateSchemaText", - submitButtonText: "connection.updateSchema", - submitButtonDataId: "refresh", - secondaryButtonText: "connection.updateSchemaWithoutReset", - onSubmit: async (type) => { - await saveConnection(values, type === "secondary"); - closeConfirmationModal(); - }, - }); + // TODO: This should render a nicer modal with a checkbox inside, once we have a proper modal service + const result = await openModal({ title: "test", content: () =>

props

}); + console.log("result", result); } - // const skipRefresh = true; - // await saveConnection(values, skipRefresh); } else { // The catalog hasn't changed. We don't need to ask for any confirmation and can simply save // TODO: Clarify if we want to have `skipRefresh` true or false in this case with BE. From 654d50204e7bba35a41829940884efa5ea1da1ed Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Wed, 13 Jul 2022 11:40:24 +0200 Subject: [PATCH 03/20] Continue work --- .../ConfirmationModal/ConfirmationModal.tsx | 20 +--- .../src/components/Modal/Modal.module.scss | 3 + airbyte-webapp/src/components/Modal/Modal.tsx | 10 +- .../ConfirmationModalService.tsx | 2 - .../src/hooks/services/Modal/ModalService.tsx | 24 +++-- .../src/hooks/services/Modal/types.ts | 12 +-- airbyte-webapp/src/locales/en.json | 8 +- .../components/ReplicationView.module.scss | 12 +++ .../components/ReplicationView.tsx | 101 +++++++++++------- .../ConnectionForm/ConnectionForm.tsx | 34 ++---- 10 files changed, 124 insertions(+), 102 deletions(-) create mode 100644 airbyte-webapp/src/components/Modal/Modal.module.scss create mode 100644 airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.module.scss diff --git a/airbyte-webapp/src/components/ConfirmationModal/ConfirmationModal.tsx b/airbyte-webapp/src/components/ConfirmationModal/ConfirmationModal.tsx index 48d0309a9da3..ab48e88fc16c 100644 --- a/airbyte-webapp/src/components/ConfirmationModal/ConfirmationModal.tsx +++ b/airbyte-webapp/src/components/ConfirmationModal/ConfirmationModal.tsx @@ -31,9 +31,7 @@ export interface ConfirmationModalProps { title: string; text: string; submitButtonText: string; - secondaryButtonText?: string; - secondaryButtonDataId?: string; - onSubmit: (button: "primary" | "secondary") => void; + onSubmit: () => void; submitButtonDataId?: string; cancelButtonText?: string; } @@ -45,13 +43,10 @@ export const ConfirmationModal: React.FC = ({ onSubmit, submitButtonText, submitButtonDataId, - secondaryButtonText, - secondaryButtonDataId, cancelButtonText, }) => { const { isLoading, startAction } = useLoadingState(); - const onSubmitBtnClick = () => startAction({ action: () => onSubmit("primary") }); - const onSecondaryBtnClick = () => startAction({ action: () => onSubmit("secondary") }); + const onSubmitBtnClick = () => startAction({ action: () => onSubmit() }); return ( }> @@ -61,17 +56,6 @@ export const ConfirmationModal: React.FC = ({ - {secondaryButtonText && ( - - - - )} diff --git a/airbyte-webapp/src/components/Modal/Modal.module.scss b/airbyte-webapp/src/components/Modal/Modal.module.scss new file mode 100644 index 000000000000..0e1c56c0b83a --- /dev/null +++ b/airbyte-webapp/src/components/Modal/Modal.module.scss @@ -0,0 +1,3 @@ +.modalContent { + max-width: 585px; +} diff --git a/airbyte-webapp/src/components/Modal/Modal.tsx b/airbyte-webapp/src/components/Modal/Modal.tsx index 38d37c414a83..34ec739aeef2 100644 --- a/airbyte-webapp/src/components/Modal/Modal.tsx +++ b/airbyte-webapp/src/components/Modal/Modal.tsx @@ -4,6 +4,8 @@ import styled, { keyframes } from "styled-components"; import ContentCard from "components/ContentCard"; +import styles from "./Modal.module.scss"; + export interface ModalProps { title?: string | React.ReactNode; onClose?: () => void; @@ -47,7 +49,13 @@ const Modal: React.FC = ({ children, title, onClose, clear, closeOnB return createPortal( (closeOnBackground && onClose ? onClose() : null)}> - {clear ? children : {children}} + {clear ? ( + children + ) : ( + + {children} + + )} , document.body ); diff --git a/airbyte-webapp/src/hooks/services/ConfirmationModal/ConfirmationModalService.tsx b/airbyte-webapp/src/hooks/services/ConfirmationModal/ConfirmationModalService.tsx index c810d0f34afb..52b3a50a9b01 100644 --- a/airbyte-webapp/src/hooks/services/ConfirmationModal/ConfirmationModalService.tsx +++ b/airbyte-webapp/src/hooks/services/ConfirmationModal/ConfirmationModalService.tsx @@ -66,8 +66,6 @@ export const ConfirmationModalService = ({ children }: { children: React.ReactNo submitButtonText={state.confirmationModal.submitButtonText} submitButtonDataId={state.confirmationModal.submitButtonDataId} cancelButtonText={state.confirmationModal.cancelButtonText} - secondaryButtonText={state.confirmationModal.secondaryButtonText} - secondaryButtonDataId={state.confirmationModal.secondaryButtonDataId} /> ) : null} diff --git a/airbyte-webapp/src/hooks/services/Modal/ModalService.tsx b/airbyte-webapp/src/hooks/services/Modal/ModalService.tsx index e1b9c1f688b2..2f816d52f59a 100644 --- a/airbyte-webapp/src/hooks/services/Modal/ModalService.tsx +++ b/airbyte-webapp/src/hooks/services/Modal/ModalService.tsx @@ -8,23 +8,29 @@ import { ModalOptions, ModalResult, ModalServiceContextType } from "./types"; const ModalServiceContext = React.createContext(undefined); export const ModalServiceProvider: React.FC = ({ children }) => { - const [modalOptions, setModalOptions] = useState(); - const promiseRef = useRef>(); + // The any here is due to the fact, that every call to open a modal might come in with + // a different type, thus we can't type this with unknown or a generic. + // The consuming code of this service though is properly typed, so that this `any` stays + // encapsulated within this component. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const [modalOptions, setModalOptions] = useState>(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const resultSubjectRef = useRef>>(); const service: ModalServiceContextType = useMemo( () => ({ openModal: (options) => { - promiseRef.current = new Subject(); + resultSubjectRef.current = new Subject(); setModalOptions(options); - return firstValueFrom(promiseRef.current).then((reason) => { + return firstValueFrom(resultSubjectRef.current).then((reason) => { setModalOptions(undefined); - promiseRef.current = undefined; + resultSubjectRef.current = undefined; return reason; }); }, closeModal: () => { - promiseRef.current?.next({ type: "canceled" }); + resultSubjectRef.current?.next({ type: "canceled" }); }, }), [] @@ -34,10 +40,10 @@ export const ModalServiceProvider: React.FC = ({ children }) => { {children} {modalOptions && ( - promiseRef.current?.next({ type: "canceled" })}> + resultSubjectRef.current?.next({ type: "canceled" })}> promiseRef.current?.next({ type: "canceled" })} - onClose={(reason: unknown) => promiseRef.current?.next({ type: "closed", reason })} + onCancel={() => resultSubjectRef.current?.next({ type: "canceled" })} + onClose={(reason) => resultSubjectRef.current?.next({ type: "closed", reason })} /> )} diff --git a/airbyte-webapp/src/hooks/services/Modal/types.ts b/airbyte-webapp/src/hooks/services/Modal/types.ts index e33574118eac..617e1ad84305 100644 --- a/airbyte-webapp/src/hooks/services/Modal/types.ts +++ b/airbyte-webapp/src/hooks/services/Modal/types.ts @@ -1,18 +1,18 @@ import React from "react"; -export interface ModalOptions { +export interface ModalOptions { title: React.ReactNode; - content: React.ComponentType; + content: React.ComponentType>; } -export type ModalResult = { type: "canceled" } | { type: "closed"; reason: unknown }; +export type ModalResult = { type: "canceled" } | { type: "closed"; reason: T }; -interface ModalContentProps { - onClose: (reason: unknown) => void; +interface ModalContentProps { + onClose: (reason: T) => void; onCancel: () => void; } export interface ModalServiceContextType { - openModal: (options: ModalOptions) => Promise; + openModal: (options: ModalOptions) => Promise>; closeModal: () => void; } diff --git a/airbyte-webapp/src/locales/en.json b/airbyte-webapp/src/locales/en.json index ef2c9f58abc2..b16855d2d33e 100644 --- a/airbyte-webapp/src/locales/en.json +++ b/airbyte-webapp/src/locales/en.json @@ -315,7 +315,12 @@ "syncMode.full_refresh": "Full refresh", "syncMode.incremental": "Incremental", - "connection.updateSchemaWithoutReset": "Save without reset", + "connection.resetModalTitle": "Stream configuration changed", + "connection.streamResetHint": "Due to changes in the stream configuration the affected streams should be resetted. This will delete all data of those streams in the destination and sync it again. Skipping this might lead to unexpected behavior and is discouraged.", + "connection.streamFullResetHint": "Due to changes in the stream configuration the all streams should be resetted. This will delete all data in the destination and sync it again. Skipping this might lead to unexpected behavior and is discouraged.", + "connection.saveWithReset": "Reset affected streams (recommended)", + "connection.saveWithFullReset": "Reset data in all streams (recommended)", + "connection.save": "Save connection", "connection.title": "Connection", "connection.description": "Connections link Sources to Destinations.", "connection.fromTo": "{source} → {destination}", @@ -328,7 +333,6 @@ "connection.sourceTestAgain": "Test source connection again", "connection.resetData": "Reset your data", "connection.updateSchema": "Refresh source schema", - "connection.updateSchemaText": "WARNING! Updating the schema will delete all the data for this connection in your destination and start syncing from scratch. Are you sure you want to do this?", "connection.newConnection": "+ New connection", "connection.newConnectionTitle": "New connection", "connection.noConnections": "Connection list is empty", diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.module.scss b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.module.scss new file mode 100644 index 000000000000..1330fd354a6a --- /dev/null +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.module.scss @@ -0,0 +1,12 @@ +@use "../../../../../scss/variables" as vars; + +.resetWarningModal { + padding: vars.$spacing-xl; +} + +.resetWarningModalButtons { + display: flex; + justify-content: flex-end; + gap: vars.$spacing-md; + margin-top: vars.$spacing-lg; +} 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 25968b19e93f..88876602508d 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx @@ -1,11 +1,11 @@ import { faSyncAlt } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import React, { useMemo, useState } from "react"; -import { FormattedMessage } from "react-intl"; -import { useAsyncFn } from "react-use"; +import { FormattedMessage, useIntl } from "react-intl"; +import { useAsyncFn, useUnmount } from "react-use"; import styled from "styled-components"; -import { Button, Card } from "components"; +import { Button, Card, LabeledSwitch } from "components"; import LoadingSchema from "components/LoadingSchema"; import { toWebBackendConnectionUpdate } from "core/domain/connection"; @@ -19,13 +19,50 @@ import { } from "hooks/services/useConnectionHook"; import { equal } from "utils/objects"; import ConnectionForm from "views/Connection/ConnectionForm"; +import { ConnectionFormSubmitResult } from "views/Connection/ConnectionForm/ConnectionForm"; import { FormikConnectionFormValues } from "views/Connection/ConnectionForm/formConfig"; +import styles from "./ReplicationView.module.scss"; + interface ReplicationViewProps { onAfterSaveSchema: () => void; connectionId: string; } +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. */} + +

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

+
+ + +
+
+ ); +}; + const Content = styled.div` max-width: 1279px; margin: 0 auto; @@ -38,7 +75,7 @@ const TryArrow = styled(FontAwesomeIcon)` `; export const ReplicationView: React.FC = ({ onAfterSaveSchema, connectionId }) => { - // const { openConfirmationModal, closeConfirmationModal } = useConfirmationModalService(); + const { formatMessage } = useIntl(); const { openModal, closeModal } = useModalService(); const [activeUpdatingSchemaMode, setActiveUpdatingSchemaMode] = useState(false); const [saved, setSaved] = useState(false); @@ -54,6 +91,8 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch [connectionId] ); + useUnmount(() => closeModal()); + const connection = useMemo(() => { if (activeUpdatingSchemaMode && connectionWithRefreshCatalog) { // merge connectionFormValues (unsaved previous form state) with the refreshed connection data: @@ -77,8 +116,10 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch const initialSyncSchema = connection.syncCatalog; const connectionAsUpdate = toWebBackendConnectionUpdate(connection); + // TODO: Remove console.log("skipReset", skipReset); + // TODO: Switch to v2 of this API await updateConnection({ ...connectionAsUpdate, ...values, @@ -101,45 +142,29 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch } }; - const onSubmitForm = async (values: ValuesProps) => { - console.log("values.syncCatalog", values.syncCatalog); - console.log("initialConnection", initialConnection.syncCatalog); + const onSubmitForm = async (values: ValuesProps): Promise => { + // Detect whether the catalog has any differences 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, initialConnection.syncCatalog); - - console.log("hasCatalogChanged?", hasCatalogChanged); + // 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); - console.log("ConnectionStateType", stateType); - if (stateType === ConnectionStateType.legacy) { - // The state type is legacy so the server will do a full reset after saving - // TODO: Show confirm dialog with full reset option - // TODO: This should render a nicer modal with a checkbox inside, once we have a proper modal service - const result = await openModal({ title: "test", content: () =>

props

}); - console.log("result", result); - if (result.type !== "canceled") { - // TODO: right skipRefresh - await saveConnection(values, false); - } - // openConfirmationModal({ - // title: "connection.updateSchema", - // text: "connection.updateSchemaText", - // submitButtonText: "connection.updateSchema", - // submitButtonDataId: "refresh", - // secondaryButtonText: "connection.updateSchemaWithoutReset", - // onSubmit: async (type) => { - // await saveConnection(values, type === "secondary"); - // closeConfirmationModal(); - // }, - // }); - } else { - // TODO: Show confirm dialog with partial reset option - // TODO: This should render a nicer modal with a checkbox inside, once we have a proper modal service - const result = await openModal({ title: "test", content: () =>

props

}); - console.log("result", result); + const result = await openModal({ + title: formatMessage({ id: "connection.resetModalTitle" }), + content: (props) => , + }); + if (result.type === "canceled") { + return { + submitCancelled: true, + }; } + // Save the connection taking into account the correct skipRefresh value from the dialog choice. + await saveConnection(values, !result.reason); } else { - // The catalog hasn't changed. We don't need to ask for any confirmation and can simply save - // TODO: Clarify if we want to have `skipRefresh` true or false in this case with BE. + // The catalog hasn't changed. We don't need to ask for any confirmation and can simply save. await saveConnection(values, true); } }; diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx index ca2eee9cfb07..9f72169f2449 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx @@ -86,8 +86,9 @@ const FormContainer = styled(Form)` } `; -interface ConnectionFormSubmitResult { - onSubmitComplete: () => void; +export interface ConnectionFormSubmitResult { + onSubmitComplete?: () => void; + submitCancelled?: boolean; } export type ConnectionFormMode = "create" | "edit" | "readonly"; @@ -154,20 +155,6 @@ const ConnectionForm: React.FC = ({ const initialValues = useInitialValues(connection, destDefinition, isEditMode); const workspace = useCurrentWorkspace(); - // TODO: Remove this - // const openResetDataModal = useCallback(() => { - // openConfirmationModal({ - // title: "form.resetData", - // text: "form.changedColumns", - // submitButtonText: "form.reset", - // cancelButtonText: "form.noNeed", - // onSubmit: async () => { - // await onReset?.(); - // closeConfirmationModal(); - // }, - // }); - // }, [closeConfirmationModal, onReset, openConfirmationModal]); - const onFormSubmit = useCallback( async (values: FormikConnectionFormValues, formikHelpers: FormikHelpers) => { const formValues: ConnectionFormValues = connectionValidationSchema.cast(values, { @@ -180,17 +167,12 @@ const ConnectionForm: React.FC = ({ try { const result = await onSubmit(formValues); - formikHelpers.resetForm({ values }); - clearFormChange(formId); - - // // TODO: Remove this - // const requiresReset = mode === "edit" && !equal(initialValues.syncCatalog, values.syncCatalog); - - // if (requiresReset) { - // openResetDataModal(); - // } + if (!result?.submitCancelled) { + formikHelpers.resetForm({ values }); + clearFormChange(formId); - result?.onSubmitComplete?.(); + result?.onSubmitComplete?.(); + } } catch (e) { setSubmitError(e); } From b89cc1971f2661fb36c9cc70fd916a4fcf134590 Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Wed, 13 Jul 2022 11:42:24 +0200 Subject: [PATCH 04/20] Fix typo --- airbyte-webapp/src/locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airbyte-webapp/src/locales/en.json b/airbyte-webapp/src/locales/en.json index b16855d2d33e..2590a0e8d334 100644 --- a/airbyte-webapp/src/locales/en.json +++ b/airbyte-webapp/src/locales/en.json @@ -317,7 +317,7 @@ "connection.resetModalTitle": "Stream configuration changed", "connection.streamResetHint": "Due to changes in the stream configuration the affected streams should be resetted. This will delete all data of those streams in the destination and sync it again. Skipping this might lead to unexpected behavior and is discouraged.", - "connection.streamFullResetHint": "Due to changes in the stream configuration the all streams should be resetted. This will delete all data in the destination and sync it again. Skipping this might lead to unexpected behavior and is discouraged.", + "connection.streamFullResetHint": "Due to changes in the stream configuration all streams should be resetted. This will delete all data in the destination and sync it again. Skipping this might lead to unexpected behavior and is discouraged.", "connection.saveWithReset": "Reset affected streams (recommended)", "connection.saveWithFullReset": "Reset data in all streams (recommended)", "connection.save": "Save connection", From 2657d88d6227242c6866f20474d43b5716cc9340 Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Wed, 13 Jul 2022 11:43:55 +0200 Subject: [PATCH 05/20] Change wording --- airbyte-webapp/src/locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airbyte-webapp/src/locales/en.json b/airbyte-webapp/src/locales/en.json index 2590a0e8d334..45eb1b2bdb34 100644 --- a/airbyte-webapp/src/locales/en.json +++ b/airbyte-webapp/src/locales/en.json @@ -319,7 +319,7 @@ "connection.streamResetHint": "Due to changes in the stream configuration the affected streams should be resetted. This will delete all data of those streams in the destination and sync it again. Skipping this might lead to unexpected behavior and is discouraged.", "connection.streamFullResetHint": "Due to changes in the stream configuration all streams should be resetted. This will delete all data in the destination and sync it again. Skipping this might lead to unexpected behavior and is discouraged.", "connection.saveWithReset": "Reset affected streams (recommended)", - "connection.saveWithFullReset": "Reset data in all streams (recommended)", + "connection.saveWithFullReset": "Reset all streams (recommended)", "connection.save": "Save connection", "connection.title": "Connection", "connection.description": "Connections link Sources to Destinations.", From 6fce4f0eb116d8c37751c8883cbc7b1c64434c70 Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Wed, 13 Jul 2022 11:46:32 +0200 Subject: [PATCH 06/20] Add todos and remove dead code --- .../src/views/Connection/ConnectionForm/ConnectionForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx index 9f72169f2449..c9bada9f38d5 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx @@ -97,6 +97,7 @@ export type ConnectionFormMode = "create" | "edit" | "readonly"; function FormValuesChangeTracker({ onChangeValues }: { onChangeValues?: (values: T) => void }) { // Grab values from context const { values, errors, dirty } = useFormikContext(); + // TODO: Remove debug output console.log("values", values); console.log("errors", errors); console.log("dirty", dirty); @@ -142,7 +143,6 @@ const ConnectionForm: React.FC = ({ connection, onChangeValues, }) => { - // const { openConfirmationModal, closeConfirmationModal } = useConfirmationModalService(); const destDefinition = useGetDestinationDefinitionSpecification(connection.destination.destinationDefinitionId); const { clearFormChange } = useFormChangeTrackerService(); const formId = useUniqueFormId(); From 5a9cbcc96b502f4467cf2d7721f4619616f20fb5 Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Wed, 13 Jul 2022 11:53:13 +0200 Subject: [PATCH 07/20] Remove dead message --- airbyte-webapp/src/locales/en.json | 1 - 1 file changed, 1 deletion(-) diff --git a/airbyte-webapp/src/locales/en.json b/airbyte-webapp/src/locales/en.json index 45eb1b2bdb34..334fadadc7e6 100644 --- a/airbyte-webapp/src/locales/en.json +++ b/airbyte-webapp/src/locales/en.json @@ -88,7 +88,6 @@ "form.noNeed": "No need!", "form.reset": "Reset", "form.resetData": "Reset your data", - "form.changedColumns": "You have changed which column is used to detect new records for a stream. You may want to reset the data in your stream.", "form.resetDataText": "Resetting your data will delete all the data for this connection in your destination and start syncs from scratch. Are you sure you want to do this?", "form.dockerError": "Could not find docker image", "form.edit": "Edit", From 9266bb56966634059c9e2ccd817db9d274dfb200 Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Wed, 13 Jul 2022 19:24:53 +0200 Subject: [PATCH 08/20] Adjust to new API --- .../connection/WebBackendConnectionService.ts | 2 +- .../src/core/domain/connection/utils.ts | 1 + .../src/hooks/services/useConnectionHook.tsx | 17 ++++------------- .../components/ReplicationView.tsx | 7 +------ 4 files changed, 7 insertions(+), 20 deletions(-) diff --git a/airbyte-webapp/src/core/domain/connection/WebBackendConnectionService.ts b/airbyte-webapp/src/core/domain/connection/WebBackendConnectionService.ts index bfd3f6c9b050..a8702ce251a7 100644 --- a/airbyte-webapp/src/core/domain/connection/WebBackendConnectionService.ts +++ b/airbyte-webapp/src/core/domain/connection/WebBackendConnectionService.ts @@ -4,7 +4,7 @@ import { webBackendCreateConnection, webBackendGetConnection, webBackendListConnectionsForWorkspace, - webBackendUpdateConnection, + webBackendUpdateConnectionNew as webBackendUpdateConnection, } from "../../request/AirbyteClient"; import { AirbyteRequestService } from "../../request/AirbyteRequestService"; diff --git a/airbyte-webapp/src/core/domain/connection/utils.ts b/airbyte-webapp/src/core/domain/connection/utils.ts index 15e242ed11b9..29bc15ababc4 100644 --- a/airbyte-webapp/src/core/domain/connection/utils.ts +++ b/airbyte-webapp/src/core/domain/connection/utils.ts @@ -19,6 +19,7 @@ export const buildConnectionUpdate = ( connection: WebBackendConnectionRead, connectionUpdate: Partial ): WebBackendConnectionUpdate => ({ + skipReset: true, ...toWebBackendConnectionUpdate(connection), ...connectionUpdate, }); diff --git a/airbyte-webapp/src/hooks/services/useConnectionHook.tsx b/airbyte-webapp/src/hooks/services/useConnectionHook.tsx index a925f10f650f..ae2708354b84 100644 --- a/airbyte-webapp/src/hooks/services/useConnectionHook.tsx +++ b/airbyte-webapp/src/hooks/services/useConnectionHook.tsx @@ -190,20 +190,11 @@ const useUpdateConnection = () => { const service = useWebConnectionService(); const queryClient = useQueryClient(); - return useMutation( - (connectionUpdate: WebBackendConnectionUpdate) => { - const withRefreshedCatalogCleaned = connectionUpdate.withRefreshedCatalog - ? { withRefreshedCatalog: connectionUpdate.withRefreshedCatalog } - : null; - - return service.update({ ...connectionUpdate, ...withRefreshedCatalogCleaned }); + return useMutation((connectionUpdate: WebBackendConnectionUpdate) => service.update(connectionUpdate), { + onSuccess: (connection) => { + queryClient.setQueryData(connectionsKeys.detail(connection.connectionId), connection); }, - { - onSuccess: (connection) => { - queryClient.setQueryData(connectionsKeys.detail(connection.connectionId), connection); - }, - } - ); + }); }; const useConnectionList = (): ListConnection => { 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 88876602508d..e1f348ad46f9 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx @@ -116,10 +116,6 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch const initialSyncSchema = connection.syncCatalog; const connectionAsUpdate = toWebBackendConnectionUpdate(connection); - // TODO: Remove - console.log("skipReset", skipReset); - - // TODO: Switch to v2 of this API await updateConnection({ ...connectionAsUpdate, ...values, @@ -128,8 +124,7 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch // The status can be toggled and the name can be changed in-between refreshing the schema name: initialConnection.name, status: initialConnection.status || "", - // TODO: skipRefresh: true/false - // skipReset, + skipReset, }); setSaved(true); From 871df8a8bb34b494f898151e4eed1e43e425c1d2 Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Thu, 14 Jul 2022 09:34:12 +0200 Subject: [PATCH 09/20] Adjust to new modal changes --- airbyte-webapp/src/hooks/services/Modal/ModalService.tsx | 6 +++++- airbyte-webapp/src/hooks/services/Modal/types.ts | 5 ++++- .../pages/ConnectionItemPage/components/ReplicationView.tsx | 1 + 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/airbyte-webapp/src/hooks/services/Modal/ModalService.tsx b/airbyte-webapp/src/hooks/services/Modal/ModalService.tsx index 2f816d52f59a..49f87e14569f 100644 --- a/airbyte-webapp/src/hooks/services/Modal/ModalService.tsx +++ b/airbyte-webapp/src/hooks/services/Modal/ModalService.tsx @@ -40,7 +40,11 @@ export const ModalServiceProvider: React.FC = ({ children }) => { {children} {modalOptions && ( - resultSubjectRef.current?.next({ type: "canceled" })}> + resultSubjectRef.current?.next({ type: "canceled" })} + > resultSubjectRef.current?.next({ type: "canceled" })} onClose={(reason) => resultSubjectRef.current?.next({ type: "closed", reason })} diff --git a/airbyte-webapp/src/hooks/services/Modal/types.ts b/airbyte-webapp/src/hooks/services/Modal/types.ts index 617e1ad84305..f81b1640bb11 100644 --- a/airbyte-webapp/src/hooks/services/Modal/types.ts +++ b/airbyte-webapp/src/hooks/services/Modal/types.ts @@ -1,7 +1,10 @@ import React from "react"; +import { ModalProps } from "components/Modal/Modal"; + export interface ModalOptions { - title: React.ReactNode; + title: ModalProps["title"]; + size?: ModalProps["size"]; content: React.ComponentType>; } 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 e1f348ad46f9..e19aab7cf44e 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx @@ -149,6 +149,7 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch const stateType = await connectionService.getStateType(connectionId); const result = await openModal({ title: formatMessage({ id: "connection.resetModalTitle" }), + size: "md", content: (props) => , }); if (result.type === "canceled") { From f0515e89438b6f1a628ccaee8556bcbfdff9fa7d Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Thu, 14 Jul 2022 09:54:36 +0200 Subject: [PATCH 10/20] Remove debug output --- .../src/views/Connection/ConnectionForm/ConnectionForm.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx index c9bada9f38d5..ffdedd876754 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx @@ -96,11 +96,7 @@ export type ConnectionFormMode = "create" | "edit" | "readonly"; // eslint-disable-next-line react/function-component-definition function FormValuesChangeTracker({ onChangeValues }: { onChangeValues?: (values: T) => void }) { // Grab values from context - const { values, errors, dirty } = useFormikContext(); - // TODO: Remove debug output - console.log("values", values); - console.log("errors", errors); - console.log("dirty", dirty); + const { values } = useFormikContext(); useDebounce( () => { onChangeValues?.(values); From 8de44f99b35be551c3f5923d52233b67503ad42d Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Thu, 14 Jul 2022 10:20:04 +0200 Subject: [PATCH 11/20] Fix e2e test --- airbyte-webapp-e2e-tests/cypress/integration/connection.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airbyte-webapp-e2e-tests/cypress/integration/connection.spec.ts b/airbyte-webapp-e2e-tests/cypress/integration/connection.spec.ts index d4a938505ff8..81a0aaa95841 100644 --- a/airbyte-webapp-e2e-tests/cypress/integration/connection.spec.ts +++ b/airbyte-webapp-e2e-tests/cypress/integration/connection.spec.ts @@ -17,7 +17,7 @@ describe("Connection main actions", () => { }); it("Update connection", () => { - cy.intercept("/api/v1/web_backend/connections/update").as("updateConnection"); + cy.intercept("/api/v1/web_backend/connections/updateNew").as("updateConnection"); createTestConnection("Test update connection source cypress", "Test update connection destination cypress"); From e08d0e575a151dc7c602452067da79a3d3180cbc Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Thu, 14 Jul 2022 12:10:26 +0200 Subject: [PATCH 12/20] Add data-testids --- .../ConnectionItemPage/components/ReplicationView.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 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 e19aab7cf44e..15a574753e3a 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx @@ -40,7 +40,7 @@ const ResetWarningModal: React.FC = ({ onCancel, onClose const [withReset, setWithReset] = useState(true); const requireFullReset = stateType === ConnectionStateType.legacy; return ( -
+
{/* TODO: This should use proper text stylings once we have them available. */}

@@ -49,13 +49,14 @@ const ResetWarningModal: React.FC = ({ onCancel, onClose onChange={(ev) => setWithReset(ev.target.checked)} label={formatMessage({ id: requireFullReset ? "connection.saveWithFullReset" : "connection.saveWithReset" })} checkbox + data-testid="resetModal-reset-checkbox" />

- -
From 6fd68b1fc58e507b9399c70fcdec97432132ed7a Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Fri, 15 Jul 2022 09:54:15 +0200 Subject: [PATCH 13/20] Adjust for PR review --- .../src/hooks/services/Modal/ModalService.tsx | 12 ++++++------ .../src/hooks/services/Modal/types.ts | 2 +- .../components/ReplicationView.tsx | 13 ++++++++----- .../ConnectionForm/ConnectionForm.tsx | 18 ++++++++++-------- 4 files changed, 25 insertions(+), 20 deletions(-) diff --git a/airbyte-webapp/src/hooks/services/Modal/ModalService.tsx b/airbyte-webapp/src/hooks/services/Modal/ModalService.tsx index 49f87e14569f..33f347b35d84 100644 --- a/airbyte-webapp/src/hooks/services/Modal/ModalService.tsx +++ b/airbyte-webapp/src/hooks/services/Modal/ModalService.tsx @@ -3,9 +3,9 @@ import { firstValueFrom, Subject } from "rxjs"; import { Modal } from "components"; -import { ModalOptions, ModalResult, ModalServiceContextType } from "./types"; +import { ModalOptions, ModalResult, ModalServiceContext } from "./types"; -const ModalServiceContext = React.createContext(undefined); +const modalServiceContext = React.createContext(undefined); export const ModalServiceProvider: React.FC = ({ children }) => { // The any here is due to the fact, that every call to open a modal might come in with @@ -17,7 +17,7 @@ export const ModalServiceProvider: React.FC = ({ children }) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const resultSubjectRef = useRef>>(); - const service: ModalServiceContextType = useMemo( + const service: ModalServiceContext = useMemo( () => ({ openModal: (options) => { resultSubjectRef.current = new Subject(); @@ -37,7 +37,7 @@ export const ModalServiceProvider: React.FC = ({ children }) => { ); return ( - + {children} {modalOptions && ( { /> )} - + ); }; export const useModalService = () => { - const context = useContext(ModalServiceContext); + const context = useContext(modalServiceContext); if (!context) { throw new Error("Can't use ModalService outside ModalServiceProvider"); } diff --git a/airbyte-webapp/src/hooks/services/Modal/types.ts b/airbyte-webapp/src/hooks/services/Modal/types.ts index f81b1640bb11..fe2592fc02f5 100644 --- a/airbyte-webapp/src/hooks/services/Modal/types.ts +++ b/airbyte-webapp/src/hooks/services/Modal/types.ts @@ -15,7 +15,7 @@ interface ModalContentProps { onCancel: () => void; } -export interface ModalServiceContextType { +export interface ModalServiceContext { openModal: (options: ModalOptions) => Promise>; closeModal: () => void; } 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 15a574753e3a..e413df0c4c48 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx @@ -41,7 +41,10 @@ const ResetWarningModal: React.FC = ({ onCancel, onClose const requireFullReset = stateType === ConnectionStateType.legacy; return (
- {/* TODO: This should use proper text stylings once we have them available. */} + {/* + TODO: This should use proper text stylings once we have them available. + See https://github.com/airbytehq/airbyte/issues/14478 + */}

= ({ onAfterSaveSch return initialConnection; }, [activeUpdatingSchemaMode, connectionWithRefreshCatalog, initialConnection, connectionFormValues]); - const saveConnection = async (values: ValuesProps, skipReset = false) => { + const saveConnection = async (values: ValuesProps, { skipReset }: { skipReset: boolean }) => { const initialSyncSchema = connection.syncCatalog; const connectionAsUpdate = toWebBackendConnectionUpdate(connection); @@ -159,10 +162,10 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch }; } // Save the connection taking into account the correct skipRefresh value from the dialog choice. - await saveConnection(values, !result.reason); + 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, true); + await saveConnection(values, { skipReset: true }); } }; @@ -187,7 +190,7 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch onSubmit={onSubmitForm} successMessage={saved && } onCancel={onCancelConnectionFormEdit} - allowSavingUntouchedForm={activeUpdatingSchemaMode} + canSubmitUntouchedForm={activeUpdatingSchemaMode} additionalSchemaControl={ -

-
+ + ); }; From a0cb0ceebef6cb637da0538221ddcc0888777f93 Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Fri, 15 Jul 2022 11:48:31 +0200 Subject: [PATCH 15/20] Add ModalService tests --- airbyte-webapp/package-lock.json | 23 ++--- airbyte-webapp/package.json | 2 +- airbyte-webapp/src/components/Modal/Modal.tsx | 6 +- .../services/Modal/ModalService.test.tsx | 93 +++++++++++++++++++ 4 files changed, 106 insertions(+), 18 deletions(-) create mode 100644 airbyte-webapp/src/hooks/services/Modal/ModalService.test.tsx diff --git a/airbyte-webapp/package-lock.json b/airbyte-webapp/package-lock.json index 893e9142ee42..4520934657de 100644 --- a/airbyte-webapp/package-lock.json +++ b/airbyte-webapp/package-lock.json @@ -64,7 +64,7 @@ "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^12.1.3", "@testing-library/react-hooks": "^7.0.2", - "@testing-library/user-event": "^13.5.0", + "@testing-library/user-event": "^14.2.4", "@types/flat": "^5.0.2", "@types/jest": "^27.4.1", "@types/json-schema": "^7.0.11", @@ -14045,15 +14045,12 @@ } }, "node_modules/@testing-library/user-event": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.5.0.tgz", - "integrity": "sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==", + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.2.4.tgz", + "integrity": "sha512-Abnzz5vdr2hw56NAzB1hs33Hx1LtLaI9LfROA8YbvS0xbUHjso7jZ6M+eqvR0PW9IFQVH8NQ6FsLQjIKz7RAeQ==", "dev": true, - "dependencies": { - "@babel/runtime": "^7.12.5" - }, "engines": { - "node": ">=10", + "node": ">=12", "npm": ">=6" }, "peerDependencies": { @@ -57134,13 +57131,11 @@ } }, "@testing-library/user-event": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.5.0.tgz", - "integrity": "sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==", + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.2.4.tgz", + "integrity": "sha512-Abnzz5vdr2hw56NAzB1hs33Hx1LtLaI9LfROA8YbvS0xbUHjso7jZ6M+eqvR0PW9IFQVH8NQ6FsLQjIKz7RAeQ==", "dev": true, - "requires": { - "@babel/runtime": "^7.12.5" - } + "requires": {} }, "@tootallnate/once": { "version": "1.1.2", diff --git a/airbyte-webapp/package.json b/airbyte-webapp/package.json index a7af0a6a2bac..a1a48d074fb2 100644 --- a/airbyte-webapp/package.json +++ b/airbyte-webapp/package.json @@ -78,7 +78,7 @@ "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^12.1.3", "@testing-library/react-hooks": "^7.0.2", - "@testing-library/user-event": "^13.5.0", + "@testing-library/user-event": "^14.2.4", "@types/flat": "^5.0.2", "@types/jest": "^27.4.1", "@types/json-schema": "^7.0.11", diff --git a/airbyte-webapp/src/components/Modal/Modal.tsx b/airbyte-webapp/src/components/Modal/Modal.tsx index 705544d1a626..3fc3dfc33084 100644 --- a/airbyte-webapp/src/components/Modal/Modal.tsx +++ b/airbyte-webapp/src/components/Modal/Modal.tsx @@ -22,10 +22,10 @@ const cardStyleBySize = { }; const Modal: React.FC = ({ children, title, onClose, clear, closeOnBackground, size }) => { - const handleUserKeyPress = useCallback((event, closeModal) => { - const { keyCode } = event; + const handleUserKeyPress = useCallback((event: KeyboardEvent, closeModal: () => void) => { + const { key } = event; // Escape key - if (keyCode === 27) { + if (key === "Escape") { closeModal(); } }, []); diff --git a/airbyte-webapp/src/hooks/services/Modal/ModalService.test.tsx b/airbyte-webapp/src/hooks/services/Modal/ModalService.test.tsx new file mode 100644 index 000000000000..f710a9f47ba9 --- /dev/null +++ b/airbyte-webapp/src/hooks/services/Modal/ModalService.test.tsx @@ -0,0 +1,93 @@ +import { render } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { useEffectOnce } from "react-use"; + +import { ModalServiceProvider, useModalService } from "./ModalService"; +import { ModalResult } from "./types"; + +const TestComponent: React.FC<{ onModalResult?: (result: ModalResult) => void }> = ({ onModalResult }) => { + const { openModal } = useModalService(); + useEffectOnce(() => { + openModal({ + title: "Test Modal Title", + content: ({ onCancel, onClose }) => ( +
+ + + +
+ ), + }).then(onModalResult); + }); + return null; +}; + +const renderModal = (resultCallback?: (reason: unknown) => void) => { + return render( + + + + ); +}; + +describe("ModalService", () => { + it("should open a modal on openModal", () => { + const rendered = renderModal(); + + expect(rendered.getByText("Test Modal Title")).toBeTruthy(); + expect(rendered.getByTestId("testModalContent")).toBeTruthy(); + }); + + it("should close the modal with escape and emit a cancel result", async () => { + const user = userEvent.setup(); + + const resultCallback = jest.fn(); + + const rendered = renderModal(resultCallback); + + await user.keyboard("{Escape}"); + + expect(rendered.queryByTestId("testModalContent")).toBeFalsy(); + expect(resultCallback).toHaveBeenCalledWith({ type: "canceled" }); + }); + + it("should allow cancelling the modal from inside", async () => { + const user = userEvent.setup(); + + const resultCallback = jest.fn(); + + const rendered = renderModal(resultCallback); + + await user.click(rendered.getByTestId("cancel")); + + expect(rendered.queryByTestId("testModalContent")).toBeFalsy(); + expect(resultCallback).toHaveBeenCalledWith({ type: "canceled" }); + }); + + it("should allow closing the button with a reason and return that reason", async () => { + const user = userEvent.setup(); + + const resultCallback = jest.fn(); + + let rendered = renderModal(resultCallback); + + await user.click(rendered.getByTestId("close-reason1")); + + expect(rendered.queryByTestId("testModalContent")).toBeFalsy(); + expect(resultCallback).toHaveBeenCalledWith({ type: "closed", reason: "reason1" }); + + resultCallback.mockReset(); + rendered = renderModal(resultCallback); + + await user.click(rendered.getByTestId("close-reason2")); + + expect(rendered.queryByTestId("testModalContent")).toBeFalsy(); + expect(resultCallback).toHaveBeenCalledWith({ type: "closed", reason: "reason2" }); + }); +}); From f736e2ec84745a09643a3951cb2101c091862bd6 Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Mon, 18 Jul 2022 18:28:54 +0100 Subject: [PATCH 16/20] Downgrade user-events again --- airbyte-webapp/package-lock.json | 23 +++++++++++-------- airbyte-webapp/package.json | 2 +- .../services/Modal/ModalService.test.tsx | 16 ++++--------- 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/airbyte-webapp/package-lock.json b/airbyte-webapp/package-lock.json index 4520934657de..893e9142ee42 100644 --- a/airbyte-webapp/package-lock.json +++ b/airbyte-webapp/package-lock.json @@ -64,7 +64,7 @@ "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^12.1.3", "@testing-library/react-hooks": "^7.0.2", - "@testing-library/user-event": "^14.2.4", + "@testing-library/user-event": "^13.5.0", "@types/flat": "^5.0.2", "@types/jest": "^27.4.1", "@types/json-schema": "^7.0.11", @@ -14045,12 +14045,15 @@ } }, "node_modules/@testing-library/user-event": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.2.4.tgz", - "integrity": "sha512-Abnzz5vdr2hw56NAzB1hs33Hx1LtLaI9LfROA8YbvS0xbUHjso7jZ6M+eqvR0PW9IFQVH8NQ6FsLQjIKz7RAeQ==", + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.5.0.tgz", + "integrity": "sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==", "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5" + }, "engines": { - "node": ">=12", + "node": ">=10", "npm": ">=6" }, "peerDependencies": { @@ -57131,11 +57134,13 @@ } }, "@testing-library/user-event": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.2.4.tgz", - "integrity": "sha512-Abnzz5vdr2hw56NAzB1hs33Hx1LtLaI9LfROA8YbvS0xbUHjso7jZ6M+eqvR0PW9IFQVH8NQ6FsLQjIKz7RAeQ==", + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.5.0.tgz", + "integrity": "sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==", "dev": true, - "requires": {} + "requires": { + "@babel/runtime": "^7.12.5" + } }, "@tootallnate/once": { "version": "1.1.2", diff --git a/airbyte-webapp/package.json b/airbyte-webapp/package.json index a1a48d074fb2..a7af0a6a2bac 100644 --- a/airbyte-webapp/package.json +++ b/airbyte-webapp/package.json @@ -78,7 +78,7 @@ "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^12.1.3", "@testing-library/react-hooks": "^7.0.2", - "@testing-library/user-event": "^14.2.4", + "@testing-library/user-event": "^13.5.0", "@types/flat": "^5.0.2", "@types/jest": "^27.4.1", "@types/json-schema": "^7.0.11", diff --git a/airbyte-webapp/src/hooks/services/Modal/ModalService.test.tsx b/airbyte-webapp/src/hooks/services/Modal/ModalService.test.tsx index f710a9f47ba9..fc3f82776b35 100644 --- a/airbyte-webapp/src/hooks/services/Modal/ModalService.test.tsx +++ b/airbyte-webapp/src/hooks/services/Modal/ModalService.test.tsx @@ -1,4 +1,4 @@ -import { render } from "@testing-library/react"; +import { render, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { useEffectOnce } from "react-use"; @@ -45,39 +45,33 @@ describe("ModalService", () => { }); it("should close the modal with escape and emit a cancel result", async () => { - const user = userEvent.setup(); - const resultCallback = jest.fn(); const rendered = renderModal(resultCallback); - await user.keyboard("{Escape}"); + await waitFor(() => userEvent.keyboard("{Escape}")); expect(rendered.queryByTestId("testModalContent")).toBeFalsy(); expect(resultCallback).toHaveBeenCalledWith({ type: "canceled" }); }); it("should allow cancelling the modal from inside", async () => { - const user = userEvent.setup(); - const resultCallback = jest.fn(); const rendered = renderModal(resultCallback); - await user.click(rendered.getByTestId("cancel")); + await waitFor(() => userEvent.click(rendered.getByTestId("cancel"))); expect(rendered.queryByTestId("testModalContent")).toBeFalsy(); expect(resultCallback).toHaveBeenCalledWith({ type: "canceled" }); }); it("should allow closing the button with a reason and return that reason", async () => { - const user = userEvent.setup(); - const resultCallback = jest.fn(); let rendered = renderModal(resultCallback); - await user.click(rendered.getByTestId("close-reason1")); + await waitFor(() => userEvent.click(rendered.getByTestId("close-reason1"))); expect(rendered.queryByTestId("testModalContent")).toBeFalsy(); expect(resultCallback).toHaveBeenCalledWith({ type: "closed", reason: "reason1" }); @@ -85,7 +79,7 @@ describe("ModalService", () => { resultCallback.mockReset(); rendered = renderModal(resultCallback); - await user.click(rendered.getByTestId("close-reason2")); + await waitFor(() => userEvent.click(rendered.getByTestId("close-reason2"))); expect(rendered.queryByTestId("testModalContent")).toBeFalsy(); expect(resultCallback).toHaveBeenCalledWith({ type: "closed", reason: "reason2" }); From b92807b30f5b3c029a3f954a57e5fb88c0e15fa0 Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Tue, 19 Jul 2022 15:47:25 +0100 Subject: [PATCH 17/20] Only compare selected streams --- .../ConnectionItemPage/components/ReplicationView.tsx | 7 +++++-- 1 file changed, 5 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 97ce7e085c2d..99d0a1a540de 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx @@ -144,10 +144,13 @@ export const ReplicationView: React.FC = ({ onAfterSaveSch }; const onSubmitForm = async (values: ValuesProps): Promise => { - // Detect whether the catalog has any differences compared to the original one. + // 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, initialConnection.syncCatalog); + 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. From 1f0b868e436b8b0f9ef9530145a61187b7ff9efb Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Tue, 19 Jul 2022 18:19:20 +0100 Subject: [PATCH 18/20] Update airbyte-webapp/src/locales/en.json Co-authored-by: Andy Jih --- airbyte-webapp/src/locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airbyte-webapp/src/locales/en.json b/airbyte-webapp/src/locales/en.json index af7dc682a3a8..798a85fce12a 100644 --- a/airbyte-webapp/src/locales/en.json +++ b/airbyte-webapp/src/locales/en.json @@ -316,7 +316,7 @@ "connection.resetModalTitle": "Stream configuration changed", "connection.streamResetHint": "Due to changes in the stream configuration the affected streams should be resetted. This will delete all data of those streams in the destination and sync it again. Skipping this might lead to unexpected behavior and is discouraged.", - "connection.streamFullResetHint": "Due to changes in the stream configuration all streams should be resetted. This will delete all data in the destination and sync it again. Skipping this might lead to unexpected behavior and is discouraged.", + "connection.streamFullResetHint": "Due to changes in the stream configuration, we recommend a data reset. A reset will delete data in the destination of the affected streams and then re-sync that data. Skipping the reset is discouraged and might lead to unexpected behavior.", "connection.saveWithReset": "Reset affected streams (recommended)", "connection.saveWithFullReset": "Reset all streams (recommended)", "connection.save": "Save connection", From 5e759ca90efa13d651b72f739fd61fd0bacdd152 Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Tue, 19 Jul 2022 18:19:27 +0100 Subject: [PATCH 19/20] Update airbyte-webapp/src/locales/en.json Co-authored-by: Andy Jih --- airbyte-webapp/src/locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airbyte-webapp/src/locales/en.json b/airbyte-webapp/src/locales/en.json index 798a85fce12a..055bcef0b2c3 100644 --- a/airbyte-webapp/src/locales/en.json +++ b/airbyte-webapp/src/locales/en.json @@ -315,7 +315,7 @@ "syncMode.incremental": "Incremental", "connection.resetModalTitle": "Stream configuration changed", - "connection.streamResetHint": "Due to changes in the stream configuration the affected streams should be resetted. This will delete all data of those streams in the destination and sync it again. Skipping this might lead to unexpected behavior and is discouraged.", + "connection.streamResetHint": "Due to changes in the stream configuration, we recommend a data reset. A reset will delete data in the destination of the affected streams and then re-sync that data. Skipping the reset is discouraged and might lead to unexpected behavior.", "connection.streamFullResetHint": "Due to changes in the stream configuration, we recommend a data reset. A reset will delete data in the destination of the affected streams and then re-sync that data. Skipping the reset is discouraged and might lead to unexpected behavior.", "connection.saveWithReset": "Reset affected streams (recommended)", "connection.saveWithFullReset": "Reset all streams (recommended)", From 95a90b68538f9834eaaf8711ad28b6fdcceed557 Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Tue, 19 Jul 2022 18:20:00 +0100 Subject: [PATCH 20/20] Remove redundant space --- airbyte-webapp/src/locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airbyte-webapp/src/locales/en.json b/airbyte-webapp/src/locales/en.json index 055bcef0b2c3..8dcf26305a3b 100644 --- a/airbyte-webapp/src/locales/en.json +++ b/airbyte-webapp/src/locales/en.json @@ -316,7 +316,7 @@ "connection.resetModalTitle": "Stream configuration changed", "connection.streamResetHint": "Due to changes in the stream configuration, we recommend a data reset. A reset will delete data in the destination of the affected streams and then re-sync that data. Skipping the reset is discouraged and might lead to unexpected behavior.", - "connection.streamFullResetHint": "Due to changes in the stream configuration, we recommend a data reset. A reset will delete data in the destination of the affected streams and then re-sync that data. Skipping the reset is discouraged and might lead to unexpected behavior.", + "connection.streamFullResetHint": "Due to changes in the stream configuration, we recommend a data reset. A reset will delete data in the destination of the affected streams and then re-sync that data. Skipping the reset is discouraged and might lead to unexpected behavior.", "connection.saveWithReset": "Reset affected streams (recommended)", "connection.saveWithFullReset": "Reset all streams (recommended)", "connection.save": "Save connection",