diff --git a/kafka-ui-react-app/package.json b/kafka-ui-react-app/package.json index defe27f5c6..9b8c455948 100644 --- a/kafka-ui-react-app/package.json +++ b/kafka-ui-react-app/package.json @@ -104,7 +104,7 @@ "eslint-plugin-jest-dom": "^4.0.2", "eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-react": "^7.29.4", + "eslint-plugin-react": "^7.30.1", "eslint-plugin-react-hooks": "^4.5.0", "fetch-mock-jest": "^1.5.1", "husky": "^8.0.1", diff --git a/kafka-ui-react-app/pnpm-lock.yaml b/kafka-ui-react-app/pnpm-lock.yaml index 014f2caf68..dd0fc376ea 100644 --- a/kafka-ui-react-app/pnpm-lock.yaml +++ b/kafka-ui-react-app/pnpm-lock.yaml @@ -50,7 +50,7 @@ specifiers: eslint-plugin-jest-dom: ^4.0.2 eslint-plugin-jsx-a11y: ^6.5.1 eslint-plugin-prettier: ^4.0.0 - eslint-plugin-react: ^7.29.4 + eslint-plugin-react: ^7.30.1 eslint-plugin-react-hooks: ^4.5.0 fetch-mock: ^9.11.0 fetch-mock-jest: ^1.5.1 @@ -155,7 +155,7 @@ devDependencies: '@typescript-eslint/parser': 5.29.0_vjep2yp2sits3sqnodefgcbnfi dotenv: 16.0.1 eslint: 8.16.0 - eslint-config-airbnb: 19.0.4_pamhosqcenlxoxj4ke2dmvzava + eslint-config-airbnb: 19.0.4_iayhaebzx3saen2ll7sn5gqmdq eslint-config-airbnb-typescript: 17.0.0_l6wia5brkiej5f4nhesunbzj5y eslint-config-prettier: 8.5.0_eslint@8.16.0 eslint-config-react-app: 7.0.1_y3w6q4zxtal6gz5auqli3lo3ny @@ -165,7 +165,7 @@ devDependencies: eslint-plugin-jest-dom: 4.0.2_eslint@8.16.0 eslint-plugin-jsx-a11y: 6.5.1_eslint@8.16.0 eslint-plugin-prettier: 4.0.0_q7a4ir2sdihdzpzdlnbgmzjlpq - eslint-plugin-react: 7.29.4_eslint@8.16.0 + eslint-plugin-react: 7.30.1_eslint@8.16.0 eslint-plugin-react-hooks: 4.5.0_eslint@8.16.0 fetch-mock-jest: 1.5.1 husky: 8.0.1 @@ -4208,7 +4208,7 @@ packages: eslint-plugin-import: 2.26.0_h5azci6ujakbaa2xblg2jlxooy dev: true - /eslint-config-airbnb/19.0.4_pamhosqcenlxoxj4ke2dmvzava: + /eslint-config-airbnb/19.0.4_iayhaebzx3saen2ll7sn5gqmdq: resolution: {integrity: sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==} engines: {node: ^10.12.0 || ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -4222,7 +4222,7 @@ packages: eslint-config-airbnb-base: 15.0.0_btspkuwbqkl4adpiufzbathtpi eslint-plugin-import: 2.26.0_h5azci6ujakbaa2xblg2jlxooy eslint-plugin-jsx-a11y: 6.5.1_eslint@8.16.0 - eslint-plugin-react: 7.29.4_eslint@8.16.0 + eslint-plugin-react: 7.30.1_eslint@8.16.0 eslint-plugin-react-hooks: 4.5.0_eslint@8.16.0 object.assign: 4.1.2 object.entries: 1.1.5 @@ -4259,7 +4259,7 @@ packages: eslint-plugin-import: 2.26.0_h5azci6ujakbaa2xblg2jlxooy eslint-plugin-jest: 25.7.0_lemdnf4suuyhpl5s2mhp2xyjqi eslint-plugin-jsx-a11y: 6.5.1_eslint@8.16.0 - eslint-plugin-react: 7.29.4_eslint@8.16.0 + eslint-plugin-react: 7.30.1_eslint@8.16.0 eslint-plugin-react-hooks: 4.5.0_eslint@8.16.0 eslint-plugin-testing-library: 5.5.0_vjep2yp2sits3sqnodefgcbnfi typescript: 4.7.4 @@ -4455,8 +4455,8 @@ packages: eslint: 8.16.0 dev: true - /eslint-plugin-react/7.29.4_eslint@8.16.0: - resolution: {integrity: sha512-CVCXajliVh509PcZYRFyu/BoUEz452+jtQJq2b3Bae4v3xBUWPLCmtmBM+ZinG4MzwmxJgJ2M5rMqhqLVn7MtQ==} + /eslint-plugin-react/7.30.1_eslint@8.16.0: + resolution: {integrity: sha512-NbEvI9jtqO46yJA3wcRF9Mo0lF9T/jhdHqhCHXiXtD+Zcb98812wvokjWpU7Q4QH5edo6dmqrukxVvWWXHlsUg==} engines: {node: '>=4'} peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 @@ -4475,7 +4475,7 @@ packages: prop-types: 15.8.1 resolve: 2.0.0-next.3 semver: 6.3.0 - string.prototype.matchall: 4.0.6 + string.prototype.matchall: 4.0.7 dev: true /eslint-plugin-testing-library/5.5.0_vjep2yp2sits3sqnodefgcbnfi: @@ -7206,8 +7206,8 @@ packages: strip-ansi: 7.0.1 dev: true - /string.prototype.matchall/4.0.6: - resolution: {integrity: sha512-6WgDX8HmQqvEd7J+G6VtAahhsQIssiZ8zl7zKh1VDMFyL3hRTJP4FTNA3RbIp2TOQ9AYNDcc7e3fH0Qbup+DBg==} + /string.prototype.matchall/4.0.7: + resolution: {integrity: sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg==} dependencies: call-bind: 1.0.2 define-properties: 1.1.4 diff --git a/kafka-ui-react-app/src/components/Topics/List/ActionsCell/ActionsCell.tsx b/kafka-ui-react-app/src/components/Topics/List/ActionsCell/ActionsCell.tsx new file mode 100644 index 0000000000..617dc8f492 --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/List/ActionsCell/ActionsCell.tsx @@ -0,0 +1,131 @@ +import React from 'react'; +import { useDispatch } from 'react-redux'; +import { + CleanUpPolicy, + SortOrder, + TopicColumnsToSort, +} from 'generated-sources'; +import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal'; +import DropdownItem from 'components/common/Dropdown/DropdownItem'; +import { TableCellProps } from 'components/common/SmartTable/TableColumn'; +import { TopicWithDetailedInfo } from 'redux/interfaces'; +import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon'; +import Dropdown from 'components/common/Dropdown/Dropdown'; +import ClusterContext from 'components/contexts/ClusterContext'; +import * as S from 'components/Topics/List/List.styled'; +import { ClusterNameRoute } from 'lib/paths'; +import useModal from 'lib/hooks/useModal'; +import useAppParams from 'lib/hooks/useAppParams'; +import { + deleteTopic, + fetchTopicsList, + recreateTopic, +} from 'redux/reducers/topics/topicsSlice'; +import { clearTopicMessages } from 'redux/reducers/topicMessages/topicMessagesSlice'; + +interface TopicsListParams { + clusterName: string; + page?: number; + perPage?: number; + showInternal?: boolean; + search?: string; + orderBy?: TopicColumnsToSort; + sortOrder?: SortOrder; +} +export interface ActionsCellProps { + topicsListParams: TopicsListParams; +} + +const ActionsCell: React.FC< + TableCellProps & ActionsCellProps +> = ({ + hovered, + dataItem: { internal, cleanUpPolicy, name }, + topicsListParams, +}) => { + const { isReadOnly, isTopicDeletionAllowed } = + React.useContext(ClusterContext); + const dispatch = useDispatch(); + const { clusterName } = useAppParams(); + + const { + isOpen: isDeleteTopicModalOpen, + setClose: closeDeleteTopicModal, + setOpen: openDeleteTopicModal, + } = useModal(false); + + const { + isOpen: isRecreateTopicModalOpen, + setClose: closeRecreateTopicModal, + setOpen: openRecreateTopicModal, + } = useModal(false); + + const { + isOpen: isClearMessagesModalOpen, + setClose: closeClearMessagesModal, + setOpen: openClearMessagesModal, + } = useModal(false); + + const isHidden = internal || isReadOnly || !hovered; + + const deleteTopicHandler = () => + dispatch(deleteTopic({ clusterName, topicName: name })); + + const clearTopicMessagesHandler = () => { + dispatch(clearTopicMessages({ clusterName, topicName: name })); + dispatch(fetchTopicsList(topicsListParams)); + closeClearMessagesModal(); + }; + + const recreateTopicHandler = () => { + dispatch(recreateTopic({ clusterName, topicName: name })); + closeRecreateTopicModal(); + }; + + return ( + <> + + {!isHidden && ( + } right> + {cleanUpPolicy === CleanUpPolicy.DELETE && ( + + Clear Messages + + )} + {isTopicDeletionAllowed && ( + + Remove Topic + + )} + + Recreate Topic + + + )} + + + Are you sure want to clear topic messages? + + + Are you sure want to remove {name} topic? + + + Are you sure to recreate {name} topic? + + + ); +}; + +export default React.memo(ActionsCell); diff --git a/kafka-ui-react-app/src/components/Topics/List/List.tsx b/kafka-ui-react-app/src/components/Topics/List/List.tsx index b958b4f6b8..b83659d4d7 100644 --- a/kafka-ui-react-app/src/components/Topics/List/List.tsx +++ b/kafka-ui-react-app/src/components/Topics/List/List.tsx @@ -12,15 +12,11 @@ import { clusterTopicNewRelativePath, } from 'lib/paths'; import usePagination from 'lib/hooks/usePagination'; -import useModal from 'lib/hooks/useModal'; import ClusterContext from 'components/contexts/ClusterContext'; import PageLoader from 'components/common/PageLoader/PageLoader'; import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal'; import { - CleanUpPolicy, - DeleteTopicRequest, GetTopicsRequest, - RecreateTopicRequest, SortOrder, TopicColumnsToSort, } from 'generated-sources'; @@ -31,14 +27,8 @@ 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 { 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, @@ -47,18 +37,17 @@ import { TopicSizeCell, } from './TopicsTableCells'; import * as S from './List.styled'; +import ActionsCell from './ActionsCell/ActionsCell'; export interface TopicsListProps { areTopicsFetching: boolean; topics: TopicWithDetailedInfo[]; totalPages: number; fetchTopicsList(payload: GetTopicsRequest): void; - deleteTopic(payload: DeleteTopicRequest): void; deleteTopics(payload: { clusterName: ClusterName; topicNames: TopicName[]; }): void; - recreateTopic(payload: RecreateTopicRequest): void; clearTopicsMessages(payload: { clusterName: ClusterName; topicNames: TopicName[]; @@ -80,10 +69,7 @@ const List: React.FC = ({ topics, totalPages, fetchTopicsList, - deleteTopic, deleteTopics, - recreateTopic, - clearTopicMessages, clearTopicsMessages, search, orderBy, @@ -91,8 +77,7 @@ const List: React.FC = ({ setTopicsSearch, setTopicsOrderBy, }) => { - const { isReadOnly, isTopicDeletionAllowed } = - React.useContext(ClusterContext); + const { isReadOnly } = React.useContext(ClusterContext); const { clusterName } = useAppParams(); const { page, perPage } = usePagination(); const [showInternal, setShowInternal] = React.useState( @@ -197,89 +182,6 @@ const List: React.FC = ({ fetchTopicsList(topicsListParams); }; - const ActionsCell = React.memo>( - ({ hovered, dataItem: { internal, cleanUpPolicy, name } }) => { - const { - isOpen: isDeleteTopicModalOpen, - setClose: closeDeleteTopicModal, - setOpen: openDeleteTopicModal, - } = useModal(false); - - const { - isOpen: isRecreateTopicModalOpen, - setClose: closeRecreateTopicModal, - setOpen: openRecreateTopicModal, - } = useModal(false); - - const { - isOpen: isClearMessagesModalOpen, - setClose: closeClearMessagesModal, - setOpen: openClearMessagesModal, - } = useModal(false); - - const isHidden = internal || isReadOnly || !hovered; - - const deleteTopicHandler = () => - deleteTopic({ clusterName, topicName: name }); - - const clearTopicMessagesHandler = () => { - clearTopicMessages({ clusterName, topicName: name }); - fetchTopicsList(topicsListParams); - closeClearMessagesModal(); - }; - - const recreateTopicHandler = () => { - recreateTopic({ clusterName, topicName: name }); - closeRecreateTopicModal(); - }; - - return ( - <> - - {!isHidden && ( - } right> - {cleanUpPolicy === CleanUpPolicy.DELETE && ( - - Clear Messages - - )} - {isTopicDeletionAllowed && ( - - Remove Topic - - )} - - Recreate Topic - - - )} - - - Are you sure want to clear topic messages? - - - Are you sure want to remove {name} topic? - - - Are you sure to recreate {name} topic? - - - ); - } - ); - return (
diff --git a/kafka-ui-react-app/src/components/Topics/List/ListContainer.ts b/kafka-ui-react-app/src/components/Topics/List/ListContainer.ts index 7670f4e868..75b06c2b43 100644 --- a/kafka-ui-react-app/src/components/Topics/List/ListContainer.ts +++ b/kafka-ui-react-app/src/components/Topics/List/ListContainer.ts @@ -3,8 +3,6 @@ import { RootState } from 'redux/interfaces'; import { clearTopicMessages } from 'redux/reducers/topicMessages/topicMessagesSlice'; import { fetchTopicsList, - deleteTopic, - recreateTopic, setTopicsSearch, setTopicsOrderBy, deleteTopics, @@ -32,9 +30,7 @@ const mapStateToProps = (state: RootState) => ({ const mapDispatchToProps = { fetchTopicsList, - deleteTopic, deleteTopics, - recreateTopic, clearTopicsMessages, clearTopicMessages, setTopicsSearch, diff --git a/kafka-ui-react-app/src/components/Topics/List/__tests__/List.spec.tsx b/kafka-ui-react-app/src/components/Topics/List/__tests__/List.spec.tsx index d85a55f209..2d756fc8b7 100644 --- a/kafka-ui-react-app/src/components/Topics/List/__tests__/List.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/List/__tests__/List.spec.tsx @@ -27,11 +27,9 @@ describe('List', () => { topics={[]} totalPages={1} fetchTopicsList={jest.fn()} - deleteTopic={jest.fn()} deleteTopics={jest.fn()} clearTopicsMessages={jest.fn()} clearTopicMessages={jest.fn()} - recreateTopic={jest.fn()} search="" orderBy={null} sortOrder={SortOrder.ASC} @@ -174,10 +172,8 @@ describe('List', () => { describe('when some list items are selected', () => { const mockDeleteTopics = jest.fn(); - const mockDeleteTopic = jest.fn(); const mockClearTopic = jest.fn(); const mockClearTopicsMessages = jest.fn(); - const mockRecreate = jest.fn(); const fetchTopicsList = jest.fn(); jest.useFakeTimers(); @@ -204,8 +200,6 @@ describe('List', () => { ], deleteTopics: mockDeleteTopics, clearTopicsMessages: mockClearTopicsMessages, - recreateTopic: mockRecreate, - deleteTopic: mockDeleteTopic, clearTopicMessages: mockClearTopic, fetchTopicsList, })} @@ -218,8 +212,6 @@ describe('List', () => { afterEach(() => { mockDeleteTopics.mockClear(); mockClearTopicsMessages.mockClear(); - mockRecreate.mockClear(); - mockDeleteTopic.mockClear(); }); const getCheckboxInput = (at: number) => { @@ -335,63 +327,5 @@ describe('List', () => { expect(mockDeleteTopics).not.toHaveBeenCalled(); }); - - const tableRowActionClickAndCheck = async ( - action: 'deleteTopics' | 'clearTopicsMessages' | 'recreate' - ) => { - const row = screen.getAllByRole('row')[1]; - userEvent.hover(row); - const actionBtn = within(row).getByRole('menu', { hidden: true }); - - userEvent.click(actionBtn); - - let textBtn; - let mock: jest.Mock; - - if (action === 'clearTopicsMessages') { - textBtn = 'Clear Messages'; - mock = mockClearTopic; - } else if (action === 'deleteTopics') { - textBtn = 'Remove Topic'; - mock = mockDeleteTopic; - } else { - textBtn = 'Recreate Topic'; - mock = mockRecreate; - } - - const ourAction = screen.getByText(textBtn); - - userEvent.click(ourAction); - - let dialog = screen.getByRole('dialog'); - expect(dialog).toBeInTheDocument(); - userEvent.click(within(dialog).getByRole('button', { name: 'Submit' })); - - await waitFor(() => { - expect(mock).toHaveBeenCalled(); - if (action === 'clearTopicsMessages') { - expect(fetchTopicsList).toHaveBeenCalled(); - } - }); - - userEvent.click(ourAction); - dialog = screen.getByRole('dialog'); - expect(dialog).toBeInTheDocument(); - userEvent.click(within(dialog).getByRole('button', { name: 'Cancel' })); - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); - expect(mock).toHaveBeenCalledTimes(1); - }; - - it('should test the actions of the row and their modal and fetching for removing', async () => { - await tableRowActionClickAndCheck('deleteTopics'); - }); - - it('should test the actions of the row and their modal and fetching for clear', async () => { - await tableRowActionClickAndCheck('clearTopicsMessages'); - }); - - it('should test the actions of the row and their modal and fetching for recreate', async () => { - await tableRowActionClickAndCheck('recreate'); - }); }); }); diff --git a/kafka-ui-react-app/src/components/common/SmartTable/TableColumn.tsx b/kafka-ui-react-app/src/components/common/SmartTable/TableColumn.tsx index 8044999ee2..b3fa869582 100644 --- a/kafka-ui-react-app/src/components/common/SmartTable/TableColumn.tsx +++ b/kafka-ui-react-app/src/components/common/SmartTable/TableColumn.tsx @@ -3,6 +3,7 @@ import { TableState } from 'lib/hooks/useTableState'; import { SortOrder } from 'generated-sources'; import * as S from 'components/common/table/TableHeaderCell/TableHeaderCell.styled'; import { DefaultTheme, StyledComponent } from 'styled-components'; +import { ActionsCellProps } from 'components/Topics/List/ActionsCell/ActionsCell'; export interface OrderableProps { orderBy: string | null; @@ -28,7 +29,7 @@ export interface TableCellProps } interface TableColumnProps { - cell?: React.FC>; + cell?: React.FC & ActionsCellProps>; children?: React.ReactElement; headerCell?: React.FC>; field?: string;