change(ui): remove mock, connect api to health status comp
This commit is contained in:
parent
6657a59cb9
commit
620489e57d
5 changed files with 141 additions and 255 deletions
|
|
@ -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 (
|
||||
<div
|
||||
className={'p-4 flex items-center gap-2 border-b cursor-pointer hover:bg-active-blue'}
|
||||
className={'px-4 py-2 flex items-center gap-2 border-b cursor-pointer hover:bg-active-blue'}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Icon name={icon} size={20} color={'green'} />
|
||||
{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<string, any> }) {
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -37,7 +38,7 @@ function HealthModal() {
|
|||
}
|
||||
>
|
||||
<div className={'text-xl font-semibold'}>Installation Status</div>
|
||||
<Button icon={'arrow-repeat'} variant={'text-primary'}>
|
||||
<Button loading={isLoading} onClick={getHealth} icon={'arrow-repeat'} variant={'text-primary'}>
|
||||
Recheck
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -46,8 +47,8 @@ function HealthModal() {
|
|||
<div className={'flex flex-col h-full'} style={{ flex: 1 }}>
|
||||
<Category name={'Databases'} healthOk={true} />
|
||||
<Category name={'Ingestion Pipeline'} healthOk={false} />
|
||||
<Category name={'Backend'} healthOk={false} />
|
||||
<Category name={'SSL'} healthOk={true} />
|
||||
<Category name={'Backend Services'} healthOk={false} />
|
||||
{/*<Category name={'SSL'} healthOk={true} />*/}
|
||||
</div>
|
||||
<div
|
||||
className={'bg-gray-lightest border-l w-fit border-figmaColors-divider'}
|
||||
|
|
@ -66,4 +67,7 @@ function HealthModal() {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export default HealthModal;
|
||||
|
|
|
|||
|
|
@ -1,187 +0,0 @@
|
|||
export const response = {
|
||||
databases: {
|
||||
postgres: {
|
||||
health: true,
|
||||
details: {
|
||||
version: 'v1.13',
|
||||
schema: 'v1.10',
|
||||
lastUpdatedOn: '12jan2023',
|
||||
},
|
||||
},
|
||||
clickhouse: {
|
||||
health: true,
|
||||
details: {
|
||||
version: 'v1.13',
|
||||
schema: 'v1.10',
|
||||
lastUpdatedOn: '12jan2023',
|
||||
},
|
||||
},
|
||||
},
|
||||
ingestionPipeline: {
|
||||
redis: {
|
||||
health: true,
|
||||
details: {
|
||||
version: 'v1.13',
|
||||
schema: 'v1.10',
|
||||
lastUpdatedOn: '12jan2023',
|
||||
},
|
||||
},
|
||||
kafka: {
|
||||
health: true,
|
||||
details: {
|
||||
version: 'v1.13',
|
||||
schema: 'v1.10',
|
||||
lastUpdatedOn: '12jan2023',
|
||||
},
|
||||
},
|
||||
},
|
||||
backendServices: {
|
||||
alerts: {
|
||||
health: true,
|
||||
details: {
|
||||
version: 'v1.13',
|
||||
schema: 'v1.10',
|
||||
lastUpdatedOn: '12jan2023',
|
||||
},
|
||||
},
|
||||
assets: {
|
||||
health: true,
|
||||
details: {
|
||||
version: 'v1.13',
|
||||
schema: 'v1.10',
|
||||
lastUpdatedOn: '12jan2023',
|
||||
},
|
||||
},
|
||||
assist: {
|
||||
health: true,
|
||||
details: {
|
||||
version: 'v1.13',
|
||||
schema: 'v1.10',
|
||||
lastUpdatedOn: '12jan2023',
|
||||
},
|
||||
},
|
||||
chalice: {
|
||||
health: true,
|
||||
details: {
|
||||
version: 'v1.13',
|
||||
schema: 'v1.10',
|
||||
lastUpdatedOn: '12jan2023',
|
||||
},
|
||||
},
|
||||
db: {
|
||||
health: true,
|
||||
details: {
|
||||
version: 'v1.13',
|
||||
schema: 'v1.10',
|
||||
lastUpdatedOn: '12jan2023',
|
||||
},
|
||||
},
|
||||
ender: {
|
||||
health: true,
|
||||
details: {
|
||||
version: 'v1.13',
|
||||
schema: 'v1.10',
|
||||
lastUpdatedOn: '12jan2023',
|
||||
},
|
||||
},
|
||||
frontend: {
|
||||
health: true,
|
||||
details: {
|
||||
version: 'v1.13',
|
||||
schema: 'v1.10',
|
||||
lastUpdatedOn: '12jan2023',
|
||||
},
|
||||
},
|
||||
heuristics: {
|
||||
health: true,
|
||||
details: {
|
||||
version: 'v1.13',
|
||||
schema: 'v1.10',
|
||||
lastUpdatedOn: '12jan2023',
|
||||
},
|
||||
},
|
||||
http: {
|
||||
health: true,
|
||||
details: {
|
||||
version: 'v1.13',
|
||||
schema: 'v1.10',
|
||||
lastUpdatedOn: '12jan2023',
|
||||
},
|
||||
},
|
||||
'ingress-nginx': {
|
||||
health: true,
|
||||
details: {
|
||||
version: 'v1.13',
|
||||
schema: 'v1.10',
|
||||
lastUpdatedOn: '12jan2023',
|
||||
},
|
||||
},
|
||||
integrations: {
|
||||
health: true,
|
||||
details: {
|
||||
version: 'v1.13',
|
||||
schema: 'v1.10',
|
||||
lastUpdatedOn: '12jan2023',
|
||||
},
|
||||
},
|
||||
peers: {
|
||||
health: true,
|
||||
details: {
|
||||
version: 'v1.13',
|
||||
schema: 'v1.10',
|
||||
lastUpdatedOn: '12jan2023',
|
||||
},
|
||||
},
|
||||
quickwit: {
|
||||
health: true,
|
||||
details: {
|
||||
version: 'v1.13',
|
||||
schema: 'v1.10',
|
||||
lastUpdatedOn: '12jan2023',
|
||||
},
|
||||
},
|
||||
sink: {
|
||||
health: true,
|
||||
details: {
|
||||
version: 'v1.13',
|
||||
schema: 'v1.10',
|
||||
lastUpdatedOn: '12jan2023',
|
||||
},
|
||||
},
|
||||
sourcemapreader: {
|
||||
health: true,
|
||||
details: {
|
||||
version: 'v1.13',
|
||||
schema: 'v1.10',
|
||||
lastUpdatedOn: '12jan2023',
|
||||
},
|
||||
},
|
||||
storage: {
|
||||
health: true,
|
||||
details: {
|
||||
version: 'v1.13',
|
||||
schema: 'v1.10',
|
||||
lastUpdatedOn: '12jan2023',
|
||||
},
|
||||
},
|
||||
utilities: {
|
||||
health: true,
|
||||
details: {
|
||||
version: 'v1.13',
|
||||
schema: 'v1.10',
|
||||
lastUpdatedOn: '12jan2023',
|
||||
},
|
||||
},
|
||||
},
|
||||
overall: {
|
||||
health: true,
|
||||
details: {
|
||||
numberOfEventCaptured: 123000,
|
||||
numberOfSessionsCaptured: 25678,
|
||||
},
|
||||
labels: {
|
||||
parent: 'information',
|
||||
},
|
||||
},
|
||||
ssl: true,
|
||||
};
|
||||
|
|
@ -1,46 +1,77 @@
|
|||
import React from 'react';
|
||||
import { Icon } from 'UI';
|
||||
import cn from 'classnames';
|
||||
import HealthModal from 'Components/Header/HealthStatus/HealthModal/HealthModal';
|
||||
import { response } from './HealthModal/mock'
|
||||
import HealthModal, { Category } from 'Components/Header/HealthStatus/HealthModal/HealthModal';
|
||||
import { healthService } from 'App/services';
|
||||
|
||||
function mapResponse(resp) {
|
||||
const dbKeys = Object.keys(resp.databases)
|
||||
const ingestKeys = Object.keys(resp.ingestionPipeline)
|
||||
const backendKeys = Object.keys(resp.backendServices)
|
||||
|
||||
if (!resp.overall.health) {
|
||||
const dbHealth: Record<string, any> = {
|
||||
overall: true,
|
||||
}
|
||||
const ingestHealth: Record<string, any> = {
|
||||
overall: true,
|
||||
}
|
||||
const backHealth: Record<string, any> = {
|
||||
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<string, any>) {
|
||||
const services = Object.keys(resp);
|
||||
const healthMap: Record<string, any> = {}
|
||||
services.forEach(service => {
|
||||
healthMap[service] = {
|
||||
// @ts-ignore
|
||||
name: categoryKeyNames[service],
|
||||
healthOk: true,
|
||||
subservices: resp[service],
|
||||
}
|
||||
Object.values(healthMap[service].subservices).forEach((subservice: Record<string, any>) => {
|
||||
if (!subservice?.health) healthMap[service].healthOk = false;
|
||||
})
|
||||
})
|
||||
|
||||
const icon = healthOk ? 'pulse' : ('exclamation-circle-fill' as const);
|
||||
const overallHealth = Object.values(healthMap).every((service: Record<string, any>) => 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 (
|
||||
<div className={'relative group h-full'}>
|
||||
<div
|
||||
|
|
@ -53,15 +84,48 @@ function HealthStatus() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<HealthMenu healthOk={healthOk} setHealth={setHealth} />
|
||||
<HealthModal />
|
||||
<HealthMenu
|
||||
healthResponse={healthResponse}
|
||||
getHealth={getHealth}
|
||||
isLoading={isLoading}
|
||||
lastAsked={lastAsked}
|
||||
setShowModal={setShowModal}
|
||||
/>
|
||||
{showModal ? (<HealthModal healthResponse={healthResponse} getHealth={getHealth} isLoading={isLoading} lastAsked={lastAsked} />) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HealthMenu({ healthOk, setHealth }: { healthOk: boolean; setHealth: any }) {
|
||||
function HealthMenu({
|
||||
healthResponse,
|
||||
getHealth,
|
||||
isLoading,
|
||||
lastAsked,
|
||||
setShowModal,
|
||||
}: {
|
||||
healthResponse: Record<string, any>;
|
||||
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<string, any>) => !service.healthOk
|
||||
) as Record<string, any>[];
|
||||
return (
|
||||
<div
|
||||
style={{ width: 220, top: '100%', right: '-30%', height: '110%' }}
|
||||
|
|
@ -84,39 +148,30 @@ function HealthMenu({ healthOk, setHealth }: { healthOk: boolean; setHealth: any
|
|||
<span>{title}</span>
|
||||
</div>
|
||||
<div className={'text-secondary flex w-full justify-between items-center text-sm'}>
|
||||
<span>Last checked 22 mins. ago </span>
|
||||
<div className={'cursor-pointer'} onClick={() => setHealth(!healthOk)}>
|
||||
<span>Last checked {lastAskedDiff} mins. ago </span>
|
||||
<div
|
||||
className={cn('cursor-pointer', isLoading ? 'animate-spin' : '')}
|
||||
onClick={() => getHealth()}
|
||||
>
|
||||
<Icon name={'arrow-repeat'} size={16} color={'main'} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={'divider w-full border border-b-light-gray'} />
|
||||
|
||||
<div className={'w-full'}>
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<div className="py-1 px-2 font-medium">Version</div>
|
||||
<div className="code-font text-black rounded text-base bg-active-blue px-2 py-1 whitespace-nowrap overflow-hidden text-clip">
|
||||
123 123
|
||||
</div>
|
||||
</div>
|
||||
{/*<div className="flex items-center justify-between mt-2">*/}
|
||||
{/* <div className="py-1 px-2 font-medium">Version</div>*/}
|
||||
{/* <div className="code-font text-black rounded text-base bg-active-blue px-2 py-1 whitespace-nowrap overflow-hidden text-clip">*/}
|
||||
{/* 123 123*/}
|
||||
{/* </div>*/}
|
||||
{/*</div>*/}
|
||||
|
||||
{healthOk ? (
|
||||
{!healthOk ? (
|
||||
<>
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<div className="py-1 px-2 font-medium">Sessions</div>
|
||||
<div className="code-font text-black rounded text-base bg-active-blue px-2 py-1 whitespace-nowrap overflow-hidden text-clip">
|
||||
10 000
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<div className="py-1 px-2 font-medium">Events</div>
|
||||
<div className="code-font text-black rounded text-base bg-active-blue px-2 py-1 whitespace-nowrap overflow-hidden text-clip">
|
||||
90 000
|
||||
</div>
|
||||
</div>
|
||||
<div className={'text-secondary pb-2'}>Observed installation Issue with the following</div>
|
||||
{problematicServices.map(service => <Category onClick={() => setShowModal(true)} healthOk={false} name={service.name} />)}
|
||||
</>
|
||||
) : (
|
||||
<div>Observed installation Issue with the following</div>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
10
frontend/app/services/HealthService.ts
Normal file
10
frontend/app/services/HealthService.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import BaseService from './BaseService';
|
||||
|
||||
export default class HealthService extends BaseService {
|
||||
fetchStatus(): Promise<any> {
|
||||
return this.client.get('/health')
|
||||
.then(r => r.json())
|
||||
.then(j => j.data || {})
|
||||
.catch(Promise.reject)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
]
|
||||
Loading…
Add table
Reference in a new issue