parent
590bdfb610
commit
595707edb6
10 changed files with 156 additions and 7 deletions
|
@ -1,5 +1,9 @@
|
|||
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 { Link, useParams } from 'react-router-dom';
|
||||
import { clusterTopicNewPath } from 'lib/paths';
|
||||
|
@ -16,6 +20,7 @@ interface Props {
|
|||
externalTopics: TopicWithDetailedInfo[];
|
||||
totalPages: number;
|
||||
fetchTopicsList(props: FetchTopicsListParams): void;
|
||||
deleteTopic(topicName: TopicName, clusterName: ClusterName): void;
|
||||
}
|
||||
|
||||
const List: React.FC<Props> = ({
|
||||
|
@ -24,6 +29,7 @@ const List: React.FC<Props> = ({
|
|||
externalTopics,
|
||||
totalPages,
|
||||
fetchTopicsList,
|
||||
deleteTopic,
|
||||
}) => {
|
||||
const { isReadOnly } = React.useContext(ClusterContext);
|
||||
const { clusterName } = useParams<{ clusterName: ClusterName }>();
|
||||
|
@ -82,17 +88,23 @@ const List: React.FC<Props> = ({
|
|||
<th>Total Partitions</th>
|
||||
<th>Out of sync replicas</th>
|
||||
<th>Type</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((topic) => (
|
||||
<ListItem
|
||||
clusterName={clusterName}
|
||||
key={topic.name}
|
||||
topic={topic}
|
||||
deleteTopic={deleteTopic}
|
||||
/>
|
||||
))}
|
||||
{items.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={10}>No topics found</td>
|
||||
</tr>
|
||||
)}
|
||||
{items.map((topic) => (
|
||||
<ListItem key={topic.name} topic={topic} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<Pagination totalPages={totalPages} />
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { RootState } from 'redux/interfaces';
|
||||
import { fetchTopicsList } from 'redux/actions';
|
||||
import { fetchTopicsList, deleteTopic } from 'redux/actions';
|
||||
import {
|
||||
getTopicList,
|
||||
getExternalTopicList,
|
||||
|
@ -18,6 +18,7 @@ const mapStateToProps = (state: RootState) => ({
|
|||
|
||||
const mapDispatchToProps = {
|
||||
fetchTopicsList,
|
||||
deleteTopic,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(List);
|
||||
|
|
|
@ -1,14 +1,22 @@
|
|||
import React from 'react';
|
||||
import cx from 'classnames';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { TopicWithDetailedInfo } from 'redux/interfaces';
|
||||
import {
|
||||
ClusterName,
|
||||
TopicName,
|
||||
TopicWithDetailedInfo,
|
||||
} from 'redux/interfaces';
|
||||
|
||||
interface ListItemProps {
|
||||
topic: TopicWithDetailedInfo;
|
||||
deleteTopic: (clusterName: ClusterName, topicName: TopicName) => void;
|
||||
clusterName: ClusterName;
|
||||
}
|
||||
|
||||
const ListItem: React.FC<ListItemProps> = ({
|
||||
topic: { name, internal, partitions },
|
||||
deleteTopic,
|
||||
clusterName,
|
||||
}) => {
|
||||
const outOfSyncReplicas = React.useMemo(() => {
|
||||
if (partitions === undefined || partitions.length === 0) {
|
||||
|
@ -21,6 +29,10 @@ const ListItem: React.FC<ListItemProps> = ({
|
|||
}, 0);
|
||||
}, [partitions]);
|
||||
|
||||
const deleteTopicHandler = React.useCallback(() => {
|
||||
deleteTopic(clusterName, name);
|
||||
}, [clusterName, name]);
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td>
|
||||
|
@ -42,6 +54,17 @@ const ListItem: React.FC<ListItemProps> = ({
|
|||
{internal ? 'Internal' : 'External'}
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -16,6 +16,7 @@ describe('List', () => {
|
|||
externalTopics={[]}
|
||||
totalPages={1}
|
||||
fetchTopicsList={jest.fn()}
|
||||
deleteTopic={jest.fn()}
|
||||
/>
|
||||
</ClusterContext.Provider>
|
||||
</StaticRouter>
|
||||
|
@ -35,6 +36,7 @@ describe('List', () => {
|
|||
externalTopics={[]}
|
||||
totalPages={1}
|
||||
fetchTopicsList={jest.fn()}
|
||||
deleteTopic={jest.fn()}
|
||||
/>
|
||||
</ClusterContext.Provider>
|
||||
</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 clusterName = 'local';
|
||||
const topicName = 'localTopic';
|
||||
const subject = 'test';
|
||||
|
||||
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 { ConsumerGroupID, TopicsState } from 'redux/interfaces';
|
||||
import { ConsumerGroupID, TopicName, TopicsState } from 'redux/interfaces';
|
||||
|
||||
import {
|
||||
Cluster,
|
||||
|
@ -79,6 +79,12 @@ export const updateTopicAction = createAsyncAction(
|
|||
'PATCH_TOPIC__FAILURE'
|
||||
)<undefined, TopicsState, undefined>();
|
||||
|
||||
export const deleteTopicAction = createAsyncAction(
|
||||
'DELETE_TOPIC__REQUEST',
|
||||
'DELETE_TOPIC__SUCCESS',
|
||||
'DELETE_TOPIC__FAILURE'
|
||||
)<undefined, TopicName, undefined>();
|
||||
|
||||
export const fetchConsumerGroupsAction = createAsyncAction(
|
||||
'GET_CONSUMER_GROUPS__REQUEST',
|
||||
'GET_CONSUMER_GROUPS__SUCCESS',
|
||||
|
|
|
@ -230,3 +230,19 @@ export const updateTopic = (
|
|||
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;
|
||||
case getType(actions.fetchTopicMessagesAction.success):
|
||||
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:
|
||||
return state;
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue