Skip to content

Real-time graphs for the background of metrics #402

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: unstable
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions backend/src/node/node.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export class NodeService {
disk_bytes_total,
used_memory,
total_memory,
sys_loadavg_5,
sys_loadavg_1,
app_uptime: bnUptime,
network_name,
nat_open,
Expand Down Expand Up @@ -94,12 +94,12 @@ export class NodeService {
? StatusColor.WARNING
: StatusColor.ERROR;

const cpuUtilization = sys_loadavg_5.toFixed(1);
const cpuUtilization = sys_loadavg_1.toFixed(1);

const cpuStatus =
sys_loadavg_5 <= 80
sys_loadavg_1 <= 80
? StatusColor.SUCCESS
: sys_loadavg_5 > 80 && sys_loadavg_5 < 90
: sys_loadavg_1 > 80 && sys_loadavg_1 < 90
? StatusColor.WARNING
: StatusColor.ERROR;

Expand Down
2 changes: 1 addition & 1 deletion backend/src/node/tests/node.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ describe('NodeController', () => {
disk_bytes_total: '132070244352',
used_memory: '16',
total_memory: '132070244352',
sys_loadavg_5: 5.95,
sys_loadavg_1: 5.95,
app_uptime: 600,
network_name: 'example',
nat_open: false,
Expand Down
156 changes: 126 additions & 30 deletions src/components/DiagnosticCard/DiagnosticCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import DarkNetwork from '../../assets/images/darkNetwork.svg'
import Network from '../../assets/images/network.svg'
import NotAvailable from '../../assets/images/notAvalilable.svg'
import { OptionalString, StatusColor } from '../../types'
import MetricLineChart from '../MetricLineChart/MetricLineChart'
import ProgressCircle from '../ProgressCircle/ProgressCircle'
import Status from '../Status/Status'
import Tooltip from '../ToolTip/Tooltip'
Expand All @@ -29,6 +30,11 @@ export interface DiagnosticCardProps {
toolTipText?: OptionalString
toolTipPosition?: PlacesType
isDisabled?: boolean
chartData?: number[]
chartColor?: string
chartLabel?: string
isChartPercentage?: boolean
iconType?: 'cpu' | 'ram' | 'disk' | 'critical' | 'error' | 'warning' | 'network' | 'beacon'
}

const DiagnosticCard: FC<DiagnosticCardProps> = ({
Expand All @@ -47,6 +53,11 @@ const DiagnosticCard: FC<DiagnosticCardProps> = ({
toolTipText,
toolTipPosition,
isDisabled,
chartData,
chartColor,
chartLabel,
isChartPercentage = true,
iconType,
}) => {
const [isReady, setReady] = useState(false)
const toolTipId = Math.random().toString()
Expand All @@ -72,53 +83,138 @@ const DiagnosticCard: FC<DiagnosticCardProps> = ({
setReady(true)
}, [])

const contentClass = addClassString('flex flex-col justify-between h-full', [
isDisabled && 'opacity-20',
])
const contentClass = addClassString('flex flex-col h-full', [isDisabled && 'opacity-20'])

// Icon component for different metric types
const renderIcon = () => {
if (!iconType || isSmall) return null

const iconClasses = 'w-4 h-4 flex-shrink-0 flex items-center justify-center'
const iconColor = chartColor || 'currentColor'

switch (iconType) {
case 'cpu':
return (
<div className={iconClasses} style={{ color: iconColor }}>
<i className='bi bi-cpu text-current leading-none' />
</div>
)
case 'ram':
return (
<div className={iconClasses} style={{ color: iconColor }}>
<i className='bi bi-memory text-current leading-none' />
</div>
)
case 'disk':
return (
<div className={iconClasses} style={{ color: iconColor }}>
<i className='bi bi-hdd text-current leading-none' />
</div>
)
case 'critical':
return (
<div className={iconClasses} style={{ color: iconColor }}>
<i className='bi bi-exclamation-triangle-fill text-current leading-none' />
</div>
)
case 'error':
return (
<div className={iconClasses} style={{ color: iconColor }}>
<i className='bi bi-x-circle-fill text-current leading-none' />
</div>
)
case 'warning':
return (
<div className={iconClasses} style={{ color: iconColor }}>
<i className='bi bi-exclamation-circle-fill text-current leading-none' />
</div>
)
case 'network':
return (
<div className={iconClasses} style={{ color: iconColor }}>
<i className='bi bi-wifi text-current leading-none' />
</div>
)
case 'beacon':
return (
<div className={iconClasses} style={{ color: iconColor }}>
<i className='bi bi-broadcast text-current leading-none' />
</div>
)
default:
return null
}
}
const renderContent = () => (
<div className={contentClass}>
{!metric ? (
{!metric && (
<NotAvailable className='absolute opacity-60 w-20 text-dark100 dark:hidden right-0 top-1/2 transform -translate-y-1/2' />
) : (
size !== 'sm' &&
isBackground && (
<div className='w-full max-h-full absolute left-0 top-1/2 transform -translate-y-1/2 overflow-hidden'>
<Network className='w-full dark:hidden' />
<DarkNetwork className='w-full hidden dark:block' />
</div>
)
)}
<div className='w-full z-10 space-x-8 flex justify-between'>
<Typography
type={isSmall ? 'text-tiny' : 'text-caption1'}
className={!isSmall ? 'xl:text-body' : ''}
>
{title}
</Typography>

{/* Header with icon, title and metric */}
<div className='w-full z-10 flex items-center justify-between flex-shrink-0 mb-2'>
<div className='flex items-center gap-2'>
{renderIcon()}
<Typography
type={isSmall ? 'text-tiny' : 'text-caption1'}
className={`${!isSmall ? 'xl:text-body' : ''} font-medium text-dark900 dark:text-white uppercase tracking-wide`}
>
{title}
</Typography>
</div>
{metric && (
<Typography
type={isSmall ? 'text-tiny' : metricTextSize ? metricTextSize : 'text-caption1'}
className={!isSmall && !metricTextSize ? 'xl:text-subtitle2' : ''}
className={`${!isSmall && !metricTextSize ? 'xl:text-body' : ''} font-normal text-dark600 dark:text-dark400`}
>
{metric}
</Typography>
)}
</div>
<div className='w-full capitalize z-10 space-x-8 flex items-center justify-between'>

{/* Utilization percentage and status */}
<div className='w-full z-10 flex items-center justify-between flex-shrink-0 mb-2'>
<Typography
type={isSmall ? 'text-tiny' : 'text-caption1'}
color={subTitleHighlightColor ? 'text-dark900' : undefined}
darkMode={subTitleHighlightColor ? 'dark:text-dark900' : undefined}
className={subTitleHighlightColor ? `${subTitleHighlightColor} px-1` : undefined}
type={isSmall ? 'text-tiny' : 'text-caption2'}
className={`${
subTitleHighlightColor
? `${subTitleHighlightColor} px-1.5 py-0.5 rounded text-xs font-medium`
: 'text-dark500 dark:text-dark300 font-normal'
} ${!subTitleHighlightColor ? '' : 'uppercase tracking-wide'}`}
>
{subTitle}
</Typography>
{percent ? (
<ProgressCircle size='sm' id={generateId(12)} percent={percent} />
) : (
status && <Status status={status} />
)}
<div className='flex items-center gap-1'>
{percent ? (
<ProgressCircle size='sm' id={generateId(12)} percent={percent} />
) : (
status && <Status status={status} />
)}
</div>
</div>

{/* Chart fills remaining space at bottom */}
{metric && size !== 'sm' && isBackground && chartData && chartColor && chartLabel && (
<div className='w-full flex-1 min-h-0 mt-2 relative overflow-hidden rounded-md'>
<div className='absolute inset-0 bg-gradient-to-t from-transparent via-transparent to-transparent opacity-50'></div>
<MetricLineChart
data={chartData}
color={chartColor}
label={chartLabel}
animate={false}
isPercentage={isChartPercentage}
showYAxis={true}
/>
</div>
)}

{/* Fallback background for non-chart cards */}
{metric && size !== 'sm' && isBackground && (!chartData || !chartColor || !chartLabel) && (
<div className='w-full max-h-full absolute left-0 top-1/2 transform -translate-y-1/2 overflow-hidden opacity-30'>
<Network className='w-full dark:hidden' />
<DarkNetwork className='w-full hidden dark:block' />
</div>
)}
</div>
)

Expand Down
45 changes: 30 additions & 15 deletions src/components/DiagnosticTable/HardwareInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import addSuffixString from '../../../utilities/addSuffixString'
import secondsToShortHand from '../../../utilities/secondsToShortHand'
import { DiagnosticType } from '../../constants/enums'
import useMediaQuery from '../../hooks/useMediaQuery'
import useMetricHistory from '../../hooks/useMetricHistory'
import { StatusColor } from '../../types'
import { SyncData } from '../../types/beacon'
import { Diagnostics } from '../../types/diagnostic'
Expand Down Expand Up @@ -36,6 +37,16 @@ const HardwareInfo: FC<HardwareInfoProps> = ({ syncData, beanHealth }) => {
natOpen,
} = beanHealth

// Get historical data for charts
const { cpuHistory, ramHistory, diskHistory } = useMetricHistory(
cpuUtilization,
memoryUtilization,
diskUtilization,
0,
0,
0,
)

const diskData = isSyncing ? diskStatus.syncing : diskStatus.synced
const remainingBeaconTime = secondsToShortHand(Number(beaconSyncTime) || 0)

Expand Down Expand Up @@ -64,6 +75,10 @@ const HardwareInfo: FC<HardwareInfoProps> = ({ syncData, beanHealth }) => {
metric={addSuffixString(Math.round(totalDiskSpace), 'GB')}
subTitle={t('utilization', { percent: diskUtilization })}
status={diskData}
chartData={diskHistory}
chartColor='#5E41D5'
chartLabel='Disk Usage'
iconType='disk'
/>
<DiagnosticCard
title={t('cpu')}
Expand All @@ -73,6 +88,10 @@ const HardwareInfo: FC<HardwareInfoProps> = ({ syncData, beanHealth }) => {
metric={frequency ? addSuffixString(frequency, 'GHz') : ' '}
subTitle={t('utilization', { percent: cpuUtilization })}
status={cpuStatus}
chartData={cpuHistory}
chartColor='#7C5FEB'
chartLabel='CPU Usage'
iconType='cpu'
/>
<DiagnosticCard
title={t('ram')}
Expand All @@ -82,6 +101,10 @@ const HardwareInfo: FC<HardwareInfoProps> = ({ syncData, beanHealth }) => {
metric={addSuffixString(Math.round(totalMemory), 'GB')}
subTitle={t('utilization', { percent: memoryUtilization })}
status={ramStatus}
chartData={ramHistory}
chartColor='#A841D5'
chartLabel='RAM Usage'
iconType='ram'
/>
</>
)
Expand Down Expand Up @@ -133,30 +156,22 @@ const HardwareInfo: FC<HardwareInfoProps> = ({ syncData, beanHealth }) => {

return (
<div className='h-full w-full flex flex-col xl:min-w-316'>
<div className='w-full h-12 border flex border-style500'>
<div
onClick={viewDeviceInfo}
className='flex-1 p-2 flex items-center justify-center cursor-pointer'
>
<div className='w-full h-12 flex items-center justify-between px-4 border-style500'>
<div onClick={viewDeviceInfo} className='cursor-pointer'>
<Typography
type='text-caption2'
className='xl:text-caption1'
type='text-caption1'
color={isDeviceView ? 'text-primary' : 'text-dark500'}
darkMode={isDeviceView ? 'dark:text-white' : undefined}
isBold={isDeviceView}
>
{t('hardwareInfo.usage')}
</Typography>
</div>
<div
onClick={viewNetworkInfo}
className='flex-1 p-2 flex items-center justify-center cursor-pointer'
>
<div onClick={viewNetworkInfo} className='cursor-pointer'>
<Typography
isBold={isNetworkView}
type='text-caption2'
className='xl:text-caption1'
color={isNetworkView ? 'text-primary' : 'text-dark500'}
type='text-tiny'
className='uppercase @1600:text-caption1'
color={isNetworkView ? 'text-primary' : 'text-dark400'}
darkMode={isNetworkView ? 'dark:text-white' : undefined}
>
{t('hardwareInfo.diagnostics')}
Expand Down
28 changes: 26 additions & 2 deletions src/components/LogStats/LogStats.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import toFixedIfNecessary from '../../../utilities/toFixedIfNecessary'
import useMetricHistory from '../../hooks/useMetricHistory'
import { Metric, StatusColor } from '../../types'
import DiagnosticCard, { CardSize } from '../DiagnosticCard/DiagnosticCard'

Expand All @@ -26,6 +27,16 @@ const LogStats: FC<LogStatsProps> = ({
const { t } = useTranslation()
const { errorCount, criticalCount, warningCount } = logMetrics

// Get historical data for log charts - we don't need hardware metrics so pass zeros
const { criticalLogsHistory, errorLogsHistory, warningLogsHistory } = useMetricHistory(
'0',
0,
0,
criticalCount,
errorCount,
warningCount,
)

const critStatus = criticalCount > 0 ? StatusColor.ERROR : StatusColor.SUCCESS
const errorStatus =
errorCount <= 0
Expand All @@ -52,9 +63,13 @@ const LogStats: FC<LogStatsProps> = ({
border='border-t-0 md:border-l-0 border-style500'
subTitle={t('critical')}
metric={`${toFixedIfNecessary(criticalCount, 2)} / HR`}
chartData={criticalLogsHistory}
chartColor='#D541B8'
chartLabel='Critical Logs'
isChartPercentage={false}
iconType='critical'
/>
<DiagnosticCard
isBackground={false}
title={t('errors')}
toolTipText={errorToolTip}
maxHeight={maxHeight}
Expand All @@ -64,9 +79,13 @@ const LogStats: FC<LogStatsProps> = ({
border='border-t-0 md:border-l-0 border-style500'
subTitle={t('logInfo.validatorLogs')}
metric={`${toFixedIfNecessary(errorCount, 2)} / HR`}
chartData={errorLogsHistory}
chartColor='#836FFF'
chartLabel='Error Logs'
isChartPercentage={false}
iconType='error'
/>
<DiagnosticCard
isBackground={false}
title={t('logInfo.warnings')}
toolTipText={warnToolTip}
maxHeight={maxHeight}
Expand All @@ -76,6 +95,11 @@ const LogStats: FC<LogStatsProps> = ({
border='border-t-0 md:border-l-0 border-style500'
subTitle={t('logInfo.validatorLogs')}
metric={`${toFixedIfNecessary(warningCount, 2)} / HR`}
chartData={warningLogsHistory}
chartColor='#5200FF'
chartLabel='Warning Logs'
isChartPercentage={false}
iconType='warning'
/>
</>
)
Expand Down
Loading