diff --git a/.gitignore b/.gitignore index e7fea06..0afc875 100755 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,10 @@ node_modules .env.local .env.development .env.production -.env.test \ No newline at end of file +.env.test + +# Local configs +vite.config.js +vite.config.local.js +.env.local +*.local.* \ No newline at end of file diff --git a/src/Components/Layout/SettingsComponents/FormulaEditor.jsx b/src/Components/Layout/SettingsComponents/FormulaEditor.jsx new file mode 100644 index 0000000..830863d --- /dev/null +++ b/src/Components/Layout/SettingsComponents/FormulaEditor.jsx @@ -0,0 +1,472 @@ +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 ( + + + {/* Заголовок с ID и статусом метрик */} + + + + {formula.name} + + + ID: {formula.id} + + + + + + + + + + + {/* Описание */} + + {formula.description} + + + {/* Метрики */} + + + Метрики в формуле: + {formula.metadata?.missingMetrics > 0 && ( + + )} + + + + + + + Метрика + Описание + Значение + Статус + + + + {formula.enrichedMetrics?.map((metric, index) => ( + + + + + {metric.originalName} + + + {metric.prometheusName} + + + + + + {metric.description} + + + + + {formatValue(metric.currentValue)} + + + + : } + label={metric.found ? 'Найдена' : 'Не найдена'} + color={getMetricStatusColor(metric.found)} + size="small" + variant={metric.found ? "filled" : "outlined"} + /> + + + ))} + +
+
+
+ + {/* Формула */} + + + Формула с описанием метрик: + + + + + {formula.humanReadableFormula} + + + + + {/* Веса */} + + + Веса (warr): + + + {formula.values?.warr?.map((weight, index) => ( + + ))} + + +
+
+ ); +}); + +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 ( + + + Редактирование формулы: {formula?.name} + + + + {formula?.description} + + + + + Доступные переменные: + + + + + + + + setEditedFormula(e.target.value)} + multiline + rows={6} + fullWidth + variant="outlined" + placeholder="Введите формулу..." + sx={{ mt: 2 }} + /> + + + + + + + ); +}; + +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 ( + + {/* Загрузка */} + {(loading || refreshing) && ( + + + + )} + + {/* Ошибки */} + + + Повторить + + } + > + {error} + + + + {/* Панель управления */} + + + + Редактор формул с метриками + + + + + + {/* Статистика */} + + + + {missingMetrics > 0 && ( + + )} + + + {/* Поиск */} + + setFilter(e.target.value)} + variant="outlined" + placeholder="Введите ID, название или описание..." + size="small" + /> + + + + + + + {/* Список формул */} + + {filteredFormulas.map((formula) => ( + + ))} + + {filteredFormulas.length === 0 && !loading && ( + + {filter ? 'Формулы не найдены' : 'Нет загруженных формул'} + + )} + + + {/* Статус бар */} + + + Всего формул: {formulas.length} • Отфильтровано: {filteredFormulas.length} + + + Метрики: {foundMetrics}/{totalMetrics} найдено + + + + {/* Диалог редактирования */} + setEditingFormula(null)} + onSave={handleSaveFormula} + /> + + {/* Уведомления */} + setSnackbar({ ...snackbar, open: false })} + > + setSnackbar({ ...snackbar, open: false })} + severity={snackbar.severity} + > + {snackbar.message} + + + + ); +}; + +export default React.memo(FormulaEditor); \ No newline at end of file diff --git a/src/Components/Layout/SettingsModal.jsx b/src/Components/Layout/SettingsModal.jsx index f52cf56..c8ec7a6 100644 --- a/src/Components/Layout/SettingsModal.jsx +++ b/src/Components/Layout/SettingsModal.jsx @@ -21,7 +21,8 @@ import CloseIcon from '@mui/icons-material/Close'; import SaveIcon from '@mui/icons-material/Save'; import MetricRangeEditor from './SettingsComponents/MetricRangeEditor'; 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) { return ; @@ -69,6 +70,10 @@ const SettingsModal = ({ open, onClose, onMenuUpdate }) => { hasChanges: false, save: () => Promise.resolve(true) }); + const [formulaEditorState, setFormulaEditorState] = useState({ + hasChanges: false, + save: () => Promise.resolve(true) + }); const handleTabChange = (event, newValue) => { if (hasChanges) { @@ -96,6 +101,10 @@ const SettingsModal = ({ open, onClose, onMenuUpdate }) => { success = success && await metricEditorState.save(); } + if (tabValue === 3 && formulaEditorState.hasChanges) { + success = success && await formulaEditorState.save(); + } + if (success) { setShowSuccess(true); setHasChanges(false); @@ -113,6 +122,11 @@ const SettingsModal = ({ open, onClose, onMenuUpdate }) => { setHasChanges(hasChanges); }; + const handleFormulaEditorChange = ({ hasChanges, saveChanges }) => { + setFormulaEditorState({ hasChanges, save: saveChanges }); + setHasChanges(hasChanges); + }; + const handleClose = () => { if (hasChanges) { setShowConfirmClose(true); @@ -163,6 +177,7 @@ const SettingsModal = ({ open, onClose, onMenuUpdate }) => { + {/* Добавить новые вкладки здесь */} @@ -180,6 +195,10 @@ const SettingsModal = ({ open, onClose, onMenuUpdate }) => { + + + + {/* Добавляйте новые TabPanel для новых вкладок */} diff --git a/vite.config.js b/vite.config.js deleted file mode 100755 index 9481f91..0000000 --- a/vite.config.js +++ /dev/null @@ -1,35 +0,0 @@ -import { defineConfig, loadEnv } from 'vite' -import react from '@vitejs/plugin-react' -import svgr from 'vite-plugin-svgr' - -export default defineConfig(({ mode }) => { - // Загружаем переменные окружения - const env = loadEnv(mode, process.cwd()) - - return { - plugins: [react(), svgr()], - server: { - host: true, - allowedHosts: ['dev.msf.enode', 'demo-msf.kis-npo.ru'], - proxy: { - '/ai-api': { - target: env.VITE_AI_API_URL || 'http://192.168.2.39:5134', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/ai-api/, ''), - }, - '/metrics-ws': { - target: env.VITE_WS_URL || 'ws://192.168.2.39:3001', - ws: true, - changeOrigin: true, - }, - '/api': { - target: env.VITE_API_URL || 'http://192.168.2.39:3000', - changeOrigin: true, - bypass(req, res, options) { - console.log('Proxying request:', req.url); - } - } - } - } - } -}) \ No newline at end of file