Metric editor optimization #48

Merged
VladislavD merged 1 commits from redisign into rc 2025-07-04 10:59:28 +03:00
2 changed files with 158 additions and 106 deletions

View File

@ -28,7 +28,9 @@
"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 } from 'react'; import React, { useState, useEffect, useCallback, useMemo } 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,6 +7,81 @@ 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([]);
@ -17,7 +92,6 @@ 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);
@ -41,31 +115,53 @@ const MetricRangeEditor = ({ onSave }) => {
loadRanges(); loadRanges();
}, [loadRanges]); }, [loadRanges]);
// Обновление диапазона const updateRange = useCallback((metricIndex, rangeIndex, field, value) => {
const updateRange = (metricIndex, rangeIndex, field, value) => { setRanges(prev => {
const newRanges = [...ranges]; const newRanges = [...prev];
newRanges[metricIndex].ranges[rangeIndex][field] = Number(value); newRanges[metricIndex] = {
setRanges(newRanges); ...newRanges[metricIndex],
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) => {
const addRange = (metricIndex) => { setRanges(prev => {
const newRanges = [...ranges]; const newRanges = [...prev];
newRanges[metricIndex].ranges.push({ min: 0, max: 100, status: 1 }); newRanges[metricIndex] = {
setRanges(newRanges); ...newRanges[metricIndex],
ranges: [...newRanges[metricIndex].ranges, { min: 0, max: 100, status: 1 }]
};
return newRanges;
});
setHasChanges(true); setHasChanges(true);
}; }, []);
// Удаление диапазона const deleteRange = useCallback((metricIndex, rangeIndex) => {
const deleteRange = (metricIndex, rangeIndex) => { setRanges(prev => {
const newRanges = [...ranges]; const newRanges = [...prev];
newRanges[metricIndex].ranges.splice(rangeIndex, 1); newRanges[metricIndex] = {
setRanges(newRanges); ...newRanges[metricIndex],
ranges: newRanges[metricIndex].ranges.filter((_, i) => i !== rangeIndex)
};
return newRanges;
});
setHasChanges(true); setHasChanges(true);
}; }, []);
const saveChanges = async () => { const saveChanges = useCallback(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);
@ -87,10 +183,9 @@ 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;
@ -99,18 +194,26 @@ const MetricRangeEditor = ({ onSave }) => {
setError('Метрика с таким именем уже существует'); setError('Метрика с таким именем уже существует');
return; return;
} }
setRanges([...ranges, { setRanges(prev => [...prev, {
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 = filter const filtered = useMemo(() => {
? ranges.filter(r => r.name.toLowerCase().includes(filter.toLowerCase())) return filter
: ranges; ? ranges.filter(r => r.name.toLowerCase().includes(filter.toLowerCase()))
: ranges;
}, [filter, ranges]);
useEffect(() => {
if (onSave) {
onSave({ hasChanges, saveChanges });
}
}, [hasChanges, onSave, saveChanges]);
return ( return (
<Box sx={{ position: 'relative' }}> <Box sx={{ position: 'relative' }}>
@ -166,76 +269,30 @@ const MetricRangeEditor = ({ onSave }) => {
<Divider sx={{ mb: 3 }} /> <Divider sx={{ mb: 3 }} />
{filtered.map((metric, i) => ( <Box sx={{ height: '60vh', width: '100%' }}>
<Box <AutoSizer>
key={metric.name} {({ height, width }) => (
sx={{ <List
mb: 3, height={height}
p: 2, width={width}
border: '1px solid', itemSize={getItemSize}
borderColor: 'divider', itemCount={filtered.length}
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 {({ index, style }) => (
label="Минимум" <Box style={style} sx={{ p: 1 }}>
type="number" <MetricItem
value={r.min} metric={filtered[index]}
onChange={(e) => updateRange(i, j, 'min', e.target.value)} index={index}
size="small" updateRange={updateRange}
/> addRange={addRange}
<TextField deleteRange={deleteRange}
label="Максимум" />
type="number" </Box>
value={r.max} )}
onChange={(e) => updateRange(i, j, 'max', e.target.value)} </List>
size="small" )}
/> </AutoSizer>
<TextField </Box>
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>
))}
{filtered.length === 0 && ( {filtered.length === 0 && (
<Typography color="text.secondary" textAlign="center" py={3}> <Typography color="text.secondary" textAlign="center" py={3}>
@ -244,15 +301,8 @@ const MetricRangeEditor = ({ onSave }) => {
)} )}
</> </>
)} )}
{/* Передаем состояние изменений в родительский компонент */}
{useEffect(() => {
if (onSave) {
onSave({ hasChanges, saveChanges });
}
}, [hasChanges, onSave])}
</Box> </Box>
); );
}; };
export default MetricRangeEditor; export default React.memo(MetricRangeEditor);