From e569e46a8acd6b5a0264c9ce40ba9d2df1fcce2e Mon Sep 17 00:00:00 2001 From: Azat Belgibayev Date: Tue, 25 Jan 2022 15:01:37 +0600 Subject: [PATCH] Refactor breadcrumbs params detection (#1394) Co-authored-by: Oleg Shur --- kafka-ui-react-app/src/components/App.tsx | 23 ++---- .../src/components/Cluster/Cluster.tsx | 81 +++++++++++-------- .../src/components/Connect/Connect.tsx | 11 +-- .../__snapshots__/Connect.spec.tsx.snap | 8 +- .../ConsumerGroups/ConsumerGroups.tsx | 9 ++- .../src/components/Dashboard/Dashboard.tsx | 6 +- .../src/components/KsqlDb/KsqlDb.tsx | 11 ++- .../src/components/Schemas/Schemas.tsx | 15 ++-- .../src/components/Topics/Topics.tsx | 13 ++- .../Topics/shared/Form/FormBreadcrumbs.tsx | 35 -------- .../common/Breadcrumb/Breadcrumb.context.ts | 16 ++++ .../common/Breadcrumb/Breadcrumb.provider.tsx | 51 ++++++++++++ .../common/Breadcrumb/Breadcrumb.route.tsx | 59 ++++++++++++++ .../common/Breadcrumb/Breadcrumb.tsx | 47 ++++------- .../Breadcrumb/__tests__/Breadcrumb.spec.tsx | 33 +++++--- 15 files changed, 264 insertions(+), 154 deletions(-) delete mode 100644 kafka-ui-react-app/src/components/Topics/shared/Form/FormBreadcrumbs.tsx create mode 100644 kafka-ui-react-app/src/components/common/Breadcrumb/Breadcrumb.context.ts create mode 100644 kafka-ui-react-app/src/components/common/Breadcrumb/Breadcrumb.provider.tsx create mode 100644 kafka-ui-react-app/src/components/common/Breadcrumb/Breadcrumb.route.tsx 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(); }); });