Compare commits
12 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
e5e4d75637 | |
|
|
3a142185a2 | |
|
|
0b600095ce | |
|
|
33a88d2a1a | |
|
|
daf9cab8ac | |
|
|
7400e77fa0 | |
|
|
86baaa29ff | |
|
|
14d2f3eb68 | |
|
|
558cf8eaba | |
|
|
585692c838 | |
|
|
555c28d942 | |
|
|
97295a6748 |
|
|
@ -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.*
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
Loading…
Reference in New Issue