- {
- navItems.map((category) => (
-
-
- {category.name}
+ {
+ !location.pathname.endsWith('/403')
+ ? (
+
+
+ {
+ navItems.map((category) => (
+
+
+ {category.name}
+
+ {
+ category.items.map((navItem) => (
+ navigate(navItem.link)}
+ >
+ {navItem.name}
+
+ ))
+ }
+
+ ))
+ }
+
+
+ {/* Uncomment this section when the support pages are implemented. */}
+ {/*
+ {supportItems.name}
{
- category.items.map((navItem) => (
+ supportItems.items.map((supportItem) => (
navigate(navItem.link)}
+ key={supportItem.name}
+ renderIcon={Icons[supportItem.icon]}
+ className={
+ supportItem.disabled
+ ? 'disabled-side-nav-option'
+ : 'side-nav-option'
+ }
+ onClick={supportItem.disabled ? null : () => navigate(supportItem.link)}
>
- {navItem.name}
+ {supportItem.name}
))
- }
+ } */}
+
- ))
- }
-
-
- {/* Uncomment this section when the support pages are implemented. */}
- {/*
- {supportItems.name}
-
- {
- supportItems.items.map((supportItem) => (
-
navigate(supportItem.link)}
- >
- {supportItem.name}
-
- ))
- } */}
-
-
-
+
+ )
+ : null
+ }
)}
/>
diff --git a/frontend/src/components/CustomQueryProvider/index.tsx b/frontend/src/components/CustomQueryProvider/index.tsx
new file mode 100644
index 000000000..f034dca69
--- /dev/null
+++ b/frontend/src/components/CustomQueryProvider/index.tsx
@@ -0,0 +1,60 @@
+import React, { ReactNode } from 'react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { isAxiosError } from 'axios';
+
+import { useNavigateContext } from '../../contexts/NavigationContext';
+
+const HTTP_STATUS_TO_NOT_RETRY = [400, 401, 403, 404];
+const MAX_RETRIES = 3;
+
+// Function to generate a QueryClient with error handling
+// and redirect, this will guarantee the redirect for all
+// requests in the application
+const useCustomQueryClient = () => {
+ const { redirectTo403 } = useNavigateContext();
+
+ const queryClient = new QueryClient(
+ {
+ defaultOptions: {
+ queries: {
+ refetchOnMount: false,
+ refetchOnWindowFocus: false,
+ // Do not retry on errors defined above
+ retry: (failureCount, error) => {
+ if (failureCount > MAX_RETRIES) {
+ return false;
+ }
+ if (isAxiosError(error)) {
+ const status = error.response?.status;
+ if (status && HTTP_STATUS_TO_NOT_RETRY.includes(status)) {
+ if (status === 403) {
+ redirectTo403();
+ }
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+ }
+ }
+ );
+
+ return queryClient;
+};
+
+interface CustomQueryProviderProps {
+ children: ReactNode;
+}
+
+const CustomQueryProvider = ({ children }: CustomQueryProviderProps) => {
+ const queryClient = useCustomQueryClient();
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default CustomQueryProvider;
diff --git a/frontend/src/contexts/NavigationContext.tsx b/frontend/src/contexts/NavigationContext.tsx
new file mode 100644
index 000000000..fa6cc400b
--- /dev/null
+++ b/frontend/src/contexts/NavigationContext.tsx
@@ -0,0 +1,32 @@
+import React, { createContext, useContext, useMemo } from 'react';
+
+interface NavigateContextType {
+ redirectTo403: Function;
+}
+
+const NavigationContext = createContext
(undefined);
+
+// This a navigation provider, where a a context will be used to access the useNavigate
+// without necessarily being inside a router context
+export const NavigateProvider: React.FC<{
+ children: React.ReactNode,
+ onRedirect: () => void
+}> = ({ children, onRedirect }) => {
+ const value = useMemo(() => ({ redirectTo403: onRedirect }), [onRedirect]);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useNavigateContext = () => {
+ const context = useContext(NavigationContext);
+
+ if (!context) {
+ throw new Error('useNavigateContext must be used within a NavigateProvider');
+ }
+
+ return context;
+};
diff --git a/frontend/src/routes/constants.ts b/frontend/src/routes/constants.ts
index b88a4c28f..83f19bc3f 100644
--- a/frontend/src/routes/constants.ts
+++ b/frontend/src/routes/constants.ts
@@ -13,6 +13,7 @@ const ROUTES = {
MY_SEEDLOTS: '/seedlots/my-seedlots',
TSC_SEEDLOTS_TABLE: '/seedlots/tsc-admin-seedlots',
FOUR_OH_FOUR: '/404',
+ FOUR_OH_THREE: '/403',
SERVICE_STATUS: '/service-status'
};
diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx
index bd000bc2a..323ae955c 100644
--- a/frontend/src/routes/index.tsx
+++ b/frontend/src/routes/index.tsx
@@ -14,6 +14,7 @@ import SeedlotDetails from '../views/Seedlot/SeedlotDetails';
import SeedlotReview from '../views/Seedlot/SeedlotReview';
import SeedlotRegFormClassA from '../views/Seedlot/SeedlotRegFormClassA';
import ReviewSeedlots from '../views/Seedlot/ReviewSeedlots';
+import FourOhThree from '../views/ErrorViews/FourOhThree';
const BrowserRoutes: Array = [
// Ensures that root paths get redirected to
@@ -90,6 +91,12 @@ const BrowserRoutes: Array = [
element: (
)
+ },
+ {
+ path: ROUTES.FOUR_OH_THREE,
+ element: (
+
+ )
}
];
diff --git a/frontend/src/views/FourOhFour/index.tsx b/frontend/src/views/ErrorViews/FourOhFour/index.tsx
similarity index 90%
rename from frontend/src/views/FourOhFour/index.tsx
rename to frontend/src/views/ErrorViews/FourOhFour/index.tsx
index d1b9dea8b..e97e74a97 100644
--- a/frontend/src/views/FourOhFour/index.tsx
+++ b/frontend/src/views/ErrorViews/FourOhFour/index.tsx
@@ -1,8 +1,8 @@
import React from 'react';
import { Link } from 'react-router-dom';
-import ROUTES from '../../routes/constants';
+import ROUTES from '../../../routes/constants';
-import mysteryImg from '../../assets/img/404-mystery.png';
+import mysteryImg from '../../../assets/img/404-mystery.png';
import './styles.scss';
diff --git a/frontend/src/views/FourOhFour/styles.scss b/frontend/src/views/ErrorViews/FourOhFour/styles.scss
similarity index 100%
rename from frontend/src/views/FourOhFour/styles.scss
rename to frontend/src/views/ErrorViews/FourOhFour/styles.scss
diff --git a/frontend/src/views/ErrorViews/FourOhThree/constants.ts b/frontend/src/views/ErrorViews/FourOhThree/constants.ts
new file mode 100644
index 000000000..216b6a8c6
--- /dev/null
+++ b/frontend/src/views/ErrorViews/FourOhThree/constants.ts
@@ -0,0 +1,7 @@
+export const fourOhThreeTexts = {
+ title: 'Access denied',
+ supportText1: 'Oops! This cone is protecting this area.',
+ supportText2: 'You don\'t have permission to access this page.',
+ buttonLabel: 'Back to home',
+ altText: 'Cone with a cop costume in front of a wall with barbed wire on top'
+};
diff --git a/frontend/src/views/ErrorViews/FourOhThree/index.tsx b/frontend/src/views/ErrorViews/FourOhThree/index.tsx
new file mode 100644
index 000000000..f8975e3c7
--- /dev/null
+++ b/frontend/src/views/ErrorViews/FourOhThree/index.tsx
@@ -0,0 +1,61 @@
+import React from 'react';
+import { useNavigate } from 'react-router-dom';
+import {
+ FlexGrid, Row, Column,
+ Button
+} from '@carbon/react';
+import { Home } from '@carbon/icons-react';
+import Error403 from '../../../assets/img/SPAR_403_error.svg';
+import ROUTES from '../../../routes/constants';
+import useWindowSize from '../../../hooks/UseWindowSize';
+import { fourOhThreeTexts } from './constants';
+import { MEDIUM_SCREEN_WIDTH } from '../../../shared-constants/shared-constants';
+
+import './styles.scss';
+
+const FourOhThree = () => {
+ const navigate = useNavigate();
+ const windowSize = useWindowSize();
+
+ return (
+
+
+
+
+
+
+
+ {fourOhThreeTexts.title}
+
+ {
+ windowSize.innerWidth > MEDIUM_SCREEN_WIDTH
+ ? (
+
+ {fourOhThreeTexts.supportText1}
+
+ )
+ : null
+ }
+
+ {fourOhThreeTexts.supportText2}
+
+
+
+
+
+ );
+};
+
+export default FourOhThree;
diff --git a/frontend/src/views/ErrorViews/FourOhThree/styles.scss b/frontend/src/views/ErrorViews/FourOhThree/styles.scss
new file mode 100644
index 000000000..cc0b85723
--- /dev/null
+++ b/frontend/src/views/ErrorViews/FourOhThree/styles.scss
@@ -0,0 +1,46 @@
+@use '@bcgov-nr/nr-theme/design-tokens/variables.scss' as vars;
+@use '@carbon/type';
+
+.four-oh-three-page {
+ // Remove the paddings from main container
+ margin-top: -5.625rem;
+ margin-left: -14rem;
+
+ h1 {
+ @include type.type-style('fluid-heading-05', true);
+ color: var(--#{vars.$bcgov-prefix}-text-primary);
+ margin-bottom: 2.5rem;
+ }
+
+ p {
+ @include type.type-style('fluid-paragraph-01', true);
+ color: var(--#{vars.$bcgov-prefix}-text-secondary);
+ margin-bottom: 0.25rem;
+ }
+
+ button {
+ margin-top: 2rem;
+ margin-bottom: 3rem;
+ inline-size: 16rem;
+ }
+
+ .four-oh-three-row {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100vh;
+ }
+
+ @media only screen and (max-width: 1055px) {
+ margin-top: 0;
+ margin-left: 0;
+ img {
+ height: 50vh;
+ width: 90vw;
+ }
+ }
+
+ @media only screen and (max-width: 672px) {
+ margin-top: 0;
+ }
+}
\ No newline at end of file
diff --git a/sync/junk.out b/sync/junk.out
new file mode 100644
index 000000000..a94193101
--- /dev/null
+++ b/sync/junk.out
@@ -0,0 +1 @@
+junk
diff --git a/sync/requirements.txt b/sync/requirements.txt
index 5dff317a4..ef2b41a3e 100644
--- a/sync/requirements.txt
+++ b/sync/requirements.txt
@@ -3,5 +3,5 @@ oracledb
numpy==2.1.0
pandas==2.2.2
psycopg2==2.9.9
-SQLAlchemy==2.0.32
+SQLAlchemy==2.0.34
pyyaml==6.0.2