浏览代码

[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>
TEDMykhailo 4 年之前
父节点
当前提交
c3ff5a2c6b

+ 30 - 26
kafka-ui-react-app/src/components/Topics/List/ListItem.tsx

@@ -64,33 +64,37 @@ const ListItem: React.FC<ListItemProps> = ({
           {internal ? 'Internal' : 'External'}
           {internal ? 'Internal' : 'External'}
         </div>
         </div>
       </td>
       </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>
       </td>
     </tr>
     </tr>
   );
   );

+ 8 - 5
kafka-ui-react-app/src/components/Topics/List/__tests__/ListItem.spec.tsx

@@ -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();

+ 6 - 4
kafka-ui-react-app/src/components/Topics/Topic/Details/Details.tsx

@@ -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">
-          <div className="buttons">
-            {!isReadOnly && (
+          {!isReadOnly && !isInternal ? (
+            <div className="buttons">
               <>
               <>
                 <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 - 0
kafka-ui-react-app/src/components/Topics/Topic/Details/DetailsContainer.ts

@@ -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 = {

+ 16 - 14
kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/Overview.tsx

@@ -75,22 +75,24 @@ 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">
-                <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>
               </td>
             </tr>
             </tr>
           ))}
           ))}

+ 42 - 0
kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/__test__/Overview.spec.tsx

@@ -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();
+    });
+  });
+});

+ 71 - 0
kafka-ui-react-app/src/components/Topics/Topic/Details/__test__/Details.spec.tsx

@@ -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();
+    });
+  });
+});

+ 5 - 0
kafka-ui-react-app/src/redux/reducers/topics/selectors.ts

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