Quellcode durchsuchen

[FE] Implement 404/403s (#2966)

* Add SuspenseQueryComponent for the ErrorBoundary delegation , implement this component in all those pages where the issue was happening

* Add Comment to the SuspenseQueryComponent

* Create the Error page

* Error page styling modifications

* Error Page redirections

* Redux Request handle error case and Navigation

* SuspenseQueryComponent test suites

* minor ErrorPage component modifications

* Add error page test suites

* SuspenseQueryComponent Error handling modification
Mgrdich vor 2 Jahren
Ursprung
Commit
4dc0f6d81e

+ 17 - 2
kafka-ui-react-app/src/components/App.tsx

@@ -1,6 +1,11 @@
 import React, { Suspense, useCallback } from 'react';
-import { Routes, Route, useLocation } from 'react-router-dom';
-import { clusterPath, getNonExactPath } from 'lib/paths';
+import { Routes, Route, useLocation, Navigate } from 'react-router-dom';
+import {
+  accessErrorPage,
+  clusterPath,
+  errorPage,
+  getNonExactPath,
+} from 'lib/paths';
 import Nav from 'components/Nav/Nav';
 import PageLoader from 'components/common/PageLoader/PageLoader';
 import Dashboard from 'components/Dashboard/Dashboard';
@@ -20,6 +25,7 @@ import DiscordIcon from 'components/common/Icons/DiscordIcon';
 import ConfirmationModal from './common/ConfirmationModal/ConfirmationModal';
 import { ConfirmContextProvider } from './contexts/ConfirmContext';
 import { GlobalSettingsProvider } from './contexts/GlobalSettingsContext';
+import ErrorPage from './ErrorPage/ErrorPage';
 
 const queryClient = new QueryClient({
   defaultOptions: {
@@ -123,6 +129,15 @@ const App: React.FC = () => {
                     path={getNonExactPath(clusterPath())}
                     element={<ClusterPage />}
                   />
+                  <Route
+                    path={accessErrorPage}
+                    element={<ErrorPage status={403} text="Access is Denied" />}
+                  />
+                  <Route path={errorPage} element={<ErrorPage />} />
+                  <Route
+                    path="*"
+                    element={<Navigate to={errorPage} replace />}
+                  />
                 </Routes>
               </S.Container>
               <Toaster position="bottom-right" />

+ 9 - 1
kafka-ui-react-app/src/components/Brokers/Brokers.tsx

@@ -3,11 +3,19 @@ import { Route, Routes } from 'react-router-dom';
 import { getNonExactPath, RouteParams } from 'lib/paths';
 import BrokersList from 'components/Brokers/BrokersList/BrokersList';
 import Broker from 'components/Brokers/Broker/Broker';
+import SuspenseQueryComponent from 'components/common/SuspenseQueryComponent/SuspenseQueryComponent';
 
 const Brokers: React.FC = () => (
   <Routes>
     <Route index element={<BrokersList />} />
-    <Route path={getNonExactPath(RouteParams.brokerId)} element={<Broker />} />
+    <Route
+      path={getNonExactPath(RouteParams.brokerId)}
+      element={
+        <SuspenseQueryComponent>
+          <Broker />
+        </SuspenseQueryComponent>
+      }
+    />
   </Routes>
 );
 

+ 6 - 1
kafka-ui-react-app/src/components/Connect/Connect.tsx

@@ -9,6 +9,7 @@ import {
   clusterConnectorsPath,
 } from 'lib/paths';
 import useAppParams from 'lib/hooks/useAppParams';
+import SuspenseQueryComponent from 'components/common/SuspenseQueryComponent/SuspenseQueryComponent';
 
 import ListPage from './List/ListPage';
 import New from './New/New';
@@ -23,7 +24,11 @@ const Connect: React.FC = () => {
       <Route path={clusterConnectorNewRelativePath} element={<New />} />
       <Route
         path={getNonExactPath(clusterConnectConnectorRelativePath)}
-        element={<DetailsPage />}
+        element={
+          <SuspenseQueryComponent>
+            <DetailsPage />
+          </SuspenseQueryComponent>
+        }
       />
       <Route
         path={clusterConnectConnectorsRelativePath}

+ 20 - 0
kafka-ui-react-app/src/components/ErrorPage/ErrorPage.styled.ts

@@ -0,0 +1,20 @@
+import styled from 'styled-components';
+
+export const Wrapper = styled.div`
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  flex-direction: column;
+  gap: 20px;
+  margin-top: 100px;
+`;
+
+export const Number = styled.div`
+  font-size: 100px;
+  color: ${({ theme }) => theme.errorPage.text};
+  line-height: initial;
+`;
+
+export const Text = styled.div`
+  font-size: 20px;
+`;

+ 28 - 0
kafka-ui-react-app/src/components/ErrorPage/ErrorPage.tsx

@@ -0,0 +1,28 @@
+import React from 'react';
+import { Button } from 'components/common/Button/Button';
+
+import * as S from './ErrorPage.styled';
+
+interface Props {
+  status?: number;
+  text?: string;
+  btnText?: string;
+}
+
+const ErrorPage: React.FC<Props> = ({
+  status = 404,
+  text = 'Page is not found',
+  btnText = 'Go Back to Dashboard',
+}) => {
+  return (
+    <S.Wrapper>
+      <S.Number>{status}</S.Number>
+      <S.Text>{text}</S.Text>
+      <Button buttonType="primary" buttonSize="M" to="/">
+        {btnText}
+      </Button>
+    </S.Wrapper>
+  );
+};
+
+export default ErrorPage;

+ 24 - 0
kafka-ui-react-app/src/components/ErrorPage/__tests__/ErrorPage.spec.tsx

@@ -0,0 +1,24 @@
+import React from 'react';
+import { screen } from '@testing-library/react';
+import { render } from 'lib/testHelpers';
+import ErrorPage from 'components/ErrorPage/ErrorPage';
+
+describe('ErrorPage', () => {
+  it('should check Error Page rendering with default text', () => {
+    render(<ErrorPage />);
+    expect(screen.getByText('404')).toBeInTheDocument();
+    expect(screen.getByText('Page is not found')).toBeInTheDocument();
+    expect(screen.getByText('Go Back to Dashboard')).toBeInTheDocument();
+  });
+  it('should check Error Page rendering with custom text', () => {
+    const props = {
+      status: 403,
+      text: 'access is denied',
+      btnText: 'Go back',
+    };
+    render(<ErrorPage {...props} />);
+    expect(screen.getByText(props.status)).toBeInTheDocument();
+    expect(screen.getByText(props.text)).toBeInTheDocument();
+    expect(screen.getByText(props.btnText)).toBeInTheDocument();
+  });
+});

+ 6 - 0
kafka-ui-react-app/src/components/Schemas/Details/Details.tsx

@@ -20,6 +20,7 @@ import {
   SCHEMA_LATEST_FETCH_ACTION,
   selectAllSchemaVersions,
   getSchemaLatest,
+  getAreSchemaLatestRejected,
 } from 'redux/reducers/schemas/schemasSlice';
 import { showServerError } from 'lib/errorHandling';
 import { resetLoaderById } from 'redux/reducers/loader/loaderSlice';
@@ -55,6 +56,7 @@ const Details: React.FC = () => {
   const versions = useAppSelector((state) => selectAllSchemaVersions(state));
   const schema = useAppSelector(getSchemaLatest);
   const isFetched = useAppSelector(getAreSchemaLatestFulfilled);
+  const isRejected = useAppSelector(getAreSchemaLatestRejected);
   const areVersionsFetched = useAppSelector(getAreSchemaVersionsFulfilled);
 
   const columns = React.useMemo(
@@ -78,6 +80,10 @@ const Details: React.FC = () => {
     }
   };
 
+  if (isRejected) {
+    navigate('/404');
+  }
+
   if (!isFetched || !schema) {
     return <PageLoader />;
   }

+ 6 - 0
kafka-ui-react-app/src/components/Schemas/Edit/Edit.tsx

@@ -25,6 +25,7 @@ import {
   SCHEMA_LATEST_FETCH_ACTION,
   getAreSchemaLatestFulfilled,
   schemaUpdated,
+  getAreSchemaLatestRejected,
 } from 'redux/reducers/schemas/schemasSlice';
 import PageLoader from 'components/common/PageLoader/PageLoader';
 import { resetLoaderById } from 'redux/reducers/loader/loaderSlice';
@@ -54,6 +55,7 @@ const Edit: React.FC = () => {
 
   const schema = useAppSelector((state) => getSchemaLatest(state));
   const isFetched = useAppSelector(getAreSchemaLatestFulfilled);
+  const isRejected = useAppSelector(getAreSchemaLatestRejected);
 
   const formatedSchema = React.useMemo(() => {
     return schema?.schemaType === SchemaType.PROTOBUF
@@ -98,6 +100,10 @@ const Edit: React.FC = () => {
     }
   };
 
+  if (isRejected) {
+    navigate('/404');
+  }
+
   if (!isFetched || !schema) {
     return <PageLoader />;
   }

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

@@ -6,6 +6,7 @@ import {
   getNonExactPath,
   RouteParams,
 } from 'lib/paths';
+import SuspenseQueryComponent from 'components/common/SuspenseQueryComponent/SuspenseQueryComponent';
 
 import New from './New/New';
 import ListPage from './List/ListPage';
@@ -16,7 +17,14 @@ const Topics: React.FC = () => (
     <Route index element={<ListPage />} />
     <Route path={clusterTopicNewRelativePath} element={<New />} />
     <Route path={clusterTopicCopyRelativePath} element={<New />} />
-    <Route path={getNonExactPath(RouteParams.topicName)} element={<Topic />} />
+    <Route
+      path={getNonExactPath(RouteParams.topicName)}
+      element={
+        <SuspenseQueryComponent>
+          <Topic />
+        </SuspenseQueryComponent>
+      }
+    />
   </Routes>
 );
 

+ 29 - 0
kafka-ui-react-app/src/components/common/SuspenseQueryComponent/SuspenseQueryComponent.tsx

@@ -0,0 +1,29 @@
+import React, { PropsWithChildren } from 'react';
+import { ErrorBoundary } from 'react-error-boundary';
+import { Navigate } from 'react-router-dom';
+
+const ErrorComponent: React.FC<{ error: Error }> = ({ error }) => {
+  const errorStatus = (error as unknown as Response)?.status
+    ? (error as unknown as Response).status
+    : '404';
+
+  return <Navigate to={`/${errorStatus}`} />;
+};
+
+/**
+ * @description
+ * basic idea that you can not choose a wrong url, that is why you are safe, but when
+ * the user tries to manipulate some url to get the the desired result and the BE returns 404
+ * it will be propagated to this component and redirected
+ *
+ * !!NOTE!! But only use this Component for GET query Throw error cause maybe in the future inner functionality may change
+ * */
+const SuspenseQueryComponent: React.FC<PropsWithChildren<unknown>> = ({
+  children,
+}) => {
+  return (
+    <ErrorBoundary FallbackComponent={ErrorComponent}>{children}</ErrorBoundary>
+  );
+};
+
+export default SuspenseQueryComponent;

+ 37 - 0
kafka-ui-react-app/src/components/common/SuspenseQueryComponent/__test__/SuspenseQueryComponent.spec.tsx

@@ -0,0 +1,37 @@
+import React from 'react';
+import { render } from 'lib/testHelpers';
+import { screen } from '@testing-library/react';
+import SuspenseQueryComponent from 'components/common/SuspenseQueryComponent/SuspenseQueryComponent';
+
+const fallback = 'fallback';
+
+jest.mock('react-router-dom', () => ({
+  ...jest.requireActual('react-router-dom'),
+  Navigate: () => <div>{fallback}</div>,
+}));
+
+describe('SuspenseQueryComponent', () => {
+  const text = 'text';
+
+  it('should render the inner component if no error occurs', () => {
+    render(<SuspenseQueryComponent>{text}</SuspenseQueryComponent>);
+    expect(screen.getByText(text)).toBeInTheDocument();
+  });
+
+  it('should not render the inner component and call navigate', () => {
+    // throwing intentional For error boundaries to work
+    jest.spyOn(console, 'error').mockImplementation(() => undefined);
+    const Component = () => {
+      throw new Error('new Error');
+    };
+
+    render(
+      <SuspenseQueryComponent>
+        <Component />
+      </SuspenseQueryComponent>
+    );
+    expect(screen.queryByText(text)).not.toBeInTheDocument();
+    expect(screen.getByText(fallback)).toBeInTheDocument();
+    jest.clearAllMocks();
+  });
+});

+ 3 - 0
kafka-ui-react-app/src/lib/paths.ts

@@ -23,6 +23,9 @@ export enum RouteParams {
 
 export const getNonExactPath = (path: string) => `${path}/*`;
 
+export const errorPage = '/404';
+export const accessErrorPage = '/403';
+
 export const clusterPath = (
   clusterName: ClusterName = RouteParams.clusterName
 ) => `/ui/clusters/${clusterName}`;

+ 5 - 0
kafka-ui-react-app/src/redux/reducers/schemas/schemasSlice.ts

@@ -134,6 +134,11 @@ export const getAreSchemaLatestFulfilled = createSelector(
   createFetchingSelector(SCHEMA_LATEST_FETCH_ACTION),
   (status) => status === AsyncRequestStatus.fulfilled
 );
+export const getAreSchemaLatestRejected = createSelector(
+  createFetchingSelector(SCHEMA_LATEST_FETCH_ACTION),
+  (status) => status === AsyncRequestStatus.rejected
+);
+
 export const getAreSchemaVersionsFulfilled = createSelector(
   createFetchingSelector(SCHEMAS_VERSIONS_FETCH_ACTION),
   (status) => status === AsyncRequestStatus.fulfilled

+ 3 - 0
kafka-ui-react-app/src/theme/theme.ts

@@ -581,6 +581,9 @@ const theme = {
   statictics: {
     createdAtColor: Colors.neutral[50],
   },
+  errorPage: {
+    text: Colors.blue[45],
+  },
 };
 
 export type ThemeType = typeof theme;