diff --git a/.github/workflows/.deploy.yml b/.github/workflows/.deploy.yml index b4bc49176..b6f6294c1 100644 --- a/.github/workflows/.deploy.yml +++ b/.github/workflows/.deploy.yml @@ -147,7 +147,6 @@ jobs: oc_namespace: ${{ vars.OC_NAMESPACE }} oc_server: ${{ vars.OC_SERVER }} oc_token: ${{ secrets.OC_TOKEN }} - oc_version: ${{ matrix.oc_version }} overwrite: ${{ matrix.overwrite }} parameters: -p TAG=${{ inputs.tag }} @@ -174,7 +173,6 @@ jobs: oc_namespace: ${{ vars.OC_NAMESPACE }} oc_server: ${{ vars.OC_SERVER }} oc_token: ${{ secrets.OC_TOKEN }} - oc_version: "4.13" overwrite: true parameters: -p TAG=${{ inputs.tag }} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e78277674..733a6f864 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,9 +5,8 @@ import { import { Amplify } from 'aws-amplify'; import { ClassPrefix } from '@carbon/react'; import { ToastContainer } from 'react-toastify'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; -import { isAxiosError } from 'axios'; +import awsconfig from './aws-exports'; import prefix from './styles/classPrefix'; import './styles/custom.scss'; @@ -15,45 +14,19 @@ import 'react-toastify/dist/ReactToastify.css'; import Layout from './layout/PrivateLayout'; import Landing from './views/Landing'; -import awsconfig from './aws-exports'; +import FourOhFour from './views/ErrorViews/FourOhFour'; +import LoginOrgSelection from './views/LoginOrgSelection'; +import ServiceStatus from './views/ServiceStatus'; +import { NavigateProvider } from './contexts/NavigationContext'; import AuthContext from './contexts/AuthContext'; import BrowserRoutes from './routes'; import ROUTES from './routes/constants'; -import FourOhFour from './views/FourOhFour'; import ProtectedRoute from './routes/ProtectedRoute'; import { ThemePreference } from './utils/ThemePreference'; -import LoginOrgSelection from './views/LoginOrgSelection'; -import ServiceStatus from './views/ServiceStatus'; +import CustomQueryProvider from './components/CustomQueryProvider'; Amplify.configure(awsconfig); -const HTTP_STATUS_TO_NOT_RETRY = [400, 401, 403, 404]; -const MAX_RETRIES = 3; - -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)) { - return false; - } - } - return true; - } - } - } - } -); - /** * Create an app structure containing all the routes. * @@ -120,14 +93,22 @@ const App: React.FC = () => { return createBrowserRouter(selectedRoutes); }; + const browserRouter = getBrowserRouter(); + + const handleRedirectTo403 = () => { + browserRouter.navigate('/403'); + }; + return ( - - - - - + + + + + + + ); diff --git a/frontend/src/assets/img/SPAR_403_error.svg b/frontend/src/assets/img/SPAR_403_error.svg new file mode 100644 index 000000000..b668ef7b4 --- /dev/null +++ b/frontend/src/assets/img/SPAR_403_error.svg @@ -0,0 +1,374 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/components/BCHeader/index.tsx b/frontend/src/components/BCHeader/index.tsx index 068e5ca60..cd8ce945f 100644 --- a/frontend/src/components/BCHeader/index.tsx +++ b/frontend/src/components/BCHeader/index.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; +import { Link, useLocation, useNavigate } from 'react-router-dom'; import { HeaderContainer, @@ -37,6 +37,8 @@ const BCHeader = () => { const [rightPanel, setRightPanel] = useState(defaultPanelState); const [overlay, setOverlay] = useState(false); + const location = useLocation(); + const windowSize = useWindowSize(); const handleRightPanel = (panel: keyof RightPanelType) => { @@ -88,11 +90,17 @@ const BCHeader = () => { onClick={isSideNavExpanded ? onClickSideNavExpand : null} > - + { + !location.pathname.endsWith('/403') + ? ( + + ) + : null + } {componentTexts.headerTitle} {componentTexts.completeTitle} @@ -164,62 +172,68 @@ const BCHeader = () => { ) : null } - -
- { - 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.altText} + + +

+ {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