Topics page refactoring (#251)
* Split thunks on files. Refactoring * [CHORE] Refactor Topics section
This commit is contained in:
parent
a8ed4ff37f
commit
bbdd60b7a5
42 changed files with 790 additions and 757 deletions
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { Switch, Route, Redirect, useParams } from 'react-router-dom';
|
import { Switch, Route, Redirect, useParams } from 'react-router-dom';
|
||||||
import BrokersContainer from 'components/Brokers/BrokersContainer';
|
import BrokersContainer from 'components/Brokers/BrokersContainer';
|
||||||
import TopicsContainer from 'components/Topics/TopicsContainer';
|
import Topics from 'components/Topics/Topics';
|
||||||
import ConsumersGroupsContainer from 'components/ConsumerGroups/ConsumersGroupsContainer';
|
import ConsumersGroupsContainer from 'components/ConsumerGroups/ConsumersGroupsContainer';
|
||||||
import Schemas from 'components/Schemas/Schemas';
|
import Schemas from 'components/Schemas/Schemas';
|
||||||
import { getClustersReadonlyStatus } from 'redux/reducers/clusters/selectors';
|
import { getClustersReadonlyStatus } from 'redux/reducers/clusters/selectors';
|
||||||
|
@ -18,10 +18,7 @@ const Cluster: React.FC = () => {
|
||||||
path="/ui/clusters/:clusterName/brokers"
|
path="/ui/clusters/:clusterName/brokers"
|
||||||
component={BrokersContainer}
|
component={BrokersContainer}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route path="/ui/clusters/:clusterName/topics" component={Topics} />
|
||||||
path="/ui/clusters/:clusterName/topics"
|
|
||||||
component={TopicsContainer}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
path="/ui/clusters/:clusterName/consumer-groups"
|
path="/ui/clusters/:clusterName/consumer-groups"
|
||||||
component={ConsumersGroupsContainer}
|
component={ConsumersGroupsContainer}
|
||||||
|
|
|
@ -1,91 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { ClusterName, TopicName } from 'redux/interfaces';
|
|
||||||
import { Topic, TopicDetails } from 'generated-sources';
|
|
||||||
import MetricsWrapper from 'components/common/Dashboard/MetricsWrapper';
|
|
||||||
import Indicator from 'components/common/Dashboard/Indicator';
|
|
||||||
import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';
|
|
||||||
|
|
||||||
interface Props extends Topic, TopicDetails {
|
|
||||||
isFetched: boolean;
|
|
||||||
clusterName: ClusterName;
|
|
||||||
topicName: TopicName;
|
|
||||||
fetchTopicDetails: (clusterName: ClusterName, topicName: TopicName) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Overview: React.FC<Props> = ({
|
|
||||||
isFetched,
|
|
||||||
clusterName,
|
|
||||||
topicName,
|
|
||||||
partitions,
|
|
||||||
underReplicatedPartitions,
|
|
||||||
inSyncReplicas,
|
|
||||||
replicas,
|
|
||||||
partitionCount,
|
|
||||||
internal,
|
|
||||||
replicationFactor,
|
|
||||||
segmentSize,
|
|
||||||
segmentCount,
|
|
||||||
fetchTopicDetails,
|
|
||||||
}) => {
|
|
||||||
React.useEffect(() => {
|
|
||||||
fetchTopicDetails(clusterName, topicName);
|
|
||||||
}, [fetchTopicDetails, clusterName, topicName]);
|
|
||||||
|
|
||||||
if (!isFetched) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<MetricsWrapper>
|
|
||||||
<Indicator label="Partitions">{partitionCount}</Indicator>
|
|
||||||
<Indicator label="Replication Factor">{replicationFactor}</Indicator>
|
|
||||||
<Indicator label="URP" title="Under replicated partitions">
|
|
||||||
{underReplicatedPartitions}
|
|
||||||
</Indicator>
|
|
||||||
<Indicator label="In sync replicas">
|
|
||||||
{inSyncReplicas}
|
|
||||||
<span className="subtitle has-text-weight-light">
|
|
||||||
{' '}
|
|
||||||
of
|
|
||||||
{replicas}
|
|
||||||
</span>
|
|
||||||
</Indicator>
|
|
||||||
<Indicator label="Type">
|
|
||||||
<span className="tag is-primary">
|
|
||||||
{internal ? 'Internal' : 'External'}
|
|
||||||
</span>
|
|
||||||
</Indicator>
|
|
||||||
<Indicator label="Segment Size" title="">
|
|
||||||
<BytesFormatted value={segmentSize} />
|
|
||||||
</Indicator>
|
|
||||||
<Indicator label="Segment count">{segmentCount}</Indicator>
|
|
||||||
</MetricsWrapper>
|
|
||||||
<div className="box">
|
|
||||||
<table className="table is-striped is-fullwidth">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Partition ID</th>
|
|
||||||
<th>Broker leader</th>
|
|
||||||
<th>Min offset</th>
|
|
||||||
<th>Max offset</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{partitions &&
|
|
||||||
partitions.map(({ partition, leader, offsetMin, offsetMax }) => (
|
|
||||||
<tr key={`partition-list-item-key-${partition}`}>
|
|
||||||
<td>{partition}</td>
|
|
||||||
<td>{leader}</td>
|
|
||||||
<td>{offsetMin}</td>
|
|
||||||
<td>{offsetMax}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Overview;
|
|
|
@ -1,16 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
to: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SettingsEditButton: React.FC<Props> = ({ to }) => (
|
|
||||||
<Link to={to}>
|
|
||||||
<button type="button" className="button is-small is-warning">
|
|
||||||
Edit settings
|
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default SettingsEditButton;
|
|
|
@ -1,64 +0,0 @@
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import {
|
|
||||||
RootState,
|
|
||||||
ClusterName,
|
|
||||||
TopicName,
|
|
||||||
Action,
|
|
||||||
TopicFormDataRaw,
|
|
||||||
} from 'redux/interfaces';
|
|
||||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
|
||||||
import {
|
|
||||||
updateTopic,
|
|
||||||
fetchTopicConfig,
|
|
||||||
fetchTopicDetails,
|
|
||||||
} from 'redux/actions';
|
|
||||||
import {
|
|
||||||
getTopicConfigFetched,
|
|
||||||
getTopicUpdated,
|
|
||||||
getIsTopicDetailsFetched,
|
|
||||||
getFullTopic,
|
|
||||||
} from 'redux/reducers/topics/selectors';
|
|
||||||
import { clusterTopicPath } from 'lib/paths';
|
|
||||||
import { ThunkDispatch } from 'redux-thunk';
|
|
||||||
|
|
||||||
import Edit from './Edit';
|
|
||||||
|
|
||||||
interface RouteProps {
|
|
||||||
clusterName: ClusterName;
|
|
||||||
topicName: TopicName;
|
|
||||||
}
|
|
||||||
|
|
||||||
type OwnProps = RouteComponentProps<RouteProps>;
|
|
||||||
|
|
||||||
const mapStateToProps = (
|
|
||||||
state: RootState,
|
|
||||||
{
|
|
||||||
match: {
|
|
||||||
params: { topicName, clusterName },
|
|
||||||
},
|
|
||||||
}: OwnProps
|
|
||||||
) => ({
|
|
||||||
clusterName,
|
|
||||||
topicName,
|
|
||||||
topic: getFullTopic(state, topicName),
|
|
||||||
isFetched: getTopicConfigFetched(state),
|
|
||||||
isTopicDetailsFetched: getIsTopicDetailsFetched(state),
|
|
||||||
isTopicUpdated: getTopicUpdated(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = (
|
|
||||||
dispatch: ThunkDispatch<RootState, undefined, Action>,
|
|
||||||
{ history }: OwnProps
|
|
||||||
) => ({
|
|
||||||
fetchTopicDetails: (clusterName: ClusterName, topicName: TopicName) =>
|
|
||||||
dispatch(fetchTopicDetails(clusterName, topicName)),
|
|
||||||
fetchTopicConfig: (clusterName: ClusterName, topicName: TopicName) =>
|
|
||||||
dispatch(fetchTopicConfig(clusterName, topicName)),
|
|
||||||
updateTopic: (clusterName: ClusterName, form: TopicFormDataRaw) =>
|
|
||||||
dispatch(updateTopic(clusterName, form)),
|
|
||||||
redirectToTopicPath: (clusterName: ClusterName, topicName: TopicName) => {
|
|
||||||
history.push(clusterTopicPath(clusterName, topicName));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Edit));
|
|
|
@ -1,28 +1,46 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { TopicWithDetailedInfo, ClusterName } from 'redux/interfaces';
|
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, useParams } from 'react-router-dom';
|
||||||
import { clusterTopicNewPath } from 'lib/paths';
|
import { clusterTopicNewPath } from 'lib/paths';
|
||||||
|
import usePagination from 'lib/hooks/usePagination';
|
||||||
|
import { FetchTopicsListParams } from 'redux/actions';
|
||||||
import ClusterContext from 'components/contexts/ClusterContext';
|
import ClusterContext from 'components/contexts/ClusterContext';
|
||||||
|
import PageLoader from 'components/common/PageLoader/PageLoader';
|
||||||
import ListItem from './ListItem';
|
import ListItem from './ListItem';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
clusterName: ClusterName;
|
areTopicsFetching: boolean;
|
||||||
topics: TopicWithDetailedInfo[];
|
topics: TopicWithDetailedInfo[];
|
||||||
externalTopics: TopicWithDetailedInfo[];
|
externalTopics: TopicWithDetailedInfo[];
|
||||||
|
fetchTopicsList(props: FetchTopicsListParams): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const List: React.FC<Props> = ({ clusterName, topics, externalTopics }) => {
|
const List: React.FC<Props> = ({
|
||||||
|
areTopicsFetching,
|
||||||
|
topics,
|
||||||
|
externalTopics,
|
||||||
|
fetchTopicsList,
|
||||||
|
}) => {
|
||||||
|
const { isReadOnly } = React.useContext(ClusterContext);
|
||||||
|
const { clusterName } = useParams<{ clusterName: ClusterName }>();
|
||||||
|
const { page, perPage } = usePagination();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
fetchTopicsList({ clusterName, page, perPage });
|
||||||
|
}, [fetchTopicsList, clusterName, page, perPage]);
|
||||||
|
|
||||||
const [showInternal, setShowInternal] = React.useState<boolean>(true);
|
const [showInternal, setShowInternal] = React.useState<boolean>(true);
|
||||||
|
|
||||||
const handleSwitch = () => setShowInternal(!showInternal);
|
const handleSwitch = React.useCallback(() => {
|
||||||
const { isReadOnly } = React.useContext(ClusterContext);
|
setShowInternal(!showInternal);
|
||||||
|
}, [showInternal]);
|
||||||
|
|
||||||
const items = showInternal ? topics : externalTopics;
|
const items = showInternal ? topics : externalTopics;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<Breadcrumb>All Topics</Breadcrumb>
|
<Breadcrumb>{showInternal ? `All Topics` : `External Topics`}</Breadcrumb>
|
||||||
|
|
||||||
<div className="box">
|
<div className="box">
|
||||||
<div className="level">
|
<div className="level">
|
||||||
<div className="level-item level-left">
|
<div className="level-item level-left">
|
||||||
|
@ -50,23 +68,33 @@ const List: React.FC<Props> = ({ clusterName, topics, externalTopics }) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="box">
|
{areTopicsFetching ? (
|
||||||
<table className="table is-striped is-fullwidth">
|
<PageLoader />
|
||||||
<thead>
|
) : (
|
||||||
<tr>
|
<div className="box">
|
||||||
<th>Topic Name</th>
|
<table className="table is-striped is-fullwidth">
|
||||||
<th>Total Partitions</th>
|
<thead>
|
||||||
<th>Out of sync replicas</th>
|
<tr>
|
||||||
<th>Type</th>
|
<th>Topic Name</th>
|
||||||
</tr>
|
<th>Total Partitions</th>
|
||||||
</thead>
|
<th>Out of sync replicas</th>
|
||||||
<tbody>
|
<th>Type</th>
|
||||||
{items.map((topic) => (
|
</tr>
|
||||||
<ListItem key={topic.id} topic={topic} />
|
</thead>
|
||||||
))}
|
<tbody>
|
||||||
</tbody>
|
{items.length > 0 ? (
|
||||||
</table>
|
items.map((topic) => (
|
||||||
</div>
|
<ListItem key={topic.name} topic={topic} />
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={10}>No topics found</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,29 +1,21 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { ClusterName, RootState } from 'redux/interfaces';
|
import { RootState } from 'redux/interfaces';
|
||||||
|
import { fetchTopicsList } from 'redux/actions';
|
||||||
import {
|
import {
|
||||||
getTopicList,
|
getTopicList,
|
||||||
getExternalTopicList,
|
getExternalTopicList,
|
||||||
|
getAreTopicsFetching,
|
||||||
} from 'redux/reducers/topics/selectors';
|
} from 'redux/reducers/topics/selectors';
|
||||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
|
||||||
import List from './List';
|
import List from './List';
|
||||||
|
|
||||||
interface RouteProps {
|
const mapStateToProps = (state: RootState) => ({
|
||||||
clusterName: ClusterName;
|
areTopicsFetching: getAreTopicsFetching(state),
|
||||||
}
|
|
||||||
|
|
||||||
type OwnProps = RouteComponentProps<RouteProps>;
|
|
||||||
|
|
||||||
const mapStateToProps = (
|
|
||||||
state: RootState,
|
|
||||||
{
|
|
||||||
match: {
|
|
||||||
params: { clusterName },
|
|
||||||
},
|
|
||||||
}: OwnProps
|
|
||||||
) => ({
|
|
||||||
clusterName,
|
|
||||||
topics: getTopicList(state),
|
topics: getTopicList(state),
|
||||||
externalTopics: getExternalTopicList(state),
|
externalTopics: getExternalTopicList(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default withRouter(connect(mapStateToProps)(List));
|
const mapDispatchToProps = {
|
||||||
|
fetchTopicsList,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(List);
|
||||||
|
|
|
@ -1,22 +1,43 @@
|
||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { StaticRouter } from 'react-router-dom';
|
||||||
import ClusterContext from 'components/contexts/ClusterContext';
|
import ClusterContext from 'components/contexts/ClusterContext';
|
||||||
import List from '../List';
|
import List from '../List';
|
||||||
|
|
||||||
describe('List', () => {
|
describe('List', () => {
|
||||||
describe('when it has readonly flag', () => {
|
describe('when it has readonly flag', () => {
|
||||||
it('does not render the Add a Topic button', () => {
|
it('does not render the Add a Topic button', () => {
|
||||||
const props = {
|
|
||||||
clusterName: 'Cluster',
|
|
||||||
topics: [],
|
|
||||||
externalTopics: [],
|
|
||||||
};
|
|
||||||
const component = mount(
|
const component = mount(
|
||||||
<ClusterContext.Provider value={{ isReadOnly: true }}>
|
<StaticRouter>
|
||||||
<List {...props} />
|
<ClusterContext.Provider value={{ isReadOnly: true }}>
|
||||||
</ClusterContext.Provider>
|
<List
|
||||||
|
areTopicsFetching={false}
|
||||||
|
topics={[]}
|
||||||
|
externalTopics={[]}
|
||||||
|
fetchTopicsList={jest.fn()}
|
||||||
|
/>
|
||||||
|
</ClusterContext.Provider>
|
||||||
|
</StaticRouter>
|
||||||
);
|
);
|
||||||
expect(component.exists('NavLink')).toBeFalsy();
|
expect(component.exists('NavLink')).toBeFalsy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when it does not have readonly flag', () => {
|
||||||
|
it('renders the Add a Topic button', () => {
|
||||||
|
const component = mount(
|
||||||
|
<StaticRouter>
|
||||||
|
<ClusterContext.Provider value={{ isReadOnly: false }}>
|
||||||
|
<List
|
||||||
|
areTopicsFetching={false}
|
||||||
|
topics={[]}
|
||||||
|
externalTopics={[]}
|
||||||
|
fetchTopicsList={jest.fn()}
|
||||||
|
/>
|
||||||
|
</ClusterContext.Provider>
|
||||||
|
</StaticRouter>
|
||||||
|
);
|
||||||
|
expect(component.exists('NavLink')).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ClusterName, TopicName } from 'redux/interfaces';
|
import { ClusterName, TopicName } from 'redux/interfaces';
|
||||||
import { Topic, TopicDetails } from 'generated-sources';
|
import { Topic, TopicDetails } from 'generated-sources';
|
||||||
import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
|
import { NavLink, Switch, Route, Link } from 'react-router-dom';
|
||||||
import { NavLink, Switch, Route } from 'react-router-dom';
|
|
||||||
import {
|
import {
|
||||||
clusterTopicsPath,
|
|
||||||
clusterTopicSettingsPath,
|
clusterTopicSettingsPath,
|
||||||
clusterTopicPath,
|
clusterTopicPath,
|
||||||
clusterTopicMessagesPath,
|
clusterTopicMessagesPath,
|
||||||
|
@ -14,7 +12,6 @@ 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';
|
||||||
import SettingsEditButton from './Settings/SettingsEditButton';
|
|
||||||
|
|
||||||
interface Props extends Topic, TopicDetails {
|
interface Props extends Topic, TopicDetails {
|
||||||
clusterName: ClusterName;
|
clusterName: ClusterName;
|
||||||
|
@ -23,27 +20,11 @@ interface Props extends Topic, TopicDetails {
|
||||||
|
|
||||||
const Details: React.FC<Props> = ({ clusterName, topicName }) => {
|
const Details: React.FC<Props> = ({ clusterName, topicName }) => {
|
||||||
const { isReadOnly } = React.useContext(ClusterContext);
|
const { isReadOnly } = React.useContext(ClusterContext);
|
||||||
return (
|
|
||||||
<div className="section">
|
|
||||||
<div className="level">
|
|
||||||
<div className="level-item level-left">
|
|
||||||
<Breadcrumb
|
|
||||||
links={[
|
|
||||||
{ href: clusterTopicsPath(clusterName), label: 'All Topics' },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{topicName}
|
|
||||||
</Breadcrumb>
|
|
||||||
</div>
|
|
||||||
{!isReadOnly && (
|
|
||||||
<SettingsEditButton
|
|
||||||
to={clusterTopicsTopicEditPath(clusterName, topicName)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="box">
|
return (
|
||||||
<nav className="navbar" role="navigation">
|
<div className="box">
|
||||||
|
<nav className="navbar" role="navigation">
|
||||||
|
<div className="navbar-start">
|
||||||
<NavLink
|
<NavLink
|
||||||
exact
|
exact
|
||||||
to={clusterTopicPath(clusterName, topicName)}
|
to={clusterTopicPath(clusterName, topicName)}
|
||||||
|
@ -68,26 +49,36 @@ const Details: React.FC<Props> = ({ clusterName, topicName }) => {
|
||||||
>
|
>
|
||||||
Settings
|
Settings
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</nav>
|
</div>
|
||||||
<br />
|
<div className="navbar-end">
|
||||||
<Switch>
|
{!isReadOnly && (
|
||||||
<Route
|
<Link
|
||||||
exact
|
to={clusterTopicsTopicEditPath(clusterName, topicName)}
|
||||||
path="/ui/clusters/:clusterName/topics/:topicName/messages"
|
className="button"
|
||||||
component={MessagesContainer}
|
>
|
||||||
/>
|
Edit settings
|
||||||
<Route
|
</Link>
|
||||||
exact
|
)}
|
||||||
path="/ui/clusters/:clusterName/topics/:topicName/settings"
|
</div>
|
||||||
component={SettingsContainer}
|
</nav>
|
||||||
/>
|
<br />
|
||||||
<Route
|
<Switch>
|
||||||
exact
|
<Route
|
||||||
path="/ui/clusters/:clusterName/topics/:topicName"
|
exact
|
||||||
component={OverviewContainer}
|
path="/ui/clusters/:clusterName/topics/:topicName/messages"
|
||||||
/>
|
component={MessagesContainer}
|
||||||
</Switch>
|
/>
|
||||||
</div>
|
<Route
|
||||||
|
exact
|
||||||
|
path="/ui/clusters/:clusterName/topics/:topicName/settings"
|
||||||
|
component={SettingsContainer}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path="/ui/clusters/:clusterName/topics/:topicName"
|
||||||
|
component={OverviewContainer}
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import MessageItem from 'components/Topics/Details/Messages/MessageItem';
|
import MessageItem from 'components/Topics/Topic/Details/Messages/MessageItem';
|
||||||
import { messages } from './fixtures';
|
import { messages } from './fixtures';
|
||||||
|
|
||||||
jest.mock('date-fns', () => ({
|
jest.mock('date-fns', () => ({
|
|
@ -3,8 +3,10 @@ import { Provider } from 'react-redux';
|
||||||
import { mount, shallow } from 'enzyme';
|
import { mount, shallow } from 'enzyme';
|
||||||
import * as useDebounce from 'use-debounce';
|
import * as useDebounce from 'use-debounce';
|
||||||
import DatePicker from 'react-datepicker';
|
import DatePicker from 'react-datepicker';
|
||||||
import Messages, { Props } from 'components/Topics/Details/Messages/Messages';
|
import Messages, {
|
||||||
import MessagesContainer from 'components/Topics/Details/Messages/MessagesContainer';
|
Props,
|
||||||
|
} from 'components/Topics/Topic/Details/Messages/Messages';
|
||||||
|
import MessagesContainer from 'components/Topics/Topic/Details/Messages/MessagesContainer';
|
||||||
import PageLoader from 'components/common/PageLoader/PageLoader';
|
import PageLoader from 'components/common/PageLoader/PageLoader';
|
||||||
import configureStore from 'redux/store/configureStore';
|
import configureStore from 'redux/store/configureStore';
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import MessagesTable, {
|
import MessagesTable, {
|
||||||
MessagesTableProp,
|
MessagesTableProp,
|
||||||
} from 'components/Topics/Details/Messages/MessagesTable';
|
} from 'components/Topics/Topic/Details/Messages/MessagesTable';
|
||||||
import { messages } from './fixtures';
|
import { messages } from './fixtures';
|
||||||
|
|
||||||
jest.mock('date-fns', () => ({
|
jest.mock('date-fns', () => ({
|
|
@ -0,0 +1,70 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Topic, TopicDetails } from 'generated-sources';
|
||||||
|
import MetricsWrapper from 'components/common/Dashboard/MetricsWrapper';
|
||||||
|
import Indicator from 'components/common/Dashboard/Indicator';
|
||||||
|
import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';
|
||||||
|
|
||||||
|
interface Props extends Topic, TopicDetails {}
|
||||||
|
|
||||||
|
const Overview: React.FC<Props> = ({
|
||||||
|
partitions,
|
||||||
|
underReplicatedPartitions,
|
||||||
|
inSyncReplicas,
|
||||||
|
replicas,
|
||||||
|
partitionCount,
|
||||||
|
internal,
|
||||||
|
replicationFactor,
|
||||||
|
segmentSize,
|
||||||
|
segmentCount,
|
||||||
|
}) => (
|
||||||
|
<>
|
||||||
|
<MetricsWrapper>
|
||||||
|
<Indicator label="Partitions">{partitionCount}</Indicator>
|
||||||
|
<Indicator label="Replication Factor">{replicationFactor}</Indicator>
|
||||||
|
<Indicator label="URP" title="Under replicated partitions">
|
||||||
|
{underReplicatedPartitions}
|
||||||
|
</Indicator>
|
||||||
|
<Indicator label="In sync replicas">
|
||||||
|
{inSyncReplicas}
|
||||||
|
<span className="subtitle has-text-weight-light">
|
||||||
|
{' '}
|
||||||
|
of
|
||||||
|
{replicas}
|
||||||
|
</span>
|
||||||
|
</Indicator>
|
||||||
|
<Indicator label="Type">
|
||||||
|
<span className="tag is-primary">
|
||||||
|
{internal ? 'Internal' : 'External'}
|
||||||
|
</span>
|
||||||
|
</Indicator>
|
||||||
|
<Indicator label="Segment Size" title="">
|
||||||
|
<BytesFormatted value={segmentSize} />
|
||||||
|
</Indicator>
|
||||||
|
<Indicator label="Segment count">{segmentCount}</Indicator>
|
||||||
|
</MetricsWrapper>
|
||||||
|
<div className="box">
|
||||||
|
<table className="table is-striped is-fullwidth">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Partition ID</th>
|
||||||
|
<th>Broker leader</th>
|
||||||
|
<th>Min offset</th>
|
||||||
|
<th>Max offset</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{partitions?.map(({ partition, leader, offsetMin, offsetMax }) => (
|
||||||
|
<tr key={`partition-list-item-key-${partition}`}>
|
||||||
|
<td>{partition}</td>
|
||||||
|
<td>{leader}</td>
|
||||||
|
<td>{offsetMin}</td>
|
||||||
|
<td>{offsetMax}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Overview;
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { RootState, TopicName, ClusterName } from 'redux/interfaces';
|
||||||
|
import { getTopicByName } from 'redux/reducers/topics/selectors';
|
||||||
|
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||||
|
import Overview from './Overview';
|
||||||
|
|
||||||
|
interface RouteProps {
|
||||||
|
clusterName: ClusterName;
|
||||||
|
topicName: TopicName;
|
||||||
|
}
|
||||||
|
|
||||||
|
type OwnProps = RouteComponentProps<RouteProps>;
|
||||||
|
|
||||||
|
const mapStateToProps = (
|
||||||
|
state: RootState,
|
||||||
|
{
|
||||||
|
match: {
|
||||||
|
params: { topicName },
|
||||||
|
},
|
||||||
|
}: OwnProps
|
||||||
|
) => ({
|
||||||
|
...getTopicByName(state, topicName),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default withRouter(connect(mapStateToProps)(Overview));
|
|
@ -1,5 +1,6 @@
|
||||||
|
import { TopicConfig } from 'generated-sources';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ClusterName, TopicName, TopicConfig } from 'redux/interfaces';
|
import { ClusterName, TopicName } from 'redux/interfaces';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
clusterName: ClusterName;
|
clusterName: ClusterName;
|
||||||
|
@ -56,7 +57,7 @@ const Sertings: React.FC<Props> = ({
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{config.map((item) => (
|
{config.map((item) => (
|
||||||
<ConfigListItem key={item.id} config={item} />
|
<ConfigListItem key={item.name} config={item} />
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
|
@ -10,22 +10,18 @@ import {
|
||||||
import { TopicConfig } from 'generated-sources';
|
import { TopicConfig } from 'generated-sources';
|
||||||
import { useForm, FormProvider } from 'react-hook-form';
|
import { useForm, FormProvider } from 'react-hook-form';
|
||||||
import { camelCase } from 'lodash';
|
import { camelCase } from 'lodash';
|
||||||
|
import TopicForm from 'components/Topics/shared/Form/TopicForm';
|
||||||
import TopicForm from '../shared/Form/TopicForm';
|
import { clusterTopicPath } from 'lib/paths';
|
||||||
import FormBreadcrumbs from '../shared/Form/FormBreadcrumbs';
|
import { useHistory } from 'react-router';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
clusterName: ClusterName;
|
clusterName: ClusterName;
|
||||||
topicName: TopicName;
|
topicName: TopicName;
|
||||||
topic?: TopicWithDetailedInfo;
|
topic?: TopicWithDetailedInfo;
|
||||||
isFetched: boolean;
|
isFetched: boolean;
|
||||||
isTopicDetailsFetched: boolean;
|
|
||||||
isTopicUpdated: boolean;
|
isTopicUpdated: boolean;
|
||||||
fetchTopicDetails: (clusterName: ClusterName, topicName: TopicName) => void;
|
|
||||||
fetchTopicConfig: (clusterName: ClusterName, topicName: TopicName) => void;
|
fetchTopicConfig: (clusterName: ClusterName, topicName: TopicName) => void;
|
||||||
updateTopic: (clusterName: ClusterName, form: TopicFormDataRaw) => void;
|
updateTopic: (clusterName: ClusterName, form: TopicFormDataRaw) => void;
|
||||||
redirectToTopicPath: (clusterName: ClusterName, topicName: TopicName) => void;
|
|
||||||
resetUploadedState: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULTS = {
|
const DEFAULTS = {
|
||||||
|
@ -68,32 +64,29 @@ const Edit: React.FC<Props> = ({
|
||||||
topicName,
|
topicName,
|
||||||
topic,
|
topic,
|
||||||
isFetched,
|
isFetched,
|
||||||
isTopicDetailsFetched,
|
|
||||||
isTopicUpdated,
|
isTopicUpdated,
|
||||||
fetchTopicDetails,
|
|
||||||
fetchTopicConfig,
|
fetchTopicConfig,
|
||||||
updateTopic,
|
updateTopic,
|
||||||
redirectToTopicPath,
|
|
||||||
}) => {
|
}) => {
|
||||||
const defaultValues = topicParams(topic);
|
const defaultValues = topicParams(topic);
|
||||||
|
|
||||||
const methods = useForm<TopicFormDataRaw>({ defaultValues });
|
const methods = useForm<TopicFormDataRaw>({ defaultValues });
|
||||||
|
|
||||||
const [isSubmitting, setIsSubmitting] = React.useState<boolean>(false);
|
const [isSubmitting, setIsSubmitting] = React.useState<boolean>(false);
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
fetchTopicConfig(clusterName, topicName);
|
fetchTopicConfig(clusterName, topicName);
|
||||||
fetchTopicDetails(clusterName, topicName);
|
}, [fetchTopicConfig, clusterName, topicName]);
|
||||||
}, [fetchTopicConfig, fetchTopicDetails, clusterName, topicName]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (isSubmitting && isTopicUpdated) {
|
if (isSubmitting && isTopicUpdated) {
|
||||||
const { name } = methods.getValues();
|
const { name } = methods.getValues();
|
||||||
redirectToTopicPath(clusterName, name);
|
history.push(clusterTopicPath(clusterName, name));
|
||||||
}
|
}
|
||||||
}, [isSubmitting, isTopicUpdated, redirectToTopicPath, clusterName, methods]);
|
}, [isSubmitting, isTopicUpdated, clusterTopicPath, clusterName, methods]);
|
||||||
|
|
||||||
if (!isFetched || !isTopicDetailsFetched || !topic || !topic.config) {
|
if (!isFetched || !topic || !topic.config) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,27 +109,17 @@ const Edit: React.FC<Props> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="section">
|
<div className="box">
|
||||||
<div className="level">
|
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||||
<FormBreadcrumbs
|
<FormProvider {...methods}>
|
||||||
clusterName={clusterName}
|
<TopicForm
|
||||||
topicName={topicName}
|
topicName={topicName}
|
||||||
current="Edit Topic"
|
config={config}
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
|
isEditing
|
||||||
|
onSubmit={methods.handleSubmit(onSubmit)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</FormProvider>
|
||||||
|
|
||||||
<div className="box">
|
|
||||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
|
||||||
<FormProvider {...methods}>
|
|
||||||
<TopicForm
|
|
||||||
topicName={topicName}
|
|
||||||
config={config}
|
|
||||||
isSubmitting={isSubmitting}
|
|
||||||
isEditing
|
|
||||||
onSubmit={methods.handleSubmit(onSubmit)}
|
|
||||||
/>
|
|
||||||
</FormProvider>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
|
@ -1,12 +1,14 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { fetchTopicDetails } from 'redux/actions';
|
import { RootState, ClusterName, TopicName } from 'redux/interfaces';
|
||||||
import { RootState, TopicName, ClusterName } from 'redux/interfaces';
|
|
||||||
import {
|
|
||||||
getTopicByName,
|
|
||||||
getIsTopicDetailsFetched,
|
|
||||||
} from 'redux/reducers/topics/selectors';
|
|
||||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||||
import Overview from './Overview';
|
import { updateTopic, fetchTopicConfig } from 'redux/actions';
|
||||||
|
import {
|
||||||
|
getTopicConfigFetched,
|
||||||
|
getTopicUpdated,
|
||||||
|
getFullTopic,
|
||||||
|
} from 'redux/reducers/topics/selectors';
|
||||||
|
|
||||||
|
import Edit from './Edit';
|
||||||
|
|
||||||
interface RouteProps {
|
interface RouteProps {
|
||||||
clusterName: ClusterName;
|
clusterName: ClusterName;
|
||||||
|
@ -25,15 +27,14 @@ const mapStateToProps = (
|
||||||
) => ({
|
) => ({
|
||||||
clusterName,
|
clusterName,
|
||||||
topicName,
|
topicName,
|
||||||
isFetched: getIsTopicDetailsFetched(state),
|
topic: getFullTopic(state, topicName),
|
||||||
...getTopicByName(state, topicName),
|
isFetched: getTopicConfigFetched(state),
|
||||||
|
isTopicUpdated: getTopicUpdated(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
fetchTopicDetails: (clusterName: ClusterName, topicName: TopicName) =>
|
fetchTopicConfig,
|
||||||
fetchTopicDetails(clusterName, topicName),
|
updateTopic,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withRouter(
|
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Edit));
|
||||||
connect(mapStateToProps, mapDispatchToProps)(Overview)
|
|
||||||
);
|
|
80
kafka-ui-react-app/src/components/Topics/Topic/Topic.tsx
Normal file
80
kafka-ui-react-app/src/components/Topics/Topic/Topic.tsx
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Switch, Route, useParams } from 'react-router-dom';
|
||||||
|
import { clusterTopicPath, clusterTopicsPath } from 'lib/paths';
|
||||||
|
import { ClusterName, TopicName } from 'redux/interfaces';
|
||||||
|
import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
|
||||||
|
import EditContainer from 'components/Topics/Topic/Edit/EditContainer';
|
||||||
|
import DetailsContainer from 'components/Topics/Topic/Details/DetailsContainer';
|
||||||
|
import PageLoader from 'components/common/PageLoader/PageLoader';
|
||||||
|
|
||||||
|
interface RouterParams {
|
||||||
|
clusterName: ClusterName;
|
||||||
|
topicName: TopicName;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TopicProps {
|
||||||
|
isTopicFetching: boolean;
|
||||||
|
fetchTopicDetails: (clusterName: ClusterName, topicName: TopicName) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Topic: React.FC<TopicProps> = ({
|
||||||
|
isTopicFetching,
|
||||||
|
fetchTopicDetails,
|
||||||
|
}) => {
|
||||||
|
const { clusterName, topicName } = useParams<RouterParams>();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
fetchTopicDetails(clusterName, topicName);
|
||||||
|
}, [fetchTopicDetails, clusterName, topicName]);
|
||||||
|
|
||||||
|
const rootBreadcrumbLinks = [
|
||||||
|
{
|
||||||
|
href: clusterTopicsPath(clusterName),
|
||||||
|
label: 'All Topics',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const childBreadcrumbLinks = [
|
||||||
|
...rootBreadcrumbLinks,
|
||||||
|
{
|
||||||
|
href: clusterTopicPath(clusterName, topicName),
|
||||||
|
label: topicName,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const topicPageUrl = '/ui/clusters/:clusterName/topics/:topicName';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="section">
|
||||||
|
<div className="level">
|
||||||
|
<div className="level-item level-left">
|
||||||
|
<Switch>
|
||||||
|
<Route exact path={`${topicPageUrl}/edit`}>
|
||||||
|
<Breadcrumb links={childBreadcrumbLinks}>Edit</Breadcrumb>
|
||||||
|
</Route>
|
||||||
|
<Route path={topicPageUrl}>
|
||||||
|
<Breadcrumb links={rootBreadcrumbLinks}>{topicName}</Breadcrumb>
|
||||||
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{isTopicFetching ? (
|
||||||
|
<PageLoader />
|
||||||
|
) : (
|
||||||
|
<Switch>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path="/ui/clusters/:clusterName/topics/:topicName/edit"
|
||||||
|
component={EditContainer}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/ui/clusters/:clusterName/topics/:topicName"
|
||||||
|
component={DetailsContainer}
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Topic;
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { RootState } from 'redux/interfaces';
|
||||||
|
import { fetchTopicDetails } from 'redux/actions';
|
||||||
|
import { getIsTopicDetailsFetching } from 'redux/reducers/topics/selectors';
|
||||||
|
import Topic from './Topic';
|
||||||
|
|
||||||
|
const mapStateToProps = (state: RootState) => ({
|
||||||
|
isTopicFetching: getIsTopicDetailsFetching(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
fetchTopicDetails,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(Topic);
|
|
@ -1,54 +1,26 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ClusterName } from 'redux/interfaces';
|
|
||||||
import { Switch, Route } from 'react-router-dom';
|
import { Switch, Route } from 'react-router-dom';
|
||||||
import PageLoader from 'components/common/PageLoader/PageLoader';
|
|
||||||
import EditContainer from 'components/Topics/Edit/EditContainer';
|
|
||||||
import ListContainer from './List/ListContainer';
|
import ListContainer from './List/ListContainer';
|
||||||
import DetailsContainer from './Details/DetailsContainer';
|
import TopicContainer from './Topic/TopicContainer';
|
||||||
import NewContainer from './New/NewContainer';
|
import NewContainer from './New/NewContainer';
|
||||||
|
|
||||||
interface Props {
|
const Topics: React.FC = () => (
|
||||||
clusterName: ClusterName;
|
<Switch>
|
||||||
isFetched: boolean;
|
<Route
|
||||||
fetchTopicsList: (clusterName: ClusterName) => void;
|
exact
|
||||||
}
|
path="/ui/clusters/:clusterName/topics"
|
||||||
|
component={ListContainer}
|
||||||
const Topics: React.FC<Props> = ({
|
/>
|
||||||
clusterName,
|
<Route
|
||||||
isFetched,
|
exact
|
||||||
fetchTopicsList,
|
path="/ui/clusters/:clusterName/topics/new"
|
||||||
}) => {
|
component={NewContainer}
|
||||||
React.useEffect(() => {
|
/>
|
||||||
fetchTopicsList(clusterName);
|
<Route
|
||||||
}, [fetchTopicsList, clusterName]);
|
path="/ui/clusters/:clusterName/topics/:topicName"
|
||||||
|
component={TopicContainer}
|
||||||
if (isFetched) {
|
/>
|
||||||
return (
|
</Switch>
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <PageLoader />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Topics;
|
export default Topics;
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
import { connect } from 'react-redux';
|
|
||||||
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 Topics from './Topics';
|
|
||||||
|
|
||||||
interface RouteProps {
|
|
||||||
clusterName: ClusterName;
|
|
||||||
}
|
|
||||||
|
|
||||||
type OwnProps = RouteComponentProps<RouteProps>;
|
|
||||||
|
|
||||||
const mapStateToProps = (
|
|
||||||
state: RootState,
|
|
||||||
{
|
|
||||||
match: {
|
|
||||||
params: { clusterName },
|
|
||||||
},
|
|
||||||
}: OwnProps
|
|
||||||
) => ({
|
|
||||||
isFetched: getIsTopicListFetched(state),
|
|
||||||
clusterName,
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
fetchTopicsList: (clusterName: ClusterName) => fetchTopicsList(clusterName),
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(Topics);
|
|
|
@ -52,7 +52,7 @@ const CustomParams: React.FC<Props> = ({ isSubmitting, config }) => {
|
||||||
...formCustomParams,
|
...formCustomParams,
|
||||||
byIndex: {
|
byIndex: {
|
||||||
...formCustomParams.byIndex,
|
...formCustomParams.byIndex,
|
||||||
[newIndex]: { name: '', value: '', id: v4() },
|
[newIndex]: { name: '', value: '' },
|
||||||
},
|
},
|
||||||
allIndexes: [newIndex, ...formCustomParams.allIndexes],
|
allIndexes: [newIndex, ...formCustomParams.allIndexes],
|
||||||
});
|
});
|
||||||
|
|
15
kafka-ui-react-app/src/lib/hooks/usePagination.ts
Normal file
15
kafka-ui-react-app/src/lib/hooks/usePagination.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { useLocation } from 'react-router';
|
||||||
|
|
||||||
|
const usePagination = () => {
|
||||||
|
const params = new URLSearchParams(useLocation().search);
|
||||||
|
|
||||||
|
const page = params.get('page');
|
||||||
|
const perPage = params.get('perPage');
|
||||||
|
|
||||||
|
return {
|
||||||
|
page: page ? Number(page) : undefined,
|
||||||
|
perPage: perPage ? Number(perPage) : undefined,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default usePagination;
|
|
@ -1,5 +1,5 @@
|
||||||
import { createAsyncAction } from 'typesafe-actions';
|
import { createAsyncAction } from 'typesafe-actions';
|
||||||
import { TopicName, ConsumerGroupID } from 'redux/interfaces';
|
import { TopicName, ConsumerGroupID, TopicsState } from 'redux/interfaces';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Cluster,
|
Cluster,
|
||||||
|
@ -50,7 +50,7 @@ export const fetchTopicsListAction = createAsyncAction(
|
||||||
'GET_TOPICS__REQUEST',
|
'GET_TOPICS__REQUEST',
|
||||||
'GET_TOPICS__SUCCESS',
|
'GET_TOPICS__SUCCESS',
|
||||||
'GET_TOPICS__FAILURE'
|
'GET_TOPICS__FAILURE'
|
||||||
)<undefined, Topic[], undefined>();
|
)<undefined, TopicsState, undefined>();
|
||||||
|
|
||||||
export const fetchTopicMessagesAction = createAsyncAction(
|
export const fetchTopicMessagesAction = createAsyncAction(
|
||||||
'GET_TOPIC_MESSAGES__REQUEST',
|
'GET_TOPIC_MESSAGES__REQUEST',
|
||||||
|
|
|
@ -1,318 +0,0 @@
|
||||||
import {
|
|
||||||
ClustersApi,
|
|
||||||
BrokersApi,
|
|
||||||
TopicsApi,
|
|
||||||
ConsumerGroupsApi,
|
|
||||||
SchemasApi,
|
|
||||||
MessagesApi,
|
|
||||||
Configuration,
|
|
||||||
Cluster,
|
|
||||||
Topic,
|
|
||||||
TopicFormData,
|
|
||||||
TopicConfig,
|
|
||||||
NewSchemaSubject,
|
|
||||||
SchemaSubject,
|
|
||||||
} from 'generated-sources';
|
|
||||||
import {
|
|
||||||
ConsumerGroupID,
|
|
||||||
PromiseThunkResult,
|
|
||||||
ClusterName,
|
|
||||||
BrokerId,
|
|
||||||
TopicName,
|
|
||||||
TopicMessageQueryParams,
|
|
||||||
TopicFormFormattedParams,
|
|
||||||
TopicFormDataRaw,
|
|
||||||
SchemaName,
|
|
||||||
} from 'redux/interfaces';
|
|
||||||
|
|
||||||
import { BASE_PARAMS } from 'lib/constants';
|
|
||||||
import * as actions from './actions';
|
|
||||||
|
|
||||||
const apiClientConf = new Configuration(BASE_PARAMS);
|
|
||||||
export const clustersApiClient = new ClustersApi(apiClientConf);
|
|
||||||
export const brokersApiClient = new BrokersApi(apiClientConf);
|
|
||||||
export const topicsApiClient = new TopicsApi(apiClientConf);
|
|
||||||
export const consumerGroupsApiClient = new ConsumerGroupsApi(apiClientConf);
|
|
||||||
export const schemasApiClient = new SchemasApi(apiClientConf);
|
|
||||||
export const messagesApiClient = new MessagesApi(apiClientConf);
|
|
||||||
|
|
||||||
export const fetchClustersList = (): PromiseThunkResult => async (dispatch) => {
|
|
||||||
dispatch(actions.fetchClusterListAction.request());
|
|
||||||
try {
|
|
||||||
const clusters: Cluster[] = await clustersApiClient.getClusters();
|
|
||||||
dispatch(actions.fetchClusterListAction.success(clusters));
|
|
||||||
} catch (e) {
|
|
||||||
dispatch(actions.fetchClusterListAction.failure());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchClusterStats = (
|
|
||||||
clusterName: ClusterName
|
|
||||||
): PromiseThunkResult => async (dispatch) => {
|
|
||||||
dispatch(actions.fetchClusterStatsAction.request());
|
|
||||||
try {
|
|
||||||
const payload = await clustersApiClient.getClusterStats({ clusterName });
|
|
||||||
dispatch(actions.fetchClusterStatsAction.success(payload));
|
|
||||||
} catch (e) {
|
|
||||||
dispatch(actions.fetchClusterStatsAction.failure());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchClusterMetrics = (
|
|
||||||
clusterName: ClusterName
|
|
||||||
): PromiseThunkResult => async (dispatch) => {
|
|
||||||
dispatch(actions.fetchClusterMetricsAction.request());
|
|
||||||
try {
|
|
||||||
const payload = await clustersApiClient.getClusterMetrics({ clusterName });
|
|
||||||
dispatch(actions.fetchClusterMetricsAction.success(payload));
|
|
||||||
} catch (e) {
|
|
||||||
dispatch(actions.fetchClusterMetricsAction.failure());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchBrokers = (
|
|
||||||
clusterName: ClusterName
|
|
||||||
): PromiseThunkResult => async (dispatch) => {
|
|
||||||
dispatch(actions.fetchBrokersAction.request());
|
|
||||||
try {
|
|
||||||
const payload = await brokersApiClient.getBrokers({ clusterName });
|
|
||||||
dispatch(actions.fetchBrokersAction.success(payload));
|
|
||||||
} catch (e) {
|
|
||||||
dispatch(actions.fetchBrokersAction.failure());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchBrokerMetrics = (
|
|
||||||
clusterName: ClusterName,
|
|
||||||
brokerId: BrokerId
|
|
||||||
): PromiseThunkResult => async (dispatch) => {
|
|
||||||
dispatch(actions.fetchBrokerMetricsAction.request());
|
|
||||||
try {
|
|
||||||
const payload = await brokersApiClient.getBrokersMetrics({
|
|
||||||
clusterName,
|
|
||||||
id: brokerId,
|
|
||||||
});
|
|
||||||
dispatch(actions.fetchBrokerMetricsAction.success(payload));
|
|
||||||
} catch (e) {
|
|
||||||
dispatch(actions.fetchBrokerMetricsAction.failure());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchTopicsList = (
|
|
||||||
clusterName: ClusterName
|
|
||||||
): PromiseThunkResult => async (dispatch) => {
|
|
||||||
dispatch(actions.fetchTopicsListAction.request());
|
|
||||||
try {
|
|
||||||
const topics = await topicsApiClient.getTopics({ clusterName });
|
|
||||||
dispatch(actions.fetchTopicsListAction.success(topics.topics || []));
|
|
||||||
} catch (e) {
|
|
||||||
dispatch(actions.fetchTopicsListAction.failure());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchTopicMessages = (
|
|
||||||
clusterName: ClusterName,
|
|
||||||
topicName: TopicName,
|
|
||||||
queryParams: Partial<TopicMessageQueryParams>
|
|
||||||
): PromiseThunkResult => async (dispatch) => {
|
|
||||||
dispatch(actions.fetchTopicMessagesAction.request());
|
|
||||||
try {
|
|
||||||
const messages = await messagesApiClient.getTopicMessages({
|
|
||||||
clusterName,
|
|
||||||
topicName,
|
|
||||||
...queryParams,
|
|
||||||
});
|
|
||||||
dispatch(actions.fetchTopicMessagesAction.success(messages));
|
|
||||||
} catch (e) {
|
|
||||||
dispatch(actions.fetchTopicMessagesAction.failure());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchTopicDetails = (
|
|
||||||
clusterName: ClusterName,
|
|
||||||
topicName: TopicName
|
|
||||||
): PromiseThunkResult => async (dispatch) => {
|
|
||||||
dispatch(actions.fetchTopicDetailsAction.request());
|
|
||||||
try {
|
|
||||||
const topicDetails = await topicsApiClient.getTopicDetails({
|
|
||||||
clusterName,
|
|
||||||
topicName,
|
|
||||||
});
|
|
||||||
dispatch(
|
|
||||||
actions.fetchTopicDetailsAction.success({
|
|
||||||
topicName,
|
|
||||||
details: topicDetails,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
dispatch(actions.fetchTopicDetailsAction.failure());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchTopicConfig = (
|
|
||||||
clusterName: ClusterName,
|
|
||||||
topicName: TopicName
|
|
||||||
): PromiseThunkResult => async (dispatch) => {
|
|
||||||
dispatch(actions.fetchTopicConfigAction.request());
|
|
||||||
try {
|
|
||||||
const config = await topicsApiClient.getTopicConfigs({
|
|
||||||
clusterName,
|
|
||||||
topicName,
|
|
||||||
});
|
|
||||||
dispatch(actions.fetchTopicConfigAction.success({ topicName, config }));
|
|
||||||
} catch (e) {
|
|
||||||
dispatch(actions.fetchTopicConfigAction.failure());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatTopicFormData = (form: TopicFormDataRaw): TopicFormData => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
partitions,
|
|
||||||
replicationFactor,
|
|
||||||
cleanupPolicy,
|
|
||||||
retentionBytes,
|
|
||||||
retentionMs,
|
|
||||||
maxMessageBytes,
|
|
||||||
minInSyncReplicas,
|
|
||||||
customParams,
|
|
||||||
} = form;
|
|
||||||
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
partitions,
|
|
||||||
replicationFactor,
|
|
||||||
configs: {
|
|
||||||
'cleanup.policy': cleanupPolicy,
|
|
||||||
'retention.ms': retentionMs,
|
|
||||||
'retention.bytes': retentionBytes,
|
|
||||||
'max.message.bytes': maxMessageBytes,
|
|
||||||
'min.insync.replicas': minInSyncReplicas,
|
|
||||||
...Object.values(customParams || {}).reduce(
|
|
||||||
(result: TopicFormFormattedParams, customParam: TopicConfig) => {
|
|
||||||
return {
|
|
||||||
...result,
|
|
||||||
[customParam.name]: customParam.value,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createTopic = (
|
|
||||||
clusterName: ClusterName,
|
|
||||||
form: TopicFormDataRaw
|
|
||||||
): PromiseThunkResult => async (dispatch) => {
|
|
||||||
dispatch(actions.createTopicAction.request());
|
|
||||||
try {
|
|
||||||
const topic: Topic = await topicsApiClient.createTopic({
|
|
||||||
clusterName,
|
|
||||||
topicFormData: formatTopicFormData(form),
|
|
||||||
});
|
|
||||||
dispatch(actions.createTopicAction.success(topic));
|
|
||||||
} catch (e) {
|
|
||||||
dispatch(actions.createTopicAction.failure());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateTopic = (
|
|
||||||
clusterName: ClusterName,
|
|
||||||
form: TopicFormDataRaw
|
|
||||||
): PromiseThunkResult => async (dispatch) => {
|
|
||||||
dispatch(actions.updateTopicAction.request());
|
|
||||||
try {
|
|
||||||
const topic: Topic = await topicsApiClient.updateTopic({
|
|
||||||
clusterName,
|
|
||||||
topicName: form.name,
|
|
||||||
topicFormData: formatTopicFormData(form),
|
|
||||||
});
|
|
||||||
dispatch(actions.updateTopicAction.success(topic));
|
|
||||||
} catch (e) {
|
|
||||||
dispatch(actions.updateTopicAction.failure());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchConsumerGroupsList = (
|
|
||||||
clusterName: ClusterName
|
|
||||||
): PromiseThunkResult => async (dispatch) => {
|
|
||||||
dispatch(actions.fetchConsumerGroupsAction.request());
|
|
||||||
try {
|
|
||||||
const consumerGroups = await consumerGroupsApiClient.getConsumerGroups({
|
|
||||||
clusterName,
|
|
||||||
});
|
|
||||||
dispatch(actions.fetchConsumerGroupsAction.success(consumerGroups));
|
|
||||||
} catch (e) {
|
|
||||||
dispatch(actions.fetchConsumerGroupsAction.failure());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchConsumerGroupDetails = (
|
|
||||||
clusterName: ClusterName,
|
|
||||||
consumerGroupID: ConsumerGroupID
|
|
||||||
): PromiseThunkResult => async (dispatch) => {
|
|
||||||
dispatch(actions.fetchConsumerGroupDetailsAction.request());
|
|
||||||
try {
|
|
||||||
const consumerGroupDetails = await consumerGroupsApiClient.getConsumerGroup(
|
|
||||||
{
|
|
||||||
clusterName,
|
|
||||||
id: consumerGroupID,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
dispatch(
|
|
||||||
actions.fetchConsumerGroupDetailsAction.success({
|
|
||||||
consumerGroupID,
|
|
||||||
details: consumerGroupDetails,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
dispatch(actions.fetchConsumerGroupDetailsAction.failure());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchSchemasByClusterName = (
|
|
||||||
clusterName: ClusterName
|
|
||||||
): PromiseThunkResult<void> => async (dispatch) => {
|
|
||||||
dispatch(actions.fetchSchemasByClusterNameAction.request());
|
|
||||||
try {
|
|
||||||
const schemas = await schemasApiClient.getSchemas({ clusterName });
|
|
||||||
dispatch(actions.fetchSchemasByClusterNameAction.success(schemas));
|
|
||||||
} catch (e) {
|
|
||||||
dispatch(actions.fetchSchemasByClusterNameAction.failure());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchSchemaVersions = (
|
|
||||||
clusterName: ClusterName,
|
|
||||||
subject: SchemaName
|
|
||||||
): PromiseThunkResult<void> => async (dispatch) => {
|
|
||||||
if (!subject) return;
|
|
||||||
dispatch(actions.fetchSchemaVersionsAction.request());
|
|
||||||
try {
|
|
||||||
const versions = await schemasApiClient.getAllVersionsBySubject({
|
|
||||||
clusterName,
|
|
||||||
subject,
|
|
||||||
});
|
|
||||||
dispatch(actions.fetchSchemaVersionsAction.success(versions));
|
|
||||||
} catch (e) {
|
|
||||||
dispatch(actions.fetchSchemaVersionsAction.failure());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createSchema = (
|
|
||||||
clusterName: ClusterName,
|
|
||||||
newSchemaSubject: NewSchemaSubject
|
|
||||||
): PromiseThunkResult => async (dispatch) => {
|
|
||||||
dispatch(actions.createSchemaAction.request());
|
|
||||||
try {
|
|
||||||
const schema: SchemaSubject = await schemasApiClient.createNewSchema({
|
|
||||||
clusterName,
|
|
||||||
newSchemaSubject,
|
|
||||||
});
|
|
||||||
dispatch(actions.createSchemaAction.success(schema));
|
|
||||||
} catch (e) {
|
|
||||||
dispatch(actions.createSchemaAction.failure());
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
};
|
|
36
kafka-ui-react-app/src/redux/actions/thunks/brokers.ts
Normal file
36
kafka-ui-react-app/src/redux/actions/thunks/brokers.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import { BrokersApi, Configuration } from 'generated-sources';
|
||||||
|
import { PromiseThunkResult, ClusterName, BrokerId } from 'redux/interfaces';
|
||||||
|
|
||||||
|
import { BASE_PARAMS } from 'lib/constants';
|
||||||
|
import * as actions from '../actions';
|
||||||
|
|
||||||
|
const apiClientConf = new Configuration(BASE_PARAMS);
|
||||||
|
export const brokersApiClient = new BrokersApi(apiClientConf);
|
||||||
|
|
||||||
|
export const fetchBrokers = (
|
||||||
|
clusterName: ClusterName
|
||||||
|
): PromiseThunkResult => async (dispatch) => {
|
||||||
|
dispatch(actions.fetchBrokersAction.request());
|
||||||
|
try {
|
||||||
|
const payload = await brokersApiClient.getBrokers({ clusterName });
|
||||||
|
dispatch(actions.fetchBrokersAction.success(payload));
|
||||||
|
} catch (e) {
|
||||||
|
dispatch(actions.fetchBrokersAction.failure());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchBrokerMetrics = (
|
||||||
|
clusterName: ClusterName,
|
||||||
|
brokerId: BrokerId
|
||||||
|
): PromiseThunkResult => async (dispatch) => {
|
||||||
|
dispatch(actions.fetchBrokerMetricsAction.request());
|
||||||
|
try {
|
||||||
|
const payload = await brokersApiClient.getBrokersMetrics({
|
||||||
|
clusterName,
|
||||||
|
id: brokerId,
|
||||||
|
});
|
||||||
|
dispatch(actions.fetchBrokerMetricsAction.success(payload));
|
||||||
|
} catch (e) {
|
||||||
|
dispatch(actions.fetchBrokerMetricsAction.failure());
|
||||||
|
}
|
||||||
|
};
|
42
kafka-ui-react-app/src/redux/actions/thunks/clusters.ts
Normal file
42
kafka-ui-react-app/src/redux/actions/thunks/clusters.ts
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import { ClustersApi, Configuration, Cluster } from 'generated-sources';
|
||||||
|
import { PromiseThunkResult, ClusterName } from 'redux/interfaces';
|
||||||
|
|
||||||
|
import { BASE_PARAMS } from 'lib/constants';
|
||||||
|
import * as actions from '../actions';
|
||||||
|
|
||||||
|
const apiClientConf = new Configuration(BASE_PARAMS);
|
||||||
|
export const clustersApiClient = new ClustersApi(apiClientConf);
|
||||||
|
|
||||||
|
export const fetchClustersList = (): PromiseThunkResult => async (dispatch) => {
|
||||||
|
dispatch(actions.fetchClusterListAction.request());
|
||||||
|
try {
|
||||||
|
const clusters: Cluster[] = await clustersApiClient.getClusters();
|
||||||
|
dispatch(actions.fetchClusterListAction.success(clusters));
|
||||||
|
} catch (e) {
|
||||||
|
dispatch(actions.fetchClusterListAction.failure());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchClusterStats = (
|
||||||
|
clusterName: ClusterName
|
||||||
|
): PromiseThunkResult => async (dispatch) => {
|
||||||
|
dispatch(actions.fetchClusterStatsAction.request());
|
||||||
|
try {
|
||||||
|
const payload = await clustersApiClient.getClusterStats({ clusterName });
|
||||||
|
dispatch(actions.fetchClusterStatsAction.success(payload));
|
||||||
|
} catch (e) {
|
||||||
|
dispatch(actions.fetchClusterStatsAction.failure());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchClusterMetrics = (
|
||||||
|
clusterName: ClusterName
|
||||||
|
): PromiseThunkResult => async (dispatch) => {
|
||||||
|
dispatch(actions.fetchClusterMetricsAction.request());
|
||||||
|
try {
|
||||||
|
const payload = await clustersApiClient.getClusterMetrics({ clusterName });
|
||||||
|
dispatch(actions.fetchClusterMetricsAction.success(payload));
|
||||||
|
} catch (e) {
|
||||||
|
dispatch(actions.fetchClusterMetricsAction.failure());
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { ConsumerGroupsApi, Configuration } from 'generated-sources';
|
||||||
|
import {
|
||||||
|
ConsumerGroupID,
|
||||||
|
PromiseThunkResult,
|
||||||
|
ClusterName,
|
||||||
|
} from 'redux/interfaces';
|
||||||
|
|
||||||
|
import { BASE_PARAMS } from 'lib/constants';
|
||||||
|
import * as actions from '../actions';
|
||||||
|
|
||||||
|
const apiClientConf = new Configuration(BASE_PARAMS);
|
||||||
|
export const consumerGroupsApiClient = new ConsumerGroupsApi(apiClientConf);
|
||||||
|
|
||||||
|
export const fetchConsumerGroupsList = (
|
||||||
|
clusterName: ClusterName
|
||||||
|
): PromiseThunkResult => async (dispatch) => {
|
||||||
|
dispatch(actions.fetchConsumerGroupsAction.request());
|
||||||
|
try {
|
||||||
|
const consumerGroups = await consumerGroupsApiClient.getConsumerGroups({
|
||||||
|
clusterName,
|
||||||
|
});
|
||||||
|
dispatch(actions.fetchConsumerGroupsAction.success(consumerGroups));
|
||||||
|
} catch (e) {
|
||||||
|
dispatch(actions.fetchConsumerGroupsAction.failure());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchConsumerGroupDetails = (
|
||||||
|
clusterName: ClusterName,
|
||||||
|
consumerGroupID: ConsumerGroupID
|
||||||
|
): PromiseThunkResult => async (dispatch) => {
|
||||||
|
dispatch(actions.fetchConsumerGroupDetailsAction.request());
|
||||||
|
try {
|
||||||
|
const consumerGroupDetails = await consumerGroupsApiClient.getConsumerGroup(
|
||||||
|
{
|
||||||
|
clusterName,
|
||||||
|
id: consumerGroupID,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
dispatch(
|
||||||
|
actions.fetchConsumerGroupDetailsAction.success({
|
||||||
|
consumerGroupID,
|
||||||
|
details: consumerGroupDetails,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
dispatch(actions.fetchConsumerGroupDetailsAction.failure());
|
||||||
|
}
|
||||||
|
};
|
5
kafka-ui-react-app/src/redux/actions/thunks/index.ts
Normal file
5
kafka-ui-react-app/src/redux/actions/thunks/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export * from './brokers';
|
||||||
|
export * from './clusters';
|
||||||
|
export * from './consumerGroups';
|
||||||
|
export * from './schemas';
|
||||||
|
export * from './topics';
|
59
kafka-ui-react-app/src/redux/actions/thunks/schemas.ts
Normal file
59
kafka-ui-react-app/src/redux/actions/thunks/schemas.ts
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import {
|
||||||
|
SchemasApi,
|
||||||
|
Configuration,
|
||||||
|
NewSchemaSubject,
|
||||||
|
SchemaSubject,
|
||||||
|
} from 'generated-sources';
|
||||||
|
import { PromiseThunkResult, ClusterName, SchemaName } from 'redux/interfaces';
|
||||||
|
|
||||||
|
import { BASE_PARAMS } from 'lib/constants';
|
||||||
|
import * as actions from '../actions';
|
||||||
|
|
||||||
|
const apiClientConf = new Configuration(BASE_PARAMS);
|
||||||
|
export const schemasApiClient = new SchemasApi(apiClientConf);
|
||||||
|
|
||||||
|
export const fetchSchemasByClusterName = (
|
||||||
|
clusterName: ClusterName
|
||||||
|
): PromiseThunkResult<void> => async (dispatch) => {
|
||||||
|
dispatch(actions.fetchSchemasByClusterNameAction.request());
|
||||||
|
try {
|
||||||
|
const schemas = await schemasApiClient.getSchemas({ clusterName });
|
||||||
|
dispatch(actions.fetchSchemasByClusterNameAction.success(schemas));
|
||||||
|
} catch (e) {
|
||||||
|
dispatch(actions.fetchSchemasByClusterNameAction.failure());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchSchemaVersions = (
|
||||||
|
clusterName: ClusterName,
|
||||||
|
subject: SchemaName
|
||||||
|
): PromiseThunkResult<void> => async (dispatch) => {
|
||||||
|
if (!subject) return;
|
||||||
|
dispatch(actions.fetchSchemaVersionsAction.request());
|
||||||
|
try {
|
||||||
|
const versions = await schemasApiClient.getAllVersionsBySubject({
|
||||||
|
clusterName,
|
||||||
|
subject,
|
||||||
|
});
|
||||||
|
dispatch(actions.fetchSchemaVersionsAction.success(versions));
|
||||||
|
} catch (e) {
|
||||||
|
dispatch(actions.fetchSchemaVersionsAction.failure());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createSchema = (
|
||||||
|
clusterName: ClusterName,
|
||||||
|
newSchemaSubject: NewSchemaSubject
|
||||||
|
): PromiseThunkResult => async (dispatch) => {
|
||||||
|
dispatch(actions.createSchemaAction.request());
|
||||||
|
try {
|
||||||
|
const schema: SchemaSubject = await schemasApiClient.createNewSchema({
|
||||||
|
clusterName,
|
||||||
|
newSchemaSubject,
|
||||||
|
});
|
||||||
|
dispatch(actions.createSchemaAction.success(schema));
|
||||||
|
} catch (e) {
|
||||||
|
dispatch(actions.createSchemaAction.failure());
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
186
kafka-ui-react-app/src/redux/actions/thunks/topics.ts
Normal file
186
kafka-ui-react-app/src/redux/actions/thunks/topics.ts
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
import {
|
||||||
|
TopicsApi,
|
||||||
|
MessagesApi,
|
||||||
|
Configuration,
|
||||||
|
Topic,
|
||||||
|
TopicFormData,
|
||||||
|
TopicConfig,
|
||||||
|
} from 'generated-sources';
|
||||||
|
import {
|
||||||
|
PromiseThunkResult,
|
||||||
|
ClusterName,
|
||||||
|
TopicName,
|
||||||
|
TopicMessageQueryParams,
|
||||||
|
TopicFormFormattedParams,
|
||||||
|
TopicFormDataRaw,
|
||||||
|
TopicsState,
|
||||||
|
} from 'redux/interfaces';
|
||||||
|
|
||||||
|
import { BASE_PARAMS } from 'lib/constants';
|
||||||
|
import * as actions from '../actions';
|
||||||
|
|
||||||
|
const apiClientConf = new Configuration(BASE_PARAMS);
|
||||||
|
export const topicsApiClient = new TopicsApi(apiClientConf);
|
||||||
|
export const messagesApiClient = new MessagesApi(apiClientConf);
|
||||||
|
|
||||||
|
export interface FetchTopicsListParams {
|
||||||
|
clusterName: ClusterName;
|
||||||
|
page?: number;
|
||||||
|
perPage?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchTopicsList = (
|
||||||
|
params: FetchTopicsListParams
|
||||||
|
): PromiseThunkResult => async (dispatch, getState) => {
|
||||||
|
dispatch(actions.fetchTopicsListAction.request());
|
||||||
|
try {
|
||||||
|
const { topics, pageCount } = await topicsApiClient.getTopics(params);
|
||||||
|
const newState = (topics || []).reduce(
|
||||||
|
(memo: TopicsState, topic) => ({
|
||||||
|
...memo,
|
||||||
|
byName: {
|
||||||
|
...memo.byName,
|
||||||
|
[topic.name]: {
|
||||||
|
...memo.byName[topic.name],
|
||||||
|
...topic,
|
||||||
|
id: v4(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
allNames: [...memo.allNames, topic.name],
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
...getState().topics,
|
||||||
|
allNames: [],
|
||||||
|
totalPages: pageCount || 1,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
dispatch(actions.fetchTopicsListAction.success(newState));
|
||||||
|
} catch (e) {
|
||||||
|
dispatch(actions.fetchTopicsListAction.failure());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchTopicMessages = (
|
||||||
|
clusterName: ClusterName,
|
||||||
|
topicName: TopicName,
|
||||||
|
queryParams: Partial<TopicMessageQueryParams>
|
||||||
|
): PromiseThunkResult => async (dispatch) => {
|
||||||
|
dispatch(actions.fetchTopicMessagesAction.request());
|
||||||
|
try {
|
||||||
|
const messages = await messagesApiClient.getTopicMessages({
|
||||||
|
clusterName,
|
||||||
|
topicName,
|
||||||
|
...queryParams,
|
||||||
|
});
|
||||||
|
dispatch(actions.fetchTopicMessagesAction.success(messages));
|
||||||
|
} catch (e) {
|
||||||
|
dispatch(actions.fetchTopicMessagesAction.failure());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchTopicDetails = (
|
||||||
|
clusterName: ClusterName,
|
||||||
|
topicName: TopicName
|
||||||
|
): PromiseThunkResult => async (dispatch) => {
|
||||||
|
dispatch(actions.fetchTopicDetailsAction.request());
|
||||||
|
try {
|
||||||
|
const topicDetails = await topicsApiClient.getTopicDetails({
|
||||||
|
clusterName,
|
||||||
|
topicName,
|
||||||
|
});
|
||||||
|
dispatch(
|
||||||
|
actions.fetchTopicDetailsAction.success({
|
||||||
|
topicName,
|
||||||
|
details: topicDetails,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
dispatch(actions.fetchTopicDetailsAction.failure());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchTopicConfig = (
|
||||||
|
clusterName: ClusterName,
|
||||||
|
topicName: TopicName
|
||||||
|
): PromiseThunkResult => async (dispatch) => {
|
||||||
|
dispatch(actions.fetchTopicConfigAction.request());
|
||||||
|
try {
|
||||||
|
const config = await topicsApiClient.getTopicConfigs({
|
||||||
|
clusterName,
|
||||||
|
topicName,
|
||||||
|
});
|
||||||
|
dispatch(actions.fetchTopicConfigAction.success({ topicName, config }));
|
||||||
|
} catch (e) {
|
||||||
|
dispatch(actions.fetchTopicConfigAction.failure());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTopicFormData = (form: TopicFormDataRaw): TopicFormData => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
partitions,
|
||||||
|
replicationFactor,
|
||||||
|
cleanupPolicy,
|
||||||
|
retentionBytes,
|
||||||
|
retentionMs,
|
||||||
|
maxMessageBytes,
|
||||||
|
minInSyncReplicas,
|
||||||
|
customParams,
|
||||||
|
} = form;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
partitions,
|
||||||
|
replicationFactor,
|
||||||
|
configs: {
|
||||||
|
'cleanup.policy': cleanupPolicy,
|
||||||
|
'retention.ms': retentionMs,
|
||||||
|
'retention.bytes': retentionBytes,
|
||||||
|
'max.message.bytes': maxMessageBytes,
|
||||||
|
'min.insync.replicas': minInSyncReplicas,
|
||||||
|
...Object.values(customParams || {}).reduce(
|
||||||
|
(result: TopicFormFormattedParams, customParam: TopicConfig) => {
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
[customParam.name]: customParam.value,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createTopic = (
|
||||||
|
clusterName: ClusterName,
|
||||||
|
form: TopicFormDataRaw
|
||||||
|
): PromiseThunkResult => async (dispatch) => {
|
||||||
|
dispatch(actions.createTopicAction.request());
|
||||||
|
try {
|
||||||
|
const topic: Topic = await topicsApiClient.createTopic({
|
||||||
|
clusterName,
|
||||||
|
topicFormData: formatTopicFormData(form),
|
||||||
|
});
|
||||||
|
dispatch(actions.createTopicAction.success(topic));
|
||||||
|
} catch (e) {
|
||||||
|
dispatch(actions.createTopicAction.failure());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateTopic = (
|
||||||
|
clusterName: ClusterName,
|
||||||
|
form: TopicFormDataRaw
|
||||||
|
): PromiseThunkResult => async (dispatch) => {
|
||||||
|
dispatch(actions.updateTopicAction.request());
|
||||||
|
try {
|
||||||
|
const topic: Topic = await topicsApiClient.updateTopic({
|
||||||
|
clusterName,
|
||||||
|
topicName: form.name,
|
||||||
|
topicFormData: formatTopicFormData(form),
|
||||||
|
});
|
||||||
|
dispatch(actions.updateTopicAction.success(topic));
|
||||||
|
} catch (e) {
|
||||||
|
dispatch(actions.updateTopicAction.failure());
|
||||||
|
}
|
||||||
|
};
|
|
@ -2,7 +2,7 @@ import {
|
||||||
Topic,
|
Topic,
|
||||||
TopicDetails,
|
TopicDetails,
|
||||||
TopicMessage,
|
TopicMessage,
|
||||||
TopicConfig as InputTopicConfig,
|
TopicConfig,
|
||||||
TopicFormData,
|
TopicFormData,
|
||||||
GetTopicMessagesRequest,
|
GetTopicMessagesRequest,
|
||||||
} from 'generated-sources';
|
} from 'generated-sources';
|
||||||
|
@ -14,10 +14,6 @@ export enum CleanupPolicy {
|
||||||
Compact = 'compact',
|
Compact = 'compact',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TopicConfig extends InputTopicConfig {
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TopicConfigByName {
|
export interface TopicConfigByName {
|
||||||
byName: TopicConfigParams;
|
byName: TopicConfigParams;
|
||||||
}
|
}
|
||||||
|
@ -50,12 +46,12 @@ export interface TopicFormCustomParams {
|
||||||
|
|
||||||
export interface TopicWithDetailedInfo extends Topic, TopicDetails {
|
export interface TopicWithDetailedInfo extends Topic, TopicDetails {
|
||||||
config?: TopicConfig[];
|
config?: TopicConfig[];
|
||||||
id: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TopicsState {
|
export interface TopicsState {
|
||||||
byName: { [topicName: string]: TopicWithDetailedInfo };
|
byName: { [topicName: string]: TopicWithDetailedInfo };
|
||||||
allNames: TopicName[];
|
allNames: TopicName[];
|
||||||
|
totalPages: number;
|
||||||
messages: TopicMessage[];
|
messages: TopicMessage[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { v4 } from 'uuid';
|
|
||||||
import { Topic, TopicMessage } from 'generated-sources';
|
import { Topic, TopicMessage } from 'generated-sources';
|
||||||
import { Action, TopicsState } from 'redux/interfaces';
|
import { Action, TopicsState } from 'redux/interfaces';
|
||||||
import { getType } from 'typesafe-actions';
|
import { getType } from 'typesafe-actions';
|
||||||
|
@ -7,38 +6,16 @@ import * as actions from 'redux/actions';
|
||||||
export const initialState: TopicsState = {
|
export const initialState: TopicsState = {
|
||||||
byName: {},
|
byName: {},
|
||||||
allNames: [],
|
allNames: [],
|
||||||
|
totalPages: 1,
|
||||||
messages: [],
|
messages: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateTopicList = (state: TopicsState, payload: Topic[]): TopicsState => {
|
|
||||||
const initialMemo: TopicsState = {
|
|
||||||
...state,
|
|
||||||
allNames: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
return payload.reduce(
|
|
||||||
(memo: TopicsState, topic) => ({
|
|
||||||
...memo,
|
|
||||||
byName: {
|
|
||||||
...memo.byName,
|
|
||||||
[topic.name]: {
|
|
||||||
...memo.byName[topic.name],
|
|
||||||
...topic,
|
|
||||||
id: v4(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
allNames: [...memo.allNames, topic.name],
|
|
||||||
}),
|
|
||||||
initialMemo
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const addToTopicList = (state: TopicsState, payload: Topic): TopicsState => {
|
const addToTopicList = (state: TopicsState, payload: Topic): TopicsState => {
|
||||||
const newState: TopicsState = {
|
const newState: TopicsState = {
|
||||||
...state,
|
...state,
|
||||||
};
|
};
|
||||||
newState.allNames.push(payload.name);
|
newState.allNames.push(payload.name);
|
||||||
newState.byName[payload.name] = { ...payload, id: v4() };
|
newState.byName[payload.name] = { ...payload };
|
||||||
return newState;
|
return newState;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -70,7 +47,7 @@ const transformTopicMessages = (
|
||||||
const reducer = (state = initialState, action: Action): TopicsState => {
|
const reducer = (state = initialState, action: Action): TopicsState => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case getType(actions.fetchTopicsListAction.success):
|
case getType(actions.fetchTopicsListAction.success):
|
||||||
return updateTopicList(state, action.payload);
|
return action.payload;
|
||||||
case getType(actions.fetchTopicDetailsAction.success):
|
case getType(actions.fetchTopicDetailsAction.success):
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
@ -93,7 +70,6 @@ const reducer = (state = initialState, action: Action): TopicsState => {
|
||||||
...state.byName[action.payload.topicName],
|
...state.byName[action.payload.topicName],
|
||||||
config: action.payload.config.map((inputConfig) => ({
|
config: action.payload.config.map((inputConfig) => ({
|
||||||
...inputConfig,
|
...inputConfig,
|
||||||
id: v4(),
|
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -26,11 +26,21 @@ const getTopicConfigFetchingStatus = createFetchingSelector('GET_TOPIC_CONFIG');
|
||||||
const getTopicCreationStatus = createFetchingSelector('POST_TOPIC');
|
const getTopicCreationStatus = createFetchingSelector('POST_TOPIC');
|
||||||
const getTopicUpdateStatus = createFetchingSelector('PATCH_TOPIC');
|
const getTopicUpdateStatus = createFetchingSelector('PATCH_TOPIC');
|
||||||
|
|
||||||
export const getIsTopicListFetched = createSelector(
|
export const getAreTopicsFetching = createSelector(
|
||||||
|
getTopicListFetchingStatus,
|
||||||
|
(status) => status === 'fetching' || status === 'notFetched'
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getAreTopicsFetched = createSelector(
|
||||||
getTopicListFetchingStatus,
|
getTopicListFetchingStatus,
|
||||||
(status) => status === 'fetched'
|
(status) => status === 'fetched'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const getIsTopicDetailsFetching = createSelector(
|
||||||
|
getTopicDetailsFetchingStatus,
|
||||||
|
(status) => status === 'notFetched' || status === 'fetching'
|
||||||
|
);
|
||||||
|
|
||||||
export const getIsTopicDetailsFetched = createSelector(
|
export const getIsTopicDetailsFetched = createSelector(
|
||||||
getTopicDetailsFetchingStatus,
|
getTopicDetailsFetchingStatus,
|
||||||
(status) => status === 'fetched'
|
(status) => status === 'fetched'
|
||||||
|
@ -57,7 +67,7 @@ export const getTopicUpdated = createSelector(
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getTopicList = createSelector(
|
export const getTopicList = createSelector(
|
||||||
getIsTopicListFetched,
|
getAreTopicsFetched,
|
||||||
getAllNames,
|
getAllNames,
|
||||||
getTopicMap,
|
getTopicMap,
|
||||||
(isFetched, allNames, byName) => {
|
(isFetched, allNames, byName) => {
|
||||||
|
|
Loading…
Add table
Reference in a new issue