diff --git a/kafka-ui-react-app/src/components/App.tsx b/kafka-ui-react-app/src/components/App.tsx
index 13e5fdef5a..83b62806c9 100644
--- a/kafka-ui-react-app/src/components/App.tsx
+++ b/kafka-ui-react-app/src/components/App.tsx
@@ -3,7 +3,6 @@ import { Switch, Route, useLocation } from 'react-router-dom';
import { GIT_TAG, GIT_COMMIT } from 'lib/constants';
import Nav from 'components/Nav/Nav';
import PageLoader from 'components/common/PageLoader/PageLoader';
-import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
import Dashboard from 'components/Dashboard/Dashboard';
import ClusterPage from 'components/Cluster/Cluster';
import Version from 'components/Version/Version';
@@ -82,20 +81,14 @@ const App: React.FC = () => {
aria-label="Overlay"
/>
{areClustersFulfilled ? (
- <>
-
-
-
-
-
- >
+
+
+
+
) : (
)}
diff --git a/kafka-ui-react-app/src/components/Cluster/Cluster.tsx b/kafka-ui-react-app/src/components/Cluster/Cluster.tsx
index 35332d77a1..1d603b1a6f 100644
--- a/kafka-ui-react-app/src/components/Cluster/Cluster.tsx
+++ b/kafka-ui-react-app/src/components/Cluster/Cluster.tsx
@@ -1,6 +1,6 @@
import React from 'react';
import { useSelector } from 'react-redux';
-import { Switch, Route, Redirect, useParams } from 'react-router-dom';
+import { Switch, Redirect, useParams } from 'react-router-dom';
import { ClusterFeaturesEnum } from 'generated-sources';
import {
getClustersFeatures,
@@ -22,6 +22,9 @@ import ClusterContext from 'components/contexts/ClusterContext';
import Brokers from 'components/Brokers/Brokers';
import ConsumersGroups from 'components/ConsumerGroups/ConsumerGroups';
import KsqlDb from 'components/KsqlDb/KsqlDb';
+import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
+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 }>();
@@ -50,41 +53,53 @@ const Cluster: React.FC = () => {
);
return (
-
-
-
-
-
- {hasSchemaRegistryConfigured && (
-
+
+
+
+
- )}
- {hasKafkaConnectConfigured && (
-
- )}
- {hasKafkaConnectConfigured && (
-
- )}
- {hasKsqlDbConfigured && (
-
- )}
-
-
-
+ {hasSchemaRegistryConfigured && (
+
+ )}
+ {hasKafkaConnectConfigured && (
+
+ )}
+ {hasKafkaConnectConfigured && (
+
+ )}
+ {hasKsqlDbConfigured && (
+
+ )}
+
+
+
+
);
};
diff --git a/kafka-ui-react-app/src/components/Connect/Connect.tsx b/kafka-ui-react-app/src/components/Connect/Connect.tsx
index 142dd5b4ac..311535a044 100644
--- a/kafka-ui-react-app/src/components/Connect/Connect.tsx
+++ b/kafka-ui-react-app/src/components/Connect/Connect.tsx
@@ -1,11 +1,12 @@
import React from 'react';
-import { Switch, Route } from 'react-router-dom';
+import { Switch } from 'react-router-dom';
import {
clusterConnectorsPath,
clusterConnectorNewPath,
clusterConnectConnectorPath,
clusterConnectConnectorEditPath,
} from 'lib/paths';
+import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route';
import ListContainer from './List/ListContainer';
import NewContainer from './New/NewContainer';
@@ -15,17 +16,17 @@ import EditContainer from './Edit/EditContainer';
const Connect: React.FC = () => (
-
-
- (
)}
component={EditContainer}
/>
-
-
-
-
-
diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/ConsumerGroups.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/ConsumerGroups.tsx
index 43301dec6d..d0cd26c7fe 100644
--- a/kafka-ui-react-app/src/components/ConsumerGroups/ConsumerGroups.tsx
+++ b/kafka-ui-react-app/src/components/ConsumerGroups/ConsumerGroups.tsx
@@ -1,6 +1,6 @@
import React from 'react';
import { ClusterName } from 'redux/interfaces';
-import { Switch, Route, useParams } from 'react-router-dom';
+import { Switch, useParams } from 'react-router-dom';
import PageLoader from 'components/common/PageLoader/PageLoader';
import Details from 'components/ConsumerGroups/Details/Details';
import List from 'components/ConsumerGroups/List/List';
@@ -10,6 +10,7 @@ import {
fetchConsumerGroups,
getAreConsumerGroupsFulfilled,
} from 'redux/reducers/consumerGroups/consumerGroupsSlice';
+import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route';
const ConsumerGroups: React.FC = () => {
const dispatch = useAppDispatch();
@@ -22,17 +23,17 @@ const ConsumerGroups: React.FC = () => {
if (isFetched) {
return (
-
-
-
diff --git a/kafka-ui-react-app/src/components/Dashboard/Dashboard.tsx b/kafka-ui-react-app/src/components/Dashboard/Dashboard.tsx
index 310a31f03c..87d54a5eb3 100644
--- a/kafka-ui-react-app/src/components/Dashboard/Dashboard.tsx
+++ b/kafka-ui-react-app/src/components/Dashboard/Dashboard.tsx
@@ -1,11 +1,13 @@
import React from 'react';
+import PageHeading from 'components/common/PageHeading/PageHeading';
import ClustersWidgetContainer from './ClustersWidget/ClustersWidgetContainer';
const Dashboard: React.FC = () => (
-
+ >
);
export default Dashboard;
diff --git a/kafka-ui-react-app/src/components/KsqlDb/KsqlDb.tsx b/kafka-ui-react-app/src/components/KsqlDb/KsqlDb.tsx
index 2fddda1ede..d99cb1eee5 100644
--- a/kafka-ui-react-app/src/components/KsqlDb/KsqlDb.tsx
+++ b/kafka-ui-react-app/src/components/KsqlDb/KsqlDb.tsx
@@ -1,14 +1,19 @@
import React from 'react';
-import { Switch, Route } from 'react-router-dom';
+import { Switch } from 'react-router-dom';
import { clusterKsqlDbPath, clusterKsqlDbQueryPath } from 'lib/paths';
import List from 'components/KsqlDb/List/List';
import Query from 'components/KsqlDb/Query/Query';
+import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route';
const KsqlDb: React.FC = () => {
return (
-
-
+
+
);
};
diff --git a/kafka-ui-react-app/src/components/Schemas/Schemas.tsx b/kafka-ui-react-app/src/components/Schemas/Schemas.tsx
index 210be4668e..8832653f0c 100644
--- a/kafka-ui-react-app/src/components/Schemas/Schemas.tsx
+++ b/kafka-ui-react-app/src/components/Schemas/Schemas.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { Switch, Route, useParams } from 'react-router-dom';
+import { Switch, useParams } from 'react-router-dom';
import {
clusterSchemaNewPath,
clusterSchemaPath,
@@ -16,6 +16,7 @@ import List from 'components/Schemas/List/List';
import Details from 'components/Schemas/Details/Details';
import New from 'components/Schemas/New/New';
import Edit from 'components/Schemas/Edit/Edit';
+import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route';
const Schemas: React.FC = () => {
const dispatch = useAppDispatch();
@@ -32,18 +33,22 @@ const Schemas: React.FC = () => {
return (
-
-
+
-
- (
-
-
-
+
diff --git a/kafka-ui-react-app/src/components/Topics/shared/Form/FormBreadcrumbs.tsx b/kafka-ui-react-app/src/components/Topics/shared/Form/FormBreadcrumbs.tsx
deleted file mode 100644
index 05449bbbd6..0000000000
--- a/kafka-ui-react-app/src/components/Topics/shared/Form/FormBreadcrumbs.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import React from 'react';
-import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
-import { clusterTopicsPath, clusterTopicPath } from 'lib/paths';
-import { ClusterName, TopicName } from 'redux/interfaces';
-
-interface Props {
- clusterName: ClusterName;
- topicName?: TopicName;
- current: string;
-}
-
-const FormBreadcrumbs: React.FC = ({
- clusterName,
- topicName,
- current,
-}) => {
- const allTopicsLink = {
- href: clusterTopicsPath(clusterName),
- label: 'All Topics',
- };
- const links = topicName
- ? [
- allTopicsLink,
- { href: clusterTopicPath(clusterName, topicName), label: topicName },
- ]
- : [allTopicsLink];
-
- return (
-
- {current}
-
- );
-};
-
-export default FormBreadcrumbs;
diff --git a/kafka-ui-react-app/src/components/common/Breadcrumb/Breadcrumb.context.ts b/kafka-ui-react-app/src/components/common/Breadcrumb/Breadcrumb.context.ts
new file mode 100644
index 0000000000..9187d74bb7
--- /dev/null
+++ b/kafka-ui-react-app/src/components/common/Breadcrumb/Breadcrumb.context.ts
@@ -0,0 +1,16 @@
+import { createContext } from 'react';
+
+export interface BreadcrumbEntry {
+ link: string;
+ path: string[];
+}
+
+interface BreadcrumbContextInterface extends BreadcrumbEntry {
+ handleRouteChange: (match: { url: string; path: string }) => void;
+}
+
+export const BreadcrumbContext = createContext({
+ link: '',
+ path: [],
+ handleRouteChange: () => {},
+});
diff --git a/kafka-ui-react-app/src/components/common/Breadcrumb/Breadcrumb.provider.tsx b/kafka-ui-react-app/src/components/common/Breadcrumb/Breadcrumb.provider.tsx
new file mode 100644
index 0000000000..558e69ab77
--- /dev/null
+++ b/kafka-ui-react-app/src/components/common/Breadcrumb/Breadcrumb.provider.tsx
@@ -0,0 +1,51 @@
+import React, { useState } from 'react';
+import capitalize from 'lodash/capitalize';
+
+import { BreadcrumbContext, BreadcrumbEntry } from './Breadcrumb.context';
+
+const mapLocationToPath = (
+ splittedLocation: string[],
+ splittedRoutePath: string[]
+) =>
+ splittedLocation.map((item, index) =>
+ splittedRoutePath[index]?.charAt(0) !== ':'
+ ? item.split('-').map(capitalize).join(' ')
+ : item
+ );
+
+export const BreadcrumbProvider: React.FC = ({ children }) => {
+ const [state, setState] = useState({
+ link: '',
+ path: [],
+ });
+
+ const handleRouteChange = (params: { url: string; path: string }) => {
+ setState((prevState) => {
+ const newState = { ...prevState };
+ const splittedRoutePath = params.path.split('/');
+ const splittedLocation = params.url.split('/');
+
+ if (prevState.link !== params.url) {
+ newState.link = params.url;
+ newState.path = mapLocationToPath(splittedLocation, splittedRoutePath);
+ }
+
+ if (prevState.path.length < params.path.split('/').length) {
+ newState.path = mapLocationToPath(splittedLocation, splittedRoutePath);
+ }
+
+ return newState;
+ });
+ };
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/kafka-ui-react-app/src/components/common/Breadcrumb/Breadcrumb.route.tsx b/kafka-ui-react-app/src/components/common/Breadcrumb/Breadcrumb.route.tsx
new file mode 100644
index 0000000000..a34e0b0cfa
--- /dev/null
+++ b/kafka-ui-react-app/src/components/common/Breadcrumb/Breadcrumb.route.tsx
@@ -0,0 +1,59 @@
+import React, { useContext, useEffect } from 'react';
+import {
+ Route,
+ RouteProps,
+ useLocation,
+ useRouteMatch,
+} from 'react-router-dom';
+
+import { BreadcrumbContext } from './Breadcrumb.context';
+
+const BreadcrumbRouteInternal: React.FC = () => {
+ const match = useRouteMatch();
+ const location = useLocation();
+ const context = useContext(BreadcrumbContext);
+
+ useEffect(() => {
+ context.handleRouteChange({ ...match, url: location.pathname });
+ }, [location.pathname]);
+
+ return null;
+};
+
+export const BreadcrumbRoute: React.FC = ({
+ children,
+ render,
+ component,
+ ...props
+}) => {
+ return (
+ {
+ if (component) {
+ return (
+ <>
+ {React.createElement(component)}
+
+ >
+ );
+ }
+ if (render) {
+ return (
+ <>
+ {render(routeParams)}
+
+ >
+ );
+ }
+
+ return (
+ <>
+ {children}
+
+ >
+ );
+ }}
+ />
+ );
+};
diff --git a/kafka-ui-react-app/src/components/common/Breadcrumb/Breadcrumb.tsx b/kafka-ui-react-app/src/components/common/Breadcrumb/Breadcrumb.tsx
index 7db5503a0a..7cfca3a83c 100644
--- a/kafka-ui-react-app/src/components/common/Breadcrumb/Breadcrumb.tsx
+++ b/kafka-ui-react-app/src/components/common/Breadcrumb/Breadcrumb.tsx
@@ -1,53 +1,34 @@
-import React from 'react';
-import { Link, useLocation, useParams } from 'react-router-dom';
+import React, { useContext } from 'react';
+import { Link } from 'react-router-dom';
import cn from 'classnames';
import { clusterPath } from 'lib/paths';
-import { capitalize } from 'lodash';
import { BreadcrumbWrapper } from './Breadcrumb.styled';
-
-export interface BreadcrumbItem {
- label: string;
- href: string;
-}
-
-interface Props {
- links?: BreadcrumbItem[];
-}
+import { BreadcrumbContext } from './Breadcrumb.context';
const basePathEntriesLength = clusterPath(':clusterName').split('/').length;
-const Breadcrumb: React.FC = () => {
- const location = useLocation();
- const params = useParams();
- const pathParams = React.useMemo(() => Object.values(params), [params]);
+const Breadcrumb: React.FC = () => {
+ const breadcrumbContext = useContext(BreadcrumbContext);
- const paths = location.pathname.split('/');
const links = React.useMemo(
- () =>
- paths.slice(basePathEntriesLength).map((path, index) => {
- return !pathParams.includes(paths[basePathEntriesLength + index])
- ? path.split('-').map(capitalize).join(' ')
- : path;
- }),
- [paths]
+ () => breadcrumbContext.path.slice(basePathEntriesLength),
+ [breadcrumbContext.link]
);
- const currentLink = React.useMemo(() => {
- if (paths.length < basePathEntriesLength) {
- return 'Dashboard';
- }
- return links[links.length - 1];
- }, [links]);
const getPathPredicate = React.useCallback(
(index: number) =>
- `${paths.slice(0, basePathEntriesLength + index + 1).join('/')}`,
- [paths]
+ `${breadcrumbContext.link
+ .split('/')
+ .slice(0, basePathEntriesLength + index + 1)
+ .join('/')}`,
+ [breadcrumbContext.link]
);
if (links.length < 2) {
return null;
}
+
return (
{links.slice(0, links.length - 1).map((link, index) => (
@@ -60,7 +41,7 @@ const Breadcrumb: React.FC = () => {
'is-size-4 has-text-weight-medium is-capitalized': links.length < 2,
})}
>
- {currentLink}
+ {links[links.length - 1]}
);
diff --git a/kafka-ui-react-app/src/components/common/Breadcrumb/__tests__/Breadcrumb.spec.tsx b/kafka-ui-react-app/src/components/common/Breadcrumb/__tests__/Breadcrumb.spec.tsx
index 4caa173480..2012305ef0 100644
--- a/kafka-ui-react-app/src/components/common/Breadcrumb/__tests__/Breadcrumb.spec.tsx
+++ b/kafka-ui-react-app/src/components/common/Breadcrumb/__tests__/Breadcrumb.spec.tsx
@@ -1,22 +1,33 @@
import React from 'react';
import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
-import { screen } from '@testing-library/react';
+import { BreadcrumbProvider } from 'components/common/Breadcrumb/Breadcrumb.provider';
+import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route';
import { render } from 'lib/testHelpers';
-const brokersPath = '/ui/clusters/local/brokers';
const createTopicPath = '/ui/clusters/local/topics/create-new';
+const createTopicRoutePath = '/ui/clusters/:clusterName/topics/create-new';
+
+const topicPath = '/ui/clusters/secondLocal/topics/topic-name';
+const topicRoutePath = '/ui/clusters/:clusterName/topics/:topicName';
describe('Breadcrumb component', () => {
- const setupComponent = (pathname: string) =>
- render(, { pathname });
+ const setupComponent = (pathname: string, routePath: string) =>
+ render(
+
+
+
+ ,
+ { pathname }
+ );
- it('renders the name of brokers path', () => {
- setupComponent(brokersPath);
- expect(screen.queryByText('Brokers')).not.toBeInTheDocument();
+ it('renders the list of links', async () => {
+ const { getByText } = setupComponent(createTopicPath, createTopicRoutePath);
+ expect(getByText('Topics')).toBeInTheDocument();
+ expect(getByText('Create New')).toBeInTheDocument();
});
- it('renders the list of links', () => {
- setupComponent(createTopicPath);
- expect(screen.getByText('Topics')).toBeInTheDocument();
- expect(screen.getByText('Create New')).toBeInTheDocument();
+ it('renders the topic overview', async () => {
+ const { getByText } = setupComponent(topicPath, topicRoutePath);
+ expect(getByText('Topics')).toBeInTheDocument();
+ expect(getByText('topic-name')).toBeInTheDocument();
});
});