added ranages editor
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good Details

pull/46/head
DmitriyA 2025-06-11 07:30:34 -04:00
parent 328018edfa
commit 405bda3df9
6 changed files with 311 additions and 175 deletions

View File

@ -0,0 +1,258 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
TextField, Box, Typography, IconButton, Divider,
CircularProgress, Alert, Collapse, Tooltip, Button
} 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';
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 = (metricIndex, rangeIndex, field, value) => {
const newRanges = [...ranges];
newRanges[metricIndex].ranges[rangeIndex][field] = Number(value);
setRanges(newRanges);
setHasChanges(true);
};
// Добавление диапазона
const addRange = (metricIndex) => {
const newRanges = [...ranges];
newRanges[metricIndex].ranges.push({ min: 0, max: 100, status: 1 });
setRanges(newRanges);
setHasChanges(true);
};
// Удаление диапазона
const deleteRange = (metricIndex, rangeIndex) => {
const newRanges = [...ranges];
newRanges[metricIndex].ranges.splice(rangeIndex, 1);
setRanges(newRanges);
setHasChanges(true);
};
const saveChanges = 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);
}
};
// Добавление новой метрики
const addNewMetric = () => {
if (!newMetricName.trim()) {
setError('Введите название метрики');
return;
}
if (ranges.some(r => r.name === newMetricName)) {
setError('Метрика с таким именем уже существует');
return;
}
setRanges([...ranges, {
name: newMetricName,
ranges: [{ min: 0, max: 100, status: 1 }]
}]);
setNewMetricName('');
setHasChanges(true);
setError(null);
};
const filtered = filter
? ranges.filter(r => r.name.toLowerCase().includes(filter.toLowerCase()))
: ranges;
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 }}>
<SearchIcon sx={{ color: 'action.active', mr: 1 }} />
<TextField
label="Поиск по метрике"
fullWidth
value={filter}
onChange={(e) => setFilter(e.target.value)}
variant="standard"
/>
</Box>
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center', 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 />
</IconButton>
</Tooltip>
</Box>
<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 }
}}
>
<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>
))}
{filtered.length === 0 && (
<Typography color="text.secondary" textAlign="center" py={3}>
{filter ? 'Ничего не найдено' : 'Нет метрик для отображения'}
</Typography>
)}
</>
)}
{/* Передаем состояние изменений в родительский компонент */}
{useEffect(() => {
if (onSave) {
onSave({ hasChanges, saveChanges });
}
}, [hasChanges, onSave])}
</Box>
);
};
export default MetricRangeEditor;

View File

@ -1,153 +0,0 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Button,
TextField,
IconButton,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Typography,
Divider,
Select,
MenuItem,
FormControl,
InputLabel
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import DeleteIcon from '@mui/icons-material/Delete';
const RangeEditor = ({ metric, onSave, onCancel }) => {
const [ranges, setRanges] = useState([...metric.ranges]);
const [newRange, setNewRange] = useState({ min: 0, max: 0, status: 1 });
const handleAddRange = () => {
if (newRange.min >= newRange.max) {
alert('Минимальное значение должно быть меньше максимального');
return;
}
// Проверка на пересечение с существующими диапазонами
const overlaps = ranges.some(range =>
(newRange.min >= range.min && newRange.min <= range.max) ||
(newRange.max >= range.min && newRange.max <= range.max)
);
if (overlaps) {
alert('Диапазон пересекается с существующим');
return;
}
setRanges([...ranges, newRange].sort((a, b) => a.min - b.min));
setNewRange({ min: 0, max: 0, status: 1 });
};
const handleDeleteRange = (index) => {
const newRanges = [...ranges];
newRanges.splice(index, 1);
setRanges(newRanges);
};
const handleSave = () => {
onSave({
...metric,
ranges
});
};
return (
<Box sx={{ p: 2 }}>
<Typography variant="h6" gutterBottom>
Редактирование диапазонов для: {metric.name}
</Typography>
<TableContainer component={Paper} sx={{ mb: 2 }}>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Минимум</TableCell>
<TableCell>Максимум</TableCell>
<TableCell>Статус</TableCell>
<TableCell>Действия</TableCell>
</TableRow>
</TableHead>
<TableBody>
{ranges.map((range, index) => (
<TableRow key={index}>
<TableCell>{range.min}</TableCell>
<TableCell>{range.max}</TableCell>
<TableCell>{range.status}</TableCell>
<TableCell>
<IconButton onClick={() => handleDeleteRange(index)}>
<DeleteIcon color="error" />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<Divider sx={{ my: 2 }} />
<Typography variant="subtitle1" gutterBottom>
Добавить новый диапазон
</Typography>
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center', mb: 2 }}>
<TextField
label="Минимум"
type="number"
value={newRange.min}
onChange={(e) => setNewRange({ ...newRange, min: parseInt(e.target.value) || 0 })}
size="small"
/>
<TextField
label="Максимум"
type="number"
value={newRange.max}
onChange={(e) => setNewRange({ ...newRange, max: parseInt(e.target.value) || 0 })}
size="small"
/>
<FormControl size="small" sx={{ minWidth: 120 }}>
<InputLabel>Статус</InputLabel>
<Select
value={newRange.status}
label="Статус"
onChange={(e) => setNewRange({ ...newRange, status: parseInt(e.target.value) })}
>
<MenuItem value={1}>Норма (1)</MenuItem>
<MenuItem value={2}>Предупреждение (2)</MenuItem>
<MenuItem value={3}>Опасность (3)</MenuItem>
<MenuItem value={4}>Критично (4)</MenuItem>
</Select>
</FormControl>
<Button
variant="contained"
startIcon={<AddIcon />}
onClick={handleAddRange}
>
Добавить
</Button>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 2 }}>
<Button variant="outlined" onClick={onCancel}>
Отмена
</Button>
<Button variant="contained" onClick={handleSave}>
Сохранить
</Button>
</Box>
</Box>
);
};
export default RangeEditor;

View File

@ -19,6 +19,7 @@ import {
} from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import SaveIcon from '@mui/icons-material/Save';
import MetricRangeEditor from './SettingsComponents/MetricRangeEditor';
const Transition = React.forwardRef(function Transition(props, ref) {
return <Slide direction="up" ref={ref} {...props} />;
@ -52,12 +53,16 @@ const TabPanel = (props) => {
);
};
const SettingsModal = ({ open, onClose }) => {
const SettingsModal = ({ open, onClose, onMenuUpdate }) => {
const [tabValue, setTabValue] = useState(0);
const [isSaving, setIsSaving] = useState(false);
const [showSuccess, setShowSuccess] = useState(false);
const [hasChanges, setHasChanges] = useState(false);
const [showConfirmClose, setShowConfirmClose] = useState(false);
const [metricEditorState, setMetricEditorState] = useState({
hasChanges: false,
save: () => { }
});
const handleTabChange = (event, newValue) => {
if (hasChanges) {
@ -67,14 +72,29 @@ const SettingsModal = ({ open, onClose }) => {
}
};
const handleSave = () => {
const handleSave = async () => {
setIsSaving(true);
// Имитация асинхронного сохранения
setTimeout(() => {
try {
let success = true;
if (tabValue === 1 && metricEditorState.hasChanges) {
success = await metricEditorState.save();
}
if (success) {
setShowSuccess(true);
setHasChanges(false);
if (onMenuUpdate) {
onMenuUpdate();
}
}
} finally {
setIsSaving(false);
setShowSuccess(true);
setHasChanges(false);
}, 1500);
}
};
const handleMetricEditorChange = ({ hasChanges, saveChanges }) => {
setMetricEditorState({ hasChanges, save: saveChanges });
setHasChanges(hasChanges);
};
const handleClose = () => {
@ -126,7 +146,7 @@ const SettingsModal = ({ open, onClose }) => {
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={tabValue} onChange={handleTabChange} aria-label="settings tabs">
<Tab label="Меню" id="settings-tab-0" aria-controls="settings-tabpanel-0" />
<Tab label="Внешний вид" id="settings-tab-1" aria-controls="settings-tabpanel-1" />
<Tab label="Границы метрик" id="settings-tab-1" aria-controls="settings-tabpanel-1" />
{/* Добавляйте новые вкладки здесь */}
</Tabs>
</Box>
@ -138,8 +158,7 @@ const SettingsModal = ({ open, onClose }) => {
</TabPanel>
<TabPanel value={tabValue} index={1}>
<Typography variant="h6">Настройки внешнего вида</Typography>
{/* Добавьте содержимое для вкладки внешнего вида */}
<MetricRangeEditor onSave={handleMetricEditorChange} />
</TabPanel>
{/* Добавляйте новые TabPanel для новых вкладок */}

View File

@ -20,7 +20,8 @@ const SidebarMenu = ({
data,
isDarkMode,
setIsDarkMode,
onSelectItem
onSelectItem,
forceRefreshMenu
}) => {
const [collapsed, setCollapsed] = useState(false);
const { sidebarWidth, startResizing } = useSidebarResize(290);
@ -146,6 +147,7 @@ const SidebarMenu = ({
collapsed={collapsed}
isDarkMode={isDarkMode}
setIsDarkMode={setIsDarkMode}
forceRefreshMenu={forceRefreshMenu}
/>
</Box>
{!collapsed && (

View File

@ -30,7 +30,7 @@ const FooterListItem = styled(ListItem)(({ theme }) => ({
alignItems: 'center'
}));
const SidebarFooter = ({ collapsed, isDarkMode, setIsDarkMode }) => {
const SidebarFooter = ({ collapsed, isDarkMode, setIsDarkMode, forceRefreshMenu }) => {
const [settingsOpen, setSettingsOpen] = useState(false);
const handleSettingsOpen = () => {
@ -98,7 +98,9 @@ const SidebarFooter = ({ collapsed, isDarkMode, setIsDarkMode }) => {
</FooterListItem>
</FooterList>
<SettingsModal open={settingsOpen} onClose={handleSettingsClose} />
<SettingsModal open={settingsOpen}
onClose={handleSettingsClose}
onMenuUpdate={forceRefreshMenu} />
</>
);
};

View File

@ -11,6 +11,13 @@ const SidebarMenuWrapper = ({ isDarkMode, setIsDarkMode, onMenuSelect }) => {
const [editingItem, setEditingItem] = useState(null);
const [editModalOpen, setEditModalOpen] = useState(false);
const [backgroundLoading, setBackgroundLoading] = useState(false);
const [refreshTrigger, setRefreshTrigger] = useState(0);
const forceRefreshMenu = () => {
setRefreshTrigger(prev => prev + 1);
localStorage.removeItem('menuCache'); // Очищаем кэш
};
// Загружаем меню из localStorage при инициализации
useEffect(() => {
@ -62,7 +69,7 @@ const SidebarMenuWrapper = ({ isDarkMode, setIsDarkMode, onMenuSelect }) => {
};
fetchMenuData();
}, []);
}, [refreshTrigger]);
// Фоновая проверка обновлений
useEffect(() => {
@ -169,6 +176,7 @@ const SidebarMenuWrapper = ({ isDarkMode, setIsDarkMode, onMenuSelect }) => {
editingItem={editingItem}
onCloseEditModal={() => setEditModalOpen(false)}
onSaveChanges={handleSaveChanges}
forceRefreshMenu={forceRefreshMenu}
/>
);
};