Topics page refactoring (#251)

* Split thunks on files. Refactoring

* [CHORE] Refactor Topics section
This commit is contained in:
Oleg Shur 2021-03-16 15:13:21 +03:00 committed by GitHub
parent a8ed4ff37f
commit bbdd60b7a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 790 additions and 757 deletions

View file

@ -2,7 +2,7 @@ import React from 'react';
import { useSelector } from 'react-redux';
import { Switch, Route, Redirect, useParams } from 'react-router-dom';
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 Schemas from 'components/Schemas/Schemas';
import { getClustersReadonlyStatus } from 'redux/reducers/clusters/selectors';
@ -18,10 +18,7 @@ const Cluster: React.FC = () => {
path="/ui/clusters/:clusterName/brokers"
component={BrokersContainer}
/>
<Route
path="/ui/clusters/:clusterName/topics"
component={TopicsContainer}
/>
<Route path="/ui/clusters/:clusterName/topics" component={Topics} />
<Route
path="/ui/clusters/:clusterName/consumer-groups"
component={ConsumersGroupsContainer}

View file

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

View file

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

View file

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

View file

@ -1,28 +1,46 @@
import React from 'react';
import { TopicWithDetailedInfo, ClusterName } from 'redux/interfaces';
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 usePagination from 'lib/hooks/usePagination';
import { FetchTopicsListParams } from 'redux/actions';
import ClusterContext from 'components/contexts/ClusterContext';
import PageLoader from 'components/common/PageLoader/PageLoader';
import ListItem from './ListItem';
interface Props {
clusterName: ClusterName;
areTopicsFetching: boolean;
topics: 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 handleSwitch = () => setShowInternal(!showInternal);
const { isReadOnly } = React.useContext(ClusterContext);
const handleSwitch = React.useCallback(() => {
setShowInternal(!showInternal);
}, [showInternal]);
const items = showInternal ? topics : externalTopics;
return (
<div className="section">
<Breadcrumb>All Topics</Breadcrumb>
<Breadcrumb>{showInternal ? `All Topics` : `External Topics`}</Breadcrumb>
<div className="box">
<div className="level">
<div className="level-item level-left">
@ -50,23 +68,33 @@ const List: React.FC<Props> = ({ clusterName, topics, externalTopics }) => {
</div>
</div>
</div>
<div className="box">
<table className="table is-striped is-fullwidth">
<thead>
<tr>
<th>Topic Name</th>
<th>Total Partitions</th>
<th>Out of sync replicas</th>
<th>Type</th>
</tr>
</thead>
<tbody>
{items.map((topic) => (
<ListItem key={topic.id} topic={topic} />
))}
</tbody>
</table>
</div>
{areTopicsFetching ? (
<PageLoader />
) : (
<div className="box">
<table className="table is-striped is-fullwidth">
<thead>
<tr>
<th>Topic Name</th>
<th>Total Partitions</th>
<th>Out of sync replicas</th>
<th>Type</th>
</tr>
</thead>
<tbody>
{items.length > 0 ? (
items.map((topic) => (
<ListItem key={topic.name} topic={topic} />
))
) : (
<tr>
<td colSpan={10}>No topics found</td>
</tr>
)}
</tbody>
</table>
</div>
)}
</div>
);
};

View file

@ -1,29 +1,21 @@
import { connect } from 'react-redux';
import { ClusterName, RootState } from 'redux/interfaces';
import { RootState } from 'redux/interfaces';
import { fetchTopicsList } from 'redux/actions';
import {
getTopicList,
getExternalTopicList,
getAreTopicsFetching,
} from 'redux/reducers/topics/selectors';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import List from './List';
interface RouteProps {
clusterName: ClusterName;
}
type OwnProps = RouteComponentProps<RouteProps>;
const mapStateToProps = (
state: RootState,
{
match: {
params: { clusterName },
},
}: OwnProps
) => ({
clusterName,
const mapStateToProps = (state: RootState) => ({
areTopicsFetching: getAreTopicsFetching(state),
topics: getTopicList(state),
externalTopics: getExternalTopicList(state),
});
export default withRouter(connect(mapStateToProps)(List));
const mapDispatchToProps = {
fetchTopicsList,
};
export default connect(mapStateToProps, mapDispatchToProps)(List);

View file

@ -1,22 +1,43 @@
import { mount } from 'enzyme';
import React from 'react';
import { StaticRouter } from 'react-router-dom';
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>
<StaticRouter>
<ClusterContext.Provider value={{ isReadOnly: true }}>
<List
areTopicsFetching={false}
topics={[]}
externalTopics={[]}
fetchTopicsList={jest.fn()}
/>
</ClusterContext.Provider>
</StaticRouter>
);
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();
});
});
});

View file

@ -1,10 +1,8 @@
import React from 'react';
import { ClusterName, TopicName } from 'redux/interfaces';
import { Topic, TopicDetails } from 'generated-sources';
import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
import { NavLink, Switch, Route } from 'react-router-dom';
import { NavLink, Switch, Route, Link } from 'react-router-dom';
import {
clusterTopicsPath,
clusterTopicSettingsPath,
clusterTopicPath,
clusterTopicMessagesPath,
@ -14,7 +12,6 @@ import ClusterContext from 'components/contexts/ClusterContext';
import OverviewContainer from './Overview/OverviewContainer';
import MessagesContainer from './Messages/MessagesContainer';
import SettingsContainer from './Settings/SettingsContainer';
import SettingsEditButton from './Settings/SettingsEditButton';
interface Props extends Topic, TopicDetails {
clusterName: ClusterName;
@ -23,27 +20,11 @@ interface Props extends Topic, TopicDetails {
const Details: React.FC<Props> = ({ clusterName, topicName }) => {
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">
<nav className="navbar" role="navigation">
return (
<div className="box">
<nav className="navbar" role="navigation">
<div className="navbar-start">
<NavLink
exact
to={clusterTopicPath(clusterName, topicName)}
@ -68,26 +49,36 @@ const Details: React.FC<Props> = ({ clusterName, topicName }) => {
>
Settings
</NavLink>
</nav>
<br />
<Switch>
<Route
exact
path="/ui/clusters/:clusterName/topics/:topicName/messages"
component={MessagesContainer}
/>
<Route
exact
path="/ui/clusters/:clusterName/topics/:topicName/settings"
component={SettingsContainer}
/>
<Route
exact
path="/ui/clusters/:clusterName/topics/:topicName"
component={OverviewContainer}
/>
</Switch>
</div>
</div>
<div className="navbar-end">
{!isReadOnly && (
<Link
to={clusterTopicsTopicEditPath(clusterName, topicName)}
className="button"
>
Edit settings
</Link>
)}
</div>
</nav>
<br />
<Switch>
<Route
exact
path="/ui/clusters/:clusterName/topics/:topicName/messages"
component={MessagesContainer}
/>
<Route
exact
path="/ui/clusters/:clusterName/topics/:topicName/settings"
component={SettingsContainer}
/>
<Route
exact
path="/ui/clusters/:clusterName/topics/:topicName"
component={OverviewContainer}
/>
</Switch>
</div>
);
};

View file

@ -1,6 +1,6 @@
import React from 'react';
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';
jest.mock('date-fns', () => ({

View file

@ -3,8 +3,10 @@ import { Provider } from 'react-redux';
import { mount, shallow } from 'enzyme';
import * as useDebounce from 'use-debounce';
import DatePicker from 'react-datepicker';
import Messages, { Props } from 'components/Topics/Details/Messages/Messages';
import MessagesContainer from 'components/Topics/Details/Messages/MessagesContainer';
import Messages, {
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 configureStore from 'redux/store/configureStore';

View file

@ -2,7 +2,7 @@ import React from 'react';
import { shallow } from 'enzyme';
import MessagesTable, {
MessagesTableProp,
} from 'components/Topics/Details/Messages/MessagesTable';
} from 'components/Topics/Topic/Details/Messages/MessagesTable';
import { messages } from './fixtures';
jest.mock('date-fns', () => ({

View file

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

View file

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

View file

@ -1,5 +1,6 @@
import { TopicConfig } from 'generated-sources';
import React from 'react';
import { ClusterName, TopicName, TopicConfig } from 'redux/interfaces';
import { ClusterName, TopicName } from 'redux/interfaces';
interface Props {
clusterName: ClusterName;
@ -56,7 +57,7 @@ const Sertings: React.FC<Props> = ({
</thead>
<tbody>
{config.map((item) => (
<ConfigListItem key={item.id} config={item} />
<ConfigListItem key={item.name} config={item} />
))}
</tbody>
</table>

View file

@ -10,22 +10,18 @@ import {
import { TopicConfig } from 'generated-sources';
import { useForm, FormProvider } from 'react-hook-form';
import { camelCase } from 'lodash';
import TopicForm from '../shared/Form/TopicForm';
import FormBreadcrumbs from '../shared/Form/FormBreadcrumbs';
import TopicForm from 'components/Topics/shared/Form/TopicForm';
import { clusterTopicPath } from 'lib/paths';
import { useHistory } from 'react-router';
interface Props {
clusterName: ClusterName;
topicName: TopicName;
topic?: TopicWithDetailedInfo;
isFetched: boolean;
isTopicDetailsFetched: boolean;
isTopicUpdated: boolean;
fetchTopicDetails: (clusterName: ClusterName, topicName: TopicName) => void;
fetchTopicConfig: (clusterName: ClusterName, topicName: TopicName) => void;
updateTopic: (clusterName: ClusterName, form: TopicFormDataRaw) => void;
redirectToTopicPath: (clusterName: ClusterName, topicName: TopicName) => void;
resetUploadedState: () => void;
}
const DEFAULTS = {
@ -68,32 +64,29 @@ const Edit: React.FC<Props> = ({
topicName,
topic,
isFetched,
isTopicDetailsFetched,
isTopicUpdated,
fetchTopicDetails,
fetchTopicConfig,
updateTopic,
redirectToTopicPath,
}) => {
const defaultValues = topicParams(topic);
const methods = useForm<TopicFormDataRaw>({ defaultValues });
const [isSubmitting, setIsSubmitting] = React.useState<boolean>(false);
const history = useHistory();
React.useEffect(() => {
fetchTopicConfig(clusterName, topicName);
fetchTopicDetails(clusterName, topicName);
}, [fetchTopicConfig, fetchTopicDetails, clusterName, topicName]);
}, [fetchTopicConfig, clusterName, topicName]);
React.useEffect(() => {
if (isSubmitting && isTopicUpdated) {
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;
}
@ -116,27 +109,17 @@ const Edit: React.FC<Props> = ({
};
return (
<div className="section">
<div className="level">
<FormBreadcrumbs
clusterName={clusterName}
<div className="box">
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<FormProvider {...methods}>
<TopicForm
topicName={topicName}
current="Edit Topic"
config={config}
isSubmitting={isSubmitting}
isEditing
onSubmit={methods.handleSubmit(onSubmit)}
/>
</div>
<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>
</FormProvider>
</div>
);
};

View file

@ -1,12 +1,14 @@
import { connect } from 'react-redux';
import { fetchTopicDetails } from 'redux/actions';
import { RootState, TopicName, ClusterName } from 'redux/interfaces';
import {
getTopicByName,
getIsTopicDetailsFetched,
} from 'redux/reducers/topics/selectors';
import { RootState, ClusterName, TopicName } from 'redux/interfaces';
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 {
clusterName: ClusterName;
@ -25,15 +27,14 @@ const mapStateToProps = (
) => ({
clusterName,
topicName,
isFetched: getIsTopicDetailsFetched(state),
...getTopicByName(state, topicName),
topic: getFullTopic(state, topicName),
isFetched: getTopicConfigFetched(state),
isTopicUpdated: getTopicUpdated(state),
});
const mapDispatchToProps = {
fetchTopicDetails: (clusterName: ClusterName, topicName: TopicName) =>
fetchTopicDetails(clusterName, topicName),
fetchTopicConfig,
updateTopic,
};
export default withRouter(
connect(mapStateToProps, mapDispatchToProps)(Overview)
);
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Edit));

View 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;

View file

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

View file

@ -1,54 +1,26 @@
import React from 'react';
import { ClusterName } from 'redux/interfaces';
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 DetailsContainer from './Details/DetailsContainer';
import TopicContainer from './Topic/TopicContainer';
import NewContainer from './New/NewContainer';
interface Props {
clusterName: ClusterName;
isFetched: boolean;
fetchTopicsList: (clusterName: ClusterName) => void;
}
const Topics: React.FC<Props> = ({
clusterName,
isFetched,
fetchTopicsList,
}) => {
React.useEffect(() => {
fetchTopicsList(clusterName);
}, [fetchTopicsList, clusterName]);
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>
);
}
return <PageLoader />;
};
const Topics: React.FC = () => (
<Switch>
<Route
exact
path="/ui/clusters/:clusterName/topics"
component={ListContainer}
/>
<Route
exact
path="/ui/clusters/:clusterName/topics/new"
component={NewContainer}
/>
<Route
path="/ui/clusters/:clusterName/topics/:topicName"
component={TopicContainer}
/>
</Switch>
);
export default Topics;

View file

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

View file

@ -52,7 +52,7 @@ const CustomParams: React.FC<Props> = ({ isSubmitting, config }) => {
...formCustomParams,
byIndex: {
...formCustomParams.byIndex,
[newIndex]: { name: '', value: '', id: v4() },
[newIndex]: { name: '', value: '' },
},
allIndexes: [newIndex, ...formCustomParams.allIndexes],
});

View 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;

View file

@ -1,5 +1,5 @@
import { createAsyncAction } from 'typesafe-actions';
import { TopicName, ConsumerGroupID } from 'redux/interfaces';
import { TopicName, ConsumerGroupID, TopicsState } from 'redux/interfaces';
import {
Cluster,
@ -50,7 +50,7 @@ export const fetchTopicsListAction = createAsyncAction(
'GET_TOPICS__REQUEST',
'GET_TOPICS__SUCCESS',
'GET_TOPICS__FAILURE'
)<undefined, Topic[], undefined>();
)<undefined, TopicsState, undefined>();
export const fetchTopicMessagesAction = createAsyncAction(
'GET_TOPIC_MESSAGES__REQUEST',

View file

@ -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;
}
};

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

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

View file

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

View file

@ -0,0 +1,5 @@
export * from './brokers';
export * from './clusters';
export * from './consumerGroups';
export * from './schemas';
export * from './topics';

View 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;
}
};

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

View file

@ -2,7 +2,7 @@ import {
Topic,
TopicDetails,
TopicMessage,
TopicConfig as InputTopicConfig,
TopicConfig,
TopicFormData,
GetTopicMessagesRequest,
} from 'generated-sources';
@ -14,10 +14,6 @@ export enum CleanupPolicy {
Compact = 'compact',
}
export interface TopicConfig extends InputTopicConfig {
id: string;
}
export interface TopicConfigByName {
byName: TopicConfigParams;
}
@ -50,12 +46,12 @@ export interface TopicFormCustomParams {
export interface TopicWithDetailedInfo extends Topic, TopicDetails {
config?: TopicConfig[];
id: string;
}
export interface TopicsState {
byName: { [topicName: string]: TopicWithDetailedInfo };
allNames: TopicName[];
totalPages: number;
messages: TopicMessage[];
}

View file

@ -1,4 +1,3 @@
import { v4 } from 'uuid';
import { Topic, TopicMessage } from 'generated-sources';
import { Action, TopicsState } from 'redux/interfaces';
import { getType } from 'typesafe-actions';
@ -7,38 +6,16 @@ import * as actions from 'redux/actions';
export const initialState: TopicsState = {
byName: {},
allNames: [],
totalPages: 1,
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 newState: TopicsState = {
...state,
};
newState.allNames.push(payload.name);
newState.byName[payload.name] = { ...payload, id: v4() };
newState.byName[payload.name] = { ...payload };
return newState;
};
@ -70,7 +47,7 @@ const transformTopicMessages = (
const reducer = (state = initialState, action: Action): TopicsState => {
switch (action.type) {
case getType(actions.fetchTopicsListAction.success):
return updateTopicList(state, action.payload);
return action.payload;
case getType(actions.fetchTopicDetailsAction.success):
return {
...state,
@ -93,7 +70,6 @@ const reducer = (state = initialState, action: Action): TopicsState => {
...state.byName[action.payload.topicName],
config: action.payload.config.map((inputConfig) => ({
...inputConfig,
id: v4(),
})),
},
},

View file

@ -26,11 +26,21 @@ const getTopicConfigFetchingStatus = createFetchingSelector('GET_TOPIC_CONFIG');
const getTopicCreationStatus = createFetchingSelector('POST_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,
(status) => status === 'fetched'
);
export const getIsTopicDetailsFetching = createSelector(
getTopicDetailsFetchingStatus,
(status) => status === 'notFetched' || status === 'fetching'
);
export const getIsTopicDetailsFetched = createSelector(
getTopicDetailsFetchingStatus,
(status) => status === 'fetched'
@ -57,7 +67,7 @@ export const getTopicUpdated = createSelector(
);
export const getTopicList = createSelector(
getIsTopicListFetched,
getAreTopicsFetched,
getAllNames,
getTopicMap,
(isFetched, allNames, byName) => {