diff --git a/frontend/app/components/Header/HealthStatus/HealthModal/HealthModal.tsx b/frontend/app/components/Header/HealthStatus/HealthModal/HealthModal.tsx index e9aef0e71..b0bbb8969 100644 --- a/frontend/app/components/Header/HealthStatus/HealthModal/HealthModal.tsx +++ b/frontend/app/components/Header/HealthStatus/HealthModal/HealthModal.tsx @@ -1,73 +1,131 @@ import React from 'react'; // @ts-ignore import slide from 'App/svg/cheers.svg'; -import { Icon, Button } from 'UI'; -import Footer from './Footer' +import { Button } from 'UI'; +import Footer from './Footer'; +import { getHighest } from 'App/constants/zindex'; +import Category from 'Components/Header/HealthStatus/ServiceCategory'; +import SubserviceHealth from 'Components/Header/HealthStatus/SubserviceHealth/SubserviceHealth'; +import { IServiceStats } from '../HealthStatus'; -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} +function HealthModal({ + getHealth, + isLoading, + healthResponse, + setShowModal, +}: { + getHealth: () => void; + isLoading: boolean; + healthResponse: { overallHealth: boolean; healthMap: Record }; + setShowModal: (isOpen: boolean) => void; +}) { + const [selectedService, setSelectedService] = React.useState(''); - -
- ) -} - -function HealthModal({ getHealth, isLoading, healthResponse }: { getHealth: () => void; isLoading: boolean; healthResponse: Record }) { + React.useEffect(() => { + if (!healthResponse.overallHealth) { + setSelectedService( + Object.keys(healthResponse.healthMap).filter( + (s) => !healthResponse.healthMap[s].healthOk + )[0] + ); + } + }, [healthResponse]); + const handleClose = () => { + setShowModal(false); + }; return (
e.stopPropagation()} + className={'flex flex-col bg-white rounded border border-figmaColors-divider'} > -
Installation Status
- -
- -
-
- - - - {/**/} -
- +
Installation Status
+
+ +
+
+ {Object.keys(healthResponse.healthMap).map((service) => ( + + setSelectedService(service)} + healthOk={healthResponse.healthMap[service].healthOk} + name={healthResponse.healthMap[service].name} + isSelectable + isSelected={selectedService === service} + /> + + ))} +
+
+ {selectedService ? ( + + ) : ( + + )} +
+
+
+ +
+
-
- -
-
); } - - +function ServiceStatus({ service }: { service: Record }) { + const { subservices } = service; + return ( +
+
+ {Object.keys(subservices).map((subservice: string) => ( + + + + ))} +
+
+ ); +} export default HealthModal; diff --git a/frontend/app/components/Header/HealthStatus/HealthStatus.tsx b/frontend/app/components/Header/HealthStatus/HealthStatus.tsx index 930419f63..7732bca9d 100644 --- a/frontend/app/components/Header/HealthStatus/HealthStatus.tsx +++ b/frontend/app/components/Header/HealthStatus/HealthStatus.tsx @@ -1,38 +1,46 @@ import React from 'react'; import { Icon } from 'UI'; -import cn from 'classnames'; -import HealthModal, { Category } from 'Components/Header/HealthStatus/HealthModal/HealthModal'; +import HealthModal from 'Components/Header/HealthStatus/HealthModal/HealthModal'; import { healthService } from 'App/services'; +import { categoryKeyNames } from './const'; +import HealthWidget from "Components/Header/HealthStatus/HealthWidget"; - -const categoryKeyNames = { - backendServices: 'Backend Services', - databases: 'Databases', - ingestionPipeline: 'Ingestion Pipeline', - ssl: 'SSL', +export interface IServiceStats { + name: 'backendServices' | 'databases' | 'ingestionPipeline' | 'ssl'; + serviceName: string; + healthOk: boolean; + subservices: { + health: boolean; + details?: { + errors?: string[]; + version?: string; + } + }[] } function mapResponse(resp: Record) { const services = Object.keys(resp); - const healthMap: Record = {} - services.forEach(service => { + const healthMap: Record = {}; + services.forEach((service) => { healthMap[service] = { // @ts-ignore name: categoryKeyNames[service], healthOk: true, subservices: resp[service], - } + serviceName: service, + }; Object.values(healthMap[service].subservices).forEach((subservice: Record) => { if (!subservice?.health) healthMap[service].healthOk = false; - }) - }) + }); + }); - const overallHealth = Object.values(healthMap).every((service: Record) => service.healthOk); + const overallHealth = Object.values(healthMap).every( + (service: Record) => service.healthOk + ); - return { overallHealth, healthMap } + return { overallHealth, healthMap }; } - function HealthStatus() { const lastAskedKey = '__openreplay_health_status'; const healthResponseKey = '__openreplay_health_response'; @@ -48,10 +56,10 @@ function HealthStatus() { try { setIsLoading(true); const r = await healthService.fetchStatus(); - const healthMap = mapResponse(r) + const healthMap = mapResponse(r); setHealthResponse(healthMap); const asked = new Date().getTime(); - localStorage.setItem(healthResponseKey, JSON.stringify(healthMap)) + localStorage.setItem(healthResponseKey, JSON.stringify(healthMap)); localStorage.setItem(lastAskedKey, asked.toString()); setLastAsked(asked.toString()); } catch (e) { @@ -73,109 +81,32 @@ function HealthStatus() { const icon = healthResponse?.overallHealth ? 'pulse' : ('exclamation-circle-fill' as const); return ( -
-
-
- -
-
- - - {showModal ? () : null} -
- ); -} - -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 {lastAskedDiff} mins. ago -
getHealth()} - > - +
+
-
-
- {/*
*/} - {/*
Version
*/} - {/*
*/} - {/* 123 123*/} - {/*
*/} - {/*
*/} - - {!healthOk ? ( - <> -
Observed installation Issue with the following
- {problematicServices.map(service => setShowModal(true)} healthOk={false} name={service.name} />)} - - ) : null} -
+
-
+ {showModal ? ( + + ) : null} + ); } + export default HealthStatus; diff --git a/frontend/app/components/Header/HealthStatus/HealthWidget.tsx b/frontend/app/components/Header/HealthStatus/HealthWidget.tsx new file mode 100644 index 000000000..c6372540b --- /dev/null +++ b/frontend/app/components/Header/HealthStatus/HealthWidget.tsx @@ -0,0 +1,93 @@ +import React from 'react' +import { Icon } from "UI"; +import ServiceCategory from "Components/Header/HealthStatus/ServiceCategory"; +import cn from 'classnames' +import { IServiceStats } from './HealthStatus' + +function HealthWidget({ + healthResponse, + getHealth, + isLoading, + lastAsked, + setShowModal, +}: { + healthResponse: { overallHealth: boolean; healthMap: 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 + ) + + return ( +
+
+
+ + {title} +
+
+ Last checked {lastAskedDiff} mins. ago +
getHealth()} + > + +
+
+
+ +
+ {!healthOk ? ( + <> +
+ Observed installation Issue with the following +
+ {problematicServices.map((service) => ( + + setShowModal(true)} + healthOk={false} + name={service.name} + /> + + ))} + + ) : null} +
+
+
+ ); +} + +export default HealthWidget \ No newline at end of file diff --git a/frontend/app/components/Header/HealthStatus/ServiceCategory.tsx b/frontend/app/components/Header/HealthStatus/ServiceCategory.tsx new file mode 100644 index 000000000..3c9259c39 --- /dev/null +++ b/frontend/app/components/Header/HealthStatus/ServiceCategory.tsx @@ -0,0 +1,43 @@ +import { Icon } from 'UI'; +import React from 'react'; +import cn from 'classnames'; + +function Category({ + name, + healthOk, + onClick, + isSelectable, + isExpandable, + isExpanded, + isSelected, +}: { + name: string; + healthOk: boolean; + onClick: (args: any) => void; + isSelectable?: boolean; + isExpandable?: boolean; + isExpanded?: boolean; + isSelected?: boolean; +}) { + const icon = healthOk ? ('check-circle-fill' as const) : ('exclamation-circle-fill' as const); + return ( +
+ + {name} + + {isSelectable ? : null} + {isExpandable ? ( + + ) : null} +
+ ); +} + +export default Category \ No newline at end of file diff --git a/frontend/app/components/Header/HealthStatus/SubserviceHealth/SubserviceHealth.tsx b/frontend/app/components/Header/HealthStatus/SubserviceHealth/SubserviceHealth.tsx new file mode 100644 index 000000000..4de64ffbe --- /dev/null +++ b/frontend/app/components/Header/HealthStatus/SubserviceHealth/SubserviceHealth.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import Category from 'Components/Header/HealthStatus/ServiceCategory'; +import cn from 'classnames'; + +function SubserviceHealth({ + subservice, + name, +}: { + name: string; + subservice: { health: boolean; details: { errors?: string[]; version?: string } }; +}) { + const [isExpanded, setIsExpanded] = React.useState(!subservice?.health); + + const isExpandable = subservice?.details && Object.keys(subservice?.details).length > 0; + return ( +
+ (isExpandable ? setIsExpanded(!isExpanded) : null)} + name={name} + healthOk={subservice?.health} + isExpandable={isExpandable} + isExpanded={isExpanded} + /> + {isExpanded ? ( +
+ {subservice?.details?.version ? ( +
+
Version
+
+ {subservice?.details?.version} +
+
+ ) : null} + {subservice?.details?.errors?.length ? ( +
+
Error log:
+ {subservice.details.errors.map((err: string, i) => ( +
+ {i + 1}. {err} +
+ ))} +
+ ) : subservice?.health ? null : ( + 'Service not responding' + )} +
+ ) : null} +
+ ); +} + +export default SubserviceHealth; diff --git a/frontend/app/components/Header/HealthStatus/const.ts b/frontend/app/components/Header/HealthStatus/const.ts new file mode 100644 index 000000000..3c13c52dd --- /dev/null +++ b/frontend/app/components/Header/HealthStatus/const.ts @@ -0,0 +1,6 @@ +export const categoryKeyNames = { + backendServices: 'Backend Services', + databases: 'Databases', + ingestionPipeline: 'Ingestion Pipeline', + ssl: 'SSL', +} as const \ No newline at end of file