Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed reverted PR: Fix cancel button when it doesn't provide feedback to the user + UX improvements #13388

Merged
merged 19 commits into from
Jun 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
5b0ebc9
turn on "curly" rule
dizel852 May 20, 2022
88b9d61
set a loading state for 'Cancel' button if user clicked
dizel852 May 3, 2022
e50ac47
extend ToolTip component to support 'not-allowed' cursor
dizel852 May 3, 2022
14f5f20
disable 'Reset data' and 'Sync now' buttons if sync is in 'pending' o…
dizel852 May 3, 2022
0228d48
set loading state and disable buttons in ResetDataModal if reset proc…
dizel852 May 3, 2022
b5ceaa7
extend ToolTip component - shoe desired cursor type when tooltip is a…
dizel852 May 6, 2022
2c0402c
refactored: control tooltips showing this props
dizel852 May 6, 2022
97fdbb5
replace functions call in jsx with components
dizel852 May 6, 2022
ba51478
add style for loading 'danger' button
dizel852 May 9, 2022
f82e1c0
minor improvements - use ternary operator conditionally setting the d…
dizel852 May 9, 2022
435c0d2
set loading state to 'false' if async action has failed
dizel852 May 10, 2022
932f56d
extend ConfirmationModal - to support optional cancelButtonText prop
dizel852 May 10, 2022
22fb670
extend useLoadingState component - to show an error notification if a…
dizel852 May 10, 2022
b46a8e2
show notification message on top
dizel852 May 10, 2022
40597fb
replace using ResetData modal with default text with useConfirmationM…
dizel852 May 9, 2022
99dc460
replace ResetDataModal (refresh schema) with useConfirmationModalService
dizel852 May 10, 2022
6fa7422
remove obsolete ResetDataModal component
dizel852 May 10, 2022
5d41409
updated tests
dizel852 May 10, 2022
30fc6e3
replace ResetDataModal (changed column) with useConfirmationModalService
dizel852 May 10, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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