diff --git a/src/components/ButtonWithConfirmDialog/ButtonWithConfirmDialog.tsx b/src/components/ButtonWithConfirmDialog/ButtonWithConfirmDialog.tsx index 40ccd0827c..44e3d855a0 100644 --- a/src/components/ButtonWithConfirmDialog/ButtonWithConfirmDialog.tsx +++ b/src/components/ButtonWithConfirmDialog/ButtonWithConfirmDialog.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import {Button} from '@gravity-ui/uikit'; -import type {ButtonProps} from '@gravity-ui/uikit'; +import {Button, Popover} from '@gravity-ui/uikit'; +import type {ButtonProps, PopoverProps} from '@gravity-ui/uikit'; import {CriticalActionDialog} from '../CriticalActionDialog'; @@ -13,6 +13,10 @@ interface ButtonWithConfirmDialogProps { buttonDisabled?: ButtonProps['disabled']; buttonView?: ButtonProps['view']; buttonClassName?: ButtonProps['className']; + withPopover?: boolean; + popoverContent?: PopoverProps['content']; + popoverPlacement?: PopoverProps['placement']; + popoverDisabled?: PopoverProps['disabled']; } export function ButtonWithConfirmDialog({ @@ -23,6 +27,10 @@ export function ButtonWithConfirmDialog({ buttonDisabled = false, buttonView = 'action', buttonClassName, + withPopover = false, + popoverContent, + popoverPlacement = 'right', + popoverDisabled = true, }: ButtonWithConfirmDialogProps) { const [isConfirmDialogVisible, setIsConfirmDialogVisible] = React.useState(false); const [buttonLoading, setButtonLoading] = React.useState(false); @@ -50,6 +58,36 @@ export function ButtonWithConfirmDialog({ setButtonLoading(false); }; + const renderButton = () => { + return ( + + ); + }; + + const renderContent = () => { + if (withPopover) { + return ( + + {renderButton()} + + ); + } + + return renderButton(); + }; + return ( ({ setIsConfirmDialogVisible(false); }} /> - + {renderContent()} ); } diff --git a/src/containers/PDiskPage/PDiskPage.tsx b/src/containers/PDiskPage/PDiskPage.tsx index 18d2bb7622..38b1a99660 100644 --- a/src/containers/PDiskPage/PDiskPage.tsx +++ b/src/containers/PDiskPage/PDiskPage.tsx @@ -29,6 +29,7 @@ export function PDiskPage() { const dispatch = useTypedDispatch(); const nodesMap = useTypedSelector(selectNodesMap); + const {isUserAllowedToMakeChanges} = useTypedSelector((state) => state.authentication); const [{nodeId, pDiskId}] = useQueryParams({ nodeId: StringParam, @@ -117,9 +118,12 @@ export function PDiskPage() { {pDiskPageKeyset('restart-pdisk-button')} diff --git a/src/containers/PDiskPage/i18n/en.json b/src/containers/PDiskPage/i18n/en.json index f876518a1f..074eeda1ef 100644 --- a/src/containers/PDiskPage/i18n/en.json +++ b/src/containers/PDiskPage/i18n/en.json @@ -5,5 +5,6 @@ "node": "Node", "restart-pdisk-button": "Restart PDisk", - "restart-pdisk-dialog": "PDisk will be restarted. Do you want to proceed?" + "restart-pdisk-dialog": "PDisk will be restarted. Do you want to proceed?", + "restart-pdisk-not-allowed": "You don't have enough rights to restart PDisk" } diff --git a/src/containers/Tablet/TabletControls/TabletControls.tsx b/src/containers/Tablet/TabletControls/TabletControls.tsx index ab700b89e3..ea34e2e5ce 100644 --- a/src/containers/Tablet/TabletControls/TabletControls.tsx +++ b/src/containers/Tablet/TabletControls/TabletControls.tsx @@ -3,6 +3,7 @@ import React from 'react'; import {ButtonWithConfirmDialog} from '../../../components/ButtonWithConfirmDialog/ButtonWithConfirmDialog'; import {ETabletState} from '../../../types/api/tablet'; import type {TTabletStateInfo} from '../../../types/api/tablet'; +import {useTypedSelector} from '../../../utils/hooks'; import {b} from '../Tablet'; import i18n from '../i18n'; @@ -14,6 +15,8 @@ interface TabletControlsProps { export const TabletControls = ({tablet, fetchData}: TabletControlsProps) => { const {TabletId, HiveId} = tablet; + const {isUserAllowedToMakeChanges} = useTypedSelector((state) => state.authentication); + const _onKillClick = () => { return window.api.killTablet(TabletId); }; @@ -43,7 +46,11 @@ export const TabletControls = ({tablet, fetchData}: TabletControlsProps) => { onConfirmAction={_onKillClick} onConfirmActionSuccess={fetchData} buttonClassName={b('control')} - buttonDisabled={isDisabledRestart} + buttonDisabled={isDisabledRestart || !isUserAllowedToMakeChanges} + withPopover + popoverContent={i18n('controls.kill-not-allowed')} + popoverPlacement={'bottom'} + popoverDisabled={isUserAllowedToMakeChanges} > {i18n('controls.kill')} @@ -54,7 +61,11 @@ export const TabletControls = ({tablet, fetchData}: TabletControlsProps) => { onConfirmAction={_onStopClick} onConfirmActionSuccess={fetchData} buttonClassName={b('control')} - buttonDisabled={isDisabledStop} + buttonDisabled={isDisabledStop || !isUserAllowedToMakeChanges} + withPopover + popoverContent={i18n('controls.stop-not-allowed')} + popoverPlacement={'bottom'} + popoverDisabled={isUserAllowedToMakeChanges} > {i18n('controls.stop')} @@ -63,7 +74,11 @@ export const TabletControls = ({tablet, fetchData}: TabletControlsProps) => { onConfirmAction={_onResumeClick} onConfirmActionSuccess={fetchData} buttonClassName={b('control')} - buttonDisabled={isDisabledResume} + buttonDisabled={isDisabledResume || !isUserAllowedToMakeChanges} + withPopover + popoverContent={i18n('controls.resume-not-allowed')} + popoverPlacement={'bottom'} + popoverDisabled={isUserAllowedToMakeChanges} > {i18n('controls.resume')} diff --git a/src/containers/Tablet/i18n/en.json b/src/containers/Tablet/i18n/en.json index 903289ec76..2bea6d4638 100644 --- a/src/containers/Tablet/i18n/en.json +++ b/src/containers/Tablet/i18n/en.json @@ -1,10 +1,17 @@ { "tablet.header": "Tablet", + "controls.kill": "Restart", "controls.stop": "Stop", "controls.resume": "Resume", + + "controls.kill-not-allowed": "You don't have enough rights to restart tablet", + "controls.stop-not-allowed": "You don't have enough rights to stop tablet", + "controls.resume-not-allowed": "You don't have enough rights to resume tablet", + "dialog.kill": "The tablet will be restarted. Do you want to proceed?", "dialog.stop": "The tablet will be stopped. Do you want to proceed?", "dialog.resume": "The tablet will be resumed. Do you want to proceed?", + "emptyState": "The tablet was not found" } diff --git a/src/containers/Tablet/i18n/index.ts b/src/containers/Tablet/i18n/index.ts index 966a41f5ad..01be056592 100644 --- a/src/containers/Tablet/i18n/index.ts +++ b/src/containers/Tablet/i18n/index.ts @@ -1,8 +1,7 @@ import {registerKeysets} from '../../../utils/i18n'; import en from './en.json'; -import ru from './ru.json'; const COMPONENT = 'ydb-tablet-page'; -export default registerKeysets(COMPONENT, {en, ru}); +export default registerKeysets(COMPONENT, {en}); diff --git a/src/containers/Tablet/i18n/ru.json b/src/containers/Tablet/i18n/ru.json deleted file mode 100644 index 57b3c376ca..0000000000 --- a/src/containers/Tablet/i18n/ru.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "tablet.header": "Таблетка", - "controls.kill": "Перезапустить", - "controls.stop": "Остановить", - "controls.resume": "Запустить", - "dialog.kill": "Таблетка будет перезапущена. Вы хотите продолжить?", - "dialog.stop": "Таблетка будет остановлена. Вы хотите продолжить?", - "dialog.resume": "Таблетка будет запущена. Вы хотите продолжить?", - "emptyState": "Таблетка не найдена" -} diff --git a/src/containers/Tablets/Tablets.tsx b/src/containers/Tablets/Tablets.tsx index 89a616f4c9..b3b84bd7fe 100644 --- a/src/containers/Tablets/Tablets.tsx +++ b/src/containers/Tablets/Tablets.tsx @@ -116,6 +116,8 @@ const columns: DataTableColumn[] = [ function TabletActions(tablet: TTabletStateInfo) { const isDisabledRestart = tablet.State === ETabletState.Stopped; const dispatch = useTypedDispatch(); + const {isUserAllowedToMakeChanges} = useTypedSelector((state) => state.authentication); + return ( { dispatch(tabletsApi.util.invalidateTags(['All'])); }} - buttonDisabled={isDisabledRestart} + buttonDisabled={isDisabledRestart || !isUserAllowedToMakeChanges} + withPopover + popoverContent={i18n('controls.kill-not-allowed')} + popoverDisabled={isUserAllowedToMakeChanges} > diff --git a/src/containers/Tablets/i18n/en.json b/src/containers/Tablets/i18n/en.json index e0a6c9c404..5065f7357e 100644 --- a/src/containers/Tablets/i18n/en.json +++ b/src/containers/Tablets/i18n/en.json @@ -7,5 +7,6 @@ "Node FQDN": "Node FQDN", "Generation": "Generation", "Uptime": "Uptime", - "dialog.kill": "The tablet will be restarted. Do you want to proceed?" + "dialog.kill": "The tablet will be restarted. Do you want to proceed?", + "controls.kill-not-allowed": "You don't have enough rights to restart tablet" } diff --git a/src/containers/VDiskPage/VDiskPage.tsx b/src/containers/VDiskPage/VDiskPage.tsx index 1416a68880..8ddd92c71f 100644 --- a/src/containers/VDiskPage/VDiskPage.tsx +++ b/src/containers/VDiskPage/VDiskPage.tsx @@ -33,6 +33,7 @@ export function VDiskPage() { const dispatch = useTypedDispatch(); const nodesMap = useTypedSelector(selectNodesMap); + const {isUserAllowedToMakeChanges} = useTypedSelector((state) => state.authentication); const [{nodeId, pDiskId, vDiskSlotId}] = useQueryParams({ nodeId: StringParam, @@ -129,9 +130,12 @@ export function VDiskPage() { {vDiskPageKeyset('evict-vdisk-button')} diff --git a/src/containers/VDiskPage/i18n/en.json b/src/containers/VDiskPage/i18n/en.json index 581fcd8cce..aec351f641 100644 --- a/src/containers/VDiskPage/i18n/en.json +++ b/src/containers/VDiskPage/i18n/en.json @@ -6,5 +6,6 @@ "group": "Group", "evict-vdisk-button": "Evict VDisk", - "evict-vdisk-dialog": "VDisk will be evicted. Do you want to proceed?" + "evict-vdisk-dialog": "VDisk will be evicted. Do you want to proceed?", + "evict-vdisk-not-allowed": "You don't have enough rights to evict VDisk" } diff --git a/src/store/reducers/authentication/authentication.ts b/src/store/reducers/authentication/authentication.ts index b5cc210184..4e4250d736 100644 --- a/src/store/reducers/authentication/authentication.ts +++ b/src/store/reducers/authentication/authentication.ts @@ -32,7 +32,13 @@ const authentication: Reducer = ( return {...state, error: action.error}; } case FETCH_USER.SUCCESS: { - return {...state, user: action.data}; + const {user, isUserAllowedToMakeChanges} = action.data; + + return { + ...state, + user, + isUserAllowedToMakeChanges, + }; } default: @@ -59,8 +65,15 @@ export const getUser = () => { request: window.api.whoami(), actions: FETCH_USER, dataHandler: (data) => { - const {UserSID, AuthType} = data; - return AuthType === 'Login' ? UserSID : undefined; + const {UserSID, AuthType, IsMonitoringAllowed} = data; + return { + user: AuthType === 'Login' ? UserSID : undefined, + // If ydb version supports this feature, + // There should be explicit flag in whoami response + // Otherwise every user is allowed to make changes + // Anyway there will be guards on backend + isUserAllowedToMakeChanges: IsMonitoringAllowed !== false, + }; }, }); }; diff --git a/src/store/reducers/authentication/types.ts b/src/store/reducers/authentication/types.ts index 5aba2838b9..df241d67e6 100644 --- a/src/store/reducers/authentication/types.ts +++ b/src/store/reducers/authentication/types.ts @@ -5,6 +5,7 @@ import type {FETCH_USER, SET_AUTHENTICATED, SET_UNAUTHENTICATED} from './authent export interface AuthenticationState { isAuthenticated: boolean; + isUserAllowedToMakeChanges?: boolean; user: string | undefined; error: AuthErrorResponse | undefined; } @@ -12,4 +13,8 @@ export interface AuthenticationState { export type AuthenticationAction = | ApiRequestAction | ApiRequestAction - | ApiRequestAction; + | ApiRequestAction< + typeof FETCH_USER, + {user: string | undefined; isUserAllowedToMakeChanges: boolean}, + unknown + >; diff --git a/src/types/api/whoami.ts b/src/types/api/whoami.ts index 88cb0aab5a..9b5cd52042 100644 --- a/src/types/api/whoami.ts +++ b/src/types/api/whoami.ts @@ -8,6 +8,13 @@ export interface TUserToken { GroupSIDs?: TProtoHashTable; OriginalUserToken?: string; AuthType?: string; + + /** Is user allowed to view data */ + IsViewerAllowed?: boolean; + /** Is user allowed to view deeper and make simple changes */ + IsMonitoringAllowed?: boolean; + /** Is user allowed to do unrestricted changes in the system */ + IsAdministrationAllowed?: boolean; } interface TProtoHashTable {