[issues-357] - User should not be able to update/delete (#392)

* [issues-357] - User should not be able to update/delete internal topics. Add confirmation for all delete/reset actions

* [issues-357] - User should not be able to update/delete internal topics. Add confirmation for all delete/reset actions

* [issues-357] - User should not be able to update/delete internal topics. Add confirmation for all delete/reset actions

* [issues-357] - User should not be able to update/delete internal topics. Add confirmation for all delete/reset actions

* [issues-357] - User should not be able to update/delete internal topics. Add confirmation for all delete/reset actions

* [issues-357] - User should not be able to update/delete

Co-authored-by: mbovtryuk <mbovtryuk@provectus.com>
This commit is contained in:
TEDMykhailo 2021-05-11 22:53:02 +03:00 committed by GitHub
parent c8829d6fcf
commit c3ff5a2c6b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 180 additions and 49 deletions

View file

@ -64,7 +64,9 @@ const ListItem: React.FC<ListItemProps> = ({
{internal ? 'Internal' : 'External'} {internal ? 'Internal' : 'External'}
</div> </div>
</td> </td>
<td> <td className="topic-action-block">
{!internal ? (
<>
<div className="has-text-right"> <div className="has-text-right">
<Dropdown <Dropdown
label={ label={
@ -91,6 +93,8 @@ const ListItem: React.FC<ListItemProps> = ({
> >
Are you sure want to remove <b>{name}</b> topic? Are you sure want to remove <b>{name}</b> topic?
</ConfirmationModal> </ConfirmationModal>
</>
) : null}
</td> </td>
</tr> </tr>
); );

View file

@ -28,28 +28,31 @@ describe('ListItem', () => {
); );
it('triggers the deleting messages when clicked on the delete messages button', () => { it('triggers the deleting messages when clicked on the delete messages button', () => {
const component = shallow(setupComponent()); const component = shallow(setupComponent({ topic: externalTopicPayload }));
expect(component.exists('.topic-action-block')).toBeTruthy();
component.find('DropdownItem').at(0).simulate('click'); component.find('DropdownItem').at(0).simulate('click');
expect(mockDeleteMessages).toBeCalledTimes(1); expect(mockDeleteMessages).toBeCalledTimes(1);
expect(mockDeleteMessages).toBeCalledWith( expect(mockDeleteMessages).toBeCalledWith(
clusterName, clusterName,
internalTopicPayload.name externalTopicPayload.name
); );
}); });
it('triggers the deleteTopic when clicked on the delete button', () => { it('triggers the deleteTopic when clicked on the delete button', () => {
const wrapper = shallow(setupComponent()); const wrapper = shallow(setupComponent({ topic: externalTopicPayload }));
expect(wrapper.exists('.topic-action-block')).toBeTruthy();
expect(wrapper.find('mock-ConfirmationModal').prop('isOpen')).toBeFalsy(); expect(wrapper.find('mock-ConfirmationModal').prop('isOpen')).toBeFalsy();
wrapper.find('DropdownItem').at(1).simulate('click'); wrapper.find('DropdownItem').at(1).simulate('click');
const modal = wrapper.find('mock-ConfirmationModal'); const modal = wrapper.find('mock-ConfirmationModal');
expect(modal.prop('isOpen')).toBeTruthy(); expect(modal.prop('isOpen')).toBeTruthy();
modal.simulate('confirm'); modal.simulate('confirm');
expect(mockDelete).toBeCalledTimes(1); expect(mockDelete).toBeCalledTimes(1);
expect(mockDelete).toBeCalledWith(clusterName, internalTopicPayload.name); expect(mockDelete).toBeCalledWith(clusterName, externalTopicPayload.name);
}); });
it('closes ConfirmationModal when clicked on the cancel button', () => { it('closes ConfirmationModal when clicked on the cancel button', () => {
const wrapper = shallow(setupComponent()); const wrapper = shallow(setupComponent({ topic: externalTopicPayload }));
expect(wrapper.exists('.topic-action-block')).toBeTruthy();
expect(wrapper.find('mock-ConfirmationModal').prop('isOpen')).toBeFalsy(); expect(wrapper.find('mock-ConfirmationModal').prop('isOpen')).toBeFalsy();
wrapper.find('DropdownItem').last().simulate('click'); wrapper.find('DropdownItem').last().simulate('click');
expect(wrapper.find('mock-ConfirmationModal').prop('isOpen')).toBeTruthy(); expect(wrapper.find('mock-ConfirmationModal').prop('isOpen')).toBeTruthy();

View file

@ -19,6 +19,7 @@ import SettingsContainer from './Settings/SettingsContainer';
interface Props extends Topic, TopicDetails { interface Props extends Topic, TopicDetails {
clusterName: ClusterName; clusterName: ClusterName;
topicName: TopicName; topicName: TopicName;
isInternal: boolean;
deleteTopic: (clusterName: ClusterName, topicName: TopicName) => void; deleteTopic: (clusterName: ClusterName, topicName: TopicName) => void;
clearTopicMessages(clusterName: ClusterName, topicName: TopicName): void; clearTopicMessages(clusterName: ClusterName, topicName: TopicName): void;
} }
@ -26,6 +27,7 @@ interface Props extends Topic, TopicDetails {
const Details: React.FC<Props> = ({ const Details: React.FC<Props> = ({
clusterName, clusterName,
topicName, topicName,
isInternal,
deleteTopic, deleteTopic,
clearTopicMessages, clearTopicMessages,
}) => { }) => {
@ -72,8 +74,8 @@ const Details: React.FC<Props> = ({
</NavLink> </NavLink>
</div> </div>
<div className="navbar-end"> <div className="navbar-end">
{!isReadOnly && !isInternal ? (
<div className="buttons"> <div className="buttons">
{!isReadOnly && (
<> <>
<button <button
type="button" type="button"
@ -105,8 +107,8 @@ const Details: React.FC<Props> = ({
Are you sure want to remove <b>{topicName}</b> topic? Are you sure want to remove <b>{topicName}</b> topic?
</ConfirmationModal> </ConfirmationModal>
</> </>
)}
</div> </div>
) : null}
</div> </div>
</nav> </nav>
<br /> <br />

View file

@ -2,6 +2,7 @@ import { connect } from 'react-redux';
import { ClusterName, RootState, TopicName } from 'redux/interfaces'; import { ClusterName, RootState, TopicName } from 'redux/interfaces';
import { withRouter, RouteComponentProps } from 'react-router-dom'; import { withRouter, RouteComponentProps } from 'react-router-dom';
import { deleteTopic, clearTopicMessages } from 'redux/actions'; import { deleteTopic, clearTopicMessages } from 'redux/actions';
import { getIsTopicInternal } from 'redux/reducers/topics/selectors';
import Details from './Details'; import Details from './Details';
@ -22,6 +23,7 @@ const mapStateToProps = (
) => ({ ) => ({
clusterName, clusterName,
topicName, topicName,
isInternal: getIsTopicInternal(state, topicName),
}); });
const mapDispatchToProps = { const mapDispatchToProps = {

View file

@ -75,6 +75,7 @@ const Overview: React.FC<Props> = ({
<td>{offsetMin}</td> <td>{offsetMin}</td>
<td>{offsetMax}</td> <td>{offsetMax}</td>
<td className="has-text-right"> <td className="has-text-right">
{!internal ? (
<Dropdown <Dropdown
label={ label={
<span className="icon"> <span className="icon">
@ -91,6 +92,7 @@ const Overview: React.FC<Props> = ({
<span className="has-text-danger">Clear Messages</span> <span className="has-text-danger">Clear Messages</span>
</DropdownItem> </DropdownItem>
</Dropdown> </Dropdown>
) : null}
</td> </td>
</tr> </tr>
))} ))}

View file

@ -0,0 +1,42 @@
import React from 'react';
import { shallow } from 'enzyme';
import Overview from 'components/Topics/Topic/Details/Overview/Overview';
describe('Overview', () => {
const mockInternal = false;
const mockClusterName = 'local';
const mockTopicName = 'topic';
const mockClearTopicMessages = jest.fn();
const mockPartitions = [
{
partition: 1,
leader: 1,
replicas: [
{
broker: 1,
leader: false,
inSync: true,
},
],
offsetMax: 0,
offsetMin: 0,
},
];
describe('when it has internal flag', () => {
it('does not render the Action button a Topic', () => {
const component = shallow(
<Overview
name={mockTopicName}
partitions={mockPartitions}
internal={mockInternal}
clusterName={mockClusterName}
topicName={mockTopicName}
clearTopicMessages={mockClearTopicMessages}
/>
);
expect(component.exists('Dropdown')).toBeTruthy();
});
});
});

View file

@ -0,0 +1,71 @@
import React from 'react';
import { mount } from 'enzyme';
import { StaticRouter } from 'react-router-dom';
import ClusterContext from 'components/contexts/ClusterContext';
import Details from 'components/Topics/Topic/Details/Details';
import {
internalTopicPayload,
externalTopicPayload,
} from 'redux/reducers/topics/__test__/fixtures';
describe('Details', () => {
const mockDelete = jest.fn();
const mockClusterName = 'local';
const mockClearTopicMessages = jest.fn();
const mockInternalTopicPayload = internalTopicPayload.internal;
const mockExternalTopicPayload = externalTopicPayload.internal;
describe('when it has readonly flag', () => {
it('does not render the Action button a Topic', () => {
const component = mount(
<StaticRouter>
<ClusterContext.Provider
value={{
isReadOnly: true,
hasKafkaConnectConfigured: true,
hasSchemaRegistryConfigured: true,
}}
>
<Details
clusterName={mockClusterName}
topicName={internalTopicPayload.name}
name={internalTopicPayload.name}
isInternal={mockInternalTopicPayload}
deleteTopic={mockDelete}
clearTopicMessages={mockClearTopicMessages}
/>
</ClusterContext.Provider>
</StaticRouter>
);
expect(component.exists('button')).toBeFalsy();
});
});
describe('when it does not have readonly flag', () => {
it('renders the Action button a Topic', () => {
const component = mount(
<StaticRouter>
<ClusterContext.Provider
value={{
isReadOnly: false,
hasKafkaConnectConfigured: true,
hasSchemaRegistryConfigured: true,
}}
>
<Details
clusterName={mockClusterName}
topicName={internalTopicPayload.name}
name={internalTopicPayload.name}
isInternal={mockExternalTopicPayload}
deleteTopic={mockDelete}
clearTopicMessages={mockClearTopicMessages}
/>
</ClusterContext.Provider>
</StaticRouter>
);
expect(component.exists('button')).toBeTruthy();
});
});
});

View file

@ -121,3 +121,8 @@ export const getTopicConfigByParamName = createSelector(
return byParamName; return byParamName;
} }
); );
export const getIsTopicInternal = createSelector(
getTopicByName,
({ internal }) => !!internal
);