Compare commits

..

12 Commits
1.0.20 ... main

Author SHA1 Message Date
deployer3000 e5e4d75637 Merge pull request 'rc' (#66) from rc into main
Reviewed-on: http://git.enode/deployer3000/trust-module-frontend/pulls/66
2026-03-23 15:14:38 +03:00
Vladislav Drozdov 3a142185a2 Merge pull request 'redisign' (#65) from redisign into rc
Reviewed-on: http://git.enode/deployer3000/trust-module-frontend/pulls/65
2026-03-23 15:14:13 +03:00
DmitriyA 0b600095ce Merge branch 'main' of http://git.enode/deployer3000/trust-module-frontend into redisign 2026-03-23 07:58:24 -04:00
DmitriyA 33a88d2a1a add licensing component 2026-03-23 07:55:22 -04:00
deployer3000 daf9cab8ac Merge pull request 'rc' (#64) from rc into main
Reviewed-on: http://git.enode/deployer3000/trust-module-frontend/pulls/64
2025-12-02 12:47:58 +03:00
Vladislav Drozdov 7400e77fa0 Merge pull request 'redisign' (#63) from redisign into rc
test-org/trust-module-frontend/pipeline/pr-main Build started... Details
Reviewed-on: http://git.enode/deployer3000/trust-module-frontend/pulls/63
2025-12-02 12:38:51 +03:00
deployer3000 86baaa29ff Удалить vite.config.js
test-org/trust-module-frontend/pipeline/pr-rc Build queued... Details
2025-12-02 12:38:01 +03:00
DmitriyA 14d2f3eb68 version update 2025-12-02 04:34:56 -05:00
DmitriyA 558cf8eaba added formula 2025-10-21 09:14:20 -04:00
DmitriyA 585692c838 Merge branch 'rc' of http://git.enode/deployer3000/trust-module-frontend into redisign 2025-09-01 09:46:56 -04:00
DmitriyA 555c28d942 Merge branch 'redisign' of http://git.enode/deployer3000/trust-module-frontend into redisign 2025-09-01 09:44:36 -04:00
DmitriyA 97295a6748 Merge branch 'rc' of http://git.enode/deployer3000/trust-module-frontend into redisign 2025-09-01 09:44:21 -04:00
5 changed files with 724 additions and 37 deletions

6
.gitignore vendored
View File

@ -32,3 +32,9 @@ node_modules
.env.development .env.development
.env.production .env.production
.env.test .env.test
# Local configs
vite.config.js
vite.config.local.js
.env.local
*.local.*

View File

@ -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 (
<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);

View File

@ -0,0 +1,206 @@
// components/SettingsComponents/Licensing.jsx
import React, { useState, useEffect } from 'react';
import {
Box,
Paper,
Typography,
List,
ListItem,
ListItemIcon,
ListItemText,
Divider,
Chip,
Button,
TextField,
InputAdornment,
IconButton,
Alert,
Stack,
Grid
} from '@mui/material';
import {
CheckCircle as CheckCircleIcon,
Cancel as CancelIcon,
VpnKey as VpnKeyIcon,
Refresh as RefreshIcon,
Api as ApiIcon,
Devices as DevicesIcon,
Storage as StorageIcon,
Security as SecurityIcon,
ContentCopy as ContentCopyIcon
} from '@mui/icons-material';
import { styled } from '@mui/material/styles';
const StyledPaper = styled(Paper)(({ theme }) => ({
padding: theme.spacing(3),
marginBottom: theme.spacing(3),
backgroundColor: theme.palette.background.default,
}));
const LicenseKeyBox = styled(Box)(({ theme }) => ({
backgroundColor: theme.palette.background.paper,
padding: theme.spacing(2),
borderRadius: theme.shape.borderRadius,
border: `1px solid ${theme.palette.divider}`,
fontFamily: 'monospace',
fontSize: '1.1rem',
letterSpacing: '0.5px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
marginTop: theme.spacing(2),
}));
const Licensing = ({ onSave }) => {
const [hasChanges, setHasChanges] = useState(false);
const [licenseKey, setLicenseKey] = useState('ABCDE-FGHIJ-KLMNO-PQRST-UVWXY');
const [showCopySuccess, setShowCopySuccess] = useState(false);
// Текущий состав лицензии (заглушка)
const licenseFeatures = [
{ name: 'Модуль API', active: true, icon: ApiIcon, description: 'Полный доступ к API' },
{ name: 'Подключение устройств', active: true, icon: DevicesIcon, value: '', description: '' },
{ name: 'Модуль контроля параметров устойчивого функционирования компонентов, доверенного ПАК', active: true, icon: StorageIcon, value: '', description: '' },
//{ name: 'Расширенная безопасность', active: false, icon: SecurityIcon, description: '' },
];
// Уведомляем родительский компонент об изменениях
useEffect(() => {
if (onSave) {
onSave({
hasChanges,
saveChanges: handleSave
});
}
}, [hasChanges]);
const handleSave = async () => {
// Здесь будет логика сохранения
console.log('Сохранение лицензионных настроек');
setHasChanges(false);
return true;
};
const handleRefreshLicense = () => {
// Заглушка для обновления лицензии
const newKey = generateLicenseKey();
setLicenseKey(newKey);
setHasChanges(true);
};
const generateLicenseKey = () => {
// Заглушка для генерации ключа
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
const segments = [];
for (let i = 0; i < 5; i++) {
let segment = '';
for (let j = 0; j < 5; j++) {
segment += chars[Math.floor(Math.random() * chars.length)];
}
segments.push(segment);
}
return segments.join('-');
};
const handleCopyKey = () => {
navigator.clipboard.writeText(licenseKey);
setShowCopySuccess(true);
setTimeout(() => setShowCopySuccess(false), 2000);
};
return (
<Box>
{/* Текущий состав лицензии */}
<StyledPaper elevation={0}>
<Typography variant="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<VpnKeyIcon color="primary" />
Текущий состав лицензии
</Typography>
<List>
{licenseFeatures.map((feature, index) => {
const IconComponent = feature.icon;
return (
<React.Fragment key={feature.name}>
<ListItem>
<ListItemIcon>
<IconComponent color={feature.active ? "primary" : "disabled"} />
</ListItemIcon>
<ListItemText
primary={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="body1">{feature.name}</Typography>
{feature.value && (
<Chip
label={feature.value}
size="small"
color={feature.active ? "success" : "default"}
variant="outlined"
/>
)}
</Box>
}
secondary={feature.description}
/>
<ListItemIcon>
{feature.active ? (
<CheckCircleIcon color="success" />
) : (
<CancelIcon color="error" />
)}
</ListItemIcon>
</ListItem>
{index < licenseFeatures.length - 1 && <Divider variant="inset" component="li" />}
</React.Fragment>
);
})}
</List>
</StyledPaper>
{/* Идентификатор лицензии */}
<StyledPaper elevation={0}>
<Grid container spacing={2} alignItems="center">
<Grid item xs={12}>
<Typography variant="h6" gutterBottom>
Идентификатор лицензии
</Typography>
<Typography variant="body2" color="textSecondary" paragraph>
Этот ключ используется для активации и обновления лицензии
</Typography>
</Grid>
<Grid item xs={12}>
<LicenseKeyBox>
<Typography variant="body1" sx={{ fontFamily: 'monospace' }}>
{licenseKey}
</Typography>
<Box>
<IconButton onClick={handleCopyKey} size="small" title="Копировать">
<ContentCopyIcon />
</IconButton>
</Box>
</LicenseKeyBox>
{showCopySuccess && (
<Alert severity="success" sx={{ mt: 1 }}>Ключ скопирован в буфер обмена</Alert>
)}
</Grid>
<Grid item xs={12}>
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 2 }}>
<Button
variant="contained"
color="primary"
//onClick={handleRefreshLicense}
//startIcon={<RefreshIcon />}
>
Обновить лицензию
</Button>
</Box>
</Grid>
</Grid>
</StyledPaper>
</Box>
);
};
export default Licensing;

View File

@ -21,7 +21,9 @@ 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';
import Licensing from './SettingsComponents/Licensing';
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 +71,14 @@ 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 [licensingState, setLicensingState] = useState({
hasChanges: false,
save: () => Promise.resolve(true)
});
const handleTabChange = (event, newValue) => { const handleTabChange = (event, newValue) => {
if (hasChanges) { if (hasChanges) {
@ -96,6 +106,14 @@ 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 (tabValue === 4 && licensingState.hasChanges) {
success = success && await licensingState.save();
}
if (success) { if (success) {
setShowSuccess(true); setShowSuccess(true);
setHasChanges(false); setHasChanges(false);
@ -113,6 +131,16 @@ const SettingsModal = ({ open, onClose, onMenuUpdate }) => {
setHasChanges(hasChanges); setHasChanges(hasChanges);
}; };
const handleFormulaEditorChange = ({ hasChanges, saveChanges }) => {
setFormulaEditorState({ hasChanges, save: saveChanges });
setHasChanges(hasChanges);
};
const handleLicensingChange = ({ hasChanges, saveChanges }) => {
setLicensingState({ hasChanges, save: saveChanges });
setHasChanges(hasChanges);
};
const handleClose = () => { const handleClose = () => {
if (hasChanges) { if (hasChanges) {
setShowConfirmClose(true); setShowConfirmClose(true);
@ -163,6 +191,8 @@ 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" />
<Tab label="Лицензирование" id="settings-tab-4" aria-controls="settings-tabpanel-4" />
{/* Добавить новые вкладки здесь */} {/* Добавить новые вкладки здесь */}
</Tabs> </Tabs>
</Box> </Box>
@ -180,6 +210,14 @@ const SettingsModal = ({ open, onClose, onMenuUpdate }) => {
<UserManagement /> <UserManagement />
</TabPanel> </TabPanel>
<TabPanel value={tabValue} index={3}>
<FormulaEditor onSave={handleFormulaEditorChange} />
</TabPanel>
<TabPanel value={tabValue} index={4}>
<Licensing onSave={handleLicensingChange} />
</TabPanel>
{/* Добавляйте новые TabPanel для новых вкладок */} {/* Добавляйте новые TabPanel для новых вкладок */}
</DialogContent> </DialogContent>

View File

@ -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);
}
}
}
}
}
})