change(ui): remove keys warning, create service health type, keep response in localstorage for 15 mins

This commit is contained in:
nick-delirium 2023-03-14 12:17:50 +01:00 committed by Delirium
parent 620489e57d
commit 12d7ff4f99
6 changed files with 346 additions and 163 deletions

View file

@ -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 (
<div
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}
function HealthModal({
getHealth,
isLoading,
healthResponse,
setShowModal,
}: {
getHealth: () => void;
isLoading: boolean;
healthResponse: { overallHealth: boolean; healthMap: Record<string, IServiceStats> };
setShowModal: (isOpen: boolean) => void;
}) {
const [selectedService, setSelectedService] = React.useState('');
<Icon name={"chevron-right"} size={16} className={"ml-auto"} />
</div>
)
}
function HealthModal({ getHealth, isLoading, healthResponse }: { getHealth: () => void; isLoading: boolean; healthResponse: Record<string, any> }) {
React.useEffect(() => {
if (!healthResponse.overallHealth) {
setSelectedService(
Object.keys(healthResponse.healthMap).filter(
(s) => !healthResponse.healthMap[s].healthOk
)[0]
);
}
}, [healthResponse]);
const handleClose = () => {
setShowModal(false);
};
return (
<div
style={{
width: 640,
width: '100vw',
height: '100vh',
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
background: 'rgba(0, 0, 0, 0.5)',
top: 0,
left: 0,
zIndex: getHighest(),
}}
className={'flex flex-col bg-white rounded border border-figmaColors-divider'}
onClick={handleClose}
>
<div
className={
'flex w-full justify-between items-center p-4 border-b border-figmaColors-divider'
}
style={{
width: 640,
position: 'absolute',
top: '50%',
left: '50%',
height: '600px',
transform: 'translate(-50%, -50%)',
}}
onClick={(e) => e.stopPropagation()}
className={'flex flex-col bg-white rounded border border-figmaColors-divider'}
>
<div className={'text-xl font-semibold'}>Installation Status</div>
<Button loading={isLoading} onClick={getHealth} icon={'arrow-repeat'} variant={'text-primary'}>
Recheck
</Button>
</div>
<div className={'flex w-full'}>
<div className={'flex flex-col h-full'} style={{ flex: 1 }}>
<Category name={'Databases'} healthOk={true} />
<Category name={'Ingestion Pipeline'} healthOk={false} />
<Category name={'Backend Services'} healthOk={false} />
{/*<Category name={'SSL'} healthOk={true} />*/}
</div>
<div
className={'bg-gray-lightest border-l w-fit border-figmaColors-divider'}
style={{ flex: 2 }}
className={
'flex w-full justify-between items-center p-4 border-b border-figmaColors-divider'
}
>
<img src={slide} width={392} />
<div className={'text-xl font-semibold'}>Installation Status</div>
<Button
loading={isLoading}
onClick={getHealth}
icon={'arrow-repeat'}
variant={'text-primary'}
>
Recheck
</Button>
</div>
<div className={'flex w-full'}>
<div className={'flex flex-col h-full'} style={{ flex: 1 }}>
{Object.keys(healthResponse.healthMap).map((service) => (
<React.Fragment key={service}>
<Category
onClick={() => setSelectedService(service)}
healthOk={healthResponse.healthMap[service].healthOk}
name={healthResponse.healthMap[service].name}
isSelectable
isSelected={selectedService === service}
/>
</React.Fragment>
))}
</div>
<div
className={
'bg-gray-lightest border-l w-fit border-figmaColors-divider overflow-y-scroll'
}
style={{ flex: 2, height: 420 }}
>
{selectedService ? (
<ServiceStatus service={healthResponse.healthMap[selectedService]} />
) : (
<img src={slide} width={392} />
)}
</div>
</div>
<div className={'p-4 mt-auto w-full border-t border-figmaColors-divider'}>
<Button variant={'primary'} className={'ml-auto'}>
Create Account
</Button>
</div>
<Footer />
</div>
<div className={'p-4 w-full border-t border-figmaColors-divider'}>
<Button variant={'primary'} className={'ml-auto'}>
Create Account
</Button>
</div>
<Footer />
</div>
);
}
function ServiceStatus({ service }: { service: Record<string, any> }) {
const { subservices } = service;
return (
<div className={'p-2'}>
<div className={'border border-light-gray'}>
{Object.keys(subservices).map((subservice: string) => (
<React.Fragment key={subservice}>
<SubserviceHealth name={subservice} subservice={subservices[subservice]} />
</React.Fragment>
))}
</div>
</div>
);
}
export default HealthModal;

View file

@ -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<string, any>) {
const services = Object.keys(resp);
const healthMap: Record<string, any> = {}
services.forEach(service => {
const healthMap: Record<string, IServiceStats> = {};
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<string, any>) => {
if (!subservice?.health) healthMap[service].healthOk = false;
})
})
});
});
const overallHealth = Object.values(healthMap).every((service: Record<string, any>) => service.healthOk);
const overallHealth = Object.values(healthMap).every(
(service: Record<string, any>) => 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 (
<div className={'relative group h-full'}>
<div
className={
'rounded cursor-pointer p-2 flex items-center hover:bg-figmaColors-secondary-outlined-hover-background'
}
>
<div className={'rounded p-2 border border-light-gray bg-white flex items-center '}>
<Icon name={icon} size={18} />
</div>
</div>
<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({
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%' }}
className={'absolute group invisible group-hover:visible pt-4'}
>
<div
className={
'w-full flex flex-col border border-light-gray gap-2 rounded items-center p-4 bg-white'
}
>
<>
<div className={'relative group h-full'}>
<div
className={cn(
'p-2 gap-2 w-full font-semibold flex items-center rounded',
healthOk
? 'color-green bg-figmaColors-secondary-outlined-hover-background'
: 'bg-red-lightest'
)}
className={
'rounded cursor-pointer p-2 flex items-center hover:bg-figmaColors-secondary-outlined-hover-background'
}
>
<Icon name={icon} size={16} color={'green'} />
<span>{title}</span>
</div>
<div className={'text-secondary flex w-full justify-between items-center text-sm'}>
<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 className={'rounded p-2 border border-light-gray bg-white flex items-center '}>
<Icon name={icon} size={18} />
</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>*/}
{!healthOk ? (
<>
<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} />)}
</>
) : null}
</div>
<HealthWidget
healthResponse={healthResponse}
getHealth={getHealth}
isLoading={isLoading}
lastAsked={lastAsked}
setShowModal={setShowModal}
/>
</div>
</div>
{showModal ? (
<HealthModal setShowModal={setShowModal} healthResponse={healthResponse} getHealth={getHealth} isLoading={isLoading} />
) : null}
</>
);
}
export default HealthStatus;

View file

@ -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<string, IServiceStats> };
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
)
return (
<div
style={{ width: 220, top: '100%', right: '-30%', height: '110%' }}
className={'absolute group invisible group-hover:visible pt-4'}
>
<div
className={
'w-full flex flex-col border border-light-gray gap-2 rounded items-center p-4 bg-white'
}
>
<div
className={cn(
'p-2 gap-2 w-full font-semibold flex items-center rounded',
healthOk
? 'color-green bg-figmaColors-secondary-outlined-hover-background'
: 'bg-red-lightest'
)}
>
<Icon name={icon} size={16} color={'green'} />
<span>{title}</span>
</div>
<div className={'text-secondary flex w-full justify-between items-center text-sm'}>
<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'}>
{!healthOk ? (
<>
<div className={'text-secondary pb-2'}>
Observed installation Issue with the following
</div>
{problematicServices.map((service) => (
<React.Fragment key={service.serviceName}>
<ServiceCategory
onClick={() => setShowModal(true)}
healthOk={false}
name={service.name}
/>
</React.Fragment>
))}
</>
) : null}
</div>
</div>
</div>
);
}
export default HealthWidget

View file

@ -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 (
<div
className={cn(
'px-4 py-2 flex items-center gap-2 border-b cursor-pointer',
isExpandable || isSelectable ? 'hover:bg-active-blue' : '',
isSelected ? 'bg-active-blue' : ''
)}
onClick={onClick}
>
<Icon name={icon} size={20} color={'green'} />
{name}
{isSelectable ? <Icon name={'chevron-right'} size={16} className={'ml-auto'} /> : null}
{isExpandable ? (
<Icon name={isExpanded ? 'chevron-up' : 'chevron-down'} size={16} className={'ml-auto'} />
) : null}
</div>
);
}
export default Category

View file

@ -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 (
<div className={cn(isExpanded && isExpandable ? 'bg-active-blue' : 'bg-white')}>
<Category
onClick={() => (isExpandable ? setIsExpanded(!isExpanded) : null)}
name={name}
healthOk={subservice?.health}
isExpandable={isExpandable}
isExpanded={isExpanded}
/>
{isExpanded ? (
<div className={'p-3'}>
{subservice?.details?.version ? (
<div className="flex items-center justify-between mt-2 px-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">
{subservice?.details?.version}
</div>
</div>
) : null}
{subservice?.details?.errors?.length ? (
<div className={'py-2 px-4 bg-white rounded-xl border border-light-gray'}>
<div>Error log:</div>
{subservice.details.errors.map((err: string, i) => (
<div className={'mt-2'} key={i + 1}>
{i + 1}. {err}
</div>
))}
</div>
) : subservice?.health ? null : (
'Service not responding'
)}
</div>
) : null}
</div>
);
}
export default SubserviceHealth;

View file

@ -0,0 +1,6 @@
export const categoryKeyNames = {
backendServices: 'Backend Services',
databases: 'Databases',
ingestionPipeline: 'Ingestion Pipeline',
ssl: 'SSL',
} as const