From 211978fd77bd0ae19fef6a3f6aa4ec724351931d Mon Sep 17 00:00:00 2001 From: Herrera Date: Mon, 3 Mar 2025 16:04:50 -0800 Subject: [PATCH 1/3] PSP-9900 : PIMS service availability notifications --- source/backend/api/Startup.cs | 17 ++- .../src/assets/scss/_variables.module.scss | 1 + .../frontend/src/assets/scss/_variables.scss | 1 + .../components/common/ApiVersionInfo.test.tsx | 4 + .../layout/Healthcheck/HealthcheckView.tsx | 130 ++++++++++++++++++ .../components/layout/SideNavBar/styles.ts | 4 +- .../frontend/src/constants/healthChecksMsg.ts | 21 +++ .../hooks/pims-api/interfaces/IApiHealth.ts | 3 + .../hooks/pims-api/interfaces/ISystemCheck.ts | 47 +++++++ .../src/hooks/pims-api/useApiHealth.ts | 2 + source/frontend/src/layouts/Healthcheck.tsx | 15 ++ source/frontend/src/layouts/PublicLayout.tsx | 106 +++++++++++++- source/frontend/src/layouts/styles.ts | 16 +++ 13 files changed, 356 insertions(+), 11 deletions(-) create mode 100644 source/frontend/src/components/layout/Healthcheck/HealthcheckView.tsx create mode 100644 source/frontend/src/constants/healthChecksMsg.ts create mode 100644 source/frontend/src/hooks/pims-api/interfaces/ISystemCheck.ts create mode 100644 source/frontend/src/layouts/Healthcheck.tsx diff --git a/source/backend/api/Startup.cs b/source/backend/api/Startup.cs index b86989dbed..65ca112de2 100644 --- a/source/backend/api/Startup.cs +++ b/source/backend/api/Startup.cs @@ -304,7 +304,7 @@ public void ConfigureServices(IServiceCollection services) "PmbcExternalApi", sp => new PimsExternalApiHealthcheck(this.Configuration.GetSection("HealthChecks:PmbcExternalApi")), null, - new string[] { "services", "external" }) + new string[] { "services", "external", "system-check" }) { Period = TimeSpan.FromMinutes(allHealthCheckOptions.PmbcExternalApi.Period) }); } @@ -314,7 +314,7 @@ public void ConfigureServices(IServiceCollection services) "Geoserver", sp => new PimsGeoserverHealthCheck(this.Configuration), null, - new string[] { "services" }) + new string[] { "services", "system", "system-check" }) { Period = TimeSpan.FromMinutes(allHealthCheckOptions.Geoserver.Period) }); } @@ -324,7 +324,7 @@ public void ConfigureServices(IServiceCollection services) "Mayan", sp => new PimsMayanHealthcheck(sp.GetService()), null, - new string[] { "services" }) + new string[] { "services", "system-check" }) { Period = TimeSpan.FromMinutes(allHealthCheckOptions.Mayan.Period) }); } @@ -334,7 +334,7 @@ public void ConfigureServices(IServiceCollection services) "Ltsa", sp => new PimsLtsaHealthcheck(allHealthCheckOptions.Ltsa, sp.GetService()), null, - new string[] { "services", "external" }) + new string[] { "services", "external", "system-check" }) { Period = TimeSpan.FromMinutes(allHealthCheckOptions.Ltsa.Period) }); } @@ -344,7 +344,7 @@ public void ConfigureServices(IServiceCollection services) "Geocoder", sp => new PimsGeocoderHealthcheck(this.Configuration, sp.GetService()), null, - new string[] { "services", "external" }) + new string[] { "services", "external", "system-check" }) { Period = TimeSpan.FromMinutes(allHealthCheckOptions.Geocoder.Period) }); } @@ -354,7 +354,7 @@ public void ConfigureServices(IServiceCollection services) "Cdogs", sp => new PimsCdogsHealthcheck(sp.GetService()), null, - new string[] { "services", "external" }) + new string[] { "services", "external", "system-check" }) { Period = TimeSpan.FromMinutes(allHealthCheckOptions.Cdogs.Period) }); } @@ -489,6 +489,11 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVers app.UseEndpoints(config => { config.MapControllers(); + config.MapHealthChecks("health/system", new HealthCheckOptions() + { + Predicate = r => r.Tags.Contains("system-check"), + ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse, + }); // Enable the /metrics page to export Prometheus metrics config.MapMetrics(); diff --git a/source/frontend/src/assets/scss/_variables.module.scss b/source/frontend/src/assets/scss/_variables.module.scss index b1d3eac9d0..dc60cd93c8 100644 --- a/source/frontend/src/assets/scss/_variables.module.scss +++ b/source/frontend/src/assets/scss/_variables.module.scss @@ -2,6 +2,7 @@ @import './variables'; :export { + healthcheckHeight: $healthcheck-height; mapfilterHeight: $mapfilter-height; footerHeight: $footer-height; headerHeight: $header-height; diff --git a/source/frontend/src/assets/scss/_variables.scss b/source/frontend/src/assets/scss/_variables.scss index 1691150f62..76a309f721 100644 --- a/source/frontend/src/assets/scss/_variables.scss +++ b/source/frontend/src/assets/scss/_variables.scss @@ -1,5 +1,6 @@ @import './colors.scss'; +$healthcheck-height: 4.5rem; $header-height: 7.2rem; $mapfilter-height: 6rem; $footer-height: 4.4rem; diff --git a/source/frontend/src/components/common/ApiVersionInfo.test.tsx b/source/frontend/src/components/common/ApiVersionInfo.test.tsx index 4b78380013..a57dea3a5d 100644 --- a/source/frontend/src/components/common/ApiVersionInfo.test.tsx +++ b/source/frontend/src/components/common/ApiVersionInfo.test.tsx @@ -3,6 +3,8 @@ import { useApiHealth } from '@/hooks/pims-api/useApiHealth'; import { render, RenderOptions, waitForEffects } from '@/utils/test-utils'; import { ApiVersionInfo } from './ApiVersionInfo'; +import ISystemCheck from '@/hooks/pims-api/interfaces/ISystemCheck'; +import { AxiosResponse } from 'axios'; const defaultVersion: IApiVersion = { environment: 'test', @@ -15,12 +17,14 @@ const defaultVersion: IApiVersion = { const mockGetVersionApi = vi.fn(); const mockGetLiveApi = vi.fn(); const mockGetReady = vi.fn(); +const mockGetSystemCheckApi = vi.fn(); vi.mock('@/hooks/pims-api/useApiHealth'); vi.mocked(useApiHealth).mockReturnValue({ getVersion: mockGetVersionApi, getLive: mockGetLiveApi, getReady: mockGetReady, + getSystemCheck: mockGetSystemCheckApi, }); describe('ApiVersionInfo suite', () => { diff --git a/source/frontend/src/components/layout/Healthcheck/HealthcheckView.tsx b/source/frontend/src/components/layout/Healthcheck/HealthcheckView.tsx new file mode 100644 index 0000000000..0ffea755ab --- /dev/null +++ b/source/frontend/src/components/layout/Healthcheck/HealthcheckView.tsx @@ -0,0 +1,130 @@ +import { FaBan } from 'react-icons/fa'; +import styled from 'styled-components'; + +import { LinkButton } from '@/components/common/buttons/LinkButton'; +import { ModalSize } from '@/components/common/GenericModal'; +import { useModalContext } from '@/hooks/useModalContext'; + +export interface IHealthCheckIssue { + key: string; + msg: string; +} + +export interface IHealthCheckViewProps { + systemChecks: IHealthCheckIssue[]; +} + +const HealthcheckView: React.FunctionComponent = ({ systemChecks }) => { + const { setModalContent, setDisplayModal } = useModalContext(); + + return ( + systemChecks.length && ( + + + + + + + {systemChecks.length > 1 && ( + { + setModalContent({ + variant: 'error', + title: 'Error', + modalSize: ModalSize.LARGE, + message: ( + + {systemChecks.map(x => { + return ( + + ); + })} + + ), + okButtonText: 'Close', + handleOk: async () => { + setDisplayModal(false); + }, + }); + setDisplayModal(true); + }} + > + See the full list here... + + )} + + + ) + ); +}; + +const StyledWrapperDiv = styled.div` + display: flex; + height: 100%; + background-color: ${props => props.theme.css.dangerBackgroundColor}; +`; + +const StyledIconDiv = styled.div` + display: flex; + justify-content: center; + align-items: center; + width: auto; + min-width: 6rem; + height: 100%; + background-color: ${props => props.theme.bcTokens.typographyColorDanger}; + + svg { + color: ${props => props.theme.css.pimsWhite}; + } +`; + +const StyledContainer = styled.div` + display: flex; + align-items: center; + flex-direction: row; + flex-grow: 1; + justify-content: flex-start; + padding-left: 6rem; + background-color: ${props => props.theme.css.dangerBackgroundColor}; + + label { + display: list-item; + margin-bottom: 0; + + span { + text-transform: uppercase; + font-weight: bolder; + } + } + + button { + margin-left: 6rem; + } +`; + +const StyledList = styled.div` + padding: 1rem 2rem; + + label { + display: list-item; + margin-bottom: 0; + + span { + text-transform: uppercase; + font-weight: bolder; + } + } + + button { + margin-left: 6rem; + } +`; + +export default HealthcheckView; diff --git a/source/frontend/src/components/layout/SideNavBar/styles.ts b/source/frontend/src/components/layout/SideNavBar/styles.ts index 30defef2db..82dba52d5c 100644 --- a/source/frontend/src/components/layout/SideNavBar/styles.ts +++ b/source/frontend/src/components/layout/SideNavBar/styles.ts @@ -12,9 +12,7 @@ export const TrayHeader = styled(H1)` `; export const SideNavBar = styled.div` - height: calc( - 100vh - ${props => props.theme.css.headerHeight} - ${props => props.theme.css.footerHeight} - ); + height: 100%; overflow-y: auto; overflow-x: hidden; padding-top: 1.5rem; diff --git a/source/frontend/src/constants/healthChecksMsg.ts b/source/frontend/src/constants/healthChecksMsg.ts new file mode 100644 index 0000000000..e295765f17 --- /dev/null +++ b/source/frontend/src/constants/healthChecksMsg.ts @@ -0,0 +1,21 @@ +type TSystemCheckMessages = { + [dict_key: string]: string; +}; + +const SystemCheckMessages: TSystemCheckMessages = { + PimsApi: + 'The PIMS server is currently unavailable, PIMS will not be useable until this is resolved', + PmbcExternalApi: + 'The BC Data Warehouse is experiencing service degradation, this will limit PIMS map functionality until resolved.', + Geoserver: + 'The MOTT Geoserver is experiencing service degradation, PIMS map layer functionality will be limited until resolved.', + Mayan: + 'The PIMS Document server is experiencing service degradation, you will be unable to view, download or upload documents until resolved.', + Ltsa: 'The LTSA title service is experiencing service degradation, the LTSA tab within a property will not be viewable until resolved.', + Geocoder: + 'The BC Geocoder is experiencing service degradation, address search will be unavailable until resolved.', + Cdogs: + 'The DevExchange Document Generation Service is experiencing service degradation, you will be unable to generate form documents (ie. H120, H1005) until resolved.', +}; + +export default SystemCheckMessages; diff --git a/source/frontend/src/hooks/pims-api/interfaces/IApiHealth.ts b/source/frontend/src/hooks/pims-api/interfaces/IApiHealth.ts index 2384c4a557..9e57918fc6 100644 --- a/source/frontend/src/hooks/pims-api/interfaces/IApiHealth.ts +++ b/source/frontend/src/hooks/pims-api/interfaces/IApiHealth.ts @@ -3,6 +3,7 @@ import { AxiosResponse } from 'axios'; import IApiVersion from './IApiVersion'; import IHealthLive from './IHealthLive'; import IHealthReady from './IHealthReady'; +import ISystemCheck from './ISystemCheck'; export interface IApiHealth { // Get the api version information. @@ -11,4 +12,6 @@ export interface IApiHealth { getLive: () => Promise>; // Get the status of the api to determine if it is ready. getReady: () => Promise>; + // Get the status of the api to determine if it is ready. + getSystemCheck: () => Promise>; } diff --git a/source/frontend/src/hooks/pims-api/interfaces/ISystemCheck.ts b/source/frontend/src/hooks/pims-api/interfaces/ISystemCheck.ts new file mode 100644 index 0000000000..b4a8ae23f8 --- /dev/null +++ b/source/frontend/src/hooks/pims-api/interfaces/ISystemCheck.ts @@ -0,0 +1,47 @@ +export interface ISystemCheck { + // Status of the api. + status: string; + // Length of time the api has been healthy. + totalDuration: Date; + // Dictionary of health information. + entries: { + PmbcExternalApi: { + data: object; + duration: Date; + status: string; + tags: string[]; + }; + Geoserver: { + data: object; + duration: Date; + status: string; + tags: string[]; + }; + Mayan: { + data: object; + duration: Date; + status: string; + tags: string[]; + }; + Ltsa: { + data: object; + duration: Date; + status: string; + tags: string[]; + }; + Geocoder: { + data: object; + duration: Date; + status: string; + tags: string[]; + }; + Cdogs: { + data: object; + duration: Date; + status: string; + tags: string[]; + }; + }; +} + +export default ISystemCheck; diff --git a/source/frontend/src/hooks/pims-api/useApiHealth.ts b/source/frontend/src/hooks/pims-api/useApiHealth.ts index d082b59a8b..519f1841b2 100644 --- a/source/frontend/src/hooks/pims-api/useApiHealth.ts +++ b/source/frontend/src/hooks/pims-api/useApiHealth.ts @@ -4,6 +4,7 @@ import { IApiHealth } from './interfaces/IApiHealth'; import IApiVersion from './interfaces/IApiVersion'; import IHealthLive from './interfaces/IHealthLive'; import IHealthReady from './interfaces/IHealthReady'; +import ISystemCheck from './interfaces/ISystemCheck'; import useAxiosApi from './useApi'; /** @@ -19,6 +20,7 @@ export const useApiHealth = () => { getVersion: () => api.get('health/env'), getLive: () => api.get('health/live'), getReady: () => api.get('health/ready'), + getSystemCheck: () => api.get('health/system'), } as IApiHealth), [api], ); diff --git a/source/frontend/src/layouts/Healthcheck.tsx b/source/frontend/src/layouts/Healthcheck.tsx new file mode 100644 index 0000000000..17579afe60 --- /dev/null +++ b/source/frontend/src/layouts/Healthcheck.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import styled from 'styled-components'; + +const HealthCheckStyled: React.FC>> = ({ + ...rest +}) => { + return ; +}; + +const HealthcheckStyled = styled.div` + grid-area: health; + border: 1px solid ${props => props.theme.bcTokens.typographyColorDanger}; +`; + +export default HealthCheckStyled; diff --git a/source/frontend/src/layouts/PublicLayout.tsx b/source/frontend/src/layouts/PublicLayout.tsx index b8b3b3cdc4..666ca6d2ee 100644 --- a/source/frontend/src/layouts/PublicLayout.tsx +++ b/source/frontend/src/layouts/PublicLayout.tsx @@ -1,19 +1,121 @@ -import React from 'react'; +import axios, { AxiosError } from 'axios'; +import React, { useCallback, useEffect, useState } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; import LoadingBar from 'react-redux-loading-bar'; import ErrorModal from '@/components/common/ErrorModal'; import { Footer, Header } from '@/components/layout'; +import HealthcheckView, { + IHealthCheckIssue, +} from '@/components/layout/Healthcheck/HealthcheckView'; +import SystemCheckMessages from '@/constants/healthChecksMsg'; +import ISystemCheck from '@/hooks/pims-api/interfaces/ISystemCheck'; +import { useApiHealth } from '@/hooks/pims-api/useApiHealth'; +import useKeycloakWrapper from '@/hooks/useKeycloakWrapper'; +import { IApiError } from '@/interfaces/IApiError'; import FooterStyled from './Footer'; import HeaderStyled from './Header'; +import HealthCheckStyled from './Healthcheck'; import * as Styled from './styles'; const PublicLayout: React.FC>> = ({ children, ...rest }) => { - return ( + const [systemChecked, setSystemChecked] = useState(null); + const [systemDegraded, setSystemDegraded] = useState(false); + const [healthCheckIssues, sethealthCheckIssues] = useState(null); + + const { getLive, getSystemCheck } = useApiHealth(); + const keycloak = useKeycloakWrapper(); + + const fetchSystemCheckInformation = useCallback(async () => { + const systemIssues: IHealthCheckIssue[] = []; + try { + const pimsApi = await getLive(); + if (pimsApi.data.status !== 'Healthy') { + systemIssues.push({ key: 'PimsApi', msg: SystemCheckMessages.PimsApi }); + setSystemDegraded(true); + } + + const systemCheck = await getSystemCheck(); + if (systemCheck.data.status === 'Healthy') { + return; + } + + sethealthCheckIssues(systemIssues); + } catch (e) { + if (axios.isAxiosError(e)) { + const axiosError = e as AxiosError; + // 500 - API NOT Responding + if (axiosError?.response?.status === 500) { + setSystemDegraded(true); + systemIssues.push({ key: 'PimsApi', msg: SystemCheckMessages.PimsApi }); + } + + // 503 - API responding service not available + if (axiosError?.response?.status === 503) { + const data = axiosError?.response?.data as unknown as ISystemCheck; + if (data.entries?.Geoserver !== null && data.entries.Geoserver?.status !== 'Healthy') { + systemIssues.push({ key: 'Geoserver', msg: SystemCheckMessages.Geoserver }); + } + + if ( + data.entries?.PmbcExternalApi !== null && + data.entries.PmbcExternalApi?.status !== 'Healthy' + ) { + systemIssues.push({ key: 'PmbcExternalApi', msg: SystemCheckMessages.PmbcExternalApi }); + } + + if (data.entries?.Mayan !== null && data.entries.Mayan?.status !== 'Healthy') { + systemIssues.push({ key: 'Mayan', msg: SystemCheckMessages.Mayan }); + } + + if (data.entries?.Ltsa !== null && data.entries.Ltsa?.status !== 'Healthy') { + systemIssues.push({ key: 'Ltsa', msg: SystemCheckMessages.Ltsa }); + } + + if (data.entries?.Geocoder !== null && data.entries.Geocoder?.status !== 'Healthy') { + systemIssues.push({ key: 'Geocoder', msg: SystemCheckMessages.Geocoder }); + } + + if (data.entries?.Cdogs !== null && data.entries.Cdogs?.status !== 'Healthy') { + systemIssues.push({ key: 'Cdogs', msg: SystemCheckMessages.Cdogs }); + } + + setSystemDegraded(true); + sethealthCheckIssues(systemIssues); + } + } + } finally { + setSystemChecked(true); + } + }, [getLive, getSystemCheck]); + + useEffect(() => { + if (systemChecked == null && keycloak.obj.authenticated) { + fetchSystemCheckInformation(); + } + }, [fetchSystemCheckInformation, keycloak.obj.authenticated, systemChecked]); + + return systemDegraded && systemChecked ? ( + <> + + + + + + +
+ + {children} + +