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),
schemas: getSchemaList(state),
globalSchemaCompatibilityLevel: getGlobalSchemaCompatibilityLevel(state),
isGlobalSchemaCompatibilityLevelFetched:
getGlobalSchemaCompatibilityLevelFetched(state),
isGlobalSchemaCompatibilityLevelFetched: getGlobalSchemaCompatibilityLevelFetched(
state
),
});
const mapDispatchToProps = {

View file

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

View file

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

View file

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

View file

@ -24,6 +24,10 @@ describe('List', () => {
fetchTopicsList={jest.fn()}
deleteTopic={jest.fn()}
clearTopicMessages={jest.fn()}
search=""
orderBy={null}
setTopicsSearch={jest.fn()}
setTopicsOrderBy={jest.fn()}
/>
</ClusterContext.Provider>
</StaticRouter>
@ -33,7 +37,8 @@ describe('List', () => {
});
describe('when it does not have readonly flag', () => {
it('renders the Add a Topic button', () => {
const mockFetch = jest.fn();
jest.useFakeTimers();
const component = mount(
<StaticRouter>
<ClusterContext.Provider
@ -48,14 +53,32 @@ describe('List', () => {
topics={[]}
externalTopics={[]}
totalPages={1}
fetchTopicsList={jest.fn()}
fetchTopicsList={mockFetch}
deleteTopic={jest.fn()}
clearTopicMessages={jest.fn()}
search=""
orderBy={null}
setTopicsSearch={jest.fn()}
setTopicsOrderBy={jest.fn()}
/>
</ClusterContext.Provider>
</StaticRouter>
);
it('renders the Add a Topic button', () => {
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 { isReadOnly } = React.useContext(ClusterContext);
const [isDeleteTopicConfirmationVisible, setDeleteTopicConfirmationVisible] =
React.useState(false);
const [
isDeleteTopicConfirmationVisible,
setDeleteTopicConfirmationVisible,
] = React.useState(false);
const deleteTopicHandler = React.useCallback(() => {
deleteTopic(clusterName, topicName);
history.push(clusterTopicsPath(clusterName));

View file

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

View file

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

View file

@ -35,8 +35,10 @@ const CustomParams: React.FC<Props> = ({ isSubmitting, config }) => {
)
: {};
const [formCustomParams, setFormCustomParams] =
React.useState<TopicFormCustomParams>({
const [
formCustomParams,
setFormCustomParams,
] = React.useState<TopicFormCustomParams>({
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,
} from 'redux/reducers/schemas/__test__/fixtures';
import * as actions from 'redux/actions';
import { TopicColumnsToSort } from 'generated-sources';
describe('Actions', () => {
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,
SchemaSubject,
CompatibilityLevelCompatibilityEnum,
TopicColumnsToSort,
Connector,
FullConnectorInfo,
Connect,
@ -233,3 +234,11 @@ export const updateConnectorConfigAction = createAsyncAction(
'PATCH_CONNECTOR_CONFIG__SUCCESS',
'PATCH_CONNECTOR_CONFIG__FAILURE'
)<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);
export const brokersApiClient = new BrokersApi(apiClientConf);
export const fetchBrokers =
(clusterName: ClusterName): PromiseThunkResult =>
async (dispatch) => {
export const fetchBrokers = (
clusterName: ClusterName
): PromiseThunkResult => async (dispatch) => {
dispatch(actions.fetchBrokersAction.request());
try {
const payload = await brokersApiClient.getBrokers({ clusterName });
@ -18,9 +18,10 @@ export const fetchBrokers =
}
};
export const fetchBrokerMetrics =
(clusterName: ClusterName, brokerId: BrokerId): PromiseThunkResult =>
async (dispatch) => {
export const fetchBrokerMetrics = (
clusterName: ClusterName,
brokerId: BrokerId
): PromiseThunkResult => async (dispatch) => {
dispatch(actions.fetchBrokerMetricsAction.request());
try {
const payload = await brokersApiClient.getBrokersMetrics({

View file

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

View file

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

View file

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

View file

@ -20,9 +20,9 @@ import { isEqual } from 'lodash';
const apiClientConf = new Configuration(BASE_PARAMS);
export const schemasApiClient = new SchemasApi(apiClientConf);
export const fetchSchemasByClusterName =
(clusterName: ClusterName): PromiseThunkResult<void> =>
async (dispatch) => {
export const fetchSchemasByClusterName = (
clusterName: ClusterName
): PromiseThunkResult<void> => async (dispatch) => {
dispatch(actions.fetchSchemasByClusterNameAction.request());
try {
const schemas = await schemasApiClient.getSchemas({ clusterName });
@ -32,9 +32,10 @@ export const fetchSchemasByClusterName =
}
};
export const fetchSchemaVersions =
(clusterName: ClusterName, subject: SchemaName): PromiseThunkResult<void> =>
async (dispatch) => {
export const fetchSchemaVersions = (
clusterName: ClusterName,
subject: SchemaName
): PromiseThunkResult<void> => async (dispatch) => {
if (!subject) return;
dispatch(actions.fetchSchemaVersionsAction.request());
try {
@ -48,9 +49,9 @@ export const fetchSchemaVersions =
}
};
export const fetchGlobalSchemaCompatibilityLevel =
(clusterName: ClusterName): PromiseThunkResult<void> =>
async (dispatch) => {
export const fetchGlobalSchemaCompatibilityLevel = (
clusterName: ClusterName
): PromiseThunkResult<void> => async (dispatch) => {
dispatch(actions.fetchGlobalSchemaCompatibilityLevelAction.request());
try {
const result = await schemasApiClient.getGlobalSchemaCompatibilityLevel({
@ -66,12 +67,10 @@ export const fetchGlobalSchemaCompatibilityLevel =
}
};
export const updateGlobalSchemaCompatibilityLevel =
(
export const updateGlobalSchemaCompatibilityLevel = (
clusterName: ClusterName,
compatibilityLevel: CompatibilityLevelCompatibilityEnum
): PromiseThunkResult<void> =>
async (dispatch) => {
): PromiseThunkResult<void> => async (dispatch) => {
dispatch(actions.updateGlobalSchemaCompatibilityLevelAction.request());
try {
await schemasApiClient.updateGlobalSchemaCompatibilityLevel({
@ -88,12 +87,10 @@ export const updateGlobalSchemaCompatibilityLevel =
}
};
export const createSchema =
(
export const createSchema = (
clusterName: ClusterName,
newSchemaSubject: NewSchemaSubject
): PromiseThunkResult =>
async (dispatch) => {
): PromiseThunkResult => async (dispatch) => {
dispatch(actions.createSchemaAction.request());
try {
const schema: SchemaSubject = await schemasApiClient.createNewSchema({
@ -113,16 +110,14 @@ export const createSchema =
}
};
export const updateSchema =
(
export const updateSchema = (
latestSchema: SchemaSubject,
newSchema: string,
newSchemaType: SchemaType,
newCompatibilityLevel: CompatibilityLevelCompatibilityEnum,
clusterName: string,
subject: string
): PromiseThunkResult =>
async (dispatch) => {
): PromiseThunkResult => async (dispatch) => {
dispatch(actions.updateSchemaAction.request());
try {
let schema: SchemaSubject = latestSchema;
@ -161,9 +156,10 @@ export const updateSchema =
throw e;
}
};
export const deleteSchema =
(clusterName: ClusterName, subject: string): PromiseThunkResult =>
async (dispatch) => {
export const deleteSchema = (
clusterName: ClusterName,
subject: string
): PromiseThunkResult => async (dispatch) => {
dispatch(actions.deleteSchemaAction.request());
try {
await schemasApiClient.deleteSchema({

View file

@ -7,6 +7,7 @@ import {
TopicCreation,
TopicUpdate,
TopicConfig,
TopicColumnsToSort,
} from 'generated-sources';
import {
PromiseThunkResult,
@ -30,11 +31,14 @@ export interface FetchTopicsListParams {
clusterName: ClusterName;
page?: number;
perPage?: number;
showInternal?: boolean;
search?: string;
orderBy?: TopicColumnsToSort;
}
export const fetchTopicsList =
(params: FetchTopicsListParams): PromiseThunkResult =>
async (dispatch, getState) => {
export const fetchTopicsList = (
params: FetchTopicsListParams
): PromiseThunkResult => async (dispatch, getState) => {
dispatch(actions.fetchTopicsListAction.request());
try {
const { topics, pageCount } = await topicsApiClient.getTopics(params);
@ -63,13 +67,11 @@ export const fetchTopicsList =
}
};
export const fetchTopicMessages =
(
export const fetchTopicMessages = (
clusterName: ClusterName,
topicName: TopicName,
queryParams: Partial<TopicMessageQueryParams>
): PromiseThunkResult =>
async (dispatch) => {
): PromiseThunkResult => async (dispatch) => {
dispatch(actions.fetchTopicMessagesAction.request());
try {
const messages = await messagesApiClient.getTopicMessages({
@ -83,13 +85,11 @@ export const fetchTopicMessages =
}
};
export const clearTopicMessages =
(
export const clearTopicMessages = (
clusterName: ClusterName,
topicName: TopicName,
partitions?: number[]
): PromiseThunkResult =>
async (dispatch) => {
): PromiseThunkResult => async (dispatch) => {
dispatch(actions.clearMessagesTopicAction.request());
try {
await messagesApiClient.deleteTopicMessages({
@ -109,9 +109,10 @@ export const clearTopicMessages =
}
};
export const fetchTopicDetails =
(clusterName: ClusterName, topicName: TopicName): PromiseThunkResult =>
async (dispatch, getState) => {
export const fetchTopicDetails = (
clusterName: ClusterName,
topicName: TopicName
): PromiseThunkResult => async (dispatch, getState) => {
dispatch(actions.fetchTopicDetailsAction.request());
try {
const topicDetails = await topicsApiClient.getTopicDetails({
@ -135,9 +136,10 @@ export const fetchTopicDetails =
}
};
export const fetchTopicConfig =
(clusterName: ClusterName, topicName: TopicName): PromiseThunkResult =>
async (dispatch, getState) => {
export const fetchTopicConfig = (
clusterName: ClusterName,
topicName: TopicName
): PromiseThunkResult => async (dispatch, getState) => {
dispatch(actions.fetchTopicConfigAction.request());
try {
const config = await topicsApiClient.getTopicConfigs({
@ -231,9 +233,10 @@ const formatTopicUpdate = (form: TopicFormDataRaw): TopicUpdate => {
};
};
export const createTopic =
(clusterName: ClusterName, form: TopicFormDataRaw): PromiseThunkResult =>
async (dispatch, getState) => {
export const createTopic = (
clusterName: ClusterName,
form: TopicFormDataRaw
): PromiseThunkResult => async (dispatch, getState) => {
dispatch(actions.createTopicAction.request());
try {
const topic: Topic = await topicsApiClient.createTopic({
@ -265,13 +268,11 @@ export const createTopic =
}
};
export const updateTopic =
(
export const updateTopic = (
clusterName: ClusterName,
topicName: TopicName,
form: TopicFormDataRaw
): PromiseThunkResult =>
async (dispatch, getState) => {
): PromiseThunkResult => async (dispatch, getState) => {
dispatch(actions.updateTopicAction.request());
try {
const topic: Topic = await topicsApiClient.updateTopic({
@ -298,9 +299,10 @@ export const updateTopic =
}
};
export const deleteTopic =
(clusterName: ClusterName, topicName: TopicName): PromiseThunkResult =>
async (dispatch) => {
export const deleteTopic = (
clusterName: ClusterName,
topicName: TopicName
): PromiseThunkResult => async (dispatch) => {
dispatch(actions.deleteTopicAction.request());
try {
await topicsApiClient.deleteTopic({

View file

@ -5,6 +5,7 @@ import {
TopicConfig,
TopicCreation,
GetTopicMessagesRequest,
TopicColumnsToSort,
} from 'generated-sources';
export type TopicName = Topic['name'];
@ -45,6 +46,8 @@ export interface TopicsState {
allNames: TopicName[];
totalPages: number;
messages: TopicMessage[];
search: string;
orderBy: TopicColumnsToSort | null;
}
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';
const topic = {
@ -13,15 +19,17 @@ const state = {
allNames: [topic.name],
messages: [],
totalPages: 1,
search: '',
orderBy: null,
};
describe('topics reducer', () => {
describe('delete topic', () => {
it('deletes the topic from the list on DELETE_TOPIC__SUCCESS', () => {
expect(reducer(state, deleteTopicAction.success(topic.name))).toEqual({
...state,
byName: {},
allNames: [],
messages: [],
totalPages: 1,
});
});
@ -31,3 +39,24 @@ describe('topics reducer', () => {
).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: [],
totalPages: 1,
messages: [],
search: '',
orderBy: null,
};
const transformTopicMessages = (
@ -59,6 +61,18 @@ const reducer = (state = initialState, action: Action): TopicsState => {
messages: [],
};
}
case getType(actions.setTopicsSearchAction): {
return {
...state,
search: action.payload,
};
}
case getType(actions.setTopicsOrderByAction): {
return {
...state,
orderBy: action.payload,
};
}
default:
return state;
}

View file

@ -18,10 +18,12 @@ export const getTopicListTotalPages = (state: RootState) =>
topicsState(state).totalPages;
const getTopicListFetchingStatus = createFetchingSelector('GET_TOPICS');
const getTopicDetailsFetchingStatus =
createFetchingSelector('GET_TOPIC_DETAILS');
const getTopicMessagesFetchingStatus =
createFetchingSelector('GET_TOPIC_MESSAGES');
const getTopicDetailsFetchingStatus = createFetchingSelector(
'GET_TOPIC_DETAILS'
);
const getTopicMessagesFetchingStatus = createFetchingSelector(
'GET_TOPIC_MESSAGES'
);
const getTopicConfigFetchingStatus = createFetchingSelector('GET_TOPIC_CONFIG');
const getTopicCreationStatus = createFetchingSelector('POST_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(
getTopicByName,
({ internal }) => !!internal

View file

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