Compare commits

..

5 Commits

2 changed files with 502 additions and 1 deletions

View File

@ -0,0 +1,482 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
TextField, Box, Typography, IconButton, Divider,
CircularProgress, Alert, Collapse, Tooltip, Button,
Card, CardContent, Chip, Dialog, DialogTitle,
DialogContent, DialogActions, Snackbar
} from '@mui/material';
import RefreshIcon from '@mui/icons-material/Refresh';
import SearchIcon from '@mui/icons-material/Search';
import EditIcon from '@mui/icons-material/Edit';
import SaveIcon from '@mui/icons-material/Save';
import axios from 'axios';
const FormulaItem = React.memo(({ data, onEdit }) => {
const formatValue = (value) => {
if (typeof value === 'object' && value !== null) {
return JSON.stringify(value, null, 2);
}
return String(value);
};
const getValueColor = (value) => {
if (typeof value === 'boolean') return 'primary';
if (typeof value === 'number') return 'secondary';
if (value === null) return 'default';
return 'info';
};
return (
<Card sx={{ mb: 2, border: '1px solid', borderColor: 'divider' }}>
<CardContent>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}>
<Typography variant="h6" color="primary">
ID: {data.id || 'Без ID'}
</Typography>
<Button
startIcon={<EditIcon />}
onClick={() => onEdit(data)}
variant="outlined"
size="small"
>
Редактировать
</Button>
</Box>
<Typography variant="subtitle1" gutterBottom sx={{ fontWeight: 'bold' }}>
{data.data.name}
</Typography>
<Typography variant="body2" color="text.secondary" gutterBottom>
{data.data.desription}
</Typography>
<Box sx={{ mt: 2 }}>
<Typography variant="subtitle2" gutterBottom>
Параметры:
</Typography>
<Box sx={{ mb: 2 }}>
<Chip label="statusarr" color="primary" sx={{ mb: 1 }} />
<Typography variant="body2" component="pre" sx={{
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
fontFamily: 'monospace',
fontSize: '0.75rem',
backgroundColor: 'grey.100',
p: 1,
borderRadius: 1
}}>
{JSON.stringify(data.data.values?.statusarr, null, 2)}
</Typography>
</Box>
<Box sx={{ mb: 2 }}>
<Chip label="warr" color="secondary" sx={{ mb: 1 }} />
<Typography variant="body2" component="pre" sx={{
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
fontFamily: 'monospace',
fontSize: '0.75rem',
backgroundColor: 'grey.100',
p: 1,
borderRadius: 1
}}>
{JSON.stringify(data.data.values?.warr, null, 2)}
</Typography>
</Box>
<Box sx={{ mb: 2 }}>
<Chip label="formula" color="success" sx={{ mb: 1 }} />
<Typography variant="body2" component="pre" sx={{
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
fontFamily: 'monospace',
fontSize: '0.75rem',
backgroundColor: 'success.light',
color: 'success.contrastText',
p: 1,
borderRadius: 1
}}>
{data.data.formula}
</Typography>
</Box>
</Box>
<Box sx={{ display: 'flex', gap: 1, mt: 2, flexWrap: 'wrap' }}>
<Chip
label={`Поля: ${Object.keys(data.data || {}).length}`}
size="small"
variant="outlined"
/>
<Chip
label={`Тип: ${typeof data.data}`}
size="small"
variant="outlined"
/>
</Box>
</CardContent>
</Card>
);
});
const EditFormulaDialog = ({ open, formula, onClose, onSave }) => {
const [editedFormula, setEditedFormula] = useState('');
useEffect(() => {
if (formula) {
setEditedFormula(formula.data.formula || '');
}
}, [formula]);
const handleSave = () => {
if (formula && editedFormula.trim()) {
onSave(formula.id, editedFormula.trim());
}
};
return (
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
<DialogTitle>
Редактирование формулы: {formula?.data.name}
</DialogTitle>
<DialogContent>
<Typography variant="body2" color="text.secondary" gutterBottom>
{formula?.data.desription}
</Typography>
<Box sx={{ mt: 2, mb: 2 }}>
<Typography variant="subtitle2" gutterBottom>
Доступные переменные:
</Typography>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
<Chip label="statusarr[]" size="small" />
<Chip label="warr[]" size="small" />
</Box>
</Box>
<TextField
label="Формула"
value={editedFormula}
onChange={(e) => setEditedFormula(e.target.value)}
multiline
rows={6}
fullWidth
variant="outlined"
placeholder="Введите формулу..."
sx={{ mt: 2 }}
/>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>Отмена</Button>
<Button
onClick={handleSave}
variant="contained"
startIcon={<SaveIcon />}
disabled={!editedFormula.trim()}
>
Сохранить
</Button>
</DialogActions>
</Dialog>
);
};
const FormulaEditor = () => {
const [formulas, setFormulas] = useState([]);
const [filter, setFilter] = useState('');
const [formulaId, setFormulaId] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [refreshing, setRefreshing] = useState(false);
const [editingFormula, setEditingFormula] = useState(null);
const [saveLoading, setSaveLoading] = useState(false);
const [snackbar, setSnackbar] = useState({ open: false, message: '', severity: 'success' });
const showSnackbar = (message, severity = 'success') => {
setSnackbar({ open: true, message, severity });
};
const loadFormulas = useCallback(async (id = null) => {
try {
setLoading(true);
setError(null);
const targetId = id || formulaId;
const res = await axios.get(`http://192.168.2.39:3000/api/formula/7777/options`);
console.log('Полученные данные:', res.data);
let formattedData;
if (Array.isArray(res.data)) {
formattedData = res.data.map((item, index) => ({
id: item.id || `formula_${index + 1}`,
data: item
}));
} else if (typeof res.data === 'object' && res.data !== null) {
formattedData = [{
id: targetId,
data: res.data
}];
} else {
formattedData = [{
id: targetId,
data: { value: res.data }
}];
}
console.log('Форматированные данные:', formattedData);
setFormulas(formattedData);
} catch (err) {
console.error('Ошибка при загрузке формул:', err);
setError(`Ошибка загрузки: ${err.message}`);
} finally {
setLoading(false);
setRefreshing(false);
}
}, [formulaId]);
const handleEditFormula = (formula) => {
setEditingFormula(formula);
};
const handleSaveFormula = async (formulaId, newFormula) => {
try {
setSaveLoading(true);
// Обновляем формулу в локальном состоянии
const updatedFormulas = formulas.map(formula =>
formula.id === formulaId
? { ...formula, data: { ...formula.data, formula: newFormula } }
: formula
);
setFormulas(updatedFormulas);
setEditingFormula(null);
showSnackbar('Формула успешно обновлена!');
} catch (err) {
console.error('Ошибка при сохранении формулы:', err);
showSnackbar('Ошибка при сохранении формулы', 'error');
} finally {
setSaveLoading(false);
}
};
const handleSendAllFormulas = async () => {
try {
setSaveLoading(true);
// Преобразуем данные обратно в исходный формат для отправки
const dataToSend = formulas.map(formula => ({
id: formula.data.id,
name: formula.data.name,
desription: formula.data.desription,
values: formula.data.values,
formula: formula.data.formula
}));
console.log('Отправляемые данные:', dataToSend);
const response = await axios.post(
'http://192.168.2.39:9999/api/integration/3333',
dataToSend,
{
headers: {
'Content-Type': 'application/json'
}
}
);
console.log('Ответ сервера:', response.data);
showSnackbar('Данные успешно отправлены на сервер!');
} catch (err) {
console.error('Ошибка при отправке данных:', err);
showSnackbar(`Ошибка отправки: ${err.message}`, 'error');
} finally {
setSaveLoading(false);
}
};
const refreshData = useCallback(() => {
setRefreshing(true);
loadFormulas();
}, [loadFormulas]);
const handleFormulaIdChange = (e) => {
setFormulaId(e.target.value);
};
const handleLoadClick = () => {
if (formulaId.trim()) {
loadFormulas(formulaId);
}
};
const filteredFormulas = formulas.filter(formula =>
formula.id.toLowerCase().includes(filter.toLowerCase()) ||
JSON.stringify(formula.data).toLowerCase().includes(filter.toLowerCase())
);
useEffect(() => {
loadFormulas();
}, []);
return (
<Box sx={{ position: 'relative', p: 2 }}>
{/* Загрузка */}
{(loading || refreshing) && (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
)}
{/* Ошибки */}
<Collapse in={!!error}>
<Alert
severity="error"
sx={{ mb: 2 }}
action={
<Button color="inherit" size="small" onClick={refreshData}>
Повторить
</Button>
}
>
{error}
</Alert>
</Collapse>
{/* Панель управления */}
<Box sx={{ mb: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h3" color="primary">
Редактор формул
</Typography>
<Button
onClick={handleSendAllFormulas}
variant="contained"
color="success"
startIcon={<SaveIcon />}
disabled={formulas.length === 0 || saveLoading}
>
{saveLoading ? <CircularProgress size={24} /> : 'Отправить все формулы'}
</Button>
</Box>
<Box sx={{ display: 'flex', gap: 2, alignItems: 'flex-end', mb: 2 }}>
<TextField
label="ID формулы"
value={formulaId}
onChange={handleFormulaIdChange}
variant="outlined"
placeholder="Например: 7777"
sx={{ minWidth: 200 }}
/>
<Button
onClick={handleLoadClick}
variant="contained"
startIcon={<RefreshIcon />}
disabled={!formulaId.trim() || loading}
>
Загрузить
</Button>
<Chip
label={`Найдено: ${formulas.length}`}
color="primary"
variant="outlined"
/>
</Box>
<Box sx={{ display: 'flex', alignItems: 'flex-end', gap: 1 }}>
<TextField
label="Поиск по формулам"
fullWidth
value={filter}
onChange={(e) => setFilter(e.target.value)}
variant="standard"
placeholder="Введите текст для поиска..."
/>
<SearchIcon sx={{ color: 'action.active', mb: 1 }} />
</Box>
</Box>
<Divider sx={{ mb: 3 }} />
{/* Список формул */}
<Box sx={{ maxHeight: '70vh', overflowY: 'auto', pr: 1 }}>
{filteredFormulas.map((formula, index) => (
<FormulaItem
key={formula.id || index}
data={formula}
onEdit={handleEditFormula}
/>
))}
{filteredFormulas.length === 0 && !loading && (
<Typography
color="text.secondary"
textAlign="center"
py={3}
variant="h6"
>
{filter ? 'Формулы не найдены' : 'Загрузите формулы по ID'}
</Typography>
)}
</Box>
{/* Статус бар */}
<Box sx={{
position: 'sticky',
bottom: 0,
backgroundColor: 'background.paper',
p: 1,
borderTop: 1,
borderColor: 'divider',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<Typography variant="body2" color="text.secondary">
Всего формул: {formulas.length} Отфильтровано: {filteredFormulas.length}
</Typography>
<Button
onClick={refreshData}
startIcon={<RefreshIcon />}
disabled={refreshing}
size="small"
>
Обновить
</Button>
</Box>
{/* Диалог редактирования */}
<EditFormulaDialog
open={!!editingFormula}
formula={editingFormula}
onClose={() => setEditingFormula(null)}
onSave={handleSaveFormula}
/>
{/* Уведомления */}
<Snackbar
open={snackbar.open}
autoHideDuration={6000}
onClose={() => setSnackbar({ ...snackbar, open: false })}
>
<Alert
onClose={() => setSnackbar({ ...snackbar, open: false })}
severity={snackbar.severity}
>
{snackbar.message}
</Alert>
</Snackbar>
</Box>
);
};
export default React.memo(FormulaEditor);

View File

@ -21,7 +21,8 @@ import CloseIcon from '@mui/icons-material/Close';
import SaveIcon from '@mui/icons-material/Save'; import SaveIcon from '@mui/icons-material/Save';
import MetricRangeEditor from './SettingsComponents/MetricRangeEditor'; import MetricRangeEditor from './SettingsComponents/MetricRangeEditor';
import UserManagement from './SettingsComponents/UserManagement'; import UserManagement from './SettingsComponents/UserManagement';
import MenuEditor from './SettingsComponents/MenuEditor' import MenuEditor from './SettingsComponents/MenuEditor';
import FormulaEditor from './SettingsComponents/FormulaEditor';
const Transition = React.forwardRef(function Transition(props, ref) { const Transition = React.forwardRef(function Transition(props, ref) {
return <Slide direction="up" ref={ref} {...props} />; return <Slide direction="up" ref={ref} {...props} />;
@ -69,6 +70,10 @@ const SettingsModal = ({ open, onClose, onMenuUpdate }) => {
hasChanges: false, hasChanges: false,
save: () => Promise.resolve(true) save: () => Promise.resolve(true)
}); });
const [formulaEditorState, setFormulaEditorState] = useState({
hasChanges: false,
save: () => Promise.resolve(true)
});
const handleTabChange = (event, newValue) => { const handleTabChange = (event, newValue) => {
if (hasChanges) { if (hasChanges) {
@ -96,6 +101,10 @@ const SettingsModal = ({ open, onClose, onMenuUpdate }) => {
success = success && await metricEditorState.save(); success = success && await metricEditorState.save();
} }
if (tabValue === 3 && formulaEditorState.hasChanges) {
success = success && await formulaEditorState.save();
}
if (success) { if (success) {
setShowSuccess(true); setShowSuccess(true);
setHasChanges(false); setHasChanges(false);
@ -113,6 +122,11 @@ const SettingsModal = ({ open, onClose, onMenuUpdate }) => {
setHasChanges(hasChanges); setHasChanges(hasChanges);
}; };
const handleFormulaEditorChange = ({ hasChanges, saveChanges }) => {
setFormulaEditorState({ hasChanges, save: saveChanges });
setHasChanges(hasChanges);
};
const handleClose = () => { const handleClose = () => {
if (hasChanges) { if (hasChanges) {
setShowConfirmClose(true); setShowConfirmClose(true);
@ -163,6 +177,7 @@ const SettingsModal = ({ open, onClose, onMenuUpdate }) => {
<Tab label="Меню" id="settings-tab-0" aria-controls="settings-tabpanel-0" /> <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" />
<Tab label="Управление пользователями" id="settings-tab-2" aria-controls="settings-tabpanel-2" /> <Tab label="Управление пользователями" id="settings-tab-2" aria-controls="settings-tabpanel-2" />
<Tab label="Настройка формул" id="settings-tab-3" aria-controls="settings-tabpanel-3" />
{/* Добавить новые вкладки здесь */} {/* Добавить новые вкладки здесь */}
</Tabs> </Tabs>
</Box> </Box>
@ -180,6 +195,10 @@ const SettingsModal = ({ open, onClose, onMenuUpdate }) => {
<UserManagement /> <UserManagement />
</TabPanel> </TabPanel>
<TabPanel value={tabValue} index={3}>
<FormulaEditor onSave={handleFormulaEditorChange} />
</TabPanel>
{/* Добавляйте новые TabPanel для новых вкладок */} {/* Добавляйте новые TabPanel для новых вкладок */}
</DialogContent> </DialogContent>