[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:
parent
c8829d6fcf
commit
c3ff5a2c6b
8 changed files with 180 additions and 49 deletions
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -121,3 +121,8 @@ export const getTopicConfigByParamName = createSelector(
|
||||||
return byParamName;
|
return byParamName;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const getIsTopicInternal = createSelector(
|
||||||
|
getTopicByName,
|
||||||
|
({ internal }) => !!internal
|
||||||
|
);
|
||||||
|
|
Loading…
Add table
Reference in a new issue