added ranages editor
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good
Details
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good
Details
parent
328018edfa
commit
405bda3df9
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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 для новых вкладок */}
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue