parent
590bdfb610
commit
595707edb6
10 changed files with 156 additions and 7 deletions
|
@ -1,5 +1,9 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { TopicWithDetailedInfo, ClusterName } from 'redux/interfaces';
|
import {
|
||||||
|
TopicWithDetailedInfo,
|
||||||
|
ClusterName,
|
||||||
|
TopicName,
|
||||||
|
} from 'redux/interfaces';
|
||||||
import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
|
import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
|
||||||
import { Link, useParams } from 'react-router-dom';
|
import { Link, useParams } from 'react-router-dom';
|
||||||
import { clusterTopicNewPath } from 'lib/paths';
|
import { clusterTopicNewPath } from 'lib/paths';
|
||||||
|
@ -16,6 +20,7 @@ interface Props {
|
||||||
externalTopics: TopicWithDetailedInfo[];
|
externalTopics: TopicWithDetailedInfo[];
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
fetchTopicsList(props: FetchTopicsListParams): void;
|
fetchTopicsList(props: FetchTopicsListParams): void;
|
||||||
|
deleteTopic(topicName: TopicName, clusterName: ClusterName): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const List: React.FC<Props> = ({
|
const List: React.FC<Props> = ({
|
||||||
|
@ -24,6 +29,7 @@ const List: React.FC<Props> = ({
|
||||||
externalTopics,
|
externalTopics,
|
||||||
totalPages,
|
totalPages,
|
||||||
fetchTopicsList,
|
fetchTopicsList,
|
||||||
|
deleteTopic,
|
||||||
}) => {
|
}) => {
|
||||||
const { isReadOnly } = React.useContext(ClusterContext);
|
const { isReadOnly } = React.useContext(ClusterContext);
|
||||||
const { clusterName } = useParams<{ clusterName: ClusterName }>();
|
const { clusterName } = useParams<{ clusterName: ClusterName }>();
|
||||||
|
@ -82,17 +88,23 @@ const List: React.FC<Props> = ({
|
||||||
<th>Total Partitions</th>
|
<th>Total Partitions</th>
|
||||||
<th>Out of sync replicas</th>
|
<th>Out of sync replicas</th>
|
||||||
<th>Type</th>
|
<th>Type</th>
|
||||||
|
<th> </th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
{items.map((topic) => (
|
||||||
|
<ListItem
|
||||||
|
clusterName={clusterName}
|
||||||
|
key={topic.name}
|
||||||
|
topic={topic}
|
||||||
|
deleteTopic={deleteTopic}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
{items.length === 0 && (
|
{items.length === 0 && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={10}>No topics found</td>
|
<td colSpan={10}>No topics found</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
{items.map((topic) => (
|
|
||||||
<ListItem key={topic.name} topic={topic} />
|
|
||||||
))}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<Pagination totalPages={totalPages} />
|
<Pagination totalPages={totalPages} />
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { RootState } from 'redux/interfaces';
|
import { RootState } from 'redux/interfaces';
|
||||||
import { fetchTopicsList } from 'redux/actions';
|
import { fetchTopicsList, deleteTopic } from 'redux/actions';
|
||||||
import {
|
import {
|
||||||
getTopicList,
|
getTopicList,
|
||||||
getExternalTopicList,
|
getExternalTopicList,
|
||||||
|
@ -18,6 +18,7 @@ const mapStateToProps = (state: RootState) => ({
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
fetchTopicsList,
|
fetchTopicsList,
|
||||||
|
deleteTopic,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(List);
|
export default connect(mapStateToProps, mapDispatchToProps)(List);
|
||||||
|
|
|
@ -1,14 +1,22 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
import { TopicWithDetailedInfo } from 'redux/interfaces';
|
import {
|
||||||
|
ClusterName,
|
||||||
|
TopicName,
|
||||||
|
TopicWithDetailedInfo,
|
||||||
|
} from 'redux/interfaces';
|
||||||
|
|
||||||
interface ListItemProps {
|
interface ListItemProps {
|
||||||
topic: TopicWithDetailedInfo;
|
topic: TopicWithDetailedInfo;
|
||||||
|
deleteTopic: (clusterName: ClusterName, topicName: TopicName) => void;
|
||||||
|
clusterName: ClusterName;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ListItem: React.FC<ListItemProps> = ({
|
const ListItem: React.FC<ListItemProps> = ({
|
||||||
topic: { name, internal, partitions },
|
topic: { name, internal, partitions },
|
||||||
|
deleteTopic,
|
||||||
|
clusterName,
|
||||||
}) => {
|
}) => {
|
||||||
const outOfSyncReplicas = React.useMemo(() => {
|
const outOfSyncReplicas = React.useMemo(() => {
|
||||||
if (partitions === undefined || partitions.length === 0) {
|
if (partitions === undefined || partitions.length === 0) {
|
||||||
|
@ -21,6 +29,10 @@ const ListItem: React.FC<ListItemProps> = ({
|
||||||
}, 0);
|
}, 0);
|
||||||
}, [partitions]);
|
}, [partitions]);
|
||||||
|
|
||||||
|
const deleteTopicHandler = React.useCallback(() => {
|
||||||
|
deleteTopic(clusterName, name);
|
||||||
|
}, [clusterName, name]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
|
@ -42,6 +54,17 @@ const ListItem: React.FC<ListItemProps> = ({
|
||||||
{internal ? 'Internal' : 'External'}
|
{internal ? 'Internal' : 'External'}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="is-small button is-danger"
|
||||||
|
onClick={deleteTopicHandler}
|
||||||
|
>
|
||||||
|
<span className="icon is-small">
|
||||||
|
<i className="far fa-trash-alt" />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,6 +16,7 @@ describe('List', () => {
|
||||||
externalTopics={[]}
|
externalTopics={[]}
|
||||||
totalPages={1}
|
totalPages={1}
|
||||||
fetchTopicsList={jest.fn()}
|
fetchTopicsList={jest.fn()}
|
||||||
|
deleteTopic={jest.fn()}
|
||||||
/>
|
/>
|
||||||
</ClusterContext.Provider>
|
</ClusterContext.Provider>
|
||||||
</StaticRouter>
|
</StaticRouter>
|
||||||
|
@ -35,6 +36,7 @@ describe('List', () => {
|
||||||
externalTopics={[]}
|
externalTopics={[]}
|
||||||
totalPages={1}
|
totalPages={1}
|
||||||
fetchTopicsList={jest.fn()}
|
fetchTopicsList={jest.fn()}
|
||||||
|
deleteTopic={jest.fn()}
|
||||||
/>
|
/>
|
||||||
</ClusterContext.Provider>
|
</ClusterContext.Provider>
|
||||||
</StaticRouter>
|
</StaticRouter>
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
import ListItem from '../ListItem';
|
||||||
|
|
||||||
|
describe('ListItem', () => {
|
||||||
|
it('triggers the deleting thunk when clicked on the delete button', () => {
|
||||||
|
const mockDelete = jest.fn();
|
||||||
|
const topic = { name: 'topic', id: 'id' };
|
||||||
|
const clustterName = 'cluster';
|
||||||
|
const component = shallow(
|
||||||
|
<ListItem
|
||||||
|
topic={topic}
|
||||||
|
deleteTopic={mockDelete}
|
||||||
|
clusterName={clustterName}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
component.find('button').simulate('click');
|
||||||
|
expect(mockDelete).toBeCalledTimes(1);
|
||||||
|
expect(mockDelete).toBeCalledWith(clustterName, topic.name);
|
||||||
|
});
|
||||||
|
});
|
|
@ -22,6 +22,7 @@ const mockStoreCreator: MockStoreCreator<
|
||||||
const store: MockStoreEnhanced<RootState, DispatchExts> = mockStoreCreator();
|
const store: MockStoreEnhanced<RootState, DispatchExts> = mockStoreCreator();
|
||||||
|
|
||||||
const clusterName = 'local';
|
const clusterName = 'local';
|
||||||
|
const topicName = 'localTopic';
|
||||||
const subject = 'test';
|
const subject = 'test';
|
||||||
|
|
||||||
describe('Thunks', () => {
|
describe('Thunks', () => {
|
||||||
|
@ -137,4 +138,34 @@ describe('Thunks', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('deleteTopis', () => {
|
||||||
|
it('creates DELETE_TOPIC__SUCCESS when deleting existing topic', async () => {
|
||||||
|
fetchMock.deleteOnce(
|
||||||
|
`/api/clusters/${clusterName}/topics/${topicName}`,
|
||||||
|
200
|
||||||
|
);
|
||||||
|
await store.dispatch(thunks.deleteTopic(clusterName, topicName));
|
||||||
|
expect(store.getActions()).toEqual([
|
||||||
|
actions.deleteTopicAction.request(),
|
||||||
|
actions.deleteTopicAction.success(topicName),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates DELETE_TOPIC__FAILURE when deleting existing topic', async () => {
|
||||||
|
fetchMock.postOnce(
|
||||||
|
`/api/clusters/${clusterName}/topics/${topicName}`,
|
||||||
|
404
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await store.dispatch(thunks.deleteTopic(clusterName, topicName));
|
||||||
|
} catch (error) {
|
||||||
|
expect(error.status).toEqual(404);
|
||||||
|
expect(store.getActions()).toEqual([
|
||||||
|
actions.deleteTopicAction.request(),
|
||||||
|
actions.deleteTopicAction.failure(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { createAsyncAction } from 'typesafe-actions';
|
import { createAsyncAction } from 'typesafe-actions';
|
||||||
import { ConsumerGroupID, TopicsState } from 'redux/interfaces';
|
import { ConsumerGroupID, TopicName, TopicsState } from 'redux/interfaces';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Cluster,
|
Cluster,
|
||||||
|
@ -79,6 +79,12 @@ export const updateTopicAction = createAsyncAction(
|
||||||
'PATCH_TOPIC__FAILURE'
|
'PATCH_TOPIC__FAILURE'
|
||||||
)<undefined, TopicsState, undefined>();
|
)<undefined, TopicsState, undefined>();
|
||||||
|
|
||||||
|
export const deleteTopicAction = createAsyncAction(
|
||||||
|
'DELETE_TOPIC__REQUEST',
|
||||||
|
'DELETE_TOPIC__SUCCESS',
|
||||||
|
'DELETE_TOPIC__FAILURE'
|
||||||
|
)<undefined, TopicName, undefined>();
|
||||||
|
|
||||||
export const fetchConsumerGroupsAction = createAsyncAction(
|
export const fetchConsumerGroupsAction = createAsyncAction(
|
||||||
'GET_CONSUMER_GROUPS__REQUEST',
|
'GET_CONSUMER_GROUPS__REQUEST',
|
||||||
'GET_CONSUMER_GROUPS__SUCCESS',
|
'GET_CONSUMER_GROUPS__SUCCESS',
|
||||||
|
|
|
@ -230,3 +230,19 @@ export const updateTopic = (
|
||||||
dispatch(actions.updateTopicAction.failure());
|
dispatch(actions.updateTopicAction.failure());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const deleteTopic = (
|
||||||
|
clusterName: ClusterName,
|
||||||
|
topicName: TopicName
|
||||||
|
): PromiseThunkResult => async (dispatch) => {
|
||||||
|
dispatch(actions.deleteTopicAction.request());
|
||||||
|
try {
|
||||||
|
await topicsApiClient.deleteTopic({
|
||||||
|
clusterName,
|
||||||
|
topicName,
|
||||||
|
});
|
||||||
|
dispatch(actions.deleteTopicAction.success(topicName));
|
||||||
|
} catch (e) {
|
||||||
|
dispatch(actions.deleteTopicAction.failure());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { deleteTopicAction } from 'redux/actions';
|
||||||
|
import reducer from '../reducer';
|
||||||
|
|
||||||
|
describe('topics reducer', () => {
|
||||||
|
it('deletes the topic from the list on DELETE_TOPIC__SUCCESS', () => {
|
||||||
|
const topic = {
|
||||||
|
name: 'topic',
|
||||||
|
id: 'id',
|
||||||
|
};
|
||||||
|
expect(
|
||||||
|
reducer(
|
||||||
|
{
|
||||||
|
byName: {
|
||||||
|
topic,
|
||||||
|
},
|
||||||
|
allNames: [topic.name],
|
||||||
|
messages: [],
|
||||||
|
totalPages: 1,
|
||||||
|
},
|
||||||
|
deleteTopicAction.success(topic.name)
|
||||||
|
)
|
||||||
|
).toEqual({
|
||||||
|
byName: {},
|
||||||
|
allNames: [],
|
||||||
|
messages: [],
|
||||||
|
totalPages: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -45,6 +45,14 @@ const reducer = (state = initialState, action: Action): TopicsState => {
|
||||||
return action.payload;
|
return action.payload;
|
||||||
case getType(actions.fetchTopicMessagesAction.success):
|
case getType(actions.fetchTopicMessagesAction.success):
|
||||||
return transformTopicMessages(state, action.payload);
|
return transformTopicMessages(state, action.payload);
|
||||||
|
case getType(actions.deleteTopicAction.success): {
|
||||||
|
const newState: TopicsState = { ...state };
|
||||||
|
delete newState.byName[action.payload];
|
||||||
|
newState.allNames = newState.allNames.filter(
|
||||||
|
(name) => name !== action.payload
|
||||||
|
);
|
||||||
|
return newState;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue