kafka-ui/kafka-ui-react-app/src/components/Topics/List/List.tsx
Arsen Simonyan 8413998974 Persist show internal topics switch state (#1832)
* persist show internal topics switch state

* minor improvements in topics list tests

* prevent duplication in topic list test file
2022-05-04 10:47:51 +04:00

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;