Compare commits

..

No commits in common. "b681add6fc95cd9ff421e194132fe9aee7e4127f" and "5e9e40aad25ec660837c0685aacb6edc23db07d4" have entirely different histories.

2 changed files with 106 additions and 158 deletions

View File

@ -28,9 +28,7 @@
"vite-plugin-svgr": "^4.3.0", "vite-plugin-svgr": "^4.3.0",
"react-scripts": "^5.0.1", "react-scripts": "^5.0.1",
"socket.io-client": "^4.8.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": { "devDependencies": {
"@eslint/js": "^9.17.0", "@eslint/js": "^9.17.0",

View File

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