Topics' filtration (#405)

* Implement topics' filtration
This commit is contained in:
Alexander Krivonosov 2021-05-12 16:34:24 +03:00 committed by GitHub
parent 00da655e54
commit 98fcc90c6b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 1163 additions and 504 deletions

View file

@ -18,8 +18,9 @@ const mapStateToProps = (state: RootState) => ({
isFetching: getIsSchemaListFetching(state), isFetching: getIsSchemaListFetching(state),
schemas: getSchemaList(state), schemas: getSchemaList(state),
globalSchemaCompatibilityLevel: getGlobalSchemaCompatibilityLevel(state), globalSchemaCompatibilityLevel: getGlobalSchemaCompatibilityLevel(state),
isGlobalSchemaCompatibilityLevelFetched: isGlobalSchemaCompatibilityLevelFetched: getGlobalSchemaCompatibilityLevelFetched(
getGlobalSchemaCompatibilityLevelFetched(state), state
),
}); });
const mapDispatchToProps = { const mapDispatchToProps = {

View file

@ -12,6 +12,9 @@ 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 PageLoader from 'components/common/PageLoader/PageLoader';
import Pagination from 'components/common/Pagination/Pagination'; import Pagination from 'components/common/Pagination/Pagination';
import { TopicColumnsToSort } from 'generated-sources';
import SortableColumnHeader from 'components/common/table/SortableCulumnHeader/SortableColumnHeader';
import Search from 'components/common/Search/Search';
import ListItem from './ListItem'; import ListItem from './ListItem';
@ -27,6 +30,10 @@ interface Props {
clusterName: ClusterName, clusterName: ClusterName,
partitions?: number[] partitions?: number[]
): void; ): void;
search: string;
orderBy: TopicColumnsToSort | null;
setTopicsSearch(search: string): void;
setTopicsOrderBy(orderBy: TopicColumnsToSort | null): void;
} }
const List: React.FC<Props> = ({ const List: React.FC<Props> = ({
@ -37,14 +44,24 @@ const List: React.FC<Props> = ({
fetchTopicsList, fetchTopicsList,
deleteTopic, deleteTopic,
clearTopicMessages, clearTopicMessages,
search,
orderBy,
setTopicsSearch,
setTopicsOrderBy,
}) => { }) => {
const { isReadOnly } = React.useContext(ClusterContext); const { isReadOnly } = React.useContext(ClusterContext);
const { clusterName } = useParams<{ clusterName: ClusterName }>(); const { clusterName } = useParams<{ clusterName: ClusterName }>();
const { page, perPage } = usePagination(); const { page, perPage } = usePagination();
React.useEffect(() => { React.useEffect(() => {
fetchTopicsList({ clusterName, page, perPage }); fetchTopicsList({
}, [fetchTopicsList, clusterName, page, perPage]); clusterName,
page,
perPage,
orderBy: orderBy || undefined,
search,
});
}, [fetchTopicsList, clusterName, page, perPage, orderBy, search]);
const [showInternal, setShowInternal] = React.useState<boolean>(true); const [showInternal, setShowInternal] = React.useState<boolean>(true);
@ -52,14 +69,16 @@ const List: React.FC<Props> = ({
setShowInternal(!showInternal); setShowInternal(!showInternal);
}, [showInternal]); }, [showInternal]);
const handleSearch = (value: string) => setTopicsSearch(value);
const items = showInternal ? topics : externalTopics; const items = showInternal ? topics : externalTopics;
return ( return (
<div className="section"> <div className="section">
<Breadcrumb>{showInternal ? `All Topics` : `External Topics`}</Breadcrumb> <Breadcrumb>{showInternal ? `All Topics` : `External Topics`}</Breadcrumb>
<div className="box"> <div className="box">
<div className="level"> <div className="columns">
<div className="level-item level-left"> <div className="column is-one-quarter is-align-items-center is-flex">
<div className="field"> <div className="field">
<input <input
id="switchRoundedDefault" id="switchRoundedDefault"
@ -72,7 +91,14 @@ const List: React.FC<Props> = ({
<label htmlFor="switchRoundedDefault">Show Internal Topics</label> <label htmlFor="switchRoundedDefault">Show Internal Topics</label>
</div> </div>
</div> </div>
<div className="level-item level-right"> <div className="column">
<Search
handleSearch={handleSearch}
placeholder="Search by Topic Name"
value={search}
/>
</div>
<div className="column is-2 is-justify-content-flex-end is-flex">
{!isReadOnly && ( {!isReadOnly && (
<Link <Link
className="button is-primary" className="button is-primary"
@ -91,9 +117,24 @@ const List: React.FC<Props> = ({
<table className="table is-fullwidth"> <table className="table is-fullwidth">
<thead> <thead>
<tr> <tr>
<th>Topic Name</th> <SortableColumnHeader
<th>Total Partitions</th> value={TopicColumnsToSort.NAME}
<th>Out of sync replicas</th> title="Topic Name"
orderBy={orderBy}
setOrderBy={setTopicsOrderBy}
/>
<SortableColumnHeader
value={TopicColumnsToSort.TOTAL_PARTITIONS}
title="Total Partitions"
orderBy={orderBy}
setOrderBy={setTopicsOrderBy}
/>
<SortableColumnHeader
value={TopicColumnsToSort.OUT_OF_SYNC_REPLICAS}
title="Out of sync replicas"
orderBy={orderBy}
setOrderBy={setTopicsOrderBy}
/>
<th>Type</th> <th>Type</th>
<th> </th> <th> </th>
</tr> </tr>

View file

@ -4,12 +4,16 @@ import {
fetchTopicsList, fetchTopicsList,
deleteTopic, deleteTopic,
clearTopicMessages, clearTopicMessages,
setTopicsSearchAction,
setTopicsOrderByAction,
} from 'redux/actions'; } from 'redux/actions';
import { import {
getTopicList, getTopicList,
getExternalTopicList, getExternalTopicList,
getAreTopicsFetching, getAreTopicsFetching,
getTopicListTotalPages, getTopicListTotalPages,
getTopicsSearch,
getTopicsOrderBy,
} from 'redux/reducers/topics/selectors'; } from 'redux/reducers/topics/selectors';
import List from './List'; import List from './List';
@ -19,12 +23,16 @@ const mapStateToProps = (state: RootState) => ({
topics: getTopicList(state), topics: getTopicList(state),
externalTopics: getExternalTopicList(state), externalTopics: getExternalTopicList(state),
totalPages: getTopicListTotalPages(state), totalPages: getTopicListTotalPages(state),
search: getTopicsSearch(state),
orderBy: getTopicsOrderBy(state),
}); });
const mapDispatchToProps = { const mapDispatchToProps = {
fetchTopicsList, fetchTopicsList,
deleteTopic, deleteTopic,
clearTopicMessages, clearTopicMessages,
setTopicsSearch: setTopicsSearchAction,
setTopicsOrderBy: setTopicsOrderByAction,
}; };
export default connect(mapStateToProps, mapDispatchToProps)(List); export default connect(mapStateToProps, mapDispatchToProps)(List);

View file

@ -23,8 +23,10 @@ const ListItem: React.FC<ListItemProps> = ({
clusterName, clusterName,
clearTopicMessages, clearTopicMessages,
}) => { }) => {
const [isDeleteTopicConfirmationVisible, setDeleteTopicConfirmationVisible] = const [
React.useState(false); isDeleteTopicConfirmationVisible,
setDeleteTopicConfirmationVisible,
] = React.useState(false);
const outOfSyncReplicas = React.useMemo(() => { const outOfSyncReplicas = React.useMemo(() => {
if (partitions === undefined || partitions.length === 0) { if (partitions === undefined || partitions.length === 0) {

View file

@ -24,6 +24,10 @@ describe('List', () => {
fetchTopicsList={jest.fn()} fetchTopicsList={jest.fn()}
deleteTopic={jest.fn()} deleteTopic={jest.fn()}
clearTopicMessages={jest.fn()} clearTopicMessages={jest.fn()}
search=""
orderBy={null}
setTopicsSearch={jest.fn()}
setTopicsOrderBy={jest.fn()}
/> />
</ClusterContext.Provider> </ClusterContext.Provider>
</StaticRouter> </StaticRouter>
@ -33,7 +37,8 @@ describe('List', () => {
}); });
describe('when it does not have readonly flag', () => { describe('when it does not have readonly flag', () => {
it('renders the Add a Topic button', () => { const mockFetch = jest.fn();
jest.useFakeTimers();
const component = mount( const component = mount(
<StaticRouter> <StaticRouter>
<ClusterContext.Provider <ClusterContext.Provider
@ -48,14 +53,32 @@ describe('List', () => {
topics={[]} topics={[]}
externalTopics={[]} externalTopics={[]}
totalPages={1} totalPages={1}
fetchTopicsList={jest.fn()} fetchTopicsList={mockFetch}
deleteTopic={jest.fn()} deleteTopic={jest.fn()}
clearTopicMessages={jest.fn()} clearTopicMessages={jest.fn()}
search=""
orderBy={null}
setTopicsSearch={jest.fn()}
setTopicsOrderBy={jest.fn()}
/> />
</ClusterContext.Provider> </ClusterContext.Provider>
</StaticRouter> </StaticRouter>
); );
it('renders the Add a Topic button', () => {
expect(component.exists('Link')).toBeTruthy(); expect(component.exists('Link')).toBeTruthy();
}); });
it('matches the snapshot', () => {
expect(component).toMatchSnapshot();
});
it('calls fetchTopicsList on input', () => {
const input = component.find('input').at(1);
input.simulate('change', { target: { value: 't' } });
expect(setTimeout).toHaveBeenCalledTimes(1);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 300);
setTimeout(() => {
expect(mockFetch).toHaveBeenCalledTimes(1);
}, 301);
});
}); });
}); });

View file

@ -0,0 +1,255 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`List when it does not have readonly flag matches the snapshot 1`] = `
<StaticRouter>
<Router
history={
Object {
"action": "POP",
"block": [Function],
"createHref": [Function],
"go": [Function],
"goBack": [Function],
"goForward": [Function],
"listen": [Function],
"location": Object {
"hash": "",
"pathname": "/",
"search": "",
"state": undefined,
},
"push": [Function],
"replace": [Function],
}
}
staticContext={Object {}}
>
<List
areTopicsFetching={false}
clearTopicMessages={[MockFunction]}
deleteTopic={[MockFunction]}
externalTopics={Array []}
fetchTopicsList={[MockFunction]}
orderBy={null}
search=""
setTopicsOrderBy={[MockFunction]}
setTopicsSearch={[MockFunction]}
topics={Array []}
totalPages={1}
>
<div
className="section"
>
<Breadcrumb>
<nav
aria-label="breadcrumbs"
className="breadcrumb"
>
<ul>
<li
className="is-active"
>
<span
className=""
>
All Topics
</span>
</li>
</ul>
</nav>
</Breadcrumb>
<div
className="box"
>
<div
className="columns"
>
<div
className="column is-one-quarter is-align-items-center is-flex"
>
<div
className="field"
>
<input
checked={true}
className="switch is-rounded"
id="switchRoundedDefault"
name="switchRoundedDefault"
onChange={[Function]}
type="checkbox"
/>
<label
htmlFor="switchRoundedDefault"
>
Show Internal Topics
</label>
</div>
</div>
<div
className="column"
>
<Search
handleSearch={[Function]}
placeholder="Search by Topic Name"
value=""
>
<p
className="control has-icons-left"
>
<input
className="input"
defaultValue=""
onChange={[Function]}
placeholder="Search by Topic Name"
type="text"
/>
<span
className="icon is-small is-left"
>
<i
className="fas fa-search"
/>
</span>
</p>
</Search>
</div>
<div
className="column is-2 is-justify-content-flex-end is-flex"
>
<Link
className="button is-primary"
to="/ui/clusters/undefined/topics/create_new"
>
<LinkAnchor
className="button is-primary"
href="/ui/clusters/undefined/topics/create_new"
navigate={[Function]}
>
<a
className="button is-primary"
href="/ui/clusters/undefined/topics/create_new"
onClick={[Function]}
>
Add a Topic
</a>
</LinkAnchor>
</Link>
</div>
</div>
</div>
<div
className="box"
>
<table
className="table is-fullwidth"
>
<thead>
<tr>
<ListHeaderCell
orderBy={null}
setOrderBy={[MockFunction]}
title="Topic Name"
value="NAME"
>
<th
className="is-clickable"
onClick={[Function]}
>
Topic Name
<span
className="icon is-small"
>
<i
className="fas fa-sort"
/>
</span>
</th>
</ListHeaderCell>
<ListHeaderCell
orderBy={null}
setOrderBy={[MockFunction]}
title="Total Partitions"
value="TOTAL_PARTITIONS"
>
<th
className="is-clickable"
onClick={[Function]}
>
Total Partitions
<span
className="icon is-small"
>
<i
className="fas fa-sort"
/>
</span>
</th>
</ListHeaderCell>
<ListHeaderCell
orderBy={null}
setOrderBy={[MockFunction]}
title="Out of sync replicas"
value="OUT_OF_SYNC_REPLICAS"
>
<th
className="is-clickable"
onClick={[Function]}
>
Out of sync replicas
<span
className="icon is-small"
>
<i
className="fas fa-sort"
/>
</span>
</th>
</ListHeaderCell>
<th>
Type
</th>
<th>
</th>
</tr>
</thead>
<tbody>
<tr>
<td
colSpan={10}
>
No topics found
</td>
</tr>
</tbody>
</table>
<Pagination
totalPages={1}
>
<nav
aria-label="pagination"
className="pagination is-small is-right"
role="navigation"
>
<button
className="pagination-previous"
disabled={true}
type="button"
>
Previous
</button>
<button
className="pagination-next"
disabled={true}
type="button"
>
Next page
</button>
</nav>
</Pagination>
</div>
</div>
</List>
</Router>
</StaticRouter>
`;

View file

@ -33,8 +33,10 @@ const Details: React.FC<Props> = ({
}) => { }) => {
const history = useHistory(); const history = useHistory();
const { isReadOnly } = React.useContext(ClusterContext); const { isReadOnly } = React.useContext(ClusterContext);
const [isDeleteTopicConfirmationVisible, setDeleteTopicConfirmationVisible] = const [
React.useState(false); isDeleteTopicConfirmationVisible,
setDeleteTopicConfirmationVisible,
] = React.useState(false);
const deleteTopicHandler = React.useCallback(() => { const deleteTopicHandler = React.useCallback(() => {
deleteTopic(clusterName, topicName); deleteTopic(clusterName, topicName);
history.push(clusterTopicsPath(clusterName)); history.push(clusterTopicsPath(clusterName));

View file

@ -50,8 +50,9 @@ const Messages: React.FC<Props> = ({
fetchTopicMessages, fetchTopicMessages,
}) => { }) => {
const [searchQuery, setSearchQuery] = React.useState<string>(''); const [searchQuery, setSearchQuery] = React.useState<string>('');
const [searchTimestamp, setSearchTimestamp] = const [searchTimestamp, setSearchTimestamp] = React.useState<Date | null>(
React.useState<Date | null>(null); null
);
const [filterProps, setFilterProps] = React.useState<FilterProps[]>([]); const [filterProps, setFilterProps] = React.useState<FilterProps[]>([]);
const [selectedSeekType, setSelectedSeekType] = React.useState<SeekType>( const [selectedSeekType, setSelectedSeekType] = React.useState<SeekType>(
SeekType.OFFSET SeekType.OFFSET

View file

@ -39,8 +39,9 @@ const CustomParamSelect: React.FC<CustomParamSelectProps> = ({
return valid || 'Custom Parameter must be unique'; return valid || 'Custom Parameter must be unique';
}; };
const onChange = const onChange = (inputName: string) => (
(inputName: string) => (event: React.ChangeEvent<HTMLSelectElement>) => { event: React.ChangeEvent<HTMLSelectElement>
) => {
trigger(inputName); trigger(inputName);
onNameChange(index, event.target.value); onNameChange(index, event.target.value);
}; };

View file

@ -35,8 +35,10 @@ const CustomParams: React.FC<Props> = ({ isSubmitting, config }) => {
) )
: {}; : {};
const [formCustomParams, setFormCustomParams] = const [
React.useState<TopicFormCustomParams>({ formCustomParams,
setFormCustomParams,
] = React.useState<TopicFormCustomParams>({
byIndex, byIndex,
allIndexes: Object.keys(byIndex), allIndexes: Object.keys(byIndex),
}); });

View file

@ -0,0 +1,35 @@
import React from 'react';
import { useDebouncedCallback } from 'use-debounce';
interface SearchProps {
handleSearch: (value: string) => void;
placeholder?: string;
value: string;
}
const Search: React.FC<SearchProps> = ({
handleSearch,
placeholder = 'Search',
value,
}) => {
const onChange = useDebouncedCallback(
(e) => handleSearch(e.target.value),
300
);
return (
<p className="control has-icons-left">
<input
className="input"
type="text"
placeholder={placeholder}
onChange={onChange}
defaultValue={value}
/>
<span className="icon is-small is-left">
<i className="fas fa-search" />
</span>
</p>
);
};
export default Search;

View file

@ -0,0 +1,35 @@
import { shallow } from 'enzyme';
import Search from 'components/common/Search/Search';
import React from 'react';
jest.mock('use-debounce', () => ({
useDebouncedCallback: (fn: (e: Event) => void) => fn,
}));
describe('Search', () => {
const handleSearch = jest.fn();
let component = shallow(
<Search
handleSearch={handleSearch}
value=""
placeholder="Search bt the Topic name"
/>
);
it('calls handleSearch on input', () => {
component.find('input').simulate('change', { target: { value: 'test' } });
expect(handleSearch).toHaveBeenCalledTimes(1);
});
describe('when placeholder is provided', () => {
it('matches the snapshot', () => {
expect(component).toMatchSnapshot();
});
});
describe('when placeholder is not provided', () => {
component = shallow(<Search handleSearch={handleSearch} value="" />);
it('matches the snapshot', () => {
expect(component).toMatchSnapshot();
});
});
});

View file

@ -0,0 +1,43 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Search when placeholder is not provided matches the snapshot 1`] = `
<p
className="control has-icons-left"
>
<input
className="input"
defaultValue=""
onChange={[Function]}
placeholder="Search"
type="text"
/>
<span
className="icon is-small is-left"
>
<i
className="fas fa-search"
/>
</span>
</p>
`;
exports[`Search when placeholder is provided matches the snapshot 1`] = `
<p
className="control has-icons-left"
>
<input
className="input"
defaultValue=""
onChange={[Function]}
placeholder="Search"
type="text"
/>
<span
className="icon is-small is-left"
>
<i
className="fas fa-search"
/>
</span>
</p>
`;

View file

@ -0,0 +1,29 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import React from 'react';
import cx from 'classnames';
export interface ListHeaderProps {
value: any;
title: string;
orderBy: any;
setOrderBy: React.Dispatch<React.SetStateAction<any>>;
}
const ListHeaderCell: React.FC<ListHeaderProps> = ({
value,
title,
orderBy,
setOrderBy,
}) => (
<th
className={cx('is-clickable', orderBy === value && 'has-text-link-dark')}
onClick={() => setOrderBy(value)}
>
{title}
<span className="icon is-small">
<i className="fas fa-sort" />
</span>
</th>
);
export default ListHeaderCell;

View file

@ -0,0 +1,37 @@
import SortableColumnHeader from 'components/common/table/SortableCulumnHeader/SortableColumnHeader';
import { mount } from 'enzyme';
import { TopicColumnsToSort } from 'generated-sources';
import React from 'react';
describe('ListHeader', () => {
const setOrderBy = jest.fn();
const component = mount(
<table>
<thead>
<tr>
<SortableColumnHeader
value={TopicColumnsToSort.NAME}
title="Name"
orderBy={null}
setOrderBy={setOrderBy}
/>
</tr>
</thead>
</table>
);
it('matches the snapshot', () => {
expect(component).toMatchSnapshot();
});
describe('on column click', () => {
it('calls setOrderBy', () => {
component.find('th').simulate('click');
expect(setOrderBy).toHaveBeenCalledTimes(1);
expect(setOrderBy).toHaveBeenCalledWith(TopicColumnsToSort.NAME);
});
it('matches the snapshot', () => {
expect(component).toMatchSnapshot();
});
});
});

View file

@ -0,0 +1,59 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ListHeader matches the snapshot 1`] = `
<table>
<thead>
<tr>
<ListHeaderCell
orderBy={null}
setOrderBy={[MockFunction]}
title="Name"
value="NAME"
>
<th
className="is-clickable"
onClick={[Function]}
>
Name
<span
className="icon is-small"
>
<i
className="fas fa-sort"
/>
</span>
</th>
</ListHeaderCell>
</tr>
</thead>
</table>
`;
exports[`ListHeader on column click matches the snapshot 1`] = `
<table>
<thead>
<tr>
<ListHeaderCell
orderBy={null}
setOrderBy={[MockFunction]}
title="Name"
value="NAME"
>
<th
className="is-clickable"
onClick={[Function]}
>
Name
<span
className="icon is-small"
>
<i
className="fas fa-sort"
/>
</span>
</th>
</ListHeaderCell>
</tr>
</thead>
</table>
`;

View file

@ -3,6 +3,7 @@ import {
schemaVersionsPayload, schemaVersionsPayload,
} from 'redux/reducers/schemas/__test__/fixtures'; } from 'redux/reducers/schemas/__test__/fixtures';
import * as actions from 'redux/actions'; import * as actions from 'redux/actions';
import { TopicColumnsToSort } from 'generated-sources';
describe('Actions', () => { describe('Actions', () => {
describe('fetchClusterStatsAction', () => { describe('fetchClusterStatsAction', () => {
@ -131,4 +132,22 @@ describe('Actions', () => {
}); });
}); });
}); });
describe('setTopicsSearchAction', () => {
it('creartes SET_TOPICS_SEARCH', () => {
expect(actions.setTopicsSearchAction('test')).toEqual({
type: 'SET_TOPICS_SEARCH',
payload: 'test',
});
});
});
describe('setTopicsOrderByAction', () => {
it('creartes SET_TOPICS_ORDER_BY', () => {
expect(actions.setTopicsOrderByAction(TopicColumnsToSort.NAME)).toEqual({
type: 'SET_TOPICS_ORDER_BY',
payload: TopicColumnsToSort.NAME,
});
});
});
}); });

View file

@ -18,6 +18,7 @@ import {
ConsumerGroupDetails, ConsumerGroupDetails,
SchemaSubject, SchemaSubject,
CompatibilityLevelCompatibilityEnum, CompatibilityLevelCompatibilityEnum,
TopicColumnsToSort,
Connector, Connector,
FullConnectorInfo, FullConnectorInfo,
Connect, Connect,
@ -233,3 +234,11 @@ export const updateConnectorConfigAction = createAsyncAction(
'PATCH_CONNECTOR_CONFIG__SUCCESS', 'PATCH_CONNECTOR_CONFIG__SUCCESS',
'PATCH_CONNECTOR_CONFIG__FAILURE' 'PATCH_CONNECTOR_CONFIG__FAILURE'
)<undefined, { connector: Connector }, { alert?: FailurePayload }>(); )<undefined, { connector: Connector }, { alert?: FailurePayload }>();
export const setTopicsSearchAction = createAction(
'SET_TOPICS_SEARCH'
)<string>();
export const setTopicsOrderByAction = createAction(
'SET_TOPICS_ORDER_BY'
)<TopicColumnsToSort>();

View file

@ -6,9 +6,9 @@ import * as actions from 'redux/actions/actions';
const apiClientConf = new Configuration(BASE_PARAMS); const apiClientConf = new Configuration(BASE_PARAMS);
export const brokersApiClient = new BrokersApi(apiClientConf); export const brokersApiClient = new BrokersApi(apiClientConf);
export const fetchBrokers = export const fetchBrokers = (
(clusterName: ClusterName): PromiseThunkResult => clusterName: ClusterName
async (dispatch) => { ): PromiseThunkResult => async (dispatch) => {
dispatch(actions.fetchBrokersAction.request()); dispatch(actions.fetchBrokersAction.request());
try { try {
const payload = await brokersApiClient.getBrokers({ clusterName }); const payload = await brokersApiClient.getBrokers({ clusterName });
@ -16,11 +16,12 @@ export const fetchBrokers =
} catch (e) { } catch (e) {
dispatch(actions.fetchBrokersAction.failure()); dispatch(actions.fetchBrokersAction.failure());
} }
}; };
export const fetchBrokerMetrics = export const fetchBrokerMetrics = (
(clusterName: ClusterName, brokerId: BrokerId): PromiseThunkResult => clusterName: ClusterName,
async (dispatch) => { brokerId: BrokerId
): PromiseThunkResult => async (dispatch) => {
dispatch(actions.fetchBrokerMetricsAction.request()); dispatch(actions.fetchBrokerMetricsAction.request());
try { try {
const payload = await brokersApiClient.getBrokersMetrics({ const payload = await brokersApiClient.getBrokersMetrics({
@ -31,4 +32,4 @@ export const fetchBrokerMetrics =
} catch (e) { } catch (e) {
dispatch(actions.fetchBrokerMetricsAction.failure()); dispatch(actions.fetchBrokerMetricsAction.failure());
} }
}; };

View file

@ -16,9 +16,9 @@ export const fetchClustersList = (): PromiseThunkResult => async (dispatch) => {
} }
}; };
export const fetchClusterStats = export const fetchClusterStats = (
(clusterName: ClusterName): PromiseThunkResult => clusterName: ClusterName
async (dispatch) => { ): PromiseThunkResult => async (dispatch) => {
dispatch(actions.fetchClusterStatsAction.request()); dispatch(actions.fetchClusterStatsAction.request());
try { try {
const payload = await clustersApiClient.getClusterStats({ clusterName }); const payload = await clustersApiClient.getClusterStats({ clusterName });
@ -26,11 +26,11 @@ export const fetchClusterStats =
} catch (e) { } catch (e) {
dispatch(actions.fetchClusterStatsAction.failure()); dispatch(actions.fetchClusterStatsAction.failure());
} }
}; };
export const fetchClusterMetrics = export const fetchClusterMetrics = (
(clusterName: ClusterName): PromiseThunkResult => clusterName: ClusterName
async (dispatch) => { ): PromiseThunkResult => async (dispatch) => {
dispatch(actions.fetchClusterMetricsAction.request()); dispatch(actions.fetchClusterMetricsAction.request());
try { try {
const payload = await clustersApiClient.getClusterMetrics({ const payload = await clustersApiClient.getClusterMetrics({
@ -40,4 +40,4 @@ export const fetchClusterMetrics =
} catch (e) { } catch (e) {
dispatch(actions.fetchClusterMetricsAction.failure()); dispatch(actions.fetchClusterMetricsAction.failure());
} }
}; };

View file

@ -20,7 +20,6 @@ import { getResponse } from 'lib/errorHandling';
const apiClientConf = new Configuration(BASE_PARAMS); const apiClientConf = new Configuration(BASE_PARAMS);
export const kafkaConnectApiClient = new KafkaConnectApi(apiClientConf); export const kafkaConnectApiClient = new KafkaConnectApi(apiClientConf);
export const fetchConnects = export const fetchConnects =
(clusterName: ClusterName): PromiseThunkResult<void> => (clusterName: ClusterName): PromiseThunkResult<void> =>
async (dispatch) => { async (dispatch) => {

View file

@ -10,9 +10,9 @@ import * as actions from 'redux/actions/actions';
const apiClientConf = new Configuration(BASE_PARAMS); const apiClientConf = new Configuration(BASE_PARAMS);
export const consumerGroupsApiClient = new ConsumerGroupsApi(apiClientConf); export const consumerGroupsApiClient = new ConsumerGroupsApi(apiClientConf);
export const fetchConsumerGroupsList = export const fetchConsumerGroupsList = (
(clusterName: ClusterName): PromiseThunkResult => clusterName: ClusterName
async (dispatch) => { ): PromiseThunkResult => async (dispatch) => {
dispatch(actions.fetchConsumerGroupsAction.request()); dispatch(actions.fetchConsumerGroupsAction.request());
try { try {
const consumerGroups = await consumerGroupsApiClient.getConsumerGroups({ const consumerGroups = await consumerGroupsApiClient.getConsumerGroups({
@ -22,21 +22,20 @@ export const fetchConsumerGroupsList =
} catch (e) { } catch (e) {
dispatch(actions.fetchConsumerGroupsAction.failure()); dispatch(actions.fetchConsumerGroupsAction.failure());
} }
}; };
export const fetchConsumerGroupDetails = export const fetchConsumerGroupDetails = (
(
clusterName: ClusterName, clusterName: ClusterName,
consumerGroupID: ConsumerGroupID consumerGroupID: ConsumerGroupID
): PromiseThunkResult => ): PromiseThunkResult => async (dispatch) => {
async (dispatch) => {
dispatch(actions.fetchConsumerGroupDetailsAction.request()); dispatch(actions.fetchConsumerGroupDetailsAction.request());
try { try {
const consumerGroupDetails = const consumerGroupDetails = await consumerGroupsApiClient.getConsumerGroup(
await consumerGroupsApiClient.getConsumerGroup({ {
clusterName, clusterName,
id: consumerGroupID, id: consumerGroupID,
}); }
);
dispatch( dispatch(
actions.fetchConsumerGroupDetailsAction.success({ actions.fetchConsumerGroupDetailsAction.success({
consumerGroupID, consumerGroupID,
@ -46,4 +45,4 @@ export const fetchConsumerGroupDetails =
} catch (e) { } catch (e) {
dispatch(actions.fetchConsumerGroupDetailsAction.failure()); dispatch(actions.fetchConsumerGroupDetailsAction.failure());
} }
}; };

View file

@ -20,9 +20,9 @@ import { isEqual } from 'lodash';
const apiClientConf = new Configuration(BASE_PARAMS); const apiClientConf = new Configuration(BASE_PARAMS);
export const schemasApiClient = new SchemasApi(apiClientConf); export const schemasApiClient = new SchemasApi(apiClientConf);
export const fetchSchemasByClusterName = export const fetchSchemasByClusterName = (
(clusterName: ClusterName): PromiseThunkResult<void> => clusterName: ClusterName
async (dispatch) => { ): PromiseThunkResult<void> => async (dispatch) => {
dispatch(actions.fetchSchemasByClusterNameAction.request()); dispatch(actions.fetchSchemasByClusterNameAction.request());
try { try {
const schemas = await schemasApiClient.getSchemas({ clusterName }); const schemas = await schemasApiClient.getSchemas({ clusterName });
@ -30,11 +30,12 @@ export const fetchSchemasByClusterName =
} catch (e) { } catch (e) {
dispatch(actions.fetchSchemasByClusterNameAction.failure()); dispatch(actions.fetchSchemasByClusterNameAction.failure());
} }
}; };
export const fetchSchemaVersions = export const fetchSchemaVersions = (
(clusterName: ClusterName, subject: SchemaName): PromiseThunkResult<void> => clusterName: ClusterName,
async (dispatch) => { subject: SchemaName
): PromiseThunkResult<void> => async (dispatch) => {
if (!subject) return; if (!subject) return;
dispatch(actions.fetchSchemaVersionsAction.request()); dispatch(actions.fetchSchemaVersionsAction.request());
try { try {
@ -46,11 +47,11 @@ export const fetchSchemaVersions =
} catch (e) { } catch (e) {
dispatch(actions.fetchSchemaVersionsAction.failure()); dispatch(actions.fetchSchemaVersionsAction.failure());
} }
}; };
export const fetchGlobalSchemaCompatibilityLevel = export const fetchGlobalSchemaCompatibilityLevel = (
(clusterName: ClusterName): PromiseThunkResult<void> => clusterName: ClusterName
async (dispatch) => { ): PromiseThunkResult<void> => async (dispatch) => {
dispatch(actions.fetchGlobalSchemaCompatibilityLevelAction.request()); dispatch(actions.fetchGlobalSchemaCompatibilityLevelAction.request());
try { try {
const result = await schemasApiClient.getGlobalSchemaCompatibilityLevel({ const result = await schemasApiClient.getGlobalSchemaCompatibilityLevel({
@ -64,14 +65,12 @@ export const fetchGlobalSchemaCompatibilityLevel =
} catch (e) { } catch (e) {
dispatch(actions.fetchGlobalSchemaCompatibilityLevelAction.failure()); dispatch(actions.fetchGlobalSchemaCompatibilityLevelAction.failure());
} }
}; };
export const updateGlobalSchemaCompatibilityLevel = export const updateGlobalSchemaCompatibilityLevel = (
(
clusterName: ClusterName, clusterName: ClusterName,
compatibilityLevel: CompatibilityLevelCompatibilityEnum compatibilityLevel: CompatibilityLevelCompatibilityEnum
): PromiseThunkResult<void> => ): PromiseThunkResult<void> => async (dispatch) => {
async (dispatch) => {
dispatch(actions.updateGlobalSchemaCompatibilityLevelAction.request()); dispatch(actions.updateGlobalSchemaCompatibilityLevelAction.request());
try { try {
await schemasApiClient.updateGlobalSchemaCompatibilityLevel({ await schemasApiClient.updateGlobalSchemaCompatibilityLevel({
@ -86,14 +85,12 @@ export const updateGlobalSchemaCompatibilityLevel =
} catch (e) { } catch (e) {
dispatch(actions.updateGlobalSchemaCompatibilityLevelAction.failure()); dispatch(actions.updateGlobalSchemaCompatibilityLevelAction.failure());
} }
}; };
export const createSchema = export const createSchema = (
(
clusterName: ClusterName, clusterName: ClusterName,
newSchemaSubject: NewSchemaSubject newSchemaSubject: NewSchemaSubject
): PromiseThunkResult => ): PromiseThunkResult => async (dispatch) => {
async (dispatch) => {
dispatch(actions.createSchemaAction.request()); dispatch(actions.createSchemaAction.request());
try { try {
const schema: SchemaSubject = await schemasApiClient.createNewSchema({ const schema: SchemaSubject = await schemasApiClient.createNewSchema({
@ -111,18 +108,16 @@ export const createSchema =
dispatch(actions.createSchemaAction.failure({ alert })); dispatch(actions.createSchemaAction.failure({ alert }));
throw error; throw error;
} }
}; };
export const updateSchema = export const updateSchema = (
(
latestSchema: SchemaSubject, latestSchema: SchemaSubject,
newSchema: string, newSchema: string,
newSchemaType: SchemaType, newSchemaType: SchemaType,
newCompatibilityLevel: CompatibilityLevelCompatibilityEnum, newCompatibilityLevel: CompatibilityLevelCompatibilityEnum,
clusterName: string, clusterName: string,
subject: string subject: string
): PromiseThunkResult => ): PromiseThunkResult => async (dispatch) => {
async (dispatch) => {
dispatch(actions.updateSchemaAction.request()); dispatch(actions.updateSchemaAction.request());
try { try {
let schema: SchemaSubject = latestSchema; let schema: SchemaSubject = latestSchema;
@ -160,10 +155,11 @@ export const updateSchema =
dispatch(actions.updateSchemaAction.failure({ alert })); dispatch(actions.updateSchemaAction.failure({ alert }));
throw e; throw e;
} }
}; };
export const deleteSchema = export const deleteSchema = (
(clusterName: ClusterName, subject: string): PromiseThunkResult => clusterName: ClusterName,
async (dispatch) => { subject: string
): PromiseThunkResult => async (dispatch) => {
dispatch(actions.deleteSchemaAction.request()); dispatch(actions.deleteSchemaAction.request());
try { try {
await schemasApiClient.deleteSchema({ await schemasApiClient.deleteSchema({
@ -180,4 +176,4 @@ export const deleteSchema =
}; };
dispatch(actions.deleteSchemaAction.failure({ alert })); dispatch(actions.deleteSchemaAction.failure({ alert }));
} }
}; };

View file

@ -7,6 +7,7 @@ import {
TopicCreation, TopicCreation,
TopicUpdate, TopicUpdate,
TopicConfig, TopicConfig,
TopicColumnsToSort,
} from 'generated-sources'; } from 'generated-sources';
import { import {
PromiseThunkResult, PromiseThunkResult,
@ -30,11 +31,14 @@ export interface FetchTopicsListParams {
clusterName: ClusterName; clusterName: ClusterName;
page?: number; page?: number;
perPage?: number; perPage?: number;
showInternal?: boolean;
search?: string;
orderBy?: TopicColumnsToSort;
} }
export const fetchTopicsList = export const fetchTopicsList = (
(params: FetchTopicsListParams): PromiseThunkResult => params: FetchTopicsListParams
async (dispatch, getState) => { ): PromiseThunkResult => async (dispatch, getState) => {
dispatch(actions.fetchTopicsListAction.request()); dispatch(actions.fetchTopicsListAction.request());
try { try {
const { topics, pageCount } = await topicsApiClient.getTopics(params); const { topics, pageCount } = await topicsApiClient.getTopics(params);
@ -61,15 +65,13 @@ export const fetchTopicsList =
} catch (e) { } catch (e) {
dispatch(actions.fetchTopicsListAction.failure()); dispatch(actions.fetchTopicsListAction.failure());
} }
}; };
export const fetchTopicMessages = export const fetchTopicMessages = (
(
clusterName: ClusterName, clusterName: ClusterName,
topicName: TopicName, topicName: TopicName,
queryParams: Partial<TopicMessageQueryParams> queryParams: Partial<TopicMessageQueryParams>
): PromiseThunkResult => ): PromiseThunkResult => async (dispatch) => {
async (dispatch) => {
dispatch(actions.fetchTopicMessagesAction.request()); dispatch(actions.fetchTopicMessagesAction.request());
try { try {
const messages = await messagesApiClient.getTopicMessages({ const messages = await messagesApiClient.getTopicMessages({
@ -81,15 +83,13 @@ export const fetchTopicMessages =
} catch (e) { } catch (e) {
dispatch(actions.fetchTopicMessagesAction.failure()); dispatch(actions.fetchTopicMessagesAction.failure());
} }
}; };
export const clearTopicMessages = export const clearTopicMessages = (
(
clusterName: ClusterName, clusterName: ClusterName,
topicName: TopicName, topicName: TopicName,
partitions?: number[] partitions?: number[]
): PromiseThunkResult => ): PromiseThunkResult => async (dispatch) => {
async (dispatch) => {
dispatch(actions.clearMessagesTopicAction.request()); dispatch(actions.clearMessagesTopicAction.request());
try { try {
await messagesApiClient.deleteTopicMessages({ await messagesApiClient.deleteTopicMessages({
@ -107,11 +107,12 @@ export const clearTopicMessages =
}; };
dispatch(actions.clearMessagesTopicAction.failure({ alert })); dispatch(actions.clearMessagesTopicAction.failure({ alert }));
} }
}; };
export const fetchTopicDetails = export const fetchTopicDetails = (
(clusterName: ClusterName, topicName: TopicName): PromiseThunkResult => clusterName: ClusterName,
async (dispatch, getState) => { topicName: TopicName
): PromiseThunkResult => async (dispatch, getState) => {
dispatch(actions.fetchTopicDetailsAction.request()); dispatch(actions.fetchTopicDetailsAction.request());
try { try {
const topicDetails = await topicsApiClient.getTopicDetails({ const topicDetails = await topicsApiClient.getTopicDetails({
@ -133,11 +134,12 @@ export const fetchTopicDetails =
} catch (e) { } catch (e) {
dispatch(actions.fetchTopicDetailsAction.failure()); dispatch(actions.fetchTopicDetailsAction.failure());
} }
}; };
export const fetchTopicConfig = export const fetchTopicConfig = (
(clusterName: ClusterName, topicName: TopicName): PromiseThunkResult => clusterName: ClusterName,
async (dispatch, getState) => { topicName: TopicName
): PromiseThunkResult => async (dispatch, getState) => {
dispatch(actions.fetchTopicConfigAction.request()); dispatch(actions.fetchTopicConfigAction.request());
try { try {
const config = await topicsApiClient.getTopicConfigs({ const config = await topicsApiClient.getTopicConfigs({
@ -163,7 +165,7 @@ export const fetchTopicConfig =
} catch (e) { } catch (e) {
dispatch(actions.fetchTopicConfigAction.failure()); dispatch(actions.fetchTopicConfigAction.failure());
} }
}; };
const formatTopicCreation = (form: TopicFormDataRaw): TopicCreation => { const formatTopicCreation = (form: TopicFormDataRaw): TopicCreation => {
const { const {
@ -231,9 +233,10 @@ const formatTopicUpdate = (form: TopicFormDataRaw): TopicUpdate => {
}; };
}; };
export const createTopic = export const createTopic = (
(clusterName: ClusterName, form: TopicFormDataRaw): PromiseThunkResult => clusterName: ClusterName,
async (dispatch, getState) => { form: TopicFormDataRaw
): PromiseThunkResult => async (dispatch, getState) => {
dispatch(actions.createTopicAction.request()); dispatch(actions.createTopicAction.request());
try { try {
const topic: Topic = await topicsApiClient.createTopic({ const topic: Topic = await topicsApiClient.createTopic({
@ -263,15 +266,13 @@ export const createTopic =
}; };
dispatch(actions.createTopicAction.failure({ alert })); dispatch(actions.createTopicAction.failure({ alert }));
} }
}; };
export const updateTopic = export const updateTopic = (
(
clusterName: ClusterName, clusterName: ClusterName,
topicName: TopicName, topicName: TopicName,
form: TopicFormDataRaw form: TopicFormDataRaw
): PromiseThunkResult => ): PromiseThunkResult => async (dispatch, getState) => {
async (dispatch, getState) => {
dispatch(actions.updateTopicAction.request()); dispatch(actions.updateTopicAction.request());
try { try {
const topic: Topic = await topicsApiClient.updateTopic({ const topic: Topic = await topicsApiClient.updateTopic({
@ -296,11 +297,12 @@ export const updateTopic =
} catch (e) { } catch (e) {
dispatch(actions.updateTopicAction.failure()); dispatch(actions.updateTopicAction.failure());
} }
}; };
export const deleteTopic = export const deleteTopic = (
(clusterName: ClusterName, topicName: TopicName): PromiseThunkResult => clusterName: ClusterName,
async (dispatch) => { topicName: TopicName
): PromiseThunkResult => async (dispatch) => {
dispatch(actions.deleteTopicAction.request()); dispatch(actions.deleteTopicAction.request());
try { try {
await topicsApiClient.deleteTopic({ await topicsApiClient.deleteTopic({
@ -311,4 +313,4 @@ export const deleteTopic =
} catch (e) { } catch (e) {
dispatch(actions.deleteTopicAction.failure()); dispatch(actions.deleteTopicAction.failure());
} }
}; };

View file

@ -5,6 +5,7 @@ import {
TopicConfig, TopicConfig,
TopicCreation, TopicCreation,
GetTopicMessagesRequest, GetTopicMessagesRequest,
TopicColumnsToSort,
} from 'generated-sources'; } from 'generated-sources';
export type TopicName = Topic['name']; export type TopicName = Topic['name'];
@ -45,6 +46,8 @@ export interface TopicsState {
allNames: TopicName[]; allNames: TopicName[];
totalPages: number; totalPages: number;
messages: TopicMessage[]; messages: TopicMessage[];
search: string;
orderBy: TopicColumnsToSort | null;
} }
export type TopicFormFormattedParams = TopicCreation['configs']; export type TopicFormFormattedParams = TopicCreation['configs'];

View file

@ -1,4 +1,10 @@
import { deleteTopicAction, clearMessagesTopicAction } from 'redux/actions'; import { TopicColumnsToSort } from 'generated-sources';
import {
deleteTopicAction,
clearMessagesTopicAction,
setTopicsSearchAction,
setTopicsOrderByAction,
} from 'redux/actions';
import reducer from 'redux/reducers/topics/reducer'; import reducer from 'redux/reducers/topics/reducer';
const topic = { const topic = {
@ -13,15 +19,17 @@ const state = {
allNames: [topic.name], allNames: [topic.name],
messages: [], messages: [],
totalPages: 1, totalPages: 1,
search: '',
orderBy: null,
}; };
describe('topics reducer', () => { describe('topics reducer', () => {
describe('delete topic', () => {
it('deletes the topic from the list on DELETE_TOPIC__SUCCESS', () => { it('deletes the topic from the list on DELETE_TOPIC__SUCCESS', () => {
expect(reducer(state, deleteTopicAction.success(topic.name))).toEqual({ expect(reducer(state, deleteTopicAction.success(topic.name))).toEqual({
...state,
byName: {}, byName: {},
allNames: [], allNames: [],
messages: [],
totalPages: 1,
}); });
}); });
@ -30,4 +38,25 @@ describe('topics reducer', () => {
reducer(state, clearMessagesTopicAction.success(topic.name)) reducer(state, clearMessagesTopicAction.success(topic.name))
).toEqual(state); ).toEqual(state);
}); });
});
describe('search topics', () => {
it('sets the search string', () => {
expect(reducer(state, setTopicsSearchAction('test'))).toEqual({
...state,
search: 'test',
});
});
});
describe('order topics', () => {
it('sets the orderBy', () => {
expect(
reducer(state, setTopicsOrderByAction(TopicColumnsToSort.NAME))
).toEqual({
...state,
orderBy: TopicColumnsToSort.NAME,
});
});
});
}); });

View file

@ -8,6 +8,8 @@ export const initialState: TopicsState = {
allNames: [], allNames: [],
totalPages: 1, totalPages: 1,
messages: [], messages: [],
search: '',
orderBy: null,
}; };
const transformTopicMessages = ( const transformTopicMessages = (
@ -59,6 +61,18 @@ const reducer = (state = initialState, action: Action): TopicsState => {
messages: [], messages: [],
}; };
} }
case getType(actions.setTopicsSearchAction): {
return {
...state,
search: action.payload,
};
}
case getType(actions.setTopicsOrderByAction): {
return {
...state,
orderBy: action.payload,
};
}
default: default:
return state; return state;
} }

View file

@ -18,10 +18,12 @@ export const getTopicListTotalPages = (state: RootState) =>
topicsState(state).totalPages; topicsState(state).totalPages;
const getTopicListFetchingStatus = createFetchingSelector('GET_TOPICS'); const getTopicListFetchingStatus = createFetchingSelector('GET_TOPICS');
const getTopicDetailsFetchingStatus = const getTopicDetailsFetchingStatus = createFetchingSelector(
createFetchingSelector('GET_TOPIC_DETAILS'); 'GET_TOPIC_DETAILS'
const getTopicMessagesFetchingStatus = );
createFetchingSelector('GET_TOPIC_MESSAGES'); const getTopicMessagesFetchingStatus = createFetchingSelector(
'GET_TOPIC_MESSAGES'
);
const getTopicConfigFetchingStatus = createFetchingSelector('GET_TOPIC_CONFIG'); 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');
@ -122,6 +124,16 @@ export const getTopicConfigByParamName = createSelector(
} }
); );
export const getTopicsSearch = createSelector(
topicsState,
(state) => state.search
);
export const getTopicsOrderBy = createSelector(
topicsState,
(state) => state.orderBy
);
export const getIsTopicInternal = createSelector( export const getIsTopicInternal = createSelector(
getTopicByName, getTopicByName,
({ internal }) => !!internal ({ internal }) => !!internal

View file

@ -6,7 +6,9 @@ import { RootState, Action } from 'redux/interfaces';
const middlewares: Array<Middleware> = [thunk]; const middlewares: Array<Middleware> = [thunk];
type DispatchExts = ThunkDispatch<RootState, undefined, Action>; type DispatchExts = ThunkDispatch<RootState, undefined, Action>;
const mockStoreCreator: MockStoreCreator<RootState, DispatchExts> = const mockStoreCreator: MockStoreCreator<
configureMockStore<RootState, DispatchExts>(middlewares); RootState,
DispatchExts
> = configureMockStore<RootState, DispatchExts>(middlewares);
export default mockStoreCreator(); export default mockStoreCreator();