From 620489e57d17dc9e81a48e2a1ea4507e70578db5 Mon Sep 17 00:00:00 2001 From: nick-delirium Date: Mon, 13 Mar 2023 16:06:58 +0100 Subject: [PATCH] change(ui): remove mock, connect api to health status comp --- .../HealthStatus/HealthModal/HealthModal.tsx | 16 +- .../Header/HealthStatus/HealthModal/mock.ts | 187 ------------------ .../Header/HealthStatus/HealthStatus.tsx | 179 +++++++++++------ frontend/app/services/HealthService.ts | 10 + frontend/app/services/index.ts | 4 + 5 files changed, 141 insertions(+), 255 deletions(-) delete mode 100644 frontend/app/components/Header/HealthStatus/HealthModal/mock.ts create mode 100644 frontend/app/services/HealthService.ts diff --git a/frontend/app/components/Header/HealthStatus/HealthModal/HealthModal.tsx b/frontend/app/components/Header/HealthStatus/HealthModal/HealthModal.tsx index 85cfd2dcf..e9aef0e71 100644 --- a/frontend/app/components/Header/HealthStatus/HealthModal/HealthModal.tsx +++ b/frontend/app/components/Header/HealthStatus/HealthModal/HealthModal.tsx @@ -4,11 +4,12 @@ import slide from 'App/svg/cheers.svg'; import { Icon, Button } from 'UI'; import Footer from './Footer' -function Category({ name, healthOk }: { name: string; healthOk: boolean }) { +export function Category({ name, healthOk, onClick }: { name: string; healthOk: boolean; onClick: (args: any) => void }) { const icon = healthOk ? ('check-circle-fill' as const) : ('exclamation-circle-fill' as const); return (
{name} @@ -18,7 +19,7 @@ function Category({ name, healthOk }: { name: string; healthOk: boolean }) { ) } -function HealthModal() { +function HealthModal({ getHealth, isLoading, healthResponse }: { getHealth: () => void; isLoading: boolean; healthResponse: Record }) { return (
Installation Status
-
@@ -46,8 +47,8 @@ function HealthModal() {
- - + + {/**/}
= { - overall: true, - } - const ingestHealth: Record = { - overall: true, - } - const backHealth: Record = { - overall: true, - } - dbKeys.forEach(key => { - const dbStatus = resp.databases[key].health - if (!dbStatus) dbHealth.overall = false - dbHealth[key] = resp.databases.key - }) - ingestKeys.forEach(key => { - const ingestStatus = resp.ingestionPipeline[key].health - if (!ingestStatus) ingestHealth.overall = false - ingestHealth[key] = resp.ingestionPipeline.key - }) - backendKeys.forEach(key => { - const backendStatus = resp.backendServices[key].health - if (!backendStatus) backHealth.overall = false - backHealth[key] = resp.backendServices.key - }) - } +const categoryKeyNames = { + backendServices: 'Backend Services', + databases: 'Databases', + ingestionPipeline: 'Ingestion Pipeline', + ssl: 'SSL', } -function HealthStatus() { - const [healthOk, setHealth] = React.useState(false); +function mapResponse(resp: Record) { + const services = Object.keys(resp); + const healthMap: Record = {} + services.forEach(service => { + healthMap[service] = { + // @ts-ignore + name: categoryKeyNames[service], + healthOk: true, + subservices: resp[service], + } + Object.values(healthMap[service].subservices).forEach((subservice: Record) => { + if (!subservice?.health) healthMap[service].healthOk = false; + }) + }) - const icon = healthOk ? 'pulse' : ('exclamation-circle-fill' as const); + const overallHealth = Object.values(healthMap).every((service: Record) => service.healthOk); + + return { overallHealth, healthMap } +} + + +function HealthStatus() { + const lastAskedKey = '__openreplay_health_status'; + const healthResponseKey = '__openreplay_health_response'; + const healthResponseSaved = localStorage.getItem(healthResponseKey) || '{}'; + const [healthResponse, setHealthResponse] = React.useState(JSON.parse(healthResponseSaved)); + const [isLoading, setIsLoading] = React.useState(false); + const lastAskedSaved = localStorage.getItem(lastAskedKey); + const [lastAsked, setLastAsked] = React.useState(lastAskedSaved); + const [showModal, setShowModal] = React.useState(false); + + const getHealth = async () => { + if (isLoading) return; + try { + setIsLoading(true); + const r = await healthService.fetchStatus(); + const healthMap = mapResponse(r) + setHealthResponse(healthMap); + const asked = new Date().getTime(); + localStorage.setItem(healthResponseKey, JSON.stringify(healthMap)) + localStorage.setItem(lastAskedKey, asked.toString()); + setLastAsked(asked.toString()); + } catch (e) { + console.error(e); + } finally { + setIsLoading(false); + } + }; + + React.useEffect(() => { + const now = new Date(); + const lastAskedDate = lastAsked ? new Date(parseInt(lastAsked, 10)) : null; + const diff = lastAskedDate ? now.getTime() - lastAskedDate.getTime() : 0; + const diffInMinutes = Math.round(diff / 1000 / 60); + if (Object.keys(healthResponse).length === 0 || !lastAskedDate || diffInMinutes > 10) { + void getHealth(); + } + }, []); + + const icon = healthResponse?.overallHealth ? 'pulse' : ('exclamation-circle-fill' as const); return (
- - + + {showModal ? () : null}
); } -function HealthMenu({ healthOk, setHealth }: { healthOk: boolean; setHealth: any }) { +function HealthMenu({ + healthResponse, + getHealth, + isLoading, + lastAsked, + setShowModal, +}: { + healthResponse: Record; + getHealth: Function; + isLoading: boolean; + lastAsked: string | null; + setShowModal: (visible: boolean) => void; +}) { + const [lastAskedDiff, setLastAskedDiff] = React.useState(0); + const healthOk = healthResponse?.overallHealth; + + React.useEffect(() => { + const now = new Date(); + const lastAskedDate = lastAsked ? new Date(parseInt(lastAsked, 10)) : null; + const diff = lastAskedDate ? now.getTime() - lastAskedDate.getTime() : 0; + const diffInMinutes = Math.round(diff / 1000 / 60); + setLastAskedDiff(diffInMinutes); + }, [lastAsked]); + const title = healthOk ? 'All Systems Operational' : 'Service disruption'; const icon = healthOk ? ('check-circle-fill' as const) : ('exclamation-circle-fill' as const); + + const problematicServices = Object.values(healthResponse?.healthMap || {}).filter( + (service: Record) => !service.healthOk + ) as Record[]; return (
{title}
- Last checked 22 mins. ago -
setHealth(!healthOk)}> + Last checked {lastAskedDiff} mins. ago +
getHealth()} + >
-
-
Version
-
- 123 123 -
-
+ {/*
*/} + {/*
Version
*/} + {/*
*/} + {/* 123 123*/} + {/*
*/} + {/*
*/} - {healthOk ? ( + {!healthOk ? ( <> -
-
Sessions
-
- 10 000 -
-
-
-
Events
-
- 90 000 -
-
+
Observed installation Issue with the following
+ {problematicServices.map(service => setShowModal(true)} healthOk={false} name={service.name} />)} - ) : ( -
Observed installation Issue with the following
- )} + ) : null}
diff --git a/frontend/app/services/HealthService.ts b/frontend/app/services/HealthService.ts new file mode 100644 index 000000000..019863bb3 --- /dev/null +++ b/frontend/app/services/HealthService.ts @@ -0,0 +1,10 @@ +import BaseService from './BaseService'; + +export default class HealthService extends BaseService { + fetchStatus(): Promise { + return this.client.get('/health') + .then(r => r.json()) + .then(j => j.data || {}) + .catch(Promise.reject) + } +} \ No newline at end of file diff --git a/frontend/app/services/index.ts b/frontend/app/services/index.ts index 816113e68..32e216127 100644 --- a/frontend/app/services/index.ts +++ b/frontend/app/services/index.ts @@ -10,6 +10,7 @@ import RecordingsService from "./RecordingsService"; import ConfigService from './ConfigService' import AlertsService from './AlertsService' import WebhookService from './WebhookService' +import HealthService from "./HealthService"; export const dashboardService = new DashboardService(); export const metricService = new MetricService(); @@ -24,6 +25,8 @@ export const configService = new ConfigService(); export const alertsService = new AlertsService(); export const webhookService = new WebhookService(); +export const healthService = new HealthService(); + export const services = [ dashboardService, metricService, @@ -37,4 +40,5 @@ export const services = [ configService, alertsService, webhookService, + healthService, ] \ No newline at end of file