Selaa lähdekoodia

Refactor breadcrumbs params detection (#1394)

Co-authored-by: Oleg Shur <workshur@gmail.com>
Azat Belgibayev 3 vuotta sitten
vanhempi
commit
e569e46a8a

+ 8 - 15
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 ? (
-            <>
-              <Breadcrumb />
-              <Switch>
-                <Route
-                  exact
-                  path={['/', '/ui', '/ui/clusters']}
-                  component={Dashboard}
-                />
-                <Route
-                  path="/ui/clusters/:clusterName"
-                  component={ClusterPage}
-                />
-              </Switch>
-            </>
+            <Switch>
+              <Route
+                exact
+                path={['/', '/ui', '/ui/clusters']}
+                component={Dashboard}
+              />
+              <Route path="/ui/clusters/:clusterName" component={ClusterPage} />
+            </Switch>
           ) : (
             <PageLoader />
           )}

+ 48 - 33
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 (
-    <ClusterContext.Provider value={contextValue}>
-      <Switch>
-        <Route path={clusterBrokersPath(':clusterName')} component={Brokers} />
-        <Route path={clusterTopicsPath(':clusterName')} component={Topics} />
-        <Route
-          path={clusterConsumerGroupsPath(':clusterName')}
-          component={ConsumersGroups}
-        />
-        {hasSchemaRegistryConfigured && (
-          <Route
-            path={clusterSchemasPath(':clusterName')}
-            component={Schemas}
+    <BreadcrumbProvider>
+      <Breadcrumb />
+      <ClusterContext.Provider value={contextValue}>
+        <Switch>
+          <BreadcrumbRoute
+            path={clusterBrokersPath(':clusterName')}
+            component={Brokers}
           />
-        )}
-        {hasKafkaConnectConfigured && (
-          <Route
-            path={clusterConnectsPath(':clusterName')}
-            component={Connect}
+          <BreadcrumbRoute
+            path={clusterTopicsPath(':clusterName')}
+            component={Topics}
           />
-        )}
-        {hasKafkaConnectConfigured && (
-          <Route
-            path={clusterConnectorsPath(':clusterName')}
-            component={Connect}
+          <BreadcrumbRoute
+            path={clusterConsumerGroupsPath(':clusterName')}
+            component={ConsumersGroups}
           />
-        )}
-        {hasKsqlDbConfigured && (
-          <Route path={clusterKsqlDbPath(':clusterName')} component={KsqlDb} />
-        )}
-        <Redirect
-          from="/ui/clusters/:clusterName"
-          to="/ui/clusters/:clusterName/brokers"
-        />
-      </Switch>
-    </ClusterContext.Provider>
+          {hasSchemaRegistryConfigured && (
+            <BreadcrumbRoute
+              path={clusterSchemasPath(':clusterName')}
+              component={Schemas}
+            />
+          )}
+          {hasKafkaConnectConfigured && (
+            <BreadcrumbRoute
+              path={clusterConnectsPath(':clusterName')}
+              component={Connect}
+            />
+          )}
+          {hasKafkaConnectConfigured && (
+            <BreadcrumbRoute
+              path={clusterConnectorsPath(':clusterName')}
+              component={Connect}
+            />
+          )}
+          {hasKsqlDbConfigured && (
+            <BreadcrumbRoute
+              path={clusterKsqlDbPath(':clusterName')}
+              component={KsqlDb}
+            />
+          )}
+          <Redirect
+            from="/ui/clusters/:clusterName"
+            to="/ui/clusters/:clusterName/brokers"
+          />
+        </Switch>
+      </ClusterContext.Provider>
+    </BreadcrumbProvider>
   );
 };
 

+ 6 - 5
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 = () => (
   <div>
     <Switch>
-      <Route
+      <BreadcrumbRoute
         exact
         path={clusterConnectorsPath(':clusterName')}
         component={ListContainer}
       />
-      <Route
+      <BreadcrumbRoute
         exact
         path={clusterConnectorNewPath(':clusterName')}
         component={NewContainer}
       />
-      <Route
+      <BreadcrumbRoute
         exact
         path={clusterConnectConnectorEditPath(
           ':clusterName',
@@ -34,7 +35,7 @@ const Connect: React.FC = () => (
         )}
         component={EditContainer}
       />
-      <Route
+      <BreadcrumbRoute
         path={clusterConnectConnectorPath(
           ':clusterName',
           ':connectName',

+ 4 - 4
kafka-ui-react-app/src/components/Connect/__tests__/__snapshots__/Connect.spec.tsx.snap

@@ -3,7 +3,7 @@
 exports[`Connect matches snapshot 1`] = `
 <div>
   <Switch>
-    <Route
+    <BreadcrumbRoute
       component={
         Object {
           "$$typeof": Symbol(react.memo),
@@ -15,17 +15,17 @@ exports[`Connect matches snapshot 1`] = `
       exact={true}
       path="/ui/clusters/:clusterName/connectors"
     />
-    <Route
+    <BreadcrumbRoute
       component={[Function]}
       exact={true}
       path="/ui/clusters/:clusterName/connectors/create-new"
     />
-    <Route
+    <BreadcrumbRoute
       component={[Function]}
       exact={true}
       path="/ui/clusters/:clusterName/connects/:connectName/connectors/:connectorName/edit"
     />
-    <Route
+    <BreadcrumbRoute
       component={[Function]}
       path="/ui/clusters/:clusterName/connects/:connectName/connectors/:connectorName"
     />

+ 5 - 4
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 (
       <Switch>
-        <Route
+        <BreadcrumbRoute
           exact
           path="/ui/clusters/:clusterName/consumer-groups"
           component={List}
         />
-        <Route
+        <BreadcrumbRoute
           exact
           path="/ui/clusters/:clusterName/consumer-groups/:consumerGroupID"
           component={Details}
         />
-        <Route
+        <BreadcrumbRoute
           path="/ui/clusters/:clusterName/consumer-groups/:consumerGroupID/reset-offsets"
           component={ResetOffsets}
         />

+ 4 - 2
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 = () => (
-  <div>
+  <>
+    <PageHeading text="Dashboard" />
     <ClustersWidgetContainer />
-  </div>
+  </>
 );
 
 export default Dashboard;

+ 8 - 3
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 (
     <Switch>
-      <Route exact path={clusterKsqlDbPath()} component={List} />
-      <Route exact path={clusterKsqlDbQueryPath()} component={Query} />
+      <BreadcrumbRoute exact path={clusterKsqlDbPath()} component={List} />
+      <BreadcrumbRoute
+        exact
+        path={clusterKsqlDbQueryPath()}
+        component={Query}
+      />
     </Switch>
   );
 };

+ 10 - 5
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 (
     <Switch>
-      <Route exact path={clusterSchemasPath(':clusterName')} component={List} />
-      <Route
+      <BreadcrumbRoute
+        exact
+        path={clusterSchemasPath(':clusterName')}
+        component={List}
+      />
+      <BreadcrumbRoute
         exact
         path={clusterSchemaNewPath(':clusterName')}
         component={New}
       />
-      <Route
+      <BreadcrumbRoute
         exact
         path={clusterSchemaPath(':clusterName', ':subject')}
         component={Details}
       />
-      <Route
+      <BreadcrumbRoute
         exact
         path={clusterSchemaEditPath(':clusterName', ':subject')}
         component={Edit}

+ 9 - 4
kafka-ui-react-app/src/components/Topics/Topics.tsx

@@ -1,10 +1,11 @@
 import React from 'react';
-import { Switch, Route } from 'react-router-dom';
+import { Switch } from 'react-router-dom';
 import {
   clusterTopicNewPath,
   clusterTopicPath,
   clusterTopicsPath,
 } from 'lib/paths';
+import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route';
 
 import ListContainer from './List/ListContainer';
 import TopicContainer from './Topic/TopicContainer';
@@ -12,13 +13,17 @@ import New from './New/New';
 
 const Topics: React.FC = () => (
   <Switch>
-    <Route
+    <BreadcrumbRoute
       exact
       path={clusterTopicsPath(':clusterName')}
       component={ListContainer}
     />
-    <Route exact path={clusterTopicNewPath(':clusterName')} component={New} />
-    <Route
+    <BreadcrumbRoute
+      exact
+      path={clusterTopicNewPath(':clusterName')}
+      component={New}
+    />
+    <BreadcrumbRoute
       path={clusterTopicPath(':clusterName', ':topicName')}
       component={TopicContainer}
     />

+ 0 - 35
kafka-ui-react-app/src/components/Topics/shared/Form/FormBreadcrumbs.tsx

@@ -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<Props> = ({
-  clusterName,
-  topicName,
-  current,
-}) => {
-  const allTopicsLink = {
-    href: clusterTopicsPath(clusterName),
-    label: 'All Topics',
-  };
-  const links = topicName
-    ? [
-        allTopicsLink,
-        { href: clusterTopicPath(clusterName, topicName), label: topicName },
-      ]
-    : [allTopicsLink];
-
-  return (
-    <div className="level-item level-left">
-      <Breadcrumb links={links}>{current}</Breadcrumb>
-    </div>
-  );
-};
-
-export default FormBreadcrumbs;

+ 16 - 0
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<BreadcrumbContextInterface>({
+  link: '',
+  path: [],
+  handleRouteChange: () => {},
+});

+ 51 - 0
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<BreadcrumbEntry>({
+    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 (
+    <BreadcrumbContext.Provider
+      value={{
+        ...state,
+        handleRouteChange,
+      }}
+    >
+      {children}
+    </BreadcrumbContext.Provider>
+  );
+};

+ 59 - 0
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<RouteProps> = ({
+  children,
+  render,
+  component,
+  ...props
+}) => {
+  return (
+    <Route
+      {...props}
+      render={(routeParams) => {
+        if (component) {
+          return (
+            <>
+              {React.createElement(component)}
+              <BreadcrumbRouteInternal />
+            </>
+          );
+        }
+        if (render) {
+          return (
+            <>
+              {render(routeParams)}
+              <BreadcrumbRouteInternal />
+            </>
+          );
+        }
+
+        return (
+          <>
+            {children}
+            <BreadcrumbRouteInternal />
+          </>
+        );
+      }}
+    />
+  );
+};

+ 14 - 33
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<Props> = () => {
-  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 (
     <BreadcrumbWrapper role="list">
       {links.slice(0, links.length - 1).map((link, index) => (
@@ -60,7 +41,7 @@ const Breadcrumb: React.FC<Props> = () => {
           'is-size-4 has-text-weight-medium is-capitalized': links.length < 2,
         })}
       >
-        <span>{currentLink}</span>
+        <span>{links[links.length - 1]}</span>
       </li>
     </BreadcrumbWrapper>
   );

+ 22 - 11
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(<Breadcrumb />, { pathname });
+  const setupComponent = (pathname: string, routePath: string) =>
+    render(
+      <BreadcrumbProvider>
+        <Breadcrumb />
+        <BreadcrumbRoute path={routePath} />
+      </BreadcrumbProvider>,
+      { 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();
   });
 });