List.tsx 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. import React from 'react';
  2. import { useNavigate } from 'react-router-dom';
  3. import useAppParams from 'lib/hooks/useAppParams';
  4. import {
  5. TopicWithDetailedInfo,
  6. ClusterName,
  7. TopicName,
  8. } from 'redux/interfaces';
  9. import {
  10. ClusterNameRoute,
  11. clusterTopicCopyRelativePath,
  12. clusterTopicNewRelativePath,
  13. } from 'lib/paths';
  14. import usePagination from 'lib/hooks/usePagination';
  15. import ClusterContext from 'components/contexts/ClusterContext';
  16. import PageLoader from 'components/common/PageLoader/PageLoader';
  17. import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
  18. import {
  19. GetTopicsRequest,
  20. SortOrder,
  21. TopicColumnsToSort,
  22. } from 'generated-sources';
  23. import Search from 'components/common/Search/Search';
  24. import { PER_PAGE } from 'lib/constants';
  25. import { Button } from 'components/common/Button/Button';
  26. import PageHeading from 'components/common/PageHeading/PageHeading';
  27. import { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled';
  28. import Switch from 'components/common/Switch/Switch';
  29. import { SmartTable } from 'components/common/SmartTable/SmartTable';
  30. import { TableColumn } from 'components/common/SmartTable/TableColumn';
  31. import { useTableState } from 'lib/hooks/useTableState';
  32. import PlusIcon from 'components/common/Icons/PlusIcon';
  33. import {
  34. MessagesCell,
  35. OutOfSyncReplicasCell,
  36. TitleCell,
  37. TopicSizeCell,
  38. } from './TopicsTableCells';
  39. import * as S from './List.styled';
  40. import ActionsCell from './ActionsCell/ActionsCell';
  41. export interface TopicsListProps {
  42. areTopicsFetching: boolean;
  43. topics: TopicWithDetailedInfo[];
  44. totalPages: number;
  45. fetchTopicsList(payload: GetTopicsRequest): void;
  46. deleteTopics(payload: {
  47. clusterName: ClusterName;
  48. topicNames: TopicName[];
  49. }): void;
  50. clearTopicsMessages(payload: {
  51. clusterName: ClusterName;
  52. topicNames: TopicName[];
  53. }): void;
  54. clearTopicMessages(payload: {
  55. topicName: TopicName;
  56. clusterName: ClusterName;
  57. partitions?: number[];
  58. }): void;
  59. search: string;
  60. orderBy: string | null;
  61. sortOrder: SortOrder;
  62. setTopicsSearch(search: string): void;
  63. setTopicsOrderBy(orderBy: string | null): void;
  64. }
  65. const List: React.FC<TopicsListProps> = ({
  66. areTopicsFetching,
  67. topics,
  68. totalPages,
  69. fetchTopicsList,
  70. deleteTopics,
  71. clearTopicsMessages,
  72. search,
  73. orderBy,
  74. sortOrder,
  75. setTopicsSearch,
  76. setTopicsOrderBy,
  77. }) => {
  78. const { isReadOnly } = React.useContext(ClusterContext);
  79. const { clusterName } = useAppParams<ClusterNameRoute>();
  80. const { page, perPage } = usePagination();
  81. const [showInternal, setShowInternal] = React.useState<boolean>(
  82. !localStorage.getItem('hideInternalTopics') && true
  83. );
  84. const [cachedPage, setCachedPage] = React.useState<number | null>(
  85. page || null
  86. );
  87. const navigate = useNavigate();
  88. const topicsListParams = React.useMemo(
  89. () => ({
  90. clusterName,
  91. page,
  92. perPage,
  93. orderBy: (orderBy as TopicColumnsToSort) || undefined,
  94. sortOrder,
  95. search,
  96. showInternal,
  97. }),
  98. [clusterName, page, perPage, orderBy, sortOrder, search, showInternal]
  99. );
  100. React.useEffect(() => {
  101. fetchTopicsList(topicsListParams);
  102. }, [fetchTopicsList, topicsListParams]);
  103. const tableState = useTableState<TopicWithDetailedInfo, string>(
  104. topics,
  105. {
  106. idSelector: (topic) => topic.name,
  107. totalPages,
  108. isRowSelectable: (topic) => !topic.internal,
  109. },
  110. {
  111. handleOrderBy: setTopicsOrderBy,
  112. orderBy,
  113. sortOrder,
  114. }
  115. );
  116. const getSelectedTopic = (): string => {
  117. const name = Array.from(tableState.selectedIds)[0];
  118. const selectedTopic =
  119. tableState.data.find(
  120. (topic: TopicWithDetailedInfo) => topic.name === name
  121. ) || {};
  122. return Object.keys(selectedTopic)
  123. .map((x: string) => {
  124. const value = selectedTopic[x as keyof typeof selectedTopic];
  125. return value && x !== 'partitions' ? `${x}=${value}` : null;
  126. })
  127. .join('&');
  128. };
  129. const handleSwitch = () => {
  130. if (showInternal) {
  131. localStorage.setItem('hideInternalTopics', 'true');
  132. } else {
  133. localStorage.removeItem('hideInternalTopics');
  134. }
  135. setShowInternal(!showInternal);
  136. navigate({
  137. search: `?page=1&perPage=${perPage || PER_PAGE}`,
  138. });
  139. };
  140. const [confirmationModal, setConfirmationModal] = React.useState<
  141. '' | 'deleteTopics' | 'purgeMessages'
  142. >('');
  143. const [confirmationModalText, setConfirmationModalText] =
  144. React.useState<string>('');
  145. const closeConfirmationModal = () => {
  146. setConfirmationModal('');
  147. };
  148. const clearSelectedTopics = () => tableState.toggleSelection(false);
  149. const searchHandler = (searchString: string) => {
  150. setTopicsSearch(searchString);
  151. setCachedPage(page || null);
  152. const newPageQuery = !searchString && cachedPage ? cachedPage : 1;
  153. navigate({
  154. search: `?page=${newPageQuery}&perPage=${perPage || PER_PAGE}`,
  155. });
  156. };
  157. const deleteOrPurgeConfirmationHandler = () => {
  158. const selectedIds = Array.from(tableState.selectedIds);
  159. if (confirmationModal === 'deleteTopics') {
  160. deleteTopics({ clusterName, topicNames: selectedIds });
  161. } else {
  162. clearTopicsMessages({ clusterName, topicNames: selectedIds });
  163. }
  164. closeConfirmationModal();
  165. clearSelectedTopics();
  166. fetchTopicsList(topicsListParams);
  167. };
  168. return (
  169. <div>
  170. <div>
  171. <PageHeading text="All Topics">
  172. {!isReadOnly && (
  173. <Button
  174. buttonType="primary"
  175. buttonSize="M"
  176. to={clusterTopicNewRelativePath}
  177. >
  178. <PlusIcon /> Add a Topic
  179. </Button>
  180. )}
  181. </PageHeading>
  182. <ControlPanelWrapper hasInput>
  183. <div>
  184. <Search
  185. handleSearch={searchHandler}
  186. placeholder="Search by Topic Name"
  187. value={search}
  188. />
  189. </div>
  190. <div>
  191. <Switch
  192. name="ShowInternalTopics"
  193. checked={showInternal}
  194. onChange={handleSwitch}
  195. />
  196. <label>Show Internal Topics</label>
  197. </div>
  198. </ControlPanelWrapper>
  199. </div>
  200. {areTopicsFetching ? (
  201. <PageLoader />
  202. ) : (
  203. <div>
  204. {tableState.selectedCount > 0 && (
  205. <>
  206. <ControlPanelWrapper data-testid="delete-buttons">
  207. <Button
  208. buttonSize="M"
  209. buttonType="secondary"
  210. onClick={() => {
  211. setConfirmationModal('deleteTopics');
  212. setConfirmationModalText(
  213. 'Are you sure you want to remove selected topics?'
  214. );
  215. }}
  216. >
  217. Delete selected topics
  218. </Button>
  219. {tableState.selectedCount === 1 && (
  220. <Button
  221. buttonSize="M"
  222. buttonType="secondary"
  223. to={{
  224. pathname: clusterTopicCopyRelativePath,
  225. search: `?${getSelectedTopic()}`,
  226. }}
  227. >
  228. Copy selected topic
  229. </Button>
  230. )}
  231. <Button
  232. buttonSize="M"
  233. buttonType="secondary"
  234. onClick={() => {
  235. setConfirmationModal('purgeMessages');
  236. setConfirmationModalText(
  237. 'Are you sure you want to purge messages of selected topics?'
  238. );
  239. }}
  240. >
  241. Purge messages of selected topics
  242. </Button>
  243. </ControlPanelWrapper>
  244. <ConfirmationModal
  245. isOpen={confirmationModal !== ''}
  246. onCancel={closeConfirmationModal}
  247. onConfirm={deleteOrPurgeConfirmationHandler}
  248. >
  249. {confirmationModalText}
  250. </ConfirmationModal>
  251. </>
  252. )}
  253. <SmartTable
  254. selectable={!isReadOnly}
  255. tableState={tableState}
  256. placeholder="No topics found"
  257. isFullwidth
  258. paginated
  259. hoverable
  260. >
  261. <TableColumn
  262. maxWidth="350px"
  263. title="Topic Name"
  264. cell={TitleCell}
  265. orderValue={TopicColumnsToSort.NAME}
  266. />
  267. <TableColumn
  268. title="Total Partitions"
  269. field="partitions.length"
  270. orderValue={TopicColumnsToSort.TOTAL_PARTITIONS}
  271. />
  272. <TableColumn
  273. title="Out of sync replicas"
  274. cell={OutOfSyncReplicasCell}
  275. orderValue={TopicColumnsToSort.OUT_OF_SYNC_REPLICAS}
  276. />
  277. <TableColumn title="Replication Factor" field="replicationFactor" />
  278. <TableColumn title="Number of messages" cell={MessagesCell} />
  279. <TableColumn
  280. title="Size"
  281. cell={TopicSizeCell}
  282. orderValue={TopicColumnsToSort.SIZE}
  283. />
  284. <TableColumn
  285. maxWidth="4%"
  286. cell={ActionsCell}
  287. customTd={S.ActionsTd}
  288. />
  289. </SmartTable>
  290. </div>
  291. )}
  292. </div>
  293. );
  294. };
  295. export default List;