From bdeadfca587f0272b4f33eab243039d854b2eb84 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Fri, 3 Jun 2022 17:06:50 +0300 Subject: [PATCH] Fixed reverted PR: Fix cancel button when it doesn't provide feedback to the user + UX improvements (#13388) * turn on "curly" rule * set a loading state for 'Cancel' button if user clicked * extend ToolTip component to support 'not-allowed' cursor * disable 'Reset data' and 'Sync now' buttons if sync is in 'pending' or 'running' status * set loading state and disable buttons in ResetDataModal if reset process is launched * extend ToolTip component - shoe desired cursor type when tooltip is active * refactored: control tooltips showing this props * replace functions call in jsx with components * add style for loading 'danger' button * minor improvements - use ternary operator conditionally setting the desired text * set loading state to 'false' if async action has failed * extend ConfirmationModal - to support optional cancelButtonText prop * extend useLoadingState component - to show an error notification if async action is failed * show notification message on top * replace using ResetData modal with default text with useConfirmationModalService * replace ResetDataModal (refresh schema) with useConfirmationModalService * remove obsolete ResetDataModal component * updated tests * replace ResetDataModal (changed column) with useConfirmationModalService --- airbyte-webapp/.eslintrc | 1 + .../ConfirmationModal/ConfirmationModal.tsx | 40 +++++---- .../JobItem/components/MainInfo.tsx | 25 ++++-- .../ResetDataModal/ResetDataModal.tsx | 82 ------------------ .../src/components/ResetDataModal/index.tsx | 3 - .../src/components/ResetDataModal/types.ts | 5 -- .../SingletonCard/SingletonCard.tsx | 1 + .../src/components/ToolTip/ToolTip.tsx | 8 +- .../components/base/Button/LoadingButton.tsx | 12 ++- .../ConfirmationModalService.tsx | 1 + airbyte-webapp/src/hooks/useLoadingState.tsx | 42 ++++++--- airbyte-webapp/src/locales/en.json | 3 + .../src/packages/firebaseReact/sdk.tsx | 8 +- .../components/ReplicationView.tsx | 39 ++++----- .../components/StatusView.tsx | 85 ++++++++++++------- .../ConnectionForm/ConnectionForm.test.tsx | 28 ++++-- .../ConnectionForm/ConnectionForm.tsx | 40 +++++---- 17 files changed, 206 insertions(+), 217 deletions(-) delete mode 100644 airbyte-webapp/src/components/ResetDataModal/ResetDataModal.tsx delete mode 100644 airbyte-webapp/src/components/ResetDataModal/index.tsx delete mode 100644 airbyte-webapp/src/components/ResetDataModal/types.ts diff --git a/airbyte-webapp/.eslintrc b/airbyte-webapp/.eslintrc index 38c7b05515be..16931adccbcc 100644 --- a/airbyte-webapp/.eslintrc +++ b/airbyte-webapp/.eslintrc @@ -15,6 +15,7 @@ } }, "rules": { + "curly": "error", "prettier/prettier": "error", "unused-imports/no-unused-imports": "error", "import/order": [ diff --git a/airbyte-webapp/src/components/ConfirmationModal/ConfirmationModal.tsx b/airbyte-webapp/src/components/ConfirmationModal/ConfirmationModal.tsx index 5bc370088960..ab48e88fc16c 100644 --- a/airbyte-webapp/src/components/ConfirmationModal/ConfirmationModal.tsx +++ b/airbyte-webapp/src/components/ConfirmationModal/ConfirmationModal.tsx @@ -2,9 +2,12 @@ import React from "react"; import { FormattedMessage } from "react-intl"; import styled from "styled-components"; +import { LoadingButton } from "components"; import { Button } from "components/base/Button"; import Modal from "components/Modal"; +import useLoadingState from "../../hooks/useLoadingState"; + const Content = styled.div` width: 585px; font-size: 14px; @@ -30,6 +33,7 @@ export interface ConfirmationModalProps { submitButtonText: string; onSubmit: () => void; submitButtonDataId?: string; + cancelButtonText?: string; } export const ConfirmationModal: React.FC = ({ @@ -39,18 +43,24 @@ export const ConfirmationModal: React.FC = ({ onSubmit, submitButtonText, submitButtonDataId, -}) => ( - }> - - - - - - - - - - -); + cancelButtonText, +}) => { + const { isLoading, startAction } = useLoadingState(); + const onSubmitBtnClick = () => startAction({ action: () => onSubmit() }); + + return ( + }> + + + + + + + + + + + + + ); +}; diff --git a/airbyte-webapp/src/components/JobItem/components/MainInfo.tsx b/airbyte-webapp/src/components/JobItem/components/MainInfo.tsx index bf3e1dca0113..9516c1246d4f 100644 --- a/airbyte-webapp/src/components/JobItem/components/MainInfo.tsx +++ b/airbyte-webapp/src/components/JobItem/components/MainInfo.tsx @@ -4,14 +4,15 @@ import React from "react"; import { FormattedDateParts, FormattedMessage, FormattedTimeParts } from "react-intl"; import styled from "styled-components"; -import { Button, StatusIcon } from "components"; +import { LoadingButton, StatusIcon } from "components"; import { Cell, Row } from "components/SimpleTableComponents"; +import { AttemptRead, JobStatus } from "core/request/AirbyteClient"; import { SynchronousJobReadWithStatus } from "core/request/LogsRequestError"; +import useLoadingState from "hooks/useLoadingState"; import { JobsWithJobs } from "pages/ConnectionPage/pages/ConnectionItemPage/components/JobsList"; +import { useCancelJob } from "services/job/JobService"; -import { AttemptRead, JobStatus } from "../../../core/request/AirbyteClient"; -import { useCancelJob } from "../../../services/job/JobService"; import { getJobId, getJobStatus } from "../JobItem"; import AttemptDetails from "./AttemptDetails"; @@ -43,7 +44,7 @@ const AttemptCount = styled.div` color: ${({ theme }) => theme.dangerColor}; `; -const CancelButton = styled(Button)` +const CancelButton = styled(LoadingButton)` margin-right: 10px; padding: 3px 7px; z-index: 1; @@ -105,11 +106,13 @@ const MainInfo: React.FC = ({ shortInfo, isPartialSuccess, }) => { + const { isLoading, showFeedback, startAction } = useLoadingState(); const cancelJob = useCancelJob(); - const onCancelJob = async (event: React.SyntheticEvent) => { + const onCancelJob = (event: React.SyntheticEvent) => { event.stopPropagation(); - return cancelJob(Number(getJobId(job))); + const jobId = Number(getJobId(job)); + return startAction({ action: () => cancelJob(jobId) }); }; const jobStatus = getJobStatus(job); @@ -152,8 +155,14 @@ const MainInfo: React.FC = ({ {!shortInfo && isNotCompleted && ( - - + + )} diff --git a/airbyte-webapp/src/components/ResetDataModal/ResetDataModal.tsx b/airbyte-webapp/src/components/ResetDataModal/ResetDataModal.tsx deleted file mode 100644 index 1431e7ef2799..000000000000 --- a/airbyte-webapp/src/components/ResetDataModal/ResetDataModal.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import React from "react"; -import { FormattedMessage } from "react-intl"; -import styled from "styled-components"; - -import { Button } from "components"; -import Modal from "components/Modal"; - -import { ModalTypes } from "./types"; - -export type IProps = { - onClose: () => void; - onSubmit: (data: unknown) => void; - modalType?: ModalTypes; -}; - -const Content = styled.div` - padding: 18px 37px 28px; - font-size: 14px; - line-height: 28px; - max-width: 585px; -`; -const ButtonContent = styled.div` - padding-top: 27px; - text-align: right; -`; -const ButtonWithMargin = styled(Button)` - margin-right: 9px; -`; - -const ResetDataModal: React.FC = ({ onClose, onSubmit, modalType }) => { - const modalText = () => { - if (modalType === ModalTypes.RESET_CHANGED_COLUMN) { - return ; - } - - if (modalType === ModalTypes.UPDATE_SCHEMA) { - return ; - } - - return ; - }; - - const modalTitle = () => { - if (modalType === ModalTypes.UPDATE_SCHEMA) { - return ; - } - - return ; - }; - - const modalCancelButtonText = () => { - if (modalType === ModalTypes.UPDATE_SCHEMA) { - return ; - } - - return ; - }; - - const modalSubmitButtonText = () => { - if (modalType === ModalTypes.UPDATE_SCHEMA) { - return ; - } - - return ; - }; - - return ( - - - {modalText()} - - - {modalCancelButtonText()} - - - - - - ); -}; - -export default ResetDataModal; diff --git a/airbyte-webapp/src/components/ResetDataModal/index.tsx b/airbyte-webapp/src/components/ResetDataModal/index.tsx deleted file mode 100644 index befa2b2296dc..000000000000 --- a/airbyte-webapp/src/components/ResetDataModal/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import ResetDataModal from "./ResetDataModal"; - -export default ResetDataModal; diff --git a/airbyte-webapp/src/components/ResetDataModal/types.ts b/airbyte-webapp/src/components/ResetDataModal/types.ts deleted file mode 100644 index 3311353a055f..000000000000 --- a/airbyte-webapp/src/components/ResetDataModal/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum ModalTypes { - RESET_DATA = "ResetData", - RESET_CHANGED_COLUMN = "ResetChangedColumn", - UPDATE_SCHEMA = "UpdateSchema", -} diff --git a/airbyte-webapp/src/components/SingletonCard/SingletonCard.tsx b/airbyte-webapp/src/components/SingletonCard/SingletonCard.tsx index 227ce2ff7364..3fd312ce99a1 100644 --- a/airbyte-webapp/src/components/SingletonCard/SingletonCard.tsx +++ b/airbyte-webapp/src/components/SingletonCard/SingletonCard.tsx @@ -30,6 +30,7 @@ const Singleton = styled.div<{ hasError?: boolean }>` bottom: 49px; left: 50%; transform: translate(-50%, 0); + z-index: 20; padding: 25px 25px 22px; diff --git a/airbyte-webapp/src/components/ToolTip/ToolTip.tsx b/airbyte-webapp/src/components/ToolTip/ToolTip.tsx index 247dd8f25df8..98ba67b9a2ec 100644 --- a/airbyte-webapp/src/components/ToolTip/ToolTip.tsx +++ b/airbyte-webapp/src/components/ToolTip/ToolTip.tsx @@ -5,13 +5,13 @@ type ToolTipProps = { control: React.ReactNode; className?: string; disabled?: boolean; - cursor?: "pointer" | "help"; + cursor?: "pointer" | "help" | "not-allowed"; }; -const Control = styled.div<{ $cursor?: "pointer" | "help" }>` +const Control = styled.div<{ $cursor?: "pointer" | "help" | "not-allowed"; $showCursor?: boolean }>` display: inline-block; position: relative; - cursor: ${({ $cursor }) => $cursor ?? "pointer"}; + cursor: ${({ $cursor, $showCursor = true }) => ($showCursor && $cursor) ?? "pointer"}; `; const ToolTipView = styled.div<{ $disabled?: boolean }>` @@ -39,7 +39,7 @@ const ToolTipView = styled.div<{ $disabled?: boolean }>` const ToolTip: React.FC = ({ children, control, className, disabled, cursor }) => { return ( - + {control} {children} diff --git a/airbyte-webapp/src/components/base/Button/LoadingButton.tsx b/airbyte-webapp/src/components/base/Button/LoadingButton.tsx index 2c621077d5c4..1681bba49f1a 100644 --- a/airbyte-webapp/src/components/base/Button/LoadingButton.tsx +++ b/airbyte-webapp/src/components/base/Button/LoadingButton.tsx @@ -21,13 +21,13 @@ const SymbolSpinner = styled(FontAwesomeIcon)` position: absolute; left: 50%; animation: ${SpinAnimation} 1.5s linear 0s infinite; - color: ${({ theme }) => theme.primaryColor}; + color: ${({ theme, danger }) => (danger ? theme.dangerColor : theme.primaryColor)}; margin: -1px 0 -3px -9px; `; const ButtonView = styled(Button)` pointer-events: none; - background: ${({ theme }) => theme.primaryColor25}; + background: ${({ theme, danger }) => (danger ? theme.dangerColor25 : theme.primaryColor25)}; border-color: transparent; position: relative; `; @@ -35,14 +35,18 @@ const ButtonView = styled(Button)` const Invisible = styled.div` color: rgba(255, 255, 255, 0); `; - +/* + * TODO: this component need to be refactored - we need to have + * the only one + ); + + const syncNowBtn = ( + startAction({ action: onSync })} + > + {showFeedback ? ( + + ) : ( + <> + + + + )} + + ); + return ( = ({ connection, isStatusUpdating }) {connection.status === ConnectionStatus.active && (
- - startAction({ action: onSync })} - > - {showFeedback ? ( - - ) : ( - <> - - - - )} - + + + + + +
)} @@ -98,15 +130,6 @@ const StatusView: React.FC = ({ connection, isStatusUpdating }) > {jobs.length ? : } />}
- {isModalOpen && ( - setIsModalOpen(false)} - onSubmit={async () => { - await onReset(); - setIsModalOpen(false); - }} - /> - )}
); }; diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.test.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.test.tsx index 7acd4abbcdd7..bf2961bcbac2 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.test.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.test.tsx @@ -8,9 +8,10 @@ import { SourceRead, WebBackendConnectionRead, } from "core/request/AirbyteClient"; +import { ConfirmationModalService } from "hooks/services/ConfirmationModal/ConfirmationModalService"; import { render } from "utils/testutils"; -import ConnectionForm from "./ConnectionForm"; +import ConnectionForm, { ConnectionFormProps } from "./ConnectionForm"; const mockSource: SourceRead = { sourceId: "test-source", @@ -67,13 +68,23 @@ jest.mock("services/workspaces/WorkspacesService", () => { }; }); +const renderConnectionForm = (props: ConnectionFormProps) => + render( + + + + ); + describe("", () => { let container: HTMLElement; describe("edit mode", () => { beforeEach(async () => { - const renderResult = await render( - - ); + const renderResult = await renderConnectionForm({ + onSubmit: jest.fn(), + mode: "edit", + connection: mockConnection, + }); + container = renderResult.container; }); test("it renders relevant items", async () => { @@ -89,9 +100,12 @@ describe("", () => { }); describe("readonly mode", () => { beforeEach(async () => { - const renderResult = await render( - - ); + const renderResult = await renderConnectionForm({ + onSubmit: jest.fn(), + mode: "readonly", + connection: mockConnection, + }); + container = renderResult.container; }); test("it renders only relevant items for the mode", async () => { diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx index 6a10b436b57f..767d07444c24 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx @@ -5,20 +5,15 @@ import styled from "styled-components"; import { ControlLabels, DropDown, DropDownRow, H5, Input, Label } from "components"; import { FormChangeTracker } from "components/FormChangeTracker"; -import ResetDataModal from "components/ResetDataModal"; -import { ModalTypes } from "components/ResetDataModal/types"; +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 { - ConnectionSchedule, - NamespaceDefinitionType, - WebBackendConnectionRead, -} from "../../../core/request/AirbyteClient"; import CreateControls from "./components/CreateControls"; import EditControls from "./components/EditControls"; import { NamespaceDefinitionField } from "./components/NamespaceDefinitionField"; @@ -129,19 +124,30 @@ const ConnectionForm: React.FC = ({ additionalSchemaControl, connection, }) => { + const { openConfirmationModal, closeConfirmationModal } = useConfirmationModalService(); const destDefinition = useGetDestinationDefinitionSpecification(connection.destination.destinationDefinitionId); const { clearFormChange } = useFormChangeTrackerService(); const formId = useUniqueFormId(); - - const [modalIsOpen, setResetModalIsOpen] = useState(false); const [submitError, setSubmitError] = useState(null); - const formatMessage = useIntl().formatMessage; const isEditMode: boolean = mode !== "create"; 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]); + const onFormSubmit = useCallback( async (values: FormikConnectionFormValues, formikHelpers: FormikHelpers) => { const formValues: ConnectionFormValues = connectionValidationSchema.cast(values, { @@ -159,8 +165,9 @@ const ConnectionForm: React.FC = ({ const requiresReset = mode === "edit" && !equal(initialValues.syncCatalog, values.syncCatalog) && !editSchemeMode; + if (requiresReset) { - setResetModalIsOpen(true); + openResetDataModal(); } result?.onSubmitComplete?.(); @@ -177,6 +184,7 @@ const ConnectionForm: React.FC = ({ mode, initialValues.syncCatalog, editSchemeMode, + openResetDataModal, ] ); @@ -351,16 +359,6 @@ const ConnectionForm: React.FC = ({ )} - {modalIsOpen && ( - setResetModalIsOpen(false)} - onSubmit={async () => { - await onReset?.(); - setResetModalIsOpen(false); - }} - /> - )} )}