Skip to content

Commit

Permalink
Feat: Alert Management (#186)
Browse files Browse the repository at this point in the history
  • Loading branch information
rickimoore committed Jul 14, 2023
1 parent c0f05f5 commit e808e6b
Show file tree
Hide file tree
Showing 21 changed files with 419 additions and 147 deletions.
11 changes: 8 additions & 3 deletions src/components/AlertCard/AlertCard.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
import StatusBar, { StatusBarProps } from '../StatusBar/StatusBar'
import Typography from '../Typography/Typography'
import { FC } from 'react'

export interface AlertCardProps extends StatusBarProps {
text: string
subText: string
onClick?: () => void
}

const AlertCard: FC<AlertCardProps> = ({ text, subText, ...props }) => {
const AlertCard: FC<AlertCardProps> = ({ text, subText, onClick, ...props }) => {
return (
<div className='w-full h-14 border-t-0 md:border-l-0 border-style500 flex justify-between items-center space-x-2 p-2'>
<div className='w-full h-14 group border-b-style500 flex justify-between items-center space-x-2 p-2'>
<StatusBar {...props} />
<div className='w-full max-w-tiny'>
<Typography type='text-caption2'>{text}</Typography>
<Typography type='text-caption2' isUpperCase>
{subText}
</Typography>
</div>
<i className='bi-info-circle-fill text-dark200 dark:text-dark300' />
<i
onClick={onClick}
className='bi-trash-fill cursor-pointer opacity-0 group-hover:opacity-100 text-dark200 dark:text-dark300'
/>
</div>
)
}
Expand Down
53 changes: 53 additions & 0 deletions src/components/AlertFilterSettings/AlertFilterSettings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import DropDown from '../DropDown/DropDown'
import { FC, useState } from 'react'
import useClickOutside from '../../hooks/useClickOutside'
import { StatusColor } from '../../types'
import { useTranslation } from 'react-i18next'

export type FilterValue = StatusColor | 'all'

export interface AlertFilterSettingsProps {
onChange: (value: FilterValue) => void
value: FilterValue
}

const AlertFilterSettings: FC<AlertFilterSettingsProps> = ({ onChange, value }) => {
const { t } = useTranslation()
const [isOpen, toggle] = useState(false)

const openDrop = () => toggle(true)
const closeDrop = () => toggle(false)

const { ref } = useClickOutside<HTMLDivElement>(closeDrop)

const selectOption = (value: FilterValue) => {
onChange(value)
closeDrop()
}
const filterOptions = [
{ text: t('alertMessages.all'), value: 'all' },
{ text: t('alertMessages.severe'), value: StatusColor.ERROR },
{ text: t('alertMessages.warning'), value: StatusColor.WARNING },
{ text: t('alertMessages.fair'), value: StatusColor.SUCCESS },
]

return (
<div ref={ref} className='relative'>
<i onClick={openDrop} className='bi-sliders2 text-dark400 cursor-pointer' />
<DropDown width='w-42' position='right-0' isOpen={isOpen}>
{filterOptions.map((option, index) => (
<li
key={index}
className='block cursor-pointer flex justify-between py-2 px-4 hover:bg-gray-100 dark:hover:bg-dark750 dark:hover:text-white'
onClick={() => selectOption(option.value as FilterValue)}
>
{option.text}
{option.value === value && <i className='bi-check-circle' />}
</li>
))}
</DropDown>
</div>
)
}

export default AlertFilterSettings
88 changes: 64 additions & 24 deletions src/components/DiagnosticTable/AlertInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,79 @@ import Typography from '../Typography/Typography'
import AlertCard from '../AlertCard/AlertCard'
import { useTranslation } from 'react-i18next'
import useDiagnosticAlerts from '../../hooks/useDiagnosticAlerts'
import useDivDimensions from '../../hooks/useDivDimensions'
import { useEffect, useMemo, useState } from 'react'
import sortAlertMessagesBySeverity from '../../utilities/sortAlerts'
import { StatusColor } from '../../types'
import AlertFilterSettings, { FilterValue } from '../AlertFilterSettings/AlertFilterSettings'

const AlertInfo = () => {
const { t } = useTranslation()
const { natAlert, peerCountAlert } = useDiagnosticAlerts()
const { alerts, dismissAlert, resetDismissed } = useDiagnosticAlerts()
const { ref, dimensions } = useDivDimensions()
const [filter, setFilter] = useState('all')

const setFilterValue = (value: FilterValue) => setFilter(value)

const formattedAlerts = useMemo(() => {
let baseAlerts = alerts

if (filter !== 'all') {
baseAlerts = baseAlerts.filter(({ severity }) => severity === filter)
}

return sortAlertMessagesBySeverity(baseAlerts)
}, [alerts, filter])

const isFiller = formattedAlerts.length < 6

useEffect(() => {
const intervalId = setInterval(() => {
resetDismissed()
}, 60000)

return () => clearInterval(intervalId)
}, [])

return (
<div className='h-full w-full flex flex-col'>
<div className='w-full h-12 flex items-center justify-between px-4 md:border-l-0 border-style500'>
<div ref={ref} className='h-full w-full flex flex-col border-l-0 border-t-0 border-style500'>
<div className='w-full h-12 flex items-center justify-between px-4 md:border-l-0 border-r-0 border-style500'>
<Typography type='text-caption1' color='text-primary' darkMode='dark:text-white' isBold>
{t('alertInfo.alerts')}
</Typography>
<Typography type='text-tiny' className='uppercase' color='text-dark400'>
{t('viewAll')}
</Typography>
<AlertFilterSettings value={filter as FilterValue} onChange={setFilterValue} />
</div>
{natAlert && (
<AlertCard
status={natAlert.severity}
count={3}
subText={natAlert.subText}
text={natAlert.message}
/>
{dimensions && (
<div
style={{ maxHeight: `${dimensions.height - 48}px` }}
className='h-full w-full flex flex-col'
>
{formattedAlerts.length > 0 && (
<div className={`overflow-scroll scrollbar-hide ${!isFiller ? 'flex-1' : ''}`}>
{formattedAlerts.map((alert) => {
const { severity, subText, message, id } = alert
const count =
severity === StatusColor.SUCCESS ? 1 : severity === StatusColor.WARNING ? 2 : 3
return (
<AlertCard
key={id}
status={severity}
count={count}
onClick={() => dismissAlert(alert)}
subText={subText}
text={message}
/>
)
})}
</div>
)}
{isFiller && (
<div className='flex-1 flex items-center justify-center'>
<i className='bi bi-lightning-fill text-primary text-h3 opacity-20' />
</div>
)}
</div>
)}
{peerCountAlert && (
<AlertCard
status={peerCountAlert.severity}
count={2}
subText={peerCountAlert.subText}
text={peerCountAlert.message}
/>
)}
<div className='flex-1 md:border-l-0 border-t-0 border-style500 flex items-center justify-center'>
<i className='bi bi-lightning-fill text-primary text-h3 opacity-20' />
</div>
</div>
)
}
Expand Down
6 changes: 1 addition & 5 deletions src/components/DropDown/DropDown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ const DropDown: FC<DropDownProps> = ({
className = '',
position = 'top-full left-0 z-50',
}) => {
const isChildrenArray = Array.isArray(children)

return (
<div
id='dropdown'
Expand All @@ -25,9 +23,7 @@ const DropDown: FC<DropDownProps> = ({
} ${className} ${position} animate-fadeSlideIn bg-white rounded divide-y divide-gray-100 shadow dark:bg-black`}
>
<ul
className={`text-sm max-h-32 overflow-scroll relative text-gray-700 dark:text-gray-200 ${
isChildrenArray && children.length >= 4 ? 'pb-6' : ''
}`}
className='text-sm max-h-32 overflow-scroll relative text-gray-700 dark:text-gray-200'
aria-labelledby='dropdownDefault'
>
{children}
Expand Down
27 changes: 27 additions & 0 deletions src/components/NetworkPeerSpeedometer/NetworkPeerSpeedometer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,38 @@ import { uiMode, validatorPeerCount } from '../../recoil/atoms'
import Typography from '../Typography/Typography'
import { useTranslation } from 'react-i18next'
import Tooltip from '../ToolTip/Tooltip'
import { StatusColor } from '../../types'
import useDiagnosticAlerts from '../../hooks/useDiagnosticAlerts'
import { useEffect } from 'react'
import { ALERT_ID } from '../../constants/constants'

const NetworkPeerSpeedometer = () => {
const { t } = useTranslation()
const mode = useRecoilValue(uiMode)
const peers = useRecoilValue(validatorPeerCount)
const { updateAlert } = useDiagnosticAlerts()

useEffect(() => {
if (!peers) return

if (peers <= 50) {
if (peers <= 20) {
updateAlert({
message: t('alert.peerCountLow', { type: t('alert.type.nodeValidator') }),
subText: t('poor'),
severity: StatusColor.ERROR,
id: ALERT_ID.PEER_COUNT,
})
return
}
updateAlert({
message: t('alert.peerCountMedium', { type: t('alert.type.nodeValidator') }),
subText: t('fair'),
severity: StatusColor.WARNING,
id: ALERT_ID.PEER_COUNT,
})
}
}, [peers])

return (
<Tooltip
Expand Down
22 changes: 21 additions & 1 deletion src/components/TopBar/BeaconMetric.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,31 @@ import SyncMetric from '../SyncMetric/SyncMetric'
import { useTranslation } from 'react-i18next'
import { useRecoilValue } from 'recoil'
import { selectBeaconSyncInfo } from '../../recoil/selectors/selectBeaconSyncInfo'
import { useEffect } from 'react'
import useDiagnosticAlerts from '../../hooks/useDiagnosticAlerts'
import { StatusColor } from '../../types'
import { ALERT_ID } from '../../constants/constants'

const BeaconMetric = () => {
const { t } = useTranslation()
const { headSlot, slotDistance, isSyncing, beaconPercentage } =
const { headSlot, slotDistance, isSyncing, beaconPercentage, beaconSyncTime } =
useRecoilValue(selectBeaconSyncInfo)
const { storeAlert, removeAlert } = useDiagnosticAlerts()

useEffect(() => {
if (beaconSyncTime <= 0) {
removeAlert(ALERT_ID.BEACON_SYNC)
}

if (beaconSyncTime > 0) {
storeAlert({
id: ALERT_ID.BEACON_SYNC,
severity: StatusColor.WARNING,
subText: t('fair'),
message: t('alertMessages.beaconNotSync'),
})
}
}, [beaconSyncTime])

return (
<SyncMetric
Expand Down
18 changes: 18 additions & 0 deletions src/components/TopBar/ValidatorMetric.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,30 @@ import SyncMetric from '../SyncMetric/SyncMetric'
import { useRecoilValue } from 'recoil'
import { useTranslation } from 'react-i18next'
import { selectValidatorSyncInfo } from '../../recoil/selectors/selectValidatorSyncInfo'
import { useEffect } from 'react'
import { StatusColor } from '../../types'
import useDiagnosticAlerts from '../../hooks/useDiagnosticAlerts'
import { ALERT_ID } from '../../constants/constants'

const ValidatorMetric = () => {
const { t } = useTranslation()
const syncInfo = useRecoilValue(selectValidatorSyncInfo)
const { storeAlert, removeAlert } = useDiagnosticAlerts()
const { headSlot, cachedHeadSlot, syncPercentage, isReady } = syncInfo

useEffect(() => {
if (isReady) {
removeAlert(ALERT_ID.VALIDATOR_SYNC)
}

storeAlert({
id: ALERT_ID.VALIDATOR_SYNC,
severity: StatusColor.WARNING,
subText: t('fair'),
message: t('alertMessages.ethClientNotSync'),
})
}, [isReady])

return (
<SyncMetric
id='ethMain'
Expand Down
8 changes: 8 additions & 0 deletions src/constants/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,11 @@ export const LogTypeOptions = [
{ title: 'Validator', value: LogType.VALIDATOR },
{ title: 'Beacon', value: LogType.BEACON },
]

export const ALERT_ID = {
VALIDATOR_SYNC: 'VALIDATOR_SYNC',
BEACON_SYNC: 'BEACON_SYNC',
NAT: 'NAT',
WARNING_LOG: 'WARNING_LOG',
PEER_COUNT: 'PEER_COUNT',
}
12 changes: 11 additions & 1 deletion src/hooks/__tests__/useDeviceDiagnostics.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,19 @@ import { mockedRecoilValue } from '../../../test.helpers'
import { mockBeaconSyncResult } from '../../mocks/beaconSyncResults'
import clearAllMocks = jest.clearAllMocks
import { StatusColor } from '../../types'

jest.mock('../../utilities/formatGigBytes', () => jest.fn())

jest.mock('../useDiagnosticAlerts', () => ({
__esModule: true,
default: jest.fn(() => ({
alerts: [],
storeAlert: jest.fn(),
updateAlert: jest.fn(),
dismissAlert: jest.fn(),
removeAlert: jest.fn(),
})),
}))

const mockedFormatGigBytes = formatGigBytes as jest.MockedFn<typeof formatGigBytes>

describe('useDeviceDiagnostics', () => {
Expand Down
Loading

0 comments on commit e808e6b

Please sign in to comment.