diff --git a/app/api/log-metrics/[[...type]]/route.ts b/app/api/log-metrics/[[...type]]/route.ts new file mode 100644 index 00000000..30291883 --- /dev/null +++ b/app/api/log-metrics/[[...type]]/route.ts @@ -0,0 +1,15 @@ +import { NextResponse } from 'next/server' +import getReqAuthToken from '../../../../utilities/getReqAuthToken'; +import { fetchLogMetrics } from '../../logs'; + +export async function GET(req: Request, context: any) { + try { + const { type } = context.params; + + const token = getReqAuthToken(req) + const data = await fetchLogMetrics(token, type) + return NextResponse.json(data) + } catch (error) { + return NextResponse.json({ error: 'Failed to fetch log metrics' }, { status: 500 }) + } +} diff --git a/app/api/logs.ts b/app/api/logs.ts index 8d27d496..f8507aed 100644 --- a/app/api/logs.ts +++ b/app/api/logs.ts @@ -1,5 +1,7 @@ +import { LogType } from '../../src/types'; import fetchFromApi from '../../utilities/fetchFromApi'; const backendUrl = process.env.BACKEND_URL -export const fetchLogMetrics = async (token: string) => fetchFromApi(`${backendUrl}/logs/metrics`, token) -export const dismissLogAlert = async (token: string, index: string) => fetchFromApi(`${backendUrl}/logs/dismiss/${index}`, token) \ No newline at end of file +export const fetchLogMetrics = async (token: string, type?: LogType) => fetchFromApi(`${backendUrl}/logs/metrics${type ? `/${type}` : ''}`, token) +export const dismissLogAlert = async (token: string, index: string) => fetchFromApi(`${backendUrl}/logs/dismiss/${index}`, token) +export const fetchPriorityLogs = async (token: string, page?: string) => fetchFromApi(`${backendUrl}/logs/priority${page ? `/${page}` : ''}`, token) \ No newline at end of file diff --git a/app/api/priority-logs/[[...page]]/route.ts b/app/api/priority-logs/[[...page]]/route.ts new file mode 100644 index 00000000..9c1ce825 --- /dev/null +++ b/app/api/priority-logs/[[...page]]/route.ts @@ -0,0 +1,14 @@ +import { NextResponse } from 'next/server' +import getReqAuthToken from '../../../../utilities/getReqAuthToken'; +import { fetchPriorityLogs } from '../../logs'; + +export async function GET(req: Request, context: any) { + try { + const { page } = context.params; + const token = getReqAuthToken(req) + const data = await fetchPriorityLogs(token, page) + return NextResponse.json(data) + } catch (error) { + return NextResponse.json({ error: 'Failed to fetch priority logs' }, { status: 500 }) + } +} diff --git a/app/api/priority-logs/route.ts b/app/api/priority-logs/route.ts deleted file mode 100644 index d64d0247..00000000 --- a/app/api/priority-logs/route.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { NextResponse } from 'next/server' -import getReqAuthToken from '../../../utilities/getReqAuthToken'; -import { fetchLogMetrics } from '../logs'; - -export async function GET(req: Request) { - try { - const token = getReqAuthToken(req) - const data = await fetchLogMetrics(token) - return NextResponse.json(data) - } catch (error) { - return NextResponse.json({ error: 'Failed to fetch priority logs' }, { status: 500 }) - } -} diff --git a/app/dashboard/Main.tsx b/app/dashboard/Main.tsx index d3cff9c2..3f220e51 100644 --- a/app/dashboard/Main.tsx +++ b/app/dashboard/Main.tsx @@ -1,6 +1,6 @@ 'use client' -import React, { FC, useEffect } from 'react'; +import React, { FC, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next' import { useSetRecoilState } from 'recoil' import pckJson from '../../package.json' @@ -17,7 +17,7 @@ import useLocalStorage from '../../src/hooks/useLocalStorage' import useNetworkMonitor from '../../src/hooks/useNetworkMonitor' import useSWRPolling from '../../src/hooks/useSWRPolling' import { exchangeRates, proposerDuties } from '../../src/recoil/atoms'; -import { LogMetric, ProposerDuty, StatusColor } from '../../src/types'; +import { LogMetric, PriorityLogResults, ProposerDuty, StatusColor } from '../../src/types'; import { BeaconNodeSpecResults, SyncData } from '../../src/types/beacon' import { Diagnostics, PeerDataResults } from '../../src/types/diagnostic' import { ValidatorCache, ValidatorInclusionData, ValidatorInfo } from '../../src/types/validator' @@ -36,6 +36,7 @@ export interface MainProps { initInclusionRate: ValidatorInclusionData initProposerDuties: ProposerDuty[] initLogMetrics: LogMetric + initPriorityLogs: PriorityLogResults } const Main: FC = (props) => { @@ -52,6 +53,7 @@ const Main: FC = (props) => { genesisTime, initProposerDuties, initLogMetrics, + initPriorityLogs } = props const { t } = useTranslation() @@ -62,6 +64,7 @@ const Main: FC = (props) => { const [username] = useLocalStorage('username', 'Keeper') const setExchangeRate = useSetRecoilState(exchangeRates) const setDuties = useSetRecoilState(proposerDuties) + const [priorityAlertPage, setPage] = useState(1) const { isValidatorError, isBeaconError } = useNetworkMonitor() @@ -111,18 +114,24 @@ const Main: FC = (props) => { networkError, }) - const { data: logMetrics } = useSWRPolling('/api/priority-logs', { + const { data: logMetrics } = useSWRPolling('/api/log-metrics', { refreshInterval: slotInterval / 2, fallbackData: initLogMetrics, networkError, }) + const { data: priorityLogs, isLoading } = useSWRPolling(`/api/priority-logs/${priorityAlertPage}`, { + refreshInterval: slotInterval / 2, + fallbackData: initPriorityLogs, + networkError, + }) + const { beaconSync, executionSync } = syncData const { isSyncing } = beaconSync const { isReady } = executionSync const { connected } = peerData const { natOpen } = nodeHealth - const warningCount = logMetrics.warningLogs?.length || 0 + const warningCount = logMetrics.warningLogs || 0 useEffect(() => { setDuties(prev => formatUniqueObjectArray([...prev, ...valDuties])) @@ -215,6 +224,8 @@ const Main: FC = (props) => { removeAlert(ALERT_ID.WARNING_LOG) }, [warningCount, storeAlert, removeAlert]) + const loadMoreLogs = async () => setPage(prev => prev + 1) + return ( = (props) => { /> = ({ initSyncData, beaconSpec, initNodeHealth, initLog networkError, }) - const { data: logMetrics } = useSWRPolling('/api/priority-logs', { + const { data: logMetrics } = useSWRPolling(`/api/log-metrics/${logType}`, { refreshInterval: slotInterval / 2, fallbackData: initLogMetrics, networkError, }) - const filteredLogs = useMemo(() => { - return { - warningLogs: logMetrics.warningLogs.filter(({type}) => type === logType), - errorLogs: logMetrics.errorLogs.filter(({type}) => type === logType), - criticalLogs: logMetrics.criticalLogs.filter(({type}) => type === logType) - } - }, [logMetrics, logType]) - const toggleLogType = (selection: OptionType) => { if (selection === logType) return @@ -79,7 +71,7 @@ const Main: FC = ({ initSyncData, beaconSpec, initNodeHealth, initLog >
- +
) diff --git a/app/dashboard/logs/page.tsx b/app/dashboard/logs/page.tsx index 2359fa50..4a2077ce 100644 --- a/app/dashboard/logs/page.tsx +++ b/app/dashboard/logs/page.tsx @@ -1,15 +1,16 @@ -import '../../../src/global.css' +import '../../../src/global.css'; import { redirect } from 'next/navigation'; +import { LogType } from '../../../src/types'; import getSessionCookie from '../../../utilities/getSessionCookie'; -import { fetchBeaconSpec, fetchNodeHealth, fetchSyncData } from '../../api/beacon' +import { fetchBeaconSpec, fetchNodeHealth, fetchSyncData } from '../../api/beacon'; import { fetchLogMetrics } from '../../api/logs'; -import Wrapper from './Wrapper' +import Wrapper from './Wrapper'; export default async function Page() { try { const token = getSessionCookie() - const logMetrics = await fetchLogMetrics(token) + const logMetrics = await fetchLogMetrics(token, LogType.VALIDATOR) const beaconSpec = await fetchBeaconSpec(token) const syncData = await fetchSyncData(token) const nodeHealth = await fetchNodeHealth(token) diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index cb131f8e..8710fd3e 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -1,17 +1,18 @@ -import '../../src/global.css' +import '../../src/global.css'; import { redirect } from 'next/navigation'; import getSessionCookie from '../../utilities/getSessionCookie'; import { fetchBeaconSpec, fetchInclusionRate, fetchNodeHealth, - fetchPeerData, fetchProposerDuties, + fetchPeerData, + fetchProposerDuties, fetchSyncData } from '../api/beacon'; -import { fetchBeaconNodeVersion, fetchGenesisData, fetchValidatorVersion } from '../api/config' -import { fetchLogMetrics } from '../api/logs'; -import { fetchValCaches, fetchValStates } from '../api/validator' -import Wrapper from './Wrapper' +import { fetchBeaconNodeVersion, fetchGenesisData, fetchValidatorVersion } from '../api/config'; +import { fetchLogMetrics, fetchPriorityLogs } from '../api/logs'; +import { fetchValCaches, fetchValStates } from '../api/validator'; +import Wrapper from './Wrapper'; export default async function Page() { try { @@ -29,10 +30,12 @@ export default async function Page() { const lighthouseVersion = await fetchValidatorVersion(token) const proposerDuties = await fetchProposerDuties(token) const logMetrics = await fetchLogMetrics(token) + const priorityLogs = await fetchPriorityLogs(token) return ( { const {t} = useTranslation() diff --git a/backend/src/logs/logs.controller.ts b/backend/src/logs/logs.controller.ts index 78498620..43d5a248 100644 --- a/backend/src/logs/logs.controller.ts +++ b/backend/src/logs/logs.controller.ts @@ -3,6 +3,7 @@ import { Controller, Get, Res, Req, Param, UseGuards } from '@nestjs/common'; import { Request, Response } from 'express'; import { LogsService } from './logs.service'; import { SessionGuard } from '../session.guard'; +import { LogType } from '../../../src/types'; @Controller('logs') @UseGuards(SessionGuard) @@ -21,9 +22,24 @@ export class LogsController { this.logsService.getSseStream(req, res, `${beaconUrl}/lighthouse/logs`); } + @Get('priority') + getPriorityLogs() { + return this.logsService.readPriorityLogs() + } + + @Get('priority/:page') + getPriorityLogsPage(@Param('page') page: string) { + return this.logsService.readPriorityLogs(page) + } + @Get('metrics') getLogMetrics() { - return this.logsService.readLogMetrics() + return this.logsService.fetchLogCounts() + } + + @Get('metrics/:type') + getLogTypeMetrics(@Param('type') type: LogType) { + return this.logsService.fetchLogCounts(type) } @Get('dismiss/:index') diff --git a/backend/src/logs/logs.service.ts b/backend/src/logs/logs.service.ts index 9460b8f4..542877e7 100644 --- a/backend/src/logs/logs.service.ts +++ b/backend/src/logs/logs.service.ts @@ -79,14 +79,18 @@ export class LogsService { } } - async readLogMetrics(type?: LogType) { + async fetchLogCounts(type?: LogType) { if(type && !this.logTypes.includes(type)) { throw new Error('Invalid log type'); } let warnOptions = { where: { level: LogLevels.WARN } } as any let errorOptions = { where: { level: LogLevels.ERRO } } as any - let critOptions = { where: { level: LogLevels.CRIT } } as any + let critOptions = { + where: { level: LogLevels.CRIT }, + createdAt: { + [Op.gte]: new Date(Date.now() - 60 * 60 * 1000) + } } as any if(type) { warnOptions.where.type = { [Op.eq]: type }; @@ -94,9 +98,9 @@ export class LogsService { critOptions.where.type = { [Op.eq]: type }; } - const warningLogs = (await this.logRepository.findAll(warnOptions)).map(data => data.dataValues) - const errorLogs = (await this.logRepository.findAll(errorOptions)).map(data => data.dataValues) - const criticalLogs = (await this.logRepository.findAll(critOptions)).map(data => data.dataValues) + const { count: warningLogs } = await this.logRepository.findAndCountAll(warnOptions) + const { count: errorLogs } = await this.logRepository.findAndCountAll(errorOptions) + const { count: criticalLogs } = await this.logRepository.findAndCountAll(critOptions) return { warningLogs, @@ -105,6 +109,39 @@ export class LogsService { } } + async readPriorityLogs(page?: string) { + const formattedPage = page || 1 + const max = 15 + const limit = Number(formattedPage) * max + + let options = { + where: { + [Op.and]: [ + { + [Op.or]: [ + { level: LogLevels.CRIT }, + { level: LogLevels.ERRO } + ] + }, + { + isHidden: false + } + ] + }, + limit: limit + 1, + order: [['createdAt', 'DESC']] + } as any + + const results = (await this.logRepository.findAll(options)).map(data => data.dataValues) + + const hasNextPage = results.length > limit + + return { + logs: results.slice(0, limit), + hasNextPage + } + } + async dismissLog(id: string) { return await this.logRepository.update({isHidden: true}, {where: {id}}) } diff --git a/backend/src/logs/tests/logs.controller.spec.ts b/backend/src/logs/tests/logs.controller.spec.ts index bb67ea15..63876a63 100644 --- a/backend/src/logs/tests/logs.controller.spec.ts +++ b/backend/src/logs/tests/logs.controller.spec.ts @@ -10,6 +10,7 @@ import { SequelizeModule } from '@nestjs/sequelize'; import { Log } from '../entities/log.entity'; import { Sequelize } from 'sequelize-typescript'; import { mockCritLog, mockErrorLog, mockWarningLog } from '../../../../src/mocks/logs'; +import { LogType } from '../../../../src/types'; describe('LogsController', () => { let logsService: LogsService; @@ -74,15 +75,43 @@ describe('LogsController', () => { it('should return log metrics', async () => { const data = { - warningLogs: [mockWarningLog], - errorLogs: [mockErrorLog], - criticalLogs: [mockCritLog], + warningLogs: 1, + errorLogs: 1, + criticalLogs: 1, }; const result = await controller.getLogMetrics(); expect(result).toEqual(data); }); + it('should return validator log metrics', async () => { + const data = { + warningLogs: 0, + errorLogs: 0, + criticalLogs: 1, + }; + + const result = await controller.getLogTypeMetrics(LogType.VALIDATOR); + expect(result).toEqual(data); + }); + + it('should return beacon log metrics', async () => { + const data = { + warningLogs: 1, + errorLogs: 1, + criticalLogs: 0, + }; + + const result = await controller.getLogTypeMetrics(LogType.BEACON); + expect(result).toEqual(data); + }); + + it('should return priority logs', async () => { + const result = await controller.getPriorityLogs() + + expect(result).toEqual({logs: [mockErrorLog, mockCritLog], hasNextPage: false}) + }) + it('should update log metrics', async () => { const result = await controller.dismissLogAlert('1'); expect(result).toEqual([1]); diff --git a/package.json b/package.json index df3be92f..c44be92c 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "tailwind-scrollbar-hide": "^1.1.7", "tailwindcss": "^3.1.8", "tailwindcss-scoped-groups": "^2.0.0", - "typescript": "5.5.2", + "typescript": "5.5.3", "yup": "^0.32.11" }, "scripts": { @@ -75,7 +75,7 @@ ] }, "devDependencies": { - "@types/node": "20.14.8", + "@types/node": "20.14.10", "@typescript-eslint/eslint-plugin": "^7.1.0", "eslint": "8.57.0", "eslint-config-next": "14.1.1", diff --git a/src/components/AlertCard/AlertCard.tsx b/src/components/AlertCard/AlertCard.tsx index 63de1d84..a39b5ec0 100644 --- a/src/components/AlertCard/AlertCard.tsx +++ b/src/components/AlertCard/AlertCard.tsx @@ -1,3 +1,4 @@ +import { motion } from 'framer-motion'; import { FC } from 'react' import StatusBar, { StatusBarProps } from '../StatusBar/StatusBar' import Typography from '../Typography/Typography' @@ -5,12 +6,17 @@ import Typography from '../Typography/Typography' export interface AlertCardProps extends StatusBarProps { text: string subText: string + animKey?: string onClick?: () => void } -const AlertCard: FC = ({ text, subText, onClick, ...props }) => { +const AlertCard: FC = ({ text, subText, onClick, animKey, ...props }) => { return ( -
+
{text} @@ -22,7 +28,7 @@ const AlertCard: FC = ({ text, subText, onClick, ...props }) => onClick={onClick} className='bi-trash-fill cursor-pointer opacity-0 group-hover:opacity-100 text-dark200 dark:text-dark300' /> -
+
) } diff --git a/src/components/AlertInfo/AlertInfo.tsx b/src/components/AlertInfo/AlertInfo.tsx index dbc6dfe6..98a3f02d 100644 --- a/src/components/AlertInfo/AlertInfo.tsx +++ b/src/components/AlertInfo/AlertInfo.tsx @@ -6,17 +6,20 @@ import useDiagnosticAlerts from '../../hooks/useDiagnosticAlerts'; import useDivDimensions from '../../hooks/useDivDimensions'; import useMediaQuery from '../../hooks/useMediaQuery'; import { proposerDuties } from '../../recoil/atoms'; -import { LogLevels, StatusColor } from '../../types'; -import AlertCard from '../AlertCard/AlertCard'; +import { PriorityLogResults, StatusColor } from '../../types'; import AlertFilterSettings, { FilterValue } from '../AlertFilterSettings/AlertFilterSettings'; -import { LogsInfoProps } from '../DiagnosticTable/LogsInfo'; import ProposerAlerts, { ProposerAlertsProps } from '../ProposerAlerts/ProposerAlerts'; import Typography from '../Typography/Typography'; import PriorityLogAlerts from './PriorityLogAlerts'; +import StandardAlerts from './StandardAlerts'; -export interface AlertInfoProps extends Omit, LogsInfoProps {} +export interface AlertInfoProps extends Omit { + logData: PriorityLogResults + isLoadingPriority: boolean + onLoadMore?: (() => void) | undefined +} -const AlertInfo: FC = ({metrics, ...props}) => { +const AlertInfo: FC = ({logData, isLoadingPriority, onLoadMore, ...props}) => { const { t } = useTranslation() const { alerts, dismissAlert, resetDismissed } = useDiagnosticAlerts() const { ref, dimensions } = useDivDimensions() @@ -24,12 +27,6 @@ const AlertInfo: FC = ({metrics, ...props}) => { const [filter, setFilter] = useState('all') const duties = useRecoilValue(proposerDuties) - const priorityLogAlerts = useMemo(() => { - return Object.values(metrics).flat().filter(({level}) => - level === LogLevels.CRIT || level === LogLevels.ERRO) - .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); - }, [metrics]); - const setFilterValue = (value: FilterValue) => setFilter(value) const isMobile = useMediaQuery('(max-width: 425px)') @@ -42,15 +39,14 @@ const AlertInfo: FC = ({metrics, ...props}) => { return sortAlertMessagesBySeverity(baseAlerts) }, [alerts, filter]) - - const isSeverFilter = (filter === 'all' || filter === StatusColor.ERROR) - - const isFiller = formattedAlerts.length + (duties?.length || 0) + (priorityLogAlerts.length || 0) < 6 - const isPriorityAlerts = priorityLogAlerts.length > 0 + const isPriorityAlerts = logData.logs.length > 0 && (filter === 'all' || filter === StatusColor.ERROR) const isAlerts = formattedAlerts.length > 0 || duties?.length > 0 || isPriorityAlerts const isProposerAlerts = duties?.length > 0 && (filter === 'all' || filter === StatusColor.SUCCESS) + const totalAlertCount = formattedAlerts.length + (isPriorityAlerts ? duties.length + 1 : 0) + (isPriorityAlerts ? logData.logs.length : 0); + const isFiller = totalAlertCount < 6 + useEffect(() => { const intervalId = setInterval(() => { resetDismissed() @@ -77,28 +73,14 @@ const AlertInfo: FC = ({metrics, ...props}) => { ? undefined : { maxHeight: `${dimensions.height - (headerDimensions?.dimensions?.height || 0)}px`, - } + } as any } className='h-full w-full flex flex-col' > {isAlerts && (
- {isPriorityAlerts && isSeverFilter && ()} - {formattedAlerts.map((alert) => { - const { severity, subText, message, id } = alert - const count = - severity === StatusColor.SUCCESS ? 1 : severity === StatusColor.WARNING ? 2 : 3 - return ( - dismissAlert(alert)} - subText={subText} - text={message} - /> - ) - })} + {isPriorityAlerts && } + {isProposerAlerts && }
)} diff --git a/src/components/AlertInfo/PriorityLogAlerts.tsx b/src/components/AlertInfo/PriorityLogAlerts.tsx index 8458b7e5..d377ca51 100644 --- a/src/components/AlertInfo/PriorityLogAlerts.tsx +++ b/src/components/AlertInfo/PriorityLogAlerts.tsx @@ -1,4 +1,5 @@ import axios from 'axios'; +import { AnimatePresence } from 'framer-motion'; import Cookies from 'js-cookie'; import moment from 'moment'; import { FC, useEffect, useMemo, useState } from 'react'; @@ -6,12 +7,17 @@ import { useTranslation } from 'react-i18next'; import displayToast from '../../../utilities/displayToast'; import { LogData, StatusColor, ToastType } from '../../types'; import AlertCard from '../AlertCard/AlertCard'; +import LoadingDots from '../LoadingDots/LoadingDots'; +import Typography from '../Typography/Typography'; export interface LogAlertsProps { alerts: LogData[] + hasNextPage: boolean + onLoadMore?: (() => void) | undefined + isLoading: boolean } -const PriorityLogAlerts:FC = ({alerts}) => { +const PriorityLogAlerts:FC = ({alerts, hasNextPage, onLoadMore, isLoading}) => { const {t} = useTranslation() const [data, setData] = useState(alerts) @@ -53,20 +59,36 @@ const PriorityLogAlerts:FC = ({alerts}) => { } } - return visibleAlerts.map(({id, data, createdAt}) => { - const alertData = JSON.parse(data) - const date = moment(createdAt).fromNow() - return ( - dismissAlert(id)} - subText={t('poor')} - text={`${alertData.msg} ${date}`} - /> - ) - }) + return ( + + { + visibleAlerts.map(({id, data, createdAt}) => { + const alertData = JSON.parse(data) + const date = moment(createdAt).fromNow() + return ( + dismissAlert(id)} + subText={t('poor')} + text={`${t(`alertMessages.log.${alertData.level}`)}: ${alertData.msg} ${date}`} + /> + ) + }) + } + {hasNextPage && onLoadMore && ( +
+ {isLoading ? ( + + ) : ( + {t('alertMessages.log.loadMore')} + )} +
+ )} +
+ ) } export default PriorityLogAlerts; \ No newline at end of file diff --git a/src/components/AlertInfo/StandardAlerts.tsx b/src/components/AlertInfo/StandardAlerts.tsx new file mode 100644 index 00000000..d31ea1e3 --- /dev/null +++ b/src/components/AlertInfo/StandardAlerts.tsx @@ -0,0 +1,35 @@ +import { AnimatePresence } from 'framer-motion'; +import { FC } from 'react'; +import { AlertMessage, StatusColor } from '../../types'; +import AlertCard from '../AlertCard/AlertCard'; + +export interface StandardAlertsProps { + alerts: AlertMessage[] + onDismiss: (alert: AlertMessage) => void +} + +const StandardAlerts:FC = ({alerts, onDismiss}) => { + return ( + + { + alerts.map((alert) => { + const { severity, subText, message, id } = alert + const count = + severity === StatusColor.SUCCESS ? 1 : severity === StatusColor.WARNING ? 2 : 3 + return ( + onDismiss(alert)} + subText={subText} + text={message} + /> + ) + }) + } + + ) +} + +export default StandardAlerts \ No newline at end of file diff --git a/src/components/DiagnosticTable/DiagnosticTable.tsx b/src/components/DiagnosticTable/DiagnosticTable.tsx index f04c8e59..9d314268 100644 --- a/src/components/DiagnosticTable/DiagnosticTable.tsx +++ b/src/components/DiagnosticTable/DiagnosticTable.tsx @@ -5,12 +5,12 @@ import LogsInfo, { LogsInfoProps } from './LogsInfo'; export interface DiagnosticTableProps extends HardwareInfoProps, AlertInfoProps, LogsInfoProps {} -const DiagnosticTable: FC = ({syncData, beanHealth, metrics, bnSpec}) => { +const DiagnosticTable: FC = ({syncData, beanHealth, metrics, logData, isLoadingPriority, onLoadMore, bnSpec}) => { return (
- +
) } diff --git a/src/components/LogDisplay/LogDisplay.tsx b/src/components/LogDisplay/LogDisplay.tsx index 2bb76719..2818da08 100644 --- a/src/components/LogDisplay/LogDisplay.tsx +++ b/src/components/LogDisplay/LogDisplay.tsx @@ -12,10 +12,10 @@ import LogRow from './LogRow' export interface LogDisplayProps { type: LogType isLoading?: boolean - priorityLogs: LogMetric + metrics: LogMetric } -const LogDisplay: FC = React.memo(function ({ type, isLoading, priorityLogs }) { +const LogDisplay: FC = React.memo(function ({ type, isLoading, metrics }) { const { t } = useTranslation() const [searchText, setText] = useState('') const scrollableRef = useRef(null) @@ -127,7 +127,7 @@ const LogDisplay: FC = React.memo(function ({ type, isLoading, size='lg' maxHeight='h-32 md:flex-1' maxWidth='w-full' - metrics={priorityLogs} + metrics={metrics} />
diff --git a/src/components/LogStats/LogStats.tsx b/src/components/LogStats/LogStats.tsx index 649d5ec7..a139c54c 100644 --- a/src/components/LogStats/LogStats.tsx +++ b/src/components/LogStats/LogStats.tsx @@ -1,6 +1,5 @@ -import { FC, useMemo } from 'react'; +import { FC } from 'react'; import { useTranslation } from 'react-i18next' -import timeFilterObjArray from '../../../utilities/timeFilterObjArray'; import toFixedIfNecessary from '../../../utilities/toFixedIfNecessary' import { LogMetric, StatusColor } from '../../types'; import DiagnosticCard, { CardSize } from '../DiagnosticCard/DiagnosticCard' @@ -27,33 +26,17 @@ const LogStats: FC = ({ const { t } = useTranslation() const { criticalLogs, warningLogs, errorLogs } = metrics - const hourlyCriticalLogs = useMemo(() => { - return timeFilterObjArray(criticalLogs, 'createdAt', 'minutes', 60) - }, [criticalLogs]) - - const hourlyWarningLogs = useMemo(() => { - return timeFilterObjArray(warningLogs, 'createdAt', 'minutes', 60) - }, [warningLogs]) - - const hourlyErrorLogs = useMemo(() => { - return timeFilterObjArray(errorLogs, 'createdAt', 'minutes', 60) - }, [errorLogs]) - - const criticalMetrics = hourlyCriticalLogs.length - const warningMetrics = hourlyWarningLogs.length - const errorMetrics = hourlyErrorLogs.length - - const critStatus = criticalMetrics > 0 + const critStatus = criticalLogs > 0 ? StatusColor.ERROR : StatusColor.SUCCESS - const errorStatus = errorMetrics <= 0 + const errorStatus = errorLogs <= 0 ? StatusColor.SUCCESS - : errorMetrics <= 2 + : errorLogs <= 2 ? StatusColor.WARNING : StatusColor.ERROR - const warnStatus = warningMetrics < 5 + const warnStatus = warningLogs < 5 ? StatusColor.SUCCESS - : warningMetrics <= 50 + : warningLogs <= 50 ? StatusColor.WARNING : StatusColor.ERROR @@ -68,7 +51,7 @@ const LogStats: FC = ({ size={size} border='border-t-0 md:border-l-0 border-style500' subTitle={t('critical')} - metric={`${toFixedIfNecessary(criticalMetrics, 2)} / HR`} + metric={`${toFixedIfNecessary(criticalLogs, 2)} / HR`} /> = ({ size={size} border='border-t-0 md:border-l-0 border-style500' subTitle={t('logInfo.validatorLogs')} - metric={`${toFixedIfNecessary(errorMetrics, 2)} / HR`} + metric={`${toFixedIfNecessary(errorLogs, 2)} / HR`} /> = ({ size={size} border='border-t-0 md:border-l-0 border-style500' subTitle={t('logInfo.validatorLogs')} - metric={`${toFixedIfNecessary(warningMetrics, 2)} / HR`} + metric={`${toFixedIfNecessary(warningLogs, 2)} / HR`} /> ) diff --git a/src/components/StepChart/StepChart.tsx b/src/components/StepChart/StepChart.tsx index 998b5a31..91465bbe 100644 --- a/src/components/StepChart/StepChart.tsx +++ b/src/components/StepChart/StepChart.tsx @@ -55,6 +55,7 @@ const StepChart: FC = ({ data, stepSize, onClick, className }) = const chartEl = useRef(null) const mode = useRecoilValue(uiMode) const [hasAnimated, toggleAnimated] = useState(false) + const uiGridColor = mode === UiMode.LIGHT ? '#F3F5FB' : 'rgba(255, 255, 255, 0.03)' useEffect(() => { const ctx = chartEl.current @@ -110,7 +111,7 @@ const StepChart: FC = ({ data, stepSize, onClick, className }) = scales: { x: { grid: { - color: mode === UiMode.DARK ? 'rgba(255, 255, 255, 0.03)' : '#F3F5FB', + color: uiGridColor, }, }, y: { @@ -119,7 +120,7 @@ const StepChart: FC = ({ data, stepSize, onClick, className }) = stepSize, }, grid: { - color: mode === UiMode.DARK ? 'rgba(255, 255, 255, 0.03)' : '#F3F5FB', + color: uiGridColor, }, }, }, diff --git a/src/hooks/useSWRPolling.ts b/src/hooks/useSWRPolling.ts index efbd87d2..0f07f556 100644 --- a/src/hooks/useSWRPolling.ts +++ b/src/hooks/useSWRPolling.ts @@ -1,3 +1,4 @@ +import axios from 'axios'; import Cookies from 'js-cookie'; import { useState } from 'react' import useSWR from 'swr' @@ -12,7 +13,8 @@ const useSWRPolling = ( networkError?: boolean }, callBack?: (url: string | null) => void, -): {data: T} => { +): {data: T, refreshData: (nextUrl?: string) => Promise, isLoading: boolean} => { + const apiToken = Cookies.get('session-token') const { refreshInterval = 12000, fallbackData, errorRetryCount = 2, networkError } = config || {} const [errorCount, setErrors] = useState(0) @@ -25,14 +27,24 @@ const useSWRPolling = ( callBack?.(api) } - const { data } = useSWR([errorCount <= errorRetryCount && !networkError ? api : null, Cookies.get('session-token')], swrGetFetcher, { + const { data, mutate, isLoading } = useSWR([errorCount <= errorRetryCount && !networkError ? api : null, apiToken], swrGetFetcher, { refreshInterval, fallbackData, errorRetryCount, onError: incrementCount, }) - return {data: data as T} + const refreshData = async (nextUrl?: string) => { + const { data } = await axios.get((nextUrl || api) as string, { + headers: { + Authorization: `Bearer ${apiToken}` + } + }) + + await mutate(data, {revalidate: false}) + } + + return {data: data as T, refreshData, isLoading} } export default useSWRPolling diff --git a/src/locales/translations/en-US.json b/src/locales/translations/en-US.json index c4c88a03..57efa92d 100644 --- a/src/locales/translations/en-US.json +++ b/src/locales/translations/en-US.json @@ -445,6 +445,12 @@ "severe": "Severe", "warning": "Warning", "fair": "Fair", + "priority": "PRIORITY LOG", + "log": { + "loadMore": "Load more logs", + "ERRO": "ERROR LOG", + "CRIT": "CRITICAL LOG" + }, "dismiss": { "success": "Successfully dismissed alert!", "error": "Error occurred, unable to dismiss alert!" diff --git a/src/types/index.ts b/src/types/index.ts index 315c7fef..8c361024 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -53,9 +53,14 @@ export type LogData = { } export type LogMetric = { - warningLogs: LogData[], - errorLogs: LogData[], - criticalLogs: LogData[], + warningLogs: number, + errorLogs: number, + criticalLogs: number, +} + +export type PriorityLogResults = { + logs: LogData[], + hasNextPage: boolean } export enum LogType { diff --git a/yarn.lock b/yarn.lock index 3bf1437b..d1857c88 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1808,7 +1808,7 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.5.tgz#e6c29b58e66995d57cd170ce3e2a61926d55ee04" integrity sha512-MBIOHVZqVqgfro1euRDWX7OO0fBVUUMrN6Pwm8LQsz8cWhEpihlvR70ENj3f40j58TNxZaWv2ndSkInykNBBJw== -"@types/node@*", "@types/node@20.14.8": +"@types/node@*": version "20.14.8" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.8.tgz#45c26a2a5de26c3534a9504530ddb3b27ce031ac" integrity sha512-DO+2/jZinXfROG7j7WKFn/3C6nFwxy2lLpgLjEXJz+0XKphZlTLJ14mo8Vfg8X5BWN6XjyESXq+LcYdT7tR3bA== @@ -1820,6 +1820,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.14.10": + version "20.14.10" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.10.tgz#a1a218290f1b6428682e3af044785e5874db469a" + integrity sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ== + dependencies: + undici-types "~5.26.4" + "@types/parse5@^6.0.0": version "6.0.3" resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-6.0.3.tgz#705bb349e789efa06f43f128cef51240753424cb" @@ -7309,10 +7316,10 @@ typed-array-length@^1.0.6: is-typed-array "^1.1.13" possible-typed-array-names "^1.0.0" -typescript@5.5.2: - version "5.5.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.2.tgz#c26f023cb0054e657ce04f72583ea2d85f8d0507" - integrity sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew== +typescript@5.5.3: + version "5.5.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.3.tgz#e1b0a3c394190838a0b168e771b0ad56a0af0faa" + integrity sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ== unbox-primitive@^1.0.2: version "1.0.2"