Skip to content

Commit

Permalink
Fixed reverted PR: Fix cancel button when it doesn't provide feedback…
Browse files Browse the repository at this point in the history
… 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
  • Loading branch information
dizel852 authored Jun 3, 2022
1 parent 75e4114 commit bdeadfc
Show file tree
Hide file tree
Showing 17 changed files with 206 additions and 217 deletions.
1 change: 1 addition & 0 deletions airbyte-webapp/.eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
}
},
"rules": {
"curly": "error",
"prettier/prettier": "error",
"unused-imports/no-unused-imports": "error",
"import/order": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,6 +33,7 @@ export interface ConfirmationModalProps {
submitButtonText: string;
onSubmit: () => void;
submitButtonDataId?: string;
cancelButtonText?: string;
}

export const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
Expand All @@ -39,18 +43,24 @@ export const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
onSubmit,
submitButtonText,
submitButtonDataId,
}) => (
<Modal onClose={onClose} title={<FormattedMessage id={title} />}>
<Content>
<FormattedMessage id={text} />
<ButtonContent>
<ButtonWithMargin onClick={onClose} type="button" secondary>
<FormattedMessage id="form.cancel" />
</ButtonWithMargin>
<Button type="button" danger onClick={onSubmit} data-id={submitButtonDataId}>
<FormattedMessage id={submitButtonText} />
</Button>
</ButtonContent>
</Content>
</Modal>
);
cancelButtonText,
}) => {
const { isLoading, startAction } = useLoadingState();
const onSubmitBtnClick = () => startAction({ action: () => onSubmit() });

return (
<Modal onClose={onClose} title={<FormattedMessage id={title} />}>
<Content>
<FormattedMessage id={text} />
<ButtonContent>
<ButtonWithMargin onClick={onClose} type="button" secondary disabled={isLoading}>
<FormattedMessage id={cancelButtonText ?? "form.cancel"} />
</ButtonWithMargin>
<LoadingButton danger onClick={onSubmitBtnClick} data-id={submitButtonDataId} isLoading={isLoading}>
<FormattedMessage id={submitButtonText} />
</LoadingButton>
</ButtonContent>
</Content>
</Modal>
);
};
25 changes: 17 additions & 8 deletions airbyte-webapp/src/components/JobItem/components/MainInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -105,11 +106,13 @@ const MainInfo: React.FC<MainInfoProps> = ({
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);
Expand Down Expand Up @@ -152,8 +155,14 @@ const MainInfo: React.FC<MainInfoProps> = ({
</InfoCell>
<InfoCell>
{!shortInfo && isNotCompleted && (
<CancelButton secondary onClick={onCancelJob}>
<FormattedMessage id="form.cancel" />
<CancelButton
secondary
disabled={isLoading}
isLoading={isLoading}
wasActive={showFeedback}
onClick={onCancelJob}
>
<FormattedMessage id={showFeedback ? "form.canceling" : "form.cancel"} />
</CancelButton>
)}
<FormattedTimeParts value={getJobCreatedAt(job) * 1000} hour="numeric" minute="2-digit">
Expand Down
82 changes: 0 additions & 82 deletions airbyte-webapp/src/components/ResetDataModal/ResetDataModal.tsx

This file was deleted.

3 changes: 0 additions & 3 deletions airbyte-webapp/src/components/ResetDataModal/index.tsx

This file was deleted.

5 changes: 0 additions & 5 deletions airbyte-webapp/src/components/ResetDataModal/types.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 4 additions & 4 deletions airbyte-webapp/src/components/ToolTip/ToolTip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }>`
Expand Down Expand Up @@ -39,7 +39,7 @@ const ToolTipView = styled.div<{ $disabled?: boolean }>`

const ToolTip: React.FC<ToolTipProps> = ({ children, control, className, disabled, cursor }) => {
return (
<Control $cursor={cursor}>
<Control $cursor={cursor} $showCursor={!disabled}>
{control}
<ToolTipView className={className} $disabled={disabled}>
{children}
Expand Down
12 changes: 8 additions & 4 deletions airbyte-webapp/src/components/base/Button/LoadingButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,28 +21,32 @@ const SymbolSpinner = styled(FontAwesomeIcon)<ButtonProps>`
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)<ButtonProps>`
pointer-events: none;
background: ${({ theme }) => theme.primaryColor25};
background: ${({ theme, danger }) => (danger ? theme.dangerColor25 : theme.primaryColor25)};
border-color: transparent;
position: relative;
`;

const Invisible = styled.div`
color: rgba(255, 255, 255, 0);
`;

/*
* TODO: this component need to be refactored - we need to have
* the only one <Button/> component and set loading state via props
* issue: https://github.com/airbytehq/airbyte/issues/12705
* */
const LoadingButton: React.FC<ButtonProps> = (props) => {
if (props.isLoading) {
return (
<ButtonView {...props}>
{props.isLoading ? (
<>
<SymbolSpinner icon={faCircleNotch} />
<SymbolSpinner icon={faCircleNotch} danger={props.danger} />
<Invisible>{props.children}</Invisible>
</>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export const ConfirmationModalService = ({ children }: { children: React.ReactNo
onSubmit={state.confirmationModal.onSubmit}
submitButtonText={state.confirmationModal.submitButtonText}
submitButtonDataId={state.confirmationModal.submitButtonDataId}
cancelButtonText={state.confirmationModal.cancelButtonText}
/>
) : null}
</>
Expand Down
42 changes: 31 additions & 11 deletions airbyte-webapp/src/hooks/useLoadingState.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,48 @@
import { useState } from "react";
import { useIntl } from "react-intl";

import { useNotificationService } from "./services/Notification";

const useLoadingState = (): {
isLoading: boolean;
startAction: ({ action, feedbackAction }: { action: () => void; feedbackAction?: () => void }) => Promise<void>;
showFeedback: boolean;
} => {
const { formatMessage } = useIntl();
const { registerNotification } = useNotificationService();
const [isLoading, setIsLoading] = useState(false);
const [showFeedback, setShowFeedback] = useState(false);

const errorNotificationId = "error.somethingWentWrong";
const errorNotification = (message: string) => ({
isError: true,
title: formatMessage({ id: `notifications.${errorNotificationId}` }),
text: message,
id: errorNotificationId,
});

const startAction = async ({ action, feedbackAction }: { action: () => void; feedbackAction?: () => void }) => {
setIsLoading(true);
setShowFeedback(false);
try {
setIsLoading(true);
setShowFeedback(false);

await action();
await action();

setIsLoading(false);
setShowFeedback(true);
setIsLoading(false);
setShowFeedback(true);

setTimeout(() => {
setShowFeedback(false);
if (feedbackAction) {
feedbackAction();
}
}, 2000);
setTimeout(() => {
setShowFeedback(false);
if (feedbackAction) {
feedbackAction();
}
}, 2000);
} catch (error) {
const message = error?.message || formatMessage({ id: "notifications.error.noMessage" });

setIsLoading(false);
registerNotification(errorNotification(message));
}
};

return { isLoading, showFeedback, startAction };
Expand Down
3 changes: 3 additions & 0 deletions airbyte-webapp/src/locales/en.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
{
"webapp.cannotReachServer": "Cannot reach server. The server may still be starting up.",
"notifications.error.health": "Cannot reach server",
"notifications.error.somethingWentWrong": "Something went wrong",
"notifications.error.noMessage": "No error message",

"sidebar.sources": "Sources",
"sidebar.destinations": "Destinations",
Expand Down Expand Up @@ -50,6 +52,7 @@
"form.dataSync.readonly": "Activated streams",
"form.dataSync.message": "Don’t worry! You’ll be able to change this later on.",
"form.cancel": "Cancel",
"form.canceling": "Canceling",
"form.submit": "Submit",
"form.delete": "Delete",
"form.change": "Change",
Expand Down
8 changes: 6 additions & 2 deletions airbyte-webapp/src/packages/firebaseReact/sdk.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,15 @@ type FirebaseSdks = Auth;

function getSdkProvider<Sdk extends FirebaseSdks>(SdkContext: React.Context<Sdk | undefined>) {
return function SdkProvider(props: React.PropsWithChildren<{ sdk: Sdk }>) {
if (!props.sdk) throw new Error("no sdk provided");
if (!props.sdk) {
throw new Error("no sdk provided");
}

const contextualAppName = useFirebaseApp().name;
const sdkAppName = props?.sdk?.app?.name;
if (sdkAppName !== contextualAppName) throw new Error("sdk was initialized with a different firebase app");
if (sdkAppName !== contextualAppName) {
throw new Error("sdk was initialized with a different firebase app");
}

return <SdkContext.Provider value={props.sdk} {...props} />;
};
Expand Down
Loading

0 comments on commit bdeadfc

Please sign in to comment.