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-redux": "^7.2.6",
"react-router": "^5.2.0",
"react-router-dom": "^5.3.1",
"react-router-dom": "^6.3.0",
"redux": "^4.1.1",
"redux-thunk": "^2.3.0",
"sass": "^1.43.4",
@ -16158,7 +16158,6 @@
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz",
"integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==",
"dev": true,
"dependencies": {
"@babel/runtime": "^7.7.6"
}
@ -24856,11 +24855,11 @@
}
},
"node_modules/react-router": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz",
"integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==",
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.3.tgz",
"integrity": "sha512-mzQGUvS3bM84TnbtMYR8ZjKnuPJ71IjSzR+DE6UkUqvN4czWIqEs17yLL8xkAycv4ev0AiN+IGrWu88vJs/p2w==",
"dependencies": {
"@babel/runtime": "^7.1.2",
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1",
@ -24876,53 +24875,27 @@
}
},
"node_modules/react-router-dom": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.1.tgz",
"integrity": "sha512-f0pj/gMAbv9e8gahTmCEY20oFhxhrmHwYeIwH5EO5xu0qme+wXtsdB8YfUOAZzUz4VaXmb58m3ceiLtjMhqYmQ==",
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz",
"integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==",
"dependencies": {
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
"react-router": "5.3.1",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
"history": "^5.2.0",
"react-router": "6.3.0"
},
"peerDependencies": {
"react": ">=15"
}
},
"node_modules/react-router-dom/node_modules/history": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
"dependencies": {
"@babel/runtime": "^7.1.2",
"loose-envify": "^1.2.0",
"resolve-pathname": "^3.0.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0",
"value-equal": "^1.0.1"
"react": ">=16.8",
"react-dom": ">=16.8"
}
},
"node_modules/react-router-dom/node_modules/react-router": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.1.tgz",
"integrity": "sha512-v+zwjqb7bakqgF+wMVKlAPTca/cEmPOvQ9zt7gpSNyPXau1+0qvuYZ5BWzzNDP1y6s15zDwgb9rPN63+SIniRQ==",
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz",
"integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==",
"dependencies": {
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1",
"mini-create-react-context": "^0.4.0",
"path-to-regexp": "^1.7.0",
"prop-types": "^15.6.2",
"react-is": "^16.6.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
"history": "^5.2.0"
},
"peerDependencies": {
"react": ">=15"
"react": ">=16.8"
}
},
"node_modules/react-router/node_modules/history": {
@ -40798,7 +40771,6 @@
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz",
"integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==",
"dev": true,
"requires": {
"@babel/runtime": "^7.7.6"
}
@ -47310,11 +47282,11 @@
"dev": true
},
"react-router": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz",
"integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==",
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.3.tgz",
"integrity": "sha512-mzQGUvS3bM84TnbtMYR8ZjKnuPJ71IjSzR+DE6UkUqvN4czWIqEs17yLL8xkAycv4ev0AiN+IGrWu88vJs/p2w==",
"requires": {
"@babel/runtime": "^7.1.2",
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1",
@ -47342,47 +47314,20 @@
}
},
"react-router-dom": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.1.tgz",
"integrity": "sha512-f0pj/gMAbv9e8gahTmCEY20oFhxhrmHwYeIwH5EO5xu0qme+wXtsdB8YfUOAZzUz4VaXmb58m3ceiLtjMhqYmQ==",
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz",
"integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==",
"requires": {
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
"react-router": "5.3.1",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
"history": "^5.2.0",
"react-router": "6.3.0"
},
"dependencies": {
"history": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
"requires": {
"@babel/runtime": "^7.1.2",
"loose-envify": "^1.2.0",
"resolve-pathname": "^3.0.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0",
"value-equal": "^1.0.1"
}
},
"react-router": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.1.tgz",
"integrity": "sha512-v+zwjqb7bakqgF+wMVKlAPTca/cEmPOvQ9zt7gpSNyPXau1+0qvuYZ5BWzzNDP1y6s15zDwgb9rPN63+SIniRQ==",
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz",
"integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==",
"requires": {
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1",
"mini-create-react-context": "^0.4.0",
"path-to-regexp": "^1.7.0",
"prop-types": "^15.6.2",
"react-is": "^16.6.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
"history": "^5.2.0"
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,20 +1,30 @@
import React from 'react';
import { Switch } from 'react-router-dom';
import { clusterKsqlDbPath, clusterKsqlDbQueryPath } from 'lib/paths';
import { Route, Routes } from 'react-router-dom';
import { clusterKsqlDbQueryRelativePath } from 'lib/paths';
import List from 'components/KsqlDb/List/List';
import Query from 'components/KsqlDb/Query/Query';
import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route';
const KsqlDb: React.FC = () => {
return (
<Switch>
<BreadcrumbRoute exact path={clusterKsqlDbPath()} component={List} />
<BreadcrumbRoute
exact
path={clusterKsqlDbQueryPath()}
component={Query}
<Routes>
<Route
index
element={
<BreadcrumbRoute>
<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 PageLoader from 'components/common/PageLoader/PageLoader';
import ListItem from 'components/KsqlDb/List/ListItem';
import React, { FC, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { fetchKsqlDbTables } from 'redux/reducers/ksqlDb/ksqlDbSlice';
import { getKsqlDbTables } from 'redux/reducers/ksqlDb/selectors';
import { clusterKsqlDbQueryPath } from 'lib/paths';
import { clusterKsqlDbQueryRelativePath, ClusterNameRoute } from 'lib/paths';
import PageHeading from 'components/common/PageHeading/PageHeading';
import { Table } from 'components/common/table/Table/Table.styled';
import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
@ -25,7 +25,7 @@ const accessors = headers.map((header) => header.accessor);
const List: FC = () => {
const dispatch = useDispatch();
const { clusterName } = useParams<{ clusterName: string }>();
const { clusterName } = useAppParams<ClusterNameRoute>();
const { rows, fetching, tablesCount, streamsCount } =
useSelector(getKsqlDbTables);
@ -38,8 +38,7 @@ const List: FC = () => {
<>
<PageHeading text="KSQL DB">
<Button
isLink
to={clusterKsqlDbQueryPath(clusterName)}
to={clusterKsqlDbQueryRelativePath}
buttonType="primary"
buttonSize="M"
>

View file

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

View file

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

View file

@ -1,5 +1,5 @@
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 {
executeKsql,
@ -11,6 +11,7 @@ import { BASE_PARAMS } from 'lib/constants';
import { KsqlResponse, KsqlTableResponse } from 'generated-sources';
import { alertAdded, alertDissmissed } from 'redux/reducers/alerts/alertsSlice';
import { now } from 'lodash';
import { ClusterNameRoute } from 'lib/paths';
import type { FormValues } from './QueryForm/QueryForm';
import * as S from './Query.styled';
@ -61,7 +62,7 @@ export const getFormattedErrorFromTableData = (
};
const Query: FC = () => {
const { clusterName } = useParams<{ clusterName: string }>();
const { clusterName } = useAppParams<ClusterNameRoute>();
const sseRef = React.useRef<{ sse: EventSource | null; isOpen: boolean }>({
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 Query, {
getFormattedErrorFromTableData,
@ -6,18 +6,17 @@ import Query, {
import { screen, within } from '@testing-library/dom';
import fetchMock from 'fetch-mock';
import userEvent from '@testing-library/user-event';
import { Route } from 'react-router-dom';
import { clusterKsqlDbQueryPath } from 'lib/paths';
import { act } from '@testing-library/react';
const clusterName = 'testLocal';
const renderComponent = () =>
render(
<Route path={clusterKsqlDbQueryPath(':clusterName')}>
<WithRoute path={clusterKsqlDbQueryPath()}>
<Query />
</Route>,
</WithRoute>,
{
pathname: clusterKsqlDbQueryPath(clusterName),
initialEntries: [clusterKsqlDbQueryPath(clusterName)],
}
);

View file

@ -1,15 +1,42 @@
import React from 'react';
import KsqlDb from 'components/KsqlDb/KsqlDb';
import { render } from 'lib/testHelpers';
import { render, WithRoute } from 'lib/testHelpers';
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', () => {
it('to be in the document', () => {
render(<KsqlDb />, { pathname: clusterKsqlDbPath() });
expect(screen.getByText('KSQL DB')).toBeInTheDocument();
expect(screen.getByText('Execute KSQL Request')).toBeInTheDocument();
});
const clusterName = 'clusterName';
const renderComponent = (path: string) =>
render(
<WithRoute path={getNonExactPath(clusterKsqlDbPath())}>
<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,
clusterSchemasPath,
clusterConnectorsPath,
clusterConnectsPath,
clusterKsqlDbPath,
} from 'lib/paths';
@ -54,10 +53,6 @@ const ClusterMenu: React.FC<Props> = ({
<ClusterMenuItem
to={clusterConnectorsPath(name)}
title="Kafka Connect"
isActive={(_, location) =>
location.pathname.startsWith(clusterConnectsPath(name)) ||
location.pathname.startsWith(clusterConnectorsPath(name))
}
/>
)}
{hasFeatureConfigured(ClusterFeaturesEnum.KSQL_DB) && (

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,10 +1,13 @@
import React from 'react';
import { render } from 'lib/testHelpers';
import { render, WithRoute } from 'lib/testHelpers';
import { screen } from '@testing-library/react';
import ConsumerGroups, {
import TopicConsumerGroups, {
Props,
} 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', () => {
const mockClusterName = 'localClusterName';
@ -32,18 +35,32 @@ describe('TopicConsumerGroups', () => {
},
];
const setUpComponent = (props: Partial<Props> = {}) => {
const { name, topicName, consumerGroups, isFetched } = props;
const setUpComponent = (
props: Partial<Props> = {},
consumerGroups?: ConsumerGroup[]
) => {
const topic: TopicWithDetailedInfo = {
name: mockTopicName,
consumerGroups,
};
const topicsState = getTopicStateFixtures([topic]);
return render(
<ConsumerGroups
clusterName={mockClusterName}
consumerGroups={consumerGroups?.length ? consumerGroups : []}
name={name || mockTopicName}
fetchTopicConsumerGroups={jest.fn()}
topicName={topicName || mockTopicName}
isFetched={'isFetched' in props ? !!isFetched : false}
/>
<WithRoute path={clusterTopicConsumerGroupsPath()}>
<TopicConsumerGroups
fetchTopicConsumerGroups={jest.fn()}
isFetched={false}
{...props}
/>
</WithRoute>,
{
initialEntries: [
clusterTopicConsumerGroupsPath(mockClusterName, mockTopicName),
],
preloadedState: {
topics: topicsState,
},
}
);
};
@ -62,10 +79,18 @@ describe('TopicConsumerGroups', () => {
});
it('render ConsumerGroups in Topic', () => {
setUpComponent({
consumerGroups: mockWithConsumerGroup,
isFetched: true,
});
setUpComponent(
{
isFetched: true,
},
mockWithConsumerGroup
);
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' })<{
leader: boolean | undefined;
leader?: boolean;
}>`
${this} ~ ${this}::before {
color: black;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
import React from 'react';
import { Partition, Replica, Topic, TopicDetails } from 'generated-sources';
import { Partition, Replica } from 'generated-sources';
import { ClusterName, TopicName } from 'redux/interfaces';
import Dropdown from 'components/common/Dropdown/Dropdown';
import DropdownItem from 'components/common/Dropdown/DropdownItem';
@ -10,11 +10,13 @@ import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeader
import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
import * as Metrics from 'components/common/Metrics';
import { Tag } from 'components/common/Tag/Tag.styled';
import { useAppSelector } from 'lib/hooks/redux';
import { getTopicByName } from 'redux/reducers/topics/selectors';
import { ReplicaCell } from 'components/Topics/Topic/Details/Details.styled';
import { RouteParamsClusterTopic } from 'lib/paths';
import useAppParams from 'lib/hooks/useAppParams';
export interface Props extends Topic, TopicDetails {
clusterName: ClusterName;
topicName: TopicName;
export interface Props {
clearTopicMessages(params: {
clusterName: ClusterName;
topicName: TopicName;
@ -22,21 +24,25 @@ export interface Props extends Topic, TopicDetails {
}): void;
}
const Overview: React.FC<Props> = ({
partitions,
underReplicatedPartitions,
inSyncReplicas,
replicas,
partitionCount,
internal,
replicationFactor,
segmentSize,
segmentCount,
clusterName,
topicName,
cleanUpPolicy,
clearTopicMessages,
}) => {
const Overview: React.FC<Props> = ({ clearTopicMessages }) => {
const { clusterName, topicName } = useAppParams<RouteParamsClusterTopic>();
const {
partitions,
underReplicatedPartitions,
inSyncReplicas,
replicas,
partitionCount,
internal,
replicationFactor,
segmentSize,
segmentCount,
cleanUpPolicy,
} = useAppSelector((state) => {
const res = getTopicByName(state, topicName);
return res || {};
});
const { isReadOnly } = React.useContext(ClusterContext);
const messageCount = React.useMemo(

View file

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

View file

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

View file

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

View file

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

View file

@ -1,14 +1,20 @@
import React from 'react';
import { render } from 'lib/testHelpers';
import { render, WithRoute } from 'lib/testHelpers';
import { screen } from '@testing-library/react';
import Settings from 'components/Topics/Topic/Details/Settings/Settings';
import Settings, {
Props,
} from 'components/Topics/Topic/Details/Settings/Settings';
import { TopicConfig } from 'generated-sources';
import { clusterTopicSettingsPath } from 'lib/paths';
import { getTopicStateFixtures } from 'redux/reducers/topics/__test__/fixtures';
describe('Settings', () => {
const mockClusterName = 'Cluster_Name';
const mockTopicName = 'Topic_Name';
let expectedResult: number;
const mockFn = jest.fn();
const mockClusterName = 'Cluster Name';
const mockTopicName = 'Topic Name';
const mockConfig: TopicConfig[] = [
{
name: 'first',
@ -20,43 +26,50 @@ describe('Settings', () => {
},
];
it('should check it returns null if no config is passed', () => {
render(
<Settings
clusterName={mockClusterName}
topicName={mockTopicName}
isFetched
fetchTopicConfig={mockFn}
/>
const setUpComponent = (
props: Partial<Props> = {},
config?: TopicConfig[]
) => {
const topic = {
name: mockTopicName,
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();
});
it('should show Page loader when it is in fetching state and config is given', () => {
render(
<Settings
clusterName={mockClusterName}
topicName={mockTopicName}
isFetched={false}
fetchTopicConfig={mockFn}
config={mockConfig}
/>
);
setUpComponent({ isFetched: false }, mockConfig);
expect(screen.queryByRole('table')).not.toBeInTheDocument();
expect(screen.getByRole('progressbar')).toBeInTheDocument();
});
it('should check and return null if it is not fetched and config is not given', () => {
render(
<Settings
clusterName={mockClusterName}
topicName={mockTopicName}
isFetched={false}
fetchTopicConfig={mockFn}
/>
);
setUpComponent({ isFetched: false });
expect(screen.queryByRole('table')).not.toBeInTheDocument();
});
@ -64,15 +77,7 @@ describe('Settings', () => {
describe('Settings Component with Data', () => {
beforeEach(() => {
expectedResult = mockConfig.length + 1; // include the header table row as well
render(
<Settings
clusterName={mockClusterName}
topicName={mockTopicName}
isFetched
fetchTopicConfig={mockFn}
config={mockConfig}
/>
);
setUpComponent({ isFetched: true }, mockConfig);
});
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 ClusterContext from 'components/contexts/ClusterContext';
import Details from 'components/Topics/Topic/Details/Details';
import { internalTopicPayload } from 'redux/reducers/topics/__test__/fixtures';
import { render } from 'lib/testHelpers';
import {
clusterTopicEditPath,
clusterTopicPath,
clusterTopicsPath,
} from 'lib/paths';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
getTopicStateFixtures,
internalTopicPayload,
} from 'redux/reducers/topics/__test__/fixtures';
import { render, WithRoute } from 'lib/testHelpers';
import { clusterTopicEditRelativePath, clusterTopicPath } from 'lib/paths';
import { CleanUpPolicy, Topic } from 'generated-sources';
const mockNavigate = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: () => mockNavigate,
}));
describe('Details', () => {
const mockDelete = jest.fn();
const mockClusterName = 'local';
const mockClearTopicMessages = jest.fn();
const mockInternalTopicPayload = internalTopicPayload.internal;
const mockRecreateTopic = jest.fn();
const defaultPathname = clusterTopicPath(
mockClusterName,
internalTopicPayload.name
);
const mockHistory = createMemoryHistory({
initialEntries: [defaultPathname],
});
jest.spyOn(mockHistory, 'push');
const setupComponent = (
pathname = defaultPathname,
history = mockHistory,
props = {}
) =>
const topic: Topic = {
...internalTopicPayload,
cleanUpPolicy: CleanUpPolicy.DELETE,
internal: false,
};
const mockTopicsState = getTopicStateFixtures([topic]);
const setupComponent = (props = {}) =>
render(
<ClusterContext.Provider
value={{
@ -42,24 +41,33 @@ describe('Details', () => {
isTopicDeletionAllowed: true,
}}
>
<Router history={history}>
<WithRoute path={clusterTopicPath()}>
<Details
clusterName={mockClusterName}
topicName={internalTopicPayload.name}
name={internalTopicPayload.name}
isInternal={false}
deleteTopic={mockDelete}
recreateTopic={mockRecreateTopic}
clearTopicMessages={mockClearTopicMessages}
isDeleted={false}
isDeletePolicy
{...props}
/>
</Router>
</WithRoute>
</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', () => {
it('does not render the Action button a Topic', () => {
render(
@ -72,15 +80,10 @@ describe('Details', () => {
}}
>
<Details
clusterName={mockClusterName}
topicName={internalTopicPayload.name}
name={internalTopicPayload.name}
isInternal={mockInternalTopicPayload}
deleteTopic={mockDelete}
recreateTopic={mockRecreateTopic}
clearTopicMessages={mockClearTopicMessages}
isDeleted={false}
isDeletePolicy
/>
</ClusterContext.Provider>
);
@ -148,30 +151,23 @@ describe('Details', () => {
const button = screen.getAllByText('Edit settings')[0];
userEvent.click(button);
const redirectRoute = clusterTopicEditPath(
mockClusterName,
internalTopicPayload.name
);
expect(mockHistory.push).toHaveBeenCalledWith(redirectRoute);
expect(mockNavigate).toHaveBeenCalledWith(clusterTopicEditRelativePath);
});
});
it('redirects to the correct route if topic is deleted', () => {
setupComponent(defaultPathname, mockHistory, { isDeleted: true });
const redirectRoute = clusterTopicsPath(mockClusterName);
setupComponent({ isDeleted: true });
expect(mockHistory.push).toHaveBeenCalledWith(redirectRoute);
expect(mockNavigate).toHaveBeenCalledWith('../..');
});
it('shows a confirmation popup on deleting topic messages', () => {
setupComponent();
const { getByText } = screen;
const clearMessagesButton = getByText(/Clear messages/i);
const clearMessagesButton = screen.getAllByText(/Clear messages/i)[0];
userEvent.click(clearMessagesButton);
expect(
getByText(/Are you sure want to clear topic messages?/i)
screen.getByText(/Are you sure want to clear topic messages?/i)
).toBeInTheDocument();
});

View file

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

View file

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

View file

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

View file

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

View file

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

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