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}
+
+
+
+
+
+
+ }
+ onClick={() => onEdit(formula)}
+ variant="outlined"
+ size="small"
+ >
+ Редактировать
+
+
+
+
+ {/* Описание */}
+
+ {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 (
+
+ );
+};
+
+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}
+
+
+
+ {/* Панель управления */}
+
+
+
+ Редактор формул с метриками
+
+
+ }
+ disabled={refreshing}
+ >
+ Обновить
+
+
+
+ {/* Статистика */}
+
+
+
+ {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