* Implement read-only flag logic * Test new functionality
This commit is contained in:
parent
e02dd84491
commit
1188ce9bc2
16 changed files with 208 additions and 78 deletions
|
@ -17,6 +17,7 @@ const ClusterWidget: React.FC<ClusterWidgetProps> = ({
|
|||
bytesInPerSec,
|
||||
bytesOutPerSec,
|
||||
onlinePartitionCount,
|
||||
readOnly,
|
||||
},
|
||||
}) => (
|
||||
<div className="column is-full-modile is-6">
|
||||
|
@ -29,6 +30,9 @@ const ClusterWidget: React.FC<ClusterWidgetProps> = ({
|
|||
>
|
||||
{status}
|
||||
</div>
|
||||
{readOnly && (
|
||||
<div className="tag has-margin-right is-info is-light">readonly</div>
|
||||
)}
|
||||
{name}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -70,4 +70,17 @@ describe('ClusterWidget', () => {
|
|||
).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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<DetailsProps> = ({
|
|||
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<DetailsProps> = ({
|
|||
</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>
|
||||
<LatestVersionItem schema={schema} />
|
||||
</div>
|
||||
|
|
|
@ -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(
|
||||
<StaticRouter>
|
||||
<ClusterContext.Provider value={{ isReadOnly: true }}>
|
||||
{setupWrapper({ versions })}
|
||||
</ClusterContext.Provider>
|
||||
</StaticRouter>
|
||||
).exists('.level-right')
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<ListProps> = ({ schemas }) => {
|
||||
const { isReadOnly } = React.useContext(ClusterContext);
|
||||
const { clusterName } = useParams<{ clusterName: string }>();
|
||||
|
||||
return (
|
||||
|
@ -17,14 +19,16 @@ const List: React.FC<ListProps> = ({ schemas }) => {
|
|||
<Breadcrumb>Schema Registry</Breadcrumb>
|
||||
<div className="box">
|
||||
<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>
|
||||
|
||||
|
|
|
@ -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(
|
||||
<StaticRouter>
|
||||
<ClusterContext.Provider value={{ isReadOnly: true }}>
|
||||
{setupWrapper({ schemas: [] })}
|
||||
</ClusterContext.Provider>
|
||||
</StaticRouter>
|
||||
);
|
||||
it('does not render Create Schema button', () => {
|
||||
expect(wrapper.exists('NavLink')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<SchemasProps> = ({
|
||||
isFetching,
|
||||
fetchSchemasByClusterName,
|
||||
isReadOnly,
|
||||
}) => {
|
||||
const { clusterName } = useParams<{ clusterName: string }>();
|
||||
|
||||
|
@ -26,23 +29,25 @@ const Schemas: React.FC<SchemasProps> = ({
|
|||
}
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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<RouteProps>;
|
||||
|
||||
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)
|
||||
);
|
||||
|
|
|
@ -30,6 +30,7 @@ describe('Schemas', () => {
|
|||
<Schemas
|
||||
isFetching
|
||||
fetchSchemasByClusterName={jest.fn()}
|
||||
isReadOnly={false}
|
||||
{...props}
|
||||
/>
|
||||
</StaticRouter>
|
||||
|
|
|
@ -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<Props> = ({ clusterName, topicName }) => {
|
||||
const { isReadOnly } = React.useContext(ClusterContext);
|
||||
return (
|
||||
<div className="section">
|
||||
<div className="level">
|
||||
|
@ -33,9 +35,11 @@ const Details: React.FC<Props> = ({ clusterName, topicName }) => {
|
|||
{topicName}
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
<SettingsEditButton
|
||||
to={clusterTopicsTopicEditPath(clusterName, topicName)}
|
||||
/>
|
||||
{!isReadOnly && (
|
||||
<SettingsEditButton
|
||||
to={clusterTopicsTopicEditPath(clusterName, topicName)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="box">
|
||||
|
|
|
@ -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<Props> = ({ clusterName, topics, externalTopics }) => {
|
|||
const [showInternal, setShowInternal] = React.useState<boolean>(true);
|
||||
|
||||
const handleSwitch = () => setShowInternal(!showInternal);
|
||||
|
||||
const { isReadOnly } = React.useContext(ClusterContext);
|
||||
const items = showInternal ? topics : externalTopics;
|
||||
|
||||
return (
|
||||
|
@ -38,12 +39,14 @@ const List: React.FC<Props> = ({ clusterName, topics, externalTopics }) => {
|
|||
</div>
|
||||
</div>
|
||||
<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>
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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<Props> = ({
|
||||
clusterName,
|
||||
isFetched,
|
||||
fetchTopicsList,
|
||||
isReadOnly,
|
||||
}) => {
|
||||
React.useEffect(() => {
|
||||
fetchTopicsList(clusterName);
|
||||
|
@ -25,27 +28,29 @@ const Topics: React.FC<Props> = ({
|
|||
|
||||
if (isFetched) {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import React from 'react';
|
||||
|
||||
const initialValue: { isReadOnly: boolean } = {
|
||||
isReadOnly: false,
|
||||
};
|
||||
const ClusterContext = React.createContext(initialValue);
|
||||
|
||||
export default ClusterContext;
|
|
@ -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
|
||||
);
|
||||
|
|
Loading…
Add table
Reference in a new issue