diff --git a/kafka-ui-react-app/package-lock.json b/kafka-ui-react-app/package-lock.json index afff9bf523..dccf512e7c 100644 --- a/kafka-ui-react-app/package-lock.json +++ b/kafka-ui-react-app/package-lock.json @@ -36,7 +36,7 @@ "react-multi-select-component": "^4.0.6", "react-redux": "^7.2.6", "react-router": "^5.2.0", - "react-router-dom": "^5.3.1", + "react-router-dom": "^6.3.0", "redux": "^4.1.1", "redux-thunk": "^2.3.0", "sass": "^1.43.4", @@ -16158,7 +16158,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", - "dev": true, "dependencies": { "@babel/runtime": "^7.7.6" } @@ -24856,11 +24855,11 @@ } }, "node_modules/react-router": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz", - "integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.3.tgz", + "integrity": "sha512-mzQGUvS3bM84TnbtMYR8ZjKnuPJ71IjSzR+DE6UkUqvN4czWIqEs17yLL8xkAycv4ev0AiN+IGrWu88vJs/p2w==", "dependencies": { - "@babel/runtime": "^7.1.2", + "@babel/runtime": "^7.12.13", "history": "^4.9.0", "hoist-non-react-statics": "^3.1.0", "loose-envify": "^1.3.1", @@ -24876,53 +24875,27 @@ } }, "node_modules/react-router-dom": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.1.tgz", - "integrity": "sha512-f0pj/gMAbv9e8gahTmCEY20oFhxhrmHwYeIwH5EO5xu0qme+wXtsdB8YfUOAZzUz4VaXmb58m3ceiLtjMhqYmQ==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz", + "integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==", "dependencies": { - "@babel/runtime": "^7.12.13", - "history": "^4.9.0", - "loose-envify": "^1.3.1", - "prop-types": "^15.6.2", - "react-router": "5.3.1", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0" + "history": "^5.2.0", + "react-router": "6.3.0" }, "peerDependencies": { - "react": ">=15" - } - }, - "node_modules/react-router-dom/node_modules/history": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", - "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", - "dependencies": { - "@babel/runtime": "^7.1.2", - "loose-envify": "^1.2.0", - "resolve-pathname": "^3.0.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0", - "value-equal": "^1.0.1" + "react": ">=16.8", + "react-dom": ">=16.8" } }, "node_modules/react-router-dom/node_modules/react-router": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.1.tgz", - "integrity": "sha512-v+zwjqb7bakqgF+wMVKlAPTca/cEmPOvQ9zt7gpSNyPXau1+0qvuYZ5BWzzNDP1y6s15zDwgb9rPN63+SIniRQ==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz", + "integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==", "dependencies": { - "@babel/runtime": "^7.12.13", - "history": "^4.9.0", - "hoist-non-react-statics": "^3.1.0", - "loose-envify": "^1.3.1", - "mini-create-react-context": "^0.4.0", - "path-to-regexp": "^1.7.0", - "prop-types": "^15.6.2", - "react-is": "^16.6.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0" + "history": "^5.2.0" }, "peerDependencies": { - "react": ">=15" + "react": ">=16.8" } }, "node_modules/react-router/node_modules/history": { @@ -40798,7 +40771,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", - "dev": true, "requires": { "@babel/runtime": "^7.7.6" } @@ -47310,11 +47282,11 @@ "dev": true }, "react-router": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz", - "integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.3.tgz", + "integrity": "sha512-mzQGUvS3bM84TnbtMYR8ZjKnuPJ71IjSzR+DE6UkUqvN4czWIqEs17yLL8xkAycv4ev0AiN+IGrWu88vJs/p2w==", "requires": { - "@babel/runtime": "^7.1.2", + "@babel/runtime": "^7.12.13", "history": "^4.9.0", "hoist-non-react-statics": "^3.1.0", "loose-envify": "^1.3.1", @@ -47342,47 +47314,20 @@ } }, "react-router-dom": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.1.tgz", - "integrity": "sha512-f0pj/gMAbv9e8gahTmCEY20oFhxhrmHwYeIwH5EO5xu0qme+wXtsdB8YfUOAZzUz4VaXmb58m3ceiLtjMhqYmQ==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz", + "integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==", "requires": { - "@babel/runtime": "^7.12.13", - "history": "^4.9.0", - "loose-envify": "^1.3.1", - "prop-types": "^15.6.2", - "react-router": "5.3.1", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0" + "history": "^5.2.0", + "react-router": "6.3.0" }, "dependencies": { - "history": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", - "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", - "requires": { - "@babel/runtime": "^7.1.2", - "loose-envify": "^1.2.0", - "resolve-pathname": "^3.0.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0", - "value-equal": "^1.0.1" - } - }, "react-router": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.1.tgz", - "integrity": "sha512-v+zwjqb7bakqgF+wMVKlAPTca/cEmPOvQ9zt7gpSNyPXau1+0qvuYZ5BWzzNDP1y6s15zDwgb9rPN63+SIniRQ==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz", + "integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==", "requires": { - "@babel/runtime": "^7.12.13", - "history": "^4.9.0", - "hoist-non-react-statics": "^3.1.0", - "loose-envify": "^1.3.1", - "mini-create-react-context": "^0.4.0", - "path-to-regexp": "^1.7.0", - "prop-types": "^15.6.2", - "react-is": "^16.6.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0" + "history": "^5.2.0" } } } diff --git a/kafka-ui-react-app/package.json b/kafka-ui-react-app/package.json index 9c7c649032..7f01e4a5e0 100644 --- a/kafka-ui-react-app/package.json +++ b/kafka-ui-react-app/package.json @@ -32,7 +32,7 @@ "react-multi-select-component": "^4.0.6", "react-redux": "^7.2.6", "react-router": "^5.2.0", - "react-router-dom": "^5.3.1", + "react-router-dom": "^6.3.0", "redux": "^4.1.1", "redux-thunk": "^2.3.0", "sass": "^1.43.4", diff --git a/kafka-ui-react-app/src/components/App.tsx b/kafka-ui-react-app/src/components/App.tsx index 8e9f40f816..5f4824f125 100644 --- a/kafka-ui-react-app/src/components/App.tsx +++ b/kafka-ui-react-app/src/components/App.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import { Switch, Route, useLocation } from 'react-router-dom'; +import { Routes, Route, useLocation } from 'react-router-dom'; import { GIT_TAG, GIT_COMMIT } from 'lib/constants'; +import { clusterPath, getNonExactPath } from 'lib/paths'; import Nav from 'components/Nav/Nav'; import PageLoader from 'components/common/PageLoader/PageLoader'; import Dashboard from 'components/Dashboard/Dashboard'; @@ -81,14 +82,19 @@ const App: React.FC = () => { aria-label="Overlay" /> {areClustersFulfilled ? ( - + + {['/', '/ui', '/ui/clusters'].map((path) => ( + } + /> + ))} } /> - - + ) : ( )} diff --git a/kafka-ui-react-app/src/components/Brokers/Brokers.tsx b/kafka-ui-react-app/src/components/Brokers/Brokers.tsx index 75e3d3be6a..80ced9baed 100644 --- a/kafka-ui-react-app/src/components/Brokers/Brokers.tsx +++ b/kafka-ui-react-app/src/components/Brokers/Brokers.tsx @@ -1,22 +1,22 @@ import React from 'react'; -import { ClusterName } from 'redux/interfaces'; import useInterval from 'lib/hooks/useInterval'; import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted'; -import { useParams } from 'react-router-dom'; import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell'; import { Table } from 'components/common/table/Table/Table.styled'; import PageHeading from 'components/common/PageHeading/PageHeading'; import * as Metrics from 'components/common/Metrics'; import { useAppDispatch, useAppSelector } from 'lib/hooks/redux'; +import { ClusterNameRoute } from 'lib/paths'; import { fetchBrokers, fetchClusterStats, selectStats, } from 'redux/reducers/brokers/brokersSlice'; +import useAppParams from 'lib/hooks/useAppParams'; const Brokers: React.FC = () => { const dispatch = useAppDispatch(); - const { clusterName } = useParams<{ clusterName: ClusterName }>(); + const { clusterName } = useAppParams(); const { brokerCount, activeControllers, diff --git a/kafka-ui-react-app/src/components/Brokers/__test__/Brokers.spec.tsx b/kafka-ui-react-app/src/components/Brokers/__test__/Brokers.spec.tsx index e8344246ff..8e5d015790 100644 --- a/kafka-ui-react-app/src/components/Brokers/__test__/Brokers.spec.tsx +++ b/kafka-ui-react-app/src/components/Brokers/__test__/Brokers.spec.tsx @@ -1,8 +1,7 @@ import React from 'react'; import Brokers from 'components/Brokers/Brokers'; -import { render } from 'lib/testHelpers'; +import { render, WithRoute } from 'lib/testHelpers'; import { screen, waitFor } from '@testing-library/dom'; -import { Route } from 'react-router-dom'; import { clusterBrokersPath } from 'lib/paths'; import fetchMock from 'fetch-mock'; import { clusterStatsPayload } from 'redux/reducers/brokers/__test__/fixtures'; @@ -18,11 +17,11 @@ describe('Brokers Component', () => { const renderComponent = () => render( - + - , + , { - pathname: clusterBrokersPath(clusterName), + initialEntries: [clusterBrokersPath(clusterName)], } ); diff --git a/kafka-ui-react-app/src/components/Cluster/Cluster.tsx b/kafka-ui-react-app/src/components/Cluster/Cluster.tsx index 54538de4a7..e60346ceae 100644 --- a/kafka-ui-react-app/src/components/Cluster/Cluster.tsx +++ b/kafka-ui-react-app/src/components/Cluster/Cluster.tsx @@ -1,19 +1,22 @@ import React from 'react'; import { useSelector } from 'react-redux'; -import { Switch, Redirect, useParams } from 'react-router-dom'; +import { Routes, Navigate, Route, Outlet } from 'react-router-dom'; +import useAppParams from 'lib/hooks/useAppParams'; import { ClusterFeaturesEnum } from 'generated-sources'; import { getClustersFeatures, getClustersReadonlyStatus, } from 'redux/reducers/clusters/clustersSlice'; import { - clusterBrokersPath, - clusterConnectorsPath, - clusterConnectsPath, - clusterConsumerGroupsPath, - clusterKsqlDbPath, - clusterSchemasPath, - clusterTopicsPath, + clusterBrokerRelativePath, + clusterConnectorsRelativePath, + clusterConnectsRelativePath, + clusterConsumerGroupsRelativePath, + clusterKsqlDbRelativePath, + ClusterNameRoute, + clusterSchemasRelativePath, + clusterTopicsRelativePath, + getNonExactPath, } from 'lib/paths'; import Topics from 'components/Topics/Topics'; import Schemas from 'components/Schemas/Schemas'; @@ -27,7 +30,7 @@ import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route'; import { BreadcrumbProvider } from 'components/common/Breadcrumb/Breadcrumb.provider'; const Cluster: React.FC = () => { - const { clusterName } = useParams<{ clusterName: string }>(); + const { clusterName } = useAppParams(); const isReadOnly = useSelector(getClustersReadonlyStatus(clusterName)); const features = useSelector(getClustersFeatures(clusterName)); @@ -61,48 +64,77 @@ const Cluster: React.FC = () => { - - + + + + } /> - + + + } /> - + + + } /> {hasSchemaRegistryConfigured && ( - + + + } /> )} {hasKafkaConnectConfigured && ( - + + + } /> )} {hasKafkaConnectConfigured && ( - + + + } /> )} {hasKsqlDbConfigured && ( - + + + } /> )} - } /> - + + ); diff --git a/kafka-ui-react-app/src/components/Cluster/__tests__/Cluster.spec.tsx b/kafka-ui-react-app/src/components/Cluster/__tests__/Cluster.spec.tsx index cb26131eef..2ef3a0de95 100644 --- a/kafka-ui-react-app/src/components/Cluster/__tests__/Cluster.spec.tsx +++ b/kafka-ui-react-app/src/components/Cluster/__tests__/Cluster.spec.tsx @@ -1,51 +1,71 @@ import React from 'react'; -import { Route } from 'react-router-dom'; import { ClusterFeaturesEnum } from 'generated-sources'; import { store } from 'redux/store'; import { onlineClusterPayload } from 'redux/reducers/clusters/__test__/fixtures'; import Cluster from 'components/Cluster/Cluster'; import { fetchClusters } from 'redux/reducers/clusters/clustersSlice'; import { screen } from '@testing-library/react'; -import { render } from 'lib/testHelpers'; +import { render, WithRoute } from 'lib/testHelpers'; import { clusterBrokersPath, clusterConnectsPath, clusterConsumerGroupsPath, clusterKsqlDbPath, + clusterPath, clusterSchemasPath, clusterTopicsPath, } from 'lib/paths'; -jest.mock('components/Topics/Topics', () => () =>
Topics
); -jest.mock('components/Schemas/Schemas', () => () =>
Schemas
); -jest.mock('components/Connect/Connect', () => () =>
Connect
); -jest.mock('components/Connect/Connect', () => () =>
Connect
); -jest.mock('components/Brokers/Brokers', () => () =>
Brokers
); -jest.mock('components/ConsumerGroups/ConsumerGroups', () => () => ( -
ConsumerGroups
+const CLusterCompText = { + Topics: 'Topics', + Schemas: 'Schemas', + Connect: 'Connect', + Brokers: 'Brokers', + ConsumerGroups: 'ConsumerGroups', + KsqlDb: 'KsqlDb', +}; + +jest.mock('components/Topics/Topics', () => () => ( +
{CLusterCompText.Topics}
+)); +jest.mock('components/Schemas/Schemas', () => () => ( +
{CLusterCompText.Schemas}
+)); +jest.mock('components/Connect/Connect', () => () => ( +
{CLusterCompText.Connect}
+)); +jest.mock('components/Brokers/Brokers', () => () => ( +
{CLusterCompText.Brokers}
+)); +jest.mock('components/ConsumerGroups/ConsumerGroups', () => () => ( +
{CLusterCompText.ConsumerGroups}
+)); +jest.mock('components/KsqlDb/KsqlDb', () => () => ( +
{CLusterCompText.KsqlDb}
)); -jest.mock('components/KsqlDb/KsqlDb', () => () =>
KsqlDb
); describe('Cluster', () => { const renderComponent = (pathname: string) => render( - + - , - { pathname, store } + , + { initialEntries: [pathname], store } ); it('renders Brokers', () => { renderComponent(clusterBrokersPath('second')); - expect(screen.getByText('Brokers')).toBeInTheDocument(); + expect(screen.getByText(CLusterCompText.Brokers)).toBeInTheDocument(); }); it('renders Topics', () => { renderComponent(clusterTopicsPath('second')); - expect(screen.getByText('Topics')).toBeInTheDocument(); + expect(screen.getByText(CLusterCompText.Topics)).toBeInTheDocument(); }); it('renders ConsumerGroups', () => { renderComponent(clusterConsumerGroupsPath('second')); - expect(screen.getByText('ConsumerGroups')).toBeInTheDocument(); + expect( + screen.getByText(CLusterCompText.ConsumerGroups) + ).toBeInTheDocument(); }); describe('configured features', () => { @@ -62,7 +82,9 @@ describe('Cluster', () => { ) ); renderComponent(clusterSchemasPath('second')); - expect(screen.queryByText('Schemas')).not.toBeInTheDocument(); + expect( + screen.queryByText(CLusterCompText.Schemas) + ).not.toBeInTheDocument(); }); it('renders Schemas if SCHEMA_REGISTRY is configured', async () => { store.dispatch( @@ -77,7 +99,7 @@ describe('Cluster', () => { ) ); renderComponent(clusterSchemasPath(onlineClusterPayload.name)); - expect(screen.getByText('Schemas')).toBeInTheDocument(); + expect(screen.getByText(CLusterCompText.Schemas)).toBeInTheDocument(); }); it('renders Connect if KAFKA_CONNECT is configured', async () => { store.dispatch( @@ -92,7 +114,7 @@ describe('Cluster', () => { ) ); renderComponent(clusterConnectsPath(onlineClusterPayload.name)); - expect(screen.getByText('Connect')).toBeInTheDocument(); + expect(screen.getByText(CLusterCompText.Connect)).toBeInTheDocument(); }); it('renders KSQL if KSQL_DB is configured', async () => { store.dispatch( @@ -107,7 +129,7 @@ describe('Cluster', () => { ) ); renderComponent(clusterKsqlDbPath(onlineClusterPayload.name)); - expect(screen.getByText('KsqlDb')).toBeInTheDocument(); + expect(screen.getByText(CLusterCompText.KsqlDb)).toBeInTheDocument(); }); }); }); diff --git a/kafka-ui-react-app/src/components/Connect/Connect.tsx b/kafka-ui-react-app/src/components/Connect/Connect.tsx index a3106dbbc2..f1bb7ef406 100644 --- a/kafka-ui-react-app/src/components/Connect/Connect.tsx +++ b/kafka-ui-react-app/src/components/Connect/Connect.tsx @@ -1,12 +1,12 @@ import React from 'react'; -import { Switch, Redirect } from 'react-router-dom'; +import { Navigate, Routes, Route } from 'react-router-dom'; import { - clusterConnectorsPath, - clusterConnectsPath, - clusterConnectorNewPath, - clusterConnectConnectorPath, - clusterConnectConnectorEditPath, - clusterConnectConnectorsPath, + RouteParams, + clusterConnectConnectorEditRelativePath, + clusterConnectConnectorRelativePath, + clusterConnectConnectorsRelativePath, + clusterConnectorNewRelativePath, + getNonExactPath, } from 'lib/paths'; import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route'; @@ -16,45 +16,48 @@ import DetailsContainer from './Details/DetailsContainer'; import EditContainer from './Edit/EditContainer'; const Connect: React.FC = () => ( -
- - - - - - - - -
+ + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + } + /> + } + /> + ); export default Connect; diff --git a/kafka-ui-react-app/src/components/Connect/Details/Actions/Actions.tsx b/kafka-ui-react-app/src/components/Connect/Details/Actions/Actions.tsx index dd068e464c..db26a27dee 100644 --- a/kafka-ui-react-app/src/components/Connect/Details/Actions/Actions.tsx +++ b/kafka-ui-react-app/src/components/Connect/Details/Actions/Actions.tsx @@ -1,21 +1,17 @@ import React from 'react'; -import { useHistory, useParams } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; +import useAppParams from 'lib/hooks/useAppParams'; import { ConnectorState, ConnectorAction } from 'generated-sources'; import { ClusterName, ConnectName, ConnectorName } from 'redux/interfaces'; import { clusterConnectConnectorEditPath, clusterConnectorsPath, + RouterParamsClusterConnectConnector, } from 'lib/paths'; import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal'; import styled from 'styled-components'; import { Button } from 'components/common/Button/Button'; -interface RouterParams { - clusterName: ClusterName; - connectName: ConnectName; - connectorName: ConnectorName; -} - const ConnectorActionsWrapperStyled = styled.div` display: flex; gap: 8px; @@ -63,9 +59,11 @@ const Actions: React.FC = ({ resumeConnector, isConnectorActionRunning, }) => { - const { clusterName, connectName, connectorName } = useParams(); + const { clusterName, connectName, connectorName } = + useAppParams(); + + const navigate = useNavigate(); - const history = useHistory(); const [ isDeleteConnectorConfirmationVisible, setIsDeleteConnectorConfirmationVisible, @@ -74,7 +72,7 @@ const Actions: React.FC = ({ const deleteConnectorHandler = async () => { try { await deleteConnector({ clusterName, connectName, connectorName }); - history.push(clusterConnectorsPath(clusterName)); + navigate(clusterConnectorsPath(clusterName)); } catch { // do not redirect } @@ -175,7 +173,6 @@ const Actions: React.FC = ({ buttonSize="M" buttonType="primary" type="button" - isLink disabled={isConnectorActionRunning} to={clusterConnectConnectorEditPath( clusterName, diff --git a/kafka-ui-react-app/src/components/Connect/Details/Actions/ActionsContainer.ts b/kafka-ui-react-app/src/components/Connect/Details/Actions/ActionsContainer.ts index f6ffb299d8..e0a78343ea 100644 --- a/kafka-ui-react-app/src/components/Connect/Details/Actions/ActionsContainer.ts +++ b/kafka-ui-react-app/src/components/Connect/Details/Actions/ActionsContainer.ts @@ -1,5 +1,4 @@ import { connect } from 'react-redux'; -import { withRouter } from 'react-router-dom'; import { RootState } from 'redux/interfaces'; import { deleteConnector, @@ -30,6 +29,4 @@ const mapDispatchToProps = { resumeConnector, }; -export default withRouter( - connect(mapStateToProps, mapDispatchToProps)(Actions) -); +export default connect(mapStateToProps, mapDispatchToProps)(Actions); diff --git a/kafka-ui-react-app/src/components/Connect/Details/Actions/__tests__/Actions.spec.tsx b/kafka-ui-react-app/src/components/Connect/Details/Actions/__tests__/Actions.spec.tsx index 4b998f420c..d11cabb40a 100644 --- a/kafka-ui-react-app/src/components/Connect/Details/Actions/__tests__/Actions.spec.tsx +++ b/kafka-ui-react-app/src/components/Connect/Details/Actions/__tests__/Actions.spec.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { Route } from 'react-router-dom'; -import { render } from 'lib/testHelpers'; +import { render, WithRoute } from 'lib/testHelpers'; import { clusterConnectConnectorPath, clusterConnectorsPath } from 'lib/paths'; import ActionsContainer from 'components/Connect/Details/Actions/ActionsContainer'; import Actions, { @@ -19,9 +18,7 @@ const cancelMock = jest.fn(); jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), - useHistory: () => ({ - push: mockHistoryPush, - }), + useNavigate: () => mockHistoryPush, })); jest.mock( @@ -38,6 +35,12 @@ const expectActionButtonsExists = () => { }; describe('Actions', () => { + afterEach(() => { + mockHistoryPush.mockClear(); + deleteConnector.mockClear(); + cancelMock.mockClear(); + }); + const actionsContainer = (props: Partial = {}) => ( { }); describe('view', () => { - const pathname = clusterConnectConnectorPath( - ':clusterName', - ':connectName', - ':connectorName' - ); + const pathname = clusterConnectConnectorPath(); const clusterName = 'my-cluster'; const connectName = 'my-connect'; const connectorName = 'my-connector'; const confirmationModal = (props: Partial = {}) => ( - + @@ -91,11 +90,11 @@ describe('Actions', () => { Confirm - + ); const component = (props: Partial = {}) => ( - + { isConnectorActionRunning={false} {...props} /> - + ); it('renders buttons when paused', () => { render(component({ connectorStatus: ConnectorState.PAUSED }), { - pathname: clusterConnectConnectorPath( - clusterName, - connectName, - connectorName - ), + initialEntries: [ + clusterConnectConnectorPath(clusterName, connectName, connectorName), + ], }); expect(screen.getAllByRole('button').length).toEqual(6); expect(screen.getByText('Resume')).toBeInTheDocument(); @@ -127,11 +124,9 @@ describe('Actions', () => { it('renders buttons when failed', () => { render(component({ connectorStatus: ConnectorState.FAILED }), { - pathname: clusterConnectConnectorPath( - clusterName, - connectName, - connectorName - ), + initialEntries: [ + clusterConnectConnectorPath(clusterName, connectName, connectorName), + ], }); expect(screen.getAllByRole('button').length).toEqual(5); @@ -143,11 +138,9 @@ describe('Actions', () => { it('renders buttons when unassigned', () => { render(component({ connectorStatus: ConnectorState.UNASSIGNED }), { - pathname: clusterConnectConnectorPath( - clusterName, - connectName, - connectorName - ), + initialEntries: [ + clusterConnectConnectorPath(clusterName, connectName, connectorName), + ], }); expect(screen.getAllByRole('button').length).toEqual(5); expect(screen.queryByText('Resume')).not.toBeInTheDocument(); @@ -157,11 +150,9 @@ describe('Actions', () => { it('renders buttons when running connector action', () => { render(component({ connectorStatus: ConnectorState.RUNNING }), { - pathname: clusterConnectConnectorPath( - clusterName, - connectName, - connectorName - ), + initialEntries: [ + clusterConnectConnectorPath(clusterName, connectName, connectorName), + ], }); expect(screen.getAllByRole('button').length).toEqual(6); expect(screen.queryByText('Resume')).not.toBeInTheDocument(); @@ -172,11 +163,9 @@ describe('Actions', () => { it('opens confirmation modal when delete button clicked', () => { render(component({ deleteConnector }), { - pathname: clusterConnectConnectorPath( - clusterName, - connectName, - connectorName - ), + initialEntries: [ + clusterConnectConnectorPath(clusterName, connectName, connectorName), + ], }); userEvent.click(screen.getByRole('button', { name: 'Delete' })); @@ -187,11 +176,9 @@ describe('Actions', () => { it('closes when cancel button clicked', () => { render(confirmationModal({ isOpen: true }), { - pathname: clusterConnectConnectorPath( - clusterName, - connectName, - connectorName - ), + initialEntries: [ + clusterConnectConnectorPath(clusterName, connectName, connectorName), + ], }); const cancelBtn = screen.getByRole('button', { name: 'Cancel' }); userEvent.click(cancelBtn); @@ -200,11 +187,9 @@ describe('Actions', () => { it('calls deleteConnector when confirm button clicked', () => { render(confirmationModal({ isOpen: true }), { - pathname: clusterConnectConnectorPath( - clusterName, - connectName, - connectorName - ), + initialEntries: [ + clusterConnectConnectorPath(clusterName, connectName, connectorName), + ], }); const confirmBtn = screen.getByRole('button', { name: 'Confirm' }); userEvent.click(confirmBtn); @@ -218,11 +203,9 @@ describe('Actions', () => { it('redirects after delete', async () => { render(confirmationModal({ isOpen: true }), { - pathname: clusterConnectConnectorPath( - clusterName, - connectName, - connectorName - ), + initialEntries: [ + clusterConnectConnectorPath(clusterName, connectName, connectorName), + ], }); const confirmBtn = screen.getByRole('button', { name: 'Confirm' }); userEvent.click(confirmBtn); @@ -235,11 +218,9 @@ describe('Actions', () => { it('calls restartConnector when restart button clicked', () => { const restartConnector = jest.fn(); render(component({ restartConnector }), { - pathname: clusterConnectConnectorPath( - clusterName, - connectName, - connectorName - ), + initialEntries: [ + clusterConnectConnectorPath(clusterName, connectName, connectorName), + ], }); userEvent.click( screen.getByRole('button', { name: 'Restart Connector' }) @@ -260,11 +241,13 @@ describe('Actions', () => { pauseConnector, }), { - pathname: clusterConnectConnectorPath( - clusterName, - connectName, - connectorName - ), + initialEntries: [ + clusterConnectConnectorPath( + clusterName, + connectName, + connectorName + ), + ], } ); userEvent.click(screen.getByRole('button', { name: 'Pause' })); @@ -284,11 +267,13 @@ describe('Actions', () => { resumeConnector, }), { - pathname: clusterConnectConnectorPath( - clusterName, - connectName, - connectorName - ), + initialEntries: [ + clusterConnectConnectorPath( + clusterName, + connectName, + connectorName + ), + ], } ); userEvent.click(screen.getByRole('button', { name: 'Resume' })); diff --git a/kafka-ui-react-app/src/components/Connect/Details/Config/Config.tsx b/kafka-ui-react-app/src/components/Connect/Details/Config/Config.tsx index c2d57e85bb..7c7d4a086e 100644 --- a/kafka-ui-react-app/src/components/Connect/Details/Config/Config.tsx +++ b/kafka-ui-react-app/src/components/Connect/Details/Config/Config.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { useParams } from 'react-router-dom'; +import useAppParams from 'lib/hooks/useAppParams'; import { ClusterName, ConnectName, @@ -9,12 +9,7 @@ import { import PageLoader from 'components/common/PageLoader/PageLoader'; import Editor from 'components/common/Editor/Editor'; import styled from 'styled-components'; - -interface RouterParams { - clusterName: ClusterName; - connectName: ConnectName; - connectorName: ConnectorName; -} +import { RouterParamsClusterConnectConnector } from 'lib/paths'; export interface ConfigProps { fetchConfig(payload: { @@ -35,7 +30,8 @@ const Config: React.FC = ({ isConfigFetching, config, }) => { - const { clusterName, connectName, connectorName } = useParams(); + const { clusterName, connectName, connectorName } = + useAppParams(); React.useEffect(() => { fetchConfig({ clusterName, connectName, connectorName }); diff --git a/kafka-ui-react-app/src/components/Connect/Details/Config/ConfigContainer.ts b/kafka-ui-react-app/src/components/Connect/Details/Config/ConfigContainer.ts index c2caa6d89a..10a28149c7 100644 --- a/kafka-ui-react-app/src/components/Connect/Details/Config/ConfigContainer.ts +++ b/kafka-ui-react-app/src/components/Connect/Details/Config/ConfigContainer.ts @@ -1,5 +1,4 @@ import { connect } from 'react-redux'; -import { withRouter } from 'react-router-dom'; import { RootState } from 'redux/interfaces'; import { fetchConnectorConfig } from 'redux/reducers/connect/connectSlice'; import { @@ -18,4 +17,4 @@ const mapDispatchToProps = { fetchConfig: fetchConnectorConfig, }; -export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Config)); +export default connect(mapStateToProps, mapDispatchToProps)(Config); diff --git a/kafka-ui-react-app/src/components/Connect/Details/Config/__test__/Config.spec.tsx b/kafka-ui-react-app/src/components/Connect/Details/Config/__test__/Config.spec.tsx index d12183338f..6f8fa90951 100644 --- a/kafka-ui-react-app/src/components/Connect/Details/Config/__test__/Config.spec.tsx +++ b/kafka-ui-react-app/src/components/Connect/Details/Config/__test__/Config.spec.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { render } from 'lib/testHelpers'; -import { Route } from 'react-router-dom'; +import { render, WithRoute } from 'lib/testHelpers'; import { clusterConnectConnectorConfigPath } from 'lib/paths'; import Config, { ConfigProps } from 'components/Connect/Details/Config/Config'; import { connector } from 'redux/reducers/connect/__test__/fixtures'; @@ -9,44 +8,44 @@ import { screen } from '@testing-library/dom'; jest.mock('components/common/Editor/Editor', () => 'mock-Editor'); describe('Config', () => { - const pathname = clusterConnectConnectorConfigPath( - ':clusterName', - ':connectName', - ':connectorName' - ); + const pathname = clusterConnectConnectorConfigPath(); const clusterName = 'my-cluster'; const connectName = 'my-connect'; const connectorName = 'my-connector'; const component = (props: Partial = {}) => ( - + - + ); it('to be in the document when fetching config', () => { render(component({ isConfigFetching: true }), { - pathname: clusterConnectConnectorConfigPath( - clusterName, - connectName, - connectorName - ), + initialEntries: [ + clusterConnectConnectorConfigPath( + clusterName, + connectName, + connectorName + ), + ], }); expect(screen.getByRole('progressbar')).toBeInTheDocument(); }); it('is empty when no config', () => { const { container } = render(component({ config: null }), { - pathname: clusterConnectConnectorConfigPath( - clusterName, - connectName, - connectorName - ), + initialEntries: [ + clusterConnectConnectorConfigPath( + clusterName, + connectName, + connectorName + ), + ], }); expect(container).toBeEmptyDOMElement(); }); @@ -54,11 +53,13 @@ describe('Config', () => { it('fetches config on mount', () => { const fetchConfig = jest.fn(); render(component({ fetchConfig }), { - pathname: clusterConnectConnectorConfigPath( - clusterName, - connectName, - connectorName - ), + initialEntries: [ + clusterConnectConnectorConfigPath( + clusterName, + connectName, + connectorName + ), + ], }); expect(fetchConfig).toHaveBeenCalledTimes(1); expect(fetchConfig).toHaveBeenCalledWith({ diff --git a/kafka-ui-react-app/src/components/Connect/Details/Details.tsx b/kafka-ui-react-app/src/components/Connect/Details/Details.tsx index 815f80d916..f36fe3fedf 100644 --- a/kafka-ui-react-app/src/components/Connect/Details/Details.tsx +++ b/kafka-ui-react-app/src/components/Connect/Details/Details.tsx @@ -1,11 +1,15 @@ import React from 'react'; -import { NavLink, Route, Switch, useParams } from 'react-router-dom'; +import { NavLink, Route, Routes } from 'react-router-dom'; +import useAppParams from 'lib/hooks/useAppParams'; import { Connector, Task } from 'generated-sources'; import { ClusterName, ConnectName, ConnectorName } from 'redux/interfaces'; import { clusterConnectConnectorConfigPath, + clusterConnectConnectorConfigRelativePath, clusterConnectConnectorPath, clusterConnectConnectorTasksPath, + clusterConnectConnectorTasksRelativePath, + RouterParamsClusterConnectConnector, } from 'lib/paths'; import PageLoader from 'components/common/PageLoader/PageLoader'; import Navbar from 'components/common/Navigation/Navbar.styled'; @@ -16,12 +20,6 @@ import TasksContainer from './Tasks/TasksContainer'; import ConfigContainer from './Config/ConfigContainer'; import ActionsContainer from './Actions/ActionsContainer'; -interface RouterParams { - clusterName: ClusterName; - connectName: ConnectName; - connectorName: ConnectorName; -} - export interface DetailsProps { fetchConnector(payload: { clusterName: ClusterName; @@ -46,7 +44,8 @@ const Details: React.FC = ({ areTasksFetching, connector, }) => { - const { clusterName, connectName, connectorName } = useParams(); + const { clusterName, connectName, connectorName } = + useAppParams(); React.useEffect(() => { fetchConnector({ clusterName, connectName, connectorName }); @@ -69,68 +68,47 @@ const Details: React.FC = ({ (isActive ? 'is-active' : '')} > Overview (isActive ? 'is-active' : '')} > Tasks (isActive ? 'is-active' : '')} > Config - + + } /> } /> } /> - - + ); }; diff --git a/kafka-ui-react-app/src/components/Connect/Details/DetailsContainer.ts b/kafka-ui-react-app/src/components/Connect/Details/DetailsContainer.ts index 3c12442bd9..9f6d792d76 100644 --- a/kafka-ui-react-app/src/components/Connect/Details/DetailsContainer.ts +++ b/kafka-ui-react-app/src/components/Connect/Details/DetailsContainer.ts @@ -1,5 +1,4 @@ import { connect } from 'react-redux'; -import { withRouter } from 'react-router-dom'; import { RootState } from 'redux/interfaces'; import { fetchConnector, @@ -26,6 +25,4 @@ const mapDispatchToProps = { fetchTasks: fetchConnectorTasks, }; -export default withRouter( - connect(mapStateToProps, mapDispatchToProps)(Details) -); +export default connect(mapStateToProps, mapDispatchToProps)(Details); diff --git a/kafka-ui-react-app/src/components/Connect/Details/Overview/OverviewContainer.ts b/kafka-ui-react-app/src/components/Connect/Details/Overview/OverviewContainer.ts index e48650597c..6bca8b22a6 100644 --- a/kafka-ui-react-app/src/components/Connect/Details/Overview/OverviewContainer.ts +++ b/kafka-ui-react-app/src/components/Connect/Details/Overview/OverviewContainer.ts @@ -1,5 +1,4 @@ import { connect } from 'react-redux'; -import { withRouter } from 'react-router-dom'; import { RootState } from 'redux/interfaces'; import { getConnector, @@ -15,4 +14,4 @@ const mapStateToProps = (state: RootState) => ({ failedTasksCount: getConnectorFailedTasksCount(state), }); -export default withRouter(connect(mapStateToProps)(Overview)); +export default connect(mapStateToProps)(Overview); diff --git a/kafka-ui-react-app/src/components/Connect/Details/Tasks/ListItem/ListItem.tsx b/kafka-ui-react-app/src/components/Connect/Details/Tasks/ListItem/ListItem.tsx index 5b8267528d..91e2d97c67 100644 --- a/kafka-ui-react-app/src/components/Connect/Details/Tasks/ListItem/ListItem.tsx +++ b/kafka-ui-react-app/src/components/Connect/Details/Tasks/ListItem/ListItem.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { useParams } from 'react-router-dom'; +import useAppParams from 'lib/hooks/useAppParams'; import { Task, TaskId } from 'generated-sources'; import { ClusterName, ConnectName, ConnectorName } from 'redux/interfaces'; import Dropdown from 'components/common/Dropdown/Dropdown'; @@ -7,12 +7,7 @@ import DropdownItem from 'components/common/Dropdown/DropdownItem'; import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon'; import * as C from 'components/common/Tag/Tag.styled'; import getTagColor from 'components/common/Tag/getTagColor'; - -interface RouterParams { - clusterName: ClusterName; - connectName: ConnectName; - connectorName: ConnectorName; -} +import { RouterParamsClusterConnectConnector } from 'lib/paths'; export interface ListItemProps { task: Task; @@ -25,7 +20,8 @@ export interface ListItemProps { } const ListItem: React.FC = ({ task, restartTask }) => { - const { clusterName, connectName, connectorName } = useParams(); + const { clusterName, connectName, connectorName } = + useAppParams(); const restartTaskHandler = async () => { await restartTask({ diff --git a/kafka-ui-react-app/src/components/Connect/Details/Tasks/ListItem/ListItemContainer.ts b/kafka-ui-react-app/src/components/Connect/Details/Tasks/ListItem/ListItemContainer.ts index df16bbac13..9e170da61c 100644 --- a/kafka-ui-react-app/src/components/Connect/Details/Tasks/ListItem/ListItemContainer.ts +++ b/kafka-ui-react-app/src/components/Connect/Details/Tasks/ListItem/ListItemContainer.ts @@ -1,12 +1,11 @@ import { connect } from 'react-redux'; -import { RouteComponentProps, withRouter } from 'react-router-dom'; import { Task } from 'generated-sources'; import { RootState } from 'redux/interfaces'; import { restartConnectorTask } from 'redux/reducers/connect/connectSlice'; import ListItem from './ListItem'; -interface OwnProps extends RouteComponentProps { +interface OwnProps { task: Task; } @@ -18,6 +17,4 @@ const mapDispatchToProps = { restartTask: restartConnectorTask, }; -export default withRouter( - connect(mapStateToProps, mapDispatchToProps)(ListItem) -); +export default connect(mapStateToProps, mapDispatchToProps)(ListItem); diff --git a/kafka-ui-react-app/src/components/Connect/Details/Tasks/ListItem/__tests__/ListItem.spec.tsx b/kafka-ui-react-app/src/components/Connect/Details/Tasks/ListItem/__tests__/ListItem.spec.tsx index 647066e06f..8796281cc6 100644 --- a/kafka-ui-react-app/src/components/Connect/Details/Tasks/ListItem/__tests__/ListItem.spec.tsx +++ b/kafka-ui-react-app/src/components/Connect/Details/Tasks/ListItem/__tests__/ListItem.spec.tsx @@ -1,19 +1,14 @@ import React from 'react'; -import { render } from 'lib/testHelpers'; +import { render, WithRoute } from 'lib/testHelpers'; import { clusterConnectConnectorTasksPath } from 'lib/paths'; import ListItem, { ListItemProps, } from 'components/Connect/Details/Tasks/ListItem/ListItem'; import { tasks } from 'redux/reducers/connect/__test__/fixtures'; -import { Route } from 'react-router-dom'; import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -const pathname = clusterConnectConnectorTasksPath( - ':clusterName', - ':connectName', - ':connectorName' -); +const pathname = clusterConnectConnectorTasksPath(); const clusterName = 'my-cluster'; const connectName = 'my-connect'; const connectorName = 'my-connector'; @@ -22,19 +17,21 @@ const task = tasks[0]; const renderComponent = (props: ListItemProps = { task, restartTask }) => { return render( - +
-
, + , { - pathname: clusterConnectConnectorTasksPath( - clusterName, - connectName, - connectorName - ), + initialEntries: [ + clusterConnectConnectorTasksPath( + clusterName, + connectName, + connectorName + ), + ], } ); }; diff --git a/kafka-ui-react-app/src/components/Connect/Details/Tasks/TasksContainer.ts b/kafka-ui-react-app/src/components/Connect/Details/Tasks/TasksContainer.ts index 60cde38951..59162c4388 100644 --- a/kafka-ui-react-app/src/components/Connect/Details/Tasks/TasksContainer.ts +++ b/kafka-ui-react-app/src/components/Connect/Details/Tasks/TasksContainer.ts @@ -1,5 +1,4 @@ import { connect } from 'react-redux'; -import { withRouter } from 'react-router-dom'; import { RootState } from 'redux/interfaces'; import { fetchConnectorTasks } from 'redux/reducers/connect/connectSlice'; import { @@ -18,4 +17,4 @@ const mapDispatchToProps = { fetchTasks: fetchConnectorTasks, }; -export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Tasks)); +export default connect(mapStateToProps, mapDispatchToProps)(Tasks); diff --git a/kafka-ui-react-app/src/components/Connect/Details/Tasks/__tests__/Tasks.spec.tsx b/kafka-ui-react-app/src/components/Connect/Details/Tasks/__tests__/Tasks.spec.tsx index 1ec12ecfcd..b3dfb82d29 100644 --- a/kafka-ui-react-app/src/components/Connect/Details/Tasks/__tests__/Tasks.spec.tsx +++ b/kafka-ui-react-app/src/components/Connect/Details/Tasks/__tests__/Tasks.spec.tsx @@ -1,10 +1,9 @@ import React from 'react'; -import { render } from 'lib/testHelpers'; +import { render, WithRoute } from 'lib/testHelpers'; import { clusterConnectConnectorTasksPath } from 'lib/paths'; import TasksContainer from 'components/Connect/Details/Tasks/TasksContainer'; import Tasks, { TasksProps } from 'components/Connect/Details/Tasks/Tasks'; import { tasks } from 'redux/reducers/connect/__test__/fixtures'; -import { Route } from 'react-router-dom'; import { screen } from '@testing-library/dom'; jest.mock( @@ -19,28 +18,25 @@ describe('Tasks', () => { }); describe('view', () => { - const pathname = clusterConnectConnectorTasksPath( - ':clusterName', - ':connectName', - ':connectorName' - ); const clusterName = 'my-cluster'; const connectName = 'my-connect'; const connectorName = 'my-connector'; const setupWrapper = (props: Partial = {}) => ( - + - + ); it('to be in the document when fetching tasks', () => { render(setupWrapper({ areTasksFetching: true }), { - pathname: clusterConnectConnectorTasksPath( - clusterName, - connectName, - connectorName - ), + initialEntries: [ + clusterConnectConnectorTasksPath( + clusterName, + connectName, + connectorName + ), + ], }); expect(screen.getByRole('progressbar')).toBeInTheDocument(); expect(screen.queryByRole('table')).not.toBeInTheDocument(); @@ -48,11 +44,13 @@ describe('Tasks', () => { it('to be in the document when no tasks', () => { render(setupWrapper({ tasks: [] }), { - pathname: clusterConnectConnectorTasksPath( - clusterName, - connectName, - connectorName - ), + initialEntries: [ + clusterConnectConnectorTasksPath( + clusterName, + connectName, + connectorName + ), + ], }); expect(screen.getByRole('table')).toBeInTheDocument(); expect(screen.getByText('No tasks found')).toBeInTheDocument(); diff --git a/kafka-ui-react-app/src/components/Connect/Details/__tests__/Details.spec.tsx b/kafka-ui-react-app/src/components/Connect/Details/__tests__/Details.spec.tsx index 3cda6c1a7c..ff4d9618a2 100644 --- a/kafka-ui-react-app/src/components/Connect/Details/__tests__/Details.spec.tsx +++ b/kafka-ui-react-app/src/components/Connect/Details/__tests__/Details.spec.tsx @@ -1,100 +1,89 @@ import React from 'react'; -import { Route } from 'react-router-dom'; -import { render } from 'lib/testHelpers'; -import { clusterConnectConnectorPath } from 'lib/paths'; +import { render, WithRoute } from 'lib/testHelpers'; +import { + clusterConnectConnectorConfigPath, + clusterConnectConnectorPath, + clusterConnectConnectorTasksPath, + getNonExactPath, +} from 'lib/paths'; import Details, { DetailsProps } from 'components/Connect/Details/Details'; import { connector, tasks } from 'redux/reducers/connect/__test__/fixtures'; import { screen } from '@testing-library/dom'; -jest.mock( - 'components/Connect/Details/Overview/OverviewContainer', - () => 'mock-OverviewContainer' -); +const DetailsCompText = { + overview: 'OverviewContainer', + tasks: 'TasksContainer', + config: 'ConfigContainer', + actions: 'ActionsContainer', +}; -jest.mock( - 'components/Connect/Details/Tasks/TasksContainer', - () => 'mock-TasksContainer' -); +jest.mock('components/Connect/Details/Overview/OverviewContainer', () => () => ( +
{DetailsCompText.overview}
+)); -jest.mock( - 'components/Connect/Details/Config/ConfigContainer', - () => 'mock-ConfigContainer' -); +jest.mock('components/Connect/Details/Tasks/TasksContainer', () => () => ( +
{DetailsCompText.tasks}
+)); -jest.mock( - 'components/Connect/Details/Actions/ActionsContainer', - () => 'mock-ActionsContainer' -); +jest.mock('components/Connect/Details/Config/ConfigContainer', () => () => ( +
{DetailsCompText.config}
+)); + +jest.mock('components/Connect/Details/Actions/ActionsContainer', () => () => ( +
{DetailsCompText.actions}
+)); describe('Details', () => { - const pathname = clusterConnectConnectorPath( - ':clusterName', - ':connectName', - ':connectorName' - ); const clusterName = 'my-cluster'; const connectName = 'my-connect'; const connectorName = 'my-connector'; - - const setupWrapper = (props: Partial = {}) => ( - -
- + const defaultPath = clusterConnectConnectorPath( + clusterName, + connectName, + connectorName ); + const setupWrapper = ( + props: Partial = {}, + path: string = defaultPath + ) => + render( + +
+ , + { initialEntries: [path] } + ); + it('renders progressbar when fetching connector', () => { - render(setupWrapper({ isConnectorFetching: true }), { - pathname: clusterConnectConnectorPath( - clusterName, - connectName, - connectorName - ), - }); + setupWrapper({ isConnectorFetching: true }); expect(screen.getByRole('progressbar')).toBeInTheDocument(); expect(screen.queryByRole('navigation')).not.toBeInTheDocument(); }); it('renders progressbar when fetching tasks', () => { - render(setupWrapper({ areTasksFetching: true }), { - pathname: clusterConnectConnectorPath( - clusterName, - connectName, - connectorName - ), - }); + setupWrapper({ areTasksFetching: true }); + expect(screen.getByRole('progressbar')).toBeInTheDocument(); expect(screen.queryByRole('navigation')).not.toBeInTheDocument(); }); it('is empty when no connector', () => { - const { container } = render(setupWrapper({ connector: null }), { - pathname: clusterConnectConnectorPath( - clusterName, - connectName, - connectorName - ), - }); + const { container } = setupWrapper({ connector: null }); expect(container).toBeEmptyDOMElement(); }); it('fetches connector on mount', () => { const fetchConnector = jest.fn(); - render(setupWrapper({ fetchConnector }), { - pathname: clusterConnectConnectorPath( - clusterName, - connectName, - connectorName - ), - }); + setupWrapper({ fetchConnector }); expect(fetchConnector).toHaveBeenCalledTimes(1); expect(fetchConnector).toHaveBeenCalledWith({ clusterName, @@ -105,13 +94,7 @@ describe('Details', () => { it('fetches tasks on mount', () => { const fetchTasks = jest.fn(); - render(setupWrapper({ fetchTasks }), { - pathname: clusterConnectConnectorPath( - clusterName, - connectName, - connectorName - ), - }); + setupWrapper({ fetchTasks }); expect(fetchTasks).toHaveBeenCalledTimes(1); expect(fetchTasks).toHaveBeenCalledWith({ clusterName, @@ -119,4 +102,35 @@ describe('Details', () => { connectorName, }); }); + + describe('Router component tests', () => { + it('should test if overview is rendering', () => { + setupWrapper({}); + expect(screen.getByText(DetailsCompText.overview)); + }); + + it('should test if tasks is rendering', () => { + setupWrapper( + {}, + clusterConnectConnectorTasksPath( + clusterName, + connectName, + connectorName + ) + ); + expect(screen.getByText(DetailsCompText.tasks)); + }); + + it('should test if list is rendering', () => { + setupWrapper( + {}, + clusterConnectConnectorConfigPath( + clusterName, + connectName, + connectorName + ) + ); + expect(screen.getByText(DetailsCompText.config)); + }); + }); }); diff --git a/kafka-ui-react-app/src/components/Connect/Edit/Edit.tsx b/kafka-ui-react-app/src/components/Connect/Edit/Edit.tsx index feb57dd55a..c2b86ed3b2 100644 --- a/kafka-ui-react-app/src/components/Connect/Edit/Edit.tsx +++ b/kafka-ui-react-app/src/components/Connect/Edit/Edit.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import { useHistory, useParams } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; +import useAppParams from 'lib/hooks/useAppParams'; import { Controller, useForm } from 'react-hook-form'; import { ErrorMessage } from '@hookform/error-message'; import { yupResolver } from '@hookform/resolvers/yup'; @@ -9,7 +10,10 @@ import { ConnectorConfig, ConnectorName, } from 'redux/interfaces'; -import { clusterConnectConnectorConfigPath } from 'lib/paths'; +import { + clusterConnectConnectorConfigPath, + RouterParamsClusterConnectConnector, +} from 'lib/paths'; import yup from 'lib/yupExtended'; import Editor from 'components/common/Editor/Editor'; import PageLoader from 'components/common/PageLoader/PageLoader'; @@ -24,12 +28,6 @@ const validationSchema = yup.object().shape({ config: yup.string().required().isJsonObject(), }); -interface RouterParams { - clusterName: ClusterName; - connectName: ConnectName; - connectorName: ConnectorName; -} - interface FormValues { config: string; } @@ -56,8 +54,9 @@ const Edit: React.FC = ({ config, updateConfig, }) => { - const { clusterName, connectName, connectorName } = useParams(); - const history = useHistory(); + const { clusterName, connectName, connectorName } = + useAppParams(); + const navigate = useNavigate(); const { handleSubmit, control, @@ -89,7 +88,7 @@ const Edit: React.FC = ({ connectorConfig: JSON.parse(values.config.trim()), }); if (connector) { - history.push( + navigate( clusterConnectConnectorConfigPath( clusterName, connectName, diff --git a/kafka-ui-react-app/src/components/Connect/Edit/EditContainer.ts b/kafka-ui-react-app/src/components/Connect/Edit/EditContainer.ts index 42b35e67ee..5c3dd2d323 100644 --- a/kafka-ui-react-app/src/components/Connect/Edit/EditContainer.ts +++ b/kafka-ui-react-app/src/components/Connect/Edit/EditContainer.ts @@ -1,5 +1,4 @@ import { connect } from 'react-redux'; -import { withRouter } from 'react-router-dom'; import { RootState } from 'redux/interfaces'; import { fetchConnectorConfig, @@ -22,4 +21,4 @@ const mapDispatchToProps = { updateConfig: updateConnectorConfig, }; -export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Edit)); +export default connect(mapStateToProps, mapDispatchToProps)(Edit); diff --git a/kafka-ui-react-app/src/components/Connect/Edit/__tests__/Edit.spec.tsx b/kafka-ui-react-app/src/components/Connect/Edit/__tests__/Edit.spec.tsx index 8e4f926f5e..c17637c792 100644 --- a/kafka-ui-react-app/src/components/Connect/Edit/__tests__/Edit.spec.tsx +++ b/kafka-ui-react-app/src/components/Connect/Edit/__tests__/Edit.spec.tsx @@ -1,12 +1,11 @@ import React from 'react'; -import { render } from 'lib/testHelpers'; +import { render, WithRoute } from 'lib/testHelpers'; import { clusterConnectConnectorConfigPath, clusterConnectConnectorEditPath, } from 'lib/paths'; import Edit, { EditProps } from 'components/Connect/Edit/Edit'; import { connector } from 'redux/reducers/connect/__test__/fixtures'; -import { Route } from 'react-router-dom'; import { waitFor } from '@testing-library/dom'; import { act, fireEvent, screen } from '@testing-library/react'; @@ -17,24 +16,18 @@ jest.mock('components/common/Editor/Editor', () => 'mock-Editor'); const mockHistoryPush = jest.fn(); jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), - useHistory: () => ({ - push: mockHistoryPush, - }), + useNavigate: () => mockHistoryPush, })); describe('Edit', () => { - const pathname = clusterConnectConnectorEditPath( - ':clusterName', - ':connectName', - ':connectorName' - ); + const pathname = clusterConnectConnectorEditPath(); const clusterName = 'my-cluster'; const connectName = 'my-connect'; const connectorName = 'my-connector'; const renderComponent = (props: Partial = {}) => render( - + { updateConfig={jest.fn()} {...props} /> - , + , { - pathname: clusterConnectConnectorEditPath( - clusterName, - connectName, - connectorName - ), + initialEntries: [ + clusterConnectConnectorEditPath( + clusterName, + connectName, + connectorName + ), + ], } ); diff --git a/kafka-ui-react-app/src/components/Connect/List/List.tsx b/kafka-ui-react-app/src/components/Connect/List/List.tsx index d7e911c190..0ea1f4e6c0 100644 --- a/kafka-ui-react-app/src/components/Connect/List/List.tsx +++ b/kafka-ui-react-app/src/components/Connect/List/List.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import { useParams } from 'react-router-dom'; +import useAppParams from 'lib/hooks/useAppParams'; import { Connect, FullConnectorInfo } from 'generated-sources'; import { ClusterName, ConnectorSearch } from 'redux/interfaces'; -import { clusterConnectorNewPath } from 'lib/paths'; +import { clusterConnectorNewRelativePath, ClusterNameRoute } from 'lib/paths'; import ClusterContext from 'components/contexts/ClusterContext'; import PageLoader from 'components/common/PageLoader/PageLoader'; import Search from 'components/common/Search/Search'; @@ -40,7 +40,7 @@ const List: React.FC = ({ setConnectorSearch, }) => { const { isReadOnly } = React.useContext(ClusterContext); - const { clusterName } = useParams<{ clusterName: string }>(); + const { clusterName } = useAppParams(); React.useEffect(() => { fetchConnects(clusterName); @@ -58,10 +58,9 @@ const List: React.FC = ({ {!isReadOnly && ( diff --git a/kafka-ui-react-app/src/components/Connect/List/ListItem.tsx b/kafka-ui-react-app/src/components/Connect/List/ListItem.tsx index ff0f3805f1..50f8242e23 100644 --- a/kafka-ui-react-app/src/components/Connect/List/ListItem.tsx +++ b/kafka-ui-react-app/src/components/Connect/List/ListItem.tsx @@ -60,10 +60,7 @@ const ListItem: React.FC = ({ return ( - + {name} diff --git a/kafka-ui-react-app/src/components/Connect/New/New.tsx b/kafka-ui-react-app/src/components/Connect/New/New.tsx index 96b2a4f7e4..64d307ce1b 100644 --- a/kafka-ui-react-app/src/components/Connect/New/New.tsx +++ b/kafka-ui-react-app/src/components/Connect/New/New.tsx @@ -1,11 +1,12 @@ import React from 'react'; -import { useHistory, useParams } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; +import useAppParams from 'lib/hooks/useAppParams'; import { Controller, FormProvider, useForm } from 'react-hook-form'; import { ErrorMessage } from '@hookform/error-message'; import { yupResolver } from '@hookform/resolvers/yup'; import { Connect, Connector, NewConnector } from 'generated-sources'; import { ClusterName, ConnectName } from 'redux/interfaces'; -import { clusterConnectConnectorPath } from 'lib/paths'; +import { clusterConnectConnectorPath, ClusterNameRoute } from 'lib/paths'; import yup from 'lib/yupExtended'; import Editor from 'components/common/Editor/Editor'; import PageLoader from 'components/common/PageLoader/PageLoader'; @@ -23,10 +24,6 @@ const validationSchema = yup.object().shape({ config: yup.string().required().isJsonObject(), }); -interface RouterParams { - clusterName: ClusterName; -} - export interface NewProps { fetchConnects(clusterName: ClusterName): unknown; areConnectsFetching: boolean; @@ -50,8 +47,8 @@ const New: React.FC = ({ connects, createConnector, }) => { - const { clusterName } = useParams(); - const history = useHistory(); + const { clusterName } = useAppParams(); + const navigate = useNavigate(); const methods = useForm({ mode: 'onTouched', @@ -96,7 +93,7 @@ const New: React.FC = ({ }); if (connector) { - history.push( + navigate( clusterConnectConnectorPath( clusterName, connector.connect, diff --git a/kafka-ui-react-app/src/components/Connect/New/NewContainer.ts b/kafka-ui-react-app/src/components/Connect/New/NewContainer.ts index 1428652e67..23e577c4c1 100644 --- a/kafka-ui-react-app/src/components/Connect/New/NewContainer.ts +++ b/kafka-ui-react-app/src/components/Connect/New/NewContainer.ts @@ -1,5 +1,4 @@ import { connect } from 'react-redux'; -import { withRouter } from 'react-router-dom'; import { createConnector, fetchConnects, @@ -22,4 +21,4 @@ const mapDispatchToProps = { createConnector: createConnector as unknown as NewProps['createConnector'], }; -export default withRouter(connect(mapStateToProps, mapDispatchToProps)(New)); +export default connect(mapStateToProps, mapDispatchToProps)(New); diff --git a/kafka-ui-react-app/src/components/Connect/New/__tests__/New.spec.tsx b/kafka-ui-react-app/src/components/Connect/New/__tests__/New.spec.tsx index 649f566bc1..94cba3f5a2 100644 --- a/kafka-ui-react-app/src/components/Connect/New/__tests__/New.spec.tsx +++ b/kafka-ui-react-app/src/components/Connect/New/__tests__/New.spec.tsx @@ -1,12 +1,11 @@ import React from 'react'; -import { render } from 'lib/testHelpers'; +import { render, WithRoute } from 'lib/testHelpers'; import { clusterConnectConnectorPath, clusterConnectorNewPath, } from 'lib/paths'; import New, { NewProps } from 'components/Connect/New/New'; import { connects, connector } from 'redux/reducers/connect/__test__/fixtures'; -import { Route } from 'react-router-dom'; import { fireEvent, screen, act } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { ControllerRenderProps } from 'react-hook-form'; @@ -22,9 +21,7 @@ jest.mock( const mockHistoryPush = jest.fn(); jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), - useHistory: () => ({ - push: mockHistoryPush, - }), + useNavigate: () => mockHistoryPush, })); describe('New', () => { @@ -51,7 +48,7 @@ describe('New', () => { const renderComponent = (props: Partial = {}) => render( - + { createConnector={jest.fn()} {...props} /> - , - { pathname: clusterConnectorNewPath(clusterName) } + , + { initialEntries: [clusterConnectorNewPath(clusterName)] } ); it('fetches connects on mount', async () => { diff --git a/kafka-ui-react-app/src/components/Connect/__tests__/Connect.spec.tsx b/kafka-ui-react-app/src/components/Connect/__tests__/Connect.spec.tsx index 8708a2cea3..b1f1d2b185 100644 --- a/kafka-ui-react-app/src/components/Connect/__tests__/Connect.spec.tsx +++ b/kafka-ui-react-app/src/components/Connect/__tests__/Connect.spec.tsx @@ -1,53 +1,68 @@ import React from 'react'; -import { render } from 'lib/testHelpers'; +import { render, WithRoute } from 'lib/testHelpers'; import { screen } from '@testing-library/react'; import Connect from 'components/Connect/Connect'; import { store } from 'redux/store'; -import { Route } from 'react-router-dom'; import { clusterConnectorsPath, clusterConnectorNewPath, clusterConnectConnectorPath, clusterConnectConnectorEditPath, + getNonExactPath, + clusterConnectsPath, } from 'lib/paths'; +const ConnectCompText = { + new: 'NewContainer', + list: 'ListContainer', + details: 'DetailsContainer', + edit: 'EditContainer', +}; + jest.mock('components/Connect/New/NewContainer', () => () => ( -
NewContainer
+
{ConnectCompText.new}
)); jest.mock('components/Connect/List/ListContainer', () => () => ( -
ListContainer
+
{ConnectCompText.list}
)); jest.mock('components/Connect/Details/DetailsContainer', () => () => ( -
DetailsContainer
+
{ConnectCompText.details}
)); jest.mock('components/Connect/Edit/EditContainer', () => () => ( -
EditContainer
+
{ConnectCompText.edit}
)); describe('Connect', () => { - const renderComponent = (pathname: string) => + const renderComponent = (pathname: string, routePath: string) => render( - + - , - { pathname, store } + , + { initialEntries: [pathname], store } ); it('renders ListContainer', () => { - renderComponent(clusterConnectorsPath('my-cluster')); - expect(screen.getByText('ListContainer')).toBeInTheDocument(); + renderComponent( + clusterConnectorsPath('my-cluster'), + clusterConnectorsPath() + ); + expect(screen.getByText(ConnectCompText.list)).toBeInTheDocument(); }); it('renders NewContainer', () => { - renderComponent(clusterConnectorNewPath('my-cluster')); - expect(screen.getByText('NewContainer')).toBeInTheDocument(); + renderComponent( + clusterConnectorNewPath('my-cluster'), + clusterConnectorsPath() + ); + expect(screen.getByText(ConnectCompText.new)).toBeInTheDocument(); }); it('renders DetailsContainer', () => { renderComponent( - clusterConnectConnectorPath('my-cluster', 'my-connect', 'my-connector') + clusterConnectConnectorPath('my-cluster', 'my-connect', 'my-connector'), + clusterConnectsPath() ); - expect(screen.getByText('DetailsContainer')).toBeInTheDocument(); + expect(screen.getByText(ConnectCompText.details)).toBeInTheDocument(); }); it('renders EditContainer', () => { @@ -56,8 +71,9 @@ describe('Connect', () => { 'my-cluster', 'my-connect', 'my-connector' - ) + ), + clusterConnectsPath() ); - expect(screen.getByText('EditContainer')).toBeInTheDocument(); + expect(screen.getByText(ConnectCompText.edit)).toBeInTheDocument(); }); }); diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/ConsumerGroups.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/ConsumerGroups.tsx index 0fa0a6506f..8e7dfc4c30 100644 --- a/kafka-ui-react-app/src/components/ConsumerGroups/ConsumerGroups.tsx +++ b/kafka-ui-react-app/src/components/ConsumerGroups/ConsumerGroups.tsx @@ -1,28 +1,42 @@ import React from 'react'; -import { Switch } from 'react-router-dom'; +import { Route, Routes } from 'react-router-dom'; import Details from 'components/ConsumerGroups/Details/Details'; import ListContainer from 'components/ConsumerGroups/List/ListContainer'; import ResetOffsets from 'components/ConsumerGroups/Details/ResetOffsets/ResetOffsets'; import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route'; +import { + clusterConsumerGroupResetOffsetsRelativePath, + RouteParams, +} from 'lib/paths'; const ConsumerGroups: React.FC = () => { return ( - - + + + + } /> - +
+ + } /> - + + + } /> - + ); }; diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/Details/Details.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/Details/Details.tsx index b298fa8313..f76e2267eb 100644 --- a/kafka-ui-react-app/src/components/ConsumerGroups/Details/Details.tsx +++ b/kafka-ui-react-app/src/components/ConsumerGroups/Details/Details.tsx @@ -1,13 +1,12 @@ import React from 'react'; -import { ClusterName } from 'redux/interfaces'; +import { useNavigate } from 'react-router-dom'; +import useAppParams from 'lib/hooks/useAppParams'; import { - clusterConsumerGroupResetOffsetsPath, - clusterConsumerGroupsPath, + clusterConsumerGroupResetRelativePath, + ClusterGroupParam, } from 'lib/paths'; -import { ConsumerGroupID } from 'redux/interfaces/consumerGroup'; import PageLoader from 'components/common/PageLoader/PageLoader'; import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal'; -import { useHistory, useParams } from 'react-router-dom'; import ClusterContext from 'components/contexts/ClusterContext'; import PageHeading from 'components/common/PageHeading/PageHeading'; import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon'; @@ -31,10 +30,9 @@ import getTagColor from 'components/common/Tag/getTagColor'; import ListItem from './ListItem'; const Details: React.FC = () => { - const history = useHistory(); + const navigate = useNavigate(); const { isReadOnly } = React.useContext(ClusterContext); - const { consumerGroupID, clusterName } = - useParams<{ consumerGroupID: ConsumerGroupID; clusterName: ClusterName }>(); + const { consumerGroupID, clusterName } = useAppParams(); const dispatch = useAppDispatch(); const consumerGroup = useAppSelector((state) => selectById(state, consumerGroupID) @@ -55,14 +53,12 @@ const Details: React.FC = () => { }; React.useEffect(() => { if (isDeleted) { - history.push(clusterConsumerGroupsPath(clusterName)); + navigate('../'); } - }, [clusterName, history, isDeleted]); + }, [clusterName, navigate, isDeleted]); const onResetOffsets = () => { - history.push( - clusterConsumerGroupResetOffsetsPath(clusterName, consumerGroupID) - ); + navigate(clusterConsumerGroupResetRelativePath); }; if (!isFetched || !consumerGroup) { diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/ResetOffsets.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/ResetOffsets.tsx index fecb1f5762..7c32d76c0f 100644 --- a/kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/ResetOffsets.tsx +++ b/kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/ResetOffsets.tsx @@ -1,13 +1,13 @@ -import { ConsumerGroupOffsetsResetType } from 'generated-sources'; -import { clusterConsumerGroupDetailsPath } from 'lib/paths'; import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { ConsumerGroupOffsetsResetType } from 'generated-sources'; +import { ClusterGroupParam } from 'lib/paths'; import { Controller, FormProvider, useFieldArray, useForm, } from 'react-hook-form'; -import { ClusterName, ConsumerGroupID } from 'redux/interfaces'; import MultiSelect from 'react-multi-select-component'; import { Option } from 'react-multi-select-component/dist/lib/interfaces'; import DatePicker from 'react-datepicker'; @@ -15,7 +15,6 @@ import 'react-datepicker/dist/react-datepicker.css'; import { groupBy } from 'lodash'; import PageLoader from 'components/common/PageLoader/PageLoader'; import { ErrorMessage } from '@hookform/error-message'; -import { useHistory, useParams } from 'react-router-dom'; import Select from 'components/common/Select/Select'; import { InputLabel } from 'components/common/Input/InputLabel.styled'; import { Button } from 'components/common/Button/Button'; @@ -30,6 +29,7 @@ import { resetConsumerGroupOffsets, } from 'redux/reducers/consumerGroups/consumerGroupsSlice'; import { useAppDispatch, useAppSelector } from 'lib/hooks/redux'; +import useAppParams from 'lib/hooks/useAppParams'; import { resetLoaderById } from 'redux/reducers/loader/loaderSlice'; import { @@ -48,8 +48,7 @@ interface FormType { const ResetOffsets: React.FC = () => { const dispatch = useAppDispatch(); - const { consumerGroupID, clusterName } = - useParams<{ consumerGroupID: ConsumerGroupID; clusterName: ClusterName }>(); + const { consumerGroupID, clusterName } = useAppParams(); const consumerGroup = useAppSelector((state) => selectById(state, consumerGroupID) ); @@ -162,15 +161,13 @@ const ResetOffsets: React.FC = () => { } }; - const history = useHistory(); + const navigate = useNavigate(); React.useEffect(() => { if (isOffsetReseted) { dispatch(resetLoaderById('consumerGroups/resetConsumerGroupOffsets')); - history.push( - clusterConsumerGroupDetailsPath(clusterName, consumerGroupID) - ); + navigate('../'); } - }, [clusterName, consumerGroupID, dispatch, history, isOffsetReseted]); + }, [clusterName, consumerGroupID, dispatch, navigate, isOffsetReseted]); if (!isFetched || !consumerGroup) { return ; diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/__test__/ResetOffsets.spec.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/__test__/ResetOffsets.spec.tsx index 5c034670c1..963600b797 100644 --- a/kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/__test__/ResetOffsets.spec.tsx +++ b/kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/__test__/ResetOffsets.spec.tsx @@ -1,9 +1,8 @@ import React from 'react'; import fetchMock from 'fetch-mock'; -import { Route } from 'react-router-dom'; import { act, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { render } from 'lib/testHelpers'; +import { render, WithRoute } from 'lib/testHelpers'; import { clusterConsumerGroupResetOffsetsPath } from 'lib/paths'; import { consumerGroupPayload } from 'redux/reducers/consumerGroups/__test__/fixtures'; import ResetOffsets from 'components/ConsumerGroups/Details/ResetOffsets/ResetOffsets'; @@ -13,19 +12,16 @@ const { groupId } = consumerGroupPayload; const renderComponent = () => render( - + - , + , { - pathname: clusterConsumerGroupResetOffsetsPath( - clusterName, - consumerGroupPayload.groupId - ), + initialEntries: [ + clusterConsumerGroupResetOffsetsPath( + clusterName, + consumerGroupPayload.groupId + ), + ], } ); diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/Details/TopicContents/__test__/TopicContents.spec.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/Details/TopicContents/__test__/TopicContents.spec.tsx index 2d1a16d802..e1a10b7cd5 100644 --- a/kafka-ui-react-app/src/components/ConsumerGroups/Details/TopicContents/__test__/TopicContents.spec.tsx +++ b/kafka-ui-react-app/src/components/ConsumerGroups/Details/TopicContents/__test__/TopicContents.spec.tsx @@ -3,28 +3,27 @@ import { clusterConsumerGroupDetailsPath } from 'lib/paths'; import { screen } from '@testing-library/react'; import TopicContents from 'components/ConsumerGroups/Details/TopicContents/TopicContents'; import { consumerGroupPayload } from 'redux/reducers/consumerGroups/__test__/fixtures'; -import { render } from 'lib/testHelpers'; -import { Route } from 'react-router-dom'; +import { render, WithRoute } from 'lib/testHelpers'; import { ConsumerGroupTopicPartition } from 'generated-sources'; const clusterName = 'cluster1'; const renderComponent = (consumers: ConsumerGroupTopicPartition[] = []) => render( - +
-
, + , { - pathname: clusterConsumerGroupDetailsPath( - clusterName, - consumerGroupPayload.groupId - ), + initialEntries: [ + clusterConsumerGroupDetailsPath( + clusterName, + consumerGroupPayload.groupId + ), + ], } ); diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/Details/__tests__/Details.spec.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/Details/__tests__/Details.spec.tsx index 16e15171ce..315cc4b768 100644 --- a/kafka-ui-react-app/src/components/ConsumerGroups/Details/__tests__/Details.spec.tsx +++ b/kafka-ui-react-app/src/components/ConsumerGroups/Details/__tests__/Details.spec.tsx @@ -1,13 +1,10 @@ import Details from 'components/ConsumerGroups/Details/Details'; import React from 'react'; import fetchMock from 'fetch-mock'; -import { createMemoryHistory } from 'history'; -import { render } from 'lib/testHelpers'; -import { Route, Router } from 'react-router-dom'; +import { render, WithRoute } from 'lib/testHelpers'; import { clusterConsumerGroupDetailsPath, - clusterConsumerGroupResetOffsetsPath, - clusterConsumerGroupsPath, + clusterConsumerGroupResetRelativePath, } from 'lib/paths'; import { consumerGroupPayload } from 'redux/reducers/consumerGroups/__test__/fixtures'; import { @@ -20,26 +17,25 @@ import { act } from '@testing-library/react'; const clusterName = 'cluster1'; const { groupId } = consumerGroupPayload; -const history = createMemoryHistory(); + +const mockNavigate = jest.fn(); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => mockNavigate, +})); const renderComponent = () => { - history.push(clusterConsumerGroupDetailsPath(clusterName, groupId)); render( - - -
- - + +
+ , + { initialEntries: [clusterConsumerGroupDetailsPath(clusterName, groupId)] } ); }; describe('Details component', () => { afterEach(() => { fetchMock.reset(); + mockNavigate.mockClear(); }); describe('when consumer groups are NOT fetched', () => { @@ -76,8 +72,8 @@ describe('Details component', () => { it('handles [Reset offset] click', async () => { userEvent.click(screen.getByText('Reset offset')); - expect(history.location.pathname).toEqual( - clusterConsumerGroupResetOffsetsPath(clusterName, groupId) + expect(mockNavigate).toHaveBeenLastCalledWith( + clusterConsumerGroupResetRelativePath ); }); @@ -106,9 +102,7 @@ describe('Details component', () => { }); expect(deleteConsumerGroupMock.called()).toBeTruthy(); expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); - expect(history.location.pathname).toEqual( - clusterConsumerGroupsPath(clusterName) - ); + expect(mockNavigate).toHaveBeenLastCalledWith('../'); }); }); }); diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/Details/__tests__/ListItem.spec.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/Details/__tests__/ListItem.spec.tsx index 5deab301c3..5bbce7a927 100644 --- a/kafka-ui-react-app/src/components/ConsumerGroups/Details/__tests__/ListItem.spec.tsx +++ b/kafka-ui-react-app/src/components/ConsumerGroups/Details/__tests__/ListItem.spec.tsx @@ -4,17 +4,14 @@ import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import ListItem from 'components/ConsumerGroups/Details/ListItem'; import { consumerGroupPayload } from 'redux/reducers/consumerGroups/__test__/fixtures'; -import { render } from 'lib/testHelpers'; -import { Route } from 'react-router-dom'; +import { render, WithRoute } from 'lib/testHelpers'; import { ConsumerGroupTopicPartition } from 'generated-sources'; const clusterName = 'cluster1'; const renderComponent = (consumers: ConsumerGroupTopicPartition[] = []) => render( - + />
-
, + , { - pathname: clusterConsumerGroupDetailsPath( - clusterName, - consumerGroupPayload.groupId - ), + initialEntries: [ + clusterConsumerGroupDetailsPath( + clusterName, + consumerGroupPayload.groupId + ), + ], } ); diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/List/ConsumerGroupsTableCells.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/List/ConsumerGroupsTableCells.tsx index b47e76537b..f4295600c1 100644 --- a/kafka-ui-react-app/src/components/ConsumerGroups/List/ConsumerGroupsTableCells.tsx +++ b/kafka-ui-react-app/src/components/ConsumerGroups/List/ConsumerGroupsTableCells.tsx @@ -17,7 +17,7 @@ export const GroupIDCell: React.FC> = ({ }) => { return ( - {groupId} + {groupId} ); }; diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/List/List.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/List/List.tsx index 1286268d13..18c9142f5d 100644 --- a/kafka-ui-react-app/src/components/ConsumerGroups/List/List.tsx +++ b/kafka-ui-react-app/src/components/ConsumerGroups/List/List.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { useParams } from 'react-router-dom'; import PageHeading from 'components/common/PageHeading/PageHeading'; import Search from 'components/common/Search/Search'; import { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled'; @@ -18,7 +17,8 @@ import { import usePagination from 'lib/hooks/usePagination'; import useSearch from 'lib/hooks/useSearch'; import { useAppDispatch } from 'lib/hooks/redux'; -import { ClusterName } from 'redux/interfaces'; +import useAppParams from 'lib/hooks/useAppParams'; +import { ClusterNameRoute } from 'lib/paths'; import { fetchConsumerGroupsPaged } from 'redux/reducers/consumerGroups/consumerGroupsSlice'; import PageLoader from 'components/common/PageLoader/PageLoader'; @@ -42,7 +42,7 @@ const List: React.FC = ({ const { page, perPage } = usePagination(); const [searchText, handleSearchText] = useSearch(); const dispatch = useAppDispatch(); - const { clusterName } = useParams<{ clusterName: ClusterName }>(); + const { clusterName } = useAppParams(); React.useEffect(() => { dispatch( diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/List/__test__/ConsumerGroupsTableCells.spec.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/List/__test__/ConsumerGroupsTableCells.spec.tsx index 2da6fc6613..7aff424cb8 100644 --- a/kafka-ui-react-app/src/components/ConsumerGroups/List/__test__/ConsumerGroupsTableCells.spec.tsx +++ b/kafka-ui-react-app/src/components/ConsumerGroups/List/__test__/ConsumerGroupsTableCells.spec.tsx @@ -40,10 +40,7 @@ describe('Consumer Groups Table Cells', () => { ); const linkElement = screen.getByRole('link'); expect(linkElement).toBeInTheDocument(); - expect(linkElement).toHaveAttribute( - 'href', - `/consumer-groups/${consumerGroup.groupId}` - ); + expect(linkElement).toHaveAttribute('href', `/${consumerGroup.groupId}`); }); }); diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/__test__/ConsumerGroups.spec.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/__test__/ConsumerGroups.spec.tsx index 98bd21c3d4..7aedbd038c 100644 --- a/kafka-ui-react-app/src/components/ConsumerGroups/__test__/ConsumerGroups.spec.tsx +++ b/kafka-ui-react-app/src/components/ConsumerGroups/__test__/ConsumerGroups.spec.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { clusterConsumerGroupsPath } from 'lib/paths'; +import { clusterConsumerGroupsPath, getNonExactPath } from 'lib/paths'; import { act, screen, @@ -11,27 +11,19 @@ import { consumerGroups, noConsumerGroupsResponse, } from 'redux/reducers/consumerGroups/__test__/fixtures'; -import { render } from 'lib/testHelpers'; +import { render, WithRoute } from 'lib/testHelpers'; import fetchMock from 'fetch-mock'; -import { Route, Router } from 'react-router-dom'; import { ConsumerGroupOrdering, SortOrder } from 'generated-sources'; -import { createMemoryHistory } from 'history'; const clusterName = 'cluster1'; -const historyMock = createMemoryHistory({ - initialEntries: [clusterConsumerGroupsPath(clusterName)], -}); - -const renderComponent = (history = historyMock) => +const renderComponent = (path?: string) => render( - - - - - , + + + , { - pathname: clusterConsumerGroupsPath(clusterName), + initialEntries: [path || clusterConsumerGroupsPath(clusterName)], } ); @@ -123,12 +115,9 @@ describe('ConsumerGroups', () => { } ); - const mockedHistory = createMemoryHistory({ - initialEntries: [ - `${clusterConsumerGroupsPath(clusterName)}?q=${searchText}`, - ], - }); - renderComponent(mockedHistory); + renderComponent( + `${clusterConsumerGroupsPath(clusterName)}?q=${searchText}` + ); await waitForElementToBeRemoved(() => screen.getByRole('progressbar')); await waitFor(() => expect(consumerGroupsMock.called()).toBeTruthy()); diff --git a/kafka-ui-react-app/src/components/KsqlDb/KsqlDb.tsx b/kafka-ui-react-app/src/components/KsqlDb/KsqlDb.tsx index d99cb1eee5..cb64314c69 100644 --- a/kafka-ui-react-app/src/components/KsqlDb/KsqlDb.tsx +++ b/kafka-ui-react-app/src/components/KsqlDb/KsqlDb.tsx @@ -1,20 +1,30 @@ import React from 'react'; -import { Switch } from 'react-router-dom'; -import { clusterKsqlDbPath, clusterKsqlDbQueryPath } from 'lib/paths'; +import { Route, Routes } from 'react-router-dom'; +import { clusterKsqlDbQueryRelativePath } from 'lib/paths'; import List from 'components/KsqlDb/List/List'; import Query from 'components/KsqlDb/Query/Query'; import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route'; const KsqlDb: React.FC = () => { return ( - - - + + + + } /> - + + + + } + /> + ); }; diff --git a/kafka-ui-react-app/src/components/KsqlDb/List/List.tsx b/kafka-ui-react-app/src/components/KsqlDb/List/List.tsx index f2c8525630..1c0d5ffcc1 100644 --- a/kafka-ui-react-app/src/components/KsqlDb/List/List.tsx +++ b/kafka-ui-react-app/src/components/KsqlDb/List/List.tsx @@ -1,12 +1,12 @@ +import React, { FC, useEffect } from 'react'; +import useAppParams from 'lib/hooks/useAppParams'; import * as Metrics from 'components/common/Metrics'; import PageLoader from 'components/common/PageLoader/PageLoader'; import ListItem from 'components/KsqlDb/List/ListItem'; -import React, { FC, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { useParams } from 'react-router-dom'; import { fetchKsqlDbTables } from 'redux/reducers/ksqlDb/ksqlDbSlice'; import { getKsqlDbTables } from 'redux/reducers/ksqlDb/selectors'; -import { clusterKsqlDbQueryPath } from 'lib/paths'; +import { clusterKsqlDbQueryRelativePath, ClusterNameRoute } from 'lib/paths'; import PageHeading from 'components/common/PageHeading/PageHeading'; import { Table } from 'components/common/table/Table/Table.styled'; import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell'; @@ -25,7 +25,7 @@ const accessors = headers.map((header) => header.accessor); const List: FC = () => { const dispatch = useDispatch(); - const { clusterName } = useParams<{ clusterName: string }>(); + const { clusterName } = useAppParams(); const { rows, fetching, tablesCount, streamsCount } = useSelector(getKsqlDbTables); @@ -38,8 +38,7 @@ const List: FC = () => { <> diff --git a/kafka-ui-react-app/src/components/Schemas/Details/__test__/Details.spec.tsx b/kafka-ui-react-app/src/components/Schemas/Details/__test__/Details.spec.tsx index 9021402a87..fefcf99801 100644 --- a/kafka-ui-react-app/src/components/Schemas/Details/__test__/Details.spec.tsx +++ b/kafka-ui-react-app/src/components/Schemas/Details/__test__/Details.spec.tsx @@ -1,7 +1,6 @@ import React from 'react'; import Details from 'components/Schemas/Details/Details'; -import { render } from 'lib/testHelpers'; -import { Route } from 'react-router-dom'; +import { render, WithRoute } from 'lib/testHelpers'; import { clusterSchemaPath } from 'lib/paths'; import { screen, waitFor } from '@testing-library/dom'; import { @@ -27,13 +26,13 @@ const renderComponent = ( context: ContextProps = contextInitialValue ) => render( - +
- , + , { - pathname: clusterSchemaPath(clusterName, schemaVersion.subject), + initialEntries: [clusterSchemaPath(clusterName, schemaVersion.subject)], preloadedState: { schemas: initialState, }, diff --git a/kafka-ui-react-app/src/components/Schemas/Diff/Diff.tsx b/kafka-ui-react-app/src/components/Schemas/Diff/Diff.tsx index 3f9dddcb6f..4c12bc66fa 100644 --- a/kafka-ui-react-app/src/components/Schemas/Diff/Diff.tsx +++ b/kafka-ui-react-app/src/components/Schemas/Diff/Diff.tsx @@ -1,9 +1,9 @@ import React from 'react'; import { SchemaSubject } from 'generated-sources'; -import { clusterSchemaSchemaDiffPath } from 'lib/paths'; +import { clusterSchemaSchemaDiffPath, ClusterSubjectParam } from 'lib/paths'; import PageLoader from 'components/common/PageLoader/PageLoader'; import DiffViewer from 'components/common/DiffViewer/DiffViewer'; -import { useHistory, useParams, useLocation } from 'react-router-dom'; +import { useNavigate, useLocation } from 'react-router-dom'; import { fetchSchemaVersions, SCHEMAS_VERSIONS_FETCH_ACTION, @@ -12,31 +12,32 @@ import { useForm, Controller } from 'react-hook-form'; import Select from 'components/common/Select/Select'; import { useAppDispatch } from 'lib/hooks/redux'; import { resetLoaderById } from 'redux/reducers/loader/loaderSlice'; +import useAppParams from 'lib/hooks/useAppParams'; import * as S from './Diff.styled'; export interface DiffProps { - leftVersionInPath?: string; - rightVersionInPath?: string; versions: SchemaSubject[]; areVersionsFetched: boolean; } -const Diff: React.FC = ({ - leftVersionInPath, - rightVersionInPath, - versions, - areVersionsFetched, -}) => { - const [leftVersion, setLeftVersion] = React.useState(leftVersionInPath || ''); - const [rightVersion, setRightVersion] = React.useState( - rightVersionInPath || '' - ); - const history = useHistory(); +const Diff: React.FC = ({ versions, areVersionsFetched }) => { + const { clusterName, subject } = useAppParams(); + const navigate = useNavigate(); const location = useLocation(); - const { clusterName, subject } = - useParams<{ clusterName: string; subject: string }>(); + const searchParams = React.useMemo( + () => new URLSearchParams(location.search), + [location] + ); + + const [leftVersion, setLeftVersion] = React.useState( + searchParams.get('leftVersion') || '' + ); + const [rightVersion, setRightVersion] = React.useState( + searchParams.get('rightVersion') || '' + ); + const dispatch = useAppDispatch(); React.useEffect(() => { @@ -64,11 +65,6 @@ const Diff: React.FC = ({ control, } = methods; - const searchParams = React.useMemo( - () => new URLSearchParams(location.search), - [location] - ); - return ( {areVersionsFetched ? ( @@ -89,7 +85,7 @@ const Diff: React.FC = ({ leftVersion === '' ? versions[0].version : leftVersion } onChange={(event) => { - history.push( + navigate( clusterSchemaSchemaDiffPath(clusterName, subject) ); searchParams.set('leftVersion', event.toString()); @@ -99,7 +95,7 @@ const Diff: React.FC = ({ ? versions[0].version : rightVersion ); - history.push({ + navigate({ search: `?${searchParams.toString()}`, }); setLeftVersion(event.toString()); @@ -130,7 +126,7 @@ const Diff: React.FC = ({ rightVersion === '' ? versions[0].version : rightVersion } onChange={(event) => { - history.push( + navigate( clusterSchemaSchemaDiffPath(clusterName, subject) ); searchParams.set( @@ -138,7 +134,7 @@ const Diff: React.FC = ({ leftVersion === '' ? versions[0].version : leftVersion ); searchParams.set('rightVersion', event.toString()); - history.push({ + navigate({ search: `?${searchParams.toString()}`, }); setRightVersion(event.toString()); diff --git a/kafka-ui-react-app/src/components/Schemas/Diff/DiffContainer.ts b/kafka-ui-react-app/src/components/Schemas/Diff/DiffContainer.ts index d6f1d6e7d3..4b89529a19 100644 --- a/kafka-ui-react-app/src/components/Schemas/Diff/DiffContainer.ts +++ b/kafka-ui-react-app/src/components/Schemas/Diff/DiffContainer.ts @@ -1,6 +1,5 @@ import { connect } from 'react-redux'; import { RootState } from 'redux/interfaces'; -import { RouteComponentProps, withRouter } from 'react-router-dom'; import { getAreSchemaVersionsFulfilled, selectAllSchemaVersions, @@ -8,25 +7,9 @@ import { import Diff from './Diff'; -interface RouteProps { - leftVersion?: string; - rightVersion?: string; -} - -type OwnProps = RouteComponentProps; - -const mapStateToProps = ( - state: RootState, - { - match: { - params: { leftVersion, rightVersion }, - }, - }: OwnProps -) => ({ +const mapStateToProps = (state: RootState) => ({ versions: selectAllSchemaVersions(state), areVersionsFetched: getAreSchemaVersionsFulfilled(state), - leftVersionInPath: leftVersion, - rightVersionInPath: rightVersion, }); -export default withRouter(connect(mapStateToProps)(Diff)); +export default connect(mapStateToProps)(Diff); diff --git a/kafka-ui-react-app/src/components/Schemas/Diff/__test__/Diff.spec.tsx b/kafka-ui-react-app/src/components/Schemas/Diff/__test__/Diff.spec.tsx index 88efab8382..418393bb50 100644 --- a/kafka-ui-react-app/src/components/Schemas/Diff/__test__/Diff.spec.tsx +++ b/kafka-ui-react-app/src/components/Schemas/Diff/__test__/Diff.spec.tsx @@ -1,20 +1,46 @@ import React from 'react'; import Diff, { DiffProps } from 'components/Schemas/Diff/Diff'; -import { render } from 'lib/testHelpers'; +import { render, WithRoute } from 'lib/testHelpers'; import { screen } from '@testing-library/react'; +import { clusterSchemaSchemaDiffPath } from 'lib/paths'; import { versions } from './fixtures'; +const defaultClusterName = 'defaultClusterName'; +const defaultSubject = 'defaultSubject'; +const defaultPathName = clusterSchemaSchemaDiffPath( + defaultClusterName, + defaultSubject +); + describe('Diff', () => { - const setupComponent = (props: DiffProps) => - render( - + const setupComponent = ( + props: DiffProps, + searchQuery: { rightVersion?: string; leftVersion?: string } = {} + ) => { + let pathname = defaultPathName; + const searchParams = new URLSearchParams(pathname); + if (searchQuery.rightVersion) { + searchParams.set('rightVersion', searchQuery.rightVersion); + } + if (searchQuery.leftVersion) { + searchParams.set('leftVersion', searchQuery.leftVersion); + } + + pathname = `${pathname}?${searchParams.toString()}`; + + return render( + + + , + { + initialEntries: [pathname], + } ); + }; describe('Container', () => { it('renders view', () => { @@ -69,12 +95,13 @@ describe('Diff', () => { }); describe('when schema versions are loaded and two versions in path', () => { beforeEach(() => { - setupComponent({ - areVersionsFetched: true, - versions, - leftVersionInPath: '1', - rightVersionInPath: '2', - }); + setupComponent( + { + areVersionsFetched: true, + versions, + }, + { leftVersion: '1', rightVersion: '2' } + ); }); it('renders left select with version 1', () => { @@ -92,11 +119,15 @@ describe('Diff', () => { describe('when schema versions are loaded and only one versions in path', () => { beforeEach(() => { - setupComponent({ - areVersionsFetched: true, - versions, - leftVersionInPath: '1', - }); + setupComponent( + { + areVersionsFetched: true, + versions, + }, + { + leftVersion: '1', + } + ); }); it('renders left select with version 1', () => { diff --git a/kafka-ui-react-app/src/components/Schemas/Edit/Edit.tsx b/kafka-ui-react-app/src/components/Schemas/Edit/Edit.tsx index 639c289848..625da9f88e 100644 --- a/kafka-ui-react-app/src/components/Schemas/Edit/Edit.tsx +++ b/kafka-ui-react-app/src/components/Schemas/Edit/Edit.tsx @@ -1,11 +1,11 @@ import React from 'react'; -import { useHistory, useParams } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import { useForm, Controller, FormProvider } from 'react-hook-form'; import { CompatibilityLevelCompatibilityEnum, SchemaType, } from 'generated-sources'; -import { clusterSchemaPath } from 'lib/paths'; +import { clusterSchemaPath, ClusterSubjectParam } from 'lib/paths'; import { NewSchemaSubjectRaw } from 'redux/interfaces'; import Editor from 'components/common/Editor/Editor'; import Select from 'components/common/Select/Select'; @@ -13,6 +13,7 @@ import { Button } from 'components/common/Button/Button'; import { InputLabel } from 'components/common/Input/InputLabel.styled'; import PageHeading from 'components/common/PageHeading/PageHeading'; import { useAppDispatch, useAppSelector } from 'lib/hooks/redux'; +import useAppParams from 'lib/hooks/useAppParams'; import { schemaAdded, schemasApiClient, @@ -30,11 +31,10 @@ import { resetLoaderById } from 'redux/reducers/loader/loaderSlice'; import * as S from './Edit.styled'; const Edit: React.FC = () => { - const history = useHistory(); + const navigate = useNavigate(); const dispatch = useAppDispatch(); - const { clusterName, subject } = - useParams<{ clusterName: string; subject: string }>(); + const { clusterName, subject } = useAppParams(); const methods = useForm({ mode: 'onChange' }); const { formState: { isDirty, isSubmitting, dirtyFields }, @@ -90,7 +90,7 @@ const Edit: React.FC = () => { ); } - history.push(clusterSchemaPath(clusterName, subject)); + navigate(clusterSchemaPath(clusterName, subject)); } catch (e) { const err = await getResponse(e as Response); dispatch(serverErrorAlertAdded(err)); diff --git a/kafka-ui-react-app/src/components/Schemas/Edit/__tests__/Edit.spec.tsx b/kafka-ui-react-app/src/components/Schemas/Edit/__tests__/Edit.spec.tsx index 3c96b0fb23..ae4e74d7a9 100644 --- a/kafka-ui-react-app/src/components/Schemas/Edit/__tests__/Edit.spec.tsx +++ b/kafka-ui-react-app/src/components/Schemas/Edit/__tests__/Edit.spec.tsx @@ -1,12 +1,11 @@ import React from 'react'; import Edit from 'components/Schemas/Edit/Edit'; -import { render } from 'lib/testHelpers'; +import { render, WithRoute } from 'lib/testHelpers'; import { clusterSchemaEditPath } from 'lib/paths'; import { schemasInitialState, schemaVersion, } from 'redux/reducers/schemas/__test__/fixtures'; -import { Route } from 'react-router-dom'; import { screen, waitFor } from '@testing-library/dom'; import ClusterContext, { ContextProps, @@ -24,13 +23,15 @@ const renderComponent = ( context: ContextProps = contextInitialValue ) => render( - + - , + , { - pathname: clusterSchemaEditPath(clusterName, schemaVersion.subject), + initialEntries: [ + clusterSchemaEditPath(clusterName, schemaVersion.subject), + ], preloadedState: { schemas: initialState, }, @@ -41,7 +42,7 @@ describe('Edit', () => { afterEach(() => fetchMock.reset()); describe('fetch failed', () => { - it('renders pageloader', async () => { + it('renders page loader', async () => { const schemasAPILatestMock = fetchMock.getOnce(schemasAPILatestUrl, 404); await act(() => { renderComponent(); diff --git a/kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector/GlobalSchemaSelector.tsx b/kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector/GlobalSchemaSelector.tsx index 9157d454c6..77f7435114 100644 --- a/kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector/GlobalSchemaSelector.tsx +++ b/kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector/GlobalSchemaSelector.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal'; import Select from 'components/common/Select/Select'; import { CompatibilityLevelCompatibilityEnum } from 'generated-sources'; @@ -5,18 +6,18 @@ import { getResponse } from 'lib/errorHandling'; import { useAppDispatch } from 'lib/hooks/redux'; import usePagination from 'lib/hooks/usePagination'; import useSearch from 'lib/hooks/useSearch'; -import React from 'react'; -import { useParams } from 'react-router-dom'; +import useAppParams from 'lib/hooks/useAppParams'; import { serverErrorAlertAdded } from 'redux/reducers/alerts/alertsSlice'; import { fetchSchemas, schemasApiClient, } from 'redux/reducers/schemas/schemasSlice'; +import { ClusterNameRoute } from 'lib/paths'; import * as S from './GlobalSchemaSelector.styled'; const GlobalSchemaSelector: React.FC = () => { - const { clusterName } = useParams<{ clusterName: string }>(); + const { clusterName } = useAppParams(); const dispatch = useAppDispatch(); const [searchText] = useSearch(); const { page, perPage } = usePagination(); diff --git a/kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector/__test__/GlobalSchemaSelector.spec.tsx b/kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector/__test__/GlobalSchemaSelector.spec.tsx index 3d9a375bb5..df49cc1f5e 100644 --- a/kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector/__test__/GlobalSchemaSelector.spec.tsx +++ b/kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector/__test__/GlobalSchemaSelector.spec.tsx @@ -1,11 +1,10 @@ import React from 'react'; import { act, screen, waitFor, within } from '@testing-library/react'; -import { render } from 'lib/testHelpers'; +import { render, WithRoute } from 'lib/testHelpers'; import { CompatibilityLevelCompatibilityEnum } from 'generated-sources'; import GlobalSchemaSelector from 'components/Schemas/List/GlobalSchemaSelector/GlobalSchemaSelector'; import userEvent from '@testing-library/user-event'; import { clusterSchemasPath } from 'lib/paths'; -import { Route } from 'react-router-dom'; import fetchMock from 'fetch-mock'; const clusterName = 'testClusterName'; @@ -29,11 +28,11 @@ const expectOptionIsSelected = (option: string) => { describe('GlobalSchemaSelector', () => { const renderComponent = () => render( - + - , + , { - pathname: clusterSchemasPath(clusterName), + initialEntries: [clusterSchemasPath(clusterName)], } ); diff --git a/kafka-ui-react-app/src/components/Schemas/List/List.tsx b/kafka-ui-react-app/src/components/Schemas/List/List.tsx index 5a5de18b14..fa2e601838 100644 --- a/kafka-ui-react-app/src/components/Schemas/List/List.tsx +++ b/kafka-ui-react-app/src/components/Schemas/List/List.tsx @@ -1,12 +1,12 @@ import React from 'react'; -import { useParams } from 'react-router-dom'; -import { clusterSchemaNewPath } from 'lib/paths'; +import { ClusterNameRoute, clusterSchemaNewRelativePath } from 'lib/paths'; import ClusterContext from 'components/contexts/ClusterContext'; import * as C from 'components/common/table/Table/Table.styled'; import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell'; import { Button } from 'components/common/Button/Button'; import PageHeading from 'components/common/PageHeading/PageHeading'; import { useAppDispatch, useAppSelector } from 'lib/hooks/redux'; +import useAppParams from 'lib/hooks/useAppParams'; import { selectAllSchemas, fetchSchemas, @@ -27,7 +27,7 @@ import GlobalSchemaSelector from './GlobalSchemaSelector/GlobalSchemaSelector'; const List: React.FC = () => { const dispatch = useAppDispatch(); const { isReadOnly } = React.useContext(ClusterContext); - const { clusterName } = useParams<{ clusterName: string }>(); + const { clusterName } = useAppParams(); const schemas = useAppSelector(selectAllSchemas); const isFetched = useAppSelector(getAreSchemasFulfilled); @@ -52,8 +52,7 @@ const List: React.FC = () => { diff --git a/kafka-ui-react-app/src/components/Schemas/List/ListItem.tsx b/kafka-ui-react-app/src/components/Schemas/List/ListItem.tsx index b949a40121..be642fd6f1 100644 --- a/kafka-ui-react-app/src/components/Schemas/List/ListItem.tsx +++ b/kafka-ui-react-app/src/components/Schemas/List/ListItem.tsx @@ -13,7 +13,7 @@ const ListItem: React.FC = ({ return ( - + {subject} diff --git a/kafka-ui-react-app/src/components/Schemas/List/__test__/List.spec.tsx b/kafka-ui-react-app/src/components/Schemas/List/__test__/List.spec.tsx index da6ae2e434..2f66047229 100644 --- a/kafka-ui-react-app/src/components/Schemas/List/__test__/List.spec.tsx +++ b/kafka-ui-react-app/src/components/Schemas/List/__test__/List.spec.tsx @@ -1,7 +1,6 @@ import React from 'react'; import List from 'components/Schemas/List/List'; -import { render } from 'lib/testHelpers'; -import { Route } from 'react-router-dom'; +import { render, WithRoute } from 'lib/testHelpers'; import { clusterSchemasPath } from 'lib/paths'; import { act, screen } from '@testing-library/react'; import { @@ -27,13 +26,13 @@ const renderComponent = ( context: ContextProps = contextInitialValue ) => render( - + - , + , { - pathname: clusterSchemasPath(clusterName), + initialEntries: [clusterSchemasPath(clusterName)], preloadedState: { schemas: initialState, }, diff --git a/kafka-ui-react-app/src/components/Schemas/New/New.tsx b/kafka-ui-react-app/src/components/Schemas/New/New.tsx index 4e4bc09143..9f796d47e0 100644 --- a/kafka-ui-react-app/src/components/Schemas/New/New.tsx +++ b/kafka-ui-react-app/src/components/Schemas/New/New.tsx @@ -2,10 +2,10 @@ import React from 'react'; import { NewSchemaSubjectRaw } from 'redux/interfaces'; import { FormProvider, useForm, Controller } from 'react-hook-form'; import { ErrorMessage } from '@hookform/error-message'; -import { clusterSchemaPath } from 'lib/paths'; +import { ClusterNameRoute, clusterSchemaPath } from 'lib/paths'; import { SchemaType } from 'generated-sources'; import { SCHEMA_NAME_VALIDATION_PATTERN } from 'lib/constants'; -import { useHistory, useParams } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import { InputLabel } from 'components/common/Input/InputLabel.styled'; import Input from 'components/common/Input/Input'; import { FormError } from 'components/common/Input/Input.styled'; @@ -18,6 +18,7 @@ import { schemasApiClient, } from 'redux/reducers/schemas/schemasSlice'; import { useAppDispatch } from 'lib/hooks/redux'; +import useAppParams from 'lib/hooks/useAppParams'; import { serverErrorAlertAdded } from 'redux/reducers/alerts/alertsSlice'; import { getResponse } from 'lib/errorHandling'; @@ -30,8 +31,8 @@ const SchemaTypeOptions: Array = [ ]; const New: React.FC = () => { - const { clusterName } = useParams<{ clusterName: string }>(); - const history = useHistory(); + const { clusterName } = useAppParams(); + const navigate = useNavigate(); const dispatch = useAppDispatch(); const methods = useForm(); const { @@ -52,7 +53,7 @@ const New: React.FC = () => { newSchemaSubject: { subject, schema, schemaType }, }); dispatch(schemaAdded(resp)); - history.push(clusterSchemaPath(clusterName, subject)); + navigate(clusterSchemaPath(clusterName, subject)); } catch (e) { const err = await getResponse(e as Response); dispatch(serverErrorAlertAdded(err)); diff --git a/kafka-ui-react-app/src/components/Schemas/New/__test__/New.spec.tsx b/kafka-ui-react-app/src/components/Schemas/New/__test__/New.spec.tsx index 3b4c545dab..c4bcf77abc 100644 --- a/kafka-ui-react-app/src/components/Schemas/New/__test__/New.spec.tsx +++ b/kafka-ui-react-app/src/components/Schemas/New/__test__/New.spec.tsx @@ -1,8 +1,7 @@ import React from 'react'; import New from 'components/Schemas/New/New'; -import { render } from 'lib/testHelpers'; +import { render, WithRoute } from 'lib/testHelpers'; import { clusterSchemaNewPath } from 'lib/paths'; -import { Route } from 'react-router-dom'; import { screen } from '@testing-library/dom'; const clusterName = 'local'; @@ -10,11 +9,11 @@ const clusterName = 'local'; describe('New Component', () => { beforeEach(() => { render( - + - , + , { - pathname: clusterSchemaNewPath(clusterName), + initialEntries: [clusterSchemaNewPath(clusterName)], } ); }); diff --git a/kafka-ui-react-app/src/components/Schemas/Schemas.tsx b/kafka-ui-react-app/src/components/Schemas/Schemas.tsx index 8d3b83a72d..e98cd33375 100644 --- a/kafka-ui-react-app/src/components/Schemas/Schemas.tsx +++ b/kafka-ui-react-app/src/components/Schemas/Schemas.tsx @@ -1,11 +1,9 @@ import React from 'react'; -import { Switch } from 'react-router-dom'; +import { Route, Routes } from 'react-router-dom'; import { - clusterSchemaNewPath, - clusterSchemaPath, - clusterSchemaEditPath, - clusterSchemasPath, - clusterSchemaSchemaDiffPath, + clusterSchemaEditRelativePath, + clusterSchemaNewRelativePath, + RouteParams, } from 'lib/paths'; import List from 'components/Schemas/List/List'; import Details from 'components/Schemas/Details/Details'; @@ -16,33 +14,48 @@ import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route'; const Schemas: React.FC = () => { return ( - - + + + + } /> - + + + } /> - +
+ + } /> - + + + } /> - + + + } /> - + ); }; diff --git a/kafka-ui-react-app/src/components/Schemas/__test__/Schemas.spec.tsx b/kafka-ui-react-app/src/components/Schemas/__test__/Schemas.spec.tsx index 5f8b4d122b..604a21d0b5 100644 --- a/kafka-ui-react-app/src/components/Schemas/__test__/Schemas.spec.tsx +++ b/kafka-ui-react-app/src/components/Schemas/__test__/Schemas.spec.tsx @@ -1,32 +1,46 @@ import React from 'react'; import Schemas from 'components/Schemas/Schemas'; -import { render } from 'lib/testHelpers'; +import { render, WithRoute } from 'lib/testHelpers'; import { - clusterPath, clusterSchemaEditPath, clusterSchemaNewPath, clusterSchemaPath, clusterSchemasPath, + getNonExactPath, } from 'lib/paths'; import { screen, waitFor } from '@testing-library/dom'; -import { Route } from 'react-router-dom'; import fetchMock from 'fetch-mock'; import { schemaVersion } from 'redux/reducers/schemas/__test__/fixtures'; const renderComponent = (pathname: string) => render( - + - , - { pathname } + , + { initialEntries: [pathname] } ); const clusterName = 'secondLocal'; -jest.mock('components/Schemas/List/List', () => () =>
List
); -jest.mock('components/Schemas/Details/Details', () => () =>
Details
); -jest.mock('components/Schemas/New/New', () => () =>
New
); -jest.mock('components/Schemas/Edit/Edit', () => () =>
Edit
); +const SchemaCompText = { + List: 'List', + Details: 'Details', + New: 'New', + Edit: 'Edit', +}; + +jest.mock('components/Schemas/List/List', () => () => ( +
{SchemaCompText.List}
+)); +jest.mock('components/Schemas/Details/Details', () => () => ( +
{SchemaCompText.Details}
+)); +jest.mock('components/Schemas/New/New', () => () => ( +
{SchemaCompText.New}
+)); +jest.mock('components/Schemas/Edit/Edit', () => () => ( +
{SchemaCompText.Edit}
+)); describe('Schemas', () => { beforeEach(() => { @@ -35,20 +49,26 @@ describe('Schemas', () => { afterEach(() => fetchMock.restore()); it('renders List', async () => { renderComponent(clusterSchemasPath(clusterName)); - await waitFor(() => expect(screen.queryByText('List')).toBeInTheDocument()); + await waitFor(() => + expect(screen.queryByText(SchemaCompText.List)).toBeInTheDocument() + ); }); it('renders New', async () => { renderComponent(clusterSchemaNewPath(clusterName)); - await waitFor(() => expect(screen.queryByText('New')).toBeInTheDocument()); + await waitFor(() => + expect(screen.queryByText(SchemaCompText.New)).toBeInTheDocument() + ); }); it('renders Details', async () => { renderComponent(clusterSchemaPath(clusterName, schemaVersion.subject)); await waitFor(() => - expect(screen.queryByText('Details')).toBeInTheDocument() + expect(screen.queryByText(SchemaCompText.Details)).toBeInTheDocument() ); }); it('renders Edit', async () => { renderComponent(clusterSchemaEditPath(clusterName, schemaVersion.subject)); - await waitFor(() => expect(screen.queryByText('Edit')).toBeInTheDocument()); + await waitFor(() => + expect(screen.queryByText(SchemaCompText.Edit)).toBeInTheDocument() + ); }); }); diff --git a/kafka-ui-react-app/src/components/Topics/List/List.styled.ts b/kafka-ui-react-app/src/components/Topics/List/List.styled.ts index 95214624d3..abda595b45 100644 --- a/kafka-ui-react-app/src/components/Topics/List/List.styled.ts +++ b/kafka-ui-react-app/src/components/Topics/List/List.styled.ts @@ -2,10 +2,10 @@ import { Td } from 'components/common/table/TableHeaderCell/TableHeaderCell.styl import { NavLink } from 'react-router-dom'; import styled, { css } from 'styled-components'; -export const Link = styled(NavLink).attrs({ activeClassName: 'is-active' })<{ +export const Link = styled(NavLink)<{ $isInternal?: boolean; }>( - ({ theme, activeClassName, $isInternal }) => css` + ({ theme, $isInternal }) => css` color: ${theme.topicsList.color.normal}; font-weight: 500; padding-left: ${$isInternal ? '5px' : 0}; @@ -15,7 +15,7 @@ export const Link = styled(NavLink).attrs({ activeClassName: 'is-active' })<{ color: ${theme.topicsList.color.hover}; } - &.${activeClassName} { + &.active { background-color: ${theme.topicsList.backgroundColor.active}; color: ${theme.topicsList.color.active}; } 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 ee293eb526..42a9796c96 100644 --- a/kafka-ui-react-app/src/components/Topics/List/List.tsx +++ b/kafka-ui-react-app/src/components/Topics/List/List.tsx @@ -1,11 +1,16 @@ import React from 'react'; -import { useHistory, useParams } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; +import useAppParams from 'lib/hooks/useAppParams'; import { TopicWithDetailedInfo, ClusterName, TopicName, } from 'redux/interfaces'; -import { clusterTopicCopyPath, clusterTopicNewPath } from 'lib/paths'; +import { + ClusterNameRoute, + clusterTopicCopyRelativePath, + clusterTopicNewRelativePath, +} from 'lib/paths'; import usePagination from 'lib/hooks/usePagination'; import useModal from 'lib/hooks/useModal'; import ClusterContext from 'components/contexts/ClusterContext'; @@ -88,13 +93,15 @@ const List: React.FC = ({ }) => { const { isReadOnly, isTopicDeletionAllowed } = React.useContext(ClusterContext); - const { clusterName } = useParams<{ clusterName: ClusterName }>(); - const { page, perPage, pathname } = usePagination(); + const { clusterName } = useAppParams(); + const { page, perPage } = usePagination(); const [showInternal, setShowInternal] = React.useState( !localStorage.getItem('hideInternalTopics') && true ); - const [cachedPage, setCachedPage] = React.useState(null); - const history = useHistory(); + const [cachedPage, setCachedPage] = React.useState( + page || null + ); + const navigate = useNavigate(); const topicsListParams = React.useMemo( () => ({ @@ -154,7 +161,9 @@ const List: React.FC = ({ } setShowInternal(!showInternal); - history.push(`${pathname}?page=1&perPage=${perPage || PER_PAGE}`); + navigate({ + search: `?page=1&perPage=${perPage || PER_PAGE}`, + }); }; const [confirmationModal, setConfirmationModal] = React.useState< @@ -176,9 +185,9 @@ const List: React.FC = ({ const newPageQuery = !searchString && cachedPage ? cachedPage : 1; - history.push( - `${pathname}?page=${newPageQuery}&perPage=${perPage || PER_PAGE}` - ); + navigate({ + search: `?page=${newPageQuery}&perPage=${perPage || PER_PAGE}`, + }); }; const deleteOrPurgeConfirmationHandler = () => { const selectedIds = Array.from(tableState.selectedIds); @@ -283,8 +292,7 @@ const List: React.FC = ({ @@ -331,9 +339,8 @@ const List: React.FC = ({ - + + + Produce Message + + } + /> + {!isReadOnly && !isInternal && ( - - } right> - - history.push(clusterTopicEditPath(clusterName, topicName)) - } - > - Edit settings - - Pay attention! This operation has -
- especially important consequences. -
-
- {isDeletePolicy && ( - setClearTopicConfirmationVisible(true)} - danger - > - Clear messages - - )} - setRecreateTopicConfirmationVisible(true)} - danger - > - Recreate Topic - - {isTopicDeletionAllowed && ( - setDeleteTopicConfirmationVisible(true)} - danger - > - Remove topic - - )} -
-
+ + } right> + navigate(clusterTopicEditRelativePath)} + > + Edit settings + + Pay attention! This operation has +
+ especially important consequences. +
+
+ {isDeletePolicy && ( + setClearTopicConfirmationVisible(true)} + danger + > + Clear messages + + )} + setRecreateTopicConfirmationVisible(true)} + danger + > + Recreate Topic + + {isTopicDeletionAllowed && ( + setDeleteTopicConfirmationVisible(true)} + danger + > + Remove topic + + )} + + } + /> +
)} @@ -177,56 +187,45 @@ const Details: React.FC = ({ (isActive ? 'is-active is-primary' : '')} > Overview (isActive ? 'is-active' : '')} > Messages (isActive ? 'is-active' : '')} > Consumers (isActive ? 'is-active' : '')} > Settings - + + } /> + + } /> + } /> + } /> - - - + ); }; diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Details/DetailsContainer.ts b/kafka-ui-react-app/src/components/Topics/Topic/Details/DetailsContainer.ts index e3fbd01f47..325d4d84e2 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Details/DetailsContainer.ts +++ b/kafka-ui-react-app/src/components/Topics/Topic/Details/DetailsContainer.ts @@ -1,36 +1,13 @@ import { connect } from 'react-redux'; -import { ClusterName, RootState, TopicName } from 'redux/interfaces'; -import { withRouter, RouteComponentProps } from 'react-router-dom'; +import { RootState } from 'redux/interfaces'; import { deleteTopic, recreateTopic } from 'redux/reducers/topics/topicsSlice'; import { clearTopicMessages } from 'redux/reducers/topicMessages/topicMessagesSlice'; -import { - getIsTopicDeleted, - getIsTopicDeletePolicy, - getIsTopicInternal, -} from 'redux/reducers/topics/selectors'; +import { getIsTopicDeleted } from 'redux/reducers/topics/selectors'; import Details from './Details'; -interface RouteProps { - clusterName: ClusterName; - topicName: TopicName; -} - -type OwnProps = RouteComponentProps; - -const mapStateToProps = ( - state: RootState, - { - match: { - params: { topicName, clusterName }, - }, - }: OwnProps -) => ({ - clusterName, - topicName, - isInternal: getIsTopicInternal(state, topicName), +const mapStateToProps = (state: RootState) => ({ isDeleted: getIsTopicDeleted(state), - isDeletePolicy: getIsTopicDeletePolicy(state, topicName), }); const mapDispatchToProps = { @@ -39,6 +16,4 @@ const mapDispatchToProps = { clearTopicMessages, }; -export default withRouter( - connect(mapStateToProps, mapDispatchToProps)(Details) -); +export default connect(mapStateToProps, mapDispatchToProps)(Details); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/Filters.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/Filters.tsx index ad3aec6b81..cb56199287 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/Filters.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/Filters.tsx @@ -12,12 +12,11 @@ import { } from 'generated-sources'; import React, { useContext } from 'react'; import { omitBy } from 'lodash'; -import { useHistory, useLocation } from 'react-router-dom'; +import { useNavigate, useLocation } from 'react-router-dom'; import DatePicker from 'react-datepicker'; import MultiSelect from 'components/common/MultiSelect/MultiSelect.styled'; import { Option } from 'react-multi-select-component/dist/lib/interfaces'; import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted'; -import { ClusterName, TopicName } from 'redux/interfaces'; import { BASE_PARAMS } from 'lib/constants'; import Input from 'components/common/Input/Input'; import Select from 'components/common/Select/Select'; @@ -29,6 +28,10 @@ import FilterModal, { import { SeekDirectionOptions } from 'components/Topics/Topic/Details/Messages/Messages'; import TopicMessagesContext from 'components/contexts/TopicMessagesContext'; import useModal from 'lib/hooks/useModal'; +import { getPartitionsByTopicName } from 'redux/reducers/topics/selectors'; +import { useAppSelector } from 'lib/hooks/redux'; +import { RouteParamsClusterTopic } from 'lib/paths'; +import useAppParams from 'lib/hooks/useAppParams'; import * as S from './Filters.styled'; import { @@ -41,10 +44,7 @@ import { type Query = Record; export interface FiltersProps { - clusterName: ClusterName; - topicName: TopicName; phaseMessage?: string; - partitions: Partition[]; meta: TopicMessageConsuming; isFetching: boolean; addMessage(content: { message: TopicMessage; prepend: boolean }): void; @@ -73,9 +73,6 @@ export const SeekTypeOptions = [ ]; const Filters: React.FC = ({ - clusterName, - topicName, - partitions, phaseMessage, meta: { elapsedMs, bytesConsumed, messagesConsumed }, isFetching, @@ -85,8 +82,13 @@ const Filters: React.FC = ({ updateMeta, setIsFetching, }) => { + const { clusterName, topicName } = useAppParams(); const location = useLocation(); - const history = useHistory(); + const navigate = useNavigate(); + + const partitions = useAppSelector((state) => + getPartitionsByTopicName(state, topicName) + ); const { searchParams, seekDirection, isLive, changeSeekDirection } = useContext(TopicMessagesContext); @@ -212,7 +214,7 @@ const Filters: React.FC = ({ .map((key) => `${key}=${newProps[key]}`) .join('&'); - history.push({ + navigate({ search: `?${qs}`, }); }, @@ -224,6 +226,7 @@ const Filters: React.FC = ({ timestamp, query, selectedPartitions, + navigate, ] ); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/FiltersContainer.ts b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/FiltersContainer.ts index 24c891111a..144672ee60 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/FiltersContainer.ts +++ b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/FiltersContainer.ts @@ -1,6 +1,5 @@ import { connect } from 'react-redux'; -import { ClusterName, RootState, TopicName } from 'redux/interfaces'; -import { withRouter, RouteComponentProps } from 'react-router-dom'; +import { RootState } from 'redux/interfaces'; import { addTopicMessage, resetTopicMessages, @@ -13,29 +12,11 @@ import { getTopicMessgesPhase, getIsTopicMessagesFetching, } from 'redux/reducers/topicMessages/selectors'; -import { getPartitionsByTopicName } from 'redux/reducers/topics/selectors'; import Filters from './Filters'; -interface RouteProps { - clusterName: ClusterName; - topicName: TopicName; -} - -type OwnProps = RouteComponentProps; - -const mapStateToProps = ( - state: RootState, - { - match: { - params: { topicName, clusterName }, - }, - }: OwnProps -) => ({ - clusterName, - topicName, +const mapStateToProps = (state: RootState) => ({ phaseMessage: getTopicMessgesPhase(state), - partitions: getPartitionsByTopicName(state, topicName), meta: getTopicMessgesMeta(state), isFetching: getIsTopicMessagesFetching(state), }); @@ -48,6 +29,4 @@ const mapDispatchToProps = { setIsFetching: setTopicMessagesFetchingStatus, }; -export default withRouter( - connect(mapStateToProps, mapDispatchToProps)(Filters) -); +export default connect(mapStateToProps, mapDispatchToProps)(Filters); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/__tests__/Filters.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/__tests__/Filters.spec.tsx index 79cddac6d7..d6030b0554 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/__tests__/Filters.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/__tests__/Filters.spec.tsx @@ -4,7 +4,7 @@ import Filters, { FiltersProps, SeekTypeOptions, } from 'components/Topics/Topic/Details/Messages/Filters/Filters'; -import { render } from 'lib/testHelpers'; +import { EventSourceMock, render } from 'lib/testHelpers'; import { act, screen, within, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import TopicMessagesContext, { @@ -26,9 +26,6 @@ const renderComponent = ( render( { + Object.defineProperty(window, 'EventSource', { + value: EventSourceMock, + }); + it('shows cancel button while fetching', () => { renderComponent({ isFetching: true }); expect(screen.getByText('Cancel')).toBeInTheDocument(); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/Messages.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/Messages.spec.tsx index b752f901d4..cca37c518f 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/Messages.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/Messages.spec.tsx @@ -5,8 +5,6 @@ import Messages, { SeekDirectionOptions, SeekDirectionOptionsObj, } from 'components/Topics/Topic/Details/Messages/Messages'; -import { Router } from 'react-router-dom'; -import { createMemoryHistory } from 'history'; import { SeekDirection, SeekType } from 'generated-sources'; import userEvent from '@testing-library/user-event'; @@ -14,15 +12,9 @@ describe('Messages', () => { const searchParams = `?filterQueryType=STRING_CONTAINS&attempt=0&limit=100&seekDirection=${SeekDirection.FORWARD}&seekType=${SeekType.OFFSET}&seekTo=0::9`; const setUpComponent = (param: string = searchParams) => { - const history = createMemoryHistory(); - history.push({ - search: new URLSearchParams(param).toString(), + return render(, { + initialEntries: [`/?${new URLSearchParams(param).toString()}`], }); - return render( - - - - ); }; beforeEach(() => { diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/MessagesTable.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/MessagesTable.spec.tsx index e3b09f5b18..992e15d8f4 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/MessagesTable.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/MessagesTable.spec.tsx @@ -2,8 +2,6 @@ import React from 'react'; import { screen } from '@testing-library/react'; import { render } from 'lib/testHelpers'; import MessagesTable from 'components/Topics/Topic/Details/Messages/MessagesTable'; -import { Router } from 'react-router-dom'; -import { createMemoryHistory, MemoryHistory } from 'history'; import { SeekDirection, SeekType, TopicMessage } from 'generated-sources'; import TopicMessagesContext, { ContextProps, @@ -15,6 +13,12 @@ import { const mockTopicsMessages: TopicMessage[] = [{ ...topicMessagePayload }]; +const mockNavigate = jest.fn(); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => mockNavigate, +})); + describe('MessagesTable', () => { const seekToResult = '&seekTo=0::9'; const searchParamsValue = `?filterQueryType=STRING_CONTAINS&attempt=0&limit=100&seekDirection=${SeekDirection.FORWARD}&seekType=${SeekType.OFFSET}${seekToResult}`; @@ -31,20 +35,15 @@ describe('MessagesTable', () => { ctx: ContextProps = contextValue, messages: TopicMessage[] = [], isFetching?: boolean, - customHistory?: MemoryHistory + path?: string ) => { - const history = - customHistory || - createMemoryHistory({ - initialEntries: [params.toString()], - }); + const customPath = path || params.toString(); return render( - - - - - , + + + , { + initialEntries: [customPath], preloadedState: { topicMessages: { messages, diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/Overview.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/Overview.tsx index 6e497b017e..3f155d9791 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/Overview.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/Overview.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Partition, Replica, Topic, TopicDetails } from 'generated-sources'; +import { Partition, Replica } from 'generated-sources'; import { ClusterName, TopicName } from 'redux/interfaces'; import Dropdown from 'components/common/Dropdown/Dropdown'; import DropdownItem from 'components/common/Dropdown/DropdownItem'; @@ -10,11 +10,13 @@ import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeader import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon'; import * as Metrics from 'components/common/Metrics'; import { Tag } from 'components/common/Tag/Tag.styled'; +import { useAppSelector } from 'lib/hooks/redux'; +import { getTopicByName } from 'redux/reducers/topics/selectors'; import { ReplicaCell } from 'components/Topics/Topic/Details/Details.styled'; +import { RouteParamsClusterTopic } from 'lib/paths'; +import useAppParams from 'lib/hooks/useAppParams'; -export interface Props extends Topic, TopicDetails { - clusterName: ClusterName; - topicName: TopicName; +export interface Props { clearTopicMessages(params: { clusterName: ClusterName; topicName: TopicName; @@ -22,21 +24,25 @@ export interface Props extends Topic, TopicDetails { }): void; } -const Overview: React.FC = ({ - partitions, - underReplicatedPartitions, - inSyncReplicas, - replicas, - partitionCount, - internal, - replicationFactor, - segmentSize, - segmentCount, - clusterName, - topicName, - cleanUpPolicy, - clearTopicMessages, -}) => { +const Overview: React.FC = ({ clearTopicMessages }) => { + const { clusterName, topicName } = useAppParams(); + + const { + partitions, + underReplicatedPartitions, + inSyncReplicas, + replicas, + partitionCount, + internal, + replicationFactor, + segmentSize, + segmentCount, + cleanUpPolicy, + } = useAppSelector((state) => { + const res = getTopicByName(state, topicName); + return res || {}; + }); + const { isReadOnly } = React.useContext(ClusterContext); const messageCount = React.useMemo( diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/OverviewContainer.ts b/kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/OverviewContainer.ts index d5f9011484..f32a602804 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/OverviewContainer.ts +++ b/kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/OverviewContainer.ts @@ -1,34 +1,9 @@ import { connect } from 'react-redux'; -import { RootState, TopicName, ClusterName } from 'redux/interfaces'; -import { getTopicByName } from 'redux/reducers/topics/selectors'; -import { withRouter, RouteComponentProps } from 'react-router-dom'; import { clearTopicMessages } from 'redux/reducers/topicMessages/topicMessagesSlice'; import Overview from 'components/Topics/Topic/Details/Overview/Overview'; -interface RouteProps { - clusterName: ClusterName; - topicName: TopicName; -} - -type OwnProps = RouteComponentProps; - -const mapStateToProps = ( - state: RootState, - { - match: { - params: { topicName, clusterName }, - }, - }: OwnProps -) => ({ - ...getTopicByName(state, topicName), - topicName, - clusterName, -}); - const mapDispatchToProps = { clearTopicMessages, }; -export default withRouter( - connect(mapStateToProps, mapDispatchToProps)(Overview) -); +export default connect(null, mapDispatchToProps)(Overview); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/__test__/Overview.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/__test__/Overview.spec.tsx index 0d3fb36368..bb134d912b 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/__test__/Overview.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/__test__/Overview.spec.tsx @@ -1,20 +1,22 @@ import React from 'react'; import { screen } from '@testing-library/react'; -import { render } from 'lib/testHelpers'; +import { render, WithRoute } from 'lib/testHelpers'; import Overview, { Props as OverviewProps, } from 'components/Topics/Topic/Details/Overview/Overview'; import theme from 'theme/theme'; -import { CleanUpPolicy } from 'generated-sources'; +import { CleanUpPolicy, Topic } from 'generated-sources'; import ClusterContext from 'components/contexts/ClusterContext'; import userEvent from '@testing-library/user-event'; +import { getTopicStateFixtures } from 'redux/reducers/topics/__test__/fixtures'; +import { clusterTopicPath } from 'lib/paths'; import { ReplicaCell } from 'components/Topics/Topic/Details/Details.styled'; describe('Overview', () => { - const getReplicaCell = () => screen.getByLabelText('replica-info'); const mockClusterName = 'local'; const mockTopicName = 'topic'; - const mockClearTopicMessages = jest.fn(); + const mockTopic = { name: mockTopicName }; + const mockPartitions = [ { partition: 1, @@ -36,67 +38,63 @@ describe('Overview', () => { hasSchemaRegistryConfigured: true, isTopicDeletionAllowed: true, }; - const defaultProps: OverviewProps = { - name: mockTopicName, - partitions: [], - internal: true, - clusterName: mockClusterName, - topicName: mockTopicName, - clearTopicMessages: mockClearTopicMessages, - }; const setupComponent = ( - props = defaultProps, - contextValues = defaultContextValues, - underReplicatedPartitions?: number, - inSyncReplicas?: number, - replicas?: number + props: Partial = {}, + topicState: Topic = mockTopic, + contextValues = defaultContextValues ) => { + const topics = getTopicStateFixtures([topicState]); + return render( - - - + + + + + , + { + initialEntries: [clusterTopicPath(mockClusterName, mockTopicName)], + preloadedState: { topics }, + } ); }; - afterEach(() => { - mockClearTopicMessages.mockClear(); - }); - it('at least one replica was rendered', () => { - setupComponent({ - ...defaultProps, - underReplicatedPartitions: 0, - inSyncReplicas: 1, - replicas: 1, - }); - expect(getReplicaCell()).toBeInTheDocument(); + setupComponent( + {}, + { + ...mockTopic, + partitions: mockPartitions, + internal: false, + cleanUpPolicy: CleanUpPolicy.DELETE, + } + ); + expect(screen.getByLabelText('replica-info')).toBeInTheDocument(); }); it('renders replica cell with props', () => { render(); - expect(getReplicaCell()).toBeInTheDocument(); - expect(getReplicaCell()).toHaveStyleRule('color', 'orange'); + const element = screen.getByLabelText('replica-info'); + expect(element).toBeInTheDocument(); + expect(element).toHaveStyleRule('color', 'orange'); }); describe('when it has internal flag', () => { it('does not render the Action button a Topic', () => { - setupComponent({ - ...defaultProps, - partitions: mockPartitions, - internal: false, - cleanUpPolicy: CleanUpPolicy.DELETE, - }); + setupComponent( + {}, + { + ...mockTopic, + partitions: mockPartitions, + internal: false, + cleanUpPolicy: CleanUpPolicy.DELETE, + } + ); expect(screen.getAllByRole('menu')[0]).toBeInTheDocument(); }); it('does not render Partitions', () => { - setupComponent(); + setupComponent({}, { ...mockTopic, partitions: [] }); expect(screen.getByText('No Partitions found')).toBeInTheDocument(); }); @@ -110,12 +108,15 @@ describe('Overview', () => { }); it('should be the appropriate color', () => { - setupComponent({ - ...defaultProps, - underReplicatedPartitions: 0, - inSyncReplicas: 1, - replicas: 2, - }); + setupComponent( + {}, + { + ...mockTopic, + underReplicatedPartitions: 0, + inSyncReplicas: 1, + replicas: 2, + } + ); const circles = screen.getAllByRole('circle'); expect(circles[0]).toHaveStyle( `fill: ${theme.circularAlert.color.success}` @@ -127,24 +128,30 @@ describe('Overview', () => { }); describe('when Clear Messages is clicked', () => { - setupComponent({ - ...defaultProps, - partitions: mockPartitions, - internal: false, - cleanUpPolicy: CleanUpPolicy.DELETE, + it('should when Clear Messages is clicked', () => { + const mockClearTopicMessages = jest.fn(); + setupComponent( + { clearTopicMessages: mockClearTopicMessages }, + { + ...mockTopic, + partitions: mockPartitions, + internal: false, + cleanUpPolicy: CleanUpPolicy.DELETE, + } + ); + + const clearMessagesButton = screen.getByText('Clear Messages'); + userEvent.click(clearMessagesButton); + expect(mockClearTopicMessages).toHaveBeenCalledTimes(1); }); - - const clearMessagesButton = screen.getByText('Clear Messages'); - userEvent.click(clearMessagesButton); - - expect(mockClearTopicMessages).toHaveBeenCalledTimes(1); }); describe('when the table partition dropdown appearance', () => { it('should check if the dropdown is not present when it is readOnly', () => { setupComponent( + {}, { - ...defaultProps, + ...mockTopic, partitions: mockPartitions, internal: true, cleanUpPolicy: CleanUpPolicy.DELETE, @@ -155,32 +162,41 @@ describe('Overview', () => { }); it('should check if the dropdown is not present when it is internal', () => { - setupComponent({ - ...defaultProps, - partitions: mockPartitions, - internal: true, - cleanUpPolicy: CleanUpPolicy.DELETE, - }); + setupComponent( + {}, + { + ...mockTopic, + partitions: mockPartitions, + internal: true, + cleanUpPolicy: CleanUpPolicy.DELETE, + } + ); expect(screen.queryByText('Clear Messages')).not.toBeInTheDocument(); }); it('should check if the dropdown is not present when cleanUpPolicy is not DELETE', () => { - setupComponent({ - ...defaultProps, - partitions: mockPartitions, - internal: false, - cleanUpPolicy: CleanUpPolicy.COMPACT, - }); + setupComponent( + {}, + { + ...mockTopic, + partitions: mockPartitions, + internal: false, + cleanUpPolicy: CleanUpPolicy.COMPACT, + } + ); expect(screen.queryByText('Clear Messages')).not.toBeInTheDocument(); }); it('should check if the dropdown action to be in visible', () => { - setupComponent({ - ...defaultProps, - partitions: mockPartitions, - internal: false, - cleanUpPolicy: CleanUpPolicy.DELETE, - }); + setupComponent( + {}, + { + ...mockTopic, + partitions: mockPartitions, + internal: false, + cleanUpPolicy: CleanUpPolicy.DELETE, + } + ); expect(screen.getByText('Clear Messages')).toBeInTheDocument(); }); }); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Details/Settings/Settings.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Details/Settings/Settings.tsx index 3521e37bf4..880097fb55 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Details/Settings/Settings.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Details/Settings/Settings.tsx @@ -1,16 +1,16 @@ +import React from 'react'; import PageLoader from 'components/common/PageLoader/PageLoader'; import { Table } from 'components/common/table/Table/Table.styled'; import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell'; -import { TopicConfig } from 'generated-sources'; -import React from 'react'; import { ClusterName, TopicName } from 'redux/interfaces'; +import { useAppSelector } from 'lib/hooks/redux'; +import { getTopicConfig } from 'redux/reducers/topics/selectors'; +import { RouteParamsClusterTopic } from 'lib/paths'; +import useAppParams from 'lib/hooks/useAppParams'; import ConfigListItem from './ConfigListItem'; -interface Props { - clusterName: ClusterName; - topicName: TopicName; - config?: TopicConfig[]; +export interface Props { isFetched: boolean; fetchTopicConfig: (payload: { clusterName: ClusterName; @@ -18,13 +18,11 @@ interface Props { }) => void; } -const Settings: React.FC = ({ - clusterName, - topicName, - isFetched, - fetchTopicConfig, - config, -}) => { +const Settings: React.FC = ({ isFetched, fetchTopicConfig }) => { + const { clusterName, topicName } = useAppParams(); + + const config = useAppSelector((state) => getTopicConfig(state, topicName)); + React.useEffect(() => { fetchTopicConfig({ clusterName, topicName }); }, [fetchTopicConfig, clusterName, topicName]); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Details/Settings/SettingsContainer.ts b/kafka-ui-react-app/src/components/Topics/Topic/Details/Settings/SettingsContainer.ts index b38031a443..c24895c080 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Details/Settings/SettingsContainer.ts +++ b/kafka-ui-react-app/src/components/Topics/Topic/Details/Settings/SettingsContainer.ts @@ -1,32 +1,11 @@ import { connect } from 'react-redux'; -import { RootState, ClusterName, TopicName } from 'redux/interfaces'; -import { withRouter, RouteComponentProps } from 'react-router-dom'; +import { RootState } from 'redux/interfaces'; import { fetchTopicConfig } from 'redux/reducers/topics/topicsSlice'; -import { - getTopicConfig, - getTopicConfigFetched, -} from 'redux/reducers/topics/selectors'; +import { getTopicConfigFetched } from 'redux/reducers/topics/selectors'; import Settings from './Settings'; -interface RouteProps { - clusterName: ClusterName; - topicName: TopicName; -} - -type OwnProps = RouteComponentProps; - -const mapStateToProps = ( - state: RootState, - { - match: { - params: { topicName, clusterName }, - }, - }: OwnProps -) => ({ - clusterName, - topicName, - config: getTopicConfig(state, topicName), +const mapStateToProps = (state: RootState) => ({ isFetched: getTopicConfigFetched(state), }); @@ -34,6 +13,4 @@ const mapDispatchToProps = { fetchTopicConfig, }; -export default withRouter( - connect(mapStateToProps, mapDispatchToProps)(Settings) -); +export default connect(mapStateToProps, mapDispatchToProps)(Settings); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Details/Settings/__test__/Settings.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Details/Settings/__test__/Settings.spec.tsx index 23a39b50c6..c2d31a0a79 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Details/Settings/__test__/Settings.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Details/Settings/__test__/Settings.spec.tsx @@ -1,14 +1,20 @@ import React from 'react'; -import { render } from 'lib/testHelpers'; +import { render, WithRoute } from 'lib/testHelpers'; import { screen } from '@testing-library/react'; -import Settings from 'components/Topics/Topic/Details/Settings/Settings'; +import Settings, { + Props, +} from 'components/Topics/Topic/Details/Settings/Settings'; import { TopicConfig } from 'generated-sources'; +import { clusterTopicSettingsPath } from 'lib/paths'; +import { getTopicStateFixtures } from 'redux/reducers/topics/__test__/fixtures'; describe('Settings', () => { + const mockClusterName = 'Cluster_Name'; + const mockTopicName = 'Topic_Name'; + let expectedResult: number; const mockFn = jest.fn(); - const mockClusterName = 'Cluster Name'; - const mockTopicName = 'Topic Name'; + const mockConfig: TopicConfig[] = [ { name: 'first', @@ -20,43 +26,50 @@ describe('Settings', () => { }, ]; - it('should check it returns null if no config is passed', () => { - render( - + const setUpComponent = ( + props: Partial = {}, + config?: TopicConfig[] + ) => { + const topic = { + name: mockTopicName, + config, + }; + const topics = getTopicStateFixtures([topic]); + + return render( + + + , + { + initialEntries: [ + clusterTopicSettingsPath(mockClusterName, mockTopicName), + ], + preloadedState: { + topics, + }, + } ); + }; + + afterEach(() => { + mockFn.mockClear(); + }); + + it('should check it returns null if no config is passed', () => { + setUpComponent(); expect(screen.queryByRole('table')).not.toBeInTheDocument(); }); it('should show Page loader when it is in fetching state and config is given', () => { - render( - - ); + setUpComponent({ isFetched: false }, mockConfig); expect(screen.queryByRole('table')).not.toBeInTheDocument(); expect(screen.getByRole('progressbar')).toBeInTheDocument(); }); it('should check and return null if it is not fetched and config is not given', () => { - render( - - ); + setUpComponent({ isFetched: false }); expect(screen.queryByRole('table')).not.toBeInTheDocument(); }); @@ -64,15 +77,7 @@ describe('Settings', () => { describe('Settings Component with Data', () => { beforeEach(() => { expectedResult = mockConfig.length + 1; // include the header table row as well - render( - - ); + setUpComponent({ isFetched: true }, mockConfig); }); it('should view the correct number of table row with header included elements after config fetching', () => { diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Details/__test__/Details.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Details/__test__/Details.spec.tsx index 267fdd6c37..fb81c426a6 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Details/__test__/Details.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Details/__test__/Details.spec.tsx @@ -3,36 +3,35 @@ import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import ClusterContext from 'components/contexts/ClusterContext'; import Details from 'components/Topics/Topic/Details/Details'; -import { internalTopicPayload } from 'redux/reducers/topics/__test__/fixtures'; -import { render } from 'lib/testHelpers'; import { - clusterTopicEditPath, - clusterTopicPath, - clusterTopicsPath, -} from 'lib/paths'; -import { Router } from 'react-router-dom'; -import { createMemoryHistory } from 'history'; + getTopicStateFixtures, + internalTopicPayload, +} from 'redux/reducers/topics/__test__/fixtures'; +import { render, WithRoute } from 'lib/testHelpers'; +import { clusterTopicEditRelativePath, clusterTopicPath } from 'lib/paths'; +import { CleanUpPolicy, Topic } from 'generated-sources'; + +const mockNavigate = jest.fn(); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => mockNavigate, +})); describe('Details', () => { const mockDelete = jest.fn(); const mockClusterName = 'local'; const mockClearTopicMessages = jest.fn(); - const mockInternalTopicPayload = internalTopicPayload.internal; const mockRecreateTopic = jest.fn(); - const defaultPathname = clusterTopicPath( - mockClusterName, - internalTopicPayload.name - ); - const mockHistory = createMemoryHistory({ - initialEntries: [defaultPathname], - }); - jest.spyOn(mockHistory, 'push'); - const setupComponent = ( - pathname = defaultPathname, - history = mockHistory, - props = {} - ) => + const topic: Topic = { + ...internalTopicPayload, + cleanUpPolicy: CleanUpPolicy.DELETE, + internal: false, + }; + + const mockTopicsState = getTopicStateFixtures([topic]); + + const setupComponent = (props = {}) => render( { isTopicDeletionAllowed: true, }} > - +
- + , - { pathname } + { + initialEntries: [ + clusterTopicPath(mockClusterName, internalTopicPayload.name), + ], + preloadedState: { + topics: mockTopicsState, + }, + } ); + afterEach(() => { + mockNavigate.mockClear(); + mockDelete.mockClear(); + mockClearTopicMessages.mockClear(); + mockRecreateTopic.mockClear(); + }); + describe('when it has readonly flag', () => { it('does not render the Action button a Topic', () => { render( @@ -72,15 +80,10 @@ describe('Details', () => { }} >
); @@ -148,30 +151,23 @@ describe('Details', () => { const button = screen.getAllByText('Edit settings')[0]; userEvent.click(button); - const redirectRoute = clusterTopicEditPath( - mockClusterName, - internalTopicPayload.name - ); - - expect(mockHistory.push).toHaveBeenCalledWith(redirectRoute); + expect(mockNavigate).toHaveBeenCalledWith(clusterTopicEditRelativePath); }); }); it('redirects to the correct route if topic is deleted', () => { - setupComponent(defaultPathname, mockHistory, { isDeleted: true }); - const redirectRoute = clusterTopicsPath(mockClusterName); + setupComponent({ isDeleted: true }); - expect(mockHistory.push).toHaveBeenCalledWith(redirectRoute); + expect(mockNavigate).toHaveBeenCalledWith('../..'); }); it('shows a confirmation popup on deleting topic messages', () => { setupComponent(); - const { getByText } = screen; - const clearMessagesButton = getByText(/Clear messages/i); + const clearMessagesButton = screen.getAllByText(/Clear messages/i)[0]; userEvent.click(clearMessagesButton); expect( - getByText(/Are you sure want to clear topic messages?/i) + screen.getByText(/Are you sure want to clear topic messages?/i) ).toBeInTheDocument(); }); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/DangerZone.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/DangerZone.tsx index d2baede5f0..3f7576d9f6 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/DangerZone.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/DangerZone.tsx @@ -6,13 +6,13 @@ import { FormError } from 'components/common/Input/Input.styled'; import { InputLabel } from 'components/common/Input/InputLabel.styled'; import React from 'react'; import { FormProvider, useForm } from 'react-hook-form'; +import { RouteParamsClusterTopic } from 'lib/paths'; import { ClusterName, TopicName } from 'redux/interfaces'; +import useAppParams from 'lib/hooks/useAppParams'; import * as S from './DangerZone.styled'; export interface Props { - clusterName: string; - topicName: string; defaultPartitions: number; defaultReplicationFactor: number; partitionsCountIncreased: boolean; @@ -30,8 +30,6 @@ export interface Props { } const DangerZone: React.FC = ({ - clusterName, - topicName, defaultPartitions, defaultReplicationFactor, partitionsCountIncreased, @@ -39,6 +37,8 @@ const DangerZone: React.FC = ({ updateTopicPartitionsCount, updateTopicReplicationFactor, }) => { + const { clusterName, topicName } = useAppParams(); + const [isPartitionsConfirmationVisible, setIsPartitionsConfirmationVisible] = React.useState(false); const [ diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/DangerZoneContainer.ts b/kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/DangerZoneContainer.ts index 5697369330..722f5144db 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/DangerZoneContainer.ts +++ b/kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/DangerZoneContainer.ts @@ -1,6 +1,5 @@ import { connect } from 'react-redux'; -import { RootState, ClusterName, TopicName } from 'redux/interfaces'; -import { withRouter, RouteComponentProps } from 'react-router-dom'; +import { RootState } from 'redux/interfaces'; import { updateTopicPartitionsCount, updateTopicReplicationFactor, @@ -12,11 +11,6 @@ import { import DangerZone from './DangerZone'; -interface RouteProps { - clusterName: ClusterName; - topicName: TopicName; -} - type OwnProps = { defaultPartitions: number; defaultReplicationFactor: number; @@ -24,16 +18,8 @@ type OwnProps = { const mapStateToProps = ( state: RootState, - { - match: { - params: { topicName, clusterName }, - }, - defaultPartitions, - defaultReplicationFactor, - }: OwnProps & RouteComponentProps + { defaultPartitions, defaultReplicationFactor }: OwnProps ) => ({ - clusterName, - topicName, defaultPartitions, defaultReplicationFactor, partitionsCountIncreased: getTopicPartitionsCountIncreased(state), @@ -45,6 +31,4 @@ const mapDispatchToProps = { updateTopicReplicationFactor, }; -export default withRouter( - connect(mapStateToProps, mapDispatchToProps)(DangerZone) -); +export default connect(mapStateToProps, mapDispatchToProps)(DangerZone); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/__test__/DangerZone.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/__test__/DangerZone.spec.tsx index 80f4802bdc..2099f63305 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/__test__/DangerZone.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/__test__/DangerZone.spec.tsx @@ -4,28 +4,30 @@ import DangerZone, { } from 'components/Topics/Topic/Edit/DangerZone/DangerZone'; import { act, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { render } from 'lib/testHelpers'; +import { render, WithRoute } from 'lib/testHelpers'; import { topicName, clusterName, } from 'components/Topics/Topic/Edit/__test__/fixtures'; +import { clusterTopicSendMessagePath } from 'lib/paths'; const defaultPartitions = 3; const defaultReplicationFactor = 3; const renderComponent = (props?: Partial) => render( - + + + , + { initialEntries: [clusterTopicSendMessagePath(clusterName, topicName)] } ); const clickOnDialogSubmitButton = () => { @@ -199,8 +201,6 @@ describe('DangerZone', () => { await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument()); rerender( { await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument()); rerender( void; - updateTopicPartitionsCount: (payload: { - clusterName: string; - topicname: string; - partitions: number; - }) => void; } const EditWrapperStyled = styled.div` @@ -83,22 +78,24 @@ const topicParams = (topic: TopicWithDetailedInfo | undefined) => { let formInit = false; const Edit: React.FC = ({ - clusterName, - topicName, - topic, isFetched, isTopicUpdated, fetchTopicConfig, updateTopic, }) => { + const { clusterName, topicName } = useAppParams(); + + const topic = useAppSelector((state) => getFullTopic(state, topicName)); + const defaultValues = React.useMemo(() => topicParams(topic), [topic]); + const methods = useForm({ defaultValues, resolver: yupResolver(topicFormValidationSchema), }); const [isSubmitting, setIsSubmitting] = React.useState(false); - const history = useHistory(); + const navigate = useNavigate(); React.useEffect(() => { fetchTopicConfig({ clusterName, topicName }); @@ -106,10 +103,9 @@ const Edit: React.FC = ({ React.useEffect(() => { if (isSubmitting && isTopicUpdated) { - const { name } = methods.getValues(); - history.push(clusterTopicPath(clusterName, name)); + navigate('../'); } - }, [isSubmitting, isTopicUpdated, clusterName, methods, history]); + }, [isSubmitting, isTopicUpdated, clusterName, navigate]); if (!isFetched || !topic || !topic.config) { return null; diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Edit/EditContainer.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Edit/EditContainer.tsx index b02f6d7548..5ff9dcc675 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Edit/EditContainer.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Edit/EditContainer.tsx @@ -1,6 +1,5 @@ import { connect } from 'react-redux'; -import { RootState, ClusterName, TopicName } from 'redux/interfaces'; -import { withRouter, RouteComponentProps } from 'react-router-dom'; +import { RootState } from 'redux/interfaces'; import { updateTopic, fetchTopicConfig, @@ -8,29 +7,11 @@ import { import { getTopicConfigFetched, getTopicUpdated, - getFullTopic, } from 'redux/reducers/topics/selectors'; import Edit from './Edit'; -interface RouteProps { - clusterName: ClusterName; - topicName: TopicName; -} - -type OwnProps = RouteComponentProps; - -const mapStateToProps = ( - state: RootState, - { - match: { - params: { topicName, clusterName }, - }, - }: OwnProps -) => ({ - clusterName, - topicName, - topic: getFullTopic(state, topicName), +const mapStateToProps = (state: RootState) => ({ isFetched: getTopicConfigFetched(state), isTopicUpdated: getTopicUpdated(state), }); @@ -40,4 +21,4 @@ const mapDispatchToProps = { updateTopic, }; -export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Edit)); +export default connect(mapStateToProps, mapDispatchToProps)(Edit); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Edit/__test__/Edit.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Edit/__test__/Edit.spec.tsx index 676160ef4b..b658ac8019 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Edit/__test__/Edit.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Edit/__test__/Edit.spec.tsx @@ -1,38 +1,52 @@ import React from 'react'; import Edit, { DEFAULTS, Props } from 'components/Topics/Topic/Edit/Edit'; import { act, screen } from '@testing-library/react'; -import { render } from 'lib/testHelpers'; +import { render, WithRoute } from 'lib/testHelpers'; import userEvent from '@testing-library/user-event'; -import { Router } from 'react-router-dom'; -import { createMemoryHistory } from 'history'; -import { clusterTopicPath, clusterTopicsPath } from 'lib/paths'; +import { clusterTopicEditPath } from 'lib/paths'; +import { TopicsState, TopicWithDetailedInfo } from 'redux/interfaces'; +import { getTopicStateFixtures } from 'redux/reducers/topics/__test__/fixtures'; import { topicName, clusterName, topicWithInfo } from './fixtures'; -const historyMock = createMemoryHistory(); +const mockNavigate = jest.fn(); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => mockNavigate, +})); -const renderComponent = (props: Partial = {}, history = historyMock) => - render( - +const renderComponent = ( + props: Partial = {}, + topic: TopicWithDetailedInfo | null = topicWithInfo +) => { + let topics: TopicsState | undefined; + + if (topic === null) { + topics = undefined; + } else { + topics = getTopicStateFixtures([topic]); + } + + return render( + - + , + { + initialEntries: [clusterTopicEditPath(clusterName, topicName)], + preloadedState: { topics }, + } ); +}; describe('Edit Component', () => { + afterEach(() => {}); + it('renders the Edit Component', () => { renderComponent(); @@ -45,7 +59,7 @@ describe('Edit Component', () => { }); it('should check Edit component renders null is not rendered when topic is not passed', () => { - renderComponent({ topic: undefined }); + renderComponent({}, { ...topicWithInfo, config: undefined }); expect( screen.queryByRole('heading', { name: `Edit ${topicName}` }) ).not.toBeInTheDocument(); @@ -67,7 +81,7 @@ describe('Edit Component', () => { it('should check Edit component renders null is not topic config is not passed is false', () => { const modifiedTopic = { ...topicWithInfo }; modifiedTopic.config = undefined; - renderComponent({ topic: modifiedTopic }); + renderComponent({}, modifiedTopic); expect( screen.queryByRole('heading', { name: `Edit ${topicName}` }) ).not.toBeInTheDocument(); @@ -77,19 +91,19 @@ describe('Edit Component', () => { }); describe('Edit Component with its topic default and modified values', () => { - it('should check the default partitions value in the DangerZone', () => { - renderComponent({ - topic: { ...topicWithInfo, partitionCount: undefined }, - }); - expect(screen.getByPlaceholderText('Number of partitions')).toHaveValue( - DEFAULTS.partitions - ); + it('should check the default partitions value in the DangerZone', async () => { + renderComponent({}, { ...topicWithInfo, partitionCount: 0 }); + // cause topic selector will return falsy + expect( + screen.queryByRole('heading', { name: `Edit ${topicName}` }) + ).not.toBeInTheDocument(); + expect( + screen.queryByRole('heading', { name: `Danger Zone` }) + ).not.toBeInTheDocument(); }); - it('should check the default partitions value in the DangerZone', () => { - renderComponent({ - topic: { ...topicWithInfo, replicationFactor: undefined }, - }); + it('should check the default partitions value in the DangerZone', async () => { + renderComponent({}, { ...topicWithInfo, replicationFactor: undefined }); expect(screen.getByPlaceholderText('Replication Factor')).toHaveValue( DEFAULTS.replicationFactor ); @@ -99,12 +113,8 @@ describe('Edit Component', () => { describe('Submit Case of the Edit Component', () => { it('should check the submit functionality when topic updated is false', async () => { const updateTopicMock = jest.fn(); - const mocked = createMemoryHistory({ - initialEntries: [`${clusterTopicsPath(clusterName)}/${topicName}/edit`], - }); - jest.spyOn(mocked, 'push'); - renderComponent({ updateTopic: updateTopicMock }, mocked); + renderComponent({ updateTopic: updateTopicMock }, undefined); const btn = screen.getAllByText(/submit/i)[0]; expect(btn).toBeEnabled(); @@ -117,18 +127,15 @@ describe('Edit Component', () => { userEvent.click(btn); }); expect(updateTopicMock).toHaveBeenCalledTimes(1); - expect(mocked.push).not.toHaveBeenCalled(); + expect(mockNavigate).not.toHaveBeenCalled(); }); it('should check the submit functionality when topic updated is true', async () => { const updateTopicMock = jest.fn(); - const mocked = createMemoryHistory({ - initialEntries: [`${clusterTopicsPath(clusterName)}/${topicName}/edit`], - }); - jest.spyOn(mocked, 'push'); + renderComponent( { updateTopic: updateTopicMock, isTopicUpdated: true }, - mocked + undefined ); const btn = screen.getAllByText(/submit/i)[0]; @@ -141,10 +148,7 @@ describe('Edit Component', () => { userEvent.click(btn); }); expect(updateTopicMock).toHaveBeenCalledTimes(1); - expect(mocked.push).toHaveBeenCalled(); - expect(mocked.location.pathname).toBe( - clusterTopicPath(clusterName, topicName) - ); + expect(mockNavigate).toHaveBeenLastCalledWith('../'); }); }); }); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.tsx b/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.tsx index 35894e9bfb..2c96fd69ff 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.tsx @@ -1,9 +1,10 @@ -import Editor from 'components/common/Editor/Editor'; -import PageLoader from 'components/common/PageLoader/PageLoader'; import React, { useEffect } from 'react'; import { useForm, Controller } from 'react-hook-form'; -import { useHistory, useParams } from 'react-router-dom'; -import { clusterTopicMessagesPath } from 'lib/paths'; +import { useNavigate } from 'react-router-dom'; +import { + clusterTopicMessagesRelativePath, + RouteParamsClusterTopic, +} from 'lib/paths'; import jsf from 'json-schema-faker'; import { messagesApiClient } from 'redux/reducers/topicMessages/topicMessagesSlice'; import { @@ -14,25 +15,22 @@ import { useAppDispatch, useAppSelector } from 'lib/hooks/redux'; import { alertAdded } from 'redux/reducers/alerts/alertsSlice'; import { now } from 'lodash'; import { Button } from 'components/common/Button/Button'; -import { ClusterName, TopicName } from 'redux/interfaces'; +import Editor from 'components/common/Editor/Editor'; +import PageLoader from 'components/common/PageLoader/PageLoader'; import { getMessageSchemaByTopicName, getPartitionsByTopicName, getTopicMessageSchemaFetched, } from 'redux/reducers/topics/selectors'; +import useAppParams from 'lib/hooks/useAppParams'; import validateMessage from './validateMessage'; import * as S from './SendMessage.styled'; -interface RouterParams { - clusterName: ClusterName; - topicName: TopicName; -} - const SendMessage: React.FC = () => { const dispatch = useAppDispatch(); - const { clusterName, topicName } = useParams(); - const history = useHistory(); + const { clusterName, topicName } = useAppParams(); + const navigate = useNavigate(); jsf.option('fillProperties', false); jsf.option('alwaysFakeOptionals', true); @@ -147,7 +145,7 @@ const SendMessage: React.FC = () => { }) ); } - history.push(clusterTopicMessagesPath(clusterName, topicName)); + navigate(`../${clusterTopicMessagesRelativePath}`); } }; diff --git a/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/__test__/SendMessage.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/__test__/SendMessage.spec.tsx index 0e1425d885..ba844c7eca 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/__test__/SendMessage.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/__test__/SendMessage.spec.tsx @@ -3,11 +3,9 @@ import SendMessage from 'components/Topics/Topic/SendMessage/SendMessage'; import { act, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import fetchMock from 'fetch-mock'; -import { createMemoryHistory } from 'history'; -import { render } from 'lib/testHelpers'; -import { Route, Router } from 'react-router-dom'; +import { render, WithRoute } from 'lib/testHelpers'; import { - clusterTopicMessagesPath, + clusterTopicMessagesRelativePath, clusterTopicSendMessagePath, } from 'lib/paths'; import { store } from 'redux/store'; @@ -34,27 +32,30 @@ jest.mock('components/Topics/Topic/SendMessage/validateMessage', () => jest.fn() ); +const mockNavigate = jest.fn(); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => mockNavigate, +})); + const clusterName = 'testCluster'; const topicName = externalTopicPayload.name; -const history = createMemoryHistory(); const renderComponent = async () => { - history.push(clusterTopicSendMessagePath(clusterName, topicName)); await act(() => { render( <> - - - - - + + + , - { store } + { + initialEntries: [clusterTopicSendMessagePath(clusterName, topicName)], + store, + } ); }); }; @@ -87,6 +88,7 @@ describe('SendMessage', () => { }); afterEach(() => { fetchMock.reset(); + mockNavigate.mockClear(); }); it('fetches schema on first render', async () => { @@ -114,8 +116,8 @@ describe('SendMessage', () => { const sendTopicMessageMock = fetchMock.postOnce(url, 200); await renderAndSubmitData(); expect(sendTopicMessageMock.called(url)).toBeTruthy(); - expect(history.location.pathname).toEqual( - clusterTopicMessagesPath(clusterName, topicName) + expect(mockNavigate).toHaveBeenLastCalledWith( + `../${clusterTopicMessagesRelativePath}` ); }); @@ -126,8 +128,8 @@ describe('SendMessage', () => { await renderAndSubmitData(); expect(sendTopicMessageMock.called(url)).toBeTruthy(); expect(screen.getByRole('alert')).toBeInTheDocument(); - expect(history.location.pathname).toEqual( - clusterTopicMessagesPath(clusterName, topicName) + expect(mockNavigate).toHaveBeenLastCalledWith( + `../${clusterTopicMessagesRelativePath}` ); }); @@ -135,9 +137,7 @@ describe('SendMessage', () => { const sendTopicMessageMock = fetchMock.postOnce(url, 200); await renderAndSubmitData(['error']); expect(sendTopicMessageMock.called(url)).toBeFalsy(); - expect(history.location.pathname).not.toEqual( - clusterTopicMessagesPath(clusterName, topicName) - ); + expect(mockNavigate).not.toHaveBeenCalled(); }); }); }); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Topic.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Topic.tsx index 88256a0f40..14fb518a75 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Topic.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Topic.tsx @@ -1,17 +1,18 @@ import React from 'react'; -import { Switch, Route, useParams } from 'react-router-dom'; +import { Routes, Route } from 'react-router-dom'; import { ClusterName, TopicName } from 'redux/interfaces'; import EditContainer from 'components/Topics/Topic/Edit/EditContainer'; import DetailsContainer from 'components/Topics/Topic/Details/DetailsContainer'; import PageLoader from 'components/common/PageLoader/PageLoader'; +import { + clusterTopicEditRelativePath, + clusterTopicSendMessageRelativePath, + RouteParamsClusterTopic, +} from 'lib/paths'; +import useAppParams from 'lib/hooks/useAppParams'; import SendMessage from './SendMessage/SendMessage'; -interface RouterParams { - clusterName: ClusterName; - topicName: TopicName; -} - interface TopicProps { isTopicFetching: boolean; resetTopicMessages: () => void; @@ -26,7 +27,7 @@ const Topic: React.FC = ({ fetchTopicDetails, resetTopicMessages, }) => { - const { clusterName, topicName } = useParams(); + const { clusterName, topicName } = useAppParams(); React.useEffect(() => { fetchTopicDetails({ clusterName, topicName }); @@ -43,22 +44,14 @@ const Topic: React.FC = ({ } return ( - + + } /> + } /> } /> - - - + ); }; diff --git a/kafka-ui-react-app/src/components/Topics/Topic/__tests__/Topic.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/__tests__/Topic.spec.tsx index 97ff07b9f3..84880fe3e0 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/__tests__/Topic.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/__tests__/Topic.spec.tsx @@ -1,12 +1,12 @@ import React from 'react'; -import { Route } from 'react-router-dom'; -import { render } from 'lib/testHelpers'; +import { render, WithRoute } from 'lib/testHelpers'; import { screen } from '@testing-library/react'; import Topic from 'components/Topics/Topic/Topic'; import { clusterTopicPath, clusterTopicEditPath, clusterTopicSendMessagePath, + getNonExactPath, } from 'lib/paths'; const topicText = { @@ -35,14 +35,14 @@ describe('Topic Component', () => { const renderComponent = (pathname: string, topicFetching: boolean) => render( - + - , - { pathname } + , + { initialEntries: [pathname] } ); afterEach(() => { diff --git a/kafka-ui-react-app/src/components/Topics/Topics.tsx b/kafka-ui-react-app/src/components/Topics/Topics.tsx index 391645a32b..6e1b7e2104 100644 --- a/kafka-ui-react-app/src/components/Topics/Topics.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topics.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import { Switch } from 'react-router-dom'; +import { Route, Routes } from 'react-router-dom'; import { - clusterTopicCopyPath, - clusterTopicNewPath, - clusterTopicPath, - clusterTopicsPath, + clusterTopicCopyRelativePath, + clusterTopicNewRelativePath, + getNonExactPath, + RouteParams, } from 'lib/paths'; import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route'; @@ -13,27 +13,40 @@ import TopicContainer from './Topic/TopicContainer'; import New from './New/New'; const Topics: React.FC = () => ( - - + + + + } /> - + + + } /> - + + + } /> - + + + } /> - + ); export default Topics; diff --git a/kafka-ui-react-app/src/components/Topics/__tests__/Topics.spec.tsx b/kafka-ui-react-app/src/components/Topics/__tests__/Topics.spec.tsx index e76230bef5..b699c5ff09 100644 --- a/kafka-ui-react-app/src/components/Topics/__tests__/Topics.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/__tests__/Topics.spec.tsx @@ -1,14 +1,13 @@ import React from 'react'; -import { render } from 'lib/testHelpers'; +import { render, WithRoute } from 'lib/testHelpers'; import Topics from 'components/Topics/Topics'; -import { Router } from 'react-router-dom'; -import { createMemoryHistory } from 'history'; import { screen } from '@testing-library/react'; import { clusterTopicCopyPath, clusterTopicNewPath, clusterTopicPath, clusterTopicsPath, + getNonExactPath, } from 'lib/paths'; const listContainer = 'listContainer'; @@ -29,13 +28,11 @@ describe('Topics Component', () => { const clusterName = 'clusterName'; const topicName = 'topicName'; const setUpComponent = (path: string) => { - const history = createMemoryHistory({ - initialEntries: [path], - }); return render( - + - + , + { initialEntries: [path] } ); }; diff --git a/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParamsContainer.tsx b/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParamsContainer.tsx deleted file mode 100644 index 5806d11e92..0000000000 --- a/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParamsContainer.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { connect } from 'react-redux'; -import { RootState, TopicConfigByName } from 'redux/interfaces'; -import { withRouter, RouteComponentProps } from 'react-router-dom'; - -import CustomParams from './CustomParams'; - -interface OwnProps extends RouteComponentProps { - isSubmitting: boolean; - config?: TopicConfigByName; -} - -const mapStateToProps = ( - _state: RootState, - { isSubmitting, config }: OwnProps -) => ({ - isSubmitting, - config, -}); - -export default withRouter(connect(mapStateToProps)(CustomParams)); diff --git a/kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.tsx b/kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.tsx index 9c74749108..bd0fb16beb 100644 --- a/kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.tsx +++ b/kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.tsx @@ -10,7 +10,7 @@ import { InputLabel } from 'components/common/Input/InputLabel.styled'; import { FormError } from 'components/common/Input/Input.styled'; import { StyledForm } from 'components/common/Form/Form.styled'; -import CustomParamsContainer from './CustomParams/CustomParamsContainer'; +import CustomParams from './CustomParams/CustomParams'; import TimeToRetain from './TimeToRetain'; import * as S from './TopicForm.styled'; @@ -207,7 +207,7 @@ const TopicForm: React.FC = ({ Custom parameters - +