trust-module-frontend/src/Components/Layout/SettingsComponents/MetricRangeEditor.jsx

322 lines
9.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import React, { useState, useEffect, useCallback, useMemo } from 'react';
import {
TextField, Box, Typography, IconButton, Divider,
CircularProgress, Alert, Collapse, Tooltip, Button, Select, MenuItem
} from '@mui/material';
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 { statusConfig } from './statusConfig';
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: 'flex-end', // Изменено с 'center' на 'flex-end'
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"
/>
<Select
label="Статус"
value={r.status}
onChange={(e) => updateRange(index, j, 'status', e.target.value)}
size="small"
variant="standard"
sx={{
// Добавляем вертикальное выравнивание для label
'& .MuiInputLabel-root': {
transform: 'translate(0, -20px) scale(0.75)'
},
// Корректируем положение выбранного значения
'& .MuiSelect-select': {
paddingBottom: '8px'
}
}}
>
{statusConfig.getAvailableStatuses().map(({ value, text }) => (
<MenuItem key={value} value={value}>
{text}
</MenuItem>
))}
</Select>
<Tooltip title="Удалить диапазон">
<IconButton
onClick={() => deleteRange(index, j)}
size="small"
sx={{
flex: 'none',
// Корректируем положение иконки
marginBottom: '8px'
}}
>
<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([]);
const [filter, setFilter] = useState('');
const [newMetricName, setNewMetricName] = useState('');
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [hasChanges, setHasChanges] = useState(false);
const [success, setSuccess] = useState(false);
const loadRanges = useCallback(async () => {
try {
setLoading(true);
const res = await axios.get(`${import.meta.env.VITE_BACK_URL}/api/ranges/list`);
setRanges(
Object.entries(res.data).map(([name, r]) => ({
name,
ranges: Array.isArray(r) ? r : []
}))
);
setError(null);
} catch (err) {
console.error('Ошибка при получении ranges:', err);
setError('Не удалось загрузить данные');
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
loadRanges();
}, [loadRanges]);
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 = 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 = 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 () => {
try {
setLoading(true);
await axios.post(`${import.meta.env.VITE_BACK_URL}/api/ranges/update`, ranges);
setHasChanges(false);
setSuccess(true);
setTimeout(() => setSuccess(false), 3000);
if (onSave) {
onSave({
hasChanges: false,
saveChanges: saveChanges
});
}
return true;
} catch (err) {
console.error('Ошибка при сохранении:', err);
setError('Ошибка при сохранении');
return false;
} finally {
setLoading(false);
}
}, [ranges, onSave]);
const addNewMetric = useCallback(() => {
if (!newMetricName.trim()) {
setError('Введите название метрики');
return;
}
if (ranges.some(r => r.name === newMetricName)) {
setError('Метрика с таким именем уже существует');
return;
}
setRanges(prev => [...prev, {
name: newMetricName,
ranges: [{ min: 0, max: 100, status: 1 }]
}]);
setNewMetricName('');
setHasChanges(true);
setError(null);
}, [newMetricName, 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' }}>
{loading && (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
)}
<Collapse in={!!error}>
<Alert severity="error" sx={{ mb: 2 }} onClose={() => setError(null)}>
{error}
</Alert>
</Collapse>
<Collapse in={success}>
<Alert severity="success" sx={{ mb: 2 }} onClose={() => setSuccess(false)}>
Изменения успешно сохранены!
</Alert>
</Collapse>
{!loading && (
<>
<Box sx={{ display: 'flex', alignItems: 'flex-end', gap: 1, mb: 2 }}>
<TextField
label="Поиск по метрике"
fullWidth
value={filter}
onChange={(e) => setFilter(e.target.value)}
variant="standard"
/>
<SearchIcon sx={{ color: 'action.active', mr: 1 }} />
</Box>
<Box sx={{
display: 'flex',
gap: 2,
alignItems: 'flex-end', // меняем с 'center' на 'flex-end'
mb: 3
}}>
<TextField
label="Новая метрика"
value={newMetricName}
onChange={(e) => setNewMetricName(e.target.value)}
fullWidth
variant="standard"
/>
<Tooltip title="Добавить метрику">
<IconButton
onClick={addNewMetric}
color="primary"
disabled={!newMetricName.trim()}
>
<AddIcon sx={{ color: 'action.active' }} />
</IconButton>
</Tooltip>
</Box>
<Divider sx={{ mb: 3 }} />
<Box sx={{ maxHeight: '60vh', overflowY: 'auto', pr: 1 }}>
{filtered.map((metric, index) => (
<MetricItem
key={metric.name}
metric={metric}
index={index}
updateRange={updateRange}
addRange={addRange}
deleteRange={deleteRange}
/>
))}
</Box>
{filtered.length === 0 && (
<Typography color="text.secondary" textAlign="center" py={3}>
{filter ? 'Ничего не найдено' : 'Нет метрик для отображения'}
</Typography>
)}
</>
)}
</Box>
);
};
export default React.memo(MetricRangeEditor);