[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,33 +64,37 @@ const ListItem: React.FC<ListItemProps> = ({
{internal ? 'Internal' : 'External'}
</div>
</td>
<td>
<div className="has-text-right">
<Dropdown
label={
<span className="icon">
<i className="fas fa-cog" />
</span>
}
right
>
<DropdownItem onClick={clearTopicMessagesHandler}>
<span className="has-text-danger">Clear Messages</span>
</DropdownItem>
<DropdownItem
onClick={() => setDeleteTopicConfirmationVisible(true)}
<td className="topic-action-block">
{!internal ? (
<>
<div className="has-text-right">
<Dropdown
label={
<span className="icon">
<i className="fas fa-cog" />
</span>
}
right
>
<DropdownItem onClick={clearTopicMessagesHandler}>
<span className="has-text-danger">Clear Messages</span>
</DropdownItem>
<DropdownItem
onClick={() => setDeleteTopicConfirmationVisible(true)}
>
<span className="has-text-danger">Remove Topic</span>
</DropdownItem>
</Dropdown>
</div>
<ConfirmationModal
isOpen={isDeleteTopicConfirmationVisible}
onCancel={() => setDeleteTopicConfirmationVisible(false)}
onConfirm={deleteTopicHandler}
>
<span className="has-text-danger">Remove Topic</span>
</DropdownItem>
</Dropdown>
</div>
<ConfirmationModal
isOpen={isDeleteTopicConfirmationVisible}
onCancel={() => setDeleteTopicConfirmationVisible(false)}
onConfirm={deleteTopicHandler}
>
Are you sure want to remove <b>{name}</b> topic?
</ConfirmationModal>
Are you sure want to remove <b>{name}</b> topic?
</ConfirmationModal>
</>
) : null}
</td>
</tr>
);

View file

@ -28,28 +28,31 @@ describe('ListItem', () => {
);
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');
expect(mockDeleteMessages).toBeCalledTimes(1);
expect(mockDeleteMessages).toBeCalledWith(
clusterName,
internalTopicPayload.name
externalTopicPayload.name
);
});
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();
wrapper.find('DropdownItem').at(1).simulate('click');
const modal = wrapper.find('mock-ConfirmationModal');
expect(modal.prop('isOpen')).toBeTruthy();
modal.simulate('confirm');
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', () => {
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();
wrapper.find('DropdownItem').last().simulate('click');
expect(wrapper.find('mock-ConfirmationModal').prop('isOpen')).toBeTruthy();

View file

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

View file

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

View file

@ -75,22 +75,24 @@ const Overview: React.FC<Props> = ({
<td>{offsetMin}</td>
<td>{offsetMax}</td>
<td className="has-text-right">
<Dropdown
label={
<span className="icon">
<i className="fas fa-cog" />
</span>
}
right
>
<DropdownItem
onClick={() =>
clearTopicMessages(clusterName, topicName, [partition])
{!internal ? (
<Dropdown
label={
<span className="icon">
<i className="fas fa-cog" />
</span>
}
right
>
<span className="has-text-danger">Clear Messages</span>
</DropdownItem>
</Dropdown>
<DropdownItem
onClick={() =>
clearTopicMessages(clusterName, topicName, [partition])
}
>
<span className="has-text-danger">Clear Messages</span>
</DropdownItem>
</Dropdown>
) : null}
</td>
</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;
}
);
export const getIsTopicInternal = createSelector(
getTopicByName,
({ internal }) => !!internal
);