rc #50
|
|
@ -28,7 +28,9 @@
|
|||
"vite-plugin-svgr": "^4.3.0",
|
||||
"react-scripts": "^5.0.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"antd": "^5.24.7"
|
||||
"antd": "^5.24.7",
|
||||
"react-window": "1.8.11",
|
||||
"react-virtualized-auto-sizer": "1.0.26"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.17.0",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
TextField, Box, Typography, IconButton, Divider,
|
||||
CircularProgress, Alert, Collapse, Tooltip, Button
|
||||
|
|
@ -7,6 +7,81 @@ import DeleteIcon from '@mui/icons-material/Delete';
|
|||
import AddIcon from '@mui/icons-material/Add';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import axios from 'axios';
|
||||
import { VariableSizeList as List } from 'react-window';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
|
||||
const MetricItem = React.memo(({ metric, index, updateRange, addRange, deleteRange }) => {
|
||||
return (
|
||||
<Box sx={{
|
||||
mb: 3,
|
||||
p: 2,
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
borderRadius: 2,
|
||||
backgroundColor: 'background.paper'
|
||||
}}>
|
||||
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
|
||||
{metric.name}
|
||||
</Typography>
|
||||
|
||||
{metric.ranges.map((r, j) => (
|
||||
<Box
|
||||
key={j}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
gap: 2,
|
||||
alignItems: 'center',
|
||||
mt: 1,
|
||||
'& > *': { flex: 1 }
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
label="Минимум"
|
||||
type="number"
|
||||
value={r.min}
|
||||
onChange={(e) => updateRange(index, j, 'min', e.target.value)}
|
||||
size="small"
|
||||
variant="standard"
|
||||
/>
|
||||
<TextField
|
||||
label="Максимум"
|
||||
type="number"
|
||||
value={r.max}
|
||||
onChange={(e) => updateRange(index, j, 'max', e.target.value)}
|
||||
size="small"
|
||||
variant="standard"
|
||||
/>
|
||||
<TextField
|
||||
label="Статус"
|
||||
type="number"
|
||||
value={r.status}
|
||||
onChange={(e) => updateRange(index, j, 'status', e.target.value)}
|
||||
size="small"
|
||||
variant="standard"
|
||||
/>
|
||||
<Tooltip title="Удалить диапазон">
|
||||
<IconButton
|
||||
onClick={() => deleteRange(index, j)}
|
||||
size="small"
|
||||
sx={{ flex: 'none' }}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
))}
|
||||
|
||||
<Button
|
||||
onClick={() => addRange(index)}
|
||||
startIcon={<AddIcon />}
|
||||
size="small"
|
||||
sx={{ mt: 1 }}
|
||||
>
|
||||
Добавить диапазон
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
const MetricRangeEditor = ({ onSave }) => {
|
||||
const [ranges, setRanges] = useState([]);
|
||||
|
|
@ -17,7 +92,6 @@ const MetricRangeEditor = ({ onSave }) => {
|
|||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
// Загрузка данных
|
||||
const loadRanges = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
|
@ -41,31 +115,53 @@ const MetricRangeEditor = ({ onSave }) => {
|
|||
loadRanges();
|
||||
}, [loadRanges]);
|
||||
|
||||
// Обновление диапазона
|
||||
const updateRange = (metricIndex, rangeIndex, field, value) => {
|
||||
const newRanges = [...ranges];
|
||||
newRanges[metricIndex].ranges[rangeIndex][field] = Number(value);
|
||||
setRanges(newRanges);
|
||||
const updateRange = useCallback((metricIndex, rangeIndex, field, value) => {
|
||||
setRanges(prev => {
|
||||
const newRanges = [...prev];
|
||||
newRanges[metricIndex] = {
|
||||
...newRanges[metricIndex],
|
||||
ranges: [...newRanges[metricIndex].ranges]
|
||||
};
|
||||
newRanges[metricIndex].ranges[rangeIndex] = {
|
||||
...newRanges[metricIndex].ranges[rangeIndex],
|
||||
[field]: Number(value)
|
||||
};
|
||||
return newRanges;
|
||||
});
|
||||
setHasChanges(true);
|
||||
}, []);
|
||||
|
||||
const getItemSize = (index) => {
|
||||
const baseHeight = 80;
|
||||
const rangeCount = filtered[index].ranges.length;
|
||||
return baseHeight + rangeCount * 56 + 40;
|
||||
};
|
||||
|
||||
// Добавление диапазона
|
||||
const addRange = (metricIndex) => {
|
||||
const newRanges = [...ranges];
|
||||
newRanges[metricIndex].ranges.push({ min: 0, max: 100, status: 1 });
|
||||
setRanges(newRanges);
|
||||
const addRange = useCallback((metricIndex) => {
|
||||
setRanges(prev => {
|
||||
const newRanges = [...prev];
|
||||
newRanges[metricIndex] = {
|
||||
...newRanges[metricIndex],
|
||||
ranges: [...newRanges[metricIndex].ranges, { min: 0, max: 100, status: 1 }]
|
||||
};
|
||||
return newRanges;
|
||||
});
|
||||
setHasChanges(true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Удаление диапазона
|
||||
const deleteRange = (metricIndex, rangeIndex) => {
|
||||
const newRanges = [...ranges];
|
||||
newRanges[metricIndex].ranges.splice(rangeIndex, 1);
|
||||
setRanges(newRanges);
|
||||
const deleteRange = useCallback((metricIndex, rangeIndex) => {
|
||||
setRanges(prev => {
|
||||
const newRanges = [...prev];
|
||||
newRanges[metricIndex] = {
|
||||
...newRanges[metricIndex],
|
||||
ranges: newRanges[metricIndex].ranges.filter((_, i) => i !== rangeIndex)
|
||||
};
|
||||
return newRanges;
|
||||
});
|
||||
setHasChanges(true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const saveChanges = async () => {
|
||||
const saveChanges = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await axios.post(`${import.meta.env.VITE_BACK_URL}/api/ranges/update`, ranges);
|
||||
|
|
@ -87,10 +183,9 @@ const MetricRangeEditor = ({ onSave }) => {
|
|||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, [ranges, onSave]);
|
||||
|
||||
// Добавление новой метрики
|
||||
const addNewMetric = () => {
|
||||
const addNewMetric = useCallback(() => {
|
||||
if (!newMetricName.trim()) {
|
||||
setError('Введите название метрики');
|
||||
return;
|
||||
|
|
@ -99,18 +194,26 @@ const MetricRangeEditor = ({ onSave }) => {
|
|||
setError('Метрика с таким именем уже существует');
|
||||
return;
|
||||
}
|
||||
setRanges([...ranges, {
|
||||
setRanges(prev => [...prev, {
|
||||
name: newMetricName,
|
||||
ranges: [{ min: 0, max: 100, status: 1 }]
|
||||
}]);
|
||||
setNewMetricName('');
|
||||
setHasChanges(true);
|
||||
setError(null);
|
||||
};
|
||||
}, [newMetricName, ranges]);
|
||||
|
||||
const filtered = filter
|
||||
? ranges.filter(r => r.name.toLowerCase().includes(filter.toLowerCase()))
|
||||
: ranges;
|
||||
const filtered = useMemo(() => {
|
||||
return filter
|
||||
? ranges.filter(r => r.name.toLowerCase().includes(filter.toLowerCase()))
|
||||
: ranges;
|
||||
}, [filter, ranges]);
|
||||
|
||||
useEffect(() => {
|
||||
if (onSave) {
|
||||
onSave({ hasChanges, saveChanges });
|
||||
}
|
||||
}, [hasChanges, onSave, saveChanges]);
|
||||
|
||||
return (
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
|
|
@ -166,76 +269,30 @@ const MetricRangeEditor = ({ onSave }) => {
|
|||
|
||||
<Divider sx={{ mb: 3 }} />
|
||||
|
||||
{filtered.map((metric, i) => (
|
||||
<Box
|
||||
key={metric.name}
|
||||
sx={{
|
||||
mb: 3,
|
||||
p: 2,
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
borderRadius: 2,
|
||||
backgroundColor: 'background.paper'
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
|
||||
{metric.name}
|
||||
</Typography>
|
||||
|
||||
{metric.ranges.map((r, j) => (
|
||||
<Box
|
||||
key={j}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
gap: 2,
|
||||
alignItems: 'center',
|
||||
mt: 1,
|
||||
'& > *': { flex: 1 }
|
||||
}}
|
||||
<Box sx={{ height: '60vh', width: '100%' }}>
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<List
|
||||
height={height}
|
||||
width={width}
|
||||
itemSize={getItemSize}
|
||||
itemCount={filtered.length}
|
||||
>
|
||||
<TextField
|
||||
label="Минимум"
|
||||
type="number"
|
||||
value={r.min}
|
||||
onChange={(e) => updateRange(i, j, 'min', e.target.value)}
|
||||
size="small"
|
||||
/>
|
||||
<TextField
|
||||
label="Максимум"
|
||||
type="number"
|
||||
value={r.max}
|
||||
onChange={(e) => updateRange(i, j, 'max', e.target.value)}
|
||||
size="small"
|
||||
/>
|
||||
<TextField
|
||||
label="Статус"
|
||||
type="number"
|
||||
value={r.status}
|
||||
onChange={(e) => updateRange(i, j, 'status', e.target.value)}
|
||||
size="small"
|
||||
/>
|
||||
<Tooltip title="Удалить диапазон">
|
||||
<IconButton
|
||||
onClick={() => deleteRange(i, j)}
|
||||
size="small"
|
||||
sx={{ flex: 'none' }}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
))}
|
||||
|
||||
<Button
|
||||
onClick={() => addRange(i)}
|
||||
startIcon={<AddIcon />}
|
||||
size="small"
|
||||
sx={{ mt: 1 }}
|
||||
>
|
||||
Добавить диапазон
|
||||
</Button>
|
||||
</Box>
|
||||
))}
|
||||
{({ index, style }) => (
|
||||
<Box style={style} sx={{ p: 1 }}>
|
||||
<MetricItem
|
||||
metric={filtered[index]}
|
||||
index={index}
|
||||
updateRange={updateRange}
|
||||
addRange={addRange}
|
||||
deleteRange={deleteRange}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</List>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</Box>
|
||||
|
||||
{filtered.length === 0 && (
|
||||
<Typography color="text.secondary" textAlign="center" py={3}>
|
||||
|
|
@ -244,15 +301,8 @@ const MetricRangeEditor = ({ onSave }) => {
|
|||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Передаем состояние изменений в родительский компонент */}
|
||||
{useEffect(() => {
|
||||
if (onSave) {
|
||||
onSave({ hasChanges, saveChanges });
|
||||
}
|
||||
}, [hasChanges, onSave])}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default MetricRangeEditor;
|
||||
export default React.memo(MetricRangeEditor);
|
||||
Loading…
Reference in New Issue