diff --git a/src/components/ButtonWithConfirmDialog/ButtonWithConfirmDialog.tsx b/src/components/ButtonWithConfirmDialog/ButtonWithConfirmDialog.tsx new file mode 100644 index 0000000000..f0a89e4018 --- /dev/null +++ b/src/components/ButtonWithConfirmDialog/ButtonWithConfirmDialog.tsx @@ -0,0 +1,75 @@ +import {useState, type ReactNode} from 'react'; + +import {Button, type ButtonProps} from '@gravity-ui/uikit'; + +import {CriticalActionDialog} from '../CriticalActionDialog'; + +interface ButtonWithConfirmDialogProps { + children: ReactNode; + onConfirmAction: () => Promise; + onConfirmActionSuccess?: (() => Promise) | VoidFunction; + dialogContent: string; + buttonDisabled?: ButtonProps['disabled']; + buttonView?: ButtonProps['view']; + buttonClassName?: ButtonProps['className']; +} + +export function ButtonWithConfirmDialog({ + children, + onConfirmAction, + onConfirmActionSuccess, + dialogContent, + buttonDisabled = false, + buttonView = 'action', + buttonClassName, +}: ButtonWithConfirmDialogProps) { + const [isConfirmDialogVisible, setIsConfirmDialogVisible] = useState(false); + const [buttonLoading, setButtonLoading] = useState(false); + + const handleConfirmAction = async () => { + setButtonLoading(true); + await onConfirmAction(); + setButtonLoading(false); + }; + + const handleConfirmActionSuccess = async () => { + if (onConfirmActionSuccess) { + setButtonLoading(true); + + try { + await onConfirmActionSuccess(); + } catch { + } finally { + setButtonLoading(false); + } + } + }; + + const handleConfirmActionError = () => { + setButtonLoading(false); + }; + + return ( + <> + { + setIsConfirmDialogVisible(false); + }} + /> + + + ); +} diff --git a/src/components/CriticalActionDialog/CriticalActionDialog.scss b/src/components/CriticalActionDialog/CriticalActionDialog.scss index 83fa09059f..f98f0cc3e1 100644 --- a/src/components/CriticalActionDialog/CriticalActionDialog.scss +++ b/src/components/CriticalActionDialog/CriticalActionDialog.scss @@ -1,32 +1,21 @@ .ydb-critical-dialog { - width: 252px !important; + width: 400px; &__warning-icon { margin-right: 16px; } + &__error-icon { + height: 24px; + margin-right: 16px; + + color: var(--ydb-color-status-red); + } + &__body { display: flex; align-items: center; padding: 16px 16px 0; } - - & .yc-dialog-footer { - padding: 20px 4px 4px; - } - - & .yc-dialog-footer__children { - display: none; - } - - & .yc-dialog-footer__button { - box-sizing: border-box; - min-width: 120px; - margin: 0; - } - - & .yc-dialog-footer__button_action_cancel { - margin-right: 4px; - } } diff --git a/src/components/CriticalActionDialog/CriticalActionDialog.tsx b/src/components/CriticalActionDialog/CriticalActionDialog.tsx index c62f1ca021..fb20406dff 100644 --- a/src/components/CriticalActionDialog/CriticalActionDialog.tsx +++ b/src/components/CriticalActionDialog/CriticalActionDialog.tsx @@ -1,43 +1,87 @@ import {FormEvent, useState} from 'react'; import cn from 'bem-cn-lite'; import {Dialog} from '@gravity-ui/uikit'; +import {CircleXmarkFill} from '@gravity-ui/icons'; +import type {IResponseError} from '../../types/api/error'; import {Icon} from '../Icon'; +import {criticalActionDialogKeyset} from './i18n'; import './CriticalActionDialog.scss'; const b = cn('ydb-critical-dialog'); -interface CriticalActionDialogProps { +const parseError = (error: IResponseError) => { + if (error.status === 403) { + return criticalActionDialogKeyset('no-rights-error'); + } + if (error.statusText) { + return error.statusText; + } + + return criticalActionDialogKeyset('default-error'); +}; + +interface CriticalActionDialogProps { visible: boolean; text: string; onClose: VoidFunction; - onConfirm: () => Promise; - onConfirmActionFinish: VoidFunction; + onConfirm: () => Promise; + onConfirmActionSuccess: VoidFunction; + onConfirmActionError: VoidFunction; } -export const CriticalActionDialog = ({ +export function CriticalActionDialog({ visible, text, onClose, onConfirm, - onConfirmActionFinish, -}: CriticalActionDialogProps) => { + onConfirmActionSuccess, + onConfirmActionError, +}: CriticalActionDialogProps) { const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(); const onSubmit = async (e: FormEvent) => { e.preventDefault(); setIsLoading(true); - return onConfirm().then(() => { - onConfirmActionFinish(); - setIsLoading(false); - onClose(); - }); + return onConfirm() + .then(() => { + onConfirmActionSuccess(); + onClose(); + }) + .catch((err) => { + onConfirmActionError(); + setError(err); + }) + .finally(() => { + setIsLoading(false); + }); }; - return ( - + const renderDialogContent = () => { + if (error) { + return ( + <> + + + + + {parseError(error)} + + + + + ); + } + + return (
@@ -49,13 +93,26 @@ export const CriticalActionDialog = ({ {}} /> + ); + }; + + return ( + setError(undefined)} + > + {renderDialogContent()} ); -}; +} diff --git a/src/components/CriticalActionDialog/i18n/en.json b/src/components/CriticalActionDialog/i18n/en.json new file mode 100644 index 0000000000..424202683b --- /dev/null +++ b/src/components/CriticalActionDialog/i18n/en.json @@ -0,0 +1,8 @@ +{ + "default-error": "Something went wrong, action cannot be completed", + "no-rights-error": "You don't have enough rights to complete the operation", + + "button-confirm": "Confirm", + "button-cancel": "Cancel", + "button-close": "Close" +} diff --git a/src/components/CriticalActionDialog/i18n/index.ts b/src/components/CriticalActionDialog/i18n/index.ts new file mode 100644 index 0000000000..200efd7de4 --- /dev/null +++ b/src/components/CriticalActionDialog/i18n/index.ts @@ -0,0 +1,7 @@ +import {registerKeysets} from '../../../utils/i18n'; + +import en from './en.json'; + +const COMPONENT = 'ydb-critical-action-dialog'; + +export const criticalActionDialogKeyset = registerKeysets(COMPONENT, {en}); diff --git a/src/containers/Tablet/Tablet.tsx b/src/containers/Tablet/Tablet.tsx index 9bd1bdc42c..7c4c99851a 100644 --- a/src/containers/Tablet/Tablet.tsx +++ b/src/containers/Tablet/Tablet.tsx @@ -73,7 +73,7 @@ export const Tablet = () => { }, [dispatch, tablet]); const fetchData = useCallback(() => { - dispatch(getTablet(id)); + return dispatch(getTablet(id)); }, [dispatch, id]); useAutofetcher(fetchData, [fetchData], true); diff --git a/src/containers/Tablet/TabletControls/TabletControls.tsx b/src/containers/Tablet/TabletControls/TabletControls.tsx index db664e6102..5a2527cfd2 100644 --- a/src/containers/Tablet/TabletControls/TabletControls.tsx +++ b/src/containers/Tablet/TabletControls/TabletControls.tsx @@ -1,61 +1,25 @@ -import {useEffect, useState} from 'react'; -import {Button} from '@gravity-ui/uikit'; - -import {ETabletState, TTabletStateInfo} from '../../../types/api/tablet'; -import {CriticalActionDialog} from '../../../components/CriticalActionDialog'; +import {ETabletState, type TTabletStateInfo} from '../../../types/api/tablet'; +import type {ITabletHandledResponse} from '../../../types/store/tablet'; +import {ButtonWithConfirmDialog} from '../../../components/ButtonWithConfirmDialog/ButtonWithConfirmDialog'; import i18n from '../i18n'; import {b} from '../Tablet'; -enum EVisibleDialogType { - 'kill' = 'kill', - 'stop' = 'stop', - 'resume' = 'resume', -} - -type VisibleDialogType = EVisibleDialogType | null; - interface TabletControlsProps { tablet: TTabletStateInfo; - fetchData: VoidFunction; + fetchData: () => Promise; } export const TabletControls = ({tablet, fetchData}: TabletControlsProps) => { const {TabletId, HiveId} = tablet; - const [isDialogVisible, setIsDialogVisible] = useState(false); - const [visibleDialogType, setVisibleDialogType] = useState(null); - const [isTabletActionLoading, setIsTabletActionLoading] = useState(false); - - // Enable controls after data update - useEffect(() => { - setIsTabletActionLoading(false); - }, [tablet]); - - const makeShowDialog = (type: VisibleDialogType) => () => { - setIsDialogVisible(true); - setVisibleDialogType(type); - }; - - const showKillDialog = makeShowDialog(EVisibleDialogType.kill); - const showStopDialog = makeShowDialog(EVisibleDialogType.stop); - const showResumeDialog = makeShowDialog(EVisibleDialogType.resume); - - const hideDialog = () => { - setIsDialogVisible(false); - setVisibleDialogType(null); - }; - const _onKillClick = () => { - setIsTabletActionLoading(true); return window.api.killTablet(TabletId); }; const _onStopClick = () => { - setIsTabletActionLoading(true); return window.api.stopTablet(TabletId, HiveId); }; const _onResumeClick = () => { - setIsTabletActionLoading(true); return window.api.resumeTablet(TabletId, HiveId); }; @@ -69,83 +33,38 @@ export const TabletControls = ({tablet, fetchData}: TabletControlsProps) => { const isDisabledStop = tablet.State === ETabletState.Stopped || tablet.State === ETabletState.Deleted; - const renderDialog = () => { - if (!isDialogVisible) { - return null; - } - - switch (visibleDialogType) { - case EVisibleDialogType.kill: { - return ( - - ); - } - case EVisibleDialogType.stop: { - return ( - - ); - } - case EVisibleDialogType.resume: { - return ( - - ); - } - default: - return null; - } - }; - return (
- + {hasHiveId() ? ( <> - - + ) : null} - {renderDialog()}
); };