Skip to content

Commit d26e026

Browse files
authored
FE: RBAC: ACL Delete Button is enabled despite no permissions (#1176)
1 parent ff51a99 commit d26e026

File tree

6 files changed

+146
-10
lines changed

6 files changed

+146
-10
lines changed

frontend/src/components/ACLPage/List/List.styled.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@ export const EnumCell = styled.div`
77
`;
88

99
export const DeleteCell = styled.div`
10-
svg {
11-
cursor: pointer;
12-
}
10+
display: flex;
1311
`;
1412

1513
export const Chip = styled.div<{

frontend/src/components/ACLPage/List/List.tsx

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import Search from 'components/common/Search/Search';
2525
import ResourcePageHeading from 'components/common/ResourcePageHeading/ResourcePageHeading';
2626
import BreakableTextCell from 'components/common/NewTable/BreakableTextCell';
2727
import { useQueryPersister } from 'components/common/NewTable/ColumnFilter';
28+
import { ActionPermissionWrapper } from 'components/common/ActionComponent';
2829

2930
import * as S from './List.styled';
3031

@@ -163,13 +164,23 @@ const ACList: React.FC = () => {
163164
// eslint-disable-next-line react/no-unstable-nested-components
164165
cell: ({ row }) => {
165166
return (
166-
<S.DeleteCell onClick={() => handleDeleteClick(row.original)}>
167-
<DeleteIcon
168-
fill={
169-
rowId === row.id ? theme.acl.table.deleteIcon : 'transparent'
170-
}
171-
/>
172-
</S.DeleteCell>
167+
<ActionPermissionWrapper
168+
onAction={() => handleDeleteClick(row.original)}
169+
permission={{
170+
resource: ResourceType.ACL,
171+
action: Action.EDIT,
172+
}}
173+
>
174+
<S.DeleteCell>
175+
<DeleteIcon
176+
fill={
177+
rowId === row.id
178+
? theme.acl.table.deleteIcon
179+
: 'transparent'
180+
}
181+
/>
182+
</S.DeleteCell>
183+
</ActionPermissionWrapper>
173184
);
174185
},
175186
size: 76,

frontend/src/components/common/ActionComponent/ActionComponent.styled.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export const Wrapper = styled.div`
55
flex-direction: row;
66
align-items: center;
77
justify-content: center;
8+
width: min-content;
89
`;
910

1011
export const MessageTooltip = styled.div`
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import React, { ReactElement } from 'react';
2+
import {
3+
ActionComponentProps,
4+
getDefaultActionMessage,
5+
} from 'components/common/ActionComponent/ActionComponent';
6+
import { usePermission } from 'lib/hooks/usePermission';
7+
import { useActionTooltip } from 'lib/hooks/useActionTooltip';
8+
import * as S from 'components/common/ActionComponent/ActionComponent.styled';
9+
10+
interface Props extends ActionComponentProps {
11+
children: ReactElement;
12+
onAction: () => void;
13+
}
14+
15+
const ActionPermissionWrapper: React.FC<Props> = ({
16+
permission,
17+
onAction,
18+
children,
19+
placement,
20+
message = getDefaultActionMessage(),
21+
}) => {
22+
const canDoAction = usePermission(
23+
permission.resource,
24+
permission.action,
25+
permission.value
26+
);
27+
28+
const { x, y, refs, strategy, open } = useActionTooltip(
29+
!canDoAction,
30+
placement
31+
);
32+
33+
return (
34+
<S.Wrapper
35+
ref={refs.setReference}
36+
onClick={() => canDoAction && onAction()}
37+
style={{ cursor: canDoAction ? 'pointer' : 'not-allowed' }}
38+
>
39+
{children}
40+
{open && (
41+
<S.MessageTooltipLimited
42+
ref={refs.setFloating}
43+
style={{
44+
position: strategy,
45+
top: y ?? 0,
46+
left: x ?? 0,
47+
width: 'max-content',
48+
}}
49+
>
50+
{message}
51+
</S.MessageTooltipLimited>
52+
)}
53+
</S.Wrapper>
54+
);
55+
};
56+
57+
export default ActionPermissionWrapper;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import React from 'react';
2+
import { screen } from '@testing-library/react';
3+
import ActionPermissionWrapper from 'components/common/ActionComponent/ActionPermissionWrapper/ActionPermissionWrapper';
4+
import { render } from 'lib/testHelpers';
5+
import { Action, ResourceType } from 'generated-sources';
6+
import { usePermission } from 'lib/hooks/usePermission';
7+
import userEvent from '@testing-library/user-event';
8+
9+
jest.mock('lib/hooks/usePermission', () => ({
10+
usePermission: jest.fn(),
11+
}));
12+
13+
const onActionMock = jest.fn();
14+
15+
const testText = 'test';
16+
const TestComponent = () => <div>{testText}</div>;
17+
18+
describe('ActionPermissionWrapper', () => {
19+
it('children renders', () => {
20+
render(
21+
<ActionPermissionWrapper
22+
permission={{
23+
action: Action.CREATE,
24+
resource: ResourceType.CONNECT,
25+
}}
26+
onAction={onActionMock}
27+
>
28+
<TestComponent />
29+
</ActionPermissionWrapper>
30+
);
31+
expect(screen.getByText(testText)).toBeInTheDocument();
32+
});
33+
34+
it('action calls when allowed', async () => {
35+
(usePermission as jest.Mock).mockImplementation(() => true);
36+
render(
37+
<ActionPermissionWrapper
38+
permission={{
39+
action: Action.CREATE,
40+
resource: ResourceType.CONNECT,
41+
}}
42+
onAction={onActionMock}
43+
>
44+
<TestComponent />
45+
</ActionPermissionWrapper>
46+
);
47+
await userEvent.click(screen.getByText(testText));
48+
expect(onActionMock).toHaveBeenCalledTimes(1);
49+
});
50+
51+
it('action not calls when not allowed', async () => {
52+
(usePermission as jest.Mock).mockImplementation(() => false);
53+
render(
54+
<ActionPermissionWrapper
55+
permission={{
56+
action: Action.CREATE,
57+
resource: ResourceType.CONNECT,
58+
}}
59+
onAction={onActionMock}
60+
>
61+
<TestComponent />
62+
</ActionPermissionWrapper>
63+
);
64+
await userEvent.click(screen.getByText(testText));
65+
expect(onActionMock).not.toHaveBeenCalled();
66+
});
67+
});

frontend/src/components/common/ActionComponent/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import ActionButton from './ActionButton/ActionButton';
33
import ActionCanButton from './ActionButton/ActionCanButton/ActionCanButton';
44
import ActionNavLink from './ActionNavLink/ActionNavLink';
55
import ActionDropdownItem from './ActionDropDownItem/ActionDropdownItem';
6+
import ActionPermissionWrapper from './ActionPermissionWrapper/ActionPermissionWrapper';
67

78
export {
89
ActionSelect,
910
ActionNavLink,
1011
ActionCanButton,
1112
ActionButton,
1213
ActionDropdownItem,
14+
ActionPermissionWrapper,
1315
};

0 commit comments

Comments
 (0)