From 1188ce9bc283c0b8223f611ea3b4679574bffd4a Mon Sep 17 00:00:00 2001 From: Alexander Krivonosov <31561808+GneyHabub@users.noreply.github.com> Date: Thu, 11 Mar 2021 12:00:46 +0300 Subject: [PATCH] #219 ReadOnly flag (#231) * Implement read-only flag logic * Test new functionality --- .../ClustersWidget/ClusterWidget.tsx | 4 ++ .../__test__/ClusterWidget.spec.tsx | 13 +++++ .../components/Schemas/Details/Details.tsx | 40 +++++++++------- .../Schemas/Details/__test__/Details.spec.tsx | 18 ++++++- .../src/components/Schemas/List/List.tsx | 20 ++++---- .../Schemas/List/__test__/List.spec.tsx | 14 ++++++ .../src/components/Schemas/Schemas.tsx | 39 ++++++++------- .../components/Schemas/SchemasContainer.tsx | 24 ++++++++-- .../Schemas/__test__/Schemas.spec.tsx | 1 + .../src/components/Topics/Details/Details.tsx | 10 ++-- .../src/components/Topics/List/List.tsx | 17 ++++--- .../Topics/List/__tests__/List.spec.tsx | 22 +++++++++ .../src/components/Topics/Topics.tsx | 47 ++++++++++--------- .../src/components/Topics/TopicsContainer.ts | 2 + .../src/components/contexts/ClusterContext.ts | 8 ++++ .../src/redux/reducers/clusters/selectors.ts | 7 +++ 16 files changed, 208 insertions(+), 78 deletions(-) create mode 100644 kafka-ui-react-app/src/components/Topics/List/__tests__/List.spec.tsx create mode 100644 kafka-ui-react-app/src/components/contexts/ClusterContext.ts diff --git a/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClusterWidget.tsx b/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClusterWidget.tsx index c5b0007288..9e7fa5403c 100644 --- a/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClusterWidget.tsx +++ b/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClusterWidget.tsx @@ -17,6 +17,7 @@ const ClusterWidget: React.FC = ({ bytesInPerSec, bytesOutPerSec, onlinePartitionCount, + readOnly, }, }) => (
@@ -29,6 +30,9 @@ const ClusterWidget: React.FC = ({ > {status}
+ {readOnly && ( +
readonly
+ )} {name} diff --git a/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/ClusterWidget.spec.tsx b/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/ClusterWidget.spec.tsx index e8d21e6ab3..4cc652e05c 100644 --- a/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/ClusterWidget.spec.tsx +++ b/kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/ClusterWidget.spec.tsx @@ -70,4 +70,17 @@ describe('ClusterWidget', () => { ).toMatchSnapshot(); }); }); + + describe('when cluster is read-only', () => { + it('renders the tag', () => { + expect( + shallow( + + ) + .find('.title') + .childAt(1) + .text() + ).toEqual('readonly'); + }); + }); }); diff --git a/kafka-ui-react-app/src/components/Schemas/Details/Details.tsx b/kafka-ui-react-app/src/components/Schemas/Details/Details.tsx index 1753b11e66..e343c43ff9 100644 --- a/kafka-ui-react-app/src/components/Schemas/Details/Details.tsx +++ b/kafka-ui-react-app/src/components/Schemas/Details/Details.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { SchemaSubject } from 'generated-sources'; import { ClusterName, SchemaName } from 'redux/interfaces'; import { clusterSchemasPath } from 'lib/paths'; +import ClusterContext from 'components/contexts/ClusterContext'; import Breadcrumb from '../../common/Breadcrumb/Breadcrumb'; import SchemaVersion from './SchemaVersion'; import LatestVersionItem from './LatestVersionItem'; @@ -25,6 +26,7 @@ const Details: React.FC = ({ versions, isFetched, }) => { + const { isReadOnly } = React.useContext(ClusterContext); React.useEffect(() => { fetchSchemaVersions(clusterName, schema.subject as SchemaName); }, [fetchSchemaVersions, clusterName]); @@ -54,24 +56,26 @@ const Details: React.FC = ({ -
- - -
+ {!isReadOnly && ( +
+ + +
+ )} diff --git a/kafka-ui-react-app/src/components/Schemas/Details/__test__/Details.spec.tsx b/kafka-ui-react-app/src/components/Schemas/Details/__test__/Details.spec.tsx index 816f20380c..608263e377 100644 --- a/kafka-ui-react-app/src/components/Schemas/Details/__test__/Details.spec.tsx +++ b/kafka-ui-react-app/src/components/Schemas/Details/__test__/Details.spec.tsx @@ -1,7 +1,9 @@ import React from 'react'; import { Provider } from 'react-redux'; -import { shallow } from 'enzyme'; +import { shallow, mount } from 'enzyme'; import configureStore from 'redux/store/configureStore'; +import { StaticRouter } from 'react-router'; +import ClusterContext from 'components/contexts/ClusterContext'; import DetailsContainer from '../DetailsContainer'; import Details, { DetailsProps } from '../Details'; import { schema, versions } from './fixtures'; @@ -101,6 +103,20 @@ describe('Details', () => { expect(shallow(setupWrapper({ versions }))).toMatchSnapshot(); }); }); + + describe('when the readonly flag is set', () => { + it('does not render update & delete buttons', () => { + expect( + mount( + + + {setupWrapper({ versions })} + + + ).exists('.level-right') + ).toBeFalsy(); + }); + }); }); }); }); diff --git a/kafka-ui-react-app/src/components/Schemas/List/List.tsx b/kafka-ui-react-app/src/components/Schemas/List/List.tsx index c7366d1f66..b5b7621bcd 100644 --- a/kafka-ui-react-app/src/components/Schemas/List/List.tsx +++ b/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 { clusterSchemaNewPath } from 'lib/paths'; import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb'; +import ClusterContext from 'components/contexts/ClusterContext'; import ListItem from './ListItem'; export interface ListProps { @@ -10,6 +11,7 @@ export interface ListProps { } const List: React.FC = ({ schemas }) => { + const { isReadOnly } = React.useContext(ClusterContext); const { clusterName } = useParams<{ clusterName: string }>(); return ( @@ -17,14 +19,16 @@ const List: React.FC = ({ schemas }) => { Schema Registry
-
- - Create Schema - -
+ {!isReadOnly && ( +
+ + Create Schema + +
+ )}
diff --git a/kafka-ui-react-app/src/components/Schemas/List/__test__/List.spec.tsx b/kafka-ui-react-app/src/components/Schemas/List/__test__/List.spec.tsx index 4a803432fb..776f539468 100644 --- a/kafka-ui-react-app/src/components/Schemas/List/__test__/List.spec.tsx +++ b/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 { StaticRouter } from 'react-router'; import configureStore from 'redux/store/configureStore'; +import ClusterContext from 'components/contexts/ClusterContext'; import ListContainer from '../ListContainer'; import List, { ListProps } from '../List'; import { schemas } from './fixtures'; @@ -49,5 +50,18 @@ describe('List', () => { expect(wrapper.find('ListItem').length).toEqual(3); }); }); + + describe('with readonly cluster', () => { + const wrapper = mount( + + + {setupWrapper({ schemas: [] })} + + + ); + it('does not render Create Schema button', () => { + expect(wrapper.exists('NavLink')).toBeFalsy(); + }); + }); }); }); diff --git a/kafka-ui-react-app/src/components/Schemas/Schemas.tsx b/kafka-ui-react-app/src/components/Schemas/Schemas.tsx index 030aa71229..06225d803d 100644 --- a/kafka-ui-react-app/src/components/Schemas/Schemas.tsx +++ b/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 DetailsContainer from './Details/DetailsContainer'; import NewContainer from './New/NewContainer'; +import ClusterContext from '../contexts/ClusterContext'; export interface SchemasProps { isFetching: boolean; fetchSchemasByClusterName: (clusterName: ClusterName) => void; + isReadOnly: boolean; } const Schemas: React.FC = ({ isFetching, fetchSchemasByClusterName, + isReadOnly, }) => { const { clusterName } = useParams<{ clusterName: string }>(); @@ -26,23 +29,25 @@ const Schemas: React.FC = ({ } return ( - - - - - + + + + + + + ); }; diff --git a/kafka-ui-react-app/src/components/Schemas/SchemasContainer.tsx b/kafka-ui-react-app/src/components/Schemas/SchemasContainer.tsx index a70e9d902c..31249f87b4 100644 --- a/kafka-ui-react-app/src/components/Schemas/SchemasContainer.tsx +++ b/kafka-ui-react-app/src/components/Schemas/SchemasContainer.tsx @@ -1,15 +1,33 @@ import { connect } from 'react-redux'; -import { RootState } from 'redux/interfaces'; +import { RootState, ClusterName } from 'redux/interfaces'; import { fetchSchemasByClusterName } from 'redux/actions'; 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'; -const mapStateToProps = (state: RootState) => ({ +interface RouteProps { + clusterName: ClusterName; +} + +type OwnProps = RouteComponentProps; + +const mapStateToProps = ( + state: RootState, + { + match: { + params: { clusterName }, + }, + }: OwnProps +) => ({ isFetching: getIsSchemaListFetching(state), + isReadOnly: getClustersReadonlyStatus(clusterName)(state), }); const mapDispatchToProps = { fetchSchemasByClusterName, }; -export default connect(mapStateToProps, mapDispatchToProps)(Schemas); +export default withRouter( + connect(mapStateToProps, mapDispatchToProps)(Schemas) +); diff --git a/kafka-ui-react-app/src/components/Schemas/__test__/Schemas.spec.tsx b/kafka-ui-react-app/src/components/Schemas/__test__/Schemas.spec.tsx index 1c24d2cff3..d109333c8b 100644 --- a/kafka-ui-react-app/src/components/Schemas/__test__/Schemas.spec.tsx +++ b/kafka-ui-react-app/src/components/Schemas/__test__/Schemas.spec.tsx @@ -30,6 +30,7 @@ describe('Schemas', () => { diff --git a/kafka-ui-react-app/src/components/Topics/Details/Details.tsx b/kafka-ui-react-app/src/components/Topics/Details/Details.tsx index 1b1cb9903f..796be2c6f9 100644 --- a/kafka-ui-react-app/src/components/Topics/Details/Details.tsx +++ b/kafka-ui-react-app/src/components/Topics/Details/Details.tsx @@ -10,6 +10,7 @@ import { clusterTopicMessagesPath, clusterTopicsTopicEditPath, } from 'lib/paths'; +import ClusterContext from 'components/contexts/ClusterContext'; import OverviewContainer from './Overview/OverviewContainer'; import MessagesContainer from './Messages/MessagesContainer'; import SettingsContainer from './Settings/SettingsContainer'; @@ -21,6 +22,7 @@ interface Props extends Topic, TopicDetails { } const Details: React.FC = ({ clusterName, topicName }) => { + const { isReadOnly } = React.useContext(ClusterContext); return (
@@ -33,9 +35,11 @@ const Details: React.FC = ({ clusterName, topicName }) => { {topicName}
- + {!isReadOnly && ( + + )}
diff --git a/kafka-ui-react-app/src/components/Topics/List/List.tsx b/kafka-ui-react-app/src/components/Topics/List/List.tsx index ad6a286e90..879c08134a 100644 --- a/kafka-ui-react-app/src/components/Topics/List/List.tsx +++ b/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 { NavLink } from 'react-router-dom'; import { clusterTopicNewPath } from 'lib/paths'; +import ClusterContext from 'components/contexts/ClusterContext'; import ListItem from './ListItem'; interface Props { @@ -15,7 +16,7 @@ const List: React.FC = ({ clusterName, topics, externalTopics }) => { const [showInternal, setShowInternal] = React.useState(true); const handleSwitch = () => setShowInternal(!showInternal); - + const { isReadOnly } = React.useContext(ClusterContext); const items = showInternal ? topics : externalTopics; return ( @@ -38,12 +39,14 @@ const List: React.FC = ({ clusterName, topics, externalTopics }) => {
- - Add a Topic - + {!isReadOnly && ( + + Add a Topic + + )}
diff --git a/kafka-ui-react-app/src/components/Topics/List/__tests__/List.spec.tsx b/kafka-ui-react-app/src/components/Topics/List/__tests__/List.spec.tsx new file mode 100644 index 0000000000..0e857e276b --- /dev/null +++ b/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( + + + + ); + expect(component.exists('NavLink')).toBeFalsy(); + }); + }); +}); diff --git a/kafka-ui-react-app/src/components/Topics/Topics.tsx b/kafka-ui-react-app/src/components/Topics/Topics.tsx index 5317ec1abf..73ddc1ba1e 100644 --- a/kafka-ui-react-app/src/components/Topics/Topics.tsx +++ b/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 DetailsContainer from './Details/DetailsContainer'; import NewContainer from './New/NewContainer'; +import ClusterContext from '../contexts/ClusterContext'; interface Props { clusterName: ClusterName; isFetched: boolean; fetchBrokers: (clusterName: ClusterName) => void; fetchTopicsList: (clusterName: ClusterName) => void; + isReadOnly: boolean; } const Topics: React.FC = ({ clusterName, isFetched, fetchTopicsList, + isReadOnly, }) => { React.useEffect(() => { fetchTopicsList(clusterName); @@ -25,27 +28,29 @@ const Topics: React.FC = ({ if (isFetched) { return ( - - - - - - + + + + + + + + ); } diff --git a/kafka-ui-react-app/src/components/Topics/TopicsContainer.ts b/kafka-ui-react-app/src/components/Topics/TopicsContainer.ts index 75714dd5e9..e540b4efdc 100644 --- a/kafka-ui-react-app/src/components/Topics/TopicsContainer.ts +++ b/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 { RootState, ClusterName } from 'redux/interfaces'; import { RouteComponentProps } from 'react-router-dom'; +import { getClustersReadonlyStatus } from 'redux/reducers/clusters/selectors'; import Topics from './Topics'; interface RouteProps { @@ -21,6 +22,7 @@ const mapStateToProps = ( ) => ({ isFetched: getIsTopicListFetched(state), clusterName, + isReadOnly: getClustersReadonlyStatus(clusterName)(state), }); const mapDispatchToProps = { diff --git a/kafka-ui-react-app/src/components/contexts/ClusterContext.ts b/kafka-ui-react-app/src/components/contexts/ClusterContext.ts new file mode 100644 index 0000000000..40e43eb0ce --- /dev/null +++ b/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; diff --git a/kafka-ui-react-app/src/redux/reducers/clusters/selectors.ts b/kafka-ui-react-app/src/redux/reducers/clusters/selectors.ts index e6667fc3e0..a8b06b0f17 100644 --- a/kafka-ui-react-app/src/redux/reducers/clusters/selectors.ts +++ b/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) => clusters.filter(({ status }) => status === ServerStatus.OFFLINE) ); + +export const getClustersReadonlyStatus = (clusterName: string) => + createSelector( + getClusterList, + (clusters): boolean => + clusters.find(({ name }) => name === clusterName)?.readOnly || false + );