瀏覽代碼

#219 ReadOnly flag (#231)

* Implement read-only flag logic

* Test new functionality
Alexander Krivonosov 4 年之前
父節點
當前提交
1188ce9bc2

+ 4 - 0
kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClusterWidget.tsx

@@ -17,6 +17,7 @@ const ClusterWidget: React.FC<ClusterWidgetProps> = ({
     bytesInPerSec,
     bytesInPerSec,
     bytesOutPerSec,
     bytesOutPerSec,
     onlinePartitionCount,
     onlinePartitionCount,
+    readOnly,
   },
   },
 }) => (
 }) => (
   <div className="column is-full-modile is-6">
   <div className="column is-full-modile is-6">
@@ -29,6 +30,9 @@ const ClusterWidget: React.FC<ClusterWidgetProps> = ({
         >
         >
           {status}
           {status}
         </div>
         </div>
+        {readOnly && (
+          <div className="tag has-margin-right is-info is-light">readonly</div>
+        )}
         {name}
         {name}
       </div>
       </div>
 
 

+ 13 - 0
kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/ClusterWidget.spec.tsx

@@ -70,4 +70,17 @@ describe('ClusterWidget', () => {
       ).toMatchSnapshot();
       ).toMatchSnapshot();
     });
     });
   });
   });
+
+  describe('when cluster is read-only', () => {
+    it('renders the tag', () => {
+      expect(
+        shallow(
+          <ClusterWidget cluster={{ ...onlineCluster, readOnly: true }} />
+        )
+          .find('.title')
+          .childAt(1)
+          .text()
+      ).toEqual('readonly');
+    });
+  });
 });
 });

+ 22 - 18
kafka-ui-react-app/src/components/Schemas/Details/Details.tsx

@@ -2,6 +2,7 @@ import React from 'react';
 import { SchemaSubject } from 'generated-sources';
 import { SchemaSubject } from 'generated-sources';
 import { ClusterName, SchemaName } from 'redux/interfaces';
 import { ClusterName, SchemaName } from 'redux/interfaces';
 import { clusterSchemasPath } from 'lib/paths';
 import { clusterSchemasPath } from 'lib/paths';
+import ClusterContext from 'components/contexts/ClusterContext';
 import Breadcrumb from '../../common/Breadcrumb/Breadcrumb';
 import Breadcrumb from '../../common/Breadcrumb/Breadcrumb';
 import SchemaVersion from './SchemaVersion';
 import SchemaVersion from './SchemaVersion';
 import LatestVersionItem from './LatestVersionItem';
 import LatestVersionItem from './LatestVersionItem';
@@ -25,6 +26,7 @@ const Details: React.FC<DetailsProps> = ({
   versions,
   versions,
   isFetched,
   isFetched,
 }) => {
 }) => {
+  const { isReadOnly } = React.useContext(ClusterContext);
   React.useEffect(() => {
   React.useEffect(() => {
     fetchSchemaVersions(clusterName, schema.subject as SchemaName);
     fetchSchemaVersions(clusterName, schema.subject as SchemaName);
   }, [fetchSchemaVersions, clusterName]);
   }, [fetchSchemaVersions, clusterName]);
@@ -54,24 +56,26 @@ const Details: React.FC<DetailsProps> = ({
               </div>
               </div>
             </div>
             </div>
           </div>
           </div>
-          <div className="level-right">
-            <button
-              className="button is-warning is-small level-item"
-              type="button"
-              title="in development"
-              disabled
-            >
-              Update Schema
-            </button>
-            <button
-              className="button is-danger is-small level-item"
-              type="button"
-              title="in development"
-              disabled
-            >
-              Delete
-            </button>
-          </div>
+          {!isReadOnly && (
+            <div className="level-right">
+              <button
+                className="button is-warning is-small level-item"
+                type="button"
+                title="in development"
+                disabled
+              >
+                Update Schema
+              </button>
+              <button
+                className="button is-danger is-small level-item"
+                type="button"
+                title="in development"
+                disabled
+              >
+                Delete
+              </button>
+            </div>
+          )}
         </div>
         </div>
         <LatestVersionItem schema={schema} />
         <LatestVersionItem schema={schema} />
       </div>
       </div>

+ 17 - 1
kafka-ui-react-app/src/components/Schemas/Details/__test__/Details.spec.tsx

@@ -1,7 +1,9 @@
 import React from 'react';
 import React from 'react';
 import { Provider } from 'react-redux';
 import { Provider } from 'react-redux';
-import { shallow } from 'enzyme';
+import { shallow, mount } from 'enzyme';
 import configureStore from 'redux/store/configureStore';
 import configureStore from 'redux/store/configureStore';
+import { StaticRouter } from 'react-router';
+import ClusterContext from 'components/contexts/ClusterContext';
 import DetailsContainer from '../DetailsContainer';
 import DetailsContainer from '../DetailsContainer';
 import Details, { DetailsProps } from '../Details';
 import Details, { DetailsProps } from '../Details';
 import { schema, versions } from './fixtures';
 import { schema, versions } from './fixtures';
@@ -101,6 +103,20 @@ describe('Details', () => {
           expect(shallow(setupWrapper({ versions }))).toMatchSnapshot();
           expect(shallow(setupWrapper({ versions }))).toMatchSnapshot();
         });
         });
       });
       });
+
+      describe('when the readonly flag is set', () => {
+        it('does not render update & delete buttons', () => {
+          expect(
+            mount(
+              <StaticRouter>
+                <ClusterContext.Provider value={{ isReadOnly: true }}>
+                  {setupWrapper({ versions })}
+                </ClusterContext.Provider>
+              </StaticRouter>
+            ).exists('.level-right')
+          ).toBeFalsy();
+        });
+      });
     });
     });
   });
   });
 });
 });

+ 12 - 8
kafka-ui-react-app/src/components/Schemas/List/List.tsx

@@ -3,6 +3,7 @@ import { SchemaSubject } from 'generated-sources';
 import { NavLink, useParams } from 'react-router-dom';
 import { NavLink, useParams } from 'react-router-dom';
 import { clusterSchemaNewPath } from 'lib/paths';
 import { clusterSchemaNewPath } from 'lib/paths';
 import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
 import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
+import ClusterContext from 'components/contexts/ClusterContext';
 import ListItem from './ListItem';
 import ListItem from './ListItem';
 
 
 export interface ListProps {
 export interface ListProps {
@@ -10,6 +11,7 @@ export interface ListProps {
 }
 }
 
 
 const List: React.FC<ListProps> = ({ schemas }) => {
 const List: React.FC<ListProps> = ({ schemas }) => {
+  const { isReadOnly } = React.useContext(ClusterContext);
   const { clusterName } = useParams<{ clusterName: string }>();
   const { clusterName } = useParams<{ clusterName: string }>();
 
 
   return (
   return (
@@ -17,14 +19,16 @@ const List: React.FC<ListProps> = ({ schemas }) => {
       <Breadcrumb>Schema Registry</Breadcrumb>
       <Breadcrumb>Schema Registry</Breadcrumb>
       <div className="box">
       <div className="box">
         <div className="level">
         <div className="level">
-          <div className="level-item level-right">
-            <NavLink
-              className="button is-primary"
-              to={clusterSchemaNewPath(clusterName)}
-            >
-              Create Schema
-            </NavLink>
-          </div>
+          {!isReadOnly && (
+            <div className="level-item level-right">
+              <NavLink
+                className="button is-primary"
+                to={clusterSchemaNewPath(clusterName)}
+              >
+                Create Schema
+              </NavLink>
+            </div>
+          )}
         </div>
         </div>
       </div>
       </div>
 
 

+ 14 - 0
kafka-ui-react-app/src/components/Schemas/List/__test__/List.spec.tsx

@@ -3,6 +3,7 @@ import { mount, shallow } from 'enzyme';
 import { Provider } from 'react-redux';
 import { Provider } from 'react-redux';
 import { StaticRouter } from 'react-router';
 import { StaticRouter } from 'react-router';
 import configureStore from 'redux/store/configureStore';
 import configureStore from 'redux/store/configureStore';
+import ClusterContext from 'components/contexts/ClusterContext';
 import ListContainer from '../ListContainer';
 import ListContainer from '../ListContainer';
 import List, { ListProps } from '../List';
 import List, { ListProps } from '../List';
 import { schemas } from './fixtures';
 import { schemas } from './fixtures';
@@ -49,5 +50,18 @@ describe('List', () => {
         expect(wrapper.find('ListItem').length).toEqual(3);
         expect(wrapper.find('ListItem').length).toEqual(3);
       });
       });
     });
     });
+
+    describe('with readonly cluster', () => {
+      const wrapper = mount(
+        <StaticRouter>
+          <ClusterContext.Provider value={{ isReadOnly: true }}>
+            {setupWrapper({ schemas: [] })}
+          </ClusterContext.Provider>
+        </StaticRouter>
+      );
+      it('does not render Create Schema button', () => {
+        expect(wrapper.exists('NavLink')).toBeFalsy();
+      });
+    });
   });
   });
 });
 });

+ 22 - 17
kafka-ui-react-app/src/components/Schemas/Schemas.tsx

@@ -5,15 +5,18 @@ import PageLoader from 'components/common/PageLoader/PageLoader';
 import ListContainer from './List/ListContainer';
 import ListContainer from './List/ListContainer';
 import DetailsContainer from './Details/DetailsContainer';
 import DetailsContainer from './Details/DetailsContainer';
 import NewContainer from './New/NewContainer';
 import NewContainer from './New/NewContainer';
+import ClusterContext from '../contexts/ClusterContext';
 
 
 export interface SchemasProps {
 export interface SchemasProps {
   isFetching: boolean;
   isFetching: boolean;
   fetchSchemasByClusterName: (clusterName: ClusterName) => void;
   fetchSchemasByClusterName: (clusterName: ClusterName) => void;
+  isReadOnly: boolean;
 }
 }
 
 
 const Schemas: React.FC<SchemasProps> = ({
 const Schemas: React.FC<SchemasProps> = ({
   isFetching,
   isFetching,
   fetchSchemasByClusterName,
   fetchSchemasByClusterName,
+  isReadOnly,
 }) => {
 }) => {
   const { clusterName } = useParams<{ clusterName: string }>();
   const { clusterName } = useParams<{ clusterName: string }>();
 
 
@@ -26,23 +29,25 @@ const Schemas: React.FC<SchemasProps> = ({
   }
   }
 
 
   return (
   return (
-    <Switch>
-      <Route
-        exact
-        path="/ui/clusters/:clusterName/schemas"
-        component={ListContainer}
-      />
-      <Route
-        exact
-        path="/ui/clusters/:clusterName/schemas/new"
-        component={NewContainer}
-      />
-      <Route
-        exact
-        path="/ui/clusters/:clusterName/schemas/:subject/latest"
-        component={DetailsContainer}
-      />
-    </Switch>
+    <ClusterContext.Provider value={{ isReadOnly }}>
+      <Switch>
+        <Route
+          exact
+          path="/ui/clusters/:clusterName/schemas"
+          component={ListContainer}
+        />
+        <Route
+          exact
+          path="/ui/clusters/:clusterName/schemas/new"
+          component={NewContainer}
+        />
+        <Route
+          exact
+          path="/ui/clusters/:clusterName/schemas/:subject/latest"
+          component={DetailsContainer}
+        />
+      </Switch>
+    </ClusterContext.Provider>
   );
   );
 };
 };
 
 

+ 21 - 3
kafka-ui-react-app/src/components/Schemas/SchemasContainer.tsx

@@ -1,15 +1,33 @@
 import { connect } from 'react-redux';
 import { connect } from 'react-redux';
-import { RootState } from 'redux/interfaces';
+import { RootState, ClusterName } from 'redux/interfaces';
 import { fetchSchemasByClusterName } from 'redux/actions';
 import { fetchSchemasByClusterName } from 'redux/actions';
 import { getIsSchemaListFetching } from 'redux/reducers/schemas/selectors';
 import { getIsSchemaListFetching } from 'redux/reducers/schemas/selectors';
+import { getClustersReadonlyStatus } from 'redux/reducers/clusters/selectors';
+import { RouteComponentProps, withRouter } from 'react-router-dom';
 import Schemas from './Schemas';
 import Schemas from './Schemas';
 
 
-const mapStateToProps = (state: RootState) => ({
+interface RouteProps {
+  clusterName: ClusterName;
+}
+
+type OwnProps = RouteComponentProps<RouteProps>;
+
+const mapStateToProps = (
+  state: RootState,
+  {
+    match: {
+      params: { clusterName },
+    },
+  }: OwnProps
+) => ({
   isFetching: getIsSchemaListFetching(state),
   isFetching: getIsSchemaListFetching(state),
+  isReadOnly: getClustersReadonlyStatus(clusterName)(state),
 });
 });
 
 
 const mapDispatchToProps = {
 const mapDispatchToProps = {
   fetchSchemasByClusterName,
   fetchSchemasByClusterName,
 };
 };
 
 
-export default connect(mapStateToProps, mapDispatchToProps)(Schemas);
+export default withRouter(
+  connect(mapStateToProps, mapDispatchToProps)(Schemas)
+);

+ 1 - 0
kafka-ui-react-app/src/components/Schemas/__test__/Schemas.spec.tsx

@@ -30,6 +30,7 @@ describe('Schemas', () => {
           <Schemas
           <Schemas
             isFetching
             isFetching
             fetchSchemasByClusterName={jest.fn()}
             fetchSchemasByClusterName={jest.fn()}
+            isReadOnly={false}
             {...props}
             {...props}
           />
           />
         </StaticRouter>
         </StaticRouter>

+ 7 - 3
kafka-ui-react-app/src/components/Topics/Details/Details.tsx

@@ -10,6 +10,7 @@ import {
   clusterTopicMessagesPath,
   clusterTopicMessagesPath,
   clusterTopicsTopicEditPath,
   clusterTopicsTopicEditPath,
 } from 'lib/paths';
 } from 'lib/paths';
+import ClusterContext from 'components/contexts/ClusterContext';
 import OverviewContainer from './Overview/OverviewContainer';
 import OverviewContainer from './Overview/OverviewContainer';
 import MessagesContainer from './Messages/MessagesContainer';
 import MessagesContainer from './Messages/MessagesContainer';
 import SettingsContainer from './Settings/SettingsContainer';
 import SettingsContainer from './Settings/SettingsContainer';
@@ -21,6 +22,7 @@ interface Props extends Topic, TopicDetails {
 }
 }
 
 
 const Details: React.FC<Props> = ({ clusterName, topicName }) => {
 const Details: React.FC<Props> = ({ clusterName, topicName }) => {
+  const { isReadOnly } = React.useContext(ClusterContext);
   return (
   return (
     <div className="section">
     <div className="section">
       <div className="level">
       <div className="level">
@@ -33,9 +35,11 @@ const Details: React.FC<Props> = ({ clusterName, topicName }) => {
             {topicName}
             {topicName}
           </Breadcrumb>
           </Breadcrumb>
         </div>
         </div>
-        <SettingsEditButton
-          to={clusterTopicsTopicEditPath(clusterName, topicName)}
-        />
+        {!isReadOnly && (
+          <SettingsEditButton
+            to={clusterTopicsTopicEditPath(clusterName, topicName)}
+          />
+        )}
       </div>
       </div>
 
 
       <div className="box">
       <div className="box">

+ 10 - 7
kafka-ui-react-app/src/components/Topics/List/List.tsx

@@ -3,6 +3,7 @@ import { TopicWithDetailedInfo, ClusterName } from 'redux/interfaces';
 import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
 import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
 import { NavLink } from 'react-router-dom';
 import { NavLink } from 'react-router-dom';
 import { clusterTopicNewPath } from 'lib/paths';
 import { clusterTopicNewPath } from 'lib/paths';
+import ClusterContext from 'components/contexts/ClusterContext';
 import ListItem from './ListItem';
 import ListItem from './ListItem';
 
 
 interface Props {
 interface Props {
@@ -15,7 +16,7 @@ const List: React.FC<Props> = ({ clusterName, topics, externalTopics }) => {
   const [showInternal, setShowInternal] = React.useState<boolean>(true);
   const [showInternal, setShowInternal] = React.useState<boolean>(true);
 
 
   const handleSwitch = () => setShowInternal(!showInternal);
   const handleSwitch = () => setShowInternal(!showInternal);
-
+  const { isReadOnly } = React.useContext(ClusterContext);
   const items = showInternal ? topics : externalTopics;
   const items = showInternal ? topics : externalTopics;
 
 
   return (
   return (
@@ -38,12 +39,14 @@ const List: React.FC<Props> = ({ clusterName, topics, externalTopics }) => {
             </div>
             </div>
           </div>
           </div>
           <div className="level-item level-right">
           <div className="level-item level-right">
-            <NavLink
-              className="button is-primary"
-              to={clusterTopicNewPath(clusterName)}
-            >
-              Add a Topic
-            </NavLink>
+            {!isReadOnly && (
+              <NavLink
+                className="button is-primary"
+                to={clusterTopicNewPath(clusterName)}
+              >
+                Add a Topic
+              </NavLink>
+            )}
           </div>
           </div>
         </div>
         </div>
       </div>
       </div>

+ 22 - 0
kafka-ui-react-app/src/components/Topics/List/__tests__/List.spec.tsx

@@ -0,0 +1,22 @@
+import { mount } from 'enzyme';
+import React from 'react';
+import ClusterContext from 'components/contexts/ClusterContext';
+import List from '../List';
+
+describe('List', () => {
+  describe('when it has readonly flag', () => {
+    it('does not render the Add a Topic button', () => {
+      const props = {
+        clusterName: 'Cluster',
+        topics: [],
+        externalTopics: [],
+      };
+      const component = mount(
+        <ClusterContext.Provider value={{ isReadOnly: true }}>
+          <List {...props} />
+        </ClusterContext.Provider>
+      );
+      expect(component.exists('NavLink')).toBeFalsy();
+    });
+  });
+});

+ 26 - 21
kafka-ui-react-app/src/components/Topics/Topics.tsx

@@ -6,18 +6,21 @@ import EditContainer from 'components/Topics/Edit/EditContainer';
 import ListContainer from './List/ListContainer';
 import ListContainer from './List/ListContainer';
 import DetailsContainer from './Details/DetailsContainer';
 import DetailsContainer from './Details/DetailsContainer';
 import NewContainer from './New/NewContainer';
 import NewContainer from './New/NewContainer';
+import ClusterContext from '../contexts/ClusterContext';
 
 
 interface Props {
 interface Props {
   clusterName: ClusterName;
   clusterName: ClusterName;
   isFetched: boolean;
   isFetched: boolean;
   fetchBrokers: (clusterName: ClusterName) => void;
   fetchBrokers: (clusterName: ClusterName) => void;
   fetchTopicsList: (clusterName: ClusterName) => void;
   fetchTopicsList: (clusterName: ClusterName) => void;
+  isReadOnly: boolean;
 }
 }
 
 
 const Topics: React.FC<Props> = ({
 const Topics: React.FC<Props> = ({
   clusterName,
   clusterName,
   isFetched,
   isFetched,
   fetchTopicsList,
   fetchTopicsList,
+  isReadOnly,
 }) => {
 }) => {
   React.useEffect(() => {
   React.useEffect(() => {
     fetchTopicsList(clusterName);
     fetchTopicsList(clusterName);
@@ -25,27 +28,29 @@ const Topics: React.FC<Props> = ({
 
 
   if (isFetched) {
   if (isFetched) {
     return (
     return (
-      <Switch>
-        <Route
-          exact
-          path="/ui/clusters/:clusterName/topics"
-          component={ListContainer}
-        />
-        <Route
-          exact
-          path="/ui/clusters/:clusterName/topics/new"
-          component={NewContainer}
-        />
-        <Route
-          exact
-          path="/ui/clusters/:clusterName/topics/:topicName/edit"
-          component={EditContainer}
-        />
-        <Route
-          path="/ui/clusters/:clusterName/topics/:topicName"
-          component={DetailsContainer}
-        />
-      </Switch>
+      <ClusterContext.Provider value={{ isReadOnly }}>
+        <Switch>
+          <Route
+            exact
+            path="/ui/clusters/:clusterName/topics"
+            component={ListContainer}
+          />
+          <Route
+            exact
+            path="/ui/clusters/:clusterName/topics/new"
+            component={NewContainer}
+          />
+          <Route
+            exact
+            path="/ui/clusters/:clusterName/topics/:topicName/edit"
+            component={EditContainer}
+          />
+          <Route
+            path="/ui/clusters/:clusterName/topics/:topicName"
+            component={DetailsContainer}
+          />
+        </Switch>
+      </ClusterContext.Provider>
     );
     );
   }
   }
 
 

+ 2 - 0
kafka-ui-react-app/src/components/Topics/TopicsContainer.ts

@@ -3,6 +3,7 @@ import { fetchTopicsList } from 'redux/actions';
 import { getIsTopicListFetched } from 'redux/reducers/topics/selectors';
 import { getIsTopicListFetched } from 'redux/reducers/topics/selectors';
 import { RootState, ClusterName } from 'redux/interfaces';
 import { RootState, ClusterName } from 'redux/interfaces';
 import { RouteComponentProps } from 'react-router-dom';
 import { RouteComponentProps } from 'react-router-dom';
+import { getClustersReadonlyStatus } from 'redux/reducers/clusters/selectors';
 import Topics from './Topics';
 import Topics from './Topics';
 
 
 interface RouteProps {
 interface RouteProps {
@@ -21,6 +22,7 @@ const mapStateToProps = (
 ) => ({
 ) => ({
   isFetched: getIsTopicListFetched(state),
   isFetched: getIsTopicListFetched(state),
   clusterName,
   clusterName,
+  isReadOnly: getClustersReadonlyStatus(clusterName)(state),
 });
 });
 
 
 const mapDispatchToProps = {
 const mapDispatchToProps = {

+ 8 - 0
kafka-ui-react-app/src/components/contexts/ClusterContext.ts

@@ -0,0 +1,8 @@
+import React from 'react';
+
+const initialValue: { isReadOnly: boolean } = {
+  isReadOnly: false,
+};
+const ClusterContext = React.createContext(initialValue);
+
+export default ClusterContext;

+ 7 - 0
kafka-ui-react-app/src/redux/reducers/clusters/selectors.ts

@@ -24,3 +24,10 @@ export const getOnlineClusters = createSelector(getClusterList, (clusters) =>
 export const getOfflineClusters = createSelector(getClusterList, (clusters) =>
 export const getOfflineClusters = createSelector(getClusterList, (clusters) =>
   clusters.filter(({ status }) => status === ServerStatus.OFFLINE)
   clusters.filter(({ status }) => status === ServerStatus.OFFLINE)
 );
 );
+
+export const getClustersReadonlyStatus = (clusterName: string) =>
+  createSelector(
+    getClusterList,
+    (clusters): boolean =>
+      clusters.find(({ name }) => name === clusterName)?.readOnly || false
+  );