
* persist show internal topics switch state * minor improvements in topics list tests * prevent duplication in topic list test file
406 lines
12 KiB
TypeScript
406 lines
12 KiB
TypeScript
import React from 'react';
|
|
import { useHistory } from 'react-router';
|
|
import {
|
|
TopicWithDetailedInfo,
|
|
ClusterName,
|
|
TopicName,
|
|
} from 'redux/interfaces';
|
|
import { useParams } from 'react-router-dom';
|
|
import { clusterTopicCopyPath, clusterTopicNewPath } from 'lib/paths';
|
|
import usePagination from 'lib/hooks/usePagination';
|
|
import ClusterContext from 'components/contexts/ClusterContext';
|
|
import PageLoader from 'components/common/PageLoader/PageLoader';
|
|
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
|
|
import {
|
|
CleanUpPolicy,
|
|
GetTopicsRequest,
|
|
SortOrder,
|
|
TopicColumnsToSort,
|
|
} from 'generated-sources';
|
|
import Search from 'components/common/Search/Search';
|
|
import { PER_PAGE } from 'lib/constants';
|
|
import { Button } from 'components/common/Button/Button';
|
|
import PageHeading from 'components/common/PageHeading/PageHeading';
|
|
import { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled';
|
|
import Switch from 'components/common/Switch/Switch';
|
|
import { SmartTable } from 'components/common/SmartTable/SmartTable';
|
|
import {
|
|
TableCellProps,
|
|
TableColumn,
|
|
} from 'components/common/SmartTable/TableColumn';
|
|
import { useTableState } from 'lib/hooks/useTableState';
|
|
import Dropdown from 'components/common/Dropdown/Dropdown';
|
|
import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
|
|
import DropdownItem from 'components/common/Dropdown/DropdownItem';
|
|
|
|
import {
|
|
MessagesCell,
|
|
OutOfSyncReplicasCell,
|
|
TitleCell,
|
|
TopicSizeCell,
|
|
} from './TopicsTableCells';
|
|
import { ActionsTd } from './List.styled';
|
|
|
|
export interface TopicsListProps {
|
|
areTopicsFetching: boolean;
|
|
topics: TopicWithDetailedInfo[];
|
|
totalPages: number;
|
|
fetchTopicsList(props: GetTopicsRequest): void;
|
|
deleteTopic(topicName: TopicName, clusterName: ClusterName): void;
|
|
deleteTopics(topicName: TopicName, clusterNames: ClusterName[]): void;
|
|
recreateTopic(topicName: TopicName, clusterName: ClusterName): void;
|
|
clearTopicsMessages(topicName: TopicName, clusterNames: ClusterName[]): void;
|
|
clearTopicMessages(
|
|
topicName: TopicName,
|
|
clusterName: ClusterName,
|
|
partitions?: number[]
|
|
): void;
|
|
search: string;
|
|
orderBy: TopicColumnsToSort | null;
|
|
sortOrder: SortOrder;
|
|
setTopicsSearch(search: string): void;
|
|
setTopicsOrderBy(orderBy: TopicColumnsToSort | null): void;
|
|
}
|
|
|
|
const List: React.FC<TopicsListProps> = ({
|
|
areTopicsFetching,
|
|
topics,
|
|
totalPages,
|
|
fetchTopicsList,
|
|
deleteTopic,
|
|
deleteTopics,
|
|
recreateTopic,
|
|
clearTopicMessages,
|
|
clearTopicsMessages,
|
|
search,
|
|
orderBy,
|
|
sortOrder,
|
|
setTopicsSearch,
|
|
setTopicsOrderBy,
|
|
}) => {
|
|
const { isReadOnly, isTopicDeletionAllowed } =
|
|
React.useContext(ClusterContext);
|
|
const { clusterName } = useParams<{ clusterName: ClusterName }>();
|
|
const { page, perPage, pathname } = usePagination();
|
|
const [showInternal, setShowInternal] = React.useState<boolean>(
|
|
!localStorage.getItem('hideInternalTopics') && true
|
|
);
|
|
const [cachedPage, setCachedPage] = React.useState<number | null>(null);
|
|
const history = useHistory();
|
|
|
|
React.useEffect(() => {
|
|
fetchTopicsList({
|
|
clusterName,
|
|
page,
|
|
perPage,
|
|
orderBy: orderBy || undefined,
|
|
sortOrder,
|
|
search,
|
|
showInternal,
|
|
});
|
|
}, [
|
|
fetchTopicsList,
|
|
clusterName,
|
|
page,
|
|
perPage,
|
|
orderBy,
|
|
sortOrder,
|
|
search,
|
|
showInternal,
|
|
]);
|
|
|
|
const tableState = useTableState<
|
|
TopicWithDetailedInfo,
|
|
string,
|
|
TopicColumnsToSort
|
|
>(
|
|
topics,
|
|
{
|
|
idSelector: (topic) => topic.name,
|
|
totalPages,
|
|
isRowSelectable: (topic) => !topic.internal,
|
|
},
|
|
{
|
|
handleOrderBy: setTopicsOrderBy,
|
|
orderBy,
|
|
sortOrder,
|
|
}
|
|
);
|
|
|
|
const getSelectedTopic = (): string => {
|
|
const name = Array.from(tableState.selectedIds)[0];
|
|
const selectedTopic =
|
|
tableState.data.find(
|
|
(topic: TopicWithDetailedInfo) => topic.name === name
|
|
) || {};
|
|
|
|
return Object.keys(selectedTopic)
|
|
.map((x: string) => {
|
|
const value = selectedTopic[x as keyof typeof selectedTopic];
|
|
return value && x !== 'partitions' ? `${x}=${value}` : null;
|
|
})
|
|
.join('&');
|
|
};
|
|
|
|
const handleSwitch = React.useCallback(() => {
|
|
if (showInternal) {
|
|
localStorage.setItem('hideInternalTopics', 'true');
|
|
} else {
|
|
localStorage.removeItem('hideInternalTopics');
|
|
}
|
|
|
|
setShowInternal(!showInternal);
|
|
history.push(`${pathname}?page=1&perPage=${perPage || PER_PAGE}`);
|
|
}, [history, pathname, perPage, showInternal]);
|
|
|
|
const [confirmationModal, setConfirmationModal] = React.useState<
|
|
'' | 'deleteTopics' | 'purgeMessages'
|
|
>('');
|
|
|
|
const [confirmationModalText, setConfirmationModalText] =
|
|
React.useState<string>('');
|
|
const closeConfirmationModal = () => {
|
|
setConfirmationModal('');
|
|
};
|
|
|
|
const clearSelectedTopics = React.useCallback(() => {
|
|
tableState.toggleSelection(false);
|
|
}, [tableState]);
|
|
|
|
const searchHandler = React.useCallback(
|
|
(searchString: string) => {
|
|
setTopicsSearch(searchString);
|
|
|
|
setCachedPage(page || null);
|
|
|
|
const newPageQuery = !searchString && cachedPage ? cachedPage : 1;
|
|
|
|
history.push(
|
|
`${pathname}?page=${newPageQuery}&perPage=${perPage || PER_PAGE}`
|
|
);
|
|
},
|
|
[setTopicsSearch, history, pathname, perPage, page]
|
|
);
|
|
const deleteOrPurgeConfirmationHandler = React.useCallback(() => {
|
|
const selectedIds = Array.from(tableState.selectedIds);
|
|
if (confirmationModal === 'deleteTopics') {
|
|
deleteTopics(clusterName, selectedIds);
|
|
} else {
|
|
clearTopicsMessages(clusterName, selectedIds);
|
|
}
|
|
closeConfirmationModal();
|
|
clearSelectedTopics();
|
|
}, [
|
|
confirmationModal,
|
|
clearSelectedTopics,
|
|
clusterName,
|
|
deleteTopics,
|
|
clearTopicsMessages,
|
|
tableState.selectedIds,
|
|
]);
|
|
|
|
const ActionsCell = React.memo<TableCellProps<TopicWithDetailedInfo, string>>(
|
|
({ hovered, dataItem: { internal, cleanUpPolicy, name } }) => {
|
|
const [
|
|
isDeleteTopicConfirmationVisible,
|
|
setDeleteTopicConfirmationVisible,
|
|
] = React.useState(false);
|
|
|
|
const [
|
|
isRecreateTopicConfirmationVisible,
|
|
setRecreateTopicConfirmationVisible,
|
|
] = React.useState(false);
|
|
|
|
const isHidden = internal || isReadOnly || !hovered;
|
|
|
|
const deleteTopicHandler = React.useCallback(() => {
|
|
deleteTopic(clusterName, name);
|
|
}, [name]);
|
|
|
|
const clearTopicMessagesHandler = React.useCallback(() => {
|
|
clearTopicMessages(clusterName, name);
|
|
}, [name]);
|
|
|
|
const recreateTopicHandler = React.useCallback(() => {
|
|
recreateTopic(clusterName, name);
|
|
setRecreateTopicConfirmationVisible(false);
|
|
}, [name]);
|
|
|
|
return (
|
|
<>
|
|
<div className="has-text-right">
|
|
{!isHidden && (
|
|
<Dropdown label={<VerticalElipsisIcon />} right>
|
|
{cleanUpPolicy === CleanUpPolicy.DELETE && (
|
|
<DropdownItem onClick={clearTopicMessagesHandler} danger>
|
|
Clear Messages
|
|
</DropdownItem>
|
|
)}
|
|
{isTopicDeletionAllowed && (
|
|
<DropdownItem
|
|
onClick={() => setDeleteTopicConfirmationVisible(true)}
|
|
danger
|
|
>
|
|
Remove Topic
|
|
</DropdownItem>
|
|
)}
|
|
<DropdownItem
|
|
onClick={() => setRecreateTopicConfirmationVisible(true)}
|
|
danger
|
|
>
|
|
Recreate Topic
|
|
</DropdownItem>
|
|
</Dropdown>
|
|
)}
|
|
</div>
|
|
<ConfirmationModal
|
|
isOpen={isDeleteTopicConfirmationVisible}
|
|
onCancel={() => setDeleteTopicConfirmationVisible(false)}
|
|
onConfirm={deleteTopicHandler}
|
|
>
|
|
Are you sure want to remove <b>{name}</b> topic?
|
|
</ConfirmationModal>
|
|
<ConfirmationModal
|
|
isOpen={isRecreateTopicConfirmationVisible}
|
|
onCancel={() => setRecreateTopicConfirmationVisible(false)}
|
|
onConfirm={recreateTopicHandler}
|
|
>
|
|
Are you sure to recreate <b>{name}</b> topic?
|
|
</ConfirmationModal>
|
|
</>
|
|
);
|
|
}
|
|
);
|
|
|
|
return (
|
|
<div>
|
|
<div>
|
|
<PageHeading text="All Topics">
|
|
{!isReadOnly && (
|
|
<Button
|
|
buttonType="primary"
|
|
buttonSize="M"
|
|
isLink
|
|
to={clusterTopicNewPath(clusterName)}
|
|
>
|
|
<i className="fas fa-plus" /> Add a Topic
|
|
</Button>
|
|
)}
|
|
</PageHeading>
|
|
<ControlPanelWrapper hasInput>
|
|
<div>
|
|
<Search
|
|
handleSearch={searchHandler}
|
|
placeholder="Search by Topic Name"
|
|
value={search}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Switch
|
|
name="ShowInternalTopics"
|
|
checked={showInternal}
|
|
onChange={handleSwitch}
|
|
/>
|
|
<label>Show Internal Topics</label>
|
|
</div>
|
|
</ControlPanelWrapper>
|
|
</div>
|
|
{areTopicsFetching ? (
|
|
<PageLoader />
|
|
) : (
|
|
<div>
|
|
{tableState.selectedCount > 0 && (
|
|
<>
|
|
<ControlPanelWrapper data-testid="delete-buttons">
|
|
<Button
|
|
buttonSize="M"
|
|
buttonType="secondary"
|
|
onClick={() => {
|
|
setConfirmationModal('deleteTopics');
|
|
setConfirmationModalText(
|
|
'Are you sure you want to remove selected topics?'
|
|
);
|
|
}}
|
|
>
|
|
Delete selected topics
|
|
</Button>
|
|
{tableState.selectedCount === 1 && (
|
|
<Button
|
|
buttonSize="M"
|
|
buttonType="secondary"
|
|
isLink
|
|
to={{
|
|
pathname: clusterTopicCopyPath(clusterName),
|
|
search: `?${getSelectedTopic()}`,
|
|
}}
|
|
>
|
|
Copy selected topic
|
|
</Button>
|
|
)}
|
|
|
|
<Button
|
|
buttonSize="M"
|
|
buttonType="secondary"
|
|
onClick={() => {
|
|
setConfirmationModal('purgeMessages');
|
|
setConfirmationModalText(
|
|
'Are you sure you want to purge messages of selected topics?'
|
|
);
|
|
}}
|
|
>
|
|
Purge messages of selected topics
|
|
</Button>
|
|
</ControlPanelWrapper>
|
|
<ConfirmationModal
|
|
isOpen={confirmationModal !== ''}
|
|
onCancel={closeConfirmationModal}
|
|
onConfirm={deleteOrPurgeConfirmationHandler}
|
|
>
|
|
{confirmationModalText}
|
|
</ConfirmationModal>
|
|
</>
|
|
)}
|
|
<SmartTable
|
|
selectable={!isReadOnly}
|
|
tableState={tableState}
|
|
placeholder="No topics found"
|
|
isFullwidth
|
|
paginated
|
|
hoverable
|
|
>
|
|
<TableColumn
|
|
maxWidth="350px"
|
|
title="Topic Name"
|
|
cell={TitleCell}
|
|
orderValue={TopicColumnsToSort.NAME}
|
|
/>
|
|
<TableColumn
|
|
title="Total Partitions"
|
|
field="partitions.length"
|
|
orderValue={TopicColumnsToSort.TOTAL_PARTITIONS}
|
|
/>
|
|
<TableColumn
|
|
title="Out of sync replicas"
|
|
cell={OutOfSyncReplicasCell}
|
|
orderValue={TopicColumnsToSort.OUT_OF_SYNC_REPLICAS}
|
|
/>
|
|
<TableColumn title="Replication Factor" field="replicationFactor" />
|
|
<TableColumn title="Number of messages" cell={MessagesCell} />
|
|
<TableColumn
|
|
title="Size"
|
|
cell={TopicSizeCell}
|
|
orderValue={TopicColumnsToSort.SIZE}
|
|
/>
|
|
<TableColumn
|
|
maxWidth="4%"
|
|
cell={ActionsCell}
|
|
customTd={ActionsTd}
|
|
/>
|
|
</SmartTable>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default List;
|