graph refactor
parent
b9a2be4860
commit
4dfd972615
|
|
@ -0,0 +1,97 @@
|
|||
import React from 'react';
|
||||
import DatePicker from 'react-datepicker';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
|
||||
const DateRangeSelector = ({
|
||||
startDate,
|
||||
endDate,
|
||||
onStartDateChange,
|
||||
onEndDateChange,
|
||||
onApply
|
||||
}) => {
|
||||
return (
|
||||
<div style={{
|
||||
marginTop: 10,
|
||||
backgroundColor: '#f5f5f5',
|
||||
padding: '15px',
|
||||
borderRadius: '4px'
|
||||
}}>
|
||||
<div style={{
|
||||
marginBottom: '10px',
|
||||
fontWeight: '500',
|
||||
color: '#555'
|
||||
}}>
|
||||
Укажите диапазон дат:
|
||||
</div>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: '10px',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'flex-end'
|
||||
}}>
|
||||
<div style={{ flex: '1 1 200px' }}>
|
||||
<DatePicker
|
||||
selected={startDate}
|
||||
onChange={onStartDateChange}
|
||||
showTimeSelect
|
||||
timeFormat="HH:mm"
|
||||
timeIntervals={15}
|
||||
dateFormat="yyyy-MM-dd HH:mm"
|
||||
placeholderText="Начальная дата"
|
||||
customInput={
|
||||
<input style={{
|
||||
backgroundColor: '#f9f9f9',
|
||||
color: "#555",
|
||||
width: '100%',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #ddd'
|
||||
}} />
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 200px' }}>
|
||||
<DatePicker
|
||||
selected={endDate}
|
||||
onChange={onEndDateChange}
|
||||
showTimeSelect
|
||||
timeFormat="HH:mm"
|
||||
timeIntervals={15}
|
||||
dateFormat="yyyy-MM-dd HH:mm"
|
||||
placeholderText="Конечная дата"
|
||||
customInput={
|
||||
<input style={{
|
||||
backgroundColor: '#f9f9f9',
|
||||
color: "#555",
|
||||
width: '100%',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #ddd'
|
||||
}} />
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={onApply}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
backgroundColor: '#4a6baf',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
transition: 'background-color 0.2s',
|
||||
flex: '0 0 auto',
|
||||
height: '36px'
|
||||
}}
|
||||
onMouseOver={(e) => e.target.style.backgroundColor = '#3a5a9f'}
|
||||
onMouseOut={(e) => e.target.style.backgroundColor = '#4a6baf'}
|
||||
>
|
||||
Применить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DateRangeSelector;
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
import React from 'react';
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
|
||||
|
||||
const LineChartComponent = ({
|
||||
data,
|
||||
title,
|
||||
description,
|
||||
metaInfo,
|
||||
dataKey = 'value',
|
||||
lineColor = '#8884d8',
|
||||
height = 400,
|
||||
showLegend = true,
|
||||
showGrid = true,
|
||||
customTooltip,
|
||||
customXAxisFormatter,
|
||||
customYAxis,
|
||||
additionalLines = []
|
||||
}) => {
|
||||
return (
|
||||
<div style={{ width: '100%', height: '560' }}>
|
||||
{title && <h3>{title}</h3>}
|
||||
{description && (
|
||||
<p style={{ marginTop: -10, color: '#666' }}>{description}</p>
|
||||
)}
|
||||
{metaInfo && (
|
||||
<div style={{ fontSize: 12, color: '#888', marginBottom: 10 }}>
|
||||
{metaInfo}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ResponsiveContainer width="100%" height="80%">
|
||||
<LineChart
|
||||
data={data}
|
||||
margin={{
|
||||
top: 5,
|
||||
right: 30,
|
||||
left: 20,
|
||||
bottom: 5,
|
||||
}}
|
||||
>
|
||||
{showGrid && <CartesianGrid strokeDasharray="3 3" />}
|
||||
<XAxis
|
||||
dataKey="timestamp"
|
||||
tickFormatter={customXAxisFormatter || ((timestamp) => new Date(timestamp).toLocaleTimeString())}
|
||||
/>
|
||||
{customYAxis || <YAxis />}
|
||||
<Tooltip
|
||||
content={customTooltip}
|
||||
labelFormatter={(timestamp) => new Date(timestamp).toLocaleString()}
|
||||
/>
|
||||
{showLegend && <Legend />}
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey={dataKey}
|
||||
stroke={lineColor}
|
||||
activeDot={{ r: 8 }}
|
||||
name={title}
|
||||
/>
|
||||
{additionalLines.map((lineProps, index) => (
|
||||
<Line key={index} {...lineProps} />
|
||||
))}
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LineChartComponent;
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
import { io } from 'socket.io-client';
|
||||
|
||||
class MetricsService {
|
||||
constructor(baseUrl) {
|
||||
console.log('MetricsService constructor');
|
||||
this.baseUrl = baseUrl || window.location.origin;
|
||||
this.socket = null;
|
||||
this.subscriptions = new Map();
|
||||
}
|
||||
|
||||
// HTTP методы - адаптированы под ваш бэкенд
|
||||
async fetchMetricsRange(metric, start, end, step = 15) {
|
||||
try {
|
||||
// Формируем URL согласно вашему API
|
||||
const url = new URL(`${this.baseUrl}/api/metrics`);
|
||||
url.searchParams.append('metric', metric);
|
||||
url.searchParams.append('start', start);
|
||||
url.searchParams.append('end', end);
|
||||
url.searchParams.append('step', step);
|
||||
|
||||
console.log('Fetching metrics range from:', url.toString());
|
||||
|
||||
const response = await fetch(url.toString());
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Проверяем формат данных
|
||||
if (!Array.isArray(data)) {
|
||||
console.error('Unexpected data format:', data);
|
||||
throw new Error('Invalid data format: expected array');
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error in fetchMetricsRange:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async fetchMetrics(metric) {
|
||||
try {
|
||||
// Формируем URL для текущих метрик
|
||||
const url = new URL(`${this.baseUrl}/api/metrics`);
|
||||
url.searchParams.append('metric', metric);
|
||||
|
||||
console.log('Fetching current metrics from:', url.toString());
|
||||
|
||||
const response = await fetch(url.toString());
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Проверяем формат данных
|
||||
if (!Array.isArray(data)) {
|
||||
console.error('Unexpected data format:', data);
|
||||
throw new Error('Invalid data format: expected array');
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error in fetchMetrics:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// WebSocket методы - остаются без изменений
|
||||
connectWebSocket() {
|
||||
if (this.socket && this.socket.connected) return;
|
||||
console.trace('connectWebSocket called');
|
||||
this.socket = io(`${this.baseUrl}/api/metrics-ws`, {
|
||||
transports: ['websocket'],
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
this.socket.on('connect', () => {
|
||||
console.log('Socket.IO connected');
|
||||
// Подписаться заново на все метрики
|
||||
for (const [metric, callbacks] of this.subscriptions.entries()) {
|
||||
this.socket.emit('subscribe-metric', { metric });
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.on('disconnect', () => {
|
||||
console.log('Socket.IO disconnected');
|
||||
});
|
||||
|
||||
this.socket.on('metrics-data', ({ metric, data }) => {
|
||||
const callbacks = this.subscriptions.get(metric) || [];
|
||||
callbacks.forEach(cb => cb(data));
|
||||
});
|
||||
|
||||
this.socket.on('metrics-error', payload => {
|
||||
console.error('Metrics error:', payload);
|
||||
});
|
||||
}
|
||||
|
||||
subscribeToMetric(metric, callback, interval = 5000) {
|
||||
this.connectWebSocket();
|
||||
|
||||
if (!this.subscriptions.has(metric)) {
|
||||
this.subscriptions.set(metric, []);
|
||||
this.socket.emit('subscribe-metric', { metric, interval });
|
||||
}
|
||||
|
||||
this.subscriptions.get(metric).push(callback);
|
||||
|
||||
return () => this.unsubscribeFromMetric(metric, callback);
|
||||
}
|
||||
|
||||
|
||||
unsubscribeFromMetric(metric, callback) {
|
||||
const callbacks = this.subscriptions.get(metric) || [];
|
||||
const filtered = callbacks.filter(cb => cb !== callback);
|
||||
|
||||
if (filtered.length === 0) {
|
||||
this.subscriptions.delete(metric);
|
||||
if (this.socket && this.socket.connected) {
|
||||
this.socket.emit('unsubscribe-metric', { metric });
|
||||
}
|
||||
} else {
|
||||
this.subscriptions.set(metric, filtered);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
disconnectWebSocket() {
|
||||
if (this.socket) {
|
||||
this.socket.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const metricsService = new MetricsService(import.meta.env.VITE_BACK_URL);
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import LineChartComponent from './Components/LineChartComponent';
|
||||
import DateRangeSelector from './Components/DateRangeSelector';
|
||||
import { metricsService } from './Components/metricsService';
|
||||
import { Button, Radio, message } from 'antd';
|
||||
import moment from 'moment';
|
||||
|
||||
const PrometheusChart = ({ metricName, chartHeight = 560 }) => {
|
||||
const [chartData, setChartData] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [metricInfo, setMetricInfo] = useState({});
|
||||
const [mode, setMode] = useState('realtime');
|
||||
const [startDate, setStartDate] = useState(moment().subtract(1, 'hour').toDate());
|
||||
const [endDate, setEndDate] = useState(moment().toDate());
|
||||
const [isLiveUpdating, setIsLiveUpdating] = useState(false);
|
||||
|
||||
const fetchHistoricalData = async (start, end) => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const startUnix = Math.floor(new Date(start).getTime() / 1000);
|
||||
const endUnix = Math.floor(new Date(end).getTime() / 1000);
|
||||
|
||||
const data = await metricsService.fetchMetricsRange(metricName, startUnix, endUnix, 15);
|
||||
|
||||
const dataArray = Array.isArray(data) ? data : [data];
|
||||
const formattedData = dataArray.map(item => ({
|
||||
timestamp: item.timestamp,
|
||||
value: parseFloat(item.value),
|
||||
name: item.__name__ || metricName,
|
||||
status: item.status
|
||||
}));
|
||||
|
||||
if (dataArray.length > 0) {
|
||||
setMetricInfo({
|
||||
type: dataArray[0].type,
|
||||
description: dataArray[0].description,
|
||||
instance: dataArray[0].instance,
|
||||
job: dataArray[0].job
|
||||
});
|
||||
}
|
||||
|
||||
setChartData(formattedData);
|
||||
} catch (err) {
|
||||
console.error(`Error loading historical data for ${metricName}:`, err);
|
||||
setError(err.message);
|
||||
message.error(`Failed to load historical data: ${err.message}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const startRealtimeUpdates = () => {
|
||||
setIsLiveUpdating(true);
|
||||
setIsLoading(true);
|
||||
|
||||
const end = new Date();
|
||||
const start = new Date(end.getTime() - 3600 * 1000);
|
||||
fetchHistoricalData(start, end).finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
|
||||
return metricsService.subscribeToMetric(
|
||||
metricName,
|
||||
(newData) => {
|
||||
const newDataArray = Array.isArray(newData) ? newData : [newData];
|
||||
const formattedNewData = newDataArray.map(item => ({
|
||||
timestamp: item.timestamp,
|
||||
value: parseFloat(item.value),
|
||||
name: item.__name__ || metricName,
|
||||
status: item.status
|
||||
}));
|
||||
|
||||
setChartData(prevData => [...prevData, ...formattedNewData].slice(-200));
|
||||
},
|
||||
5000
|
||||
);
|
||||
};
|
||||
|
||||
const stopRealtimeUpdates = () => {
|
||||
setIsLiveUpdating(false);
|
||||
metricsService.unsubscribeFromMetric(metricName);
|
||||
};
|
||||
|
||||
const handleCustomRangeApply = () => {
|
||||
if (startDate && endDate) {
|
||||
fetchHistoricalData(startDate, endDate);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let unsubscribe;
|
||||
|
||||
if (mode === 'realtime') {
|
||||
unsubscribe = startRealtimeUpdates();
|
||||
} else {
|
||||
stopRealtimeUpdates();
|
||||
fetchHistoricalData(startDate, endDate);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (unsubscribe) unsubscribe();
|
||||
stopRealtimeUpdates();
|
||||
};
|
||||
}, [mode, metricName]);
|
||||
|
||||
const metaInfo = [
|
||||
metricInfo.instance && `Instance: ${metricInfo.instance}`,
|
||||
metricInfo.job && `Job: ${metricInfo.job}`,
|
||||
metricInfo.type && `Type: ${metricInfo.type}`
|
||||
].filter(Boolean).join(' | ');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Radio.Group
|
||||
value={mode}
|
||||
onChange={(e) => setMode(e.target.value)}
|
||||
buttonStyle="solid"
|
||||
style={{ marginBottom: 10 }}
|
||||
>
|
||||
<Radio.Button value="realtime">Режим реального времени</Radio.Button>
|
||||
<Radio.Button value="historical">Исторические данные</Radio.Button>
|
||||
</Radio.Group>
|
||||
|
||||
{mode === 'historical' && (
|
||||
<DateRangeSelector
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onStartDateChange={setStartDate}
|
||||
onEndDateChange={setEndDate}
|
||||
onApply={handleCustomRangeApply}
|
||||
/>
|
||||
)}
|
||||
|
||||
{mode === 'realtime' && isLiveUpdating && (
|
||||
<Button
|
||||
type="primary"
|
||||
danger
|
||||
onClick={() => setMode('historical')}
|
||||
style={{ marginTop: 10 }}
|
||||
>
|
||||
Остановить обновление
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div>Loading chart data...</div>
|
||||
) : error ? (
|
||||
<div>Error loading metric: {error}</div>
|
||||
) : chartData.length === 0 ? (
|
||||
<div>No data available for {metricName}</div>
|
||||
) : (
|
||||
<LineChartComponent
|
||||
data={chartData}
|
||||
title={metricName}
|
||||
description={metricInfo.description}
|
||||
metaInfo={metaInfo}
|
||||
height={chartHeight}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PrometheusChart;
|
||||
|
|
@ -15,7 +15,7 @@ import { getStatusColor } from "../../TreeChart/dataUtils";
|
|||
const StyledListItem = styled(ListItem)(({ theme, level }) => ({
|
||||
cursor: "pointer",
|
||||
paddingLeft: theme.spacing(2 + level * 2),
|
||||
position: 'relative', // Добавляем для позиционирования индикатора
|
||||
position: 'relative',
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -45,12 +45,10 @@ const FlowChart = ({ data }) => {
|
|||
const findAndCollapseLastLevelParents = (items) => {
|
||||
items.forEach(item => {
|
||||
if (item.items && item.items.length > 0) {
|
||||
// Проверяем, есть ли у детей свои дети
|
||||
const hasGrandchildren = item.items.some(child =>
|
||||
child.items && child.items.length > 0
|
||||
);
|
||||
|
||||
// Если у детей нет своих детей - это родители последнего уровня
|
||||
if (!hasGrandchildren) {
|
||||
toggleNodeCollapse(item.id);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export const useDataParser = (nodePositions, collapsedNodes) => {
|
|||
const baseLevelRadius = 150;
|
||||
|
||||
const traverse = (item, parentId = null, level = 0, angleStart = 0, angleEnd = 2 * Math.PI, parentRadius = 0) => {
|
||||
if (!item || collapsedNodes[parentId]) return; // Пропускаем свёрнутые узлы
|
||||
if (!item || collapsedNodes[parentId]) return;
|
||||
|
||||
const nodeId = item.id;
|
||||
const items = item.items || [];
|
||||
|
|
@ -58,7 +58,7 @@ export const useDataParser = (nodePositions, collapsedNodes) => {
|
|||
data: {
|
||||
...item,
|
||||
label: item.title,
|
||||
style: getNodeStyle(item, isLeaf), // Переносим стили в data
|
||||
style: getNodeStyle(item, isLeaf),
|
||||
hasChildren: items.length > 0,
|
||||
collapsed: collapsedNodes[nodeId]
|
||||
}
|
||||
|
|
@ -88,7 +88,7 @@ export const useDataParser = (nodePositions, collapsedNodes) => {
|
|||
|
||||
const centerNode = {
|
||||
id: data.id,
|
||||
type: 'customNode', // Добавляем тип узла
|
||||
type: 'customNode',
|
||||
position: nodePositions[data.id] || { x: centerX, y: centerY },
|
||||
style: getCenterNodeStyle(data),
|
||||
data: { label: data.title, hasChildren: data.items.length > 0, collapsed: collapsedNodes[data.id] }
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@ const NodeWrapper = memo(({ id, data, selected }) => {
|
|||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden', // Чтобы текст не выходил за границы
|
||||
textOverflow: 'ellipsis', // Добавляем многоточие если текст не помещается
|
||||
whiteSpace: 'nowrap', // Запрещаем перенос строк
|
||||
padding: '0 8px', // Горизонтальный padding для текста
|
||||
boxSizing: 'border-box' // Учитываем padding в общей ширине
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
padding: '0 8px',
|
||||
boxSizing: 'border-box'
|
||||
}}
|
||||
title={data.label} // Простой tooltip при наведении
|
||||
title={data.label}
|
||||
>
|
||||
{/* Хендл для входящих соединений */}
|
||||
<Handle
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export const useFlowChart = (initialData) => {
|
|||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
const [nodePositions, setNodePositions] = useState({});
|
||||
const [collapsedNodes, setCollapsedNodes] = useState({}); // Добавили
|
||||
const [collapsedNodes, setCollapsedNodes] = useState({});
|
||||
|
||||
const toggleNodeCollapse = useCallback((nodeId) => {
|
||||
setCollapsedNodes((prev) => ({
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { useCallback } from 'react';
|
|||
|
||||
export const useNodeHandlers = (debouncedSetNodePositions) => {
|
||||
const onNodeDrag = useCallback((event, node) => {
|
||||
// Фиксируем позицию сразу при перемещении
|
||||
node.position = {
|
||||
x: Math.round(node.position.x),
|
||||
y: Math.round(node.position.y)
|
||||
|
|
|
|||
|
|
@ -1,48 +1,43 @@
|
|||
const StatusManager = () => {
|
||||
const getRandomStatus = () => {
|
||||
const statuses = [
|
||||
...Array(90).fill("green"), // 90% шанс
|
||||
...Array(6).fill("yellow"), // 6% шанс
|
||||
...Array(3).fill("orange"), // 3% шанс
|
||||
...Array(1).fill("red"), // 1% шанс
|
||||
...Array(90).fill("green"),
|
||||
...Array(6).fill("yellow"),
|
||||
...Array(3).fill("orange"),
|
||||
...Array(1).fill("red"),
|
||||
];
|
||||
return statuses[Math.floor(Math.random() * statuses.length)];
|
||||
};
|
||||
|
||||
const getStatusWeight = (status) => {
|
||||
switch (status) {
|
||||
case "green": return 1; // 100% здоровья
|
||||
case "green": return 1;
|
||||
case "yellow": return 0.75;
|
||||
case "orange": return 0.5;
|
||||
case "red": return 0.25; // 25% здоровья
|
||||
default: return 1; // По умолчанию "green"
|
||||
case "red": return 0.25;
|
||||
default: return 1;
|
||||
}
|
||||
};
|
||||
|
||||
const updateStatuses = (data) => {
|
||||
if (!data.items || data.items.length === 0) {
|
||||
// Если это элемент нижнего уровня, генерируем случайный статус
|
||||
data.status = getRandomStatus();
|
||||
return getStatusWeight(data.status);
|
||||
}
|
||||
|
||||
// Рекурсивно обновляем статусы для всех дочерних элементов
|
||||
let childStatusWeights = data.items.map((child) => updateStatuses(child));
|
||||
|
||||
// Проверяем, есть ли дочерние элементы (избегаем деления на 0)
|
||||
if (childStatusWeights.length === 0) {
|
||||
data.status = "green";
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Вычисляем среднее арифметическое значение весов статусов
|
||||
const averageStatusWeight =
|
||||
childStatusWeights.reduce((sum, weight) => sum + weight, 0) / childStatusWeights.length;
|
||||
|
||||
// Определяем статус текущего элемента
|
||||
data.status = getStatusFromWeight(averageStatusWeight);
|
||||
|
||||
return Math.max(0, averageStatusWeight); // Гарантия, что не будет отрицательных значений
|
||||
return Math.max(0, averageStatusWeight);
|
||||
};
|
||||
|
||||
const getStatusFromWeight = (weight) => {
|
||||
|
|
@ -69,16 +64,13 @@ const StatusManager = () => {
|
|||
};
|
||||
};
|
||||
|
||||
// Создаем два независимых менеджера статусов
|
||||
export const statusManager1 = StatusManager();
|
||||
export const statusManager2 = StatusManager();
|
||||
|
||||
// Функция для расчета процентов здоровья системы
|
||||
export const calculateStatusPercentage = (averageStatusValue) => {
|
||||
return Math.max(0, Math.min(100, averageStatusValue * 100));
|
||||
};
|
||||
|
||||
// Экспортируем getStatusColor отдельно
|
||||
export const getStatusColor = (status) => {
|
||||
switch (status) {
|
||||
case "green":
|
||||
|
|
|
|||
|
|
@ -400,182 +400,136 @@
|
|||
"items": [
|
||||
{
|
||||
"id": "182",
|
||||
"title": "Graviton S2082I (device$18)",
|
||||
"title": "Graviton S2082I (device$19)",
|
||||
"items": [
|
||||
{
|
||||
"id": "42",
|
||||
"title": "OS Linux (module$4) АО",
|
||||
"title": "OS Linux (module$6) АО",
|
||||
"items": [
|
||||
{
|
||||
"id": "1902",
|
||||
"id": "371",
|
||||
"title": "Загрузка процессора за 1 минуту"
|
||||
},
|
||||
{
|
||||
"id": "1912",
|
||||
"id": "372",
|
||||
"title": "Загрузка процессора за 5 минут"
|
||||
},
|
||||
{
|
||||
"id": "1922",
|
||||
"id": "373",
|
||||
"title": "Загрузка процессора за 15 минут"
|
||||
},
|
||||
{
|
||||
"id": "1972",
|
||||
"id": "378",
|
||||
"title": "Общий объем SWAP-файла"
|
||||
},
|
||||
{
|
||||
"id": "1982",
|
||||
"id": "379",
|
||||
"title": "Используемый объем SWAP-файла"
|
||||
},
|
||||
{
|
||||
"id": "1992",
|
||||
"id": "380",
|
||||
"title": "Общий объем физической оперативной памяти"
|
||||
},
|
||||
{
|
||||
"id": "2002",
|
||||
"id": "381",
|
||||
"title": "Доступный объем физической оперативной памяти"
|
||||
},
|
||||
{
|
||||
"id": "2012",
|
||||
"id": "382",
|
||||
"title": "Свободный объем физической и виртуальной оперативной памяти"
|
||||
},
|
||||
{
|
||||
"id": "2022",
|
||||
"id": "383",
|
||||
"title": "Буферизованный объем оперативной памяти"
|
||||
},
|
||||
{
|
||||
"id": "2032",
|
||||
"id": "384",
|
||||
"title": "Кэшированый объем оперативной памяти"
|
||||
},
|
||||
{
|
||||
"id": "2742",
|
||||
"title": "Используемый объем SWAP-файла"
|
||||
},
|
||||
{
|
||||
"id": "2752",
|
||||
"id": "375",
|
||||
"title": "Время затраченное процессором на процессы с пониженным приоритетом"
|
||||
},
|
||||
{
|
||||
"id": "2762",
|
||||
"id": "376",
|
||||
"title": "Время затраченное процессором на процессы ядра ОС"
|
||||
},
|
||||
{
|
||||
"id": "2772",
|
||||
"id": "377",
|
||||
"title": "Время простоя процессора"
|
||||
},
|
||||
{
|
||||
"id": "2782",
|
||||
"id": "385",
|
||||
"title": "Общая емкость жестких дисков"
|
||||
},
|
||||
{
|
||||
"id": "2792",
|
||||
"id": "386",
|
||||
"title": "Доступная емкость жестких дисков"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "52",
|
||||
"title": "Vinteo (module$5) ПО",
|
||||
"items": [
|
||||
{
|
||||
"id": "312",
|
||||
"title": "Общее количество участников"
|
||||
},
|
||||
{
|
||||
"id": "322",
|
||||
"title": "Ожидание соединения"
|
||||
},
|
||||
{
|
||||
"id": "332",
|
||||
"title": "Зарегистрированные абоненты"
|
||||
},
|
||||
{
|
||||
"id": "342",
|
||||
"title": "Количество пользоватей HLS"
|
||||
},
|
||||
{
|
||||
"id": "352",
|
||||
"title": "Общее количество P2P комнат"
|
||||
},
|
||||
{
|
||||
"id": "362",
|
||||
"title": "Общее количество конференций"
|
||||
},
|
||||
{
|
||||
"id": "372",
|
||||
"title": "Общее количество активных конференций"
|
||||
},
|
||||
{
|
||||
"id": "382",
|
||||
"title": "Статус записи"
|
||||
},
|
||||
{
|
||||
"id": "392",
|
||||
"title": "Общее количество сохранённых записей"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "2802",
|
||||
"title": "Сетевой адаптер №1 (port$261) Eth_1",
|
||||
"items": [
|
||||
{
|
||||
"id": "2072",
|
||||
"id": "388",
|
||||
"title": "Скорость порта Eth_1"
|
||||
},
|
||||
{
|
||||
"id": "2092",
|
||||
"id": "390",
|
||||
"title": "Административное состояние порта Eth_1"
|
||||
},
|
||||
{
|
||||
"id": "2102",
|
||||
"id": "391",
|
||||
"title": "Оперативное состояние порта Eth_1"
|
||||
},
|
||||
{
|
||||
"id": "2112",
|
||||
"id": "392",
|
||||
"title": "Общее количество отправленных октетов Eth_1"
|
||||
},
|
||||
{
|
||||
"id": "2122",
|
||||
"id": "393",
|
||||
"title": "Количество входящих Multicast пакетов Eth_1"
|
||||
},
|
||||
{
|
||||
"id": "2132",
|
||||
"id": "394",
|
||||
"title": "Количество иcходящих Multiicast пакетов Eth_1"
|
||||
},
|
||||
{
|
||||
"id": "2142",
|
||||
"id": "395",
|
||||
"title": "Количество входящих Broadcast пакетов Eth_1"
|
||||
},
|
||||
{
|
||||
"id": "2152",
|
||||
"id": "396",
|
||||
"title": "Количество иcходящих Broadcast пакетов Eth_1"
|
||||
},
|
||||
{
|
||||
"id": "2162",
|
||||
"id": "397",
|
||||
"title": "Количество входящих Unicast пакетов Eth_1"
|
||||
},
|
||||
{
|
||||
"id": "2172",
|
||||
"id": "398",
|
||||
"title": "Количество иcходящих Unicast пакетов Eth_1"
|
||||
},
|
||||
{
|
||||
"id": "2182",
|
||||
"id": "399",
|
||||
"title": "Количество входящих пакетов помеченные как отброшенные Eth_1"
|
||||
},
|
||||
{
|
||||
"id": "2192",
|
||||
"id": "400",
|
||||
"title": "Количество иcходящих пакетов помеченные как отброшенные Eth_1"
|
||||
},
|
||||
{
|
||||
"id": "2202",
|
||||
"id": "401",
|
||||
"title": "Количество входящих пакетов с ошибкой Eth_1"
|
||||
},
|
||||
{
|
||||
"id": "2212",
|
||||
"id": "402",
|
||||
"title": "Количество исходящих пакетов с ошибкой Eth_1"
|
||||
},
|
||||
{
|
||||
"id": "2222",
|
||||
"id": "403",
|
||||
"title": "Количество входящих пакетов с неизвестным или неподдерживаемым протоколом Eth_1"
|
||||
}
|
||||
]
|
||||
|
|
@ -585,63 +539,63 @@
|
|||
"title": "Сетевой адаптер №2 (port$262) Eth_2",
|
||||
"items": [
|
||||
{
|
||||
"id": "2242",
|
||||
"id": "405",
|
||||
"title": "Скорость порта Eth_2"
|
||||
},
|
||||
{
|
||||
"id": "2262",
|
||||
"id": "407",
|
||||
"title": "Административное состояние порта Eth_2"
|
||||
},
|
||||
{
|
||||
"id": "2272",
|
||||
"id": "408",
|
||||
"title": "Оперативное состояние порта Eth_2"
|
||||
},
|
||||
{
|
||||
"id": "2282",
|
||||
"id": "409",
|
||||
"title": "Общее количество отправленных октетов Eth_2"
|
||||
},
|
||||
{
|
||||
"id": "2292",
|
||||
"id": "410",
|
||||
"title": "Количество входящих Multicast пакетов Eth_2"
|
||||
},
|
||||
{
|
||||
"id": "2302",
|
||||
"id": "411",
|
||||
"title": "Количество иcходящих Multiicast пакетов Eth_2"
|
||||
},
|
||||
{
|
||||
"id": "2312",
|
||||
"id": "412",
|
||||
"title": "Количество входящих Broadcast пакетов Eth_2"
|
||||
},
|
||||
{
|
||||
"id": "2322",
|
||||
"id": "413",
|
||||
"title": "Количество иcходящих Broadcast пакетов Eth_2"
|
||||
},
|
||||
{
|
||||
"id": "2332",
|
||||
"id": "414",
|
||||
"title": "Количество входящих Unicast пакетов Eth_2"
|
||||
},
|
||||
{
|
||||
"id": "2342",
|
||||
"id": "415",
|
||||
"title": "Количество иcходящих Unicast пакетов Eth_2"
|
||||
},
|
||||
{
|
||||
"id": "2352",
|
||||
"id": "416",
|
||||
"title": "Количество входящих пакетов помеченные как отброшенные Eth_2"
|
||||
},
|
||||
{
|
||||
"id": "2362",
|
||||
"id": "417",
|
||||
"title": "Количество иcходящих пакетов помеченные как отброшенные Eth_2"
|
||||
},
|
||||
{
|
||||
"id": "2372",
|
||||
"id": "418",
|
||||
"title": "Количество входящих пакетов с ошибкой Eth_2"
|
||||
},
|
||||
{
|
||||
"id": "2382",
|
||||
"id": "419",
|
||||
"title": "Количество исходящих пакетов с ошибкой Eth_2"
|
||||
},
|
||||
{
|
||||
"id": "2392",
|
||||
"id": "420",
|
||||
"title": "Количество входящих пакетов с неизвестным или неподдерживаемым протоколом Eth_2"
|
||||
}
|
||||
]
|
||||
|
|
@ -651,63 +605,63 @@
|
|||
"title": "Сетевой адаптер №3 (port$263) Eth_3",
|
||||
"items": [
|
||||
{
|
||||
"id": "2412",
|
||||
"id": "422",
|
||||
"title": "Скорость порта Eth_3"
|
||||
},
|
||||
{
|
||||
"id": "2432",
|
||||
"id": "424",
|
||||
"title": "Административное состояние порта Eth_3"
|
||||
},
|
||||
{
|
||||
"id": "2442",
|
||||
"id": "425",
|
||||
"title": "Оперативное состояние порта Eth_3"
|
||||
},
|
||||
{
|
||||
"id": "2452",
|
||||
"id": "426",
|
||||
"title": "Общее количество отправленных октетов Eth_3"
|
||||
},
|
||||
{
|
||||
"id": "2462",
|
||||
"id": "427",
|
||||
"title": "Количество входящих Multicast пакетов Eth_3"
|
||||
},
|
||||
{
|
||||
"id": "2472",
|
||||
"id": "428",
|
||||
"title": "Количество иcходящих Multiicast пакетов Eth_3"
|
||||
},
|
||||
{
|
||||
"id": "2482",
|
||||
"id": "429",
|
||||
"title": "Количество входящих Broadcast пакетов Eth_3"
|
||||
},
|
||||
{
|
||||
"id": "2492",
|
||||
"id": "430",
|
||||
"title": "Количество иcходящих Broadcast пакетов Eth_3"
|
||||
},
|
||||
{
|
||||
"id": "2502",
|
||||
"id": "431",
|
||||
"title": "Количество входящих Unicast пакетов Eth_3"
|
||||
},
|
||||
{
|
||||
"id": "2512",
|
||||
"id": "432",
|
||||
"title": "Количество иcходящих Unicast пакетов Eth_3"
|
||||
},
|
||||
{
|
||||
"id": "2522",
|
||||
"id": "433",
|
||||
"title": "Количество входящих пакетов помеченные как отброшенные Eth_3"
|
||||
},
|
||||
{
|
||||
"id": "2532",
|
||||
"id": "434",
|
||||
"title": "Количество иcходящих пакетов помеченные как отброшенные Eth_3"
|
||||
},
|
||||
{
|
||||
"id": "2542",
|
||||
"id": "435",
|
||||
"title": "Количество входящих пакетов с ошибкой Eth_3"
|
||||
},
|
||||
{
|
||||
"id": "2552",
|
||||
"id": "436",
|
||||
"title": "Количество исходящих пакетов с ошибкой Eth_3"
|
||||
},
|
||||
{
|
||||
"id": "2562",
|
||||
"id": "437",
|
||||
"title": "Количество входящих пакетов с неизвестным или неподдерживаемым протоколом Eth_3"
|
||||
}
|
||||
]
|
||||
|
|
@ -717,63 +671,63 @@
|
|||
"title": "Сетевой адаптер №4 (port$264) Eth_4",
|
||||
"items": [
|
||||
{
|
||||
"id": "2582",
|
||||
"id": "439",
|
||||
"title": "Скорость порта Eth_4"
|
||||
},
|
||||
{
|
||||
"id": "2602",
|
||||
"id": "441",
|
||||
"title": "Административное состояние порта Eth_4"
|
||||
},
|
||||
{
|
||||
"id": "2612",
|
||||
"id": "442",
|
||||
"title": "Оперативное состояние порта Eth_4"
|
||||
},
|
||||
{
|
||||
"id": "2622",
|
||||
"id": "443",
|
||||
"title": "Общее количество отправленных октетов Eth_4"
|
||||
},
|
||||
{
|
||||
"id": "2632",
|
||||
"id": "444",
|
||||
"title": "Количество входящих Multicast пакетов Eth_4"
|
||||
},
|
||||
{
|
||||
"id": "2642",
|
||||
"id": "445",
|
||||
"title": "Количество иcходящих Multiicast пакетов Eth_4"
|
||||
},
|
||||
{
|
||||
"id": "2652",
|
||||
"id": "446",
|
||||
"title": "Количество входящих Broadcast пакетов Eth_4"
|
||||
},
|
||||
{
|
||||
"id": "2662",
|
||||
"id": "447",
|
||||
"title": "Количество иcходящих Broadcast пакетов Eth_4"
|
||||
},
|
||||
{
|
||||
"id": "2672",
|
||||
"id": "448",
|
||||
"title": "Количество входящих Unicast пакетов Eth_4"
|
||||
},
|
||||
{
|
||||
"id": "2682",
|
||||
"id": "449",
|
||||
"title": "Количество иcходящих Unicast пакетов Eth_4"
|
||||
},
|
||||
{
|
||||
"id": "2692",
|
||||
"id": "450",
|
||||
"title": "Количество входящих пакетов помеченные как отброшенные Eth_4"
|
||||
},
|
||||
{
|
||||
"id": "2702",
|
||||
"id": "451",
|
||||
"title": "Количество иcходящих пакетов помеченные как отброшенные Eth_4"
|
||||
},
|
||||
{
|
||||
"id": "2712",
|
||||
"id": "452",
|
||||
"title": "Количество входящих пакетов с ошибкой Eth_4"
|
||||
},
|
||||
{
|
||||
"id": "2722",
|
||||
"id": "453",
|
||||
"title": "Количество исходящих пакетов с ошибкой Eth_4"
|
||||
},
|
||||
{
|
||||
"id": "2732",
|
||||
"id": "454",
|
||||
"title": "Количество входящих пакетов с неизвестным или неподдерживаемым протоколом Eth_4"
|
||||
}
|
||||
]
|
||||
|
|
@ -889,15 +843,15 @@
|
|||
"title": "Общее количество конференций"
|
||||
},
|
||||
{
|
||||
"id": "373",
|
||||
"id": "373000",
|
||||
"title": "Общее количество активных конференций"
|
||||
},
|
||||
{
|
||||
"id": "383",
|
||||
"id": "38300",
|
||||
"title": "Статус записи"
|
||||
},
|
||||
{
|
||||
"id": "393",
|
||||
"id": "39300",
|
||||
"title": "Общее количество сохранённых записей"
|
||||
}
|
||||
]
|
||||
|
|
@ -1281,11 +1235,11 @@
|
|||
"title": "Общее количество активных конференций"
|
||||
},
|
||||
{
|
||||
"id": "384",
|
||||
"id": "38400",
|
||||
"title": "Статус записи"
|
||||
},
|
||||
{
|
||||
"id": "394",
|
||||
"id": "39400",
|
||||
"title": "Общее количество сохранённых записей"
|
||||
}
|
||||
]
|
||||
|
|
@ -1671,7 +1625,7 @@
|
|||
"title": "Общее количество конференций"
|
||||
},
|
||||
{
|
||||
"id": "379",
|
||||
"id": "37900",
|
||||
"title": "Общее количество активных конференций"
|
||||
},
|
||||
{
|
||||
|
|
@ -1679,7 +1633,7 @@
|
|||
"title": "Статус записи"
|
||||
},
|
||||
{
|
||||
"id": "399",
|
||||
"id": "39900",
|
||||
"title": "Общее количество сохранённых записей"
|
||||
}
|
||||
]
|
||||
|
|
@ -2447,15 +2401,15 @@
|
|||
"title": "Общее количество конференций"
|
||||
},
|
||||
{
|
||||
"id": "378",
|
||||
"id": "37800",
|
||||
"title": "Общее количество активных конференций"
|
||||
},
|
||||
{
|
||||
"id": "388",
|
||||
"id": "38800",
|
||||
"title": "Статус записи"
|
||||
},
|
||||
{
|
||||
"id": "398",
|
||||
"id": "39800",
|
||||
"title": "Общее количество сохранённых записей"
|
||||
}
|
||||
]
|
||||
|
|
@ -2841,15 +2795,15 @@
|
|||
"title": "Общее количество конференций"
|
||||
},
|
||||
{
|
||||
"id": "375",
|
||||
"id": "37500",
|
||||
"title": "Общее количество активных конференций"
|
||||
},
|
||||
{
|
||||
"id": "385",
|
||||
"id": "38500",
|
||||
"title": "Статус записи"
|
||||
},
|
||||
{
|
||||
"id": "395",
|
||||
"id": "39500",
|
||||
"title": "Общее количество сохранённых записей"
|
||||
}
|
||||
]
|
||||
|
|
@ -3229,15 +3183,15 @@
|
|||
"title": "Общее количество конференций"
|
||||
},
|
||||
{
|
||||
"id": "376",
|
||||
"id": "37600",
|
||||
"title": "Общее количество активных конференций"
|
||||
},
|
||||
{
|
||||
"id": "386",
|
||||
"id": "38600",
|
||||
"title": "Статус записи"
|
||||
},
|
||||
{
|
||||
"id": "396",
|
||||
"id": "39600",
|
||||
"title": "Общее количество сохранённых записей"
|
||||
}
|
||||
]
|
||||
|
|
@ -3617,7 +3571,7 @@
|
|||
"title": "Общее количество конференций"
|
||||
},
|
||||
{
|
||||
"id": "377",
|
||||
"id": "37700",
|
||||
"title": "Общее количество активных конференций"
|
||||
},
|
||||
{
|
||||
|
|
@ -3625,7 +3579,7 @@
|
|||
"title": "Статус записи"
|
||||
},
|
||||
{
|
||||
"id": "397",
|
||||
"id": "39700",
|
||||
"title": "Общее количество сохранённых записей"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@ import React, { lazy, Suspense } from "react";
|
|||
import Skeleton from '@mui/material/Skeleton';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
const PrometheusChart = lazy(() => import('../../Charts/PrometheusChart'));
|
||||
const PrometheusChart = lazy(() => import('../../Charts2/PrometheusChart'));
|
||||
import LazyChartBatchRenderer from "../hooks/LazyChartBatchRender";
|
||||
|
||||
// Функция для генерации названия метрики на основе id
|
||||
const getMetricName = (id) => {
|
||||
return `zvks_apiforsnmp_measure_${id}`;
|
||||
};
|
||||
|
|
@ -12,28 +13,29 @@ const getMetricName = (id) => {
|
|||
const getAllChildIds = (node) => {
|
||||
let ids = [];
|
||||
if (node.id) {
|
||||
ids.push(node.id);
|
||||
ids.push(node.id);
|
||||
}
|
||||
if (node.items && node.items.length > 0) {
|
||||
node.items.forEach((child) => {
|
||||
ids = ids.concat(getAllChildIds(child));
|
||||
ids = ids.concat(getAllChildIds(child));
|
||||
});
|
||||
}
|
||||
return ids;
|
||||
};
|
||||
|
||||
// Компонент Skeleton для графика
|
||||
const ChartSkeleton = () => (
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Skeleton variant="text" width="60%" height={30} /> {/* Заголовок */}
|
||||
<Skeleton variant="rectangular" width="100%" height={300} sx={{ mt: 2 }} /> {/* График */}
|
||||
<Skeleton variant="text" width="60%" height={30} />
|
||||
<Skeleton variant="rectangular" width="100%" height={300} sx={{ mt: 2 }} />
|
||||
</Box>
|
||||
);
|
||||
|
||||
// Компонент Skeleton для родительского контейнера
|
||||
const ContainerSkeleton = () => (
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Skeleton variant="text" width="40%" height={40} /> {/* Заголовок */}
|
||||
<Skeleton variant="text" width="80%" height={20} sx={{ mt: 1 }} /> {/* Описание */}
|
||||
{/* Место для дочерних элементов */}
|
||||
<Skeleton variant="text" width="80%" height={20} sx={{ mt: 1 }} />
|
||||
<Box sx={{ mt: 2 }}>
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<ChartSkeleton key={i} />
|
||||
|
|
@ -42,24 +44,22 @@ const ContainerSkeleton = () => (
|
|||
</Box>
|
||||
);
|
||||
|
||||
const tabContent = (data, existingContent = {}) => {
|
||||
const tabContent = { ...existingContent };
|
||||
const tabContent = (data) => {
|
||||
const tabContent = {};
|
||||
|
||||
// Функция для рекурсивного обхода и сбора данных
|
||||
const generateContent = (nodes) => {
|
||||
nodes.forEach((node) => {
|
||||
if (tabContent[node.id]) return;
|
||||
|
||||
if (node.items && node.items.length > 0) {
|
||||
generateContent(node.items);
|
||||
const childrenContent = generateContent(node.items);
|
||||
|
||||
const content = (
|
||||
<div key={node.id}>
|
||||
<div>
|
||||
<h2>{node.title}</h2>
|
||||
<Suspense fallback={<ContainerSkeleton />}>
|
||||
<LazyChartBatchRenderer
|
||||
charts={node.items.map(child => tabContent[child.id]?.content) || <ChartSkeleton />}
|
||||
/>
|
||||
<LazyChartBatchRenderer charts={node.items.map((child) => tabContent[child.id].content)} />
|
||||
</Suspense>
|
||||
<p>Контент для {node.title}.</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
@ -77,17 +77,26 @@ const tabContent = (data, existingContent = {}) => {
|
|||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
|
||||
tabContent[node.id] = {
|
||||
title: node.title,
|
||||
content: content,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
{nodes.map((node) => (
|
||||
<div key={node.id}>{tabContent[node.id].content}</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (data.items && data.items.length > 0) {
|
||||
generateContent(data.items);
|
||||
} else {
|
||||
console.warn("Данные отсутствуют или массив items пуст");
|
||||
}
|
||||
|
||||
return tabContent;
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ import { StrictMode } from 'react'
|
|||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.jsx'
|
||||
//import './Style/light-theme.css'; // Подключаем светлую тему по умолчанию
|
||||
//import './Style/dark-theme.css'; // Подключаем темную тему
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
|
|
|
|||
Loading…
Reference in New Issue