react router migration (#2045)

* remove withRouter HOC from FiltersContainer

* remove withRouter HOC from Topics DetailsContainer

* remove withRouter HOC from Topics TopicsConsumerGroupsContainer

* withRouter HOC from Topics TopicsConsumerGroupsContainer

* minor code refactor in the Details spec

* Routes code modifications to refactor strings representation to functions

* Settings and TopicsConsumer removal of HOC with Router

* Remove withRouter HOC from Overview file

* Remove withRouter HOC from Edit file

* replace Router path with functions instead of strings

* delete CustomParamsContainer and use the simple component in the TopicForm

* remove HOC from DangerZone container

* Remove withRouter HOC from Connect pages like Config , Overview , Tasks

* Remove withRouter HOC from Connect pages like Actions, Details, Edit, New

* Refactor Kafka Connect Codes

* Refactor Topics pages

* Remove HOC from Diff component and minor code refactor

* Route component migration into children instead of renderProps or component param in App Component

* Route component migration into children instead of renderProps or component param in Cluster Component

* Route component migration into children instead of renderProps or component param in Topics Component

* Route component migration into children instead of renderProps or component param in Topic Component

* Route component migration into children instead of renderProps or component param in Topic Component

* minor bug fix in the Overview selector spread

* change Router from component Render to child render
in ConsumerGroups page

* change Router from component Render to child render
in Schemas page

* change Router from component Render to child render
in KsqlDb page

* change Router from component Render to child render
in Connect page

* change Router from component Render to child render
in Connect Details page

* Overview Details styling code modifications

* All written path to paths with functions

* Route Parameters code fix with functions and params with variables

* Updating BreadCrumb Route

* Refactor Redirects

* WIP React Router v6 migration

* Remove unused imports from the file

* Make KsqlDb pages work with relative Routes

* WIP Make Connect pages work and fix the Schema page testing problem

* transforming consumer groups into relative path router

* Transform Topics pages into relative routes

* Transform Topic pages into relative routes

* Minor changes in Connect and KsqlDb test suites relative routes

* Minor changes in Connect and KsqlDb test suites relative routes

* change the Details into relative Routes

* Topics List naviagtion and caching issue fixed in tests suites

* Topic New Naviagation issue fix + tests suites

* Details navigate migrating into relative paths

* Send Message Submit Naviagttion with tests suites

* Topic Edit pages with working routes navigation

* Topic Details and ResetOffsets Pages tests suites and navigations

* Messages Table Tests suites

* BreadCrumbs Routes fixes

* ClusterMenu and Links styling minor code modifications

* ClusterMenu and Links styling minor code modifications

* Minor Code modifications

* Fix Lintter Problems

* fix Code Smells

* create custom useParams hook

* Adding Path tests

* minor code refactors

* Fix the Button Component redundant Props + transforming routes to relative

* Fix linter issues
This commit is contained in:
Mgrdich 2022-05-31 13:33:15 +04:00 committed by GitHub
parent 2a51f0ee14
commit 71ac16357b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
122 changed files with 2084 additions and 1977 deletions

View file

@ -36,7 +36,7 @@
"react-multi-select-component": "^4.0.6", "react-multi-select-component": "^4.0.6",
"react-redux": "^7.2.6", "react-redux": "^7.2.6",
"react-router": "^5.2.0", "react-router": "^5.2.0",
"react-router-dom": "^5.3.1", "react-router-dom": "^6.3.0",
"redux": "^4.1.1", "redux": "^4.1.1",
"redux-thunk": "^2.3.0", "redux-thunk": "^2.3.0",
"sass": "^1.43.4", "sass": "^1.43.4",
@ -16158,7 +16158,6 @@
"version": "5.3.0", "version": "5.3.0",
"resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz",
"integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==",
"dev": true,
"dependencies": { "dependencies": {
"@babel/runtime": "^7.7.6" "@babel/runtime": "^7.7.6"
} }
@ -24856,11 +24855,11 @@
} }
}, },
"node_modules/react-router": { "node_modules/react-router": {
"version": "5.2.0", "version": "5.3.3",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.3.tgz",
"integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==", "integrity": "sha512-mzQGUvS3bM84TnbtMYR8ZjKnuPJ71IjSzR+DE6UkUqvN4czWIqEs17yLL8xkAycv4ev0AiN+IGrWu88vJs/p2w==",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.1.2", "@babel/runtime": "^7.12.13",
"history": "^4.9.0", "history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0", "hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1", "loose-envify": "^1.3.1",
@ -24876,53 +24875,27 @@
} }
}, },
"node_modules/react-router-dom": { "node_modules/react-router-dom": {
"version": "5.3.1", "version": "6.3.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.1.tgz", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz",
"integrity": "sha512-f0pj/gMAbv9e8gahTmCEY20oFhxhrmHwYeIwH5EO5xu0qme+wXtsdB8YfUOAZzUz4VaXmb58m3ceiLtjMhqYmQ==", "integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.12.13", "history": "^5.2.0",
"history": "^4.9.0", "react-router": "6.3.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"
}, },
"peerDependencies": { "peerDependencies": {
"react": ">=15" "react": ">=16.8",
} "react-dom": ">=16.8"
},
"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"
} }
}, },
"node_modules/react-router-dom/node_modules/react-router": { "node_modules/react-router-dom/node_modules/react-router": {
"version": "5.3.1", "version": "6.3.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.1.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz",
"integrity": "sha512-v+zwjqb7bakqgF+wMVKlAPTca/cEmPOvQ9zt7gpSNyPXau1+0qvuYZ5BWzzNDP1y6s15zDwgb9rPN63+SIniRQ==", "integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.12.13", "history": "^5.2.0"
"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"
}, },
"peerDependencies": { "peerDependencies": {
"react": ">=15" "react": ">=16.8"
} }
}, },
"node_modules/react-router/node_modules/history": { "node_modules/react-router/node_modules/history": {
@ -40798,7 +40771,6 @@
"version": "5.3.0", "version": "5.3.0",
"resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz",
"integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==",
"dev": true,
"requires": { "requires": {
"@babel/runtime": "^7.7.6" "@babel/runtime": "^7.7.6"
} }
@ -47310,11 +47282,11 @@
"dev": true "dev": true
}, },
"react-router": { "react-router": {
"version": "5.2.0", "version": "5.3.3",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.3.tgz",
"integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==", "integrity": "sha512-mzQGUvS3bM84TnbtMYR8ZjKnuPJ71IjSzR+DE6UkUqvN4czWIqEs17yLL8xkAycv4ev0AiN+IGrWu88vJs/p2w==",
"requires": { "requires": {
"@babel/runtime": "^7.1.2", "@babel/runtime": "^7.12.13",
"history": "^4.9.0", "history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0", "hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1", "loose-envify": "^1.3.1",
@ -47342,47 +47314,20 @@
} }
}, },
"react-router-dom": { "react-router-dom": {
"version": "5.3.1", "version": "6.3.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.1.tgz", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz",
"integrity": "sha512-f0pj/gMAbv9e8gahTmCEY20oFhxhrmHwYeIwH5EO5xu0qme+wXtsdB8YfUOAZzUz4VaXmb58m3ceiLtjMhqYmQ==", "integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==",
"requires": { "requires": {
"@babel/runtime": "^7.12.13", "history": "^5.2.0",
"history": "^4.9.0", "react-router": "6.3.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"
}, },
"dependencies": { "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": { "react-router": {
"version": "5.3.1", "version": "6.3.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.1.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz",
"integrity": "sha512-v+zwjqb7bakqgF+wMVKlAPTca/cEmPOvQ9zt7gpSNyPXau1+0qvuYZ5BWzzNDP1y6s15zDwgb9rPN63+SIniRQ==", "integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==",
"requires": { "requires": {
"@babel/runtime": "^7.12.13", "history": "^5.2.0"
"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"
} }
} }
} }

View file

@ -32,7 +32,7 @@
"react-multi-select-component": "^4.0.6", "react-multi-select-component": "^4.0.6",
"react-redux": "^7.2.6", "react-redux": "^7.2.6",
"react-router": "^5.2.0", "react-router": "^5.2.0",
"react-router-dom": "^5.3.1", "react-router-dom": "^6.3.0",
"redux": "^4.1.1", "redux": "^4.1.1",
"redux-thunk": "^2.3.0", "redux-thunk": "^2.3.0",
"sass": "^1.43.4", "sass": "^1.43.4",

View file

@ -1,6 +1,7 @@
import React from 'react'; 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 { GIT_TAG, GIT_COMMIT } from 'lib/constants';
import { clusterPath, getNonExactPath } from 'lib/paths';
import Nav from 'components/Nav/Nav'; import Nav from 'components/Nav/Nav';
import PageLoader from 'components/common/PageLoader/PageLoader'; import PageLoader from 'components/common/PageLoader/PageLoader';
import Dashboard from 'components/Dashboard/Dashboard'; import Dashboard from 'components/Dashboard/Dashboard';
@ -81,14 +82,19 @@ const App: React.FC = () => {
aria-label="Overlay" aria-label="Overlay"
/> />
{areClustersFulfilled ? ( {areClustersFulfilled ? (
<Switch> <Routes>
{['/', '/ui', '/ui/clusters'].map((path) => (
<Route
key="Home" // optional: avoid full re-renders on route changes
path={path}
element={<Dashboard />}
/>
))}
<Route <Route
exact path={getNonExactPath(clusterPath())}
path={['/', '/ui', '/ui/clusters']} element={<ClusterPage />}
component={Dashboard}
/> />
<Route path="/ui/clusters/:clusterName" component={ClusterPage} /> </Routes>
</Switch>
) : ( ) : (
<PageLoader /> <PageLoader />
)} )}

View file

@ -1,22 +1,22 @@
import React from 'react'; import React from 'react';
import { ClusterName } from 'redux/interfaces';
import useInterval from 'lib/hooks/useInterval'; import useInterval from 'lib/hooks/useInterval';
import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted'; import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';
import { useParams } from 'react-router-dom';
import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell'; import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
import { Table } from 'components/common/table/Table/Table.styled'; import { Table } from 'components/common/table/Table/Table.styled';
import PageHeading from 'components/common/PageHeading/PageHeading'; import PageHeading from 'components/common/PageHeading/PageHeading';
import * as Metrics from 'components/common/Metrics'; import * as Metrics from 'components/common/Metrics';
import { useAppDispatch, useAppSelector } from 'lib/hooks/redux'; import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
import { ClusterNameRoute } from 'lib/paths';
import { import {
fetchBrokers, fetchBrokers,
fetchClusterStats, fetchClusterStats,
selectStats, selectStats,
} from 'redux/reducers/brokers/brokersSlice'; } from 'redux/reducers/brokers/brokersSlice';
import useAppParams from 'lib/hooks/useAppParams';
const Brokers: React.FC = () => { const Brokers: React.FC = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { clusterName } = useParams<{ clusterName: ClusterName }>(); const { clusterName } = useAppParams<ClusterNameRoute>();
const { const {
brokerCount, brokerCount,
activeControllers, activeControllers,

View file

@ -1,8 +1,7 @@
import React from 'react'; import React from 'react';
import Brokers from 'components/Brokers/Brokers'; 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 { screen, waitFor } from '@testing-library/dom';
import { Route } from 'react-router-dom';
import { clusterBrokersPath } from 'lib/paths'; import { clusterBrokersPath } from 'lib/paths';
import fetchMock from 'fetch-mock'; import fetchMock from 'fetch-mock';
import { clusterStatsPayload } from 'redux/reducers/brokers/__test__/fixtures'; import { clusterStatsPayload } from 'redux/reducers/brokers/__test__/fixtures';
@ -18,11 +17,11 @@ describe('Brokers Component', () => {
const renderComponent = () => const renderComponent = () =>
render( render(
<Route path={clusterBrokersPath(':clusterName')}> <WithRoute path={clusterBrokersPath()}>
<Brokers /> <Brokers />
</Route>, </WithRoute>,
{ {
pathname: clusterBrokersPath(clusterName), initialEntries: [clusterBrokersPath(clusterName)],
} }
); );

View file

@ -1,19 +1,22 @@
import React from 'react'; import React from 'react';
import { useSelector } from 'react-redux'; 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 { ClusterFeaturesEnum } from 'generated-sources';
import { import {
getClustersFeatures, getClustersFeatures,
getClustersReadonlyStatus, getClustersReadonlyStatus,
} from 'redux/reducers/clusters/clustersSlice'; } from 'redux/reducers/clusters/clustersSlice';
import { import {
clusterBrokersPath, clusterBrokerRelativePath,
clusterConnectorsPath, clusterConnectorsRelativePath,
clusterConnectsPath, clusterConnectsRelativePath,
clusterConsumerGroupsPath, clusterConsumerGroupsRelativePath,
clusterKsqlDbPath, clusterKsqlDbRelativePath,
clusterSchemasPath, ClusterNameRoute,
clusterTopicsPath, clusterSchemasRelativePath,
clusterTopicsRelativePath,
getNonExactPath,
} from 'lib/paths'; } from 'lib/paths';
import Topics from 'components/Topics/Topics'; import Topics from 'components/Topics/Topics';
import Schemas from 'components/Schemas/Schemas'; 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'; import { BreadcrumbProvider } from 'components/common/Breadcrumb/Breadcrumb.provider';
const Cluster: React.FC = () => { const Cluster: React.FC = () => {
const { clusterName } = useParams<{ clusterName: string }>(); const { clusterName } = useAppParams<ClusterNameRoute>();
const isReadOnly = useSelector(getClustersReadonlyStatus(clusterName)); const isReadOnly = useSelector(getClustersReadonlyStatus(clusterName));
const features = useSelector(getClustersFeatures(clusterName)); const features = useSelector(getClustersFeatures(clusterName));
@ -61,48 +64,77 @@ const Cluster: React.FC = () => {
<BreadcrumbProvider> <BreadcrumbProvider>
<Breadcrumb /> <Breadcrumb />
<ClusterContext.Provider value={contextValue}> <ClusterContext.Provider value={contextValue}>
<Switch> <Routes>
<BreadcrumbRoute <Route
path={clusterBrokersPath(':clusterName')} path={getNonExactPath(clusterBrokerRelativePath)}
component={Brokers} element={
<BreadcrumbRoute>
<Brokers />
</BreadcrumbRoute>
}
/> />
<BreadcrumbRoute <Route
path={clusterTopicsPath(':clusterName')} path={getNonExactPath(clusterTopicsRelativePath)}
component={Topics} element={
<BreadcrumbRoute>
<Topics />
</BreadcrumbRoute>
}
/> />
<BreadcrumbRoute <Route
path={clusterConsumerGroupsPath(':clusterName')} path={getNonExactPath(clusterConsumerGroupsRelativePath)}
component={ConsumersGroups} element={
<BreadcrumbRoute>
<ConsumersGroups />
</BreadcrumbRoute>
}
/> />
{hasSchemaRegistryConfigured && ( {hasSchemaRegistryConfigured && (
<BreadcrumbRoute <Route
path={clusterSchemasPath(':clusterName')} path={getNonExactPath(clusterSchemasRelativePath)}
component={Schemas} element={
<BreadcrumbRoute>
<Schemas />
</BreadcrumbRoute>
}
/> />
)} )}
{hasKafkaConnectConfigured && ( {hasKafkaConnectConfigured && (
<BreadcrumbRoute <Route
path={clusterConnectsPath(':clusterName')} path={getNonExactPath(clusterConnectsRelativePath)}
component={Connect} element={
<BreadcrumbRoute>
<Connect />
</BreadcrumbRoute>
}
/> />
)} )}
{hasKafkaConnectConfigured && ( {hasKafkaConnectConfigured && (
<BreadcrumbRoute <Route
path={clusterConnectorsPath(':clusterName')} path={getNonExactPath(clusterConnectorsRelativePath)}
component={Connect} element={
<BreadcrumbRoute>
<Connect />
</BreadcrumbRoute>
}
/> />
)} )}
{hasKsqlDbConfigured && ( {hasKsqlDbConfigured && (
<BreadcrumbRoute <Route
path={clusterKsqlDbPath(':clusterName')} path={getNonExactPath(clusterKsqlDbRelativePath)}
component={KsqlDb} element={
<BreadcrumbRoute>
<KsqlDb />
</BreadcrumbRoute>
}
/> />
)} )}
<Redirect <Route
from="/ui/clusters/:clusterName" path="/"
to="/ui/clusters/:clusterName/brokers" element={<Navigate to={clusterBrokerRelativePath} replace />}
/> />
</Switch> </Routes>
<Outlet />
</ClusterContext.Provider> </ClusterContext.Provider>
</BreadcrumbProvider> </BreadcrumbProvider>
); );

View file

@ -1,51 +1,71 @@
import React from 'react'; import React from 'react';
import { Route } from 'react-router-dom';
import { ClusterFeaturesEnum } from 'generated-sources'; import { ClusterFeaturesEnum } from 'generated-sources';
import { store } from 'redux/store'; import { store } from 'redux/store';
import { onlineClusterPayload } from 'redux/reducers/clusters/__test__/fixtures'; import { onlineClusterPayload } from 'redux/reducers/clusters/__test__/fixtures';
import Cluster from 'components/Cluster/Cluster'; import Cluster from 'components/Cluster/Cluster';
import { fetchClusters } from 'redux/reducers/clusters/clustersSlice'; import { fetchClusters } from 'redux/reducers/clusters/clustersSlice';
import { screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import { render } from 'lib/testHelpers'; import { render, WithRoute } from 'lib/testHelpers';
import { import {
clusterBrokersPath, clusterBrokersPath,
clusterConnectsPath, clusterConnectsPath,
clusterConsumerGroupsPath, clusterConsumerGroupsPath,
clusterKsqlDbPath, clusterKsqlDbPath,
clusterPath,
clusterSchemasPath, clusterSchemasPath,
clusterTopicsPath, clusterTopicsPath,
} from 'lib/paths'; } from 'lib/paths';
jest.mock('components/Topics/Topics', () => () => <div>Topics</div>); const CLusterCompText = {
jest.mock('components/Schemas/Schemas', () => () => <div>Schemas</div>); Topics: 'Topics',
jest.mock('components/Connect/Connect', () => () => <div>Connect</div>); Schemas: 'Schemas',
jest.mock('components/Connect/Connect', () => () => <div>Connect</div>); Connect: 'Connect',
jest.mock('components/Brokers/Brokers', () => () => <div>Brokers</div>); Brokers: 'Brokers',
jest.mock('components/ConsumerGroups/ConsumerGroups', () => () => ( ConsumerGroups: 'ConsumerGroups',
<div>ConsumerGroups</div> KsqlDb: 'KsqlDb',
};
jest.mock('components/Topics/Topics', () => () => (
<div>{CLusterCompText.Topics}</div>
));
jest.mock('components/Schemas/Schemas', () => () => (
<div>{CLusterCompText.Schemas}</div>
));
jest.mock('components/Connect/Connect', () => () => (
<div>{CLusterCompText.Connect}</div>
));
jest.mock('components/Brokers/Brokers', () => () => (
<div>{CLusterCompText.Brokers}</div>
));
jest.mock('components/ConsumerGroups/ConsumerGroups', () => () => (
<div>{CLusterCompText.ConsumerGroups}</div>
));
jest.mock('components/KsqlDb/KsqlDb', () => () => (
<div>{CLusterCompText.KsqlDb}</div>
)); ));
jest.mock('components/KsqlDb/KsqlDb', () => () => <div>KsqlDb</div>);
describe('Cluster', () => { describe('Cluster', () => {
const renderComponent = (pathname: string) => const renderComponent = (pathname: string) =>
render( render(
<Route path="/ui/clusters/:clusterName"> <WithRoute path={`${clusterPath()}/*`}>
<Cluster /> <Cluster />
</Route>, </WithRoute>,
{ pathname, store } { initialEntries: [pathname], store }
); );
it('renders Brokers', () => { it('renders Brokers', () => {
renderComponent(clusterBrokersPath('second')); renderComponent(clusterBrokersPath('second'));
expect(screen.getByText('Brokers')).toBeInTheDocument(); expect(screen.getByText(CLusterCompText.Brokers)).toBeInTheDocument();
}); });
it('renders Topics', () => { it('renders Topics', () => {
renderComponent(clusterTopicsPath('second')); renderComponent(clusterTopicsPath('second'));
expect(screen.getByText('Topics')).toBeInTheDocument(); expect(screen.getByText(CLusterCompText.Topics)).toBeInTheDocument();
}); });
it('renders ConsumerGroups', () => { it('renders ConsumerGroups', () => {
renderComponent(clusterConsumerGroupsPath('second')); renderComponent(clusterConsumerGroupsPath('second'));
expect(screen.getByText('ConsumerGroups')).toBeInTheDocument(); expect(
screen.getByText(CLusterCompText.ConsumerGroups)
).toBeInTheDocument();
}); });
describe('configured features', () => { describe('configured features', () => {
@ -62,7 +82,9 @@ describe('Cluster', () => {
) )
); );
renderComponent(clusterSchemasPath('second')); 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 () => { it('renders Schemas if SCHEMA_REGISTRY is configured', async () => {
store.dispatch( store.dispatch(
@ -77,7 +99,7 @@ describe('Cluster', () => {
) )
); );
renderComponent(clusterSchemasPath(onlineClusterPayload.name)); renderComponent(clusterSchemasPath(onlineClusterPayload.name));
expect(screen.getByText('Schemas')).toBeInTheDocument(); expect(screen.getByText(CLusterCompText.Schemas)).toBeInTheDocument();
}); });
it('renders Connect if KAFKA_CONNECT is configured', async () => { it('renders Connect if KAFKA_CONNECT is configured', async () => {
store.dispatch( store.dispatch(
@ -92,7 +114,7 @@ describe('Cluster', () => {
) )
); );
renderComponent(clusterConnectsPath(onlineClusterPayload.name)); renderComponent(clusterConnectsPath(onlineClusterPayload.name));
expect(screen.getByText('Connect')).toBeInTheDocument(); expect(screen.getByText(CLusterCompText.Connect)).toBeInTheDocument();
}); });
it('renders KSQL if KSQL_DB is configured', async () => { it('renders KSQL if KSQL_DB is configured', async () => {
store.dispatch( store.dispatch(
@ -107,7 +129,7 @@ describe('Cluster', () => {
) )
); );
renderComponent(clusterKsqlDbPath(onlineClusterPayload.name)); renderComponent(clusterKsqlDbPath(onlineClusterPayload.name));
expect(screen.getByText('KsqlDb')).toBeInTheDocument(); expect(screen.getByText(CLusterCompText.KsqlDb)).toBeInTheDocument();
}); });
}); });
}); });

View file

@ -1,12 +1,12 @@
import React from 'react'; import React from 'react';
import { Switch, Redirect } from 'react-router-dom'; import { Navigate, Routes, Route } from 'react-router-dom';
import { import {
clusterConnectorsPath, RouteParams,
clusterConnectsPath, clusterConnectConnectorEditRelativePath,
clusterConnectorNewPath, clusterConnectConnectorRelativePath,
clusterConnectConnectorPath, clusterConnectConnectorsRelativePath,
clusterConnectConnectorEditPath, clusterConnectorNewRelativePath,
clusterConnectConnectorsPath, getNonExactPath,
} from 'lib/paths'; } from 'lib/paths';
import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route'; import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route';
@ -16,45 +16,48 @@ import DetailsContainer from './Details/DetailsContainer';
import EditContainer from './Edit/EditContainer'; import EditContainer from './Edit/EditContainer';
const Connect: React.FC = () => ( const Connect: React.FC = () => (
<div> <Routes>
<Switch> <Route
<BreadcrumbRoute index
exact element={
path={clusterConnectorsPath(':clusterName')} <BreadcrumbRoute>
component={ListContainer} <ListContainer />
/> </BreadcrumbRoute>
<BreadcrumbRoute }
exact />
path={clusterConnectorNewPath(':clusterName')} <Route
component={NewContainer} path={clusterConnectorNewRelativePath}
/> element={
<BreadcrumbRoute <BreadcrumbRoute>
exact <NewContainer />
path={clusterConnectConnectorEditPath( </BreadcrumbRoute>
':clusterName', }
':connectName', />
':connectorName' <Route
)} path={clusterConnectConnectorEditRelativePath}
component={EditContainer} element={
/> <BreadcrumbRoute>
<BreadcrumbRoute <EditContainer />
path={clusterConnectConnectorPath( </BreadcrumbRoute>
':clusterName', }
':connectName', />
':connectorName' <Route
)} path={getNonExactPath(clusterConnectConnectorRelativePath)}
component={DetailsContainer} element={
/> <BreadcrumbRoute>
<Redirect <DetailsContainer />
from={clusterConnectConnectorsPath(':clusterName', ':connectName')} </BreadcrumbRoute>
to={clusterConnectorsPath(':clusterName')} }
/> />
<Redirect <Route
from={`${clusterConnectsPath(':clusterName')}/:connectName`} path={clusterConnectConnectorsRelativePath}
to={clusterConnectorsPath(':clusterName')} element={<Navigate to="/" replace />}
/> />
</Switch> <Route
</div> path={RouteParams.connectName}
element={<Navigate to="/" replace />}
/>
</Routes>
); );
export default Connect; export default Connect;

View file

@ -1,21 +1,17 @@
import React from 'react'; 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 { ConnectorState, ConnectorAction } from 'generated-sources';
import { ClusterName, ConnectName, ConnectorName } from 'redux/interfaces'; import { ClusterName, ConnectName, ConnectorName } from 'redux/interfaces';
import { import {
clusterConnectConnectorEditPath, clusterConnectConnectorEditPath,
clusterConnectorsPath, clusterConnectorsPath,
RouterParamsClusterConnectConnector,
} from 'lib/paths'; } from 'lib/paths';
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal'; import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
import styled from 'styled-components'; import styled from 'styled-components';
import { Button } from 'components/common/Button/Button'; import { Button } from 'components/common/Button/Button';
interface RouterParams {
clusterName: ClusterName;
connectName: ConnectName;
connectorName: ConnectorName;
}
const ConnectorActionsWrapperStyled = styled.div` const ConnectorActionsWrapperStyled = styled.div`
display: flex; display: flex;
gap: 8px; gap: 8px;
@ -63,9 +59,11 @@ const Actions: React.FC<ActionsProps> = ({
resumeConnector, resumeConnector,
isConnectorActionRunning, isConnectorActionRunning,
}) => { }) => {
const { clusterName, connectName, connectorName } = useParams<RouterParams>(); const { clusterName, connectName, connectorName } =
useAppParams<RouterParamsClusterConnectConnector>();
const navigate = useNavigate();
const history = useHistory();
const [ const [
isDeleteConnectorConfirmationVisible, isDeleteConnectorConfirmationVisible,
setIsDeleteConnectorConfirmationVisible, setIsDeleteConnectorConfirmationVisible,
@ -74,7 +72,7 @@ const Actions: React.FC<ActionsProps> = ({
const deleteConnectorHandler = async () => { const deleteConnectorHandler = async () => {
try { try {
await deleteConnector({ clusterName, connectName, connectorName }); await deleteConnector({ clusterName, connectName, connectorName });
history.push(clusterConnectorsPath(clusterName)); navigate(clusterConnectorsPath(clusterName));
} catch { } catch {
// do not redirect // do not redirect
} }
@ -175,7 +173,6 @@ const Actions: React.FC<ActionsProps> = ({
buttonSize="M" buttonSize="M"
buttonType="primary" buttonType="primary"
type="button" type="button"
isLink
disabled={isConnectorActionRunning} disabled={isConnectorActionRunning}
to={clusterConnectConnectorEditPath( to={clusterConnectConnectorEditPath(
clusterName, clusterName,

View file

@ -1,5 +1,4 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { RootState } from 'redux/interfaces'; import { RootState } from 'redux/interfaces';
import { import {
deleteConnector, deleteConnector,
@ -30,6 +29,4 @@ const mapDispatchToProps = {
resumeConnector, resumeConnector,
}; };
export default withRouter( export default connect(mapStateToProps, mapDispatchToProps)(Actions);
connect(mapStateToProps, mapDispatchToProps)(Actions)
);

View file

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { Route } from 'react-router-dom'; import { render, WithRoute } from 'lib/testHelpers';
import { render } from 'lib/testHelpers';
import { clusterConnectConnectorPath, clusterConnectorsPath } from 'lib/paths'; import { clusterConnectConnectorPath, clusterConnectorsPath } from 'lib/paths';
import ActionsContainer from 'components/Connect/Details/Actions/ActionsContainer'; import ActionsContainer from 'components/Connect/Details/Actions/ActionsContainer';
import Actions, { import Actions, {
@ -19,9 +18,7 @@ const cancelMock = jest.fn();
jest.mock('react-router-dom', () => ({ jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'), ...jest.requireActual('react-router-dom'),
useHistory: () => ({ useNavigate: () => mockHistoryPush,
push: mockHistoryPush,
}),
})); }));
jest.mock( jest.mock(
@ -38,6 +35,12 @@ const expectActionButtonsExists = () => {
}; };
describe('Actions', () => { describe('Actions', () => {
afterEach(() => {
mockHistoryPush.mockClear();
deleteConnector.mockClear();
cancelMock.mockClear();
});
const actionsContainer = (props: Partial<ActionsProps> = {}) => ( const actionsContainer = (props: Partial<ActionsProps> = {}) => (
<ActionsContainer> <ActionsContainer>
<Actions <Actions
@ -60,17 +63,13 @@ describe('Actions', () => {
}); });
describe('view', () => { describe('view', () => {
const pathname = clusterConnectConnectorPath( const pathname = clusterConnectConnectorPath();
':clusterName',
':connectName',
':connectorName'
);
const clusterName = 'my-cluster'; const clusterName = 'my-cluster';
const connectName = 'my-connect'; const connectName = 'my-connect';
const connectorName = 'my-connector'; const connectorName = 'my-connector';
const confirmationModal = (props: Partial<ConfirmationModalProps> = {}) => ( const confirmationModal = (props: Partial<ConfirmationModalProps> = {}) => (
<Route path={pathname}> <WithRoute path={pathname}>
<ConfirmationModal <ConfirmationModal
onCancel={cancelMock} onCancel={cancelMock}
onConfirm={() => onConfirm={() =>
@ -91,11 +90,11 @@ describe('Actions', () => {
Confirm Confirm
</button> </button>
</ConfirmationModal> </ConfirmationModal>
</Route> </WithRoute>
); );
const component = (props: Partial<ActionsProps> = {}) => ( const component = (props: Partial<ActionsProps> = {}) => (
<Route path={pathname}> <WithRoute path={pathname}>
<Actions <Actions
deleteConnector={jest.fn()} deleteConnector={jest.fn()}
isConnectorDeleting={false} isConnectorDeleting={false}
@ -107,16 +106,14 @@ describe('Actions', () => {
isConnectorActionRunning={false} isConnectorActionRunning={false}
{...props} {...props}
/> />
</Route> </WithRoute>
); );
it('renders buttons when paused', () => { it('renders buttons when paused', () => {
render(component({ connectorStatus: ConnectorState.PAUSED }), { render(component({ connectorStatus: ConnectorState.PAUSED }), {
pathname: clusterConnectConnectorPath( initialEntries: [
clusterName, clusterConnectConnectorPath(clusterName, connectName, connectorName),
connectName, ],
connectorName
),
}); });
expect(screen.getAllByRole('button').length).toEqual(6); expect(screen.getAllByRole('button').length).toEqual(6);
expect(screen.getByText('Resume')).toBeInTheDocument(); expect(screen.getByText('Resume')).toBeInTheDocument();
@ -127,11 +124,9 @@ describe('Actions', () => {
it('renders buttons when failed', () => { it('renders buttons when failed', () => {
render(component({ connectorStatus: ConnectorState.FAILED }), { render(component({ connectorStatus: ConnectorState.FAILED }), {
pathname: clusterConnectConnectorPath( initialEntries: [
clusterName, clusterConnectConnectorPath(clusterName, connectName, connectorName),
connectName, ],
connectorName
),
}); });
expect(screen.getAllByRole('button').length).toEqual(5); expect(screen.getAllByRole('button').length).toEqual(5);
@ -143,11 +138,9 @@ describe('Actions', () => {
it('renders buttons when unassigned', () => { it('renders buttons when unassigned', () => {
render(component({ connectorStatus: ConnectorState.UNASSIGNED }), { render(component({ connectorStatus: ConnectorState.UNASSIGNED }), {
pathname: clusterConnectConnectorPath( initialEntries: [
clusterName, clusterConnectConnectorPath(clusterName, connectName, connectorName),
connectName, ],
connectorName
),
}); });
expect(screen.getAllByRole('button').length).toEqual(5); expect(screen.getAllByRole('button').length).toEqual(5);
expect(screen.queryByText('Resume')).not.toBeInTheDocument(); expect(screen.queryByText('Resume')).not.toBeInTheDocument();
@ -157,11 +150,9 @@ describe('Actions', () => {
it('renders buttons when running connector action', () => { it('renders buttons when running connector action', () => {
render(component({ connectorStatus: ConnectorState.RUNNING }), { render(component({ connectorStatus: ConnectorState.RUNNING }), {
pathname: clusterConnectConnectorPath( initialEntries: [
clusterName, clusterConnectConnectorPath(clusterName, connectName, connectorName),
connectName, ],
connectorName
),
}); });
expect(screen.getAllByRole('button').length).toEqual(6); expect(screen.getAllByRole('button').length).toEqual(6);
expect(screen.queryByText('Resume')).not.toBeInTheDocument(); expect(screen.queryByText('Resume')).not.toBeInTheDocument();
@ -172,11 +163,9 @@ describe('Actions', () => {
it('opens confirmation modal when delete button clicked', () => { it('opens confirmation modal when delete button clicked', () => {
render(component({ deleteConnector }), { render(component({ deleteConnector }), {
pathname: clusterConnectConnectorPath( initialEntries: [
clusterName, clusterConnectConnectorPath(clusterName, connectName, connectorName),
connectName, ],
connectorName
),
}); });
userEvent.click(screen.getByRole('button', { name: 'Delete' })); userEvent.click(screen.getByRole('button', { name: 'Delete' }));
@ -187,11 +176,9 @@ describe('Actions', () => {
it('closes when cancel button clicked', () => { it('closes when cancel button clicked', () => {
render(confirmationModal({ isOpen: true }), { render(confirmationModal({ isOpen: true }), {
pathname: clusterConnectConnectorPath( initialEntries: [
clusterName, clusterConnectConnectorPath(clusterName, connectName, connectorName),
connectName, ],
connectorName
),
}); });
const cancelBtn = screen.getByRole('button', { name: 'Cancel' }); const cancelBtn = screen.getByRole('button', { name: 'Cancel' });
userEvent.click(cancelBtn); userEvent.click(cancelBtn);
@ -200,11 +187,9 @@ describe('Actions', () => {
it('calls deleteConnector when confirm button clicked', () => { it('calls deleteConnector when confirm button clicked', () => {
render(confirmationModal({ isOpen: true }), { render(confirmationModal({ isOpen: true }), {
pathname: clusterConnectConnectorPath( initialEntries: [
clusterName, clusterConnectConnectorPath(clusterName, connectName, connectorName),
connectName, ],
connectorName
),
}); });
const confirmBtn = screen.getByRole('button', { name: 'Confirm' }); const confirmBtn = screen.getByRole('button', { name: 'Confirm' });
userEvent.click(confirmBtn); userEvent.click(confirmBtn);
@ -218,11 +203,9 @@ describe('Actions', () => {
it('redirects after delete', async () => { it('redirects after delete', async () => {
render(confirmationModal({ isOpen: true }), { render(confirmationModal({ isOpen: true }), {
pathname: clusterConnectConnectorPath( initialEntries: [
clusterName, clusterConnectConnectorPath(clusterName, connectName, connectorName),
connectName, ],
connectorName
),
}); });
const confirmBtn = screen.getByRole('button', { name: 'Confirm' }); const confirmBtn = screen.getByRole('button', { name: 'Confirm' });
userEvent.click(confirmBtn); userEvent.click(confirmBtn);
@ -235,11 +218,9 @@ describe('Actions', () => {
it('calls restartConnector when restart button clicked', () => { it('calls restartConnector when restart button clicked', () => {
const restartConnector = jest.fn(); const restartConnector = jest.fn();
render(component({ restartConnector }), { render(component({ restartConnector }), {
pathname: clusterConnectConnectorPath( initialEntries: [
clusterName, clusterConnectConnectorPath(clusterName, connectName, connectorName),
connectName, ],
connectorName
),
}); });
userEvent.click( userEvent.click(
screen.getByRole('button', { name: 'Restart Connector' }) screen.getByRole('button', { name: 'Restart Connector' })
@ -260,11 +241,13 @@ describe('Actions', () => {
pauseConnector, pauseConnector,
}), }),
{ {
pathname: clusterConnectConnectorPath( initialEntries: [
clusterName, clusterConnectConnectorPath(
connectName, clusterName,
connectorName connectName,
), connectorName
),
],
} }
); );
userEvent.click(screen.getByRole('button', { name: 'Pause' })); userEvent.click(screen.getByRole('button', { name: 'Pause' }));
@ -284,11 +267,13 @@ describe('Actions', () => {
resumeConnector, resumeConnector,
}), }),
{ {
pathname: clusterConnectConnectorPath( initialEntries: [
clusterName, clusterConnectConnectorPath(
connectName, clusterName,
connectorName connectName,
), connectorName
),
],
} }
); );
userEvent.click(screen.getByRole('button', { name: 'Resume' })); userEvent.click(screen.getByRole('button', { name: 'Resume' }));

View file

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { useParams } from 'react-router-dom'; import useAppParams from 'lib/hooks/useAppParams';
import { import {
ClusterName, ClusterName,
ConnectName, ConnectName,
@ -9,12 +9,7 @@ import {
import PageLoader from 'components/common/PageLoader/PageLoader'; import PageLoader from 'components/common/PageLoader/PageLoader';
import Editor from 'components/common/Editor/Editor'; import Editor from 'components/common/Editor/Editor';
import styled from 'styled-components'; import styled from 'styled-components';
import { RouterParamsClusterConnectConnector } from 'lib/paths';
interface RouterParams {
clusterName: ClusterName;
connectName: ConnectName;
connectorName: ConnectorName;
}
export interface ConfigProps { export interface ConfigProps {
fetchConfig(payload: { fetchConfig(payload: {
@ -35,7 +30,8 @@ const Config: React.FC<ConfigProps> = ({
isConfigFetching, isConfigFetching,
config, config,
}) => { }) => {
const { clusterName, connectName, connectorName } = useParams<RouterParams>(); const { clusterName, connectName, connectorName } =
useAppParams<RouterParamsClusterConnectConnector>();
React.useEffect(() => { React.useEffect(() => {
fetchConfig({ clusterName, connectName, connectorName }); fetchConfig({ clusterName, connectName, connectorName });

View file

@ -1,5 +1,4 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { RootState } from 'redux/interfaces'; import { RootState } from 'redux/interfaces';
import { fetchConnectorConfig } from 'redux/reducers/connect/connectSlice'; import { fetchConnectorConfig } from 'redux/reducers/connect/connectSlice';
import { import {
@ -18,4 +17,4 @@ const mapDispatchToProps = {
fetchConfig: fetchConnectorConfig, fetchConfig: fetchConnectorConfig,
}; };
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Config)); export default connect(mapStateToProps, mapDispatchToProps)(Config);

View file

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { render } from 'lib/testHelpers'; import { render, WithRoute } from 'lib/testHelpers';
import { Route } from 'react-router-dom';
import { clusterConnectConnectorConfigPath } from 'lib/paths'; import { clusterConnectConnectorConfigPath } from 'lib/paths';
import Config, { ConfigProps } from 'components/Connect/Details/Config/Config'; import Config, { ConfigProps } from 'components/Connect/Details/Config/Config';
import { connector } from 'redux/reducers/connect/__test__/fixtures'; 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'); jest.mock('components/common/Editor/Editor', () => 'mock-Editor');
describe('Config', () => { describe('Config', () => {
const pathname = clusterConnectConnectorConfigPath( const pathname = clusterConnectConnectorConfigPath();
':clusterName',
':connectName',
':connectorName'
);
const clusterName = 'my-cluster'; const clusterName = 'my-cluster';
const connectName = 'my-connect'; const connectName = 'my-connect';
const connectorName = 'my-connector'; const connectorName = 'my-connector';
const component = (props: Partial<ConfigProps> = {}) => ( const component = (props: Partial<ConfigProps> = {}) => (
<Route path={pathname}> <WithRoute path={pathname}>
<Config <Config
fetchConfig={jest.fn()} fetchConfig={jest.fn()}
isConfigFetching={false} isConfigFetching={false}
config={connector.config} config={connector.config}
{...props} {...props}
/> />
</Route> </WithRoute>
); );
it('to be in the document when fetching config', () => { it('to be in the document when fetching config', () => {
render(component({ isConfigFetching: true }), { render(component({ isConfigFetching: true }), {
pathname: clusterConnectConnectorConfigPath( initialEntries: [
clusterName, clusterConnectConnectorConfigPath(
connectName, clusterName,
connectorName connectName,
), connectorName
),
],
}); });
expect(screen.getByRole('progressbar')).toBeInTheDocument(); expect(screen.getByRole('progressbar')).toBeInTheDocument();
}); });
it('is empty when no config', () => { it('is empty when no config', () => {
const { container } = render(component({ config: null }), { const { container } = render(component({ config: null }), {
pathname: clusterConnectConnectorConfigPath( initialEntries: [
clusterName, clusterConnectConnectorConfigPath(
connectName, clusterName,
connectorName connectName,
), connectorName
),
],
}); });
expect(container).toBeEmptyDOMElement(); expect(container).toBeEmptyDOMElement();
}); });
@ -54,11 +53,13 @@ describe('Config', () => {
it('fetches config on mount', () => { it('fetches config on mount', () => {
const fetchConfig = jest.fn(); const fetchConfig = jest.fn();
render(component({ fetchConfig }), { render(component({ fetchConfig }), {
pathname: clusterConnectConnectorConfigPath( initialEntries: [
clusterName, clusterConnectConnectorConfigPath(
connectName, clusterName,
connectorName connectName,
), connectorName
),
],
}); });
expect(fetchConfig).toHaveBeenCalledTimes(1); expect(fetchConfig).toHaveBeenCalledTimes(1);
expect(fetchConfig).toHaveBeenCalledWith({ expect(fetchConfig).toHaveBeenCalledWith({

View file

@ -1,11 +1,15 @@
import React from 'react'; 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 { Connector, Task } from 'generated-sources';
import { ClusterName, ConnectName, ConnectorName } from 'redux/interfaces'; import { ClusterName, ConnectName, ConnectorName } from 'redux/interfaces';
import { import {
clusterConnectConnectorConfigPath, clusterConnectConnectorConfigPath,
clusterConnectConnectorConfigRelativePath,
clusterConnectConnectorPath, clusterConnectConnectorPath,
clusterConnectConnectorTasksPath, clusterConnectConnectorTasksPath,
clusterConnectConnectorTasksRelativePath,
RouterParamsClusterConnectConnector,
} from 'lib/paths'; } from 'lib/paths';
import PageLoader from 'components/common/PageLoader/PageLoader'; import PageLoader from 'components/common/PageLoader/PageLoader';
import Navbar from 'components/common/Navigation/Navbar.styled'; import Navbar from 'components/common/Navigation/Navbar.styled';
@ -16,12 +20,6 @@ import TasksContainer from './Tasks/TasksContainer';
import ConfigContainer from './Config/ConfigContainer'; import ConfigContainer from './Config/ConfigContainer';
import ActionsContainer from './Actions/ActionsContainer'; import ActionsContainer from './Actions/ActionsContainer';
interface RouterParams {
clusterName: ClusterName;
connectName: ConnectName;
connectorName: ConnectorName;
}
export interface DetailsProps { export interface DetailsProps {
fetchConnector(payload: { fetchConnector(payload: {
clusterName: ClusterName; clusterName: ClusterName;
@ -46,7 +44,8 @@ const Details: React.FC<DetailsProps> = ({
areTasksFetching, areTasksFetching,
connector, connector,
}) => { }) => {
const { clusterName, connectName, connectorName } = useParams<RouterParams>(); const { clusterName, connectName, connectorName } =
useAppParams<RouterParamsClusterConnectConnector>();
React.useEffect(() => { React.useEffect(() => {
fetchConnector({ clusterName, connectName, connectorName }); fetchConnector({ clusterName, connectName, connectorName });
@ -69,68 +68,47 @@ const Details: React.FC<DetailsProps> = ({
</PageHeading> </PageHeading>
<Navbar role="navigation"> <Navbar role="navigation">
<NavLink <NavLink
exact
to={clusterConnectConnectorPath( to={clusterConnectConnectorPath(
clusterName, clusterName,
connectName, connectName,
connectorName connectorName
)} )}
activeClassName="is-active" className={({ isActive }) => (isActive ? 'is-active' : '')}
> >
Overview Overview
</NavLink> </NavLink>
<NavLink <NavLink
exact
to={clusterConnectConnectorTasksPath( to={clusterConnectConnectorTasksPath(
clusterName, clusterName,
connectName, connectName,
connectorName connectorName
)} )}
activeClassName="is-active" className={({ isActive }) => (isActive ? 'is-active' : '')}
> >
Tasks Tasks
</NavLink> </NavLink>
<NavLink <NavLink
exact
to={clusterConnectConnectorConfigPath( to={clusterConnectConnectorConfigPath(
clusterName, clusterName,
connectName, connectName,
connectorName connectorName
)} )}
activeClassName="is-active" className={({ isActive }) => (isActive ? 'is-active' : '')}
> >
Config Config
</NavLink> </NavLink>
</Navbar> </Navbar>
<Switch> <Routes>
<Route index element={<OverviewContainer />} />
<Route <Route
exact path={clusterConnectConnectorTasksRelativePath}
path={clusterConnectConnectorTasksPath( element={<TasksContainer />}
':clusterName',
':connectName',
':connectorName'
)}
component={TasksContainer}
/> />
<Route <Route
exact path={clusterConnectConnectorConfigRelativePath}
path={clusterConnectConnectorConfigPath( element={<ConfigContainer />}
':clusterName',
':connectName',
':connectorName'
)}
component={ConfigContainer}
/> />
<Route </Routes>
exact
path={clusterConnectConnectorPath(
':clusterName',
':connectName',
':connectorName'
)}
component={OverviewContainer}
/>
</Switch>
</div> </div>
); );
}; };

View file

@ -1,5 +1,4 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { RootState } from 'redux/interfaces'; import { RootState } from 'redux/interfaces';
import { import {
fetchConnector, fetchConnector,
@ -26,6 +25,4 @@ const mapDispatchToProps = {
fetchTasks: fetchConnectorTasks, fetchTasks: fetchConnectorTasks,
}; };
export default withRouter( export default connect(mapStateToProps, mapDispatchToProps)(Details);
connect(mapStateToProps, mapDispatchToProps)(Details)
);

View file

@ -1,5 +1,4 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { RootState } from 'redux/interfaces'; import { RootState } from 'redux/interfaces';
import { import {
getConnector, getConnector,
@ -15,4 +14,4 @@ const mapStateToProps = (state: RootState) => ({
failedTasksCount: getConnectorFailedTasksCount(state), failedTasksCount: getConnectorFailedTasksCount(state),
}); });
export default withRouter(connect(mapStateToProps)(Overview)); export default connect(mapStateToProps)(Overview);

View file

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { useParams } from 'react-router-dom'; import useAppParams from 'lib/hooks/useAppParams';
import { Task, TaskId } from 'generated-sources'; import { Task, TaskId } from 'generated-sources';
import { ClusterName, ConnectName, ConnectorName } from 'redux/interfaces'; import { ClusterName, ConnectName, ConnectorName } from 'redux/interfaces';
import Dropdown from 'components/common/Dropdown/Dropdown'; 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 VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
import * as C from 'components/common/Tag/Tag.styled'; import * as C from 'components/common/Tag/Tag.styled';
import getTagColor from 'components/common/Tag/getTagColor'; import getTagColor from 'components/common/Tag/getTagColor';
import { RouterParamsClusterConnectConnector } from 'lib/paths';
interface RouterParams {
clusterName: ClusterName;
connectName: ConnectName;
connectorName: ConnectorName;
}
export interface ListItemProps { export interface ListItemProps {
task: Task; task: Task;
@ -25,7 +20,8 @@ export interface ListItemProps {
} }
const ListItem: React.FC<ListItemProps> = ({ task, restartTask }) => { const ListItem: React.FC<ListItemProps> = ({ task, restartTask }) => {
const { clusterName, connectName, connectorName } = useParams<RouterParams>(); const { clusterName, connectName, connectorName } =
useAppParams<RouterParamsClusterConnectConnector>();
const restartTaskHandler = async () => { const restartTaskHandler = async () => {
await restartTask({ await restartTask({

View file

@ -1,12 +1,11 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { Task } from 'generated-sources'; import { Task } from 'generated-sources';
import { RootState } from 'redux/interfaces'; import { RootState } from 'redux/interfaces';
import { restartConnectorTask } from 'redux/reducers/connect/connectSlice'; import { restartConnectorTask } from 'redux/reducers/connect/connectSlice';
import ListItem from './ListItem'; import ListItem from './ListItem';
interface OwnProps extends RouteComponentProps { interface OwnProps {
task: Task; task: Task;
} }
@ -18,6 +17,4 @@ const mapDispatchToProps = {
restartTask: restartConnectorTask, restartTask: restartConnectorTask,
}; };
export default withRouter( export default connect(mapStateToProps, mapDispatchToProps)(ListItem);
connect(mapStateToProps, mapDispatchToProps)(ListItem)
);

View file

@ -1,19 +1,14 @@
import React from 'react'; import React from 'react';
import { render } from 'lib/testHelpers'; import { render, WithRoute } from 'lib/testHelpers';
import { clusterConnectConnectorTasksPath } from 'lib/paths'; import { clusterConnectConnectorTasksPath } from 'lib/paths';
import ListItem, { import ListItem, {
ListItemProps, ListItemProps,
} from 'components/Connect/Details/Tasks/ListItem/ListItem'; } from 'components/Connect/Details/Tasks/ListItem/ListItem';
import { tasks } from 'redux/reducers/connect/__test__/fixtures'; import { tasks } from 'redux/reducers/connect/__test__/fixtures';
import { Route } from 'react-router-dom';
import { screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
const pathname = clusterConnectConnectorTasksPath( const pathname = clusterConnectConnectorTasksPath();
':clusterName',
':connectName',
':connectorName'
);
const clusterName = 'my-cluster'; const clusterName = 'my-cluster';
const connectName = 'my-connect'; const connectName = 'my-connect';
const connectorName = 'my-connector'; const connectorName = 'my-connector';
@ -22,19 +17,21 @@ const task = tasks[0];
const renderComponent = (props: ListItemProps = { task, restartTask }) => { const renderComponent = (props: ListItemProps = { task, restartTask }) => {
return render( return render(
<Route path={pathname}> <WithRoute path={pathname}>
<table> <table>
<tbody> <tbody>
<ListItem {...props} /> <ListItem {...props} />
</tbody> </tbody>
</table> </table>
</Route>, </WithRoute>,
{ {
pathname: clusterConnectConnectorTasksPath( initialEntries: [
clusterName, clusterConnectConnectorTasksPath(
connectName, clusterName,
connectorName connectName,
), connectorName
),
],
} }
); );
}; };

View file

@ -1,5 +1,4 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { RootState } from 'redux/interfaces'; import { RootState } from 'redux/interfaces';
import { fetchConnectorTasks } from 'redux/reducers/connect/connectSlice'; import { fetchConnectorTasks } from 'redux/reducers/connect/connectSlice';
import { import {
@ -18,4 +17,4 @@ const mapDispatchToProps = {
fetchTasks: fetchConnectorTasks, fetchTasks: fetchConnectorTasks,
}; };
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Tasks)); export default connect(mapStateToProps, mapDispatchToProps)(Tasks);

View file

@ -1,10 +1,9 @@
import React from 'react'; import React from 'react';
import { render } from 'lib/testHelpers'; import { render, WithRoute } from 'lib/testHelpers';
import { clusterConnectConnectorTasksPath } from 'lib/paths'; import { clusterConnectConnectorTasksPath } from 'lib/paths';
import TasksContainer from 'components/Connect/Details/Tasks/TasksContainer'; import TasksContainer from 'components/Connect/Details/Tasks/TasksContainer';
import Tasks, { TasksProps } from 'components/Connect/Details/Tasks/Tasks'; import Tasks, { TasksProps } from 'components/Connect/Details/Tasks/Tasks';
import { tasks } from 'redux/reducers/connect/__test__/fixtures'; import { tasks } from 'redux/reducers/connect/__test__/fixtures';
import { Route } from 'react-router-dom';
import { screen } from '@testing-library/dom'; import { screen } from '@testing-library/dom';
jest.mock( jest.mock(
@ -19,28 +18,25 @@ describe('Tasks', () => {
}); });
describe('view', () => { describe('view', () => {
const pathname = clusterConnectConnectorTasksPath(
':clusterName',
':connectName',
':connectorName'
);
const clusterName = 'my-cluster'; const clusterName = 'my-cluster';
const connectName = 'my-connect'; const connectName = 'my-connect';
const connectorName = 'my-connector'; const connectorName = 'my-connector';
const setupWrapper = (props: Partial<TasksProps> = {}) => ( const setupWrapper = (props: Partial<TasksProps> = {}) => (
<Route path={pathname}> <WithRoute path={clusterConnectConnectorTasksPath()}>
<Tasks areTasksFetching={false} tasks={tasks} {...props} /> <Tasks areTasksFetching={false} tasks={tasks} {...props} />
</Route> </WithRoute>
); );
it('to be in the document when fetching tasks', () => { it('to be in the document when fetching tasks', () => {
render(setupWrapper({ areTasksFetching: true }), { render(setupWrapper({ areTasksFetching: true }), {
pathname: clusterConnectConnectorTasksPath( initialEntries: [
clusterName, clusterConnectConnectorTasksPath(
connectName, clusterName,
connectorName connectName,
), connectorName
),
],
}); });
expect(screen.getByRole('progressbar')).toBeInTheDocument(); expect(screen.getByRole('progressbar')).toBeInTheDocument();
expect(screen.queryByRole('table')).not.toBeInTheDocument(); expect(screen.queryByRole('table')).not.toBeInTheDocument();
@ -48,11 +44,13 @@ describe('Tasks', () => {
it('to be in the document when no tasks', () => { it('to be in the document when no tasks', () => {
render(setupWrapper({ tasks: [] }), { render(setupWrapper({ tasks: [] }), {
pathname: clusterConnectConnectorTasksPath( initialEntries: [
clusterName, clusterConnectConnectorTasksPath(
connectName, clusterName,
connectorName connectName,
), connectorName
),
],
}); });
expect(screen.getByRole('table')).toBeInTheDocument(); expect(screen.getByRole('table')).toBeInTheDocument();
expect(screen.getByText('No tasks found')).toBeInTheDocument(); expect(screen.getByText('No tasks found')).toBeInTheDocument();

View file

@ -1,100 +1,89 @@
import React from 'react'; import React from 'react';
import { Route } from 'react-router-dom'; import { render, WithRoute } from 'lib/testHelpers';
import { render } from 'lib/testHelpers'; import {
import { clusterConnectConnectorPath } from 'lib/paths'; clusterConnectConnectorConfigPath,
clusterConnectConnectorPath,
clusterConnectConnectorTasksPath,
getNonExactPath,
} from 'lib/paths';
import Details, { DetailsProps } from 'components/Connect/Details/Details'; import Details, { DetailsProps } from 'components/Connect/Details/Details';
import { connector, tasks } from 'redux/reducers/connect/__test__/fixtures'; import { connector, tasks } from 'redux/reducers/connect/__test__/fixtures';
import { screen } from '@testing-library/dom'; import { screen } from '@testing-library/dom';
jest.mock( const DetailsCompText = {
'components/Connect/Details/Overview/OverviewContainer', overview: 'OverviewContainer',
() => 'mock-OverviewContainer' tasks: 'TasksContainer',
); config: 'ConfigContainer',
actions: 'ActionsContainer',
};
jest.mock( jest.mock('components/Connect/Details/Overview/OverviewContainer', () => () => (
'components/Connect/Details/Tasks/TasksContainer', <div>{DetailsCompText.overview}</div>
() => 'mock-TasksContainer' ));
);
jest.mock( jest.mock('components/Connect/Details/Tasks/TasksContainer', () => () => (
'components/Connect/Details/Config/ConfigContainer', <div>{DetailsCompText.tasks}</div>
() => 'mock-ConfigContainer' ));
);
jest.mock( jest.mock('components/Connect/Details/Config/ConfigContainer', () => () => (
'components/Connect/Details/Actions/ActionsContainer', <div>{DetailsCompText.config}</div>
() => 'mock-ActionsContainer' ));
);
jest.mock('components/Connect/Details/Actions/ActionsContainer', () => () => (
<div>{DetailsCompText.actions}</div>
));
describe('Details', () => { describe('Details', () => {
const pathname = clusterConnectConnectorPath(
':clusterName',
':connectName',
':connectorName'
);
const clusterName = 'my-cluster'; const clusterName = 'my-cluster';
const connectName = 'my-connect'; const connectName = 'my-connect';
const connectorName = 'my-connector'; const connectorName = 'my-connector';
const defaultPath = clusterConnectConnectorPath(
const setupWrapper = (props: Partial<DetailsProps> = {}) => ( clusterName,
<Route path={pathname}> connectName,
<Details connectorName
fetchConnector={jest.fn()}
fetchTasks={jest.fn()}
isConnectorFetching={false}
areTasksFetching={false}
connector={connector}
tasks={tasks}
{...props}
/>
</Route>
); );
const setupWrapper = (
props: Partial<DetailsProps> = {},
path: string = defaultPath
) =>
render(
<WithRoute path={getNonExactPath(clusterConnectConnectorPath())}>
<Details
fetchConnector={jest.fn()}
fetchTasks={jest.fn()}
isConnectorFetching={false}
areTasksFetching={false}
connector={connector}
tasks={tasks}
{...props}
/>
</WithRoute>,
{ initialEntries: [path] }
);
it('renders progressbar when fetching connector', () => { it('renders progressbar when fetching connector', () => {
render(setupWrapper({ isConnectorFetching: true }), { setupWrapper({ isConnectorFetching: true });
pathname: clusterConnectConnectorPath(
clusterName,
connectName,
connectorName
),
});
expect(screen.getByRole('progressbar')).toBeInTheDocument(); expect(screen.getByRole('progressbar')).toBeInTheDocument();
expect(screen.queryByRole('navigation')).not.toBeInTheDocument(); expect(screen.queryByRole('navigation')).not.toBeInTheDocument();
}); });
it('renders progressbar when fetching tasks', () => { it('renders progressbar when fetching tasks', () => {
render(setupWrapper({ areTasksFetching: true }), { setupWrapper({ areTasksFetching: true });
pathname: clusterConnectConnectorPath(
clusterName,
connectName,
connectorName
),
});
expect(screen.getByRole('progressbar')).toBeInTheDocument(); expect(screen.getByRole('progressbar')).toBeInTheDocument();
expect(screen.queryByRole('navigation')).not.toBeInTheDocument(); expect(screen.queryByRole('navigation')).not.toBeInTheDocument();
}); });
it('is empty when no connector', () => { it('is empty when no connector', () => {
const { container } = render(setupWrapper({ connector: null }), { const { container } = setupWrapper({ connector: null });
pathname: clusterConnectConnectorPath(
clusterName,
connectName,
connectorName
),
});
expect(container).toBeEmptyDOMElement(); expect(container).toBeEmptyDOMElement();
}); });
it('fetches connector on mount', () => { it('fetches connector on mount', () => {
const fetchConnector = jest.fn(); const fetchConnector = jest.fn();
render(setupWrapper({ fetchConnector }), { setupWrapper({ fetchConnector });
pathname: clusterConnectConnectorPath(
clusterName,
connectName,
connectorName
),
});
expect(fetchConnector).toHaveBeenCalledTimes(1); expect(fetchConnector).toHaveBeenCalledTimes(1);
expect(fetchConnector).toHaveBeenCalledWith({ expect(fetchConnector).toHaveBeenCalledWith({
clusterName, clusterName,
@ -105,13 +94,7 @@ describe('Details', () => {
it('fetches tasks on mount', () => { it('fetches tasks on mount', () => {
const fetchTasks = jest.fn(); const fetchTasks = jest.fn();
render(setupWrapper({ fetchTasks }), { setupWrapper({ fetchTasks });
pathname: clusterConnectConnectorPath(
clusterName,
connectName,
connectorName
),
});
expect(fetchTasks).toHaveBeenCalledTimes(1); expect(fetchTasks).toHaveBeenCalledTimes(1);
expect(fetchTasks).toHaveBeenCalledWith({ expect(fetchTasks).toHaveBeenCalledWith({
clusterName, clusterName,
@ -119,4 +102,35 @@ describe('Details', () => {
connectorName, 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));
});
});
}); });

View file

@ -1,5 +1,6 @@
import React from 'react'; 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 { Controller, useForm } from 'react-hook-form';
import { ErrorMessage } from '@hookform/error-message'; import { ErrorMessage } from '@hookform/error-message';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
@ -9,7 +10,10 @@ import {
ConnectorConfig, ConnectorConfig,
ConnectorName, ConnectorName,
} from 'redux/interfaces'; } from 'redux/interfaces';
import { clusterConnectConnectorConfigPath } from 'lib/paths'; import {
clusterConnectConnectorConfigPath,
RouterParamsClusterConnectConnector,
} from 'lib/paths';
import yup from 'lib/yupExtended'; import yup from 'lib/yupExtended';
import Editor from 'components/common/Editor/Editor'; import Editor from 'components/common/Editor/Editor';
import PageLoader from 'components/common/PageLoader/PageLoader'; import PageLoader from 'components/common/PageLoader/PageLoader';
@ -24,12 +28,6 @@ const validationSchema = yup.object().shape({
config: yup.string().required().isJsonObject(), config: yup.string().required().isJsonObject(),
}); });
interface RouterParams {
clusterName: ClusterName;
connectName: ConnectName;
connectorName: ConnectorName;
}
interface FormValues { interface FormValues {
config: string; config: string;
} }
@ -56,8 +54,9 @@ const Edit: React.FC<EditProps> = ({
config, config,
updateConfig, updateConfig,
}) => { }) => {
const { clusterName, connectName, connectorName } = useParams<RouterParams>(); const { clusterName, connectName, connectorName } =
const history = useHistory(); useAppParams<RouterParamsClusterConnectConnector>();
const navigate = useNavigate();
const { const {
handleSubmit, handleSubmit,
control, control,
@ -89,7 +88,7 @@ const Edit: React.FC<EditProps> = ({
connectorConfig: JSON.parse(values.config.trim()), connectorConfig: JSON.parse(values.config.trim()),
}); });
if (connector) { if (connector) {
history.push( navigate(
clusterConnectConnectorConfigPath( clusterConnectConnectorConfigPath(
clusterName, clusterName,
connectName, connectName,

View file

@ -1,5 +1,4 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { RootState } from 'redux/interfaces'; import { RootState } from 'redux/interfaces';
import { import {
fetchConnectorConfig, fetchConnectorConfig,
@ -22,4 +21,4 @@ const mapDispatchToProps = {
updateConfig: updateConnectorConfig, updateConfig: updateConnectorConfig,
}; };
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Edit)); export default connect(mapStateToProps, mapDispatchToProps)(Edit);

View file

@ -1,12 +1,11 @@
import React from 'react'; import React from 'react';
import { render } from 'lib/testHelpers'; import { render, WithRoute } from 'lib/testHelpers';
import { import {
clusterConnectConnectorConfigPath, clusterConnectConnectorConfigPath,
clusterConnectConnectorEditPath, clusterConnectConnectorEditPath,
} from 'lib/paths'; } from 'lib/paths';
import Edit, { EditProps } from 'components/Connect/Edit/Edit'; import Edit, { EditProps } from 'components/Connect/Edit/Edit';
import { connector } from 'redux/reducers/connect/__test__/fixtures'; import { connector } from 'redux/reducers/connect/__test__/fixtures';
import { Route } from 'react-router-dom';
import { waitFor } from '@testing-library/dom'; import { waitFor } from '@testing-library/dom';
import { act, fireEvent, screen } from '@testing-library/react'; import { act, fireEvent, screen } from '@testing-library/react';
@ -17,24 +16,18 @@ jest.mock('components/common/Editor/Editor', () => 'mock-Editor');
const mockHistoryPush = jest.fn(); const mockHistoryPush = jest.fn();
jest.mock('react-router-dom', () => ({ jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'), ...jest.requireActual('react-router-dom'),
useHistory: () => ({ useNavigate: () => mockHistoryPush,
push: mockHistoryPush,
}),
})); }));
describe('Edit', () => { describe('Edit', () => {
const pathname = clusterConnectConnectorEditPath( const pathname = clusterConnectConnectorEditPath();
':clusterName',
':connectName',
':connectorName'
);
const clusterName = 'my-cluster'; const clusterName = 'my-cluster';
const connectName = 'my-connect'; const connectName = 'my-connect';
const connectorName = 'my-connector'; const connectorName = 'my-connector';
const renderComponent = (props: Partial<EditProps> = {}) => const renderComponent = (props: Partial<EditProps> = {}) =>
render( render(
<Route path={pathname}> <WithRoute path={pathname}>
<Edit <Edit
fetchConfig={jest.fn()} fetchConfig={jest.fn()}
isConfigFetching={false} isConfigFetching={false}
@ -42,13 +35,15 @@ describe('Edit', () => {
updateConfig={jest.fn()} updateConfig={jest.fn()}
{...props} {...props}
/> />
</Route>, </WithRoute>,
{ {
pathname: clusterConnectConnectorEditPath( initialEntries: [
clusterName, clusterConnectConnectorEditPath(
connectName, clusterName,
connectorName connectName,
), connectorName
),
],
} }
); );

View file

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import { useParams } from 'react-router-dom'; import useAppParams from 'lib/hooks/useAppParams';
import { Connect, FullConnectorInfo } from 'generated-sources'; import { Connect, FullConnectorInfo } from 'generated-sources';
import { ClusterName, ConnectorSearch } from 'redux/interfaces'; import { ClusterName, ConnectorSearch } from 'redux/interfaces';
import { clusterConnectorNewPath } from 'lib/paths'; import { clusterConnectorNewRelativePath, ClusterNameRoute } from 'lib/paths';
import ClusterContext from 'components/contexts/ClusterContext'; import ClusterContext from 'components/contexts/ClusterContext';
import PageLoader from 'components/common/PageLoader/PageLoader'; import PageLoader from 'components/common/PageLoader/PageLoader';
import Search from 'components/common/Search/Search'; import Search from 'components/common/Search/Search';
@ -40,7 +40,7 @@ const List: React.FC<ListProps> = ({
setConnectorSearch, setConnectorSearch,
}) => { }) => {
const { isReadOnly } = React.useContext(ClusterContext); const { isReadOnly } = React.useContext(ClusterContext);
const { clusterName } = useParams<{ clusterName: string }>(); const { clusterName } = useAppParams<ClusterNameRoute>();
React.useEffect(() => { React.useEffect(() => {
fetchConnects(clusterName); fetchConnects(clusterName);
@ -58,10 +58,9 @@ const List: React.FC<ListProps> = ({
<PageHeading text="Connectors"> <PageHeading text="Connectors">
{!isReadOnly && ( {!isReadOnly && (
<Button <Button
isLink
buttonType="primary" buttonType="primary"
buttonSize="M" buttonSize="M"
to={clusterConnectorNewPath(clusterName)} to={clusterConnectorNewRelativePath}
> >
Create Connector Create Connector
</Button> </Button>

View file

@ -60,10 +60,7 @@ const ListItem: React.FC<ListItemProps> = ({
return ( return (
<tr> <tr>
<TableKeyLink> <TableKeyLink>
<NavLink <NavLink to={clusterConnectConnectorPath(clusterName, connect, name)}>
exact
to={clusterConnectConnectorPath(clusterName, connect, name)}
>
{name} {name}
</NavLink> </NavLink>
</TableKeyLink> </TableKeyLink>

View file

@ -1,11 +1,12 @@
import React from 'react'; 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 { Controller, FormProvider, useForm } from 'react-hook-form';
import { ErrorMessage } from '@hookform/error-message'; import { ErrorMessage } from '@hookform/error-message';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { Connect, Connector, NewConnector } from 'generated-sources'; import { Connect, Connector, NewConnector } from 'generated-sources';
import { ClusterName, ConnectName } from 'redux/interfaces'; import { ClusterName, ConnectName } from 'redux/interfaces';
import { clusterConnectConnectorPath } from 'lib/paths'; import { clusterConnectConnectorPath, ClusterNameRoute } from 'lib/paths';
import yup from 'lib/yupExtended'; import yup from 'lib/yupExtended';
import Editor from 'components/common/Editor/Editor'; import Editor from 'components/common/Editor/Editor';
import PageLoader from 'components/common/PageLoader/PageLoader'; import PageLoader from 'components/common/PageLoader/PageLoader';
@ -23,10 +24,6 @@ const validationSchema = yup.object().shape({
config: yup.string().required().isJsonObject(), config: yup.string().required().isJsonObject(),
}); });
interface RouterParams {
clusterName: ClusterName;
}
export interface NewProps { export interface NewProps {
fetchConnects(clusterName: ClusterName): unknown; fetchConnects(clusterName: ClusterName): unknown;
areConnectsFetching: boolean; areConnectsFetching: boolean;
@ -50,8 +47,8 @@ const New: React.FC<NewProps> = ({
connects, connects,
createConnector, createConnector,
}) => { }) => {
const { clusterName } = useParams<RouterParams>(); const { clusterName } = useAppParams<ClusterNameRoute>();
const history = useHistory(); const navigate = useNavigate();
const methods = useForm<FormValues>({ const methods = useForm<FormValues>({
mode: 'onTouched', mode: 'onTouched',
@ -96,7 +93,7 @@ const New: React.FC<NewProps> = ({
}); });
if (connector) { if (connector) {
history.push( navigate(
clusterConnectConnectorPath( clusterConnectConnectorPath(
clusterName, clusterName,
connector.connect, connector.connect,

View file

@ -1,5 +1,4 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { import {
createConnector, createConnector,
fetchConnects, fetchConnects,
@ -22,4 +21,4 @@ const mapDispatchToProps = {
createConnector: createConnector as unknown as NewProps['createConnector'], createConnector: createConnector as unknown as NewProps['createConnector'],
}; };
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(New)); export default connect(mapStateToProps, mapDispatchToProps)(New);

View file

@ -1,12 +1,11 @@
import React from 'react'; import React from 'react';
import { render } from 'lib/testHelpers'; import { render, WithRoute } from 'lib/testHelpers';
import { import {
clusterConnectConnectorPath, clusterConnectConnectorPath,
clusterConnectorNewPath, clusterConnectorNewPath,
} from 'lib/paths'; } from 'lib/paths';
import New, { NewProps } from 'components/Connect/New/New'; import New, { NewProps } from 'components/Connect/New/New';
import { connects, connector } from 'redux/reducers/connect/__test__/fixtures'; import { connects, connector } from 'redux/reducers/connect/__test__/fixtures';
import { Route } from 'react-router-dom';
import { fireEvent, screen, act } from '@testing-library/react'; import { fireEvent, screen, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { ControllerRenderProps } from 'react-hook-form'; import { ControllerRenderProps } from 'react-hook-form';
@ -22,9 +21,7 @@ jest.mock(
const mockHistoryPush = jest.fn(); const mockHistoryPush = jest.fn();
jest.mock('react-router-dom', () => ({ jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'), ...jest.requireActual('react-router-dom'),
useHistory: () => ({ useNavigate: () => mockHistoryPush,
push: mockHistoryPush,
}),
})); }));
describe('New', () => { describe('New', () => {
@ -51,7 +48,7 @@ describe('New', () => {
const renderComponent = (props: Partial<NewProps> = {}) => const renderComponent = (props: Partial<NewProps> = {}) =>
render( render(
<Route path={clusterConnectorNewPath(':clusterName')}> <WithRoute path={clusterConnectorNewPath()}>
<New <New
fetchConnects={jest.fn()} fetchConnects={jest.fn()}
areConnectsFetching={false} areConnectsFetching={false}
@ -59,8 +56,8 @@ describe('New', () => {
createConnector={jest.fn()} createConnector={jest.fn()}
{...props} {...props}
/> />
</Route>, </WithRoute>,
{ pathname: clusterConnectorNewPath(clusterName) } { initialEntries: [clusterConnectorNewPath(clusterName)] }
); );
it('fetches connects on mount', async () => { it('fetches connects on mount', async () => {

View file

@ -1,53 +1,68 @@
import React from 'react'; import React from 'react';
import { render } from 'lib/testHelpers'; import { render, WithRoute } from 'lib/testHelpers';
import { screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import Connect from 'components/Connect/Connect'; import Connect from 'components/Connect/Connect';
import { store } from 'redux/store'; import { store } from 'redux/store';
import { Route } from 'react-router-dom';
import { import {
clusterConnectorsPath, clusterConnectorsPath,
clusterConnectorNewPath, clusterConnectorNewPath,
clusterConnectConnectorPath, clusterConnectConnectorPath,
clusterConnectConnectorEditPath, clusterConnectConnectorEditPath,
getNonExactPath,
clusterConnectsPath,
} from 'lib/paths'; } from 'lib/paths';
const ConnectCompText = {
new: 'NewContainer',
list: 'ListContainer',
details: 'DetailsContainer',
edit: 'EditContainer',
};
jest.mock('components/Connect/New/NewContainer', () => () => ( jest.mock('components/Connect/New/NewContainer', () => () => (
<div>NewContainer</div> <div>{ConnectCompText.new}</div>
)); ));
jest.mock('components/Connect/List/ListContainer', () => () => ( jest.mock('components/Connect/List/ListContainer', () => () => (
<div>ListContainer</div> <div>{ConnectCompText.list}</div>
)); ));
jest.mock('components/Connect/Details/DetailsContainer', () => () => ( jest.mock('components/Connect/Details/DetailsContainer', () => () => (
<div>DetailsContainer</div> <div>{ConnectCompText.details}</div>
)); ));
jest.mock('components/Connect/Edit/EditContainer', () => () => ( jest.mock('components/Connect/Edit/EditContainer', () => () => (
<div>EditContainer</div> <div>{ConnectCompText.edit}</div>
)); ));
describe('Connect', () => { describe('Connect', () => {
const renderComponent = (pathname: string) => const renderComponent = (pathname: string, routePath: string) =>
render( render(
<Route path="/ui/clusters/:clusterName"> <WithRoute path={getNonExactPath(routePath)}>
<Connect /> <Connect />
</Route>, </WithRoute>,
{ pathname, store } { initialEntries: [pathname], store }
); );
it('renders ListContainer', () => { it('renders ListContainer', () => {
renderComponent(clusterConnectorsPath('my-cluster')); renderComponent(
expect(screen.getByText('ListContainer')).toBeInTheDocument(); clusterConnectorsPath('my-cluster'),
clusterConnectorsPath()
);
expect(screen.getByText(ConnectCompText.list)).toBeInTheDocument();
}); });
it('renders NewContainer', () => { it('renders NewContainer', () => {
renderComponent(clusterConnectorNewPath('my-cluster')); renderComponent(
expect(screen.getByText('NewContainer')).toBeInTheDocument(); clusterConnectorNewPath('my-cluster'),
clusterConnectorsPath()
);
expect(screen.getByText(ConnectCompText.new)).toBeInTheDocument();
}); });
it('renders DetailsContainer', () => { it('renders DetailsContainer', () => {
renderComponent( 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', () => { it('renders EditContainer', () => {
@ -56,8 +71,9 @@ describe('Connect', () => {
'my-cluster', 'my-cluster',
'my-connect', 'my-connect',
'my-connector' 'my-connector'
) ),
clusterConnectsPath()
); );
expect(screen.getByText('EditContainer')).toBeInTheDocument(); expect(screen.getByText(ConnectCompText.edit)).toBeInTheDocument();
}); });
}); });

View file

@ -1,28 +1,42 @@
import React from 'react'; 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 Details from 'components/ConsumerGroups/Details/Details';
import ListContainer from 'components/ConsumerGroups/List/ListContainer'; import ListContainer from 'components/ConsumerGroups/List/ListContainer';
import ResetOffsets from 'components/ConsumerGroups/Details/ResetOffsets/ResetOffsets'; import ResetOffsets from 'components/ConsumerGroups/Details/ResetOffsets/ResetOffsets';
import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route'; import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route';
import {
clusterConsumerGroupResetOffsetsRelativePath,
RouteParams,
} from 'lib/paths';
const ConsumerGroups: React.FC = () => { const ConsumerGroups: React.FC = () => {
return ( return (
<Switch> <Routes>
<BreadcrumbRoute <Route
exact index
path="/ui/clusters/:clusterName/consumer-groups" element={
component={ListContainer} <BreadcrumbRoute>
<ListContainer />
</BreadcrumbRoute>
}
/> />
<BreadcrumbRoute <Route
exact path={RouteParams.consumerGroupID}
path="/ui/clusters/:clusterName/consumer-groups/:consumerGroupID" element={
component={Details} <BreadcrumbRoute>
<Details />
</BreadcrumbRoute>
}
/> />
<BreadcrumbRoute <Route
path="/ui/clusters/:clusterName/consumer-groups/:consumerGroupID/reset-offsets" path={clusterConsumerGroupResetOffsetsRelativePath}
component={ResetOffsets} element={
<BreadcrumbRoute>
<ResetOffsets />
</BreadcrumbRoute>
}
/> />
</Switch> </Routes>
); );
}; };

View file

@ -1,13 +1,12 @@
import React from 'react'; import React from 'react';
import { ClusterName } from 'redux/interfaces'; import { useNavigate } from 'react-router-dom';
import useAppParams from 'lib/hooks/useAppParams';
import { import {
clusterConsumerGroupResetOffsetsPath, clusterConsumerGroupResetRelativePath,
clusterConsumerGroupsPath, ClusterGroupParam,
} from 'lib/paths'; } from 'lib/paths';
import { ConsumerGroupID } from 'redux/interfaces/consumerGroup';
import PageLoader from 'components/common/PageLoader/PageLoader'; import PageLoader from 'components/common/PageLoader/PageLoader';
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal'; import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
import { useHistory, useParams } from 'react-router-dom';
import ClusterContext from 'components/contexts/ClusterContext'; import ClusterContext from 'components/contexts/ClusterContext';
import PageHeading from 'components/common/PageHeading/PageHeading'; import PageHeading from 'components/common/PageHeading/PageHeading';
import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon'; import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
@ -31,10 +30,9 @@ import getTagColor from 'components/common/Tag/getTagColor';
import ListItem from './ListItem'; import ListItem from './ListItem';
const Details: React.FC = () => { const Details: React.FC = () => {
const history = useHistory(); const navigate = useNavigate();
const { isReadOnly } = React.useContext(ClusterContext); const { isReadOnly } = React.useContext(ClusterContext);
const { consumerGroupID, clusterName } = const { consumerGroupID, clusterName } = useAppParams<ClusterGroupParam>();
useParams<{ consumerGroupID: ConsumerGroupID; clusterName: ClusterName }>();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const consumerGroup = useAppSelector((state) => const consumerGroup = useAppSelector((state) =>
selectById(state, consumerGroupID) selectById(state, consumerGroupID)
@ -55,14 +53,12 @@ const Details: React.FC = () => {
}; };
React.useEffect(() => { React.useEffect(() => {
if (isDeleted) { if (isDeleted) {
history.push(clusterConsumerGroupsPath(clusterName)); navigate('../');
} }
}, [clusterName, history, isDeleted]); }, [clusterName, navigate, isDeleted]);
const onResetOffsets = () => { const onResetOffsets = () => {
history.push( navigate(clusterConsumerGroupResetRelativePath);
clusterConsumerGroupResetOffsetsPath(clusterName, consumerGroupID)
);
}; };
if (!isFetched || !consumerGroup) { if (!isFetched || !consumerGroup) {

View file

@ -1,13 +1,13 @@
import { ConsumerGroupOffsetsResetType } from 'generated-sources';
import { clusterConsumerGroupDetailsPath } from 'lib/paths';
import React from 'react'; import React from 'react';
import { useNavigate } from 'react-router-dom';
import { ConsumerGroupOffsetsResetType } from 'generated-sources';
import { ClusterGroupParam } from 'lib/paths';
import { import {
Controller, Controller,
FormProvider, FormProvider,
useFieldArray, useFieldArray,
useForm, useForm,
} from 'react-hook-form'; } from 'react-hook-form';
import { ClusterName, ConsumerGroupID } from 'redux/interfaces';
import MultiSelect from 'react-multi-select-component'; import MultiSelect from 'react-multi-select-component';
import { Option } from 'react-multi-select-component/dist/lib/interfaces'; import { Option } from 'react-multi-select-component/dist/lib/interfaces';
import DatePicker from 'react-datepicker'; import DatePicker from 'react-datepicker';
@ -15,7 +15,6 @@ import 'react-datepicker/dist/react-datepicker.css';
import { groupBy } from 'lodash'; import { groupBy } from 'lodash';
import PageLoader from 'components/common/PageLoader/PageLoader'; import PageLoader from 'components/common/PageLoader/PageLoader';
import { ErrorMessage } from '@hookform/error-message'; import { ErrorMessage } from '@hookform/error-message';
import { useHistory, useParams } from 'react-router-dom';
import Select from 'components/common/Select/Select'; import Select from 'components/common/Select/Select';
import { InputLabel } from 'components/common/Input/InputLabel.styled'; import { InputLabel } from 'components/common/Input/InputLabel.styled';
import { Button } from 'components/common/Button/Button'; import { Button } from 'components/common/Button/Button';
@ -30,6 +29,7 @@ import {
resetConsumerGroupOffsets, resetConsumerGroupOffsets,
} from 'redux/reducers/consumerGroups/consumerGroupsSlice'; } from 'redux/reducers/consumerGroups/consumerGroupsSlice';
import { useAppDispatch, useAppSelector } from 'lib/hooks/redux'; import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
import useAppParams from 'lib/hooks/useAppParams';
import { resetLoaderById } from 'redux/reducers/loader/loaderSlice'; import { resetLoaderById } from 'redux/reducers/loader/loaderSlice';
import { import {
@ -48,8 +48,7 @@ interface FormType {
const ResetOffsets: React.FC = () => { const ResetOffsets: React.FC = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { consumerGroupID, clusterName } = const { consumerGroupID, clusterName } = useAppParams<ClusterGroupParam>();
useParams<{ consumerGroupID: ConsumerGroupID; clusterName: ClusterName }>();
const consumerGroup = useAppSelector((state) => const consumerGroup = useAppSelector((state) =>
selectById(state, consumerGroupID) selectById(state, consumerGroupID)
); );
@ -162,15 +161,13 @@ const ResetOffsets: React.FC = () => {
} }
}; };
const history = useHistory(); const navigate = useNavigate();
React.useEffect(() => { React.useEffect(() => {
if (isOffsetReseted) { if (isOffsetReseted) {
dispatch(resetLoaderById('consumerGroups/resetConsumerGroupOffsets')); dispatch(resetLoaderById('consumerGroups/resetConsumerGroupOffsets'));
history.push( navigate('../');
clusterConsumerGroupDetailsPath(clusterName, consumerGroupID)
);
} }
}, [clusterName, consumerGroupID, dispatch, history, isOffsetReseted]); }, [clusterName, consumerGroupID, dispatch, navigate, isOffsetReseted]);
if (!isFetched || !consumerGroup) { if (!isFetched || !consumerGroup) {
return <PageLoader />; return <PageLoader />;

View file

@ -1,9 +1,8 @@
import React from 'react'; import React from 'react';
import fetchMock from 'fetch-mock'; import fetchMock from 'fetch-mock';
import { Route } from 'react-router-dom';
import { act, screen, waitFor } from '@testing-library/react'; import { act, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { render } from 'lib/testHelpers'; import { render, WithRoute } from 'lib/testHelpers';
import { clusterConsumerGroupResetOffsetsPath } from 'lib/paths'; import { clusterConsumerGroupResetOffsetsPath } from 'lib/paths';
import { consumerGroupPayload } from 'redux/reducers/consumerGroups/__test__/fixtures'; import { consumerGroupPayload } from 'redux/reducers/consumerGroups/__test__/fixtures';
import ResetOffsets from 'components/ConsumerGroups/Details/ResetOffsets/ResetOffsets'; import ResetOffsets from 'components/ConsumerGroups/Details/ResetOffsets/ResetOffsets';
@ -13,19 +12,16 @@ const { groupId } = consumerGroupPayload;
const renderComponent = () => const renderComponent = () =>
render( render(
<Route <WithRoute path={clusterConsumerGroupResetOffsetsPath()}>
path={clusterConsumerGroupResetOffsetsPath(
':clusterName',
':consumerGroupID'
)}
>
<ResetOffsets /> <ResetOffsets />
</Route>, </WithRoute>,
{ {
pathname: clusterConsumerGroupResetOffsetsPath( initialEntries: [
clusterName, clusterConsumerGroupResetOffsetsPath(
consumerGroupPayload.groupId clusterName,
), consumerGroupPayload.groupId
),
],
} }
); );

View file

@ -3,28 +3,27 @@ import { clusterConsumerGroupDetailsPath } from 'lib/paths';
import { screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import TopicContents from 'components/ConsumerGroups/Details/TopicContents/TopicContents'; import TopicContents from 'components/ConsumerGroups/Details/TopicContents/TopicContents';
import { consumerGroupPayload } from 'redux/reducers/consumerGroups/__test__/fixtures'; import { consumerGroupPayload } from 'redux/reducers/consumerGroups/__test__/fixtures';
import { render } from 'lib/testHelpers'; import { render, WithRoute } from 'lib/testHelpers';
import { Route } from 'react-router-dom';
import { ConsumerGroupTopicPartition } from 'generated-sources'; import { ConsumerGroupTopicPartition } from 'generated-sources';
const clusterName = 'cluster1'; const clusterName = 'cluster1';
const renderComponent = (consumers: ConsumerGroupTopicPartition[] = []) => const renderComponent = (consumers: ConsumerGroupTopicPartition[] = []) =>
render( render(
<Route <WithRoute path={clusterConsumerGroupDetailsPath()}>
path={clusterConsumerGroupDetailsPath(':clusterName', ':consumerGroupID')}
>
<table> <table>
<tbody> <tbody>
<TopicContents consumers={consumers} /> <TopicContents consumers={consumers} />
</tbody> </tbody>
</table> </table>
</Route>, </WithRoute>,
{ {
pathname: clusterConsumerGroupDetailsPath( initialEntries: [
clusterName, clusterConsumerGroupDetailsPath(
consumerGroupPayload.groupId clusterName,
), consumerGroupPayload.groupId
),
],
} }
); );

View file

@ -1,13 +1,10 @@
import Details from 'components/ConsumerGroups/Details/Details'; import Details from 'components/ConsumerGroups/Details/Details';
import React from 'react'; import React from 'react';
import fetchMock from 'fetch-mock'; import fetchMock from 'fetch-mock';
import { createMemoryHistory } from 'history'; import { render, WithRoute } from 'lib/testHelpers';
import { render } from 'lib/testHelpers';
import { Route, Router } from 'react-router-dom';
import { import {
clusterConsumerGroupDetailsPath, clusterConsumerGroupDetailsPath,
clusterConsumerGroupResetOffsetsPath, clusterConsumerGroupResetRelativePath,
clusterConsumerGroupsPath,
} from 'lib/paths'; } from 'lib/paths';
import { consumerGroupPayload } from 'redux/reducers/consumerGroups/__test__/fixtures'; import { consumerGroupPayload } from 'redux/reducers/consumerGroups/__test__/fixtures';
import { import {
@ -20,26 +17,25 @@ import { act } from '@testing-library/react';
const clusterName = 'cluster1'; const clusterName = 'cluster1';
const { groupId } = consumerGroupPayload; const { groupId } = consumerGroupPayload;
const history = createMemoryHistory();
const mockNavigate = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: () => mockNavigate,
}));
const renderComponent = () => { const renderComponent = () => {
history.push(clusterConsumerGroupDetailsPath(clusterName, groupId));
render( render(
<Router history={history}> <WithRoute path={clusterConsumerGroupDetailsPath()}>
<Route <Details />
path={clusterConsumerGroupDetailsPath( </WithRoute>,
':clusterName', { initialEntries: [clusterConsumerGroupDetailsPath(clusterName, groupId)] }
':consumerGroupID'
)}
>
<Details />
</Route>
</Router>
); );
}; };
describe('Details component', () => { describe('Details component', () => {
afterEach(() => { afterEach(() => {
fetchMock.reset(); fetchMock.reset();
mockNavigate.mockClear();
}); });
describe('when consumer groups are NOT fetched', () => { describe('when consumer groups are NOT fetched', () => {
@ -76,8 +72,8 @@ describe('Details component', () => {
it('handles [Reset offset] click', async () => { it('handles [Reset offset] click', async () => {
userEvent.click(screen.getByText('Reset offset')); userEvent.click(screen.getByText('Reset offset'));
expect(history.location.pathname).toEqual( expect(mockNavigate).toHaveBeenLastCalledWith(
clusterConsumerGroupResetOffsetsPath(clusterName, groupId) clusterConsumerGroupResetRelativePath
); );
}); });
@ -106,9 +102,7 @@ describe('Details component', () => {
}); });
expect(deleteConsumerGroupMock.called()).toBeTruthy(); expect(deleteConsumerGroupMock.called()).toBeTruthy();
expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
expect(history.location.pathname).toEqual( expect(mockNavigate).toHaveBeenLastCalledWith('../');
clusterConsumerGroupsPath(clusterName)
);
}); });
}); });
}); });

View file

@ -4,17 +4,14 @@ import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import ListItem from 'components/ConsumerGroups/Details/ListItem'; import ListItem from 'components/ConsumerGroups/Details/ListItem';
import { consumerGroupPayload } from 'redux/reducers/consumerGroups/__test__/fixtures'; import { consumerGroupPayload } from 'redux/reducers/consumerGroups/__test__/fixtures';
import { render } from 'lib/testHelpers'; import { render, WithRoute } from 'lib/testHelpers';
import { Route } from 'react-router-dom';
import { ConsumerGroupTopicPartition } from 'generated-sources'; import { ConsumerGroupTopicPartition } from 'generated-sources';
const clusterName = 'cluster1'; const clusterName = 'cluster1';
const renderComponent = (consumers: ConsumerGroupTopicPartition[] = []) => const renderComponent = (consumers: ConsumerGroupTopicPartition[] = []) =>
render( render(
<Route <WithRoute path={clusterConsumerGroupDetailsPath()}>
path={clusterConsumerGroupDetailsPath(':clusterName', ':consumerGroupID')}
>
<table> <table>
<tbody> <tbody>
<ListItem <ListItem
@ -24,12 +21,14 @@ const renderComponent = (consumers: ConsumerGroupTopicPartition[] = []) =>
/> />
</tbody> </tbody>
</table> </table>
</Route>, </WithRoute>,
{ {
pathname: clusterConsumerGroupDetailsPath( initialEntries: [
clusterName, clusterConsumerGroupDetailsPath(
consumerGroupPayload.groupId clusterName,
), consumerGroupPayload.groupId
),
],
} }
); );

View file

@ -17,7 +17,7 @@ export const GroupIDCell: React.FC<TableCellProps<ConsumerGroup, string>> = ({
}) => { }) => {
return ( return (
<SmartTableKeyLink> <SmartTableKeyLink>
<Link to={`consumer-groups/${groupId}`}>{groupId}</Link> <Link to={groupId}>{groupId}</Link>
</SmartTableKeyLink> </SmartTableKeyLink>
); );
}; };

View file

@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
import { useParams } from 'react-router-dom';
import PageHeading from 'components/common/PageHeading/PageHeading'; import PageHeading from 'components/common/PageHeading/PageHeading';
import Search from 'components/common/Search/Search'; import Search from 'components/common/Search/Search';
import { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled'; import { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled';
@ -18,7 +17,8 @@ import {
import usePagination from 'lib/hooks/usePagination'; import usePagination from 'lib/hooks/usePagination';
import useSearch from 'lib/hooks/useSearch'; import useSearch from 'lib/hooks/useSearch';
import { useAppDispatch } from 'lib/hooks/redux'; 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 { fetchConsumerGroupsPaged } from 'redux/reducers/consumerGroups/consumerGroupsSlice';
import PageLoader from 'components/common/PageLoader/PageLoader'; import PageLoader from 'components/common/PageLoader/PageLoader';
@ -42,7 +42,7 @@ const List: React.FC<Props> = ({
const { page, perPage } = usePagination(); const { page, perPage } = usePagination();
const [searchText, handleSearchText] = useSearch(); const [searchText, handleSearchText] = useSearch();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { clusterName } = useParams<{ clusterName: ClusterName }>(); const { clusterName } = useAppParams<ClusterNameRoute>();
React.useEffect(() => { React.useEffect(() => {
dispatch( dispatch(

View file

@ -40,10 +40,7 @@ describe('Consumer Groups Table Cells', () => {
); );
const linkElement = screen.getByRole('link'); const linkElement = screen.getByRole('link');
expect(linkElement).toBeInTheDocument(); expect(linkElement).toBeInTheDocument();
expect(linkElement).toHaveAttribute( expect(linkElement).toHaveAttribute('href', `/${consumerGroup.groupId}`);
'href',
`/consumer-groups/${consumerGroup.groupId}`
);
}); });
}); });

View file

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { clusterConsumerGroupsPath } from 'lib/paths'; import { clusterConsumerGroupsPath, getNonExactPath } from 'lib/paths';
import { import {
act, act,
screen, screen,
@ -11,27 +11,19 @@ import {
consumerGroups, consumerGroups,
noConsumerGroupsResponse, noConsumerGroupsResponse,
} from 'redux/reducers/consumerGroups/__test__/fixtures'; } from 'redux/reducers/consumerGroups/__test__/fixtures';
import { render } from 'lib/testHelpers'; import { render, WithRoute } from 'lib/testHelpers';
import fetchMock from 'fetch-mock'; import fetchMock from 'fetch-mock';
import { Route, Router } from 'react-router-dom';
import { ConsumerGroupOrdering, SortOrder } from 'generated-sources'; import { ConsumerGroupOrdering, SortOrder } from 'generated-sources';
import { createMemoryHistory } from 'history';
const clusterName = 'cluster1'; const clusterName = 'cluster1';
const historyMock = createMemoryHistory({ const renderComponent = (path?: string) =>
initialEntries: [clusterConsumerGroupsPath(clusterName)],
});
const renderComponent = (history = historyMock) =>
render( render(
<Router history={history}> <WithRoute path={getNonExactPath(clusterConsumerGroupsPath())}>
<Route path={clusterConsumerGroupsPath(':clusterName')}> <ConsumerGroups />
<ConsumerGroups /> </WithRoute>,
</Route>
</Router>,
{ {
pathname: clusterConsumerGroupsPath(clusterName), initialEntries: [path || clusterConsumerGroupsPath(clusterName)],
} }
); );
@ -123,12 +115,9 @@ describe('ConsumerGroups', () => {
} }
); );
const mockedHistory = createMemoryHistory({ renderComponent(
initialEntries: [ `${clusterConsumerGroupsPath(clusterName)}?q=${searchText}`
`${clusterConsumerGroupsPath(clusterName)}?q=${searchText}`, );
],
});
renderComponent(mockedHistory);
await waitForElementToBeRemoved(() => screen.getByRole('progressbar')); await waitForElementToBeRemoved(() => screen.getByRole('progressbar'));
await waitFor(() => expect(consumerGroupsMock.called()).toBeTruthy()); await waitFor(() => expect(consumerGroupsMock.called()).toBeTruthy());

View file

@ -1,20 +1,30 @@
import React from 'react'; import React from 'react';
import { Switch } from 'react-router-dom'; import { Route, Routes } from 'react-router-dom';
import { clusterKsqlDbPath, clusterKsqlDbQueryPath } from 'lib/paths'; import { clusterKsqlDbQueryRelativePath } from 'lib/paths';
import List from 'components/KsqlDb/List/List'; import List from 'components/KsqlDb/List/List';
import Query from 'components/KsqlDb/Query/Query'; import Query from 'components/KsqlDb/Query/Query';
import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route'; import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route';
const KsqlDb: React.FC = () => { const KsqlDb: React.FC = () => {
return ( return (
<Switch> <Routes>
<BreadcrumbRoute exact path={clusterKsqlDbPath()} component={List} /> <Route
<BreadcrumbRoute index
exact element={
path={clusterKsqlDbQueryPath()} <BreadcrumbRoute>
component={Query} <List />
</BreadcrumbRoute>
}
/> />
</Switch> <Route
path={clusterKsqlDbQueryRelativePath}
element={
<BreadcrumbRoute>
<Query />
</BreadcrumbRoute>
}
/>
</Routes>
); );
}; };

View file

@ -1,12 +1,12 @@
import React, { FC, useEffect } from 'react';
import useAppParams from 'lib/hooks/useAppParams';
import * as Metrics from 'components/common/Metrics'; import * as Metrics from 'components/common/Metrics';
import PageLoader from 'components/common/PageLoader/PageLoader'; import PageLoader from 'components/common/PageLoader/PageLoader';
import ListItem from 'components/KsqlDb/List/ListItem'; import ListItem from 'components/KsqlDb/List/ListItem';
import React, { FC, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { fetchKsqlDbTables } from 'redux/reducers/ksqlDb/ksqlDbSlice'; import { fetchKsqlDbTables } from 'redux/reducers/ksqlDb/ksqlDbSlice';
import { getKsqlDbTables } from 'redux/reducers/ksqlDb/selectors'; 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 PageHeading from 'components/common/PageHeading/PageHeading';
import { Table } from 'components/common/table/Table/Table.styled'; import { Table } from 'components/common/table/Table/Table.styled';
import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell'; import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
@ -25,7 +25,7 @@ const accessors = headers.map((header) => header.accessor);
const List: FC = () => { const List: FC = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { clusterName } = useParams<{ clusterName: string }>(); const { clusterName } = useAppParams<ClusterNameRoute>();
const { rows, fetching, tablesCount, streamsCount } = const { rows, fetching, tablesCount, streamsCount } =
useSelector(getKsqlDbTables); useSelector(getKsqlDbTables);
@ -38,8 +38,7 @@ const List: FC = () => {
<> <>
<PageHeading text="KSQL DB"> <PageHeading text="KSQL DB">
<Button <Button
isLink to={clusterKsqlDbQueryRelativePath}
to={clusterKsqlDbQueryPath(clusterName)}
buttonType="primary" buttonType="primary"
buttonSize="M" buttonSize="M"
> >

View file

@ -1,23 +1,18 @@
import React from 'react'; import React from 'react';
import List from 'components/KsqlDb/List/List'; import List from 'components/KsqlDb/List/List';
import { Route, Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { clusterKsqlDbPath } from 'lib/paths'; import { clusterKsqlDbPath } from 'lib/paths';
import { render } from 'lib/testHelpers'; import { render, WithRoute } from 'lib/testHelpers';
import fetchMock from 'fetch-mock'; import fetchMock from 'fetch-mock';
import { screen, waitForElementToBeRemoved } from '@testing-library/dom'; import { screen, waitForElementToBeRemoved } from '@testing-library/dom';
const history = createMemoryHistory();
const clusterName = 'local'; const clusterName = 'local';
const renderComponent = () => { const renderComponent = () => {
history.push(clusterKsqlDbPath(clusterName));
render( render(
<Router history={history}> <WithRoute path={clusterKsqlDbPath()}>
<Route path={clusterKsqlDbPath(':clusterName')}> <List />
<List /> </WithRoute>,
</Route> { initialEntries: [clusterKsqlDbPath(clusterName)] }
</Router>
); );
}; };

View file

@ -1,12 +1,9 @@
import React from 'react'; import React from 'react';
import { Route, Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { clusterKsqlDbPath } from 'lib/paths'; import { clusterKsqlDbPath } from 'lib/paths';
import { render } from 'lib/testHelpers'; import { render, WithRoute } from 'lib/testHelpers';
import { screen } from '@testing-library/dom'; import { screen } from '@testing-library/dom';
import ListItem from 'components/KsqlDb/List/ListItem'; import ListItem from 'components/KsqlDb/List/ListItem';
const history = createMemoryHistory();
const clusterName = 'local'; const clusterName = 'local';
const renderComponent = ({ const renderComponent = ({
@ -16,13 +13,11 @@ const renderComponent = ({
accessors: string[]; accessors: string[];
data: Record<string, string>; data: Record<string, string>;
}) => { }) => {
history.push(clusterKsqlDbPath(clusterName));
render( render(
<Router history={history}> <WithRoute path={clusterKsqlDbPath()}>
<Route path={clusterKsqlDbPath(':clusterName')}> <ListItem accessors={accessors} data={data} />
<ListItem accessors={accessors} data={data} /> </WithRoute>,
</Route> { initialEntries: [clusterKsqlDbPath(clusterName)] }
</Router>
); );
}; };

View file

@ -1,5 +1,5 @@
import React, { useCallback, useEffect, FC, useState } from 'react'; import React, { useCallback, useEffect, FC, useState } from 'react';
import { useParams } from 'react-router-dom'; import useAppParams from 'lib/hooks/useAppParams';
import TableRenderer from 'components/KsqlDb/Query/renderer/TableRenderer/TableRenderer'; import TableRenderer from 'components/KsqlDb/Query/renderer/TableRenderer/TableRenderer';
import { import {
executeKsql, executeKsql,
@ -11,6 +11,7 @@ import { BASE_PARAMS } from 'lib/constants';
import { KsqlResponse, KsqlTableResponse } from 'generated-sources'; import { KsqlResponse, KsqlTableResponse } from 'generated-sources';
import { alertAdded, alertDissmissed } from 'redux/reducers/alerts/alertsSlice'; import { alertAdded, alertDissmissed } from 'redux/reducers/alerts/alertsSlice';
import { now } from 'lodash'; import { now } from 'lodash';
import { ClusterNameRoute } from 'lib/paths';
import type { FormValues } from './QueryForm/QueryForm'; import type { FormValues } from './QueryForm/QueryForm';
import * as S from './Query.styled'; import * as S from './Query.styled';
@ -61,7 +62,7 @@ export const getFormattedErrorFromTableData = (
}; };
const Query: FC = () => { const Query: FC = () => {
const { clusterName } = useParams<{ clusterName: string }>(); const { clusterName } = useAppParams<ClusterNameRoute>();
const sseRef = React.useRef<{ sse: EventSource | null; isOpen: boolean }>({ const sseRef = React.useRef<{ sse: EventSource | null; isOpen: boolean }>({
sse: null, sse: null,

View file

@ -1,4 +1,4 @@
import { render, EventSourceMock } from 'lib/testHelpers'; import { render, EventSourceMock, WithRoute } from 'lib/testHelpers';
import React from 'react'; import React from 'react';
import Query, { import Query, {
getFormattedErrorFromTableData, getFormattedErrorFromTableData,
@ -6,18 +6,17 @@ import Query, {
import { screen, within } from '@testing-library/dom'; import { screen, within } from '@testing-library/dom';
import fetchMock from 'fetch-mock'; import fetchMock from 'fetch-mock';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { Route } from 'react-router-dom';
import { clusterKsqlDbQueryPath } from 'lib/paths'; import { clusterKsqlDbQueryPath } from 'lib/paths';
import { act } from '@testing-library/react'; import { act } from '@testing-library/react';
const clusterName = 'testLocal'; const clusterName = 'testLocal';
const renderComponent = () => const renderComponent = () =>
render( render(
<Route path={clusterKsqlDbQueryPath(':clusterName')}> <WithRoute path={clusterKsqlDbQueryPath()}>
<Query /> <Query />
</Route>, </WithRoute>,
{ {
pathname: clusterKsqlDbQueryPath(clusterName), initialEntries: [clusterKsqlDbQueryPath(clusterName)],
} }
); );

View file

@ -1,15 +1,42 @@
import React from 'react'; import React from 'react';
import KsqlDb from 'components/KsqlDb/KsqlDb'; import KsqlDb from 'components/KsqlDb/KsqlDb';
import { render } from 'lib/testHelpers'; import { render, WithRoute } from 'lib/testHelpers';
import { screen } from '@testing-library/dom'; import { screen } from '@testing-library/dom';
import { clusterKsqlDbPath } from 'lib/paths'; import {
clusterKsqlDbPath,
clusterKsqlDbQueryPath,
getNonExactPath,
} from 'lib/paths';
const KSqLComponentText = {
list: 'list',
query: 'query',
};
jest.mock('components/KsqlDb/List/List', () => () => (
<div>{KSqLComponentText.list}</div>
));
jest.mock('components/KsqlDb/Query/Query', () => () => (
<div>{KSqLComponentText.query}</div>
));
describe('KsqlDb Component', () => { describe('KsqlDb Component', () => {
describe('KsqlDb', () => { const clusterName = 'clusterName';
it('to be in the document', () => { const renderComponent = (path: string) =>
render(<KsqlDb />, { pathname: clusterKsqlDbPath() }); render(
expect(screen.getByText('KSQL DB')).toBeInTheDocument(); <WithRoute path={getNonExactPath(clusterKsqlDbPath())}>
expect(screen.getByText('Execute KSQL Request')).toBeInTheDocument(); <KsqlDb />
}); </WithRoute>,
{ initialEntries: [path] }
);
it('Renders the List', () => {
renderComponent(clusterKsqlDbPath(clusterName));
expect(screen.getByText(KSqLComponentText.list)).toBeInTheDocument();
});
it('Renders the List', () => {
renderComponent(clusterKsqlDbQueryPath(clusterName));
expect(screen.getByText(KSqLComponentText.query)).toBeInTheDocument();
}); });
}); });

View file

@ -6,7 +6,6 @@ import {
clusterConsumerGroupsPath, clusterConsumerGroupsPath,
clusterSchemasPath, clusterSchemasPath,
clusterConnectorsPath, clusterConnectorsPath,
clusterConnectsPath,
clusterKsqlDbPath, clusterKsqlDbPath,
} from 'lib/paths'; } from 'lib/paths';
@ -54,10 +53,6 @@ const ClusterMenu: React.FC<Props> = ({
<ClusterMenuItem <ClusterMenuItem
to={clusterConnectorsPath(name)} to={clusterConnectorsPath(name)}
title="Kafka Connect" title="Kafka Connect"
isActive={(_, location) =>
location.pathname.startsWith(clusterConnectsPath(name)) ||
location.pathname.startsWith(clusterConnectorsPath(name))
}
/> />
)} )}
{hasFeatureConfigured(ClusterFeaturesEnum.KSQL_DB) && ( {hasFeatureConfigured(ClusterFeaturesEnum.KSQL_DB) && (

View file

@ -1,25 +1,22 @@
import React, { PropsWithChildren } from 'react'; import React, { PropsWithChildren } from 'react';
import { NavLinkProps } from 'react-router-dom';
import * as S from './Nav.styled'; import * as S from './Nav.styled';
export interface ClusterMenuItemProps { export interface ClusterMenuItemProps {
to: string; to: string;
title?: string; title?: string;
exact?: boolean;
isTopLevel?: boolean; isTopLevel?: boolean;
isActive?: NavLinkProps['isActive'];
} }
const ClusterMenuItem: React.FC<PropsWithChildren<ClusterMenuItemProps>> = ( const ClusterMenuItem: React.FC<PropsWithChildren<ClusterMenuItemProps>> = (
props props
) => { ) => {
const { to, title, children, exact, isTopLevel, isActive } = props; const { to, title, children, isTopLevel } = props;
if (to) { if (to) {
return ( return (
<S.ListItem $isTopLevel={isTopLevel}> <S.ListItem $isTopLevel={isTopLevel}>
<S.Link to={to} title={title} exact={exact} isActive={isActive}> <S.Link to={to} title={title}>
{title} {title}
</S.Link> </S.Link>
{children} {children}

View file

@ -14,13 +14,13 @@ export const Divider = styled.hr`
height: 1px; height: 1px;
`; `;
export const Link = styled(NavLink).attrs({ activeClassName: 'is-active' })( export const Link = styled(NavLink)(
({ theme, activeClassName }) => css` ({ theme }) => css`
width: 100%; width: 100%;
padding: 0.5em 0.75em; padding: 0.5em 0.75em;
cursor: pointer; cursor: pointer;
text-decoration: none; text-decoration: none;
margin: 0px 0px; margin: 0 0;
background-color: ${theme.menu.backgroundColor.normal}; background-color: ${theme.menu.backgroundColor.normal};
color: ${theme.menu.color.normal}; color: ${theme.menu.color.normal};
@ -28,8 +28,7 @@ export const Link = styled(NavLink).attrs({ activeClassName: 'is-active' })(
background-color: ${theme.menu.backgroundColor.hover}; background-color: ${theme.menu.backgroundColor.hover};
color: ${theme.menu.color.hover}; color: ${theme.menu.color.hover};
} }
&.active {
&.${activeClassName} {
background-color: ${theme.menu.backgroundColor.active}; background-color: ${theme.menu.backgroundColor.active};
color: ${theme.menu.color.active}; color: ${theme.menu.color.active};
} }

View file

@ -13,7 +13,7 @@ interface Props {
const Nav: React.FC<Props> = ({ areClustersFulfilled, clusters }) => ( const Nav: React.FC<Props> = ({ areClustersFulfilled, clusters }) => (
<aside aria-label="Sidebar Menu"> <aside aria-label="Sidebar Menu">
<S.List> <S.List>
<ClusterMenuItem exact to="/" title="Dashboard" isTopLevel /> <ClusterMenuItem to="/" title="Dashboard" isTopLevel />
</S.List> </S.List>
{areClustersFulfilled && {areClustersFulfilled &&

View file

@ -4,7 +4,7 @@ import { Cluster, ClusterFeaturesEnum } from 'generated-sources';
import { onlineClusterPayload } from 'redux/reducers/clusters/__test__/fixtures'; import { onlineClusterPayload } from 'redux/reducers/clusters/__test__/fixtures';
import ClusterMenu from 'components/Nav/ClusterMenu'; import ClusterMenu from 'components/Nav/ClusterMenu';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { clusterConnectorsPath, clusterConnectsPath } from 'lib/paths'; import { clusterConnectorsPath } from 'lib/paths';
import { render } from 'lib/testHelpers'; import { render } from 'lib/testHelpers';
describe('ClusterMenu', () => { describe('ClusterMenu', () => {
@ -55,7 +55,7 @@ describe('ClusterMenu', () => {
}); });
it('renders open cluster menu', () => { it('renders open cluster menu', () => {
render(setupComponent(onlineClusterPayload, true), { render(setupComponent(onlineClusterPayload, true), {
pathname: clusterConnectorsPath(onlineClusterPayload.name), initialEntries: [clusterConnectorsPath(onlineClusterPayload.name)],
}); });
expect(getMenuItems().length).toEqual(4); expect(getMenuItems().length).toEqual(4);
@ -70,28 +70,15 @@ describe('ClusterMenu', () => {
...onlineClusterPayload, ...onlineClusterPayload,
features: [ClusterFeaturesEnum.KAFKA_CONNECT], features: [ClusterFeaturesEnum.KAFKA_CONNECT],
}), }),
{ pathname: clusterConnectorsPath(onlineClusterPayload.name) } { initialEntries: [clusterConnectorsPath(onlineClusterPayload.name)] }
); );
expect(getMenuItems().length).toEqual(1); expect(getMenuItems().length).toEqual(1);
userEvent.click(getMenuItem()); userEvent.click(getMenuItem());
expect(getMenuItems().length).toEqual(5); expect(getMenuItems().length).toEqual(5);
expect(getKafkaConnect()).toBeInTheDocument(); const kafkaConnect = getKafkaConnect();
expect(getKafkaConnect()).toHaveClass('is-active'); expect(kafkaConnect).toBeInTheDocument();
});
it('makes Kafka Connect link active', () => {
render(
setupComponent({
...onlineClusterPayload,
features: [ClusterFeaturesEnum.KAFKA_CONNECT],
}),
{ pathname: clusterConnectsPath(onlineClusterPayload.name) }
);
expect(getMenuItems().length).toEqual(1);
userEvent.click(getMenuItem());
expect(getMenuItems().length).toEqual(5);
expect(getKafkaConnect()).toBeInTheDocument(); expect(getKafkaConnect()).toHaveClass('active');
expect(getKafkaConnect()).toHaveClass('is-active');
}); });
}); });

View file

@ -1,9 +1,9 @@
import React from 'react'; import React from 'react';
import { useHistory, useParams } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { import {
clusterSchemasPath, ClusterSubjectParam,
clusterSchemaSchemaDiffPath, clusterSchemaEditPageRelativePath,
clusterSchemaEditPath, clusterSchemaSchemaDiffRelativePath,
} from 'lib/paths'; } from 'lib/paths';
import ClusterContext from 'components/contexts/ClusterContext'; import ClusterContext from 'components/contexts/ClusterContext';
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal'; import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
@ -31,16 +31,16 @@ import { serverErrorAlertAdded } from 'redux/reducers/alerts/alertsSlice';
import { getResponse } from 'lib/errorHandling'; import { getResponse } from 'lib/errorHandling';
import { resetLoaderById } from 'redux/reducers/loader/loaderSlice'; import { resetLoaderById } from 'redux/reducers/loader/loaderSlice';
import { TableTitle } from 'components/common/table/TableTitle/TableTitle.styled'; import { TableTitle } from 'components/common/table/TableTitle/TableTitle.styled';
import useAppParams from 'lib/hooks/useAppParams';
import LatestVersionItem from './LatestVersion/LatestVersionItem'; import LatestVersionItem from './LatestVersion/LatestVersionItem';
import SchemaVersion from './SchemaVersion/SchemaVersion'; import SchemaVersion from './SchemaVersion/SchemaVersion';
const Details: React.FC = () => { const Details: React.FC = () => {
const history = useHistory(); const navigate = useNavigate();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { isReadOnly } = React.useContext(ClusterContext); const { isReadOnly } = React.useContext(ClusterContext);
const { clusterName, subject } = const { clusterName, subject } = useAppParams<ClusterSubjectParam>();
useParams<{ clusterName: string; subject: string }>();
const [ const [
isDeleteSchemaConfirmationVisible, isDeleteSchemaConfirmationVisible,
setDeleteSchemaConfirmationVisible, setDeleteSchemaConfirmationVisible,
@ -71,7 +71,7 @@ const Details: React.FC = () => {
clusterName, clusterName,
subject, subject,
}); });
history.push(clusterSchemasPath(clusterName)); navigate('../');
} catch (e) { } catch (e) {
const err = await getResponse(e as Response); const err = await getResponse(e as Response);
dispatch(serverErrorAlertAdded(err)); dispatch(serverErrorAlertAdded(err));
@ -87,21 +87,19 @@ const Details: React.FC = () => {
{!isReadOnly && ( {!isReadOnly && (
<> <>
<Button <Button
isLink
buttonSize="M" buttonSize="M"
buttonType="primary" buttonType="primary"
to={{ to={{
pathname: clusterSchemaSchemaDiffPath(clusterName, subject), pathname: clusterSchemaSchemaDiffRelativePath,
search: `leftVersion=${versions[0]?.version}&rightVersion=${versions[0]?.version}`, search: `leftVersion=${versions[0]?.version}&rightVersion=${versions[0]?.version}`,
}} }}
> >
Compare Versions Compare Versions
</Button> </Button>
<Button <Button
isLink
buttonSize="M" buttonSize="M"
buttonType="primary" buttonType="primary"
to={clusterSchemaEditPath(clusterName, subject)} to={clusterSchemaEditPageRelativePath}
> >
Edit Schema Edit Schema
</Button> </Button>

View file

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import Details from 'components/Schemas/Details/Details'; import Details from 'components/Schemas/Details/Details';
import { render } from 'lib/testHelpers'; import { render, WithRoute } from 'lib/testHelpers';
import { Route } from 'react-router-dom';
import { clusterSchemaPath } from 'lib/paths'; import { clusterSchemaPath } from 'lib/paths';
import { screen, waitFor } from '@testing-library/dom'; import { screen, waitFor } from '@testing-library/dom';
import { import {
@ -27,13 +26,13 @@ const renderComponent = (
context: ContextProps = contextInitialValue context: ContextProps = contextInitialValue
) => ) =>
render( render(
<Route path={clusterSchemaPath(':clusterName', ':subject')}> <WithRoute path={clusterSchemaPath()}>
<ClusterContext.Provider value={context}> <ClusterContext.Provider value={context}>
<Details /> <Details />
</ClusterContext.Provider> </ClusterContext.Provider>
</Route>, </WithRoute>,
{ {
pathname: clusterSchemaPath(clusterName, schemaVersion.subject), initialEntries: [clusterSchemaPath(clusterName, schemaVersion.subject)],
preloadedState: { preloadedState: {
schemas: initialState, schemas: initialState,
}, },

View file

@ -1,9 +1,9 @@
import React from 'react'; import React from 'react';
import { SchemaSubject } from 'generated-sources'; import { SchemaSubject } from 'generated-sources';
import { clusterSchemaSchemaDiffPath } from 'lib/paths'; import { clusterSchemaSchemaDiffPath, ClusterSubjectParam } from 'lib/paths';
import PageLoader from 'components/common/PageLoader/PageLoader'; import PageLoader from 'components/common/PageLoader/PageLoader';
import DiffViewer from 'components/common/DiffViewer/DiffViewer'; import DiffViewer from 'components/common/DiffViewer/DiffViewer';
import { useHistory, useParams, useLocation } from 'react-router-dom'; import { useNavigate, useLocation } from 'react-router-dom';
import { import {
fetchSchemaVersions, fetchSchemaVersions,
SCHEMAS_VERSIONS_FETCH_ACTION, SCHEMAS_VERSIONS_FETCH_ACTION,
@ -12,31 +12,32 @@ import { useForm, Controller } from 'react-hook-form';
import Select from 'components/common/Select/Select'; import Select from 'components/common/Select/Select';
import { useAppDispatch } from 'lib/hooks/redux'; import { useAppDispatch } from 'lib/hooks/redux';
import { resetLoaderById } from 'redux/reducers/loader/loaderSlice'; import { resetLoaderById } from 'redux/reducers/loader/loaderSlice';
import useAppParams from 'lib/hooks/useAppParams';
import * as S from './Diff.styled'; import * as S from './Diff.styled';
export interface DiffProps { export interface DiffProps {
leftVersionInPath?: string;
rightVersionInPath?: string;
versions: SchemaSubject[]; versions: SchemaSubject[];
areVersionsFetched: boolean; areVersionsFetched: boolean;
} }
const Diff: React.FC<DiffProps> = ({ const Diff: React.FC<DiffProps> = ({ versions, areVersionsFetched }) => {
leftVersionInPath, const { clusterName, subject } = useAppParams<ClusterSubjectParam>();
rightVersionInPath, const navigate = useNavigate();
versions,
areVersionsFetched,
}) => {
const [leftVersion, setLeftVersion] = React.useState(leftVersionInPath || '');
const [rightVersion, setRightVersion] = React.useState(
rightVersionInPath || ''
);
const history = useHistory();
const location = useLocation(); const location = useLocation();
const { clusterName, subject } = const searchParams = React.useMemo(
useParams<{ clusterName: string; subject: string }>(); () => new URLSearchParams(location.search),
[location]
);
const [leftVersion, setLeftVersion] = React.useState(
searchParams.get('leftVersion') || ''
);
const [rightVersion, setRightVersion] = React.useState(
searchParams.get('rightVersion') || ''
);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
React.useEffect(() => { React.useEffect(() => {
@ -64,11 +65,6 @@ const Diff: React.FC<DiffProps> = ({
control, control,
} = methods; } = methods;
const searchParams = React.useMemo(
() => new URLSearchParams(location.search),
[location]
);
return ( return (
<S.Section> <S.Section>
{areVersionsFetched ? ( {areVersionsFetched ? (
@ -89,7 +85,7 @@ const Diff: React.FC<DiffProps> = ({
leftVersion === '' ? versions[0].version : leftVersion leftVersion === '' ? versions[0].version : leftVersion
} }
onChange={(event) => { onChange={(event) => {
history.push( navigate(
clusterSchemaSchemaDiffPath(clusterName, subject) clusterSchemaSchemaDiffPath(clusterName, subject)
); );
searchParams.set('leftVersion', event.toString()); searchParams.set('leftVersion', event.toString());
@ -99,7 +95,7 @@ const Diff: React.FC<DiffProps> = ({
? versions[0].version ? versions[0].version
: rightVersion : rightVersion
); );
history.push({ navigate({
search: `?${searchParams.toString()}`, search: `?${searchParams.toString()}`,
}); });
setLeftVersion(event.toString()); setLeftVersion(event.toString());
@ -130,7 +126,7 @@ const Diff: React.FC<DiffProps> = ({
rightVersion === '' ? versions[0].version : rightVersion rightVersion === '' ? versions[0].version : rightVersion
} }
onChange={(event) => { onChange={(event) => {
history.push( navigate(
clusterSchemaSchemaDiffPath(clusterName, subject) clusterSchemaSchemaDiffPath(clusterName, subject)
); );
searchParams.set( searchParams.set(
@ -138,7 +134,7 @@ const Diff: React.FC<DiffProps> = ({
leftVersion === '' ? versions[0].version : leftVersion leftVersion === '' ? versions[0].version : leftVersion
); );
searchParams.set('rightVersion', event.toString()); searchParams.set('rightVersion', event.toString());
history.push({ navigate({
search: `?${searchParams.toString()}`, search: `?${searchParams.toString()}`,
}); });
setRightVersion(event.toString()); setRightVersion(event.toString());

View file

@ -1,6 +1,5 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { RootState } from 'redux/interfaces'; import { RootState } from 'redux/interfaces';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { import {
getAreSchemaVersionsFulfilled, getAreSchemaVersionsFulfilled,
selectAllSchemaVersions, selectAllSchemaVersions,
@ -8,25 +7,9 @@ import {
import Diff from './Diff'; import Diff from './Diff';
interface RouteProps { const mapStateToProps = (state: RootState) => ({
leftVersion?: string;
rightVersion?: string;
}
type OwnProps = RouteComponentProps<RouteProps>;
const mapStateToProps = (
state: RootState,
{
match: {
params: { leftVersion, rightVersion },
},
}: OwnProps
) => ({
versions: selectAllSchemaVersions(state), versions: selectAllSchemaVersions(state),
areVersionsFetched: getAreSchemaVersionsFulfilled(state), areVersionsFetched: getAreSchemaVersionsFulfilled(state),
leftVersionInPath: leftVersion,
rightVersionInPath: rightVersion,
}); });
export default withRouter(connect(mapStateToProps)(Diff)); export default connect(mapStateToProps)(Diff);

View file

@ -1,20 +1,46 @@
import React from 'react'; import React from 'react';
import Diff, { DiffProps } from 'components/Schemas/Diff/Diff'; 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 { screen } from '@testing-library/react';
import { clusterSchemaSchemaDiffPath } from 'lib/paths';
import { versions } from './fixtures'; import { versions } from './fixtures';
const defaultClusterName = 'defaultClusterName';
const defaultSubject = 'defaultSubject';
const defaultPathName = clusterSchemaSchemaDiffPath(
defaultClusterName,
defaultSubject
);
describe('Diff', () => { describe('Diff', () => {
const setupComponent = (props: DiffProps) => const setupComponent = (
render( props: DiffProps,
<Diff searchQuery: { rightVersion?: string; leftVersion?: string } = {}
versions={props.versions} ) => {
leftVersionInPath={props.leftVersionInPath} let pathname = defaultPathName;
rightVersionInPath={props.rightVersionInPath} const searchParams = new URLSearchParams(pathname);
areVersionsFetched={props.areVersionsFetched} if (searchQuery.rightVersion) {
/> searchParams.set('rightVersion', searchQuery.rightVersion);
}
if (searchQuery.leftVersion) {
searchParams.set('leftVersion', searchQuery.leftVersion);
}
pathname = `${pathname}?${searchParams.toString()}`;
return render(
<WithRoute path={clusterSchemaSchemaDiffPath()}>
<Diff
versions={props.versions}
areVersionsFetched={props.areVersionsFetched}
/>
</WithRoute>,
{
initialEntries: [pathname],
}
); );
};
describe('Container', () => { describe('Container', () => {
it('renders view', () => { it('renders view', () => {
@ -69,12 +95,13 @@ describe('Diff', () => {
}); });
describe('when schema versions are loaded and two versions in path', () => { describe('when schema versions are loaded and two versions in path', () => {
beforeEach(() => { beforeEach(() => {
setupComponent({ setupComponent(
areVersionsFetched: true, {
versions, areVersionsFetched: true,
leftVersionInPath: '1', versions,
rightVersionInPath: '2', },
}); { leftVersion: '1', rightVersion: '2' }
);
}); });
it('renders left select with version 1', () => { 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', () => { describe('when schema versions are loaded and only one versions in path', () => {
beforeEach(() => { beforeEach(() => {
setupComponent({ setupComponent(
areVersionsFetched: true, {
versions, areVersionsFetched: true,
leftVersionInPath: '1', versions,
}); },
{
leftVersion: '1',
}
);
}); });
it('renders left select with version 1', () => { it('renders left select with version 1', () => {

View file

@ -1,11 +1,11 @@
import React from 'react'; 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 { useForm, Controller, FormProvider } from 'react-hook-form';
import { import {
CompatibilityLevelCompatibilityEnum, CompatibilityLevelCompatibilityEnum,
SchemaType, SchemaType,
} from 'generated-sources'; } from 'generated-sources';
import { clusterSchemaPath } from 'lib/paths'; import { clusterSchemaPath, ClusterSubjectParam } from 'lib/paths';
import { NewSchemaSubjectRaw } from 'redux/interfaces'; import { NewSchemaSubjectRaw } from 'redux/interfaces';
import Editor from 'components/common/Editor/Editor'; import Editor from 'components/common/Editor/Editor';
import Select from 'components/common/Select/Select'; 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 { InputLabel } from 'components/common/Input/InputLabel.styled';
import PageHeading from 'components/common/PageHeading/PageHeading'; import PageHeading from 'components/common/PageHeading/PageHeading';
import { useAppDispatch, useAppSelector } from 'lib/hooks/redux'; import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
import useAppParams from 'lib/hooks/useAppParams';
import { import {
schemaAdded, schemaAdded,
schemasApiClient, schemasApiClient,
@ -30,11 +31,10 @@ import { resetLoaderById } from 'redux/reducers/loader/loaderSlice';
import * as S from './Edit.styled'; import * as S from './Edit.styled';
const Edit: React.FC = () => { const Edit: React.FC = () => {
const history = useHistory(); const navigate = useNavigate();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { clusterName, subject } = const { clusterName, subject } = useAppParams<ClusterSubjectParam>();
useParams<{ clusterName: string; subject: string }>();
const methods = useForm<NewSchemaSubjectRaw>({ mode: 'onChange' }); const methods = useForm<NewSchemaSubjectRaw>({ mode: 'onChange' });
const { const {
formState: { isDirty, isSubmitting, dirtyFields }, formState: { isDirty, isSubmitting, dirtyFields },
@ -90,7 +90,7 @@ const Edit: React.FC = () => {
); );
} }
history.push(clusterSchemaPath(clusterName, subject)); navigate(clusterSchemaPath(clusterName, subject));
} catch (e) { } catch (e) {
const err = await getResponse(e as Response); const err = await getResponse(e as Response);
dispatch(serverErrorAlertAdded(err)); dispatch(serverErrorAlertAdded(err));

View file

@ -1,12 +1,11 @@
import React from 'react'; import React from 'react';
import Edit from 'components/Schemas/Edit/Edit'; import Edit from 'components/Schemas/Edit/Edit';
import { render } from 'lib/testHelpers'; import { render, WithRoute } from 'lib/testHelpers';
import { clusterSchemaEditPath } from 'lib/paths'; import { clusterSchemaEditPath } from 'lib/paths';
import { import {
schemasInitialState, schemasInitialState,
schemaVersion, schemaVersion,
} from 'redux/reducers/schemas/__test__/fixtures'; } from 'redux/reducers/schemas/__test__/fixtures';
import { Route } from 'react-router-dom';
import { screen, waitFor } from '@testing-library/dom'; import { screen, waitFor } from '@testing-library/dom';
import ClusterContext, { import ClusterContext, {
ContextProps, ContextProps,
@ -24,13 +23,15 @@ const renderComponent = (
context: ContextProps = contextInitialValue context: ContextProps = contextInitialValue
) => ) =>
render( render(
<Route path={clusterSchemaEditPath(':clusterName', ':subject')}> <WithRoute path={clusterSchemaEditPath()}>
<ClusterContext.Provider value={context}> <ClusterContext.Provider value={context}>
<Edit /> <Edit />
</ClusterContext.Provider> </ClusterContext.Provider>
</Route>, </WithRoute>,
{ {
pathname: clusterSchemaEditPath(clusterName, schemaVersion.subject), initialEntries: [
clusterSchemaEditPath(clusterName, schemaVersion.subject),
],
preloadedState: { preloadedState: {
schemas: initialState, schemas: initialState,
}, },
@ -41,7 +42,7 @@ describe('Edit', () => {
afterEach(() => fetchMock.reset()); afterEach(() => fetchMock.reset());
describe('fetch failed', () => { describe('fetch failed', () => {
it('renders pageloader', async () => { it('renders page loader', async () => {
const schemasAPILatestMock = fetchMock.getOnce(schemasAPILatestUrl, 404); const schemasAPILatestMock = fetchMock.getOnce(schemasAPILatestUrl, 404);
await act(() => { await act(() => {
renderComponent(); renderComponent();

View file

@ -1,3 +1,4 @@
import React from 'react';
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal'; import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
import Select from 'components/common/Select/Select'; import Select from 'components/common/Select/Select';
import { CompatibilityLevelCompatibilityEnum } from 'generated-sources'; import { CompatibilityLevelCompatibilityEnum } from 'generated-sources';
@ -5,18 +6,18 @@ import { getResponse } from 'lib/errorHandling';
import { useAppDispatch } from 'lib/hooks/redux'; import { useAppDispatch } from 'lib/hooks/redux';
import usePagination from 'lib/hooks/usePagination'; import usePagination from 'lib/hooks/usePagination';
import useSearch from 'lib/hooks/useSearch'; import useSearch from 'lib/hooks/useSearch';
import React from 'react'; import useAppParams from 'lib/hooks/useAppParams';
import { useParams } from 'react-router-dom';
import { serverErrorAlertAdded } from 'redux/reducers/alerts/alertsSlice'; import { serverErrorAlertAdded } from 'redux/reducers/alerts/alertsSlice';
import { import {
fetchSchemas, fetchSchemas,
schemasApiClient, schemasApiClient,
} from 'redux/reducers/schemas/schemasSlice'; } from 'redux/reducers/schemas/schemasSlice';
import { ClusterNameRoute } from 'lib/paths';
import * as S from './GlobalSchemaSelector.styled'; import * as S from './GlobalSchemaSelector.styled';
const GlobalSchemaSelector: React.FC = () => { const GlobalSchemaSelector: React.FC = () => {
const { clusterName } = useParams<{ clusterName: string }>(); const { clusterName } = useAppParams<ClusterNameRoute>();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const [searchText] = useSearch(); const [searchText] = useSearch();
const { page, perPage } = usePagination(); const { page, perPage } = usePagination();

View file

@ -1,11 +1,10 @@
import React from 'react'; import React from 'react';
import { act, screen, waitFor, within } from '@testing-library/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 { CompatibilityLevelCompatibilityEnum } from 'generated-sources';
import GlobalSchemaSelector from 'components/Schemas/List/GlobalSchemaSelector/GlobalSchemaSelector'; import GlobalSchemaSelector from 'components/Schemas/List/GlobalSchemaSelector/GlobalSchemaSelector';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { clusterSchemasPath } from 'lib/paths'; import { clusterSchemasPath } from 'lib/paths';
import { Route } from 'react-router-dom';
import fetchMock from 'fetch-mock'; import fetchMock from 'fetch-mock';
const clusterName = 'testClusterName'; const clusterName = 'testClusterName';
@ -29,11 +28,11 @@ const expectOptionIsSelected = (option: string) => {
describe('GlobalSchemaSelector', () => { describe('GlobalSchemaSelector', () => {
const renderComponent = () => const renderComponent = () =>
render( render(
<Route path={clusterSchemasPath(':clusterName')}> <WithRoute path={clusterSchemasPath()}>
<GlobalSchemaSelector /> <GlobalSchemaSelector />
</Route>, </WithRoute>,
{ {
pathname: clusterSchemasPath(clusterName), initialEntries: [clusterSchemasPath(clusterName)],
} }
); );

View file

@ -1,12 +1,12 @@
import React from 'react'; import React from 'react';
import { useParams } from 'react-router-dom'; import { ClusterNameRoute, clusterSchemaNewRelativePath } from 'lib/paths';
import { clusterSchemaNewPath } from 'lib/paths';
import ClusterContext from 'components/contexts/ClusterContext'; import ClusterContext from 'components/contexts/ClusterContext';
import * as C from 'components/common/table/Table/Table.styled'; import * as C from 'components/common/table/Table/Table.styled';
import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell'; import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
import { Button } from 'components/common/Button/Button'; import { Button } from 'components/common/Button/Button';
import PageHeading from 'components/common/PageHeading/PageHeading'; import PageHeading from 'components/common/PageHeading/PageHeading';
import { useAppDispatch, useAppSelector } from 'lib/hooks/redux'; import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
import useAppParams from 'lib/hooks/useAppParams';
import { import {
selectAllSchemas, selectAllSchemas,
fetchSchemas, fetchSchemas,
@ -27,7 +27,7 @@ import GlobalSchemaSelector from './GlobalSchemaSelector/GlobalSchemaSelector';
const List: React.FC = () => { const List: React.FC = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { isReadOnly } = React.useContext(ClusterContext); const { isReadOnly } = React.useContext(ClusterContext);
const { clusterName } = useParams<{ clusterName: string }>(); const { clusterName } = useAppParams<ClusterNameRoute>();
const schemas = useAppSelector(selectAllSchemas); const schemas = useAppSelector(selectAllSchemas);
const isFetched = useAppSelector(getAreSchemasFulfilled); const isFetched = useAppSelector(getAreSchemasFulfilled);
@ -52,8 +52,7 @@ const List: React.FC = () => {
<Button <Button
buttonSize="M" buttonSize="M"
buttonType="primary" buttonType="primary"
isLink to={clusterSchemaNewRelativePath}
to={clusterSchemaNewPath(clusterName)}
> >
<i className="fas fa-plus" /> Create Schema <i className="fas fa-plus" /> Create Schema
</Button> </Button>

View file

@ -13,7 +13,7 @@ const ListItem: React.FC<ListItemProps> = ({
return ( return (
<tr> <tr>
<S.TableKeyLink> <S.TableKeyLink>
<NavLink exact to={`schemas/${subject}`} role="link"> <NavLink to={subject} role="link">
{subject} {subject}
</NavLink> </NavLink>
</S.TableKeyLink> </S.TableKeyLink>

View file

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import List from 'components/Schemas/List/List'; import List from 'components/Schemas/List/List';
import { render } from 'lib/testHelpers'; import { render, WithRoute } from 'lib/testHelpers';
import { Route } from 'react-router-dom';
import { clusterSchemasPath } from 'lib/paths'; import { clusterSchemasPath } from 'lib/paths';
import { act, screen } from '@testing-library/react'; import { act, screen } from '@testing-library/react';
import { import {
@ -27,13 +26,13 @@ const renderComponent = (
context: ContextProps = contextInitialValue context: ContextProps = contextInitialValue
) => ) =>
render( render(
<Route path={clusterSchemasPath(':clusterName')}> <WithRoute path={clusterSchemasPath()}>
<ClusterContext.Provider value={context}> <ClusterContext.Provider value={context}>
<List /> <List />
</ClusterContext.Provider> </ClusterContext.Provider>
</Route>, </WithRoute>,
{ {
pathname: clusterSchemasPath(clusterName), initialEntries: [clusterSchemasPath(clusterName)],
preloadedState: { preloadedState: {
schemas: initialState, schemas: initialState,
}, },

View file

@ -2,10 +2,10 @@ import React from 'react';
import { NewSchemaSubjectRaw } from 'redux/interfaces'; import { NewSchemaSubjectRaw } from 'redux/interfaces';
import { FormProvider, useForm, Controller } from 'react-hook-form'; import { FormProvider, useForm, Controller } from 'react-hook-form';
import { ErrorMessage } from '@hookform/error-message'; import { ErrorMessage } from '@hookform/error-message';
import { clusterSchemaPath } from 'lib/paths'; import { ClusterNameRoute, clusterSchemaPath } from 'lib/paths';
import { SchemaType } from 'generated-sources'; import { SchemaType } from 'generated-sources';
import { SCHEMA_NAME_VALIDATION_PATTERN } from 'lib/constants'; 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 { InputLabel } from 'components/common/Input/InputLabel.styled';
import Input from 'components/common/Input/Input'; import Input from 'components/common/Input/Input';
import { FormError } from 'components/common/Input/Input.styled'; import { FormError } from 'components/common/Input/Input.styled';
@ -18,6 +18,7 @@ import {
schemasApiClient, schemasApiClient,
} from 'redux/reducers/schemas/schemasSlice'; } from 'redux/reducers/schemas/schemasSlice';
import { useAppDispatch } from 'lib/hooks/redux'; import { useAppDispatch } from 'lib/hooks/redux';
import useAppParams from 'lib/hooks/useAppParams';
import { serverErrorAlertAdded } from 'redux/reducers/alerts/alertsSlice'; import { serverErrorAlertAdded } from 'redux/reducers/alerts/alertsSlice';
import { getResponse } from 'lib/errorHandling'; import { getResponse } from 'lib/errorHandling';
@ -30,8 +31,8 @@ const SchemaTypeOptions: Array<SelectOption> = [
]; ];
const New: React.FC = () => { const New: React.FC = () => {
const { clusterName } = useParams<{ clusterName: string }>(); const { clusterName } = useAppParams<ClusterNameRoute>();
const history = useHistory(); const navigate = useNavigate();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const methods = useForm<NewSchemaSubjectRaw>(); const methods = useForm<NewSchemaSubjectRaw>();
const { const {
@ -52,7 +53,7 @@ const New: React.FC = () => {
newSchemaSubject: { subject, schema, schemaType }, newSchemaSubject: { subject, schema, schemaType },
}); });
dispatch(schemaAdded(resp)); dispatch(schemaAdded(resp));
history.push(clusterSchemaPath(clusterName, subject)); navigate(clusterSchemaPath(clusterName, subject));
} catch (e) { } catch (e) {
const err = await getResponse(e as Response); const err = await getResponse(e as Response);
dispatch(serverErrorAlertAdded(err)); dispatch(serverErrorAlertAdded(err));

View file

@ -1,8 +1,7 @@
import React from 'react'; import React from 'react';
import New from 'components/Schemas/New/New'; import New from 'components/Schemas/New/New';
import { render } from 'lib/testHelpers'; import { render, WithRoute } from 'lib/testHelpers';
import { clusterSchemaNewPath } from 'lib/paths'; import { clusterSchemaNewPath } from 'lib/paths';
import { Route } from 'react-router-dom';
import { screen } from '@testing-library/dom'; import { screen } from '@testing-library/dom';
const clusterName = 'local'; const clusterName = 'local';
@ -10,11 +9,11 @@ const clusterName = 'local';
describe('New Component', () => { describe('New Component', () => {
beforeEach(() => { beforeEach(() => {
render( render(
<Route path={clusterSchemaNewPath(':clusterName')}> <WithRoute path={clusterSchemaNewPath()}>
<New /> <New />
</Route>, </WithRoute>,
{ {
pathname: clusterSchemaNewPath(clusterName), initialEntries: [clusterSchemaNewPath(clusterName)],
} }
); );
}); });

View file

@ -1,11 +1,9 @@
import React from 'react'; import React from 'react';
import { Switch } from 'react-router-dom'; import { Route, Routes } from 'react-router-dom';
import { import {
clusterSchemaNewPath, clusterSchemaEditRelativePath,
clusterSchemaPath, clusterSchemaNewRelativePath,
clusterSchemaEditPath, RouteParams,
clusterSchemasPath,
clusterSchemaSchemaDiffPath,
} from 'lib/paths'; } from 'lib/paths';
import List from 'components/Schemas/List/List'; import List from 'components/Schemas/List/List';
import Details from 'components/Schemas/Details/Details'; import Details from 'components/Schemas/Details/Details';
@ -16,33 +14,48 @@ import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route';
const Schemas: React.FC = () => { const Schemas: React.FC = () => {
return ( return (
<Switch> <Routes>
<BreadcrumbRoute <Route
exact index
path={clusterSchemasPath(':clusterName')} element={
component={List} <BreadcrumbRoute>
<List />
</BreadcrumbRoute>
}
/> />
<BreadcrumbRoute <Route
exact path={clusterSchemaNewRelativePath}
path={clusterSchemaNewPath(':clusterName')} element={
component={New} <BreadcrumbRoute>
<New />
</BreadcrumbRoute>
}
/> />
<BreadcrumbRoute <Route
exact path={RouteParams.subject}
path={clusterSchemaPath(':clusterName', ':subject')} element={
component={Details} <BreadcrumbRoute>
<Details />
</BreadcrumbRoute>
}
/> />
<BreadcrumbRoute <Route
exact path={clusterSchemaEditRelativePath}
path={clusterSchemaEditPath(':clusterName', ':subject')} element={
component={Edit} <BreadcrumbRoute>
<Edit />
</BreadcrumbRoute>
}
/> />
<BreadcrumbRoute <Route
exact path={clusterSchemaEditRelativePath}
path={clusterSchemaSchemaDiffPath(':clusterName', ':subject')} element={
component={DiffContainer} <BreadcrumbRoute>
<DiffContainer />
</BreadcrumbRoute>
}
/> />
</Switch> </Routes>
); );
}; };

View file

@ -1,32 +1,46 @@
import React from 'react'; import React from 'react';
import Schemas from 'components/Schemas/Schemas'; import Schemas from 'components/Schemas/Schemas';
import { render } from 'lib/testHelpers'; import { render, WithRoute } from 'lib/testHelpers';
import { import {
clusterPath,
clusterSchemaEditPath, clusterSchemaEditPath,
clusterSchemaNewPath, clusterSchemaNewPath,
clusterSchemaPath, clusterSchemaPath,
clusterSchemasPath, clusterSchemasPath,
getNonExactPath,
} from 'lib/paths'; } from 'lib/paths';
import { screen, waitFor } from '@testing-library/dom'; import { screen, waitFor } from '@testing-library/dom';
import { Route } from 'react-router-dom';
import fetchMock from 'fetch-mock'; import fetchMock from 'fetch-mock';
import { schemaVersion } from 'redux/reducers/schemas/__test__/fixtures'; import { schemaVersion } from 'redux/reducers/schemas/__test__/fixtures';
const renderComponent = (pathname: string) => const renderComponent = (pathname: string) =>
render( render(
<Route path={clusterPath(':clusterName')}> <WithRoute path={getNonExactPath(clusterSchemasPath())}>
<Schemas /> <Schemas />
</Route>, </WithRoute>,
{ pathname } { initialEntries: [pathname] }
); );
const clusterName = 'secondLocal'; const clusterName = 'secondLocal';
jest.mock('components/Schemas/List/List', () => () => <div>List</div>); const SchemaCompText = {
jest.mock('components/Schemas/Details/Details', () => () => <div>Details</div>); List: 'List',
jest.mock('components/Schemas/New/New', () => () => <div>New</div>); Details: 'Details',
jest.mock('components/Schemas/Edit/Edit', () => () => <div>Edit</div>); New: 'New',
Edit: 'Edit',
};
jest.mock('components/Schemas/List/List', () => () => (
<div>{SchemaCompText.List}</div>
));
jest.mock('components/Schemas/Details/Details', () => () => (
<div>{SchemaCompText.Details}</div>
));
jest.mock('components/Schemas/New/New', () => () => (
<div>{SchemaCompText.New}</div>
));
jest.mock('components/Schemas/Edit/Edit', () => () => (
<div>{SchemaCompText.Edit}</div>
));
describe('Schemas', () => { describe('Schemas', () => {
beforeEach(() => { beforeEach(() => {
@ -35,20 +49,26 @@ describe('Schemas', () => {
afterEach(() => fetchMock.restore()); afterEach(() => fetchMock.restore());
it('renders List', async () => { it('renders List', async () => {
renderComponent(clusterSchemasPath(clusterName)); renderComponent(clusterSchemasPath(clusterName));
await waitFor(() => expect(screen.queryByText('List')).toBeInTheDocument()); await waitFor(() =>
expect(screen.queryByText(SchemaCompText.List)).toBeInTheDocument()
);
}); });
it('renders New', async () => { it('renders New', async () => {
renderComponent(clusterSchemaNewPath(clusterName)); renderComponent(clusterSchemaNewPath(clusterName));
await waitFor(() => expect(screen.queryByText('New')).toBeInTheDocument()); await waitFor(() =>
expect(screen.queryByText(SchemaCompText.New)).toBeInTheDocument()
);
}); });
it('renders Details', async () => { it('renders Details', async () => {
renderComponent(clusterSchemaPath(clusterName, schemaVersion.subject)); renderComponent(clusterSchemaPath(clusterName, schemaVersion.subject));
await waitFor(() => await waitFor(() =>
expect(screen.queryByText('Details')).toBeInTheDocument() expect(screen.queryByText(SchemaCompText.Details)).toBeInTheDocument()
); );
}); });
it('renders Edit', async () => { it('renders Edit', async () => {
renderComponent(clusterSchemaEditPath(clusterName, schemaVersion.subject)); renderComponent(clusterSchemaEditPath(clusterName, schemaVersion.subject));
await waitFor(() => expect(screen.queryByText('Edit')).toBeInTheDocument()); await waitFor(() =>
expect(screen.queryByText(SchemaCompText.Edit)).toBeInTheDocument()
);
}); });
}); });

View file

@ -2,10 +2,10 @@ import { Td } from 'components/common/table/TableHeaderCell/TableHeaderCell.styl
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
export const Link = styled(NavLink).attrs({ activeClassName: 'is-active' })<{ export const Link = styled(NavLink)<{
$isInternal?: boolean; $isInternal?: boolean;
}>( }>(
({ theme, activeClassName, $isInternal }) => css` ({ theme, $isInternal }) => css`
color: ${theme.topicsList.color.normal}; color: ${theme.topicsList.color.normal};
font-weight: 500; font-weight: 500;
padding-left: ${$isInternal ? '5px' : 0}; padding-left: ${$isInternal ? '5px' : 0};
@ -15,7 +15,7 @@ export const Link = styled(NavLink).attrs({ activeClassName: 'is-active' })<{
color: ${theme.topicsList.color.hover}; color: ${theme.topicsList.color.hover};
} }
&.${activeClassName} { &.active {
background-color: ${theme.topicsList.backgroundColor.active}; background-color: ${theme.topicsList.backgroundColor.active};
color: ${theme.topicsList.color.active}; color: ${theme.topicsList.color.active};
} }

View file

@ -1,11 +1,16 @@
import React from 'react'; import React from 'react';
import { useHistory, useParams } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import useAppParams from 'lib/hooks/useAppParams';
import { import {
TopicWithDetailedInfo, TopicWithDetailedInfo,
ClusterName, ClusterName,
TopicName, TopicName,
} from 'redux/interfaces'; } from 'redux/interfaces';
import { clusterTopicCopyPath, clusterTopicNewPath } from 'lib/paths'; import {
ClusterNameRoute,
clusterTopicCopyRelativePath,
clusterTopicNewRelativePath,
} from 'lib/paths';
import usePagination from 'lib/hooks/usePagination'; import usePagination from 'lib/hooks/usePagination';
import useModal from 'lib/hooks/useModal'; import useModal from 'lib/hooks/useModal';
import ClusterContext from 'components/contexts/ClusterContext'; import ClusterContext from 'components/contexts/ClusterContext';
@ -88,13 +93,15 @@ const List: React.FC<TopicsListProps> = ({
}) => { }) => {
const { isReadOnly, isTopicDeletionAllowed } = const { isReadOnly, isTopicDeletionAllowed } =
React.useContext(ClusterContext); React.useContext(ClusterContext);
const { clusterName } = useParams<{ clusterName: ClusterName }>(); const { clusterName } = useAppParams<ClusterNameRoute>();
const { page, perPage, pathname } = usePagination(); const { page, perPage } = usePagination();
const [showInternal, setShowInternal] = React.useState<boolean>( const [showInternal, setShowInternal] = React.useState<boolean>(
!localStorage.getItem('hideInternalTopics') && true !localStorage.getItem('hideInternalTopics') && true
); );
const [cachedPage, setCachedPage] = React.useState<number | null>(null); const [cachedPage, setCachedPage] = React.useState<number | null>(
const history = useHistory(); page || null
);
const navigate = useNavigate();
const topicsListParams = React.useMemo( const topicsListParams = React.useMemo(
() => ({ () => ({
@ -154,7 +161,9 @@ const List: React.FC<TopicsListProps> = ({
} }
setShowInternal(!showInternal); setShowInternal(!showInternal);
history.push(`${pathname}?page=1&perPage=${perPage || PER_PAGE}`); navigate({
search: `?page=1&perPage=${perPage || PER_PAGE}`,
});
}; };
const [confirmationModal, setConfirmationModal] = React.useState< const [confirmationModal, setConfirmationModal] = React.useState<
@ -176,9 +185,9 @@ const List: React.FC<TopicsListProps> = ({
const newPageQuery = !searchString && cachedPage ? cachedPage : 1; const newPageQuery = !searchString && cachedPage ? cachedPage : 1;
history.push( navigate({
`${pathname}?page=${newPageQuery}&perPage=${perPage || PER_PAGE}` search: `?page=${newPageQuery}&perPage=${perPage || PER_PAGE}`,
); });
}; };
const deleteOrPurgeConfirmationHandler = () => { const deleteOrPurgeConfirmationHandler = () => {
const selectedIds = Array.from(tableState.selectedIds); const selectedIds = Array.from(tableState.selectedIds);
@ -283,8 +292,7 @@ const List: React.FC<TopicsListProps> = ({
<Button <Button
buttonType="primary" buttonType="primary"
buttonSize="M" buttonSize="M"
isLink to={clusterTopicNewRelativePath}
to={clusterTopicNewPath(clusterName)}
> >
<i className="fas fa-plus" /> Add a Topic <i className="fas fa-plus" /> Add a Topic
</Button> </Button>
@ -331,9 +339,8 @@ const List: React.FC<TopicsListProps> = ({
<Button <Button
buttonSize="M" buttonSize="M"
buttonType="secondary" buttonType="secondary"
isLink
to={{ to={{
pathname: clusterTopicCopyPath(clusterName), pathname: clusterTopicCopyRelativePath,
search: `?${getSelectedTopic()}`, search: `?${getSelectedTopic()}`,
}} }}
> >

View file

@ -12,7 +12,7 @@ export const TitleCell: React.FC<
return ( return (
<> <>
{internal && <Tag color="gray">IN</Tag>} {internal && <Tag color="gray">IN</Tag>}
<S.Link exact to={`topics/${name}`} $isInternal={internal}> <S.Link to={name} $isInternal={internal}>
{name} {name}
</S.Link> </S.Link>
</> </>

View file

@ -1,17 +1,26 @@
import React from 'react'; import React from 'react';
import { render } from 'lib/testHelpers'; import { render, WithRoute } from 'lib/testHelpers';
import { screen, waitFor, within } from '@testing-library/react'; import { act, screen, waitFor, within } from '@testing-library/react';
import { Route, Router, StaticRouter } from 'react-router-dom';
import ClusterContext, { import ClusterContext, {
ContextProps, ContextProps,
} from 'components/contexts/ClusterContext'; } from 'components/contexts/ClusterContext';
import List, { TopicsListProps } from 'components/Topics/List/List'; import List, { TopicsListProps } from 'components/Topics/List/List';
import { createMemoryHistory } from 'history';
import { externalTopicPayload } from 'redux/reducers/topics/__test__/fixtures'; import { externalTopicPayload } from 'redux/reducers/topics/__test__/fixtures';
import { CleanUpPolicy, SortOrder } from 'generated-sources'; import { CleanUpPolicy, SortOrder } from 'generated-sources';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { clusterTopicsPath } from 'lib/paths';
const mockNavigate = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: () => mockNavigate,
}));
describe('List', () => { describe('List', () => {
afterEach(() => {
mockNavigate.mockClear();
});
const setupComponent = (props: Partial<TopicsListProps> = {}) => ( const setupComponent = (props: Partial<TopicsListProps> = {}) => (
<List <List
areTopicsFetching={false} areTopicsFetching={false}
@ -32,15 +41,13 @@ describe('List', () => {
/> />
); );
const historyMock = createMemoryHistory();
const renderComponentWithProviders = ( const renderComponentWithProviders = (
contextProps: Partial<ContextProps> = {}, contextProps: Partial<ContextProps> = {},
props: Partial<TopicsListProps> = {}, props: Partial<TopicsListProps> = {},
history = historyMock queryParams = ''
) => ) =>
render( render(
<Router history={history}> <WithRoute path={clusterTopicsPath()}>
<ClusterContext.Provider <ClusterContext.Provider
value={{ value={{
isReadOnly: true, isReadOnly: true,
@ -52,7 +59,8 @@ describe('List', () => {
> >
{setupComponent(props)} {setupComponent(props)}
</ClusterContext.Provider> </ClusterContext.Provider>
</Router> </WithRoute>,
{ initialEntries: [`${clusterTopicsPath('test')}${queryParams}`] }
); );
describe('when it has readonly flag', () => { describe('when it has readonly flag', () => {
@ -112,6 +120,10 @@ describe('List', () => {
await waitFor(() => { await waitFor(() => {
expect(fetchTopicsList).toHaveBeenLastCalledWith({ expect(fetchTopicsList).toHaveBeenLastCalledWith({
clusterName: 'test',
orderBy: undefined,
page: undefined,
perPage: undefined,
search: '', search: '',
showInternal: value === 'on', showInternal: value === 'on',
sortOrder: SortOrder.ASC, sortOrder: SortOrder.ASC,
@ -119,47 +131,43 @@ describe('List', () => {
}); });
}); });
it('should reset page query param on show internal toggle change', () => { it('should reset page query param on show internal toggle change', async () => {
const mockedHistory = createMemoryHistory(); renderComponentWithProviders({ isReadOnly: false }, { fetchTopicsList });
jest.spyOn(mockedHistory, 'push');
renderComponentWithProviders(
{ isReadOnly: false },
{ fetchTopicsList },
mockedHistory
);
const internalCheckBox: HTMLInputElement = screen.getByRole('checkbox'); const internalCheckBox: HTMLInputElement = screen.getByRole('checkbox');
userEvent.click(internalCheckBox); userEvent.click(internalCheckBox);
expect(mockedHistory.push).toHaveBeenCalledWith('/?page=1&perPage=25'); expect(mockNavigate).toHaveBeenCalledWith({
search: '?page=1&perPage=25',
});
}); });
it('should set cached page query param on show internal toggle change', async () => { it('should set cached page query param on show internal toggle change', async () => {
const mockedHistory = createMemoryHistory();
jest.spyOn(mockedHistory, 'push');
const cachedPage = 5; const cachedPage = 5;
mockedHistory.push(`/?page=${cachedPage}&perPage=25`);
renderComponentWithProviders( renderComponentWithProviders(
{ isReadOnly: false }, { isReadOnly: false },
{ fetchTopicsList, totalPages: 10 }, { fetchTopicsList, totalPages: 10 },
mockedHistory `?page=${cachedPage}&perPage=25`
); );
const searchInput = screen.getByPlaceholderText('Search by Topic Name'); const searchInput = screen.getByPlaceholderText('Search by Topic Name');
userEvent.type(searchInput, 'nonEmptyString'); userEvent.type(searchInput, 'nonEmptyString');
await waitFor(() => { await waitFor(() => {
expect(mockedHistory.push).toHaveBeenCalledWith('/?page=1&perPage=25'); expect(mockNavigate).toHaveBeenCalledWith({
search: '?page=1&perPage=25',
});
}); });
userEvent.clear(searchInput); await act(() => {
userEvent.clear(searchInput);
});
await waitFor(() => { await waitFor(() => {
expect(mockedHistory.push).toHaveBeenCalledWith( expect(mockNavigate).toHaveBeenLastCalledWith({
`/?page=${cachedPage}&perPage=25` search: `?page=${cachedPage}&perPage=25`,
); });
}); });
}); });
}); });
@ -173,38 +181,37 @@ describe('List', () => {
const fetchTopicsList = jest.fn(); const fetchTopicsList = jest.fn();
jest.useFakeTimers(); jest.useFakeTimers();
const pathname = '/ui/clusters/local/topics'; const pathname = clusterTopicsPath('local');
beforeEach(() => { beforeEach(() => {
render( render(
<StaticRouter location={{ pathname }}> <WithRoute path={clusterTopicsPath()}>
<Route path="/ui/clusters/:clusterName"> <ClusterContext.Provider
<ClusterContext.Provider value={{
value={{ isReadOnly: false,
isReadOnly: false, hasKafkaConnectConfigured: true,
hasKafkaConnectConfigured: true, hasSchemaRegistryConfigured: true,
hasSchemaRegistryConfigured: true, isTopicDeletionAllowed: true,
isTopicDeletionAllowed: true, }}
}} >
> {setupComponent({
{setupComponent({ topics: [
topics: [ {
{ ...externalTopicPayload,
...externalTopicPayload, cleanUpPolicy: CleanUpPolicy.DELETE,
cleanUpPolicy: CleanUpPolicy.DELETE, },
}, { ...externalTopicPayload, name: 'external.topic2' },
{ ...externalTopicPayload, name: 'external.topic2' }, ],
], deleteTopics: mockDeleteTopics,
deleteTopics: mockDeleteTopics, clearTopicsMessages: mockClearTopicsMessages,
clearTopicsMessages: mockClearTopicsMessages, recreateTopic: mockRecreate,
recreateTopic: mockRecreate, deleteTopic: mockDeleteTopic,
deleteTopic: mockDeleteTopic, clearTopicMessages: mockClearTopic,
clearTopicMessages: mockClearTopic, fetchTopicsList,
fetchTopicsList, })}
})} </ClusterContext.Provider>
</ClusterContext.Provider> </WithRoute>,
</Route> { initialEntries: [pathname] }
</StaticRouter>
); );
}); });

View file

@ -1,18 +1,15 @@
import React from 'react'; import React from 'react';
import { ClusterName, TopicFormData } from 'redux/interfaces'; import { TopicFormData } from 'redux/interfaces';
import { useForm, FormProvider } from 'react-hook-form'; import { useForm, FormProvider } from 'react-hook-form';
import { clusterTopicPath } from 'lib/paths'; import { ClusterNameRoute } from 'lib/paths';
import TopicForm from 'components/Topics/shared/Form/TopicForm'; import TopicForm from 'components/Topics/shared/Form/TopicForm';
import { useNavigate, useLocation } from 'react-router-dom';
import { createTopic } from 'redux/reducers/topics/topicsSlice'; import { createTopic } from 'redux/reducers/topics/topicsSlice';
import { useHistory, useLocation, useParams } from 'react-router-dom';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { topicFormValidationSchema } from 'lib/yupExtended'; import { topicFormValidationSchema } from 'lib/yupExtended';
import PageHeading from 'components/common/PageHeading/PageHeading'; import PageHeading from 'components/common/PageHeading/PageHeading';
import { useAppDispatch } from 'lib/hooks/redux'; import { useAppDispatch } from 'lib/hooks/redux';
import useAppParams from 'lib/hooks/useAppParams';
interface RouterParams {
clusterName: ClusterName;
}
enum Filters { enum Filters {
NAME = 'name', NAME = 'name',
@ -28,8 +25,9 @@ const New: React.FC = () => {
resolver: yupResolver(topicFormValidationSchema), resolver: yupResolver(topicFormValidationSchema),
}); });
const { clusterName } = useParams<RouterParams>(); const { clusterName } = useAppParams<ClusterNameRoute>();
const history = useHistory(); const navigate = useNavigate();
const { search } = useLocation(); const { search } = useLocation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const params = new URLSearchParams(search); const params = new URLSearchParams(search);
@ -44,7 +42,7 @@ const New: React.FC = () => {
const { meta } = await dispatch(createTopic({ clusterName, data })); const { meta } = await dispatch(createTopic({ clusterName, data }));
if (meta.requestStatus === 'fulfilled') { if (meta.requestStatus === 'fulfilled') {
history.push(clusterTopicPath(clusterName, data.name)); navigate(`../${data.name}`);
} }
}; };

View file

@ -1,11 +1,10 @@
import React from 'react'; import React from 'react';
import New from 'components/Topics/New/New'; import New from 'components/Topics/New/New';
import { Route, Router } from 'react-router-dom'; import { Route, Routes } from 'react-router-dom';
import configureStore from 'redux-mock-store'; import configureStore from 'redux-mock-store';
import { RootState } from 'redux/interfaces'; import { RootState } from 'redux/interfaces';
import * as redux from 'react-redux'; import * as redux from 'react-redux';
import { act, screen, waitFor } from '@testing-library/react'; import { act, screen, waitFor } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import fetchMock from 'fetch-mock-jest'; import fetchMock from 'fetch-mock-jest';
import { import {
clusterTopicCopyPath, clusterTopicCopyPath,
@ -24,25 +23,37 @@ const topicName = 'test-topic';
const initialState: Partial<RootState> = {}; const initialState: Partial<RootState> = {};
const storeMock = mockStore(initialState); const storeMock = mockStore(initialState);
const historyMock = createMemoryHistory();
const renderComponent = (history = historyMock, store = storeMock) => const mockNavigate = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: () => mockNavigate,
}));
const renderComponent = (path: string, store = storeMock) =>
render( render(
<Router history={history}> <Routes>
<Route path={clusterTopicNewPath(':clusterName')}> <Route
<Provider store={store}> path={clusterTopicNewPath()}
<New /> element={
</Provider> <Provider store={store}>
</Route> <New />
<Route path={clusterTopicCopyPath(':clusterName')}> </Provider>
<Provider store={store}> }
<New /> />
</Provider>
</Route> <Route
<Route path={clusterTopicPath(':clusterName', ':topicName')}> path={clusterTopicCopyPath()}
New topic path element={
</Route> <Provider store={store}>
</Router> <New />
</Provider>
}
/>
<Route path={clusterTopicPath()} element="New topic path" />
</Routes>,
{ initialEntries: [path] }
); );
describe('New', () => { describe('New', () => {
@ -50,38 +61,26 @@ describe('New', () => {
fetchMock.reset(); fetchMock.reset();
}); });
afterEach(() => {
mockNavigate.mockClear();
});
it('checks header for create new', async () => { it('checks header for create new', async () => {
const mockedHistory = createMemoryHistory({ renderComponent(clusterTopicNewPath(clusterName));
initialEntries: [clusterTopicNewPath(clusterName)],
});
renderComponent(mockedHistory);
expect( expect(
screen.getByRole('heading', { name: 'Create new Topic' }) screen.getByRole('heading', { name: 'Create new Topic' })
).toHaveTextContent('Create new Topic'); ).toHaveTextContent('Create new Topic');
}); });
it('checks header for copy', async () => { it('checks header for copy', async () => {
const mockedHistory = createMemoryHistory({ renderComponent(`${clusterTopicCopyPath(clusterName)}?name=test`);
initialEntries: [
{
pathname: clusterTopicCopyPath(clusterName),
search: `?name=test`,
},
],
});
renderComponent(mockedHistory);
expect( expect(
screen.getByRole('heading', { name: 'Copy Topic' }) screen.getByRole('heading', { name: 'Copy Topic' })
).toHaveTextContent('Copy Topic'); ).toHaveTextContent('Copy Topic');
}); });
it('validates form', async () => { it('validates form', async () => {
const mockedHistory = createMemoryHistory({ renderComponent(clusterTopicNewPath(clusterName));
initialEntries: [clusterTopicNewPath(clusterName)],
});
jest.spyOn(mockedHistory, 'push');
renderComponent(mockedHistory);
await waitFor(() => { await waitFor(() => {
userEvent.click(screen.getByText(/submit/i)); userEvent.click(screen.getByText(/submit/i));
@ -90,7 +89,7 @@ describe('New', () => {
expect(screen.getByText('name is a required field')).toBeInTheDocument(); expect(screen.getByText('name is a required field')).toBeInTheDocument();
}); });
await waitFor(() => { await waitFor(() => {
expect(mockedHistory.push).toBeCalledTimes(0); expect(mockNavigate).not.toHaveBeenCalled();
}); });
}); });
@ -101,14 +100,8 @@ describe('New', () => {
})) as jest.Mock; })) as jest.Mock;
useDispatchSpy.mockReturnValue(useDispatchMock); useDispatchSpy.mockReturnValue(useDispatchMock);
const mockedHistory = createMemoryHistory({
initialEntries: [clusterTopicNewPath(clusterName)],
});
jest.spyOn(mockedHistory, 'push');
await act(() => { await act(() => {
renderComponent(mockedHistory); renderComponent(clusterTopicNewPath(clusterName));
}); });
await waitFor(() => { await waitFor(() => {
@ -116,14 +109,12 @@ describe('New', () => {
userEvent.click(screen.getByText(/submit/i)); userEvent.click(screen.getByText(/submit/i));
}); });
await waitFor(() => await waitFor(() => {
expect(mockedHistory.location.pathname).toBe( expect(mockNavigate).toBeCalledTimes(1);
clusterTopicPath(clusterName, topicName) expect(mockNavigate).toHaveBeenLastCalledWith(`../${topicName}`);
) });
);
expect(useDispatchMock).toHaveBeenCalledTimes(1); expect(useDispatchMock).toHaveBeenCalledTimes(1);
expect(mockedHistory.push).toBeCalledTimes(1);
}); });
it('does not redirect page when request is not fulfilled', async () => { it('does not redirect page when request is not fulfilled', async () => {
@ -131,16 +122,11 @@ describe('New', () => {
const useDispatchMock = jest.fn(() => ({ const useDispatchMock = jest.fn(() => ({
meta: { requestStatus: 'pending' }, meta: { requestStatus: 'pending' },
})) as jest.Mock; })) as jest.Mock;
useDispatchSpy.mockReturnValue(useDispatchMock); useDispatchSpy.mockReturnValue(useDispatchMock);
const mockedHistory = createMemoryHistory({
initialEntries: [clusterTopicNewPath(clusterName)],
});
jest.spyOn(mockedHistory, 'push');
await act(() => { await act(() => {
renderComponent(mockedHistory); renderComponent(clusterTopicNewPath(clusterName));
}); });
await waitFor(() => { await waitFor(() => {
@ -148,24 +134,16 @@ describe('New', () => {
userEvent.click(screen.getByText(/submit/i)); userEvent.click(screen.getByText(/submit/i));
}); });
await waitFor(() => await waitFor(() => {
expect(mockedHistory.location.pathname).toBe( expect(mockNavigate).not.toHaveBeenCalled();
clusterTopicNewPath(clusterName) });
)
);
}); });
it('submits valid form that result in an error', async () => { it('submits valid form that result in an error', async () => {
const useDispatchSpy = jest.spyOn(redux, 'useDispatch'); const useDispatchSpy = jest.spyOn(redux, 'useDispatch');
const useDispatchMock = jest.fn(); const useDispatchMock = jest.fn();
useDispatchSpy.mockReturnValue(useDispatchMock); useDispatchSpy.mockReturnValue(useDispatchMock);
renderComponent(clusterTopicNewPath(clusterName));
const mockedHistory = createMemoryHistory({
initialEntries: [clusterTopicNewPath(clusterName)],
});
jest.spyOn(mockedHistory, 'push');
renderComponent(mockedHistory);
await act(() => { await act(() => {
userEvent.type(screen.getByPlaceholderText('Topic Name'), topicName); userEvent.type(screen.getByPlaceholderText('Topic Name'), topicName);
@ -173,6 +151,6 @@ describe('New', () => {
}); });
expect(useDispatchMock).toHaveBeenCalledTimes(1); expect(useDispatchMock).toHaveBeenCalledTimes(1);
expect(mockedHistory.push).toBeCalledTimes(0); expect(mockNavigate).not.toHaveBeenCalled();
}); });
}); });

View file

@ -1,19 +1,18 @@
import React from 'react'; import React from 'react';
import { Topic, TopicDetails, ConsumerGroup } from 'generated-sources'; import { Link } from 'react-router-dom';
import { ClusterName, TopicName } from 'redux/interfaces'; import { ClusterName, TopicName } from 'redux/interfaces';
import { clusterConsumerGroupsPath } from 'lib/paths'; import { clusterConsumerGroupsPath, RouteParamsClusterTopic } from 'lib/paths';
import { Table } from 'components/common/table/Table/Table.styled'; import { Table } from 'components/common/table/Table/Table.styled';
import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell'; import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
import { Tag } from 'components/common/Tag/Tag.styled'; import { Tag } from 'components/common/Tag/Tag.styled';
import { TableKeyLink } from 'components/common/table/Table/TableKeyLink.styled'; import { TableKeyLink } from 'components/common/table/Table/TableKeyLink.styled';
import { Link } from 'react-router-dom';
import PageLoader from 'components/common/PageLoader/PageLoader'; import PageLoader from 'components/common/PageLoader/PageLoader';
import getTagColor from 'components/common/Tag/getTagColor'; import getTagColor from 'components/common/Tag/getTagColor';
import { useAppSelector } from 'lib/hooks/redux';
import { getTopicConsumerGroups } from 'redux/reducers/topics/selectors';
import useAppParams from 'lib/hooks/useAppParams';
export interface Props extends Topic, TopicDetails { export interface Props {
clusterName: ClusterName;
topicName: TopicName;
consumerGroups: ConsumerGroup[];
isFetched: boolean; isFetched: boolean;
fetchTopicConsumerGroups(payload: { fetchTopicConsumerGroups(payload: {
clusterName: ClusterName; clusterName: ClusterName;
@ -22,12 +21,15 @@ export interface Props extends Topic, TopicDetails {
} }
const TopicConsumerGroups: React.FC<Props> = ({ const TopicConsumerGroups: React.FC<Props> = ({
consumerGroups,
fetchTopicConsumerGroups, fetchTopicConsumerGroups,
clusterName,
topicName,
isFetched, isFetched,
}) => { }) => {
const { clusterName, topicName } = useAppParams<RouteParamsClusterTopic>();
const consumerGroups = useAppSelector((state) =>
getTopicConsumerGroups(state, topicName)
);
React.useEffect(() => { React.useEffect(() => {
fetchTopicConsumerGroups({ clusterName, topicName }); fetchTopicConsumerGroups({ clusterName, topicName });
}, [clusterName, fetchTopicConsumerGroups, topicName]); }, [clusterName, fetchTopicConsumerGroups, topicName]);

View file

@ -1,31 +1,10 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { RootState, TopicName, ClusterName } from 'redux/interfaces'; import { RootState } from 'redux/interfaces';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import { fetchTopicConsumerGroups } from 'redux/reducers/topics/topicsSlice'; import { fetchTopicConsumerGroups } from 'redux/reducers/topics/topicsSlice';
import TopicConsumerGroups from 'components/Topics/Topic/Details/ConsumerGroups/TopicConsumerGroups'; import TopicConsumerGroups from 'components/Topics/Topic/Details/ConsumerGroups/TopicConsumerGroups';
import { import { getTopicsConsumerGroupsFetched } from 'redux/reducers/topics/selectors';
getTopicConsumerGroups,
getTopicsConsumerGroupsFetched,
} from 'redux/reducers/topics/selectors';
interface RouteProps { const mapStateToProps = (state: RootState) => ({
clusterName: ClusterName;
topicName: TopicName;
}
type OwnProps = RouteComponentProps<RouteProps>;
const mapStateToProps = (
state: RootState,
{
match: {
params: { topicName, clusterName },
},
}: OwnProps
) => ({
consumerGroups: getTopicConsumerGroups(state, topicName),
topicName,
clusterName,
isFetched: getTopicsConsumerGroupsFetched(state), isFetched: getTopicsConsumerGroupsFetched(state),
}); });
@ -33,6 +12,7 @@ const mapDispatchToProps = {
fetchTopicConsumerGroups, fetchTopicConsumerGroups,
}; };
export default withRouter( export default connect(
connect(mapStateToProps, mapDispatchToProps)(TopicConsumerGroups) mapStateToProps,
); mapDispatchToProps
)(TopicConsumerGroups);

View file

@ -1,10 +1,13 @@
import React from 'react'; import React from 'react';
import { render } from 'lib/testHelpers'; import { render, WithRoute } from 'lib/testHelpers';
import { screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import ConsumerGroups, { import TopicConsumerGroups, {
Props, Props,
} from 'components/Topics/Topic/Details/ConsumerGroups/TopicConsumerGroups'; } from 'components/Topics/Topic/Details/ConsumerGroups/TopicConsumerGroups';
import { ConsumerGroupState } from 'generated-sources'; import { ConsumerGroup, ConsumerGroupState } from 'generated-sources';
import { getTopicStateFixtures } from 'redux/reducers/topics/__test__/fixtures';
import { TopicWithDetailedInfo } from 'redux/interfaces';
import { clusterTopicConsumerGroupsPath } from 'lib/paths';
describe('TopicConsumerGroups', () => { describe('TopicConsumerGroups', () => {
const mockClusterName = 'localClusterName'; const mockClusterName = 'localClusterName';
@ -32,18 +35,32 @@ describe('TopicConsumerGroups', () => {
}, },
]; ];
const setUpComponent = (props: Partial<Props> = {}) => { const setUpComponent = (
const { name, topicName, consumerGroups, isFetched } = props; props: Partial<Props> = {},
consumerGroups?: ConsumerGroup[]
) => {
const topic: TopicWithDetailedInfo = {
name: mockTopicName,
consumerGroups,
};
const topicsState = getTopicStateFixtures([topic]);
return render( return render(
<ConsumerGroups <WithRoute path={clusterTopicConsumerGroupsPath()}>
clusterName={mockClusterName} <TopicConsumerGroups
consumerGroups={consumerGroups?.length ? consumerGroups : []} fetchTopicConsumerGroups={jest.fn()}
name={name || mockTopicName} isFetched={false}
fetchTopicConsumerGroups={jest.fn()} {...props}
topicName={topicName || mockTopicName} />
isFetched={'isFetched' in props ? !!isFetched : false} </WithRoute>,
/> {
initialEntries: [
clusterTopicConsumerGroupsPath(mockClusterName, mockTopicName),
],
preloadedState: {
topics: topicsState,
},
}
); );
}; };
@ -62,10 +79,18 @@ describe('TopicConsumerGroups', () => {
}); });
it('render ConsumerGroups in Topic', () => { it('render ConsumerGroups in Topic', () => {
setUpComponent({ setUpComponent(
consumerGroups: mockWithConsumerGroup, {
isFetched: true, isFetched: true,
}); },
mockWithConsumerGroup
);
expect(screen.getAllByRole('rowgroup')).toHaveLength(2); expect(screen.getAllByRole('rowgroup')).toHaveLength(2);
expect(
screen.getByText(mockWithConsumerGroup[0].groupId)
).toBeInTheDocument();
expect(
screen.getByText(mockWithConsumerGroup[1].groupId)
).toBeInTheDocument();
}); });
}); });

View file

@ -8,7 +8,7 @@ export const DropdownExtraMessage = styled.div`
`; `;
export const ReplicaCell = styled.span.attrs({ 'aria-label': 'replica-info' })<{ export const ReplicaCell = styled.span.attrs({ 'aria-label': 'replica-info' })<{
leader: boolean | undefined; leader?: boolean;
}>` }>`
${this} ~ ${this}::before { ${this} ~ ${this}::before {
color: black; color: black;

View file

@ -1,15 +1,13 @@
import React from 'react'; import React from 'react';
import { ClusterName, TopicName } from 'redux/interfaces'; import { ClusterName, TopicName } from 'redux/interfaces';
import { Topic, TopicDetails } from 'generated-sources'; import { NavLink, Route, Routes, useNavigate } from 'react-router-dom';
import { NavLink, Switch, Route, useHistory } from 'react-router-dom';
import { import {
clusterTopicSettingsPath, RouteParamsClusterTopic,
clusterTopicPath, clusterTopicMessagesRelativePath,
clusterTopicMessagesPath, clusterTopicSettingsRelativePath,
clusterTopicsPath, clusterTopicConsumerGroupsRelativePath,
clusterTopicConsumerGroupsPath, clusterTopicEditRelativePath,
clusterTopicEditPath, clusterTopicSendMessageRelativePath,
clusterTopicSendMessagePath,
} from 'lib/paths'; } from 'lib/paths';
import ClusterContext from 'components/contexts/ClusterContext'; import ClusterContext from 'components/contexts/ClusterContext';
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal'; import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
@ -22,18 +20,20 @@ import DropdownItem from 'components/common/Dropdown/DropdownItem';
import styled from 'styled-components'; import styled from 'styled-components';
import Navbar from 'components/common/Navigation/Navbar.styled'; import Navbar from 'components/common/Navigation/Navbar.styled';
import * as S from 'components/Topics/Topic/Details/Details.styled'; import * as S from 'components/Topics/Topic/Details/Details.styled';
import { useAppSelector } from 'lib/hooks/redux';
import {
getIsTopicDeletePolicy,
getIsTopicInternal,
} from 'redux/reducers/topics/selectors';
import useAppParams from 'lib/hooks/useAppParams';
import OverviewContainer from './Overview/OverviewContainer'; import OverviewContainer from './Overview/OverviewContainer';
import TopicConsumerGroupsContainer from './ConsumerGroups/TopicConsumerGroupsContainer'; import TopicConsumerGroupsContainer from './ConsumerGroups/TopicConsumerGroupsContainer';
import SettingsContainer from './Settings/SettingsContainer'; import SettingsContainer from './Settings/SettingsContainer';
import Messages from './Messages/Messages'; import Messages from './Messages/Messages';
interface Props extends Topic, TopicDetails { interface Props {
clusterName: ClusterName;
topicName: TopicName;
isInternal: boolean;
isDeleted: boolean; isDeleted: boolean;
isDeletePolicy: boolean;
deleteTopic: (payload: { deleteTopic: (payload: {
clusterName: ClusterName; clusterName: ClusterName;
topicName: TopicName; topicName: TopicName;
@ -56,16 +56,22 @@ const HeaderControlsWrapper = styled.div`
`; `;
const Details: React.FC<Props> = ({ const Details: React.FC<Props> = ({
clusterName,
topicName,
isInternal,
isDeleted, isDeleted,
isDeletePolicy,
deleteTopic, deleteTopic,
recreateTopic, recreateTopic,
clearTopicMessages, clearTopicMessages,
}) => { }) => {
const history = useHistory(); const { clusterName, topicName } = useAppParams<RouteParamsClusterTopic>();
const isInternal = useAppSelector((state) =>
getIsTopicInternal(state, topicName)
);
const isDeletePolicy = useAppSelector((state) =>
getIsTopicDeletePolicy(state, topicName)
);
const navigate = useNavigate();
const dispatch = useDispatch(); const dispatch = useDispatch();
const { isReadOnly, isTopicDeletionAllowed } = const { isReadOnly, isTopicDeletionAllowed } =
React.useContext(ClusterContext); React.useContext(ClusterContext);
@ -81,9 +87,9 @@ const Details: React.FC<Props> = ({
React.useEffect(() => { React.useEffect(() => {
if (isDeleted) { if (isDeleted) {
history.push(clusterTopicsPath(clusterName)); navigate('../..');
} }
}, [isDeleted, clusterName, dispatch, history]); }, [isDeleted, clusterName, dispatch, navigate]);
const clearTopicMessagesHandler = () => { const clearTopicMessagesHandler = () => {
clearTopicMessages({ clusterName, topicName }); clearTopicMessages({ clusterName, topicName });
@ -99,58 +105,62 @@ const Details: React.FC<Props> = ({
<div> <div>
<PageHeading text={topicName}> <PageHeading text={topicName}>
<HeaderControlsWrapper> <HeaderControlsWrapper>
<Route <Routes>
exact <Route
path="/ui/clusters/:clusterName/topics/:topicName/messages" path={clusterTopicMessagesRelativePath}
> element={
<Button <Button
buttonSize="M" buttonSize="M"
buttonType="primary" buttonType="primary"
isLink to={`../${clusterTopicSendMessageRelativePath}`}
to={clusterTopicSendMessagePath(clusterName, topicName)} >
> Produce Message
Produce Message </Button>
</Button> }
</Route> />
</Routes>
{!isReadOnly && !isInternal && ( {!isReadOnly && !isInternal && (
<Route path="/ui/clusters/:clusterName/topics/:topicName"> <Routes>
<Dropdown label={<VerticalElipsisIcon />} right> <Route
<DropdownItem index
onClick={() => element={
history.push(clusterTopicEditPath(clusterName, topicName)) <Dropdown label={<VerticalElipsisIcon />} right>
} <DropdownItem
> onClick={() => navigate(clusterTopicEditRelativePath)}
Edit settings >
<S.DropdownExtraMessage> Edit settings
Pay attention! This operation has <S.DropdownExtraMessage>
<br /> Pay attention! This operation has
especially important consequences. <br />
</S.DropdownExtraMessage> especially important consequences.
</DropdownItem> </S.DropdownExtraMessage>
{isDeletePolicy && ( </DropdownItem>
<DropdownItem {isDeletePolicy && (
onClick={() => setClearTopicConfirmationVisible(true)} <DropdownItem
danger onClick={() => setClearTopicConfirmationVisible(true)}
> danger
Clear messages >
</DropdownItem> Clear messages
)} </DropdownItem>
<DropdownItem )}
onClick={() => setRecreateTopicConfirmationVisible(true)} <DropdownItem
danger onClick={() => setRecreateTopicConfirmationVisible(true)}
> danger
Recreate Topic >
</DropdownItem> Recreate Topic
{isTopicDeletionAllowed && ( </DropdownItem>
<DropdownItem {isTopicDeletionAllowed && (
onClick={() => setDeleteTopicConfirmationVisible(true)} <DropdownItem
danger onClick={() => setDeleteTopicConfirmationVisible(true)}
> danger
Remove topic >
</DropdownItem> Remove topic
)} </DropdownItem>
</Dropdown> )}
</Route> </Dropdown>
}
/>
</Routes>
)} )}
</HeaderControlsWrapper> </HeaderControlsWrapper>
</PageHeading> </PageHeading>
@ -177,56 +187,45 @@ const Details: React.FC<Props> = ({
</ConfirmationModal> </ConfirmationModal>
<Navbar role="navigation"> <Navbar role="navigation">
<NavLink <NavLink
exact to="."
to={clusterTopicPath(clusterName, topicName)} className={({ isActive }) => (isActive ? 'is-active is-primary' : '')}
activeClassName="is-active is-primary"
> >
Overview Overview
</NavLink> </NavLink>
<NavLink <NavLink
exact to={clusterTopicMessagesRelativePath}
to={clusterTopicMessagesPath(clusterName, topicName)} className={({ isActive }) => (isActive ? 'is-active' : '')}
activeClassName="is-active"
> >
Messages Messages
</NavLink> </NavLink>
<NavLink <NavLink
exact to={clusterTopicConsumerGroupsRelativePath}
to={clusterTopicConsumerGroupsPath(clusterName, topicName)} className={({ isActive }) => (isActive ? 'is-active' : '')}
activeClassName="is-active"
> >
Consumers Consumers
</NavLink> </NavLink>
<NavLink <NavLink
exact to={clusterTopicSettingsRelativePath}
to={clusterTopicSettingsPath(clusterName, topicName)} className={({ isActive }) => (isActive ? 'is-active' : '')}
activeClassName="is-active"
> >
Settings Settings
</NavLink> </NavLink>
</Navbar> </Navbar>
<Switch> <Routes>
<Route index element={<OverviewContainer />} />
<Route path={clusterTopicMessagesRelativePath} element={<Messages />} />
<Route <Route
exact path={clusterTopicSettingsRelativePath}
path="/ui/clusters/:clusterName/topics/:topicName/messages" element={<SettingsContainer />}
component={Messages}
/> />
<Route <Route
exact path={clusterTopicConsumerGroupsRelativePath}
path="/ui/clusters/:clusterName/topics/:topicName/settings" element={<TopicConsumerGroupsContainer />}
component={SettingsContainer}
/> />
<Route </Routes>
exact
path="/ui/clusters/:clusterName/topics/:topicName"
component={OverviewContainer}
/>
<Route
exact
path="/ui/clusters/:clusterName/topics/:topicName/consumer-groups"
component={TopicConsumerGroupsContainer}
/>
</Switch>
</div> </div>
); );
}; };

View file

@ -1,36 +1,13 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { ClusterName, RootState, TopicName } from 'redux/interfaces'; import { RootState } from 'redux/interfaces';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import { deleteTopic, recreateTopic } from 'redux/reducers/topics/topicsSlice'; import { deleteTopic, recreateTopic } from 'redux/reducers/topics/topicsSlice';
import { clearTopicMessages } from 'redux/reducers/topicMessages/topicMessagesSlice'; import { clearTopicMessages } from 'redux/reducers/topicMessages/topicMessagesSlice';
import { import { getIsTopicDeleted } from 'redux/reducers/topics/selectors';
getIsTopicDeleted,
getIsTopicDeletePolicy,
getIsTopicInternal,
} from 'redux/reducers/topics/selectors';
import Details from './Details'; import Details from './Details';
interface RouteProps { const mapStateToProps = (state: RootState) => ({
clusterName: ClusterName;
topicName: TopicName;
}
type OwnProps = RouteComponentProps<RouteProps>;
const mapStateToProps = (
state: RootState,
{
match: {
params: { topicName, clusterName },
},
}: OwnProps
) => ({
clusterName,
topicName,
isInternal: getIsTopicInternal(state, topicName),
isDeleted: getIsTopicDeleted(state), isDeleted: getIsTopicDeleted(state),
isDeletePolicy: getIsTopicDeletePolicy(state, topicName),
}); });
const mapDispatchToProps = { const mapDispatchToProps = {
@ -39,6 +16,4 @@ const mapDispatchToProps = {
clearTopicMessages, clearTopicMessages,
}; };
export default withRouter( export default connect(mapStateToProps, mapDispatchToProps)(Details);
connect(mapStateToProps, mapDispatchToProps)(Details)
);

View file

@ -12,12 +12,11 @@ import {
} from 'generated-sources'; } from 'generated-sources';
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { omitBy } from 'lodash'; import { omitBy } from 'lodash';
import { useHistory, useLocation } from 'react-router-dom'; import { useNavigate, useLocation } from 'react-router-dom';
import DatePicker from 'react-datepicker'; import DatePicker from 'react-datepicker';
import MultiSelect from 'components/common/MultiSelect/MultiSelect.styled'; import MultiSelect from 'components/common/MultiSelect/MultiSelect.styled';
import { Option } from 'react-multi-select-component/dist/lib/interfaces'; import { Option } from 'react-multi-select-component/dist/lib/interfaces';
import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted'; import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';
import { ClusterName, TopicName } from 'redux/interfaces';
import { BASE_PARAMS } from 'lib/constants'; import { BASE_PARAMS } from 'lib/constants';
import Input from 'components/common/Input/Input'; import Input from 'components/common/Input/Input';
import Select from 'components/common/Select/Select'; import Select from 'components/common/Select/Select';
@ -29,6 +28,10 @@ import FilterModal, {
import { SeekDirectionOptions } from 'components/Topics/Topic/Details/Messages/Messages'; import { SeekDirectionOptions } from 'components/Topics/Topic/Details/Messages/Messages';
import TopicMessagesContext from 'components/contexts/TopicMessagesContext'; import TopicMessagesContext from 'components/contexts/TopicMessagesContext';
import useModal from 'lib/hooks/useModal'; 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 * as S from './Filters.styled';
import { import {
@ -41,10 +44,7 @@ import {
type Query = Record<string, string | string[] | number>; type Query = Record<string, string | string[] | number>;
export interface FiltersProps { export interface FiltersProps {
clusterName: ClusterName;
topicName: TopicName;
phaseMessage?: string; phaseMessage?: string;
partitions: Partition[];
meta: TopicMessageConsuming; meta: TopicMessageConsuming;
isFetching: boolean; isFetching: boolean;
addMessage(content: { message: TopicMessage; prepend: boolean }): void; addMessage(content: { message: TopicMessage; prepend: boolean }): void;
@ -73,9 +73,6 @@ export const SeekTypeOptions = [
]; ];
const Filters: React.FC<FiltersProps> = ({ const Filters: React.FC<FiltersProps> = ({
clusterName,
topicName,
partitions,
phaseMessage, phaseMessage,
meta: { elapsedMs, bytesConsumed, messagesConsumed }, meta: { elapsedMs, bytesConsumed, messagesConsumed },
isFetching, isFetching,
@ -85,8 +82,13 @@ const Filters: React.FC<FiltersProps> = ({
updateMeta, updateMeta,
setIsFetching, setIsFetching,
}) => { }) => {
const { clusterName, topicName } = useAppParams<RouteParamsClusterTopic>();
const location = useLocation(); const location = useLocation();
const history = useHistory(); const navigate = useNavigate();
const partitions = useAppSelector((state) =>
getPartitionsByTopicName(state, topicName)
);
const { searchParams, seekDirection, isLive, changeSeekDirection } = const { searchParams, seekDirection, isLive, changeSeekDirection } =
useContext(TopicMessagesContext); useContext(TopicMessagesContext);
@ -212,7 +214,7 @@ const Filters: React.FC<FiltersProps> = ({
.map((key) => `${key}=${newProps[key]}`) .map((key) => `${key}=${newProps[key]}`)
.join('&'); .join('&');
history.push({ navigate({
search: `?${qs}`, search: `?${qs}`,
}); });
}, },
@ -224,6 +226,7 @@ const Filters: React.FC<FiltersProps> = ({
timestamp, timestamp,
query, query,
selectedPartitions, selectedPartitions,
navigate,
] ]
); );

View file

@ -1,6 +1,5 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { ClusterName, RootState, TopicName } from 'redux/interfaces'; import { RootState } from 'redux/interfaces';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import { import {
addTopicMessage, addTopicMessage,
resetTopicMessages, resetTopicMessages,
@ -13,29 +12,11 @@ import {
getTopicMessgesPhase, getTopicMessgesPhase,
getIsTopicMessagesFetching, getIsTopicMessagesFetching,
} from 'redux/reducers/topicMessages/selectors'; } from 'redux/reducers/topicMessages/selectors';
import { getPartitionsByTopicName } from 'redux/reducers/topics/selectors';
import Filters from './Filters'; import Filters from './Filters';
interface RouteProps { const mapStateToProps = (state: RootState) => ({
clusterName: ClusterName;
topicName: TopicName;
}
type OwnProps = RouteComponentProps<RouteProps>;
const mapStateToProps = (
state: RootState,
{
match: {
params: { topicName, clusterName },
},
}: OwnProps
) => ({
clusterName,
topicName,
phaseMessage: getTopicMessgesPhase(state), phaseMessage: getTopicMessgesPhase(state),
partitions: getPartitionsByTopicName(state, topicName),
meta: getTopicMessgesMeta(state), meta: getTopicMessgesMeta(state),
isFetching: getIsTopicMessagesFetching(state), isFetching: getIsTopicMessagesFetching(state),
}); });
@ -48,6 +29,4 @@ const mapDispatchToProps = {
setIsFetching: setTopicMessagesFetchingStatus, setIsFetching: setTopicMessagesFetchingStatus,
}; };
export default withRouter( export default connect(mapStateToProps, mapDispatchToProps)(Filters);
connect(mapStateToProps, mapDispatchToProps)(Filters)
);

View file

@ -4,7 +4,7 @@ import Filters, {
FiltersProps, FiltersProps,
SeekTypeOptions, SeekTypeOptions,
} from 'components/Topics/Topic/Details/Messages/Filters/Filters'; } 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 { act, screen, within, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import TopicMessagesContext, { import TopicMessagesContext, {
@ -26,9 +26,6 @@ const renderComponent = (
render( render(
<TopicMessagesContext.Provider value={ctx}> <TopicMessagesContext.Provider value={ctx}>
<Filters <Filters
clusterName="test-cluster"
topicName="test-topic"
partitions={[{ partition: 0, offsetMin: 0, offsetMax: 100 }]}
meta={{}} meta={{}}
isFetching={false} isFetching={false}
addMessage={jest.fn()} addMessage={jest.fn()}
@ -43,6 +40,10 @@ const renderComponent = (
}; };
describe('Filters component', () => { describe('Filters component', () => {
Object.defineProperty(window, 'EventSource', {
value: EventSourceMock,
});
it('shows cancel button while fetching', () => { it('shows cancel button while fetching', () => {
renderComponent({ isFetching: true }); renderComponent({ isFetching: true });
expect(screen.getByText('Cancel')).toBeInTheDocument(); expect(screen.getByText('Cancel')).toBeInTheDocument();

View file

@ -5,8 +5,6 @@ import Messages, {
SeekDirectionOptions, SeekDirectionOptions,
SeekDirectionOptionsObj, SeekDirectionOptionsObj,
} from 'components/Topics/Topic/Details/Messages/Messages'; } from 'components/Topics/Topic/Details/Messages/Messages';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { SeekDirection, SeekType } from 'generated-sources'; import { SeekDirection, SeekType } from 'generated-sources';
import userEvent from '@testing-library/user-event'; 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 searchParams = `?filterQueryType=STRING_CONTAINS&attempt=0&limit=100&seekDirection=${SeekDirection.FORWARD}&seekType=${SeekType.OFFSET}&seekTo=0::9`;
const setUpComponent = (param: string = searchParams) => { const setUpComponent = (param: string = searchParams) => {
const history = createMemoryHistory(); return render(<Messages />, {
history.push({ initialEntries: [`/?${new URLSearchParams(param).toString()}`],
search: new URLSearchParams(param).toString(),
}); });
return render(
<Router history={history}>
<Messages />
</Router>
);
}; };
beforeEach(() => { beforeEach(() => {

View file

@ -2,8 +2,6 @@ import React from 'react';
import { screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import { render } from 'lib/testHelpers'; import { render } from 'lib/testHelpers';
import MessagesTable from 'components/Topics/Topic/Details/Messages/MessagesTable'; 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 { SeekDirection, SeekType, TopicMessage } from 'generated-sources';
import TopicMessagesContext, { import TopicMessagesContext, {
ContextProps, ContextProps,
@ -15,6 +13,12 @@ import {
const mockTopicsMessages: TopicMessage[] = [{ ...topicMessagePayload }]; const mockTopicsMessages: TopicMessage[] = [{ ...topicMessagePayload }];
const mockNavigate = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: () => mockNavigate,
}));
describe('MessagesTable', () => { describe('MessagesTable', () => {
const seekToResult = '&seekTo=0::9'; const seekToResult = '&seekTo=0::9';
const searchParamsValue = `?filterQueryType=STRING_CONTAINS&attempt=0&limit=100&seekDirection=${SeekDirection.FORWARD}&seekType=${SeekType.OFFSET}${seekToResult}`; 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, ctx: ContextProps = contextValue,
messages: TopicMessage[] = [], messages: TopicMessage[] = [],
isFetching?: boolean, isFetching?: boolean,
customHistory?: MemoryHistory path?: string
) => { ) => {
const history = const customPath = path || params.toString();
customHistory ||
createMemoryHistory({
initialEntries: [params.toString()],
});
return render( return render(
<Router history={history}> <TopicMessagesContext.Provider value={ctx}>
<TopicMessagesContext.Provider value={ctx}> <MessagesTable />
<MessagesTable /> </TopicMessagesContext.Provider>,
</TopicMessagesContext.Provider>
</Router>,
{ {
initialEntries: [customPath],
preloadedState: { preloadedState: {
topicMessages: { topicMessages: {
messages, messages,

View file

@ -1,5 +1,5 @@
import React from 'react'; 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 { ClusterName, TopicName } from 'redux/interfaces';
import Dropdown from 'components/common/Dropdown/Dropdown'; import Dropdown from 'components/common/Dropdown/Dropdown';
import DropdownItem from 'components/common/Dropdown/DropdownItem'; 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 VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
import * as Metrics from 'components/common/Metrics'; import * as Metrics from 'components/common/Metrics';
import { Tag } from 'components/common/Tag/Tag.styled'; 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 { 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 { export interface Props {
clusterName: ClusterName;
topicName: TopicName;
clearTopicMessages(params: { clearTopicMessages(params: {
clusterName: ClusterName; clusterName: ClusterName;
topicName: TopicName; topicName: TopicName;
@ -22,21 +24,25 @@ export interface Props extends Topic, TopicDetails {
}): void; }): void;
} }
const Overview: React.FC<Props> = ({ const Overview: React.FC<Props> = ({ clearTopicMessages }) => {
partitions, const { clusterName, topicName } = useAppParams<RouteParamsClusterTopic>();
underReplicatedPartitions,
inSyncReplicas, const {
replicas, partitions,
partitionCount, underReplicatedPartitions,
internal, inSyncReplicas,
replicationFactor, replicas,
segmentSize, partitionCount,
segmentCount, internal,
clusterName, replicationFactor,
topicName, segmentSize,
cleanUpPolicy, segmentCount,
clearTopicMessages, cleanUpPolicy,
}) => { } = useAppSelector((state) => {
const res = getTopicByName(state, topicName);
return res || {};
});
const { isReadOnly } = React.useContext(ClusterContext); const { isReadOnly } = React.useContext(ClusterContext);
const messageCount = React.useMemo( const messageCount = React.useMemo(

View file

@ -1,34 +1,9 @@
import { connect } from 'react-redux'; 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 { clearTopicMessages } from 'redux/reducers/topicMessages/topicMessagesSlice';
import Overview from 'components/Topics/Topic/Details/Overview/Overview'; import Overview from 'components/Topics/Topic/Details/Overview/Overview';
interface RouteProps {
clusterName: ClusterName;
topicName: TopicName;
}
type OwnProps = RouteComponentProps<RouteProps>;
const mapStateToProps = (
state: RootState,
{
match: {
params: { topicName, clusterName },
},
}: OwnProps
) => ({
...getTopicByName(state, topicName),
topicName,
clusterName,
});
const mapDispatchToProps = { const mapDispatchToProps = {
clearTopicMessages, clearTopicMessages,
}; };
export default withRouter( export default connect(null, mapDispatchToProps)(Overview);
connect(mapStateToProps, mapDispatchToProps)(Overview)
);

View file

@ -1,20 +1,22 @@
import React from 'react'; import React from 'react';
import { screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import { render } from 'lib/testHelpers'; import { render, WithRoute } from 'lib/testHelpers';
import Overview, { import Overview, {
Props as OverviewProps, Props as OverviewProps,
} from 'components/Topics/Topic/Details/Overview/Overview'; } from 'components/Topics/Topic/Details/Overview/Overview';
import theme from 'theme/theme'; import theme from 'theme/theme';
import { CleanUpPolicy } from 'generated-sources'; import { CleanUpPolicy, Topic } from 'generated-sources';
import ClusterContext from 'components/contexts/ClusterContext'; import ClusterContext from 'components/contexts/ClusterContext';
import userEvent from '@testing-library/user-event'; 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'; import { ReplicaCell } from 'components/Topics/Topic/Details/Details.styled';
describe('Overview', () => { describe('Overview', () => {
const getReplicaCell = () => screen.getByLabelText('replica-info');
const mockClusterName = 'local'; const mockClusterName = 'local';
const mockTopicName = 'topic'; const mockTopicName = 'topic';
const mockClearTopicMessages = jest.fn(); const mockTopic = { name: mockTopicName };
const mockPartitions = [ const mockPartitions = [
{ {
partition: 1, partition: 1,
@ -36,67 +38,63 @@ describe('Overview', () => {
hasSchemaRegistryConfigured: true, hasSchemaRegistryConfigured: true,
isTopicDeletionAllowed: true, isTopicDeletionAllowed: true,
}; };
const defaultProps: OverviewProps = {
name: mockTopicName,
partitions: [],
internal: true,
clusterName: mockClusterName,
topicName: mockTopicName,
clearTopicMessages: mockClearTopicMessages,
};
const setupComponent = ( const setupComponent = (
props = defaultProps, props: Partial<OverviewProps> = {},
contextValues = defaultContextValues, topicState: Topic = mockTopic,
underReplicatedPartitions?: number, contextValues = defaultContextValues
inSyncReplicas?: number,
replicas?: number
) => { ) => {
const topics = getTopicStateFixtures([topicState]);
return render( return render(
<ClusterContext.Provider value={contextValues}> <WithRoute path={clusterTopicPath()}>
<Overview <ClusterContext.Provider value={contextValues}>
underReplicatedPartitions={underReplicatedPartitions} <Overview clearTopicMessages={jest.fn()} {...props} />
inSyncReplicas={inSyncReplicas} </ClusterContext.Provider>
replicas={replicas} </WithRoute>,
{...props} {
/> initialEntries: [clusterTopicPath(mockClusterName, mockTopicName)],
</ClusterContext.Provider> preloadedState: { topics },
}
); );
}; };
afterEach(() => {
mockClearTopicMessages.mockClear();
});
it('at least one replica was rendered', () => { it('at least one replica was rendered', () => {
setupComponent({ setupComponent(
...defaultProps, {},
underReplicatedPartitions: 0, {
inSyncReplicas: 1, ...mockTopic,
replicas: 1, partitions: mockPartitions,
}); internal: false,
expect(getReplicaCell()).toBeInTheDocument(); cleanUpPolicy: CleanUpPolicy.DELETE,
}
);
expect(screen.getByLabelText('replica-info')).toBeInTheDocument();
}); });
it('renders replica cell with props', () => { it('renders replica cell with props', () => {
render(<ReplicaCell leader />); render(<ReplicaCell leader />);
expect(getReplicaCell()).toBeInTheDocument(); const element = screen.getByLabelText('replica-info');
expect(getReplicaCell()).toHaveStyleRule('color', 'orange'); expect(element).toBeInTheDocument();
expect(element).toHaveStyleRule('color', 'orange');
}); });
describe('when it has internal flag', () => { describe('when it has internal flag', () => {
it('does not render the Action button a Topic', () => { it('does not render the Action button a Topic', () => {
setupComponent({ setupComponent(
...defaultProps, {},
partitions: mockPartitions, {
internal: false, ...mockTopic,
cleanUpPolicy: CleanUpPolicy.DELETE, partitions: mockPartitions,
}); internal: false,
cleanUpPolicy: CleanUpPolicy.DELETE,
}
);
expect(screen.getAllByRole('menu')[0]).toBeInTheDocument(); expect(screen.getAllByRole('menu')[0]).toBeInTheDocument();
}); });
it('does not render Partitions', () => { it('does not render Partitions', () => {
setupComponent(); setupComponent({}, { ...mockTopic, partitions: [] });
expect(screen.getByText('No Partitions found')).toBeInTheDocument(); expect(screen.getByText('No Partitions found')).toBeInTheDocument();
}); });
@ -110,12 +108,15 @@ describe('Overview', () => {
}); });
it('should be the appropriate color', () => { it('should be the appropriate color', () => {
setupComponent({ setupComponent(
...defaultProps, {},
underReplicatedPartitions: 0, {
inSyncReplicas: 1, ...mockTopic,
replicas: 2, underReplicatedPartitions: 0,
}); inSyncReplicas: 1,
replicas: 2,
}
);
const circles = screen.getAllByRole('circle'); const circles = screen.getAllByRole('circle');
expect(circles[0]).toHaveStyle( expect(circles[0]).toHaveStyle(
`fill: ${theme.circularAlert.color.success}` `fill: ${theme.circularAlert.color.success}`
@ -127,24 +128,30 @@ describe('Overview', () => {
}); });
describe('when Clear Messages is clicked', () => { describe('when Clear Messages is clicked', () => {
setupComponent({ it('should when Clear Messages is clicked', () => {
...defaultProps, const mockClearTopicMessages = jest.fn();
partitions: mockPartitions, setupComponent(
internal: false, { clearTopicMessages: mockClearTopicMessages },
cleanUpPolicy: CleanUpPolicy.DELETE, {
...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', () => { describe('when the table partition dropdown appearance', () => {
it('should check if the dropdown is not present when it is readOnly', () => { it('should check if the dropdown is not present when it is readOnly', () => {
setupComponent( setupComponent(
{},
{ {
...defaultProps, ...mockTopic,
partitions: mockPartitions, partitions: mockPartitions,
internal: true, internal: true,
cleanUpPolicy: CleanUpPolicy.DELETE, cleanUpPolicy: CleanUpPolicy.DELETE,
@ -155,32 +162,41 @@ describe('Overview', () => {
}); });
it('should check if the dropdown is not present when it is internal', () => { it('should check if the dropdown is not present when it is internal', () => {
setupComponent({ setupComponent(
...defaultProps, {},
partitions: mockPartitions, {
internal: true, ...mockTopic,
cleanUpPolicy: CleanUpPolicy.DELETE, partitions: mockPartitions,
}); internal: true,
cleanUpPolicy: CleanUpPolicy.DELETE,
}
);
expect(screen.queryByText('Clear Messages')).not.toBeInTheDocument(); expect(screen.queryByText('Clear Messages')).not.toBeInTheDocument();
}); });
it('should check if the dropdown is not present when cleanUpPolicy is not DELETE', () => { it('should check if the dropdown is not present when cleanUpPolicy is not DELETE', () => {
setupComponent({ setupComponent(
...defaultProps, {},
partitions: mockPartitions, {
internal: false, ...mockTopic,
cleanUpPolicy: CleanUpPolicy.COMPACT, partitions: mockPartitions,
}); internal: false,
cleanUpPolicy: CleanUpPolicy.COMPACT,
}
);
expect(screen.queryByText('Clear Messages')).not.toBeInTheDocument(); expect(screen.queryByText('Clear Messages')).not.toBeInTheDocument();
}); });
it('should check if the dropdown action to be in visible', () => { it('should check if the dropdown action to be in visible', () => {
setupComponent({ setupComponent(
...defaultProps, {},
partitions: mockPartitions, {
internal: false, ...mockTopic,
cleanUpPolicy: CleanUpPolicy.DELETE, partitions: mockPartitions,
}); internal: false,
cleanUpPolicy: CleanUpPolicy.DELETE,
}
);
expect(screen.getByText('Clear Messages')).toBeInTheDocument(); expect(screen.getByText('Clear Messages')).toBeInTheDocument();
}); });
}); });

View file

@ -1,16 +1,16 @@
import React from 'react';
import PageLoader from 'components/common/PageLoader/PageLoader'; import PageLoader from 'components/common/PageLoader/PageLoader';
import { Table } from 'components/common/table/Table/Table.styled'; import { Table } from 'components/common/table/Table/Table.styled';
import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell'; import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
import { TopicConfig } from 'generated-sources';
import React from 'react';
import { ClusterName, TopicName } from 'redux/interfaces'; 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'; import ConfigListItem from './ConfigListItem';
interface Props { export interface Props {
clusterName: ClusterName;
topicName: TopicName;
config?: TopicConfig[];
isFetched: boolean; isFetched: boolean;
fetchTopicConfig: (payload: { fetchTopicConfig: (payload: {
clusterName: ClusterName; clusterName: ClusterName;
@ -18,13 +18,11 @@ interface Props {
}) => void; }) => void;
} }
const Settings: React.FC<Props> = ({ const Settings: React.FC<Props> = ({ isFetched, fetchTopicConfig }) => {
clusterName, const { clusterName, topicName } = useAppParams<RouteParamsClusterTopic>();
topicName,
isFetched, const config = useAppSelector((state) => getTopicConfig(state, topicName));
fetchTopicConfig,
config,
}) => {
React.useEffect(() => { React.useEffect(() => {
fetchTopicConfig({ clusterName, topicName }); fetchTopicConfig({ clusterName, topicName });
}, [fetchTopicConfig, clusterName, topicName]); }, [fetchTopicConfig, clusterName, topicName]);

View file

@ -1,32 +1,11 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { RootState, ClusterName, TopicName } from 'redux/interfaces'; import { RootState } from 'redux/interfaces';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import { fetchTopicConfig } from 'redux/reducers/topics/topicsSlice'; import { fetchTopicConfig } from 'redux/reducers/topics/topicsSlice';
import { import { getTopicConfigFetched } from 'redux/reducers/topics/selectors';
getTopicConfig,
getTopicConfigFetched,
} from 'redux/reducers/topics/selectors';
import Settings from './Settings'; import Settings from './Settings';
interface RouteProps { const mapStateToProps = (state: RootState) => ({
clusterName: ClusterName;
topicName: TopicName;
}
type OwnProps = RouteComponentProps<RouteProps>;
const mapStateToProps = (
state: RootState,
{
match: {
params: { topicName, clusterName },
},
}: OwnProps
) => ({
clusterName,
topicName,
config: getTopicConfig(state, topicName),
isFetched: getTopicConfigFetched(state), isFetched: getTopicConfigFetched(state),
}); });
@ -34,6 +13,4 @@ const mapDispatchToProps = {
fetchTopicConfig, fetchTopicConfig,
}; };
export default withRouter( export default connect(mapStateToProps, mapDispatchToProps)(Settings);
connect(mapStateToProps, mapDispatchToProps)(Settings)
);

View file

@ -1,14 +1,20 @@
import React from 'react'; import React from 'react';
import { render } from 'lib/testHelpers'; import { render, WithRoute } from 'lib/testHelpers';
import { screen } from '@testing-library/react'; 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 { TopicConfig } from 'generated-sources';
import { clusterTopicSettingsPath } from 'lib/paths';
import { getTopicStateFixtures } from 'redux/reducers/topics/__test__/fixtures';
describe('Settings', () => { describe('Settings', () => {
const mockClusterName = 'Cluster_Name';
const mockTopicName = 'Topic_Name';
let expectedResult: number; let expectedResult: number;
const mockFn = jest.fn(); const mockFn = jest.fn();
const mockClusterName = 'Cluster Name';
const mockTopicName = 'Topic Name';
const mockConfig: TopicConfig[] = [ const mockConfig: TopicConfig[] = [
{ {
name: 'first', name: 'first',
@ -20,43 +26,50 @@ describe('Settings', () => {
}, },
]; ];
it('should check it returns null if no config is passed', () => { const setUpComponent = (
render( props: Partial<Props> = {},
<Settings config?: TopicConfig[]
clusterName={mockClusterName} ) => {
topicName={mockTopicName} const topic = {
isFetched name: mockTopicName,
fetchTopicConfig={mockFn} config,
/> };
const topics = getTopicStateFixtures([topic]);
return render(
<WithRoute path={clusterTopicSettingsPath()}>
<Settings isFetched fetchTopicConfig={mockFn} {...props} />
</WithRoute>,
{
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(); expect(screen.queryByRole('table')).not.toBeInTheDocument();
}); });
it('should show Page loader when it is in fetching state and config is given', () => { it('should show Page loader when it is in fetching state and config is given', () => {
render( setUpComponent({ isFetched: false }, mockConfig);
<Settings
clusterName={mockClusterName}
topicName={mockTopicName}
isFetched={false}
fetchTopicConfig={mockFn}
config={mockConfig}
/>
);
expect(screen.queryByRole('table')).not.toBeInTheDocument(); expect(screen.queryByRole('table')).not.toBeInTheDocument();
expect(screen.getByRole('progressbar')).toBeInTheDocument(); expect(screen.getByRole('progressbar')).toBeInTheDocument();
}); });
it('should check and return null if it is not fetched and config is not given', () => { it('should check and return null if it is not fetched and config is not given', () => {
render( setUpComponent({ isFetched: false });
<Settings
clusterName={mockClusterName}
topicName={mockTopicName}
isFetched={false}
fetchTopicConfig={mockFn}
/>
);
expect(screen.queryByRole('table')).not.toBeInTheDocument(); expect(screen.queryByRole('table')).not.toBeInTheDocument();
}); });
@ -64,15 +77,7 @@ describe('Settings', () => {
describe('Settings Component with Data', () => { describe('Settings Component with Data', () => {
beforeEach(() => { beforeEach(() => {
expectedResult = mockConfig.length + 1; // include the header table row as well expectedResult = mockConfig.length + 1; // include the header table row as well
render( setUpComponent({ isFetched: true }, mockConfig);
<Settings
clusterName={mockClusterName}
topicName={mockTopicName}
isFetched
fetchTopicConfig={mockFn}
config={mockConfig}
/>
);
}); });
it('should view the correct number of table row with header included elements after config fetching', () => { it('should view the correct number of table row with header included elements after config fetching', () => {

View file

@ -3,36 +3,35 @@ import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import ClusterContext from 'components/contexts/ClusterContext'; import ClusterContext from 'components/contexts/ClusterContext';
import Details from 'components/Topics/Topic/Details/Details'; import Details from 'components/Topics/Topic/Details/Details';
import { internalTopicPayload } from 'redux/reducers/topics/__test__/fixtures';
import { render } from 'lib/testHelpers';
import { import {
clusterTopicEditPath, getTopicStateFixtures,
clusterTopicPath, internalTopicPayload,
clusterTopicsPath, } from 'redux/reducers/topics/__test__/fixtures';
} from 'lib/paths'; import { render, WithRoute } from 'lib/testHelpers';
import { Router } from 'react-router-dom'; import { clusterTopicEditRelativePath, clusterTopicPath } from 'lib/paths';
import { createMemoryHistory } from 'history'; import { CleanUpPolicy, Topic } from 'generated-sources';
const mockNavigate = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: () => mockNavigate,
}));
describe('Details', () => { describe('Details', () => {
const mockDelete = jest.fn(); const mockDelete = jest.fn();
const mockClusterName = 'local'; const mockClusterName = 'local';
const mockClearTopicMessages = jest.fn(); const mockClearTopicMessages = jest.fn();
const mockInternalTopicPayload = internalTopicPayload.internal;
const mockRecreateTopic = jest.fn(); const mockRecreateTopic = jest.fn();
const defaultPathname = clusterTopicPath(
mockClusterName,
internalTopicPayload.name
);
const mockHistory = createMemoryHistory({
initialEntries: [defaultPathname],
});
jest.spyOn(mockHistory, 'push');
const setupComponent = ( const topic: Topic = {
pathname = defaultPathname, ...internalTopicPayload,
history = mockHistory, cleanUpPolicy: CleanUpPolicy.DELETE,
props = {} internal: false,
) => };
const mockTopicsState = getTopicStateFixtures([topic]);
const setupComponent = (props = {}) =>
render( render(
<ClusterContext.Provider <ClusterContext.Provider
value={{ value={{
@ -42,24 +41,33 @@ describe('Details', () => {
isTopicDeletionAllowed: true, isTopicDeletionAllowed: true,
}} }}
> >
<Router history={history}> <WithRoute path={clusterTopicPath()}>
<Details <Details
clusterName={mockClusterName}
topicName={internalTopicPayload.name}
name={internalTopicPayload.name}
isInternal={false}
deleteTopic={mockDelete} deleteTopic={mockDelete}
recreateTopic={mockRecreateTopic} recreateTopic={mockRecreateTopic}
clearTopicMessages={mockClearTopicMessages} clearTopicMessages={mockClearTopicMessages}
isDeleted={false} isDeleted={false}
isDeletePolicy
{...props} {...props}
/> />
</Router> </WithRoute>
</ClusterContext.Provider>, </ClusterContext.Provider>,
{ pathname } {
initialEntries: [
clusterTopicPath(mockClusterName, internalTopicPayload.name),
],
preloadedState: {
topics: mockTopicsState,
},
}
); );
afterEach(() => {
mockNavigate.mockClear();
mockDelete.mockClear();
mockClearTopicMessages.mockClear();
mockRecreateTopic.mockClear();
});
describe('when it has readonly flag', () => { describe('when it has readonly flag', () => {
it('does not render the Action button a Topic', () => { it('does not render the Action button a Topic', () => {
render( render(
@ -72,15 +80,10 @@ describe('Details', () => {
}} }}
> >
<Details <Details
clusterName={mockClusterName}
topicName={internalTopicPayload.name}
name={internalTopicPayload.name}
isInternal={mockInternalTopicPayload}
deleteTopic={mockDelete} deleteTopic={mockDelete}
recreateTopic={mockRecreateTopic} recreateTopic={mockRecreateTopic}
clearTopicMessages={mockClearTopicMessages} clearTopicMessages={mockClearTopicMessages}
isDeleted={false} isDeleted={false}
isDeletePolicy
/> />
</ClusterContext.Provider> </ClusterContext.Provider>
); );
@ -148,30 +151,23 @@ describe('Details', () => {
const button = screen.getAllByText('Edit settings')[0]; const button = screen.getAllByText('Edit settings')[0];
userEvent.click(button); userEvent.click(button);
const redirectRoute = clusterTopicEditPath( expect(mockNavigate).toHaveBeenCalledWith(clusterTopicEditRelativePath);
mockClusterName,
internalTopicPayload.name
);
expect(mockHistory.push).toHaveBeenCalledWith(redirectRoute);
}); });
}); });
it('redirects to the correct route if topic is deleted', () => { it('redirects to the correct route if topic is deleted', () => {
setupComponent(defaultPathname, mockHistory, { isDeleted: true }); setupComponent({ isDeleted: true });
const redirectRoute = clusterTopicsPath(mockClusterName);
expect(mockHistory.push).toHaveBeenCalledWith(redirectRoute); expect(mockNavigate).toHaveBeenCalledWith('../..');
}); });
it('shows a confirmation popup on deleting topic messages', () => { it('shows a confirmation popup on deleting topic messages', () => {
setupComponent(); setupComponent();
const { getByText } = screen; const clearMessagesButton = screen.getAllByText(/Clear messages/i)[0];
const clearMessagesButton = getByText(/Clear messages/i);
userEvent.click(clearMessagesButton); userEvent.click(clearMessagesButton);
expect( expect(
getByText(/Are you sure want to clear topic messages?/i) screen.getByText(/Are you sure want to clear topic messages?/i)
).toBeInTheDocument(); ).toBeInTheDocument();
}); });

View file

@ -6,13 +6,13 @@ import { FormError } from 'components/common/Input/Input.styled';
import { InputLabel } from 'components/common/Input/InputLabel.styled'; import { InputLabel } from 'components/common/Input/InputLabel.styled';
import React from 'react'; import React from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { RouteParamsClusterTopic } from 'lib/paths';
import { ClusterName, TopicName } from 'redux/interfaces'; import { ClusterName, TopicName } from 'redux/interfaces';
import useAppParams from 'lib/hooks/useAppParams';
import * as S from './DangerZone.styled'; import * as S from './DangerZone.styled';
export interface Props { export interface Props {
clusterName: string;
topicName: string;
defaultPartitions: number; defaultPartitions: number;
defaultReplicationFactor: number; defaultReplicationFactor: number;
partitionsCountIncreased: boolean; partitionsCountIncreased: boolean;
@ -30,8 +30,6 @@ export interface Props {
} }
const DangerZone: React.FC<Props> = ({ const DangerZone: React.FC<Props> = ({
clusterName,
topicName,
defaultPartitions, defaultPartitions,
defaultReplicationFactor, defaultReplicationFactor,
partitionsCountIncreased, partitionsCountIncreased,
@ -39,6 +37,8 @@ const DangerZone: React.FC<Props> = ({
updateTopicPartitionsCount, updateTopicPartitionsCount,
updateTopicReplicationFactor, updateTopicReplicationFactor,
}) => { }) => {
const { clusterName, topicName } = useAppParams<RouteParamsClusterTopic>();
const [isPartitionsConfirmationVisible, setIsPartitionsConfirmationVisible] = const [isPartitionsConfirmationVisible, setIsPartitionsConfirmationVisible] =
React.useState<boolean>(false); React.useState<boolean>(false);
const [ const [

View file

@ -1,6 +1,5 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { RootState, ClusterName, TopicName } from 'redux/interfaces'; import { RootState } from 'redux/interfaces';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import { import {
updateTopicPartitionsCount, updateTopicPartitionsCount,
updateTopicReplicationFactor, updateTopicReplicationFactor,
@ -12,11 +11,6 @@ import {
import DangerZone from './DangerZone'; import DangerZone from './DangerZone';
interface RouteProps {
clusterName: ClusterName;
topicName: TopicName;
}
type OwnProps = { type OwnProps = {
defaultPartitions: number; defaultPartitions: number;
defaultReplicationFactor: number; defaultReplicationFactor: number;
@ -24,16 +18,8 @@ type OwnProps = {
const mapStateToProps = ( const mapStateToProps = (
state: RootState, state: RootState,
{ { defaultPartitions, defaultReplicationFactor }: OwnProps
match: {
params: { topicName, clusterName },
},
defaultPartitions,
defaultReplicationFactor,
}: OwnProps & RouteComponentProps<RouteProps>
) => ({ ) => ({
clusterName,
topicName,
defaultPartitions, defaultPartitions,
defaultReplicationFactor, defaultReplicationFactor,
partitionsCountIncreased: getTopicPartitionsCountIncreased(state), partitionsCountIncreased: getTopicPartitionsCountIncreased(state),
@ -45,6 +31,4 @@ const mapDispatchToProps = {
updateTopicReplicationFactor, updateTopicReplicationFactor,
}; };
export default withRouter( export default connect(mapStateToProps, mapDispatchToProps)(DangerZone);
connect(mapStateToProps, mapDispatchToProps)(DangerZone)
);

View file

@ -4,28 +4,30 @@ import DangerZone, {
} from 'components/Topics/Topic/Edit/DangerZone/DangerZone'; } from 'components/Topics/Topic/Edit/DangerZone/DangerZone';
import { act, screen, waitFor, within } from '@testing-library/react'; import { act, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { render } from 'lib/testHelpers'; import { render, WithRoute } from 'lib/testHelpers';
import { import {
topicName, topicName,
clusterName, clusterName,
} from 'components/Topics/Topic/Edit/__test__/fixtures'; } from 'components/Topics/Topic/Edit/__test__/fixtures';
import { clusterTopicSendMessagePath } from 'lib/paths';
const defaultPartitions = 3; const defaultPartitions = 3;
const defaultReplicationFactor = 3; const defaultReplicationFactor = 3;
const renderComponent = (props?: Partial<Props>) => const renderComponent = (props?: Partial<Props>) =>
render( render(
<DangerZone <WithRoute path={clusterTopicSendMessagePath()}>
clusterName={clusterName} <DangerZone
topicName={topicName} defaultPartitions={defaultPartitions}
defaultPartitions={defaultPartitions} defaultReplicationFactor={defaultReplicationFactor}
defaultReplicationFactor={defaultReplicationFactor} partitionsCountIncreased={false}
partitionsCountIncreased={false} replicationFactorUpdated={false}
replicationFactorUpdated={false} updateTopicPartitionsCount={jest.fn()}
updateTopicPartitionsCount={jest.fn()} updateTopicReplicationFactor={jest.fn()}
updateTopicReplicationFactor={jest.fn()} {...props}
{...props} />
/> </WithRoute>,
{ initialEntries: [clusterTopicSendMessagePath(clusterName, topicName)] }
); );
const clickOnDialogSubmitButton = () => { const clickOnDialogSubmitButton = () => {
@ -199,8 +201,6 @@ describe('DangerZone', () => {
await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument()); await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument());
rerender( rerender(
<DangerZone <DangerZone
clusterName={clusterName}
topicName={topicName}
defaultPartitions={defaultPartitions} defaultPartitions={defaultPartitions}
defaultReplicationFactor={defaultReplicationFactor} defaultReplicationFactor={defaultReplicationFactor}
partitionsCountIncreased partitionsCountIncreased
@ -228,8 +228,6 @@ describe('DangerZone', () => {
await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument()); await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument());
rerender( rerender(
<DangerZone <DangerZone
clusterName={clusterName}
topicName={topicName}
defaultPartitions={defaultPartitions} defaultPartitions={defaultPartitions}
defaultReplicationFactor={defaultReplicationFactor} defaultReplicationFactor={defaultReplicationFactor}
partitionsCountIncreased={false} partitionsCountIncreased={false}

View file

@ -9,20 +9,20 @@ import {
} from 'redux/interfaces'; } from 'redux/interfaces';
import { useForm, FormProvider } from 'react-hook-form'; import { useForm, FormProvider } from 'react-hook-form';
import TopicForm from 'components/Topics/shared/Form/TopicForm'; import TopicForm from 'components/Topics/shared/Form/TopicForm';
import { clusterTopicPath } from 'lib/paths'; import { RouteParamsClusterTopic } from 'lib/paths';
import { useHistory } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { topicFormValidationSchema } from 'lib/yupExtended'; import { topicFormValidationSchema } from 'lib/yupExtended';
import { TOPIC_CUSTOM_PARAMS_PREFIX, TOPIC_CUSTOM_PARAMS } from 'lib/constants'; import { TOPIC_CUSTOM_PARAMS_PREFIX, TOPIC_CUSTOM_PARAMS } from 'lib/constants';
import styled from 'styled-components'; import styled from 'styled-components';
import PageHeading from 'components/common/PageHeading/PageHeading'; import PageHeading from 'components/common/PageHeading/PageHeading';
import { useAppSelector } from 'lib/hooks/redux';
import { getFullTopic } from 'redux/reducers/topics/selectors';
import useAppParams from 'lib/hooks/useAppParams';
import DangerZoneContainer from './DangerZone/DangerZoneContainer'; import DangerZoneContainer from './DangerZone/DangerZoneContainer';
export interface Props { export interface Props {
clusterName: ClusterName;
topicName: TopicName;
topic?: TopicWithDetailedInfo;
isFetched: boolean; isFetched: boolean;
isTopicUpdated: boolean; isTopicUpdated: boolean;
fetchTopicConfig: (payload: { fetchTopicConfig: (payload: {
@ -34,11 +34,6 @@ export interface Props {
topicName: TopicName; topicName: TopicName;
form: TopicFormDataRaw; form: TopicFormDataRaw;
}) => void; }) => void;
updateTopicPartitionsCount: (payload: {
clusterName: string;
topicname: string;
partitions: number;
}) => void;
} }
const EditWrapperStyled = styled.div` const EditWrapperStyled = styled.div`
@ -83,22 +78,24 @@ const topicParams = (topic: TopicWithDetailedInfo | undefined) => {
let formInit = false; let formInit = false;
const Edit: React.FC<Props> = ({ const Edit: React.FC<Props> = ({
clusterName,
topicName,
topic,
isFetched, isFetched,
isTopicUpdated, isTopicUpdated,
fetchTopicConfig, fetchTopicConfig,
updateTopic, updateTopic,
}) => { }) => {
const { clusterName, topicName } = useAppParams<RouteParamsClusterTopic>();
const topic = useAppSelector((state) => getFullTopic(state, topicName));
const defaultValues = React.useMemo(() => topicParams(topic), [topic]); const defaultValues = React.useMemo(() => topicParams(topic), [topic]);
const methods = useForm<TopicFormData>({ const methods = useForm<TopicFormData>({
defaultValues, defaultValues,
resolver: yupResolver(topicFormValidationSchema), resolver: yupResolver(topicFormValidationSchema),
}); });
const [isSubmitting, setIsSubmitting] = React.useState<boolean>(false); const [isSubmitting, setIsSubmitting] = React.useState<boolean>(false);
const history = useHistory(); const navigate = useNavigate();
React.useEffect(() => { React.useEffect(() => {
fetchTopicConfig({ clusterName, topicName }); fetchTopicConfig({ clusterName, topicName });
@ -106,10 +103,9 @@ const Edit: React.FC<Props> = ({
React.useEffect(() => { React.useEffect(() => {
if (isSubmitting && isTopicUpdated) { if (isSubmitting && isTopicUpdated) {
const { name } = methods.getValues(); navigate('../');
history.push(clusterTopicPath(clusterName, name));
} }
}, [isSubmitting, isTopicUpdated, clusterName, methods, history]); }, [isSubmitting, isTopicUpdated, clusterName, navigate]);
if (!isFetched || !topic || !topic.config) { if (!isFetched || !topic || !topic.config) {
return null; return null;

View file

@ -1,6 +1,5 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { RootState, ClusterName, TopicName } from 'redux/interfaces'; import { RootState } from 'redux/interfaces';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import { import {
updateTopic, updateTopic,
fetchTopicConfig, fetchTopicConfig,
@ -8,29 +7,11 @@ import {
import { import {
getTopicConfigFetched, getTopicConfigFetched,
getTopicUpdated, getTopicUpdated,
getFullTopic,
} from 'redux/reducers/topics/selectors'; } from 'redux/reducers/topics/selectors';
import Edit from './Edit'; import Edit from './Edit';
interface RouteProps { const mapStateToProps = (state: RootState) => ({
clusterName: ClusterName;
topicName: TopicName;
}
type OwnProps = RouteComponentProps<RouteProps>;
const mapStateToProps = (
state: RootState,
{
match: {
params: { topicName, clusterName },
},
}: OwnProps
) => ({
clusterName,
topicName,
topic: getFullTopic(state, topicName),
isFetched: getTopicConfigFetched(state), isFetched: getTopicConfigFetched(state),
isTopicUpdated: getTopicUpdated(state), isTopicUpdated: getTopicUpdated(state),
}); });
@ -40,4 +21,4 @@ const mapDispatchToProps = {
updateTopic, updateTopic,
}; };
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Edit)); export default connect(mapStateToProps, mapDispatchToProps)(Edit);

Some files were not shown because too many files have changed in this diff Show more