Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -13,6 +13,10 @@ interface ButtonWithConfirmDialogProps<T, K> {
buttonDisabled?: ButtonProps['disabled'];
buttonView?: ButtonProps['view'];
buttonClassName?: ButtonProps['className'];
withPopover?: boolean;
popoverContent?: PopoverProps['content'];
popoverPlacement?: PopoverProps['placement'];
popoverDisabled?: PopoverProps['disabled'];
}

export function ButtonWithConfirmDialog<T, K>({
Expand All @@ -23,6 +27,10 @@ export function ButtonWithConfirmDialog<T, K>({
buttonDisabled = false,
buttonView = 'action',
buttonClassName,
withPopover = false,
popoverContent,
popoverPlacement = 'right',
popoverDisabled = true,
}: ButtonWithConfirmDialogProps<T, K>) {
const [isConfirmDialogVisible, setIsConfirmDialogVisible] = React.useState(false);
const [buttonLoading, setButtonLoading] = React.useState(false);
Expand Down Expand Up @@ -50,6 +58,36 @@ export function ButtonWithConfirmDialog<T, K>({
setButtonLoading(false);
};

const renderButton = () => {
return (
<Button
onClick={() => setIsConfirmDialogVisible(true)}
view={buttonView}
disabled={buttonDisabled}
loading={!buttonDisabled && buttonLoading}
className={buttonClassName}
>
{children}
</Button>
);
};

const renderContent = () => {
if (withPopover) {
return (
<Popover
content={popoverContent}
placement={popoverPlacement}
disabled={popoverDisabled}
>
{renderButton()}
</Popover>
);
}

return renderButton();
};

return (
<React.Fragment>
<CriticalActionDialog
Expand All @@ -62,15 +100,7 @@ export function ButtonWithConfirmDialog<T, K>({
setIsConfirmDialogVisible(false);
}}
/>
<Button
onClick={() => setIsConfirmDialogVisible(true)}
view={buttonView}
disabled={buttonDisabled}
loading={!buttonDisabled && buttonLoading}
className={buttonClassName}
>
{children}
</Button>
{renderContent()}
</React.Fragment>
);
}
6 changes: 5 additions & 1 deletion src/containers/PDiskPage/PDiskPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -117,9 +118,12 @@ export function PDiskPage() {
<ButtonWithConfirmDialog
onConfirmAction={handleRestart}
onConfirmActionSuccess={handleAfterRestart}
buttonDisabled={!nodeId || !pDiskId}
buttonDisabled={!nodeId || !pDiskId || !isUserAllowedToMakeChanges}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here and farther I think we should add a tooltip with information why is this button disabled.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

buttonView="normal"
dialogContent={pDiskPageKeyset('restart-pdisk-dialog')}
withPopover
popoverContent={pDiskPageKeyset('restart-pdisk-not-allowed')}
popoverDisabled={isUserAllowedToMakeChanges}
>
<Icon data={ArrowRotateLeft} />
{pDiskPageKeyset('restart-pdisk-button')}
Expand Down
3 changes: 2 additions & 1 deletion src/containers/PDiskPage/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
21 changes: 18 additions & 3 deletions src/containers/Tablet/TabletControls/TabletControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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);
};
Expand Down Expand Up @@ -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')}
</ButtonWithConfirmDialog>
Expand All @@ -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')}
</ButtonWithConfirmDialog>
Expand All @@ -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')}
</ButtonWithConfirmDialog>
Expand Down
7 changes: 7 additions & 0 deletions src/containers/Tablet/i18n/en.json
Original file line number Diff line number Diff line change
@@ -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"
}
3 changes: 1 addition & 2 deletions src/containers/Tablet/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -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});
10 changes: 0 additions & 10 deletions src/containers/Tablet/i18n/ru.json

This file was deleted.

7 changes: 6 additions & 1 deletion src/containers/Tablets/Tablets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ const columns: DataTableColumn<TTabletStateInfo & {fqdn?: string}>[] = [
function TabletActions(tablet: TTabletStateInfo) {
const isDisabledRestart = tablet.State === ETabletState.Stopped;
const dispatch = useTypedDispatch();
const {isUserAllowedToMakeChanges} = useTypedSelector((state) => state.authentication);

return (
<ButtonWithConfirmDialog
buttonView="outlined"
Expand All @@ -126,7 +128,10 @@ function TabletActions(tablet: TTabletStateInfo) {
onConfirmActionSuccess={() => {
dispatch(tabletsApi.util.invalidateTags(['All']));
}}
buttonDisabled={isDisabledRestart}
buttonDisabled={isDisabledRestart || !isUserAllowedToMakeChanges}
withPopover
popoverContent={i18n('controls.kill-not-allowed')}
popoverDisabled={isUserAllowedToMakeChanges}
>
<Icon data={ArrowsRotateRight} />
</ButtonWithConfirmDialog>
Expand Down
3 changes: 2 additions & 1 deletion src/containers/Tablets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
6 changes: 5 additions & 1 deletion src/containers/VDiskPage/VDiskPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -129,9 +130,12 @@ export function VDiskPage() {
<ButtonWithConfirmDialog
onConfirmAction={handleEvictVDisk}
onConfirmActionSuccess={handleAfterEvictVDisk}
buttonDisabled={!VDiskId}
buttonDisabled={!VDiskId || !isUserAllowedToMakeChanges}
buttonView="normal"
dialogContent={vDiskPageKeyset('evict-vdisk-dialog')}
withPopover
popoverContent={vDiskPageKeyset('evict-vdisk-not-allowed')}
popoverDisabled={isUserAllowedToMakeChanges}
>
<Icon data={ArrowsOppositeToDots} />
{vDiskPageKeyset('evict-vdisk-button')}
Expand Down
3 changes: 2 additions & 1 deletion src/containers/VDiskPage/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
19 changes: 16 additions & 3 deletions src/store/reducers/authentication/authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,13 @@ const authentication: Reducer<AuthenticationState, AuthenticationAction> = (
return {...state, error: action.error};
}
case FETCH_USER.SUCCESS: {
return {...state, user: action.data};
const {user, isUserAllowedToMakeChanges} = action.data;

return {
...state,
user,
isUserAllowedToMakeChanges,
};
}

default:
Expand All @@ -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,
};
},
});
};
Expand Down
7 changes: 6 additions & 1 deletion src/store/reducers/authentication/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@ import type {FETCH_USER, SET_AUTHENTICATED, SET_UNAUTHENTICATED} from './authent

export interface AuthenticationState {
isAuthenticated: boolean;
isUserAllowedToMakeChanges?: boolean;
user: string | undefined;
error: AuthErrorResponse | undefined;
}

export type AuthenticationAction =
| ApiRequestAction<typeof SET_UNAUTHENTICATED, unknown, unknown>
| ApiRequestAction<typeof SET_AUTHENTICATED, unknown, AuthErrorResponse>
| ApiRequestAction<typeof FETCH_USER, string | undefined, unknown>;
| ApiRequestAction<
typeof FETCH_USER,
{user: string | undefined; isUserAllowedToMakeChanges: boolean},
unknown
>;
7 changes: 7 additions & 0 deletions src/types/api/whoami.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down