add system page
This commit is contained in:
parent
a3ff2960fc
commit
765cca1f31
@ -1,4 +1,15 @@
|
|||||||
const en = {
|
const en = {
|
||||||
|
detectorCard: {
|
||||||
|
pid: 'PID',
|
||||||
|
inferenceSpeed: 'Inference Speed',
|
||||||
|
memory: 'Memory',
|
||||||
|
},
|
||||||
|
gpuStatCard: {
|
||||||
|
gpu: 'GPU',
|
||||||
|
memory: 'Memory',
|
||||||
|
decoder: 'Decoder',
|
||||||
|
encoder: 'Encoder',
|
||||||
|
},
|
||||||
cameraStatTable: {
|
cameraStatTable: {
|
||||||
process: 'Process',
|
process: 'Process',
|
||||||
pid: 'PID',
|
pid: 'PID',
|
||||||
@ -35,6 +46,8 @@ const en = {
|
|||||||
doubleClickToFullHint: 'Double click for fullscreen',
|
doubleClickToFullHint: 'Double click for fullscreen',
|
||||||
rating: 'Rating',
|
rating: 'Rating',
|
||||||
},
|
},
|
||||||
|
version: 'Version',
|
||||||
|
uptime: 'Uptime',
|
||||||
pleaseSelectRole: 'Please select Role',
|
pleaseSelectRole: 'Please select Role',
|
||||||
pleaseSelectHost: 'Please select Host',
|
pleaseSelectHost: 'Please select Host',
|
||||||
pleaseSelectCamera: 'Please select Camera',
|
pleaseSelectCamera: 'Please select Camera',
|
||||||
@ -46,10 +59,12 @@ const en = {
|
|||||||
camersDoesNotExist: 'No cameras',
|
camersDoesNotExist: 'No cameras',
|
||||||
search: 'Search',
|
search: 'Search',
|
||||||
recordings: 'Recordings',
|
recordings: 'Recordings',
|
||||||
|
day: 'Day',
|
||||||
hour: 'Hour',
|
hour: 'Hour',
|
||||||
|
minute: 'Minute',
|
||||||
|
second: 'Second',
|
||||||
events: 'Events',
|
events: 'Events',
|
||||||
notHaveEvents: 'No events',
|
notHaveEvents: 'No events',
|
||||||
day: 'Day',
|
|
||||||
selectHost: 'Select host',
|
selectHost: 'Select host',
|
||||||
selectCamera: 'Select Camera',
|
selectCamera: 'Select Camera',
|
||||||
selectRange: 'Select period',
|
selectRange: 'Select period',
|
||||||
|
|||||||
@ -1,4 +1,15 @@
|
|||||||
const ru = {
|
const ru = {
|
||||||
|
detectorCard: {
|
||||||
|
pid: 'PID',
|
||||||
|
inferenceSpeed: 'Скорость вывода',
|
||||||
|
memory: 'Память',
|
||||||
|
},
|
||||||
|
gpuStatCard: {
|
||||||
|
gpu: 'GPU',
|
||||||
|
memory: 'Память',
|
||||||
|
decoder: 'Декодер',
|
||||||
|
encoder: 'Кодер',
|
||||||
|
},
|
||||||
hostMenu: {
|
hostMenu: {
|
||||||
editConfig: 'Редакт. конфиг.',
|
editConfig: 'Редакт. конфиг.',
|
||||||
restart: 'Перезагрузка',
|
restart: 'Перезагрузка',
|
||||||
@ -28,6 +39,8 @@ const ru = {
|
|||||||
doubleClickToFullHint: 'Двойное нажатие мышью для полноэкранного просмотра',
|
doubleClickToFullHint: 'Двойное нажатие мышью для полноэкранного просмотра',
|
||||||
rating: 'Рейтинг',
|
rating: 'Рейтинг',
|
||||||
},
|
},
|
||||||
|
version: 'Версия',
|
||||||
|
uptime: 'Время работы',
|
||||||
pleaseSelectRole: 'Пожалуйста выберите роль',
|
pleaseSelectRole: 'Пожалуйста выберите роль',
|
||||||
pleaseSelectHost: 'Пожалуйста выберите хост',
|
pleaseSelectHost: 'Пожалуйста выберите хост',
|
||||||
pleaseSelectCamera: 'Пожалуйста выберите камеру',
|
pleaseSelectCamera: 'Пожалуйста выберите камеру',
|
||||||
@ -39,10 +52,12 @@ const ru = {
|
|||||||
camersDoesNotExist: 'Камер нет',
|
camersDoesNotExist: 'Камер нет',
|
||||||
search: 'Поиск',
|
search: 'Поиск',
|
||||||
recordings: 'Записи',
|
recordings: 'Записи',
|
||||||
|
day: 'День',
|
||||||
hour: 'Час',
|
hour: 'Час',
|
||||||
|
minute: 'Минута',
|
||||||
|
second: 'Час',
|
||||||
events: 'События',
|
events: 'События',
|
||||||
notHaveEvents: 'Событий нет',
|
notHaveEvents: 'Событий нет',
|
||||||
day: 'День',
|
|
||||||
selectHost:'Выбери хост',
|
selectHost:'Выбери хост',
|
||||||
selectCamera: 'Выбери камеру',
|
selectCamera: 'Выбери камеру',
|
||||||
selectRange: 'Выбери период',
|
selectRange: 'Выбери период',
|
||||||
|
|||||||
@ -8,14 +8,21 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
import { frigateApi, frigateQueryKeys, mapHostToHostname, proxyApi } from '../services/frigate.proxy/frigate.api';
|
import { frigateApi, frigateQueryKeys, mapHostToHostname, proxyApi } from '../services/frigate.proxy/frigate.api';
|
||||||
import CenterLoader from '../shared/components/loaders/CenterLoader';
|
import CenterLoader from '../shared/components/loaders/CenterLoader';
|
||||||
import RetryErrorPage from './RetryErrorPage';
|
import RetryErrorPage from './RetryErrorPage';
|
||||||
import { Flex } from '@mantine/core';
|
import { Flex, Grid, Text } from '@mantine/core';
|
||||||
import FrigateCamerasStateTable, { CameraItem, ProcessType } from '../widgets/camera.stat.table/FrigateCameraStateTable';
|
import FrigateCamerasStateTable, { CameraItem, ProcessType } from '../widgets/camera.stat.table/FrigateCameraStateTable';
|
||||||
|
import StorageRingStat from '../shared/components/stats/StorageRingStat';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { formatUptime } from '../shared/utils/dateUtil';
|
||||||
|
import GpuStat from '../shared/components/stats/GpuStat';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
import DetectorsStat from '../shared/components/stats/DetectorsStat';
|
||||||
|
|
||||||
export const hostSystemPageQuery = {
|
export const hostSystemPageQuery = {
|
||||||
hostId: 'hostId',
|
hostId: 'hostId',
|
||||||
}
|
}
|
||||||
|
|
||||||
const HostSystemPage = () => {
|
const HostSystemPage = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const executed = useRef(false)
|
const executed = useRef(false)
|
||||||
const { sideBarsStore } = useContext(Context)
|
const { sideBarsStore } = useContext(Context)
|
||||||
const { isAdmin } = useAdminRole()
|
const { isAdmin } = useAdminRole()
|
||||||
@ -29,11 +36,6 @@ const HostSystemPage = () => {
|
|||||||
}
|
}
|
||||||
}, [sideBarsStore])
|
}, [sideBarsStore])
|
||||||
|
|
||||||
const location = useLocation()
|
|
||||||
const queryParams = useMemo(() => {
|
|
||||||
return new URLSearchParams(location.search);
|
|
||||||
}, [location.search])
|
|
||||||
|
|
||||||
let { id: paramHostId } = useParams<'id'>()
|
let { id: paramHostId } = useParams<'id'>()
|
||||||
|
|
||||||
const { data, isError, isPending, refetch } = useQuery({
|
const { data, isError, isPending, refetch } = useQuery({
|
||||||
@ -52,9 +54,9 @@ const HostSystemPage = () => {
|
|||||||
return Object.entries(data.cameras).flatMap(([name, stats]) => {
|
return Object.entries(data.cameras).flatMap(([name, stats]) => {
|
||||||
return (['Ffmpeg', 'Capture', 'Detect'] as ProcessType[]).map(type => {
|
return (['Ffmpeg', 'Capture', 'Detect'] as ProcessType[]).map(type => {
|
||||||
const pid = type === ProcessType.Ffmpeg ? stats.ffmpeg_pid :
|
const pid = type === ProcessType.Ffmpeg ? stats.ffmpeg_pid :
|
||||||
type === ProcessType.Capture ? stats.capture_pid : stats.pid;
|
type === ProcessType.Capture ? stats.capture_pid : stats.pid;
|
||||||
const fps = type === ProcessType.Ffmpeg ? stats.camera_fps :
|
const fps = type === ProcessType.Ffmpeg ? stats.camera_fps :
|
||||||
type === ProcessType.Capture ? stats.process_fps : stats.detection_fps;
|
type === ProcessType.Capture ? stats.process_fps : stats.detection_fps;
|
||||||
const cpu = data.cpu_usages[pid]?.cpu;
|
const cpu = data.cpu_usages[pid]?.cpu;
|
||||||
const mem = data.cpu_usages[pid]?.mem;
|
const mem = data.cpu_usages[pid]?.mem;
|
||||||
|
|
||||||
@ -76,9 +78,68 @@ const HostSystemPage = () => {
|
|||||||
if (!paramHostId || !data) return null
|
if (!paramHostId || !data) return null
|
||||||
|
|
||||||
const mappedCameraStat: CameraItem[] = mapCameraData()
|
const mappedCameraStat: CameraItem[] = mapCameraData()
|
||||||
|
const storageStats = Object.entries(data.service.storage).map(([name, stats]) => {
|
||||||
|
return (
|
||||||
|
<Grid.Col key={name + stats.mount_type} xs={6} sm={5} md={4} lg={3} p='0.2rem'>
|
||||||
|
<StorageRingStat
|
||||||
|
used={stats.used}
|
||||||
|
free={stats.free}
|
||||||
|
storageType={stats.mount_type}
|
||||||
|
total={stats.total}
|
||||||
|
path={name} />
|
||||||
|
</Grid.Col>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const formattedUptime = () => {
|
||||||
|
const time = formatUptime(data.service.uptime)
|
||||||
|
const translatedUnit = t(time.unit).toLowerCase().slice(0, 1)
|
||||||
|
return `${time.value.toFixed(1)} ${translatedUnit}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const gpuStats = Object.entries(data.gpu_usages).map(([name, stats]) => {
|
||||||
|
return (
|
||||||
|
<Grid.Col key={name + stats.gpu} xs={7} sm={6} md={5} lg={4} p='0.2rem'>
|
||||||
|
<GpuStat
|
||||||
|
name={name}
|
||||||
|
decoder={stats.dec}
|
||||||
|
encoder={stats.enc}
|
||||||
|
gpu={stats.gpu}
|
||||||
|
mem={stats.mem} />
|
||||||
|
</Grid.Col>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const detectorsStats = Object.entries(data.detectors).map(([name, stats]) => {
|
||||||
|
const pid = stats.pid
|
||||||
|
const cpu = data.cpu_usages[pid]?.cpu;
|
||||||
|
const mem = data.cpu_usages[pid]?.mem;
|
||||||
|
return (
|
||||||
|
<Grid.Col key={pid} xs={6} sm={5} md={4} lg={3} p='0.2rem'>
|
||||||
|
<DetectorsStat
|
||||||
|
name={name}
|
||||||
|
pid={pid}
|
||||||
|
inferenceSpeed={stats.inference_speed}
|
||||||
|
cpu={cpu}
|
||||||
|
mem={mem}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex w='100%' h='100%'>
|
<Flex w='100%' h='100%' direction='column'>
|
||||||
|
<Flex w='100%' justify='space-around'>
|
||||||
|
<Text>{t('version')} : {data.service.version}</Text>
|
||||||
|
<Text>{t('uptime')} : {formattedUptime()}</Text>
|
||||||
|
</Flex>
|
||||||
|
<Grid mt='sm' justify="center" mb='sm' align='stretch'>
|
||||||
|
{storageStats}
|
||||||
|
</Grid>
|
||||||
|
<Grid mt='sm' justify="center" mb='sm' align='stretch'>
|
||||||
|
{gpuStats}
|
||||||
|
{detectorsStats}
|
||||||
|
</Grid>
|
||||||
<FrigateCamerasStateTable data={mappedCameraStat} />
|
<FrigateCamerasStateTable data={mappedCameraStat} />
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
|||||||
42
src/shared/components/stats/DetectorsStat.tsx
Normal file
42
src/shared/components/stats/DetectorsStat.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { Card, Flex, Group, Text } from '@mantine/core';
|
||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
interface DetectorsStatProps {
|
||||||
|
name: string
|
||||||
|
pid: number,
|
||||||
|
inferenceSpeed?: number,
|
||||||
|
cpu?: string,
|
||||||
|
mem?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const DetectorsStat: React.FC<DetectorsStatProps> = ({
|
||||||
|
name,
|
||||||
|
pid,
|
||||||
|
inferenceSpeed,
|
||||||
|
cpu,
|
||||||
|
mem
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
return (
|
||||||
|
<Card withBorder radius="md" p='0.7rem'>
|
||||||
|
<Flex align='center'>
|
||||||
|
<Text c="dimmed" size="xs" tt="uppercase" fw={700} mr='0.5rem'>
|
||||||
|
{name}
|
||||||
|
</Text>
|
||||||
|
<Flex w='100%' direction='column' align='center'>
|
||||||
|
<Flex w='100%' justify='space-between'>
|
||||||
|
<Text>{t('detectorCard.pid')}: {pid}</Text>
|
||||||
|
<Text>{t('detectorCard.inferenceSpeed')}: {inferenceSpeed}</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex w='100%' justify='space-between'>
|
||||||
|
<Text>CPU: {cpu}</Text>
|
||||||
|
<Text>{t('detectorCard.memory')}: {mem}</Text>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DetectorsStat;
|
||||||
42
src/shared/components/stats/GpuStat.tsx
Normal file
42
src/shared/components/stats/GpuStat.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { Card, Flex, Group, Text } from '@mantine/core';
|
||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
interface GpuStatProps {
|
||||||
|
name: string,
|
||||||
|
decoder?: string,
|
||||||
|
encoder?: string,
|
||||||
|
gpu?: string,
|
||||||
|
mem?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const GpuStat: React.FC<GpuStatProps> = ({
|
||||||
|
name,
|
||||||
|
decoder,
|
||||||
|
encoder,
|
||||||
|
gpu,
|
||||||
|
mem
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
return (
|
||||||
|
<Card withBorder radius="md" p='0.7rem'>
|
||||||
|
<Flex align='center'>
|
||||||
|
<Text c="dimmed" size="xs" tt="uppercase" fw={700} mr='0.5rem'>
|
||||||
|
{name}
|
||||||
|
</Text>
|
||||||
|
<Flex w='100%' direction='column' align='center'>
|
||||||
|
<Flex w='100%' justify='space-between'>
|
||||||
|
<Text>{t('gpuStatCard.gpu')}: {gpu}</Text>
|
||||||
|
<Text>{t('gpuStatCard.memory')}: {mem}</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex w='100%' justify='space-between'>
|
||||||
|
<Text>{t('gpuStatCard.decoder')}: {decoder}</Text>
|
||||||
|
<Text>{t('gpuStatCard.encoder')}: {encoder}</Text>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GpuStat;
|
||||||
65
src/shared/components/stats/StorageRingStat.tsx
Normal file
65
src/shared/components/stats/StorageRingStat.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { Card, Center, Flex, Group, Paper, RingProgress, Stack, Text } from '@mantine/core';
|
||||||
|
import React from 'react';
|
||||||
|
import { formatMBytes } from '../../utils/data.size';
|
||||||
|
|
||||||
|
interface StorageRingStatProps {
|
||||||
|
used: number
|
||||||
|
free: number
|
||||||
|
total?: number
|
||||||
|
storageType: string
|
||||||
|
path?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const StorageRingStat: React.FC<StorageRingStatProps> = ({
|
||||||
|
used,
|
||||||
|
free,
|
||||||
|
storageType,
|
||||||
|
total,
|
||||||
|
path
|
||||||
|
}) => {
|
||||||
|
const calcTotal = total || used + free
|
||||||
|
const availablePercent = (used / calcTotal * 100)
|
||||||
|
return (
|
||||||
|
<Card withBorder radius="md" key={storageType} p='0.2rem'>
|
||||||
|
<Flex align='center'>
|
||||||
|
<RingProgress
|
||||||
|
size={80}
|
||||||
|
roundCaps
|
||||||
|
thickness={8}
|
||||||
|
sections={[
|
||||||
|
{ value: availablePercent, color: 'blue' },
|
||||||
|
]}
|
||||||
|
label={
|
||||||
|
<Center>
|
||||||
|
<Text size='md'>{availablePercent.toFixed(0)}%</Text>
|
||||||
|
</Center>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Flex w='100%' direction='column' align='center'>
|
||||||
|
<Text c="dimmed" size="xs" tt="uppercase" fw={700}>
|
||||||
|
{storageType}
|
||||||
|
</Text>
|
||||||
|
{!path ? null :
|
||||||
|
<Text fw={700} size="md">
|
||||||
|
{path}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
<Flex w='100%' justify='space-around'>
|
||||||
|
<Flex direction='column'>
|
||||||
|
<Text>Used:</Text>
|
||||||
|
<Text>{formatMBytes(used)}</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex direction='column'>
|
||||||
|
<Text>Free:</Text>
|
||||||
|
<Text>{formatMBytes(free)}</Text>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StorageRingStat;
|
||||||
@ -1,6 +0,0 @@
|
|||||||
export function valueIsNotEmpty(value: any) {
|
|
||||||
if (value) {
|
|
||||||
return true
|
|
||||||
} else if( typeof value === 'boolean') return true
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
13
src/shared/utils/data.size.ts
Normal file
13
src/shared/utils/data.size.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
export const formatBytes = (bytes: number, decimals = 2): string => {
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
const k = 1024;
|
||||||
|
const dm = decimals < 0 ? 0 : decimals;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const formatMBytes = (mb: number, decimals?: number): string => {
|
||||||
|
const bytes = mb * 1024 * 1024;
|
||||||
|
return formatBytes(bytes, decimals);
|
||||||
|
}
|
||||||
@ -4,6 +4,26 @@ export const longToDate = (long: number): Date => new Date(long * 1000);
|
|||||||
export const epochToLong = (date: number): number => date / 1000;
|
export const epochToLong = (date: number): number => date / 1000;
|
||||||
export const dateToLong = (date: Date): number => epochToLong(date.getTime());
|
export const dateToLong = (date: Date): number => epochToLong(date.getTime());
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param uptimeInSeconds
|
||||||
|
* @returns value: number, unit: 'day' / 'hour' / 'minute' / 'second'
|
||||||
|
*/
|
||||||
|
export const formatUptime = (uptimeInSeconds: number) => {
|
||||||
|
const secondsInAMinute = 60
|
||||||
|
const secondsInAnHour = 3600
|
||||||
|
const secondsInADay = 86400
|
||||||
|
|
||||||
|
if (uptimeInSeconds >= secondsInADay) {
|
||||||
|
return { value: (uptimeInSeconds / secondsInADay), unit: 'day' };
|
||||||
|
} else if (uptimeInSeconds >= secondsInAnHour) {
|
||||||
|
return { value: (uptimeInSeconds / secondsInAnHour), unit: 'hour' };
|
||||||
|
} else if (uptimeInSeconds >= secondsInAMinute) {
|
||||||
|
return { value: (uptimeInSeconds / secondsInAMinute), unit: 'minute' };
|
||||||
|
} else {
|
||||||
|
return { value: uptimeInSeconds, unit: 'second' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const formatFileTimestamps = (startUnixTime: number, endUnixTime: number, cameraName: string) => {
|
export const formatFileTimestamps = (startUnixTime: number, endUnixTime: number, cameraName: string) => {
|
||||||
const formatTime = (time: number) => {
|
const formatTime = (time: number) => {
|
||||||
|
|||||||
@ -1,13 +0,0 @@
|
|||||||
export function debounce<T extends (...args: any[]) => any>(func: T, wait: number): (...funcArgs: Parameters<T>) => void {
|
|
||||||
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
return function(...args: Parameters<T>): void {
|
|
||||||
const later = () => {
|
|
||||||
timeout = null;
|
|
||||||
func(...args);
|
|
||||||
};
|
|
||||||
if (timeout !== null) {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
}
|
|
||||||
timeout = setTimeout(later, wait);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
import { MutableRefObject, useEffect, useMemo, useState } from "react";
|
|
||||||
|
|
||||||
export function useResizeObserver(...refs: MutableRefObject<Element | null>[]) {
|
|
||||||
const [dimensions, setDimensions] = useState(
|
|
||||||
new Array(refs.length).fill({
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
x: -Infinity,
|
|
||||||
y: -Infinity,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
const resizeObserver = useMemo(
|
|
||||||
() =>
|
|
||||||
new ResizeObserver((entries) => {
|
|
||||||
window.requestAnimationFrame(() => {
|
|
||||||
setDimensions(entries.map((entry) => entry.contentRect));
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
refs.forEach((ref) => {
|
|
||||||
if (ref.current) {
|
|
||||||
resizeObserver.observe(ref.current);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
refs.forEach((ref) => {
|
|
||||||
if (ref.current) {
|
|
||||||
resizeObserver.unobserve(ref.current);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}, [refs, resizeObserver]);
|
|
||||||
|
|
||||||
return dimensions;
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user