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

472 lines
19 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 } from 'react';
import {
TextField, Box, Typography, IconButton, Divider,
CircularProgress, Alert, Collapse, Tooltip, Button,
Card, CardContent, Chip, Dialog, DialogTitle,
DialogContent, DialogActions, Snackbar, Table,
TableBody, TableCell, TableContainer, TableHead,
TableRow, Paper, Badge
} 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 WarningIcon from '@mui/icons-material/Warning';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import axios from 'axios';
const FormulaItem = React.memo(({ formula, onEdit }) => {
const getMetricStatusColor = (found) => {
return found ? 'success' : 'error';
};
const formatValue = (value) => {
if (value === undefined) return 'N/A';
return value.toFixed(2);
};
return (
<Card sx={{ mb: 2, border: '1px solid', borderColor: 'divider' }}>
<CardContent>
{/* Заголовок с ID и статусом метрик */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}>
<Box>
<Typography variant="h6" color="primary">
{formula.name}
</Typography>
<Typography variant="body2" color="text.secondary">
ID: {formula.id}
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Badge
badgeContent={formula.metadata?.missingMetrics}
color="error"
sx={{ mr: 1 }}
>
<Chip
label={`${formula.metadata?.foundMetrics || 0}/${formula.metadata?.totalMetrics || 0} метрик`}
color={formula.metadata?.missingMetrics === 0 ? "success" : "warning"}
size="small"
/>
</Badge>
<Button
startIcon={<EditIcon />}
onClick={() => onEdit(formula)}
variant="outlined"
size="small"
>
Редактировать
</Button>
</Box>
</Box>
{/* Описание */}
<Typography variant="body2" color="text.secondary" gutterBottom>
{formula.description}
</Typography>
{/* Метрики */}
<Box sx={{ mt: 2 }}>
<Typography variant="subtitle2" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
Метрики в формуле:
{formula.metadata?.missingMetrics > 0 && (
<WarningIcon color="warning" fontSize="small" />
)}
</Typography>
<TableContainer component={Paper} variant="outlined" sx={{ mb: 2 }}>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Метрика</TableCell>
<TableCell>Описание</TableCell>
<TableCell align="right">Значение</TableCell>
<TableCell>Статус</TableCell>
</TableRow>
</TableHead>
<TableBody>
{formula.enrichedMetrics?.map((metric, index) => (
<TableRow key={index}>
<TableCell>
<Box>
<Typography variant="body2" fontWeight="bold">
{metric.originalName}
</Typography>
<Typography variant="caption" color="text.secondary">
{metric.prometheusName}
</Typography>
</Box>
</TableCell>
<TableCell>
<Typography variant="body2">
{metric.description}
</Typography>
</TableCell>
<TableCell align="right">
<Typography
variant="body2"
fontWeight="bold"
color={metric.found ? 'text.primary' : 'text.disabled'}
>
{formatValue(metric.currentValue)}
</Typography>
</TableCell>
<TableCell>
<Chip
icon={metric.found ? <CheckCircleIcon /> : <WarningIcon />}
label={metric.found ? 'Найдена' : 'Не найдена'}
color={getMetricStatusColor(metric.found)}
size="small"
variant={metric.found ? "filled" : "outlined"}
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Box>
{/* Формула */}
<Box sx={{ mt: 2 }}>
<Typography variant="subtitle2" gutterBottom>
Формула с описанием метрик:
</Typography>
<Box sx={{ mb: 2 }}>
<Typography variant="body2" sx={{
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
backgroundColor: 'primary.light',
color: 'primary.contrastText',
p: 2,
borderRadius: 1,
fontSize: '0.9rem',
fontFamily: 'monospace'
}}>
{formula.humanReadableFormula}
</Typography>
</Box>
</Box>
{/* Веса */}
<Box sx={{ mt: 2 }}>
<Typography variant="subtitle2" gutterBottom>
Веса (warr):
</Typography>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
{formula.values?.warr?.map((weight, index) => (
<Chip
key={index}
label={`warr[${index + 1}]: ${weight}`}
size="small"
variant="outlined"
/>
))}
</Box>
</Box>
</CardContent>
</Card>
);
});
const EditFormulaDialog = ({ open, formula, onClose, onSave }) => {
const [editedFormula, setEditedFormula] = useState('');
useEffect(() => {
if (formula) {
setEditedFormula(formula.formula || '');
}
}, [formula]);
const handleSave = () => {
if (formula && editedFormula.trim()) {
onSave(formula.id, editedFormula.trim());
}
};
return (
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
<DialogTitle>
Редактирование формулы: {formula?.name}
</DialogTitle>
<DialogContent>
<Typography variant="body2" color="text.secondary" gutterBottom>
{formula?.description}
</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 [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 () => {
try {
setLoading(true);
setError(null);
const response = await axios.get('http://192.168.2.39:3000/api/enriched-formulas');
if (Array.isArray(response.data)) {
setFormulas(response.data);
showSnackbar(`Загружено ${response.data.length} формул`);
} else {
throw new Error('Некорректный формат данных');
}
} catch (err) {
console.error('Ошибка при загрузке формул:', err);
const errorMessage = axios.isAxiosError(err)
? `Ошибка сервера: ${err.response?.status} - ${err.response?.data?.message || err.message}`
: `Ошибка загрузки: ${err.message}`;
setError(errorMessage);
showSnackbar(errorMessage, 'error');
} finally {
setLoading(false);
setRefreshing(false);
}
}, []);
const handleEditFormula = (formula) => {
setEditingFormula(formula);
};
const handleSaveFormula = async (formulaId, newFormula) => {
try {
setSaveLoading(true);
await axios.post(`http://192.168.2.39:3000/api/formula/${formulaId}/update`, {
formula: newFormula
});
setFormulas(prev => prev.map(formula =>
formula.id === formulaId
? { ...formula, formula: newFormula }
: formula
));
setEditingFormula(null);
showSnackbar('Формула успешно обновлена!');
} catch (err) {
console.error('Ошибка при сохранении формулы:', err);
showSnackbar('Ошибка при сохранении формулы', 'error');
} finally {
setSaveLoading(false);
}
};
const refreshData = useCallback(() => {
setRefreshing(true);
loadFormulas();
}, [loadFormulas]);
const filteredFormulas = formulas.filter(formula =>
formula.id.toLowerCase().includes(filter.toLowerCase()) ||
formula.name.toLowerCase().includes(filter.toLowerCase()) ||
formula.description.toLowerCase().includes(filter.toLowerCase()) ||
formula.formula.toLowerCase().includes(filter.toLowerCase())
);
const totalMetrics = formulas.reduce((sum, formula) => sum + (formula.metadata?.totalMetrics || 0), 0);
const foundMetrics = formulas.reduce((sum, formula) => sum + (formula.metadata?.foundMetrics || 0), 0);
const missingMetrics = formulas.reduce((sum, formula) => sum + (formula.metadata?.missingMetrics || 0), 0);
useEffect(() => {
loadFormulas();
}, [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="h4" color="primary" fontWeight="bold">
Редактор формул с метриками
</Typography>
<Button
onClick={refreshData}
variant="contained"
startIcon={<RefreshIcon />}
disabled={refreshing}
>
Обновить
</Button>
</Box>
{/* Статистика */}
<Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap' }}>
<Chip
label={`Формулы: ${formulas.length}`}
color="primary"
variant="outlined"
/>
<Chip
label={`Метрики: ${foundMetrics}/${totalMetrics}`}
color={missingMetrics === 0 ? "success" : "warning"}
variant="outlined"
/>
{missingMetrics > 0 && (
<Chip
label={`Отсутствуют: ${missingMetrics}`}
color="error"
variant="outlined"
/>
)}
</Box>
{/* Поиск */}
<Box sx={{ display: 'flex', alignItems: 'flex-end', gap: 1, mb: 2 }}>
<TextField
label="Поиск по формулам"
fullWidth
value={filter}
onChange={(e) => setFilter(e.target.value)}
variant="outlined"
placeholder="Введите ID, название или описание..."
size="small"
/>
<SearchIcon sx={{ color: 'action.active', mb: 0.5 }} />
</Box>
</Box>
<Divider sx={{ mb: 3 }} />
{/* Список формул */}
<Box sx={{ maxHeight: '70vh', overflowY: 'auto', pr: 1 }}>
{filteredFormulas.map((formula) => (
<FormulaItem
key={formula.id}
formula={formula}
onEdit={handleEditFormula}
/>
))}
{filteredFormulas.length === 0 && !loading && (
<Typography
color="text.secondary"
textAlign="center"
py={3}
variant="h6"
>
{filter ? 'Формулы не найдены' : 'Нет загруженных формул'}
</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>
<Typography variant="body2" color={missingMetrics === 0 ? "success.main" : "warning.main"}>
Метрики: {foundMetrics}/{totalMetrics} найдено
</Typography>
</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);