List.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. import React from 'react';
  2. import { useHistory } from 'react-router';
  3. import {
  4. TopicWithDetailedInfo,
  5. ClusterName,
  6. TopicName,
  7. } from 'redux/interfaces';
  8. import { useParams } from 'react-router-dom';
  9. import { clusterTopicCopyPath, clusterTopicNewPath } from 'lib/paths';
  10. import usePagination from 'lib/hooks/usePagination';
  11. import ClusterContext from 'components/contexts/ClusterContext';
  12. import PageLoader from 'components/common/PageLoader/PageLoader';
  13. import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
  14. import {
  15. CleanUpPolicy,
  16. GetTopicsRequest,
  17. SortOrder,
  18. TopicColumnsToSort,
  19. } from 'generated-sources';
  20. import Search from 'components/common/Search/Search';
  21. import { PER_PAGE } from 'lib/constants';
  22. import { Button } from 'components/common/Button/Button';
  23. import PageHeading from 'components/common/PageHeading/PageHeading';
  24. import { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled';
  25. import Switch from 'components/common/Switch/Switch';
  26. import { SmartTable } from 'components/common/SmartTable/SmartTable';
  27. import {
  28. TableCellProps,
  29. TableColumn,
  30. } from 'components/common/SmartTable/TableColumn';
  31. import { useTableState } from 'lib/hooks/useTableState';
  32. import Dropdown from 'components/common/Dropdown/Dropdown';
  33. import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
  34. import DropdownItem from 'components/common/Dropdown/DropdownItem';
  35. import {
  36. MessagesCell,
  37. OutOfSyncReplicasCell,
  38. TitleCell,
  39. TopicSizeCell,
  40. } from './TopicsTableCells';
  41. import { ActionsTd } from './List.styled';
  42. export interface TopicsListProps {
  43. areTopicsFetching: boolean;
  44. topics: TopicWithDetailedInfo[];
  45. totalPages: number;
  46. fetchTopicsList(props: GetTopicsRequest): void;
  47. deleteTopic(topicName: TopicName, clusterName: ClusterName): void;
  48. deleteTopics(topicName: TopicName, clusterNames: ClusterName[]): void;
  49. recreateTopic(topicName: TopicName, clusterName: ClusterName): void;
  50. clearTopicsMessages(topicName: TopicName, clusterNames: ClusterName[]): void;
  51. clearTopicMessages(
  52. topicName: TopicName,
  53. clusterName: ClusterName,
  54. partitions?: number[]
  55. ): void;
  56. search: string;
  57. orderBy: TopicColumnsToSort | null;
  58. sortOrder: SortOrder;
  59. setTopicsSearch(search: string): void;
  60. setTopicsOrderBy(orderBy: TopicColumnsToSort | null): void;
  61. }
  62. const List: React.FC<TopicsListProps> = ({
  63. areTopicsFetching,
  64. topics,
  65. totalPages,
  66. fetchTopicsList,
  67. deleteTopic,
  68. deleteTopics,
  69. recreateTopic,
  70. clearTopicMessages,
  71. clearTopicsMessages,
  72. search,
  73. orderBy,
  74. sortOrder,
  75. setTopicsSearch,
  76. setTopicsOrderBy,
  77. }) => {
  78. const { isReadOnly, isTopicDeletionAllowed } =
  79. React.useContext(ClusterContext);
  80. const { clusterName } = useParams<{ clusterName: ClusterName }>();
  81. const { page, perPage, pathname } = usePagination();
  82. const [showInternal, setShowInternal] = React.useState<boolean>(true);
  83. const [cachedPage, setCachedPage] = React.useState<number | null>(null);
  84. const history = useHistory();
  85. React.useEffect(() => {
  86. fetchTopicsList({
  87. clusterName,
  88. page,
  89. perPage,
  90. orderBy: orderBy || undefined,
  91. sortOrder,
  92. search,
  93. showInternal,
  94. });
  95. }, [
  96. fetchTopicsList,
  97. clusterName,
  98. page,
  99. perPage,
  100. orderBy,
  101. sortOrder,
  102. search,
  103. showInternal,
  104. ]);
  105. const tableState = useTableState<
  106. TopicWithDetailedInfo,
  107. string,
  108. TopicColumnsToSort
  109. >(
  110. topics,
  111. {
  112. idSelector: (topic) => topic.name,
  113. totalPages,
  114. isRowSelectable: (topic) => !topic.internal,
  115. },
  116. {
  117. handleOrderBy: setTopicsOrderBy,
  118. orderBy,
  119. sortOrder,
  120. }
  121. );
  122. const getSelectedTopic = (): string => {
  123. const name = Array.from(tableState.selectedIds)[0];
  124. const selectedTopic =
  125. tableState.data.find(
  126. (topic: TopicWithDetailedInfo) => topic.name === name
  127. ) || {};
  128. return Object.keys(selectedTopic)
  129. .map((x: string) => {
  130. const value = selectedTopic[x as keyof typeof selectedTopic];
  131. return value && x !== 'partitions' ? `${x}=${value}` : null;
  132. })
  133. .join('&');
  134. };
  135. const handleSwitch = React.useCallback(() => {
  136. setShowInternal(!showInternal);
  137. history.push(`${pathname}?page=1&perPage=${perPage || PER_PAGE}`);
  138. }, [history, pathname, perPage, showInternal]);
  139. const [confirmationModal, setConfirmationModal] = React.useState<
  140. '' | 'deleteTopics' | 'purgeMessages'
  141. >('');
  142. const [confirmationModalText, setConfirmationModalText] =
  143. React.useState<string>('');
  144. const closeConfirmationModal = () => {
  145. setConfirmationModal('');
  146. };
  147. const clearSelectedTopics = React.useCallback(() => {
  148. tableState.toggleSelection(false);
  149. }, [tableState]);
  150. const searchHandler = React.useCallback(
  151. (searchString: string) => {
  152. setTopicsSearch(searchString);
  153. setCachedPage(page || null);
  154. const newPageQuery = !searchString && cachedPage ? cachedPage : 1;
  155. history.push(
  156. `${pathname}?page=${newPageQuery}&perPage=${perPage || PER_PAGE}`
  157. );
  158. },
  159. [setTopicsSearch, history, pathname, perPage, page]
  160. );
  161. const deleteOrPurgeConfirmationHandler = React.useCallback(() => {
  162. const selectedIds = Array.from(tableState.selectedIds);
  163. if (confirmationModal === 'deleteTopics') {
  164. deleteTopics(clusterName, selectedIds);
  165. } else {
  166. clearTopicsMessages(clusterName, selectedIds);
  167. }
  168. closeConfirmationModal();
  169. clearSelectedTopics();
  170. }, [
  171. confirmationModal,
  172. clearSelectedTopics,
  173. clusterName,
  174. deleteTopics,
  175. clearTopicsMessages,
  176. tableState.selectedIds,
  177. ]);
  178. const ActionsCell = React.memo<TableCellProps<TopicWithDetailedInfo, string>>(
  179. ({ hovered, dataItem: { internal, cleanUpPolicy, name } }) => {
  180. const [
  181. isDeleteTopicConfirmationVisible,
  182. setDeleteTopicConfirmationVisible,
  183. ] = React.useState(false);
  184. const [
  185. isRecreateTopicConfirmationVisible,
  186. setRecreateTopicConfirmationVisible,
  187. ] = React.useState(false);
  188. const isHidden = internal || isReadOnly || !hovered;
  189. const deleteTopicHandler = React.useCallback(() => {
  190. deleteTopic(clusterName, name);
  191. }, [name]);
  192. const clearTopicMessagesHandler = React.useCallback(() => {
  193. clearTopicMessages(clusterName, name);
  194. }, [name]);
  195. const recreateTopicHandler = React.useCallback(() => {
  196. recreateTopic(clusterName, name);
  197. setRecreateTopicConfirmationVisible(false);
  198. }, [name]);
  199. return (
  200. <>
  201. <div className="has-text-right">
  202. {!isHidden && (
  203. <Dropdown label={<VerticalElipsisIcon />} right>
  204. {cleanUpPolicy === CleanUpPolicy.DELETE && (
  205. <DropdownItem onClick={clearTopicMessagesHandler} danger>
  206. Clear Messages
  207. </DropdownItem>
  208. )}
  209. {isTopicDeletionAllowed && (
  210. <DropdownItem
  211. onClick={() => setDeleteTopicConfirmationVisible(true)}
  212. danger
  213. >
  214. Remove Topic
  215. </DropdownItem>
  216. )}
  217. <DropdownItem
  218. onClick={() => setRecreateTopicConfirmationVisible(true)}
  219. danger
  220. >
  221. Recreate Topic
  222. </DropdownItem>
  223. </Dropdown>
  224. )}
  225. </div>
  226. <ConfirmationModal
  227. isOpen={isDeleteTopicConfirmationVisible}
  228. onCancel={() => setDeleteTopicConfirmationVisible(false)}
  229. onConfirm={deleteTopicHandler}
  230. >
  231. Are you sure want to remove <b>{name}</b> topic?
  232. </ConfirmationModal>
  233. <ConfirmationModal
  234. isOpen={isRecreateTopicConfirmationVisible}
  235. onCancel={() => setRecreateTopicConfirmationVisible(false)}
  236. onConfirm={recreateTopicHandler}
  237. >
  238. Are you sure to recreate <b>{name}</b> topic?
  239. </ConfirmationModal>
  240. </>
  241. );
  242. }
  243. );
  244. return (
  245. <div>
  246. <div>
  247. <PageHeading text="All Topics">
  248. {!isReadOnly && (
  249. <Button
  250. buttonType="primary"
  251. buttonSize="M"
  252. isLink
  253. to={clusterTopicNewPath(clusterName)}
  254. >
  255. <i className="fas fa-plus" /> Add a Topic
  256. </Button>
  257. )}
  258. </PageHeading>
  259. <ControlPanelWrapper hasInput>
  260. <div>
  261. <Search
  262. handleSearch={searchHandler}
  263. placeholder="Search by Topic Name"
  264. value={search}
  265. />
  266. </div>
  267. <div>
  268. <Switch
  269. name="ShowInternalTopics"
  270. checked={showInternal}
  271. onChange={handleSwitch}
  272. />
  273. <label>Show Internal Topics</label>
  274. </div>
  275. </ControlPanelWrapper>
  276. </div>
  277. {areTopicsFetching ? (
  278. <PageLoader />
  279. ) : (
  280. <div>
  281. {tableState.selectedCount > 0 && (
  282. <>
  283. <ControlPanelWrapper data-testid="delete-buttons">
  284. <Button
  285. buttonSize="M"
  286. buttonType="secondary"
  287. onClick={() => {
  288. setConfirmationModal('deleteTopics');
  289. setConfirmationModalText(
  290. 'Are you sure you want to remove selected topics?'
  291. );
  292. }}
  293. >
  294. Delete selected topics
  295. </Button>
  296. {tableState.selectedCount === 1 && (
  297. <Button
  298. buttonSize="M"
  299. buttonType="secondary"
  300. isLink
  301. to={{
  302. pathname: clusterTopicCopyPath(clusterName),
  303. search: `?${getSelectedTopic()}`,
  304. }}
  305. >
  306. Copy selected topic
  307. </Button>
  308. )}
  309. <Button
  310. buttonSize="M"
  311. buttonType="secondary"
  312. onClick={() => {
  313. setConfirmationModal('purgeMessages');
  314. setConfirmationModalText(
  315. 'Are you sure you want to purge messages of selected topics?'
  316. );
  317. }}
  318. >
  319. Purge messages of selected topics
  320. </Button>
  321. </ControlPanelWrapper>
  322. <ConfirmationModal
  323. isOpen={confirmationModal !== ''}
  324. onCancel={closeConfirmationModal}
  325. onConfirm={deleteOrPurgeConfirmationHandler}
  326. >
  327. {confirmationModalText}
  328. </ConfirmationModal>
  329. </>
  330. )}
  331. <SmartTable
  332. selectable={!isReadOnly}
  333. tableState={tableState}
  334. placeholder="No topics found"
  335. isFullwidth
  336. paginated
  337. hoverable
  338. >
  339. <TableColumn
  340. maxWidth="350px"
  341. title="Topic Name"
  342. cell={TitleCell}
  343. orderValue={TopicColumnsToSort.NAME}
  344. />
  345. <TableColumn
  346. title="Total Partitions"
  347. field="partitions.length"
  348. orderValue={TopicColumnsToSort.TOTAL_PARTITIONS}
  349. />
  350. <TableColumn
  351. title="Out of sync replicas"
  352. cell={OutOfSyncReplicasCell}
  353. orderValue={TopicColumnsToSort.OUT_OF_SYNC_REPLICAS}
  354. />
  355. <TableColumn title="Replication Factor" field="replicationFactor" />
  356. <TableColumn title="Number of messages" cell={MessagesCell} />
  357. <TableColumn
  358. title="Size"
  359. cell={TopicSizeCell}
  360. orderValue={TopicColumnsToSort.SIZE}
  361. />
  362. <TableColumn
  363. maxWidth="4%"
  364. cell={ActionsCell}
  365. customTd={ActionsTd}
  366. />
  367. </SmartTable>
  368. </div>
  369. )}
  370. </div>
  371. );
  372. };
  373. export default List;