mirror of
https://github.com/Segate-ekb/publicator.git
synced 2024-11-16 09:58:27 +02:00
Публикация хранилища (#2)
Добавил функционал публикации сервера хранилища. Теперь данные о выбранной теме сохраняются для клиента
This commit is contained in:
parent
e3fba7afd9
commit
74a12be728
@ -4,9 +4,10 @@ services:
|
||||
publikator:
|
||||
image: 192.168.1.53:5000/publicator:8.3.20.2180
|
||||
ports:
|
||||
- 998:80
|
||||
- 3333:3333
|
||||
- 995:80
|
||||
- 4545:3333
|
||||
volumes:
|
||||
- "./volumes/config:/opt/app/winow/config"
|
||||
- "./volumes/repo:/opt/app/repo"
|
||||
# - "./volumes/autumn-properties.json:/opt/app/winow/autumn-properties.json"
|
||||
|
16
react_frontend/public/service-worker.js
Normal file
16
react_frontend/public/service-worker.js
Normal file
@ -0,0 +1,16 @@
|
||||
// use self instead of window in service worker
|
||||
self.addEventListener('install', (event) => {
|
||||
// Perform some task
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
// Perform some task
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
event.respondWith(
|
||||
caches.match(event.request).then((response) => {
|
||||
return response || fetch(event.request);
|
||||
})
|
||||
);
|
||||
});
|
@ -1,65 +1,65 @@
|
||||
// ApiProcessor.js
|
||||
const serverApi = process.env.REACT_APP_SERVER_API;
|
||||
|
||||
const sendRequest = async (url, showSnackbar) => {
|
||||
const response = await fetch(url);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.message && typeof data.message === 'string') {
|
||||
showSnackbar && showSnackbar(data.message, "success");
|
||||
}
|
||||
return data;
|
||||
} else {
|
||||
showSnackbar && showSnackbar(`Произошла ошибка при выполнении запроса: ${response.statusText}`, "error");
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const startServer = async (showSnackbar) => {
|
||||
return sendRequest(`/api/v1/ws/start`, showSnackbar);
|
||||
};
|
||||
|
||||
export const stopServer = async (showSnackbar) => {
|
||||
return sendRequest(`/api/v1/ws/stop`, showSnackbar);
|
||||
};
|
||||
|
||||
export const restartServer = async (showSnackbar) => {
|
||||
return sendRequest(`/api/v1/ws/restart`, showSnackbar);
|
||||
};
|
||||
|
||||
export const getConfig = async (showSnackbar) => {
|
||||
return sendRequest(`/api/v1/config/getconfig`, showSnackbar);
|
||||
};
|
||||
|
||||
export const getSettings = async (showSnackbar) => {
|
||||
return sendRequest(`/api/v1/config/getsettings`, showSnackbar);
|
||||
};
|
||||
|
||||
const sendPostRequest = async (url, data, showSnackbar) => {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
|
||||
if (response.ok) {
|
||||
const responseData = await response.json();
|
||||
showSnackbar && showSnackbar(responseData.message, "success");
|
||||
return responseData;
|
||||
} else {
|
||||
showSnackbar && showSnackbar(`Произошла ошибка при выполнении запроса: ${response.statusText}`, "error");
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateSettings = async (settings, showSnackbar) => {
|
||||
return sendPostRequest(`/api/v1/config/updatesettings`, settings, showSnackbar);
|
||||
};
|
||||
|
||||
export const updateConfig = async (bases, showSnackbar) => {
|
||||
return sendPostRequest(`/api/v1/config/updateconfig`, { bases }, showSnackbar);
|
||||
// ApiProcessor.js
|
||||
const serverApi = process.env.REACT_APP_SERVER_API;
|
||||
|
||||
const sendRequest = async (url, showSnackbar) => {
|
||||
const response = await fetch(url);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.message && typeof data.message === 'string') {
|
||||
showSnackbar && showSnackbar(data.message, "success");
|
||||
}
|
||||
return data;
|
||||
} else {
|
||||
showSnackbar && showSnackbar(`Произошла ошибка при выполнении запроса: ${response.statusText}`, "error");
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const startServer = async (showSnackbar) => {
|
||||
return sendRequest(`/api/v1/ws/start`, showSnackbar);
|
||||
};
|
||||
|
||||
export const stopServer = async (showSnackbar) => {
|
||||
return sendRequest(`/api/v1/ws/stop`, showSnackbar);
|
||||
};
|
||||
|
||||
export const restartServer = async (showSnackbar) => {
|
||||
return sendRequest(`/api/v1/ws/restart`, showSnackbar);
|
||||
};
|
||||
|
||||
export const getConfig = async (showSnackbar) => {
|
||||
return sendRequest(`https://publicator.1cdevelopers.ru/api/v1/config/getconfig`, showSnackbar);
|
||||
};
|
||||
|
||||
export const getSettings = async (showSnackbar) => {
|
||||
return sendRequest(`https://publicator.1cdevelopers.ru/api/v1/config/getsettings`, showSnackbar);
|
||||
};
|
||||
|
||||
const sendPostRequest = async (url, data, showSnackbar) => {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
|
||||
if (response.ok) {
|
||||
const responseData = await response.json();
|
||||
showSnackbar && showSnackbar(responseData.message, "success");
|
||||
return responseData;
|
||||
} else {
|
||||
showSnackbar && showSnackbar(`Произошла ошибка при выполнении запроса: ${response.statusText}`, "error");
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateSettings = async (settings, showSnackbar) => {
|
||||
return sendPostRequest(`/api/v1/config/updatesettings`, settings, showSnackbar);
|
||||
};
|
||||
|
||||
export const updateConfig = async (bases, crs, showSnackbar) => {
|
||||
return sendPostRequest(`/api/v1/config/updateconfig`, { bases, crs }, showSnackbar);
|
||||
};
|
@ -1,44 +1,48 @@
|
||||
import React, { createContext, useState } from "react";
|
||||
import { createTheme, ThemeProvider } from "@mui/material";
|
||||
|
||||
const lightTheme = createTheme({
|
||||
palette: {
|
||||
mode: "light",
|
||||
primary: {
|
||||
main: "#90caf9",
|
||||
},
|
||||
secondary: {
|
||||
main: "#f48fb1",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const darkTheme = createTheme({
|
||||
palette: {
|
||||
mode: "dark",
|
||||
primary: {
|
||||
main: "#90caf9",
|
||||
},
|
||||
secondary: {
|
||||
main: "#f48fb1",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const ThemeContext = createContext();
|
||||
|
||||
export const ThemeProviderWrapper = ({ children }) => {
|
||||
const [darkMode, setDarkMode] = useState(false);
|
||||
|
||||
const toggleDarkMode = () => {
|
||||
setDarkMode(!darkMode);
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ darkMode, toggleDarkMode }}>
|
||||
<ThemeProvider theme={darkMode ? darkTheme : lightTheme}>{children}</ThemeProvider>
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeContext;
|
||||
import React, { createContext, useState, useEffect } from "react";
|
||||
import { createTheme, ThemeProvider } from "@mui/material";
|
||||
|
||||
const lightTheme = createTheme({
|
||||
palette: {
|
||||
mode: "light",
|
||||
primary: {
|
||||
main: "#90caf9",
|
||||
},
|
||||
secondary: {
|
||||
main: "#f48fb1",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const darkTheme = createTheme({
|
||||
palette: {
|
||||
mode: "dark",
|
||||
primary: {
|
||||
main: "#90caf9",
|
||||
},
|
||||
secondary: {
|
||||
main: "#f48fb1",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const ThemeContext = createContext();
|
||||
|
||||
export const ThemeProviderWrapper = ({ children }) => {
|
||||
const [darkMode, setDarkMode] = useState(localStorage.getItem('theme') === 'dark');
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('theme', darkMode ? 'dark' : 'light');
|
||||
}, [darkMode]);
|
||||
|
||||
const toggleDarkMode = () => {
|
||||
setDarkMode(!darkMode);
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ darkMode, toggleDarkMode }}>
|
||||
<ThemeProvider theme={darkMode ? darkTheme : lightTheme}>{children}</ThemeProvider>
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeContext;
|
||||
|
@ -1,31 +1,31 @@
|
||||
import React from "react";
|
||||
import { Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, Button } from "@mui/material";
|
||||
|
||||
const ConfirmationDialog = ({ open, title, content, onClose }) => {
|
||||
const handleCancel = () => {
|
||||
onClose(false);
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
onClose(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={handleCancel}>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>{content}</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCancel} color="primary">
|
||||
Отмена
|
||||
</Button>
|
||||
<Button onClick={handleConfirm} color="primary" autoFocus>
|
||||
Подтвердить
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
import React from "react";
|
||||
import { Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, Button } from "@mui/material";
|
||||
|
||||
const ConfirmationDialog = ({ open, title, content, onClose }) => {
|
||||
const handleCancel = () => {
|
||||
onClose(false);
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
onClose(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={handleCancel}>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>{content}</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCancel} color="primary">
|
||||
Отмена
|
||||
</Button>
|
||||
<Button onClick={handleConfirm} color="primary" autoFocus>
|
||||
Подтвердить
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmationDialog;
|
@ -1,18 +1,18 @@
|
||||
import React from "react";
|
||||
import { Typography, Link } from "@mui/material";
|
||||
import CopyrightIcon from "@mui/icons-material/Copyright";
|
||||
|
||||
function Copyright() {
|
||||
return (
|
||||
<Typography variant="body2" color="text.secondary" align="center">
|
||||
<CopyrightIcon fontSize="small" sx={{ verticalAlign: "middle", mr: 0.5 }} />
|
||||
<Link color="inherit" href="https://1cDevelopers.ru/">
|
||||
1cDevelopers.ru
|
||||
</Link>{" "}
|
||||
{new Date().getFullYear()}
|
||||
{"."}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
import React from "react";
|
||||
import { Typography, Link } from "@mui/material";
|
||||
import CopyrightIcon from "@mui/icons-material/Copyright";
|
||||
|
||||
function Copyright() {
|
||||
return (
|
||||
<Typography variant="body2" color="text.secondary" align="center">
|
||||
<CopyrightIcon fontSize="small" sx={{ verticalAlign: "middle", mr: 0.5 }} />
|
||||
<Link color="inherit" href="https://1cDevelopers.ru/">
|
||||
1cDevelopers.ru
|
||||
</Link>{" "}
|
||||
{new Date().getFullYear()}
|
||||
{"."}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
export default Copyright;
|
@ -1,123 +1,123 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Grid, TextField, Switch, FormControlLabel } from '@mui/material';
|
||||
|
||||
|
||||
export default function GeneralSettings({ publication, onChange, onValidChange, validateTitle, validateName }) {
|
||||
const [errors, setErrors] = useState({ title: false, name: false });
|
||||
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { name, value, checked } = event.target;
|
||||
|
||||
onChange({
|
||||
...publication,
|
||||
[name]: event.target.type === 'checkbox' ? checked : value,
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
const handleBlur = (event) => {
|
||||
const { name, value } = event.target;
|
||||
|
||||
let isValid = true;
|
||||
if (name === 'title') {
|
||||
isValid = validateTitle(value);
|
||||
} else if (name === 'name') {
|
||||
isValid = validateName(value);
|
||||
}
|
||||
setErrors({ ...errors, [name]: !isValid });
|
||||
|
||||
// Вызовите колбэк с результатом валидации
|
||||
onValidChange(isValid);
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid container spacing={2} sx={{ mt: 0.5 }}>
|
||||
<Grid item xs={12} sm={10}>
|
||||
<TextField
|
||||
label="Название публикации"
|
||||
name="title"
|
||||
value={publication.title}
|
||||
required
|
||||
fullWidth
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
error={errors.title}
|
||||
helperText={errors.title ? 'Название должно быть уникальным и заполненным' : ''}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={2}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
name="active"
|
||||
checked={publication.active}
|
||||
color="primary"
|
||||
onChange={handleChange}
|
||||
/>
|
||||
}
|
||||
label="Активна"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
label="endpoint публикации"
|
||||
name="name"
|
||||
value={publication.name}
|
||||
required
|
||||
fullWidth
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
error={errors.name}
|
||||
helperText={errors.name ? 'Endpoint должен быть уникальным, заполненным и содержать только латиницу и допустимые символы URL' : ''}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
label="Имя пользователя"
|
||||
name="usr"
|
||||
value={publication.usr}
|
||||
fullWidth
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
label="пароль пользователя"
|
||||
name="pwd"
|
||||
value={publication.pwd}
|
||||
type="password"
|
||||
fullWidth
|
||||
disabled={!publication.usr}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
name="enable"
|
||||
checked={publication.enable}
|
||||
color="primary"
|
||||
onChange={handleChange}
|
||||
/>
|
||||
}
|
||||
label="вход пользователей разрешен"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
name="enableStandardOData"
|
||||
checked={publication.enableStandardOData}
|
||||
color="primary"
|
||||
onChange={handleChange}
|
||||
/>
|
||||
}
|
||||
label="разрешить Odata"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
import React, { useState } from 'react';
|
||||
import { Grid, TextField, Switch, FormControlLabel } from '@mui/material';
|
||||
|
||||
|
||||
export default function GeneralSettings({ publication, onChange, onValidChange, validateTitle, validateName }) {
|
||||
const [errors, setErrors] = useState({ title: false, name: false });
|
||||
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { name, value, checked } = event.target;
|
||||
|
||||
onChange({
|
||||
...publication,
|
||||
[name]: event.target.type === 'checkbox' ? checked : value,
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
const handleBlur = (event) => {
|
||||
const { name, value } = event.target;
|
||||
|
||||
let isValid = true;
|
||||
if (name === 'title') {
|
||||
isValid = validateTitle(value);
|
||||
} else if (name === 'name') {
|
||||
isValid = validateName(value);
|
||||
}
|
||||
setErrors({ ...errors, [name]: !isValid });
|
||||
|
||||
// Вызовите колбэк с результатом валидации
|
||||
onValidChange(isValid);
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid container spacing={2} sx={{ mt: 0.5 }}>
|
||||
<Grid item xs={12} sm={10}>
|
||||
<TextField
|
||||
label="Название публикации"
|
||||
name="title"
|
||||
value={publication.title}
|
||||
required
|
||||
fullWidth
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
error={errors.title}
|
||||
helperText={errors.title ? 'Название должно быть уникальным и заполненным' : ''}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={2}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
name="active"
|
||||
checked={publication.active}
|
||||
color="primary"
|
||||
onChange={handleChange}
|
||||
/>
|
||||
}
|
||||
label="Активна"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
label="endpoint публикации"
|
||||
name="name"
|
||||
value={publication.name}
|
||||
required
|
||||
fullWidth
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
error={errors.name}
|
||||
helperText={errors.name ? 'Endpoint должен быть уникальным, заполненным и содержать только латиницу и допустимые символы URL' : ''}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
label="Имя пользователя"
|
||||
name="usr"
|
||||
value={publication.usr}
|
||||
fullWidth
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
label="пароль пользователя"
|
||||
name="pwd"
|
||||
value={publication.pwd}
|
||||
type="password"
|
||||
fullWidth
|
||||
disabled={!publication.usr}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
name="enable"
|
||||
checked={publication.enable}
|
||||
color="primary"
|
||||
onChange={handleChange}
|
||||
/>
|
||||
}
|
||||
label="вход пользователей разрешен"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
name="enableStandardOData"
|
||||
checked={publication.enableStandardOData}
|
||||
color="primary"
|
||||
onChange={handleChange}
|
||||
/>
|
||||
}
|
||||
label="разрешить Odata"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
@ -1,73 +1,73 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { FormControlLabel, Switch, Divider } from '@mui/material';
|
||||
import ServicesArray from "./ServicesArray/ServicesArray"
|
||||
|
||||
export default function HttpSettings({ publication, onChange, onValidChange }) {
|
||||
const handleSwitchChange = (event) => {
|
||||
onChange({
|
||||
...publication,
|
||||
httpServices: {
|
||||
...publication.httpServices,
|
||||
[event.target.name]: event.target.checked,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const isServicesValid = !publication.httpServices.hsList ||
|
||||
publication.httpServices.hsList.length === 0 ||
|
||||
publication.httpServices.hsList.every(
|
||||
(service) => {
|
||||
return (
|
||||
service.name.trim() !== "" &&
|
||||
/^[a-zA-Z0-9-_]+$/.test(
|
||||
service.rootUrl
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
onValidChange(isServicesValid);
|
||||
}, [publication.httpServices.hsList, onValidChange]);
|
||||
|
||||
const handleServicesChange = (newServices) => {
|
||||
onChange({
|
||||
...publication,
|
||||
httpServices: {
|
||||
...publication.httpServices,
|
||||
hsList: newServices,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={publication.httpServices.publishExtensionsByDefault}
|
||||
onChange={handleSwitchChange}
|
||||
name="publishExtensionsByDefault"
|
||||
/>
|
||||
}
|
||||
label="Публиковать http-сервисы расширений по уммолчанию"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={publication.httpServices.publishByDefault}
|
||||
onChange={handleSwitchChange}
|
||||
name="publishByDefault"
|
||||
/>
|
||||
}
|
||||
label="Публиковать http-сервисы основной конфигурации по умолчанию"
|
||||
/>
|
||||
<Divider />
|
||||
<div>HTTP-сервисы</div>
|
||||
<ServicesArray
|
||||
type="http"
|
||||
servicesList={publication.httpServices.hsList}
|
||||
onChange={handleServicesChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
import React, { useEffect } from 'react';
|
||||
import { FormControlLabel, Switch, Divider } from '@mui/material';
|
||||
import ServicesArray from "./ServicesArray/ServicesArray"
|
||||
|
||||
export default function HttpSettings({ publication, onChange, onValidChange }) {
|
||||
const handleSwitchChange = (event) => {
|
||||
onChange({
|
||||
...publication,
|
||||
httpServices: {
|
||||
...publication.httpServices,
|
||||
[event.target.name]: event.target.checked,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const isServicesValid = !publication.httpServices.hsList ||
|
||||
publication.httpServices.hsList.length === 0 ||
|
||||
publication.httpServices.hsList.every(
|
||||
(service) => {
|
||||
return (
|
||||
service.name.trim() !== "" &&
|
||||
/^[a-zA-Z0-9-_]+$/.test(
|
||||
service.rootUrl
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
onValidChange(isServicesValid);
|
||||
}, [publication.httpServices.hsList, onValidChange]);
|
||||
|
||||
const handleServicesChange = (newServices) => {
|
||||
onChange({
|
||||
...publication,
|
||||
httpServices: {
|
||||
...publication.httpServices,
|
||||
hsList: newServices,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={publication.httpServices.publishExtensionsByDefault}
|
||||
onChange={handleSwitchChange}
|
||||
name="publishExtensionsByDefault"
|
||||
/>
|
||||
}
|
||||
label="Публиковать http-сервисы расширений по уммолчанию"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={publication.httpServices.publishByDefault}
|
||||
onChange={handleSwitchChange}
|
||||
name="publishByDefault"
|
||||
/>
|
||||
}
|
||||
label="Публиковать http-сервисы основной конфигурации по умолчанию"
|
||||
/>
|
||||
<Divider />
|
||||
<div>HTTP-сервисы</div>
|
||||
<ServicesArray
|
||||
type="http"
|
||||
servicesList={publication.httpServices.hsList}
|
||||
onChange={handleServicesChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,125 +1,125 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { TextField, Typography, Grid, Select, MenuItem, InputLabel, FormControl } from '@mui/material';
|
||||
|
||||
export default function ClientConfigSettings({ provider = {}, onChange }) {
|
||||
const [config, setConfig] = useState({});
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
setConfig(provider.clientconfig);
|
||||
}, [provider]);
|
||||
|
||||
const validate = useCallback((config) => {
|
||||
const newErrors = {};
|
||||
['authority', 'client_id', 'redirect_uri'].forEach(field => {
|
||||
if (!config[field] || config[field].trim() === '') {
|
||||
newErrors[field] = 'This field is required';
|
||||
} else if (!isValidUrl(config[field])) {
|
||||
newErrors[field] = 'Must be a valid URL';
|
||||
}
|
||||
});
|
||||
|
||||
setErrors(newErrors);
|
||||
}, []);
|
||||
|
||||
const isValidUrl = (url) => {
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const OIDC_RESPONSE_TYPES = [
|
||||
{ value: "code", label: "Code" },
|
||||
{ value: "id_token", label: "ID Token" },
|
||||
{ value: "token id_token", label: "Token ID Token" },
|
||||
{ value: "code id_token ", label: "code id_token " },
|
||||
{ value: "code token", label: "code token" },
|
||||
{ value: "code id_token token", label: "code id_token token" },
|
||||
];
|
||||
|
||||
const OIDC_SCOPES = [
|
||||
{ value: "openid", label: "OpenID" },
|
||||
{ value: "profile", label: "Profile" },
|
||||
{ value: "email", label: "Email" },
|
||||
{ value: "address", label: "Address" },
|
||||
{ value: "phone", label: "Phone" },
|
||||
];
|
||||
|
||||
const handleInputChange = (event) => {
|
||||
const updatedConfig = {
|
||||
...config,
|
||||
[event.target.name]: event.target.value,
|
||||
};
|
||||
validate(updatedConfig);
|
||||
onChange({...provider, clientconfig: updatedConfig});
|
||||
};
|
||||
|
||||
const handleScopeChange = (event) => {
|
||||
const selectedValues = event.target.value;
|
||||
const updatedConfig = {
|
||||
...config,
|
||||
scope: selectedValues.join(' '),
|
||||
};
|
||||
validate(updatedConfig);
|
||||
onChange({...provider, clientconfig: updatedConfig});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Typography variant="h6">Настройки клиента</Typography>
|
||||
<Grid container spacing={2} alignItems="center" sx={{ mt: 0.5 }}>
|
||||
{['authority', 'client_id', 'redirect_uri'].map((field) => (
|
||||
<Grid item xs={12} sm={6} key={field}>
|
||||
<TextField
|
||||
error={Boolean(errors[field])}
|
||||
helperText={errors[field]}
|
||||
label={field}
|
||||
name={field}
|
||||
value={config[field]}
|
||||
onChange={handleInputChange}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
<Grid container spacing={4} alignItems="center" sx={{ mt: 0.5 }}>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel htmlFor="response_type">Response Type</InputLabel>
|
||||
<Select
|
||||
value={config.response_type || ''}
|
||||
onChange={handleInputChange}
|
||||
inputProps={{ name: 'response_type', id: 'response_type' }}
|
||||
>
|
||||
{OIDC_RESPONSE_TYPES.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel htmlFor="scope">Scope</InputLabel>
|
||||
<Select
|
||||
value={config.scope ? config.scope.split(' ') : []}
|
||||
onChange={handleScopeChange}
|
||||
inputProps={{ name: 'scope', id: 'scope' }}
|
||||
multiple={true}
|
||||
>
|
||||
{OIDC_SCOPES.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { TextField, Typography, Grid, Select, MenuItem, InputLabel, FormControl } from '@mui/material';
|
||||
|
||||
export default function ClientConfigSettings({ provider = {}, onChange }) {
|
||||
const [config, setConfig] = useState({});
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
setConfig(provider.clientconfig);
|
||||
}, [provider]);
|
||||
|
||||
const validate = useCallback((config) => {
|
||||
const newErrors = {};
|
||||
['authority', 'client_id', 'redirect_uri'].forEach(field => {
|
||||
if (!config[field] || config[field].trim() === '') {
|
||||
newErrors[field] = 'This field is required';
|
||||
} else if (!isValidUrl(config[field])) {
|
||||
newErrors[field] = 'Must be a valid URL';
|
||||
}
|
||||
});
|
||||
|
||||
setErrors(newErrors);
|
||||
}, []);
|
||||
|
||||
const isValidUrl = (url) => {
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const OIDC_RESPONSE_TYPES = [
|
||||
{ value: "code", label: "Code" },
|
||||
{ value: "id_token", label: "ID Token" },
|
||||
{ value: "token id_token", label: "Token ID Token" },
|
||||
{ value: "code id_token ", label: "code id_token " },
|
||||
{ value: "code token", label: "code token" },
|
||||
{ value: "code id_token token", label: "code id_token token" },
|
||||
];
|
||||
|
||||
const OIDC_SCOPES = [
|
||||
{ value: "openid", label: "OpenID" },
|
||||
{ value: "profile", label: "Profile" },
|
||||
{ value: "email", label: "Email" },
|
||||
{ value: "address", label: "Address" },
|
||||
{ value: "phone", label: "Phone" },
|
||||
];
|
||||
|
||||
const handleInputChange = (event) => {
|
||||
const updatedConfig = {
|
||||
...config,
|
||||
[event.target.name]: event.target.value,
|
||||
};
|
||||
validate(updatedConfig);
|
||||
onChange({...provider, clientconfig: updatedConfig});
|
||||
};
|
||||
|
||||
const handleScopeChange = (event) => {
|
||||
const selectedValues = event.target.value;
|
||||
const updatedConfig = {
|
||||
...config,
|
||||
scope: selectedValues.join(' '),
|
||||
};
|
||||
validate(updatedConfig);
|
||||
onChange({...provider, clientconfig: updatedConfig});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Typography variant="h6">Настройки клиента</Typography>
|
||||
<Grid container spacing={2} alignItems="center" sx={{ mt: 0.5 }}>
|
||||
{['authority', 'client_id', 'redirect_uri'].map((field) => (
|
||||
<Grid item xs={12} sm={6} key={field}>
|
||||
<TextField
|
||||
error={Boolean(errors[field])}
|
||||
helperText={errors[field]}
|
||||
label={field}
|
||||
name={field}
|
||||
value={config[field]}
|
||||
onChange={handleInputChange}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
<Grid container spacing={4} alignItems="center" sx={{ mt: 0.5 }}>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel htmlFor="response_type">Response Type</InputLabel>
|
||||
<Select
|
||||
value={config.response_type || ''}
|
||||
onChange={handleInputChange}
|
||||
inputProps={{ name: 'response_type', id: 'response_type' }}
|
||||
>
|
||||
{OIDC_RESPONSE_TYPES.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel htmlFor="scope">Scope</InputLabel>
|
||||
<Select
|
||||
value={config.scope ? config.scope.split(' ') : []}
|
||||
onChange={handleScopeChange}
|
||||
inputProps={{ name: 'scope', id: 'scope' }}
|
||||
multiple={true}
|
||||
>
|
||||
{OIDC_SCOPES.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,403 +1,403 @@
|
||||
import React, { useState, useEffect, useContext } from 'react';
|
||||
import {
|
||||
Typography,
|
||||
TextField,
|
||||
Grid,
|
||||
Button,
|
||||
Box,
|
||||
} from '@mui/material';
|
||||
import { styled } from '@mui/system';
|
||||
import UploadIcon from '@mui/icons-material/Upload';
|
||||
import { ProvidersTemplate } from './ProvidersTemplate';
|
||||
import ConfirmationDialog from '../../../../../ConfirmationDialog';
|
||||
import AppContext from "../../../../../../context/AppContext";
|
||||
|
||||
const InputImage = styled('input')(({ providerId }) => ({
|
||||
display: 'none',
|
||||
id: `contained-button-file-${providerId}`,
|
||||
}));
|
||||
|
||||
const ImagePreview = styled('img')({
|
||||
maxHeight: '100px',
|
||||
maxWidth: '100px',
|
||||
});
|
||||
|
||||
export default function GeneralSettings({ provider, onChange, providers }) {
|
||||
const { state } = useContext(AppContext);
|
||||
const [imagePreview, setImagePreview] = useState(provider.image);
|
||||
const [errors, setErrors] = useState({});
|
||||
const [selectedProvider, setSelectedProvider] = useState(
|
||||
ProvidersTemplate.find((templateGroup) => templateGroup.providerType === provider.providerType)
|
||||
);
|
||||
const [selectedFlow, setSelectedFlow] = useState(null);
|
||||
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
validate(provider);
|
||||
setImagePreview(provider.image);
|
||||
}, [provider]);
|
||||
|
||||
useEffect(() => {
|
||||
if (provider.flowid && selectedProvider) {
|
||||
setSelectedFlow(provider.flowId);
|
||||
}
|
||||
}, [provider, selectedProvider]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedFlow) {
|
||||
handleFlowSelect(selectedFlow);
|
||||
}
|
||||
}, [selectedFlow]);
|
||||
|
||||
const validate = (provider) => {
|
||||
const newErrors = {};
|
||||
|
||||
if (!provider.name || provider.name.trim() === '') {
|
||||
newErrors.name = 'Name is required';
|
||||
} else if (
|
||||
providers &&
|
||||
providers.some((p) => p.name === provider.name && p !== provider)
|
||||
) {
|
||||
newErrors.name = 'Name must be unique';
|
||||
}
|
||||
|
||||
if (!provider.title || provider.title.trim() === '') {
|
||||
newErrors.title = 'Title is required';
|
||||
}
|
||||
|
||||
if (provider.discovery && !isValidUrl(provider.discovery)) {
|
||||
newErrors.discovery = 'Discovery must be a valid URL';
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
};
|
||||
|
||||
const isValidUrl = (url) => {
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (event) => {
|
||||
const updatedProvider = {
|
||||
...provider,
|
||||
[event.target.name]: event.target.value,
|
||||
};
|
||||
validate(updatedProvider);
|
||||
onChange(updatedProvider);
|
||||
};
|
||||
|
||||
const handleImageUpload = (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
const base64 = reader.result;
|
||||
setImagePreview(base64);
|
||||
onChange({
|
||||
...provider,
|
||||
image: base64,
|
||||
});
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
const [pendingProviderType, setPendingProviderType] = useState(null);
|
||||
const [pendingFlowId, setPendingFlowId] = useState(null);
|
||||
|
||||
const handleProviderSelect = (providerType) => {
|
||||
const selectedProvider = ProvidersTemplate.find(
|
||||
(templateGroup) => templateGroup.providerType === providerType
|
||||
);
|
||||
|
||||
if (
|
||||
(provider.flowId && provider.providerType !== providerType) ||
|
||||
(selectedProvider &&
|
||||
selectedProvider.flows &&
|
||||
selectedProvider.flows.length &&
|
||||
provider.flowId &&
|
||||
provider.flowId !== selectedProvider.flows[0].id)
|
||||
) {
|
||||
// If changing provider or flow, prompt for confirmation
|
||||
setPendingProviderType(providerType);
|
||||
setPendingFlowId(null);
|
||||
setConfirmationDialogOpen(true);
|
||||
} else {
|
||||
// Otherwise, update provider state immediately
|
||||
setSelectedProvider(selectedProvider);
|
||||
if (selectedProvider && selectedProvider.flows) {
|
||||
const initialFlow =
|
||||
selectedProvider.flows.find((flow) => flow.id === provider.flowId) ||
|
||||
selectedProvider.flows[0];
|
||||
setSelectedFlow(initialFlow.id);
|
||||
} else {
|
||||
setSelectedFlow(null);
|
||||
}
|
||||
onChange({
|
||||
...provider,
|
||||
providerType,
|
||||
flowId: null,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleFlowSelect = (flowId) => {
|
||||
if (!selectedProvider) return;
|
||||
|
||||
if (provider.flowId !== flowId) {
|
||||
// If changing flow, prompt for confirmation
|
||||
setPendingProviderType(null);
|
||||
setPendingFlowId(flowId);
|
||||
setConfirmationDialogOpen(true);
|
||||
} else {
|
||||
// Otherwise, update provider state immediately
|
||||
const selectedFlow = selectedProvider.flows.find((flow) => flow.id === flowId);
|
||||
setSelectedFlow(flowId);
|
||||
|
||||
if (selectedFlow) {
|
||||
onChange({
|
||||
...provider,
|
||||
flowId,
|
||||
...selectedFlow.settingsTemplate,
|
||||
});
|
||||
} else {
|
||||
onChange({
|
||||
...provider,
|
||||
flowId: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteConfirmationClose = (confirmed) => {
|
||||
if (confirmed) {
|
||||
// If user confirmed the dialog, update provider state
|
||||
if (pendingProviderType !== null) {
|
||||
const selectedProvider = ProvidersTemplate.find(
|
||||
(templateGroup) => templateGroup.providerType === pendingProviderType
|
||||
);
|
||||
setSelectedProvider(selectedProvider);
|
||||
if (selectedProvider && selectedProvider.flows) {
|
||||
const initialFlow = selectedProvider.flows[0];
|
||||
setSelectedFlow(initialFlow.id);
|
||||
onChange({
|
||||
...provider,
|
||||
providerType: pendingProviderType,
|
||||
flowId: initialFlow.id,
|
||||
...initialFlow.settingsTemplate,
|
||||
});
|
||||
} else {
|
||||
setSelectedFlow(null);
|
||||
onChange({
|
||||
...provider,
|
||||
providerType: pendingProviderType,
|
||||
flowId: null,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const selectedFlow = selectedProvider.flows.find((flow) => flow.id === pendingFlowId);
|
||||
setSelectedFlow(pendingFlowId);
|
||||
if (selectedFlow) {
|
||||
onChange({
|
||||
...provider,
|
||||
flowId: pendingFlowId,
|
||||
...selectedFlow.settingsTemplate,
|
||||
});
|
||||
} else {
|
||||
onChange({
|
||||
...provider,
|
||||
flowId: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If user cancelled the dialog, reset the pending state
|
||||
setPendingProviderType(null);
|
||||
setPendingFlowId(null);
|
||||
}
|
||||
setConfirmationDialogOpen(false);
|
||||
};
|
||||
|
||||
const versionValid = (minVersion) => {
|
||||
if (!minVersion) return true;
|
||||
const onecVersion = state.settings.onecVersion;
|
||||
if (!onecVersion) return true;
|
||||
|
||||
const [minMajor, minMinor, minPatch, minBuild] = minVersion.split('.').map(Number);
|
||||
const [onecMajor, onecMinor, onecPatch, onecBuild] = onecVersion.split('.').map(Number);
|
||||
|
||||
if (onecMajor > minMajor) return true;
|
||||
if (onecMajor < minMajor) return false;
|
||||
if (onecMinor > minMinor) return true;
|
||||
if (onecMinor < minMinor) return false;
|
||||
if (onecPatch > minPatch) return true;
|
||||
if (onecPatch < minPatch) return false;
|
||||
if (onecBuild >= minBuild) return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Grid container spacing={2} alignItems="center" sx={{ mt: 0.5 }}>
|
||||
<Grid item xs={12} sm={8}>
|
||||
<TextField
|
||||
select
|
||||
label="Выберите провайдера"
|
||||
value={provider.providerType}
|
||||
onChange={(event) => handleProviderSelect(event.target.value)}
|
||||
fullWidth
|
||||
name="providerType"
|
||||
SelectProps={{
|
||||
native: true,
|
||||
}}
|
||||
>
|
||||
{ProvidersTemplate.map((templateGroup, index) => (
|
||||
<option key={index} value={templateGroup.providerType}>
|
||||
{templateGroup.providerType}
|
||||
</option>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
{selectedProvider && selectedProvider.flows && (
|
||||
<Grid item xs={12} sm={4}>
|
||||
<TextField
|
||||
select
|
||||
label="Выберите flow подключения"
|
||||
value={selectedFlow}
|
||||
onChange={(event) => handleFlowSelect(event.target.value)}
|
||||
fullWidth
|
||||
name="flow"
|
||||
SelectProps={{
|
||||
native: true,
|
||||
}}
|
||||
>
|
||||
{selectedProvider.flows.map((flow) => (
|
||||
versionValid(flow.minVersion) ? (
|
||||
<option key={flow.id} value={flow.id}>
|
||||
{flow.id}
|
||||
</option>
|
||||
) : (
|
||||
<option key={flow.id} value={flow.id} disabled>
|
||||
(Доступен с версии {flow.minVersion}) {flow.id}
|
||||
</option>
|
||||
)
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
error={Boolean(errors.name)}
|
||||
helperText={errors.name}
|
||||
label="Имя провайдера"
|
||||
name="name"
|
||||
value={provider.name}
|
||||
onChange={handleInputChange}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
error={Boolean(errors.title)}
|
||||
helperText={errors.title}
|
||||
label="Title"
|
||||
name="title"
|
||||
value={provider.title}
|
||||
onChange={handleInputChange}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
select
|
||||
label="Authentication User Property Name"
|
||||
value={provider.authenticationUserPropertyName}
|
||||
onChange={handleInputChange}
|
||||
fullWidth
|
||||
name="authenticationUserPropertyName"
|
||||
SelectProps={{
|
||||
native: true,
|
||||
}}
|
||||
>
|
||||
{[
|
||||
{
|
||||
value: 'name',
|
||||
label: 'Имя пользователя',
|
||||
},
|
||||
{
|
||||
value: 'OSUser',
|
||||
label: 'Пользователь ОС',
|
||||
},
|
||||
{
|
||||
value: 'email',
|
||||
label: 'Email пользователя ИБ',
|
||||
},
|
||||
{
|
||||
value: 'matchingKey',
|
||||
label: 'Ключ сопоставления пользователя',
|
||||
}
|
||||
].map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
label="Authentication Claim Name"
|
||||
name="authenticationClaimName"
|
||||
value={provider.authenticationClaimName}
|
||||
onChange={handleInputChange}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
error={Boolean(errors.discovery)}
|
||||
helperText={errors.discovery}
|
||||
label="Discovery"
|
||||
name="discovery"
|
||||
value={provider.discovery}
|
||||
onChange={handleInputChange}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Box>
|
||||
<InputImage
|
||||
accept="image/*"
|
||||
id={`contained-button-file-${provider.id}`}
|
||||
type="file"
|
||||
onChange={handleImageUpload}
|
||||
providerId={provider.id}
|
||||
/>
|
||||
<label htmlFor={`contained-button-file-${provider.id}`}>
|
||||
<Button component="span" color="inherit" startIcon={<UploadIcon />}>
|
||||
Загрузить изображение
|
||||
</Button>
|
||||
</label>
|
||||
</Box>
|
||||
{imagePreview && (
|
||||
<ImagePreview
|
||||
src={imagePreview}
|
||||
alt="Предпросмотр изображения"
|
||||
/>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
<ConfirmationDialog
|
||||
open={confirmationDialogOpen}
|
||||
title="Перезаполнение данных о провайдере"
|
||||
content="Вы сменили тип провайдера. Данные будут перезаполнены! Вы уверены?"
|
||||
onClose={handleDeleteConfirmationClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
import React, { useState, useEffect, useContext } from 'react';
|
||||
import {
|
||||
Typography,
|
||||
TextField,
|
||||
Grid,
|
||||
Button,
|
||||
Box,
|
||||
} from '@mui/material';
|
||||
import { styled } from '@mui/system';
|
||||
import UploadIcon from '@mui/icons-material/Upload';
|
||||
import { ProvidersTemplate } from './ProvidersTemplate';
|
||||
import ConfirmationDialog from '../../../../../ConfirmationDialog';
|
||||
import AppContext from "../../../../../../context/AppContext";
|
||||
|
||||
const InputImage = styled('input')(({ providerId }) => ({
|
||||
display: 'none',
|
||||
id: `contained-button-file-${providerId}`,
|
||||
}));
|
||||
|
||||
const ImagePreview = styled('img')({
|
||||
maxHeight: '100px',
|
||||
maxWidth: '100px',
|
||||
});
|
||||
|
||||
export default function GeneralSettings({ provider, onChange, providers }) {
|
||||
const { state } = useContext(AppContext);
|
||||
const [imagePreview, setImagePreview] = useState(provider.image);
|
||||
const [errors, setErrors] = useState({});
|
||||
const [selectedProvider, setSelectedProvider] = useState(
|
||||
ProvidersTemplate.find((templateGroup) => templateGroup.providerType === provider.providerType)
|
||||
);
|
||||
const [selectedFlow, setSelectedFlow] = useState(null);
|
||||
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
validate(provider);
|
||||
setImagePreview(provider.image);
|
||||
}, [provider]);
|
||||
|
||||
useEffect(() => {
|
||||
if (provider.flowid && selectedProvider) {
|
||||
setSelectedFlow(provider.flowId);
|
||||
}
|
||||
}, [provider, selectedProvider]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedFlow) {
|
||||
handleFlowSelect(selectedFlow);
|
||||
}
|
||||
}, [selectedFlow]);
|
||||
|
||||
const validate = (provider) => {
|
||||
const newErrors = {};
|
||||
|
||||
if (!provider.name || provider.name.trim() === '') {
|
||||
newErrors.name = 'Name is required';
|
||||
} else if (
|
||||
providers &&
|
||||
providers.some((p) => p.name === provider.name && p !== provider)
|
||||
) {
|
||||
newErrors.name = 'Name must be unique';
|
||||
}
|
||||
|
||||
if (!provider.title || provider.title.trim() === '') {
|
||||
newErrors.title = 'Title is required';
|
||||
}
|
||||
|
||||
if (provider.discovery && !isValidUrl(provider.discovery)) {
|
||||
newErrors.discovery = 'Discovery must be a valid URL';
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
};
|
||||
|
||||
const isValidUrl = (url) => {
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (event) => {
|
||||
const updatedProvider = {
|
||||
...provider,
|
||||
[event.target.name]: event.target.value,
|
||||
};
|
||||
validate(updatedProvider);
|
||||
onChange(updatedProvider);
|
||||
};
|
||||
|
||||
const handleImageUpload = (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
const base64 = reader.result;
|
||||
setImagePreview(base64);
|
||||
onChange({
|
||||
...provider,
|
||||
image: base64,
|
||||
});
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
const [pendingProviderType, setPendingProviderType] = useState(null);
|
||||
const [pendingFlowId, setPendingFlowId] = useState(null);
|
||||
|
||||
const handleProviderSelect = (providerType) => {
|
||||
const selectedProvider = ProvidersTemplate.find(
|
||||
(templateGroup) => templateGroup.providerType === providerType
|
||||
);
|
||||
|
||||
if (
|
||||
(provider.flowId && provider.providerType !== providerType) ||
|
||||
(selectedProvider &&
|
||||
selectedProvider.flows &&
|
||||
selectedProvider.flows.length &&
|
||||
provider.flowId &&
|
||||
provider.flowId !== selectedProvider.flows[0].id)
|
||||
) {
|
||||
// If changing provider or flow, prompt for confirmation
|
||||
setPendingProviderType(providerType);
|
||||
setPendingFlowId(null);
|
||||
setConfirmationDialogOpen(true);
|
||||
} else {
|
||||
// Otherwise, update provider state immediately
|
||||
setSelectedProvider(selectedProvider);
|
||||
if (selectedProvider && selectedProvider.flows) {
|
||||
const initialFlow =
|
||||
selectedProvider.flows.find((flow) => flow.id === provider.flowId) ||
|
||||
selectedProvider.flows[0];
|
||||
setSelectedFlow(initialFlow.id);
|
||||
} else {
|
||||
setSelectedFlow(null);
|
||||
}
|
||||
onChange({
|
||||
...provider,
|
||||
providerType,
|
||||
flowId: null,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleFlowSelect = (flowId) => {
|
||||
if (!selectedProvider) return;
|
||||
|
||||
if (provider.flowId !== flowId) {
|
||||
// If changing flow, prompt for confirmation
|
||||
setPendingProviderType(null);
|
||||
setPendingFlowId(flowId);
|
||||
setConfirmationDialogOpen(true);
|
||||
} else {
|
||||
// Otherwise, update provider state immediately
|
||||
const selectedFlow = selectedProvider.flows.find((flow) => flow.id === flowId);
|
||||
setSelectedFlow(flowId);
|
||||
|
||||
if (selectedFlow) {
|
||||
onChange({
|
||||
...provider,
|
||||
flowId,
|
||||
...selectedFlow.settingsTemplate,
|
||||
});
|
||||
} else {
|
||||
onChange({
|
||||
...provider,
|
||||
flowId: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteConfirmationClose = (confirmed) => {
|
||||
if (confirmed) {
|
||||
// If user confirmed the dialog, update provider state
|
||||
if (pendingProviderType !== null) {
|
||||
const selectedProvider = ProvidersTemplate.find(
|
||||
(templateGroup) => templateGroup.providerType === pendingProviderType
|
||||
);
|
||||
setSelectedProvider(selectedProvider);
|
||||
if (selectedProvider && selectedProvider.flows) {
|
||||
const initialFlow = selectedProvider.flows[0];
|
||||
setSelectedFlow(initialFlow.id);
|
||||
onChange({
|
||||
...provider,
|
||||
providerType: pendingProviderType,
|
||||
flowId: initialFlow.id,
|
||||
...initialFlow.settingsTemplate,
|
||||
});
|
||||
} else {
|
||||
setSelectedFlow(null);
|
||||
onChange({
|
||||
...provider,
|
||||
providerType: pendingProviderType,
|
||||
flowId: null,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const selectedFlow = selectedProvider.flows.find((flow) => flow.id === pendingFlowId);
|
||||
setSelectedFlow(pendingFlowId);
|
||||
if (selectedFlow) {
|
||||
onChange({
|
||||
...provider,
|
||||
flowId: pendingFlowId,
|
||||
...selectedFlow.settingsTemplate,
|
||||
});
|
||||
} else {
|
||||
onChange({
|
||||
...provider,
|
||||
flowId: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If user cancelled the dialog, reset the pending state
|
||||
setPendingProviderType(null);
|
||||
setPendingFlowId(null);
|
||||
}
|
||||
setConfirmationDialogOpen(false);
|
||||
};
|
||||
|
||||
const versionValid = (minVersion) => {
|
||||
if (!minVersion) return true;
|
||||
const onecVersion = state.settings.onecVersion;
|
||||
if (!onecVersion) return true;
|
||||
|
||||
const [minMajor, minMinor, minPatch, minBuild] = minVersion.split('.').map(Number);
|
||||
const [onecMajor, onecMinor, onecPatch, onecBuild] = onecVersion.split('.').map(Number);
|
||||
|
||||
if (onecMajor > minMajor) return true;
|
||||
if (onecMajor < minMajor) return false;
|
||||
if (onecMinor > minMinor) return true;
|
||||
if (onecMinor < minMinor) return false;
|
||||
if (onecPatch > minPatch) return true;
|
||||
if (onecPatch < minPatch) return false;
|
||||
if (onecBuild >= minBuild) return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Grid container spacing={2} alignItems="center" sx={{ mt: 0.5 }}>
|
||||
<Grid item xs={12} sm={8}>
|
||||
<TextField
|
||||
select
|
||||
label="Выберите провайдера"
|
||||
value={provider.providerType}
|
||||
onChange={(event) => handleProviderSelect(event.target.value)}
|
||||
fullWidth
|
||||
name="providerType"
|
||||
SelectProps={{
|
||||
native: true,
|
||||
}}
|
||||
>
|
||||
{ProvidersTemplate.map((templateGroup, index) => (
|
||||
<option key={index} value={templateGroup.providerType}>
|
||||
{templateGroup.providerType}
|
||||
</option>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
{selectedProvider && selectedProvider.flows && (
|
||||
<Grid item xs={12} sm={4}>
|
||||
<TextField
|
||||
select
|
||||
label="Выберите flow подключения"
|
||||
value={selectedFlow}
|
||||
onChange={(event) => handleFlowSelect(event.target.value)}
|
||||
fullWidth
|
||||
name="flow"
|
||||
SelectProps={{
|
||||
native: true,
|
||||
}}
|
||||
>
|
||||
{selectedProvider.flows.map((flow) => (
|
||||
versionValid(flow.minVersion) ? (
|
||||
<option key={flow.id} value={flow.id}>
|
||||
{flow.id}
|
||||
</option>
|
||||
) : (
|
||||
<option key={flow.id} value={flow.id} disabled>
|
||||
(Доступен с версии {flow.minVersion}) {flow.id}
|
||||
</option>
|
||||
)
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
error={Boolean(errors.name)}
|
||||
helperText={errors.name}
|
||||
label="Имя провайдера"
|
||||
name="name"
|
||||
value={provider.name}
|
||||
onChange={handleInputChange}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
error={Boolean(errors.title)}
|
||||
helperText={errors.title}
|
||||
label="Title"
|
||||
name="title"
|
||||
value={provider.title}
|
||||
onChange={handleInputChange}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
select
|
||||
label="Authentication User Property Name"
|
||||
value={provider.authenticationUserPropertyName}
|
||||
onChange={handleInputChange}
|
||||
fullWidth
|
||||
name="authenticationUserPropertyName"
|
||||
SelectProps={{
|
||||
native: true,
|
||||
}}
|
||||
>
|
||||
{[
|
||||
{
|
||||
value: 'name',
|
||||
label: 'Имя пользователя',
|
||||
},
|
||||
{
|
||||
value: 'OSUser',
|
||||
label: 'Пользователь ОС',
|
||||
},
|
||||
{
|
||||
value: 'email',
|
||||
label: 'Email пользователя ИБ',
|
||||
},
|
||||
{
|
||||
value: 'matchingKey',
|
||||
label: 'Ключ сопоставления пользователя',
|
||||
}
|
||||
].map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
label="Authentication Claim Name"
|
||||
name="authenticationClaimName"
|
||||
value={provider.authenticationClaimName}
|
||||
onChange={handleInputChange}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
error={Boolean(errors.discovery)}
|
||||
helperText={errors.discovery}
|
||||
label="Discovery"
|
||||
name="discovery"
|
||||
value={provider.discovery}
|
||||
onChange={handleInputChange}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Box>
|
||||
<InputImage
|
||||
accept="image/*"
|
||||
id={`contained-button-file-${provider.id}`}
|
||||
type="file"
|
||||
onChange={handleImageUpload}
|
||||
providerId={provider.id}
|
||||
/>
|
||||
<label htmlFor={`contained-button-file-${provider.id}`}>
|
||||
<Button component="span" color="inherit" startIcon={<UploadIcon />}>
|
||||
Загрузить изображение
|
||||
</Button>
|
||||
</label>
|
||||
</Box>
|
||||
{imagePreview && (
|
||||
<ImagePreview
|
||||
src={imagePreview}
|
||||
alt="Предпросмотр изображения"
|
||||
/>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
<ConfirmationDialog
|
||||
open={confirmationDialogOpen}
|
||||
title="Перезаполнение данных о провайдере"
|
||||
content="Вы сменили тип провайдера. Данные будут перезаполнены! Вы уверены?"
|
||||
onClose={handleDeleteConfirmationClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,117 +1,117 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { TextField, Typography, Grid, Select, MenuItem, InputLabel, FormControl } from '@mui/material';
|
||||
|
||||
export default function ProviderConfigSettings({ provider = {}, onChange }) {
|
||||
const [providerConfig, setProviderConfig] = useState({});
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
setProviderConfig(provider.providerconfig);
|
||||
}, [provider]);
|
||||
|
||||
const validate = useCallback((config) => {
|
||||
const newErrors = {};
|
||||
|
||||
['issuer', 'authorization_endpoint', 'token_endpoint', 'jwks_uri', 'userinfo_endpoint'].forEach(field => {
|
||||
if (!config[field] || config[field].trim() === '') {
|
||||
newErrors[field] = 'This field is required';
|
||||
} else if (!isValidUrl(config[field])) {
|
||||
newErrors[field] = 'Must be a valid URL';
|
||||
}
|
||||
});
|
||||
|
||||
setErrors(newErrors);
|
||||
}, []);
|
||||
|
||||
const isValidUrl = (url) => {
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const OIDC_RESPONSE_TYPES = [
|
||||
{ value: "code", label: "Code" },
|
||||
{ value: "id_token", label: "ID Token" },
|
||||
{ value: "token id_token", label: "Token ID Token" },
|
||||
{ value: "code id_token ", label: "code id_token " },
|
||||
{ value: "code token", label: "code token" },
|
||||
{ value: "code id_token token", label: "code id_token token" },
|
||||
];
|
||||
|
||||
const OIDC_SCOPES = [
|
||||
{ value: "openid", label: "OpenID" },
|
||||
{ value: "profile", label: "Profile" },
|
||||
{ value: "email", label: "Email" },
|
||||
{ value: "address", label: "Address" },
|
||||
{ value: "phone", label: "Phone" },
|
||||
];
|
||||
|
||||
const handleInputChange = (event) => {
|
||||
const updatedConfig = {
|
||||
...providerConfig,
|
||||
[event.target.name]: event.target.value,
|
||||
};
|
||||
validate(updatedConfig);
|
||||
onChange({...provider, providerconfig: updatedConfig});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Typography variant="h6">Настройки провайдера</Typography>
|
||||
<Grid container spacing={2} alignItems="center" sx={{ mt: 0.5 }}>
|
||||
{['issuer', 'authorization_endpoint', 'token_endpoint', 'jwks_uri', 'userinfo_endpoint'].map((field) => (
|
||||
<Grid item xs={12} sm={6} key={field}>
|
||||
<TextField
|
||||
error={Boolean(errors[field])}
|
||||
helperText={errors[field]}
|
||||
label={field}
|
||||
name={field}
|
||||
value={providerConfig[field]}
|
||||
onChange={handleInputChange}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
<Grid container spacing={4} alignItems="center" sx={{ mt: 0.5 }}>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel htmlFor="response_type">Response Type</InputLabel>
|
||||
<Select
|
||||
multiple
|
||||
value={providerConfig.response_types_supported || []}
|
||||
onChange={handleInputChange}
|
||||
inputProps={{ name: 'response_types_supported', id: 'response_types_supported' }}
|
||||
>
|
||||
{OIDC_RESPONSE_TYPES.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel htmlFor="scope">Scope</InputLabel>
|
||||
<Select
|
||||
multiple
|
||||
value={providerConfig.scopes_supported || []}
|
||||
onChange={handleInputChange}
|
||||
inputProps={{ name: 'scopes_supported', id: 'scopes_supported' }}
|
||||
>
|
||||
{OIDC_SCOPES.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { TextField, Typography, Grid, Select, MenuItem, InputLabel, FormControl } from '@mui/material';
|
||||
|
||||
export default function ProviderConfigSettings({ provider = {}, onChange }) {
|
||||
const [providerConfig, setProviderConfig] = useState({});
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
setProviderConfig(provider.providerconfig);
|
||||
}, [provider]);
|
||||
|
||||
const validate = useCallback((config) => {
|
||||
const newErrors = {};
|
||||
|
||||
['issuer', 'authorization_endpoint', 'token_endpoint', 'jwks_uri', 'userinfo_endpoint'].forEach(field => {
|
||||
if (!config[field] || config[field].trim() === '') {
|
||||
newErrors[field] = 'This field is required';
|
||||
} else if (!isValidUrl(config[field])) {
|
||||
newErrors[field] = 'Must be a valid URL';
|
||||
}
|
||||
});
|
||||
|
||||
setErrors(newErrors);
|
||||
}, []);
|
||||
|
||||
const isValidUrl = (url) => {
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const OIDC_RESPONSE_TYPES = [
|
||||
{ value: "code", label: "Code" },
|
||||
{ value: "id_token", label: "ID Token" },
|
||||
{ value: "token id_token", label: "Token ID Token" },
|
||||
{ value: "code id_token ", label: "code id_token " },
|
||||
{ value: "code token", label: "code token" },
|
||||
{ value: "code id_token token", label: "code id_token token" },
|
||||
];
|
||||
|
||||
const OIDC_SCOPES = [
|
||||
{ value: "openid", label: "OpenID" },
|
||||
{ value: "profile", label: "Profile" },
|
||||
{ value: "email", label: "Email" },
|
||||
{ value: "address", label: "Address" },
|
||||
{ value: "phone", label: "Phone" },
|
||||
];
|
||||
|
||||
const handleInputChange = (event) => {
|
||||
const updatedConfig = {
|
||||
...providerConfig,
|
||||
[event.target.name]: event.target.value,
|
||||
};
|
||||
validate(updatedConfig);
|
||||
onChange({...provider, providerconfig: updatedConfig});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Typography variant="h6">Настройки провайдера</Typography>
|
||||
<Grid container spacing={2} alignItems="center" sx={{ mt: 0.5 }}>
|
||||
{['issuer', 'authorization_endpoint', 'token_endpoint', 'jwks_uri', 'userinfo_endpoint'].map((field) => (
|
||||
<Grid item xs={12} sm={6} key={field}>
|
||||
<TextField
|
||||
error={Boolean(errors[field])}
|
||||
helperText={errors[field]}
|
||||
label={field}
|
||||
name={field}
|
||||
value={providerConfig[field]}
|
||||
onChange={handleInputChange}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
<Grid container spacing={4} alignItems="center" sx={{ mt: 0.5 }}>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel htmlFor="response_type">Response Type</InputLabel>
|
||||
<Select
|
||||
multiple
|
||||
value={providerConfig.response_types_supported || []}
|
||||
onChange={handleInputChange}
|
||||
inputProps={{ name: 'response_types_supported', id: 'response_types_supported' }}
|
||||
>
|
||||
{OIDC_RESPONSE_TYPES.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel htmlFor="scope">Scope</InputLabel>
|
||||
<Select
|
||||
multiple
|
||||
value={providerConfig.scopes_supported || []}
|
||||
onChange={handleInputChange}
|
||||
inputProps={{ name: 'scopes_supported', id: 'scopes_supported' }}
|
||||
>
|
||||
{OIDC_SCOPES.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
}
|
File diff suppressed because one or more lines are too long
@ -1,73 +1,73 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Tabs, Tab, Grid, IconButton, Box } from '@mui/material';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import ConfirmationDialog from '../../../../ConfirmationDialog';
|
||||
import GeneralSettings from './Components/GeneralSettings';
|
||||
import ProviderSettingsTab from './Components/ProviderConfig';
|
||||
import ClientSettingsTab from './Components/ClientConfig';
|
||||
import { useMediaQuery } from '@mui/material';
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import VpnKeyIcon from '@mui/icons-material/VpnKey';
|
||||
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
|
||||
|
||||
export default function ProviderSettings({ provider, onChange, onDelete }) {
|
||||
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
|
||||
const [tabValue, setTabValue] = React.useState(0);
|
||||
const matches = useMediaQuery(theme => theme.breakpoints.down('md'));
|
||||
|
||||
const handleDeleteConfirmationOpen = () => {
|
||||
setConfirmationDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteConfirmationClose = (confirmed) => {
|
||||
if (confirmed) {
|
||||
onDelete();
|
||||
}
|
||||
setConfirmationDialogOpen(false);
|
||||
};
|
||||
|
||||
const handleTabChange = (event, newValue) => {
|
||||
setTabValue(newValue);
|
||||
};
|
||||
|
||||
const handleProviderChange = (updatedProviderData) => {
|
||||
// Обновите данные провайдера и передайте их в callback
|
||||
const updatedProvider = { ...provider, ...updatedProviderData };
|
||||
onChange(updatedProvider);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Grid container spacing={2} alignItems="center">
|
||||
<Grid item xs={12}>
|
||||
<Box>
|
||||
<Tabs value={tabValue} onChange={handleTabChange} variant="scrollable" scrollButtons allowScrollButtonsMobile>
|
||||
<Tab icon={<SettingsIcon />} label={!matches && "Общие настройки"} />
|
||||
<Tab icon={<VpnKeyIcon />} label={!matches && "Настройки провайдера"} />
|
||||
<Tab icon={<AccountCircleIcon />} label={!matches && "Настройки клиента"} />
|
||||
</Tabs>
|
||||
{tabValue === 0 && (
|
||||
<GeneralSettings
|
||||
provider={provider}
|
||||
onChange={handleProviderChange}
|
||||
/>
|
||||
)}
|
||||
{tabValue === 1 && <ProviderSettingsTab provider={provider} onChange={handleProviderChange} />}
|
||||
{tabValue === 2 && <ClientSettingsTab provider={provider} onChange={handleProviderChange} />}
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<IconButton onClick={handleDeleteConfirmationOpen} color="secondary">
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<ConfirmationDialog
|
||||
open={confirmationDialogOpen}
|
||||
title="Подтверждение удаления"
|
||||
content="Вы уверены, что хотите удалить провайдер?"
|
||||
onClose={handleDeleteConfirmationClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
import React, { useState } from 'react';
|
||||
import { Tabs, Tab, Grid, IconButton, Box } from '@mui/material';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import ConfirmationDialog from '../../../../ConfirmationDialog';
|
||||
import GeneralSettings from './Components/GeneralSettings';
|
||||
import ProviderSettingsTab from './Components/ProviderConfig';
|
||||
import ClientSettingsTab from './Components/ClientConfig';
|
||||
import { useMediaQuery } from '@mui/material';
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import VpnKeyIcon from '@mui/icons-material/VpnKey';
|
||||
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
|
||||
|
||||
export default function ProviderSettings({ provider, onChange, onDelete }) {
|
||||
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
|
||||
const [tabValue, setTabValue] = React.useState(0);
|
||||
const matches = useMediaQuery(theme => theme.breakpoints.down('md'));
|
||||
|
||||
const handleDeleteConfirmationOpen = () => {
|
||||
setConfirmationDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteConfirmationClose = (confirmed) => {
|
||||
if (confirmed) {
|
||||
onDelete();
|
||||
}
|
||||
setConfirmationDialogOpen(false);
|
||||
};
|
||||
|
||||
const handleTabChange = (event, newValue) => {
|
||||
setTabValue(newValue);
|
||||
};
|
||||
|
||||
const handleProviderChange = (updatedProviderData) => {
|
||||
// Обновите данные провайдера и передайте их в callback
|
||||
const updatedProvider = { ...provider, ...updatedProviderData };
|
||||
onChange(updatedProvider);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Grid container spacing={2} alignItems="center">
|
||||
<Grid item xs={12}>
|
||||
<Box>
|
||||
<Tabs value={tabValue} onChange={handleTabChange} variant="scrollable" scrollButtons allowScrollButtonsMobile>
|
||||
<Tab icon={<SettingsIcon />} label={!matches && "Общие настройки"} />
|
||||
<Tab icon={<VpnKeyIcon />} label={!matches && "Настройки провайдера"} />
|
||||
<Tab icon={<AccountCircleIcon />} label={!matches && "Настройки клиента"} />
|
||||
</Tabs>
|
||||
{tabValue === 0 && (
|
||||
<GeneralSettings
|
||||
provider={provider}
|
||||
onChange={handleProviderChange}
|
||||
/>
|
||||
)}
|
||||
{tabValue === 1 && <ProviderSettingsTab provider={provider} onChange={handleProviderChange} />}
|
||||
{tabValue === 2 && <ClientSettingsTab provider={provider} onChange={handleProviderChange} />}
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<IconButton onClick={handleDeleteConfirmationOpen} color="secondary">
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<ConfirmationDialog
|
||||
open={confirmationDialogOpen}
|
||||
title="Подтверждение удаления"
|
||||
content="Вы уверены, что хотите удалить провайдер?"
|
||||
onClose={handleDeleteConfirmationClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,132 +1,132 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Accordion, AccordionSummary, AccordionDetails, Typography, Button, Grid, Avatar, FormControlLabel, Switch, Divider } from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import ProviderSettings from './OidcProviders/ProviderSettings';
|
||||
|
||||
export default function OidcSettings({ publication, onChange }) {
|
||||
const [providers, setProviders] = useState(publication?.oidc?.providers || []);
|
||||
const [isProvidersUpdated, setIsProvidersUpdated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (publication?.oidc?.providers) {
|
||||
setProviders(publication.oidc.providers);
|
||||
setIsProvidersUpdated(true);
|
||||
}
|
||||
}, [publication]);
|
||||
|
||||
|
||||
const handleAddProvider = () => {
|
||||
const newProvider = {
|
||||
providerType: "Other",
|
||||
flowId: "default",
|
||||
name: "",
|
||||
title: "",
|
||||
authenticationClaimName: "email",
|
||||
authenticationUserPropertyName: "email ",
|
||||
image: "",
|
||||
discovery: "",
|
||||
providerconfig: {
|
||||
issuer: "",
|
||||
authorization_endpoint: "",
|
||||
token_endpoint: "",
|
||||
jwks_uri: "",
|
||||
userinfo_endpoint: "",
|
||||
response_types_supported: ["token id_token"],
|
||||
scopes_supported: [
|
||||
"openid",
|
||||
"email"
|
||||
]
|
||||
},
|
||||
clientconfig: {
|
||||
authority: "",
|
||||
client_id: "",
|
||||
redirect_uri: "",
|
||||
response_type: "token id_token",
|
||||
scope: "openid email"
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
setProviders([...providers, newProvider]);
|
||||
setIsProvidersUpdated(true);
|
||||
};
|
||||
|
||||
const handleProviderChange = (index, updatedProvider) => {
|
||||
const newProviders = providers.slice();
|
||||
newProviders[index] = updatedProvider;
|
||||
setProviders(newProviders);
|
||||
onChange({ ...publication, oidc: { ...publication.oidc, providers: newProviders } });
|
||||
};
|
||||
|
||||
const handleProviderDelete = (index) => {
|
||||
const newProviders = providers.filter((_, i) => i !== index);
|
||||
setProviders(newProviders);
|
||||
onChange({ ...publication, oidc: { ...publication.oidc, providers: newProviders } });
|
||||
};
|
||||
|
||||
const handleSwitchChange = (event) => {
|
||||
onChange({
|
||||
...publication,
|
||||
oidc: {
|
||||
...publication.oidc ?? {},
|
||||
[event.target.name]: event.target.checked,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={publication?.oidc?.allowStandardAuthentication ?? true}
|
||||
onChange={handleSwitchChange}
|
||||
name="allowStandardAuthentication"
|
||||
/>
|
||||
}
|
||||
label="Разрешить стандартный способ авторизации"
|
||||
/>
|
||||
<Divider />
|
||||
<div>OIDC-Провайдеры</div>
|
||||
<Grid container direction="column" spacing={2}>
|
||||
{isProvidersUpdated &&
|
||||
providers.map((provider, index) => (
|
||||
<Grid item key={index} xs={12}>
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
{provider.image && (
|
||||
<Avatar
|
||||
src={provider.image}
|
||||
alt={provider.name}
|
||||
sx={{ marginRight: 1 }}
|
||||
/>
|
||||
)}
|
||||
<Typography variant="h6" component="div">
|
||||
{provider.name || 'Новый провайдер'}
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<ProviderSettings
|
||||
provider={provider}
|
||||
onChange={(updatedProvider) => handleProviderChange(index, updatedProvider)}
|
||||
onDelete={() => handleProviderDelete(index)}
|
||||
/>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</Grid>
|
||||
))}
|
||||
<Grid item>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={handleAddProvider}
|
||||
startIcon={<AddIcon />}
|
||||
>
|
||||
Добавить сервис
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Accordion, AccordionSummary, AccordionDetails, Typography, Button, Grid, Avatar, FormControlLabel, Switch, Divider } from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import ProviderSettings from './OidcProviders/ProviderSettings';
|
||||
|
||||
export default function OidcSettings({ publication, onChange }) {
|
||||
const [providers, setProviders] = useState(publication?.oidc?.providers || []);
|
||||
const [isProvidersUpdated, setIsProvidersUpdated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (publication?.oidc?.providers) {
|
||||
setProviders(publication.oidc.providers);
|
||||
setIsProvidersUpdated(true);
|
||||
}
|
||||
}, [publication]);
|
||||
|
||||
|
||||
const handleAddProvider = () => {
|
||||
const newProvider = {
|
||||
providerType: "Other",
|
||||
flowId: "default",
|
||||
name: "",
|
||||
title: "",
|
||||
authenticationClaimName: "email",
|
||||
authenticationUserPropertyName: "email ",
|
||||
image: "",
|
||||
discovery: "",
|
||||
providerconfig: {
|
||||
issuer: "",
|
||||
authorization_endpoint: "",
|
||||
token_endpoint: "",
|
||||
jwks_uri: "",
|
||||
userinfo_endpoint: "",
|
||||
response_types_supported: ["token id_token"],
|
||||
scopes_supported: [
|
||||
"openid",
|
||||
"email"
|
||||
]
|
||||
},
|
||||
clientconfig: {
|
||||
authority: "",
|
||||
client_id: "",
|
||||
redirect_uri: "",
|
||||
response_type: "token id_token",
|
||||
scope: "openid email"
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
setProviders([...providers, newProvider]);
|
||||
setIsProvidersUpdated(true);
|
||||
};
|
||||
|
||||
const handleProviderChange = (index, updatedProvider) => {
|
||||
const newProviders = providers.slice();
|
||||
newProviders[index] = updatedProvider;
|
||||
setProviders(newProviders);
|
||||
onChange({ ...publication, oidc: { ...publication.oidc, providers: newProviders } });
|
||||
};
|
||||
|
||||
const handleProviderDelete = (index) => {
|
||||
const newProviders = providers.filter((_, i) => i !== index);
|
||||
setProviders(newProviders);
|
||||
onChange({ ...publication, oidc: { ...publication.oidc, providers: newProviders } });
|
||||
};
|
||||
|
||||
const handleSwitchChange = (event) => {
|
||||
onChange({
|
||||
...publication,
|
||||
oidc: {
|
||||
...publication.oidc ?? {},
|
||||
[event.target.name]: event.target.checked,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={publication?.oidc?.allowStandardAuthentication ?? true}
|
||||
onChange={handleSwitchChange}
|
||||
name="allowStandardAuthentication"
|
||||
/>
|
||||
}
|
||||
label="Разрешить стандартный способ авторизации"
|
||||
/>
|
||||
<Divider />
|
||||
<div>OIDC-Провайдеры</div>
|
||||
<Grid container direction="column" spacing={2}>
|
||||
{isProvidersUpdated &&
|
||||
providers.map((provider, index) => (
|
||||
<Grid item key={index} xs={12}>
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
{provider.image && (
|
||||
<Avatar
|
||||
src={provider.image}
|
||||
alt={provider.name}
|
||||
sx={{ marginRight: 1 }}
|
||||
/>
|
||||
)}
|
||||
<Typography variant="h6" component="div">
|
||||
{provider.name || 'Новый провайдер'}
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<ProviderSettings
|
||||
provider={provider}
|
||||
onChange={(updatedProvider) => handleProviderChange(index, updatedProvider)}
|
||||
onDelete={() => handleProviderDelete(index)}
|
||||
/>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</Grid>
|
||||
))}
|
||||
<Grid item>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={handleAddProvider}
|
||||
startIcon={<AddIcon />}
|
||||
>
|
||||
Добавить сервис
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,216 +1,216 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
TextField,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
Grid,
|
||||
Switch,
|
||||
FormControlLabel,
|
||||
IconButton
|
||||
} from "@mui/material";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import ConfirmationDialog from "../../../../ConfirmationDialog"
|
||||
|
||||
export default function ServiceForm({ service = {}, onChange, onDelete, type }) {
|
||||
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
|
||||
const [nameError, setNameError] = useState(false);
|
||||
const [rootUrlError, setRootUrlError] = useState(false);
|
||||
const [aliasError, setAliasError] = useState(false);
|
||||
|
||||
const handleInputChange = (event) => {
|
||||
onChange({
|
||||
...service,
|
||||
[event.target.name]: event.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteConfirmationOpen = () => {
|
||||
setConfirmationDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteConfirmationClose = (confirmed) => {
|
||||
if (confirmed) {
|
||||
onDelete(service);
|
||||
}
|
||||
setConfirmationDialogOpen(false);
|
||||
};
|
||||
|
||||
|
||||
|
||||
const validateName = (event, value) => {
|
||||
if (value.trim() === '') {
|
||||
setNameError(true);
|
||||
event.preventDefault();
|
||||
return false;
|
||||
} else {
|
||||
setNameError(false);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const validateRootUrl = (event, value) => {
|
||||
const pattern = /^[a-zA-Z0-9-_]+$/;
|
||||
if (!pattern.test(value)) {
|
||||
setRootUrlError(true);
|
||||
event.preventDefault();
|
||||
return false;
|
||||
} else {
|
||||
setRootUrlError(false);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const validateAlias = (event, value) => {
|
||||
const pattern = /^[a-zA-Z0-9-_]+\.1cws$/;
|
||||
if (!pattern.test(value)) {
|
||||
setAliasError(true);
|
||||
event.preventDefault();
|
||||
return false;
|
||||
} else {
|
||||
setAliasError(false);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Grid container spacing={2} alignItems="center">
|
||||
<Grid item xs={12} sm={9}>
|
||||
<TextField
|
||||
label="Название"
|
||||
name="name"
|
||||
value={service.name}
|
||||
onChange={(event) => {
|
||||
handleInputChange(event);
|
||||
}}
|
||||
onBlur={(event) => validateName(event,service.name)}
|
||||
error={nameError}
|
||||
helperText={nameError ? "Поле обязательно для заполнения" : ""}
|
||||
fullWidth
|
||||
required
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={3}>
|
||||
<FormControlLabel control={<Switch
|
||||
edge="start"
|
||||
checked={service.enable}
|
||||
onChange={handleInputChange}
|
||||
/>} label="Включен" />
|
||||
|
||||
</Grid>
|
||||
{type === 'http' && (
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
label="Корневой URL"
|
||||
name="rootUrl"
|
||||
value={service.rootUrl}
|
||||
onChange={(event) => {
|
||||
handleInputChange(event);
|
||||
}}
|
||||
onBlur={(event) => validateRootUrl(event,service.rootUrl)}
|
||||
error={rootUrlError}
|
||||
helperText={rootUrlError ? "Введите корректный URL" : ""}
|
||||
fullWidth
|
||||
required
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{type === 'web' && (
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
label="Alias"
|
||||
name="alias"
|
||||
value={service.alias}
|
||||
onChange={(event) => {
|
||||
handleInputChange(event);
|
||||
}}
|
||||
onBlur={(event) => validateAlias(event, service.alias)}
|
||||
error={aliasError}
|
||||
helperText={aliasError ? "Введите корректный Alias с расширением .1cws" : ""}
|
||||
fullWidth
|
||||
required
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<TextField
|
||||
label="Pool Size"
|
||||
name="poolSize"
|
||||
type="number"
|
||||
value={service.poolSize}
|
||||
onChange={handleInputChange}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<TextField
|
||||
label="Pool Timeout"
|
||||
name="poolTimeout"
|
||||
type="number"
|
||||
value={service.poolTimeout}
|
||||
onChange={handleInputChange}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<TextField
|
||||
label="Session Max Age"
|
||||
name="sessionMaxAge"
|
||||
type="number"
|
||||
value={service.sessionMaxAge}
|
||||
onChange={handleInputChange}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<TextField
|
||||
select
|
||||
label="Reuse Sessions"
|
||||
value={service.reuseSessions}
|
||||
onChange={handleInputChange}
|
||||
fullWidth
|
||||
name="reuseSessions"
|
||||
SelectProps={{
|
||||
native: true,
|
||||
}}
|
||||
>
|
||||
{[
|
||||
{
|
||||
value: 'dontuse',
|
||||
label: 'Не использовать',
|
||||
},
|
||||
{
|
||||
value: 'use',
|
||||
label: 'Использовать',
|
||||
},
|
||||
{
|
||||
value: 'autouse',
|
||||
label: 'Авто',
|
||||
}
|
||||
].map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
|
||||
<IconButton onClick={handleDeleteConfirmationOpen} color="secondary">
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<ConfirmationDialog
|
||||
open={confirmationDialogOpen}
|
||||
title="Подтверждение удаления"
|
||||
content="Вы уверены, что хотите удалить сервис?"
|
||||
onClose={handleDeleteConfirmationClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
TextField,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
Grid,
|
||||
Switch,
|
||||
FormControlLabel,
|
||||
IconButton
|
||||
} from "@mui/material";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import ConfirmationDialog from "../../../../ConfirmationDialog"
|
||||
|
||||
export default function ServiceForm({ service = {}, onChange, onDelete, type }) {
|
||||
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
|
||||
const [nameError, setNameError] = useState(false);
|
||||
const [rootUrlError, setRootUrlError] = useState(false);
|
||||
const [aliasError, setAliasError] = useState(false);
|
||||
|
||||
const handleInputChange = (event) => {
|
||||
onChange({
|
||||
...service,
|
||||
[event.target.name]: event.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteConfirmationOpen = () => {
|
||||
setConfirmationDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteConfirmationClose = (confirmed) => {
|
||||
if (confirmed) {
|
||||
onDelete(service);
|
||||
}
|
||||
setConfirmationDialogOpen(false);
|
||||
};
|
||||
|
||||
|
||||
|
||||
const validateName = (event, value) => {
|
||||
if (value.trim() === '') {
|
||||
setNameError(true);
|
||||
event.preventDefault();
|
||||
return false;
|
||||
} else {
|
||||
setNameError(false);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const validateRootUrl = (event, value) => {
|
||||
const pattern = /^[a-zA-Z0-9-_]+$/;
|
||||
if (!pattern.test(value)) {
|
||||
setRootUrlError(true);
|
||||
event.preventDefault();
|
||||
return false;
|
||||
} else {
|
||||
setRootUrlError(false);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const validateAlias = (event, value) => {
|
||||
const pattern = /^[a-zA-Z0-9-_]+\.1cws$/;
|
||||
if (!pattern.test(value)) {
|
||||
setAliasError(true);
|
||||
event.preventDefault();
|
||||
return false;
|
||||
} else {
|
||||
setAliasError(false);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Grid container spacing={2} alignItems="center">
|
||||
<Grid item xs={12} sm={9}>
|
||||
<TextField
|
||||
label="Название"
|
||||
name="name"
|
||||
value={service.name}
|
||||
onChange={(event) => {
|
||||
handleInputChange(event);
|
||||
}}
|
||||
onBlur={(event) => validateName(event,service.name)}
|
||||
error={nameError}
|
||||
helperText={nameError ? "Поле обязательно для заполнения" : ""}
|
||||
fullWidth
|
||||
required
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={3}>
|
||||
<FormControlLabel control={<Switch
|
||||
edge="start"
|
||||
checked={service.enable}
|
||||
onChange={handleInputChange}
|
||||
/>} label="Включен" />
|
||||
|
||||
</Grid>
|
||||
{type === 'http' && (
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
label="Корневой URL"
|
||||
name="rootUrl"
|
||||
value={service.rootUrl}
|
||||
onChange={(event) => {
|
||||
handleInputChange(event);
|
||||
}}
|
||||
onBlur={(event) => validateRootUrl(event,service.rootUrl)}
|
||||
error={rootUrlError}
|
||||
helperText={rootUrlError ? "Введите корректный URL" : ""}
|
||||
fullWidth
|
||||
required
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{type === 'web' && (
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
label="Alias"
|
||||
name="alias"
|
||||
value={service.alias}
|
||||
onChange={(event) => {
|
||||
handleInputChange(event);
|
||||
}}
|
||||
onBlur={(event) => validateAlias(event, service.alias)}
|
||||
error={aliasError}
|
||||
helperText={aliasError ? "Введите корректный Alias с расширением .1cws" : ""}
|
||||
fullWidth
|
||||
required
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<TextField
|
||||
label="Pool Size"
|
||||
name="poolSize"
|
||||
type="number"
|
||||
value={service.poolSize}
|
||||
onChange={handleInputChange}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<TextField
|
||||
label="Pool Timeout"
|
||||
name="poolTimeout"
|
||||
type="number"
|
||||
value={service.poolTimeout}
|
||||
onChange={handleInputChange}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<TextField
|
||||
label="Session Max Age"
|
||||
name="sessionMaxAge"
|
||||
type="number"
|
||||
value={service.sessionMaxAge}
|
||||
onChange={handleInputChange}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<TextField
|
||||
select
|
||||
label="Reuse Sessions"
|
||||
value={service.reuseSessions}
|
||||
onChange={handleInputChange}
|
||||
fullWidth
|
||||
name="reuseSessions"
|
||||
SelectProps={{
|
||||
native: true,
|
||||
}}
|
||||
>
|
||||
{[
|
||||
{
|
||||
value: 'dontuse',
|
||||
label: 'Не использовать',
|
||||
},
|
||||
{
|
||||
value: 'use',
|
||||
label: 'Использовать',
|
||||
},
|
||||
{
|
||||
value: 'autouse',
|
||||
label: 'Авто',
|
||||
}
|
||||
].map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
|
||||
<IconButton onClick={handleDeleteConfirmationOpen} color="secondary">
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<ConfirmationDialog
|
||||
open={confirmationDialogOpen}
|
||||
title="Подтверждение удаления"
|
||||
content="Вы уверены, что хотите удалить сервис?"
|
||||
onClose={handleDeleteConfirmationClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,103 +1,103 @@
|
||||
import React, { useState,useEffect } from "react";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
Typography,
|
||||
Button,
|
||||
Grid,
|
||||
} from "@mui/material";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import ServiceForm from "./ServiceForm";
|
||||
|
||||
export default function ServicesArray({ servicesList, onChange, type }) {
|
||||
const [services, setServices] = useState(servicesList || []);
|
||||
|
||||
const handleAddService = () => {
|
||||
setServices([
|
||||
...services, type='http'?
|
||||
{
|
||||
name: "",
|
||||
rootUrl: "",
|
||||
enable: true,
|
||||
reuseSessions: "dontuse",
|
||||
sessionMaxAge: 20,
|
||||
poolSize: 10,
|
||||
poolTimeout: 5,
|
||||
}: {
|
||||
name: "",
|
||||
alias: "",
|
||||
enable: true,
|
||||
reuseSessions: "dontuse",
|
||||
sessionMaxAge: 20,
|
||||
poolSize: 10,
|
||||
poolTimeout: 5,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const [isServicesUpdated, setIsServicesUpdated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (servicesList) {
|
||||
setServices(servicesList);
|
||||
setIsServicesUpdated(true);
|
||||
}
|
||||
}, [servicesList]);
|
||||
|
||||
const handleServiceChange = (index, updatedService) => {
|
||||
const newServices = services.slice();
|
||||
newServices[index] = updatedService;
|
||||
setServices(newServices);
|
||||
onChange(newServices);
|
||||
};
|
||||
|
||||
const handleServiceDelete = (index) => {
|
||||
const newServices = services.filter((_, i) => i !== index);
|
||||
setServices(newServices);
|
||||
onChange(newServices);
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid container direction="column" spacing={2}>
|
||||
{isServicesUpdated && services.map((service, index) => (
|
||||
<Grid item key={index}>
|
||||
<Accordion>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
>
|
||||
<Grid container alignItems="center" spacing={2}>
|
||||
<Grid item xs>
|
||||
<Typography variant="h6" component="div">
|
||||
{service.name || "Новый сервис"}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<ServiceForm
|
||||
type = {type}
|
||||
service={service}
|
||||
onChange={(updatedService) =>
|
||||
handleServiceChange(index, updatedService)
|
||||
}
|
||||
onDelete={() => handleServiceDelete(index)}
|
||||
/>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</Grid>
|
||||
))}
|
||||
<Grid item>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={handleAddService}
|
||||
startIcon={<AddIcon />}
|
||||
>
|
||||
Добавить сервис
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
import React, { useState,useEffect } from "react";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
Typography,
|
||||
Button,
|
||||
Grid,
|
||||
} from "@mui/material";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import ServiceForm from "./ServiceForm";
|
||||
|
||||
export default function ServicesArray({ servicesList, onChange, type }) {
|
||||
const [services, setServices] = useState(servicesList || []);
|
||||
|
||||
const handleAddService = () => {
|
||||
setServices([
|
||||
...services, type='http'?
|
||||
{
|
||||
name: "",
|
||||
rootUrl: "",
|
||||
enable: true,
|
||||
reuseSessions: "dontuse",
|
||||
sessionMaxAge: 20,
|
||||
poolSize: 10,
|
||||
poolTimeout: 5,
|
||||
}: {
|
||||
name: "",
|
||||
alias: "",
|
||||
enable: true,
|
||||
reuseSessions: "dontuse",
|
||||
sessionMaxAge: 20,
|
||||
poolSize: 10,
|
||||
poolTimeout: 5,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const [isServicesUpdated, setIsServicesUpdated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (servicesList) {
|
||||
setServices(servicesList);
|
||||
setIsServicesUpdated(true);
|
||||
}
|
||||
}, [servicesList]);
|
||||
|
||||
const handleServiceChange = (index, updatedService) => {
|
||||
const newServices = services.slice();
|
||||
newServices[index] = updatedService;
|
||||
setServices(newServices);
|
||||
onChange(newServices);
|
||||
};
|
||||
|
||||
const handleServiceDelete = (index) => {
|
||||
const newServices = services.filter((_, i) => i !== index);
|
||||
setServices(newServices);
|
||||
onChange(newServices);
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid container direction="column" spacing={2}>
|
||||
{isServicesUpdated && services.map((service, index) => (
|
||||
<Grid item key={index}>
|
||||
<Accordion>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
>
|
||||
<Grid container alignItems="center" spacing={2}>
|
||||
<Grid item xs>
|
||||
<Typography variant="h6" component="div">
|
||||
{service.name || "Новый сервис"}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<ServiceForm
|
||||
type = {type}
|
||||
service={service}
|
||||
onChange={(updatedService) =>
|
||||
handleServiceChange(index, updatedService)
|
||||
}
|
||||
onDelete={() => handleServiceDelete(index)}
|
||||
/>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</Grid>
|
||||
))}
|
||||
<Grid item>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={handleAddService}
|
||||
startIcon={<AddIcon />}
|
||||
>
|
||||
Добавить сервис
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
@ -1,64 +1,64 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { FormControlLabel, Switch, Divider } from '@mui/material';
|
||||
import ServicesArray from "./ServicesArray/ServicesArray";
|
||||
|
||||
export default function WebSettings({ publication, onChange, onValidChange }) {
|
||||
const handleSwitchChange = (event) => {
|
||||
onChange({
|
||||
...publication,
|
||||
ws: {
|
||||
...publication.ws,
|
||||
[event.target.name]: event.target.checked,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const isServicesValid = !publication.ws.wsList ||
|
||||
publication.ws.wsList.length === 0 ||
|
||||
publication.ws.wsList.every(
|
||||
(service) => {
|
||||
return (
|
||||
service.name.trim() !== "" &&
|
||||
/^[a-zA-Z0-9-_]+\.1cws$/.test(
|
||||
service.alias
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
onValidChange(isServicesValid);
|
||||
}, [publication.ws.wsList, onValidChange]);
|
||||
|
||||
|
||||
const handleServicesChange = (newServices) => {
|
||||
onChange({
|
||||
...publication,
|
||||
ws: {
|
||||
...publication.ws,
|
||||
wsList: newServices,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={publication.ws.publishExtensionsByDefault}
|
||||
onChange={handleSwitchChange}
|
||||
name="publishExtensionsByDefault"
|
||||
/>
|
||||
}
|
||||
label="Публиковать web-сервисы расширений по умолчанию"
|
||||
/>
|
||||
<Divider />
|
||||
<div>Web-сервисы</div>
|
||||
<ServicesArray
|
||||
type="web"
|
||||
servicesList={publication.ws.wsList}
|
||||
onChange={handleServicesChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
import React, { useEffect } from 'react';
|
||||
import { FormControlLabel, Switch, Divider } from '@mui/material';
|
||||
import ServicesArray from "./ServicesArray/ServicesArray";
|
||||
|
||||
export default function WebSettings({ publication, onChange, onValidChange }) {
|
||||
const handleSwitchChange = (event) => {
|
||||
onChange({
|
||||
...publication,
|
||||
ws: {
|
||||
...publication.ws,
|
||||
[event.target.name]: event.target.checked,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const isServicesValid = !publication.ws.wsList ||
|
||||
publication.ws.wsList.length === 0 ||
|
||||
publication.ws.wsList.every(
|
||||
(service) => {
|
||||
return (
|
||||
service.name.trim() !== "" &&
|
||||
/^[a-zA-Z0-9-_]+\.1cws$/.test(
|
||||
service.alias
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
onValidChange(isServicesValid);
|
||||
}, [publication.ws.wsList, onValidChange]);
|
||||
|
||||
|
||||
const handleServicesChange = (newServices) => {
|
||||
onChange({
|
||||
...publication,
|
||||
ws: {
|
||||
...publication.ws,
|
||||
wsList: newServices,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={publication.ws.publishExtensionsByDefault}
|
||||
onChange={handleSwitchChange}
|
||||
name="publishExtensionsByDefault"
|
||||
/>
|
||||
}
|
||||
label="Публиковать web-сервисы расширений по умолчанию"
|
||||
/>
|
||||
<Divider />
|
||||
<div>Web-сервисы</div>
|
||||
<ServicesArray
|
||||
type="web"
|
||||
servicesList={publication.ws.wsList}
|
||||
onChange={handleServicesChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,220 +1,220 @@
|
||||
import React, { useState, useEffect, useContext } from "react";
|
||||
import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Tab, Tabs, Tooltip, Grid, IconButton, useMediaQuery, useTheme } from "@mui/material";
|
||||
import GeneralSettings from "./PropertiesParts/GeneralSettings";
|
||||
import HttpSettings from "./PropertiesParts/HttpSettings";
|
||||
import WebSettings from "./PropertiesParts/WebSettings";
|
||||
import OidcSettings from "./PropertiesParts/OidcSettings";
|
||||
import AppContext from '../../../context/AppContext';
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import ConfirmationDialog from "../../ConfirmationDialog";
|
||||
import SettingsIcon from "@mui/icons-material/Settings";
|
||||
import HttpIcon from "@mui/icons-material/Http";
|
||||
import WebIcon from "@mui/icons-material/Web";
|
||||
import OidcIcon from "@mui/icons-material/VerifiedUser";
|
||||
|
||||
export default function PublicationPropertiesEdit({ open, publication, onClose }) {
|
||||
const [tabIndex, setTabIndex] = React.useState(0);
|
||||
const [newPublication, setNewPublication] = useState({
|
||||
name: "",
|
||||
title: "",
|
||||
usr: "",
|
||||
pwd: "",
|
||||
active: true,
|
||||
enable: true,
|
||||
enableStandardOData: false,
|
||||
httpServices: {
|
||||
publishExtensionsByDefault: false,
|
||||
publishByDefault: false,
|
||||
hsList:[]
|
||||
},
|
||||
ws: {
|
||||
publishExtensionsByDefault: false,
|
||||
wsList: []
|
||||
}
|
||||
});
|
||||
const [update, setUpdate] = useState(false);
|
||||
const [isGeneralSettingsValid, setIsGeneralSettingsValid] = useState(false);
|
||||
const [isHttpSettingsValid, setIsHttpSettingsValid] = useState(false);
|
||||
const [isWebSettingsValid, setIsWebSettingsValid] = useState(true);
|
||||
const [isAllSettingsValid, setIsAllSettingsValid] = useState(true);
|
||||
const { getUniqueTitles, getUniqueEndpoints, addPublication, updatePublication,deletePublication } = useContext(AppContext);
|
||||
const uniqueTitles = getUniqueTitles() || [];
|
||||
const uniqueEndpoints = getUniqueEndpoints() || [];
|
||||
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
|
||||
|
||||
const theme = useTheme();
|
||||
const isLargeScreen = useMediaQuery(theme.breakpoints.up('md'));
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setTabIndex(0);
|
||||
if (!publication) {
|
||||
setNewPublication(
|
||||
{
|
||||
id: "",
|
||||
name: "",
|
||||
title: "",
|
||||
usr: "",
|
||||
pwd: "",
|
||||
active: true,
|
||||
enable: true,
|
||||
enableStandardOData: false,
|
||||
httpServices: {
|
||||
publishExtensionsByDefault: false,
|
||||
publishByDefault: false,
|
||||
hsList: []
|
||||
},
|
||||
ws: {
|
||||
publishExtensionsByDefault: false,
|
||||
wsList:[]
|
||||
},
|
||||
});
|
||||
setUpdate(false);
|
||||
} else {
|
||||
setNewPublication(publication);
|
||||
setUpdate(true);
|
||||
}
|
||||
}
|
||||
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsAllSettingsValid(isHttpSettingsValid && isWebSettingsValid && isGeneralSettingsValid);
|
||||
|
||||
}, [isHttpSettingsValid, isWebSettingsValid, isGeneralSettingsValid]);
|
||||
useEffect(() => {
|
||||
handleGeneralSettingsValidChange(
|
||||
validateTitle(newPublication.title) && validateName(newPublication.name)
|
||||
);
|
||||
}, [newPublication]);
|
||||
|
||||
const handleTabChange = (event, newValue) => {
|
||||
setTabIndex(newValue);
|
||||
};
|
||||
|
||||
const handleChange = (updatedPublication) => {
|
||||
setNewPublication(updatedPublication);
|
||||
};
|
||||
|
||||
const handleGeneralSettingsValidChange = (isValid) => {
|
||||
setIsGeneralSettingsValid(isValid);
|
||||
};
|
||||
|
||||
|
||||
const handleHttpSettingsValidChange = (isValid) => {
|
||||
setIsHttpSettingsValid(isValid);
|
||||
};
|
||||
|
||||
|
||||
const handleWebSettingsValidChange = (isValid) => {
|
||||
setIsWebSettingsValid(isValid);
|
||||
};
|
||||
|
||||
const validateTitle = (title) => {
|
||||
const otherUniqueTitles = uniqueTitles.filter((t) => t !== newPublication?.title);
|
||||
return title && !otherUniqueTitles.includes(title);
|
||||
};
|
||||
|
||||
const validateName = (name) => {
|
||||
const urlPattern = /^[a-zA-Z0-9-_]+$/;
|
||||
const otherUniqueEndpoints = uniqueEndpoints.filter((e) => e !== newPublication?.name);
|
||||
return name && !otherUniqueEndpoints.includes(name) && urlPattern.test(name);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (update) {
|
||||
updatePublication(publication.id, newPublication);
|
||||
} else {
|
||||
addPublication(newPublication);
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleDeleteConfirmationOpen = () => {
|
||||
setConfirmationDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteConfirmationClose = (confirmed) => {
|
||||
if (confirmed) {
|
||||
deletePublication(publication);
|
||||
onClose(null);
|
||||
}
|
||||
setConfirmationDialogOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dialog open={open} onClose={onClose} fullWidth maxWidth="md">
|
||||
<DialogTitle>{update ? "Редактирование публикации: " + newPublication.title : "Создать новую публикацию"}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable" scrollButtons allowScrollButtonsMobile>
|
||||
<Tab icon={<SettingsIcon />} label={isLargeScreen ? "Основные настройки" : ""} />
|
||||
<Tab icon={<HttpIcon />} label={isLargeScreen ? "Http-сервисы" : ""} />
|
||||
<Tab icon={<WebIcon />} label={isLargeScreen ? "Web-сервисы" : ""} />
|
||||
<Tab icon={<OidcIcon />} label={isLargeScreen ? "Настройки OpenID Connect" : ""} />
|
||||
</Tabs>
|
||||
{newPublication && (
|
||||
<>
|
||||
{tabIndex === 0 && <GeneralSettings publication={newPublication}
|
||||
onChange={handleChange}
|
||||
onValidChange={handleGeneralSettingsValidChange}
|
||||
validateTitle={validateTitle}
|
||||
validateName={validateName}
|
||||
/>}
|
||||
{tabIndex === 1 && <HttpSettings publication={newPublication}
|
||||
onChange={handleChange}
|
||||
onValidChange={handleHttpSettingsValidChange}/>}
|
||||
{tabIndex === 2 && <WebSettings publication={newPublication}
|
||||
onChange={handleChange}
|
||||
onValidChange={setIsWebSettingsValid}/>}
|
||||
{tabIndex === 3 && <OidcSettings publication={newPublication}
|
||||
onChange={handleChange} />}
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Grid container justifyContent="space-between" alignItems="flex-start">
|
||||
{update && (
|
||||
<Grid item>
|
||||
<IconButton onClick={handleDeleteConfirmationOpen} color="secondary">
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs style={{ flexGrow: 1 }} />
|
||||
<Grid item>
|
||||
<Button onClick={onClose}>Отмена</Button>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Tooltip
|
||||
title={
|
||||
isAllSettingsValid
|
||||
? ""
|
||||
: "Пожалуйста, исправьте ошибки перед сохранением"
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
color="primary"
|
||||
disabled={!isAllSettingsValid}
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogActions >
|
||||
</Dialog >
|
||||
<ConfirmationDialog
|
||||
open={confirmationDialogOpen}
|
||||
title="Подтверждение удаления"
|
||||
content="Вы уверены, что хотите удалить эту публикацию?"
|
||||
onClose={handleDeleteConfirmationClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import React, { useState, useEffect, useContext } from "react";
|
||||
import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Tab, Tabs, Tooltip, Grid, IconButton, useMediaQuery, useTheme } from "@mui/material";
|
||||
import GeneralSettings from "./PropertiesParts/GeneralSettings";
|
||||
import HttpSettings from "./PropertiesParts/HttpSettings";
|
||||
import WebSettings from "./PropertiesParts/WebSettings";
|
||||
import OidcSettings from "./PropertiesParts/OidcSettings";
|
||||
import AppContext from '../../../context/AppContext';
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import ConfirmationDialog from "../../ConfirmationDialog";
|
||||
import SettingsIcon from "@mui/icons-material/Settings";
|
||||
import HttpIcon from "@mui/icons-material/Http";
|
||||
import WebIcon from "@mui/icons-material/Web";
|
||||
import OidcIcon from "@mui/icons-material/VerifiedUser";
|
||||
|
||||
export default function PublicationPropertiesEdit({ open, publication, onClose }) {
|
||||
const [tabIndex, setTabIndex] = React.useState(0);
|
||||
const [newPublication, setNewPublication] = useState({
|
||||
name: "",
|
||||
title: "",
|
||||
usr: "",
|
||||
pwd: "",
|
||||
active: true,
|
||||
enable: true,
|
||||
enableStandardOData: false,
|
||||
httpServices: {
|
||||
publishExtensionsByDefault: false,
|
||||
publishByDefault: false,
|
||||
hsList:[]
|
||||
},
|
||||
ws: {
|
||||
publishExtensionsByDefault: false,
|
||||
wsList: []
|
||||
}
|
||||
});
|
||||
const [update, setUpdate] = useState(false);
|
||||
const [isGeneralSettingsValid, setIsGeneralSettingsValid] = useState(false);
|
||||
const [isHttpSettingsValid, setIsHttpSettingsValid] = useState(false);
|
||||
const [isWebSettingsValid, setIsWebSettingsValid] = useState(true);
|
||||
const [isAllSettingsValid, setIsAllSettingsValid] = useState(true);
|
||||
const { getUniqueTitles, getUniqueEndpoints, addPublication, updatePublication,deletePublication } = useContext(AppContext);
|
||||
const uniqueTitles = getUniqueTitles() || [];
|
||||
const uniqueEndpoints = getUniqueEndpoints() || [];
|
||||
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
|
||||
|
||||
const theme = useTheme();
|
||||
const isLargeScreen = useMediaQuery(theme.breakpoints.up('md'));
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setTabIndex(0);
|
||||
if (!publication) {
|
||||
setNewPublication(
|
||||
{
|
||||
id: "",
|
||||
name: "",
|
||||
title: "",
|
||||
usr: "",
|
||||
pwd: "",
|
||||
active: true,
|
||||
enable: true,
|
||||
enableStandardOData: false,
|
||||
httpServices: {
|
||||
publishExtensionsByDefault: false,
|
||||
publishByDefault: false,
|
||||
hsList: []
|
||||
},
|
||||
ws: {
|
||||
publishExtensionsByDefault: false,
|
||||
wsList:[]
|
||||
},
|
||||
});
|
||||
setUpdate(false);
|
||||
} else {
|
||||
setNewPublication(publication);
|
||||
setUpdate(true);
|
||||
}
|
||||
}
|
||||
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsAllSettingsValid(isHttpSettingsValid && isWebSettingsValid && isGeneralSettingsValid);
|
||||
|
||||
}, [isHttpSettingsValid, isWebSettingsValid, isGeneralSettingsValid]);
|
||||
useEffect(() => {
|
||||
handleGeneralSettingsValidChange(
|
||||
validateTitle(newPublication.title) && validateName(newPublication.name)
|
||||
);
|
||||
}, [newPublication]);
|
||||
|
||||
const handleTabChange = (event, newValue) => {
|
||||
setTabIndex(newValue);
|
||||
};
|
||||
|
||||
const handleChange = (updatedPublication) => {
|
||||
setNewPublication(updatedPublication);
|
||||
};
|
||||
|
||||
const handleGeneralSettingsValidChange = (isValid) => {
|
||||
setIsGeneralSettingsValid(isValid);
|
||||
};
|
||||
|
||||
|
||||
const handleHttpSettingsValidChange = (isValid) => {
|
||||
setIsHttpSettingsValid(isValid);
|
||||
};
|
||||
|
||||
|
||||
const handleWebSettingsValidChange = (isValid) => {
|
||||
setIsWebSettingsValid(isValid);
|
||||
};
|
||||
|
||||
const validateTitle = (title) => {
|
||||
const otherUniqueTitles = uniqueTitles.filter((t) => t !== newPublication?.title);
|
||||
return title && !otherUniqueTitles.includes(title);
|
||||
};
|
||||
|
||||
const validateName = (name) => {
|
||||
const urlPattern = /^[a-zA-Z0-9-_]+$/;
|
||||
const otherUniqueEndpoints = uniqueEndpoints.filter((e) => e !== newPublication?.name);
|
||||
return name && !otherUniqueEndpoints.includes(name) && urlPattern.test(name);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (update) {
|
||||
updatePublication(publication.id, newPublication);
|
||||
} else {
|
||||
addPublication(newPublication);
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleDeleteConfirmationOpen = () => {
|
||||
setConfirmationDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteConfirmationClose = (confirmed) => {
|
||||
if (confirmed) {
|
||||
deletePublication(publication);
|
||||
onClose(null);
|
||||
}
|
||||
setConfirmationDialogOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dialog open={open} onClose={onClose} fullWidth maxWidth="md">
|
||||
<DialogTitle>{update ? "Редактирование публикации: " + newPublication.title : "Создать новую публикацию"}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable" scrollButtons allowScrollButtonsMobile>
|
||||
<Tab icon={<SettingsIcon />} label={isLargeScreen ? "Основные настройки" : ""} />
|
||||
<Tab icon={<HttpIcon />} label={isLargeScreen ? "Http-сервисы" : ""} />
|
||||
<Tab icon={<WebIcon />} label={isLargeScreen ? "Web-сервисы" : ""} />
|
||||
<Tab icon={<OidcIcon />} label={isLargeScreen ? "Настройки OpenID Connect" : ""} />
|
||||
</Tabs>
|
||||
{newPublication && (
|
||||
<>
|
||||
{tabIndex === 0 && <GeneralSettings publication={newPublication}
|
||||
onChange={handleChange}
|
||||
onValidChange={handleGeneralSettingsValidChange}
|
||||
validateTitle={validateTitle}
|
||||
validateName={validateName}
|
||||
/>}
|
||||
{tabIndex === 1 && <HttpSettings publication={newPublication}
|
||||
onChange={handleChange}
|
||||
onValidChange={handleHttpSettingsValidChange}/>}
|
||||
{tabIndex === 2 && <WebSettings publication={newPublication}
|
||||
onChange={handleChange}
|
||||
onValidChange={setIsWebSettingsValid}/>}
|
||||
{tabIndex === 3 && <OidcSettings publication={newPublication}
|
||||
onChange={handleChange} />}
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Grid container justifyContent="space-between" alignItems="flex-start">
|
||||
{update && (
|
||||
<Grid item>
|
||||
<IconButton onClick={handleDeleteConfirmationOpen} color="secondary">
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs style={{ flexGrow: 1 }} />
|
||||
<Grid item>
|
||||
<Button onClick={onClose}>Отмена</Button>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Tooltip
|
||||
title={
|
||||
isAllSettingsValid
|
||||
? ""
|
||||
: "Пожалуйста, исправьте ошибки перед сохранением"
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
color="primary"
|
||||
disabled={!isAllSettingsValid}
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogActions >
|
||||
</Dialog >
|
||||
<ConfirmationDialog
|
||||
open={confirmationDialogOpen}
|
||||
title="Подтверждение удаления"
|
||||
content="Вы уверены, что хотите удалить эту публикацию?"
|
||||
onClose={handleDeleteConfirmationClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,114 +1,114 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { Box, Card, CardContent, Typography, CardActions, IconButton, Switch, Fab, Grid, useMediaQuery, useTheme, Link } from "@mui/material";
|
||||
import EditIcon from "@mui/icons-material/Edit";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import LinkIcon from "@mui/icons-material/Link";
|
||||
import AppContext from "../../context/AppContext";
|
||||
import PublicationPropertiesEdit from "./PublicationPropertiesEdit/PublicationPropertiesEdit";
|
||||
|
||||
export default function PublicationsInfo() {
|
||||
const { state, updatePublication } = useContext(AppContext);
|
||||
const selectedBase = state.bases.find((base) => base.id === state.selectedBaseId);
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||
const [editingPublication, setEditingPublication] = useState(null);
|
||||
const theme = useTheme();
|
||||
const isMdDown = useMediaQuery(theme.breakpoints.down("md"));
|
||||
|
||||
const createPublicationUrl = (baseUrl, publicationName) => {
|
||||
const cleanedBaseUrl = baseUrl.replace(/\/+$/, ""); // убираем слэши в конце строки
|
||||
const cleanedPublicationName = publicationName.replace(/^\/+/, ""); // убираем слэши в начале строки
|
||||
return `${cleanedBaseUrl}/${cleanedPublicationName}`;
|
||||
};
|
||||
|
||||
const handlePublicationToggle = (publication) => {
|
||||
const updatedPublication = {
|
||||
...publication,
|
||||
active: !publication.active,
|
||||
};
|
||||
updatePublication(publication.id, updatedPublication);
|
||||
};
|
||||
|
||||
const handleEditDialogOpen = (publication) => {
|
||||
setEditingPublication(publication);
|
||||
setEditDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleEditDialogClose = (publication) => {
|
||||
setEditDialogOpen(false);
|
||||
setEditingPublication(null);
|
||||
};
|
||||
|
||||
if (!selectedBase) {
|
||||
return <Typography variant="h6">Выберите базу данных</Typography>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12}>
|
||||
<Fab
|
||||
variant={isMdDown ? "circular" : "extended"}
|
||||
color="primary"
|
||||
onClick={() => handleEditDialogOpen(null)}
|
||||
sx={{
|
||||
position: "fixed",
|
||||
top: 72,
|
||||
right: 24,
|
||||
}}
|
||||
>
|
||||
<AddIcon />
|
||||
{!isMdDown && "Новая публикация"}
|
||||
</Fab>
|
||||
</Grid>
|
||||
{selectedBase.publications.map((publication) => (
|
||||
<Grid item key={publication.id} xs={12} sm={6} md={6} lg={4} sx={{ marginTop: theme.spacing(5) }}>
|
||||
<Card key={publication.id} sx={{ maxWidth: 400, mb: 2 }}>
|
||||
<CardContent>
|
||||
<Typography variant="h5" component="div">
|
||||
{publication.title}
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" color="text.secondary">
|
||||
<LinkIcon sx={{ verticalAlign: "bottom", mr: 0.5 }} />
|
||||
{state.settings && state.settings.publicationServerUrl && publication.active ? (
|
||||
<Link
|
||||
href={createPublicationUrl(state.settings.publicationServerUrl, publication.name)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Ссылка на публикацию
|
||||
</Link>
|
||||
) : (
|
||||
<span style={{ color: "rgba(0, 0, 0, 0.38)" }}>Ссылка на публикацию</span>
|
||||
)}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" width="100%">
|
||||
<Switch
|
||||
checked={publication.active}
|
||||
onChange={() => handlePublicationToggle(publication)}
|
||||
inputProps={{ "aria-label": "controlled" }}
|
||||
/>
|
||||
<IconButton
|
||||
edge="end"
|
||||
color="inherit"
|
||||
aria-label="edit"
|
||||
onClick={() => handleEditDialogOpen(publication)}
|
||||
sx={{ mr: 0.1 }}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
<PublicationPropertiesEdit
|
||||
open={editDialogOpen}
|
||||
publication={editingPublication}
|
||||
onClose={handleEditDialogClose}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
import React, { useContext, useState } from "react";
|
||||
import { Box, Card, CardContent, Typography, CardActions, IconButton, Switch, Fab, Grid, useMediaQuery, useTheme, Link } from "@mui/material";
|
||||
import EditIcon from "@mui/icons-material/Edit";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import LinkIcon from "@mui/icons-material/Link";
|
||||
import AppContext from "../../context/AppContext";
|
||||
import PublicationPropertiesEdit from "./PublicationPropertiesEdit/PublicationPropertiesEdit";
|
||||
|
||||
export default function PublicationsInfo() {
|
||||
const { state, updatePublication } = useContext(AppContext);
|
||||
const selectedBase = state.bases.find((base) => base.id === state.selectedBaseId);
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||
const [editingPublication, setEditingPublication] = useState(null);
|
||||
const theme = useTheme();
|
||||
const isMdDown = useMediaQuery(theme.breakpoints.down("md"));
|
||||
|
||||
const createPublicationUrl = (baseUrl, publicationName) => {
|
||||
const cleanedBaseUrl = baseUrl.replace(/\/+$/, ""); // убираем слэши в конце строки
|
||||
const cleanedPublicationName = publicationName.replace(/^\/+/, ""); // убираем слэши в начале строки
|
||||
return `${cleanedBaseUrl}/${cleanedPublicationName}`;
|
||||
};
|
||||
|
||||
const handlePublicationToggle = (publication) => {
|
||||
const updatedPublication = {
|
||||
...publication,
|
||||
active: !publication.active,
|
||||
};
|
||||
updatePublication(publication.id, updatedPublication);
|
||||
};
|
||||
|
||||
const handleEditDialogOpen = (publication) => {
|
||||
setEditingPublication(publication);
|
||||
setEditDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleEditDialogClose = (publication) => {
|
||||
setEditDialogOpen(false);
|
||||
setEditingPublication(null);
|
||||
};
|
||||
|
||||
if (!selectedBase) {
|
||||
return <Typography variant="h6">Выберите базу данных</Typography>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12}>
|
||||
<Fab
|
||||
variant={isMdDown ? "circular" : "extended"}
|
||||
color="primary"
|
||||
onClick={() => handleEditDialogOpen(null)}
|
||||
sx={{
|
||||
position: "fixed",
|
||||
top: 72,
|
||||
right: 24,
|
||||
}}
|
||||
>
|
||||
<AddIcon />
|
||||
{!isMdDown && "Новая публикация"}
|
||||
</Fab>
|
||||
</Grid>
|
||||
{selectedBase.publications.map((publication) => (
|
||||
<Grid item key={publication.id} xs={12} sm={6} md={6} lg={4} sx={{ marginTop: theme.spacing(5) }}>
|
||||
<Card key={publication.id} sx={{ maxWidth: 400, mb: 2 }}>
|
||||
<CardContent>
|
||||
<Typography variant="h5" component="div">
|
||||
{publication.title}
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" color="text.secondary">
|
||||
<LinkIcon sx={{ verticalAlign: "bottom", mr: 0.5 }} />
|
||||
{state.settings && state.settings.publicationServerUrl && publication.active ? (
|
||||
<Link
|
||||
href={createPublicationUrl(state.settings.publicationServerUrl, publication.name)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Ссылка на публикацию
|
||||
</Link>
|
||||
) : (
|
||||
<span style={{ color: "rgba(0, 0, 0, 0.38)" }}>Ссылка на публикацию</span>
|
||||
)}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" width="100%">
|
||||
<Switch
|
||||
checked={publication.active}
|
||||
onChange={() => handlePublicationToggle(publication)}
|
||||
inputProps={{ "aria-label": "controlled" }}
|
||||
/>
|
||||
<IconButton
|
||||
edge="end"
|
||||
color="inherit"
|
||||
aria-label="edit"
|
||||
onClick={() => handleEditDialogOpen(publication)}
|
||||
sx={{ mr: 0.1 }}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
<PublicationPropertiesEdit
|
||||
open={editDialogOpen}
|
||||
publication={editingPublication}
|
||||
onClose={handleEditDialogClose}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
@ -1,45 +1,45 @@
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import { IconButton } from "@mui/material";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import { useSnackbar } from "notistack";
|
||||
|
||||
const SnackbarHandler = ({ snackbar, closeSnackbar }) => {
|
||||
const { enqueueSnackbar, closeSnackbar: dequeueSnackbar } = useSnackbar();
|
||||
|
||||
const action = useCallback(
|
||||
(key) => (
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label="close"
|
||||
color="inherit"
|
||||
onClick={() => {
|
||||
closeSnackbar(key);
|
||||
dequeueSnackbar(key);
|
||||
}}
|
||||
>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
),
|
||||
[closeSnackbar, dequeueSnackbar]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (snackbar) {
|
||||
const key = enqueueSnackbar(snackbar.message, {
|
||||
variant: snackbar.severity,
|
||||
autoHideDuration: 6000,
|
||||
anchorOrigin: {
|
||||
vertical: "bottom",
|
||||
horizontal: "right",
|
||||
},
|
||||
action: (key) => action(key),
|
||||
});
|
||||
|
||||
closeSnackbar(key);
|
||||
}
|
||||
}, [snackbar, closeSnackbar, enqueueSnackbar, action]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import { IconButton } from "@mui/material";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import { useSnackbar } from "notistack";
|
||||
|
||||
const SnackbarHandler = ({ snackbar, closeSnackbar }) => {
|
||||
const { enqueueSnackbar, closeSnackbar: dequeueSnackbar } = useSnackbar();
|
||||
|
||||
const action = useCallback(
|
||||
(key) => (
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label="close"
|
||||
color="inherit"
|
||||
onClick={() => {
|
||||
closeSnackbar(key);
|
||||
dequeueSnackbar(key);
|
||||
}}
|
||||
>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
),
|
||||
[closeSnackbar, dequeueSnackbar]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (snackbar) {
|
||||
const key = enqueueSnackbar(snackbar.message, {
|
||||
variant: snackbar.severity,
|
||||
autoHideDuration: 6000,
|
||||
anchorOrigin: {
|
||||
vertical: "bottom",
|
||||
horizontal: "right",
|
||||
},
|
||||
action: (key) => action(key),
|
||||
});
|
||||
|
||||
closeSnackbar(key);
|
||||
}
|
||||
}, [snackbar, closeSnackbar, enqueueSnackbar, action]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default SnackbarHandler;
|
@ -1,103 +1,103 @@
|
||||
// AppBarMenu.js
|
||||
import React, { useState, useContext } from "react";
|
||||
import { AppBar, Toolbar, IconButton, Button, Menu, Box, Typography, Hidden, Tooltip } from "@mui/material";
|
||||
import MenuIcon from "@mui/icons-material/Menu";
|
||||
import SettingsIcon from "@mui/icons-material/Settings";
|
||||
import SettingsModal from "./components/SettingsModal";
|
||||
import InfoModal from "./components/InfoModal";
|
||||
import DropdownMenu from "./components/DropdownMenu";
|
||||
import SettingsEthernetIcon from "@mui/icons-material/SettingsEthernet";
|
||||
import InfoIcon from "@mui/icons-material/Info";
|
||||
import Brightness4Icon from "@mui/icons-material/Brightness4";
|
||||
import Brightness7Icon from "@mui/icons-material/Brightness7";
|
||||
import ThemeContext from ".//../../ThemeContext";
|
||||
|
||||
const AppBarMenu = ({ handleDrawerToggle }) => {
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
const [infoModalOpen, setInfoModalOpen] = useState(false);
|
||||
const [serverControlAnchorEl, setServerControlAnchorEl] = useState(null);
|
||||
const { darkMode, toggleDarkMode } = useContext(ThemeContext);
|
||||
|
||||
const handleSettingsOpen = (event) => {
|
||||
setSettingsOpen(true);
|
||||
};
|
||||
|
||||
const handleSettingsClose = () => {
|
||||
setSettingsOpen(false);
|
||||
};
|
||||
|
||||
const handleServerControlClick = (event) => {
|
||||
setServerControlAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleServerControlClose = () => {
|
||||
setServerControlAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleInfoModalOpen = () => {
|
||||
setInfoModalOpen(true);
|
||||
};
|
||||
|
||||
const handleInfoModalClose = () => {
|
||||
setInfoModalOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<AppBar position="static" sx={{ zIndex: (theme) => theme.zIndex.drawer + 1, Height: (theme) => theme.mixins.toolbar.minHeight }}>
|
||||
<Toolbar>
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: (theme) => theme.mixins.toolbar.minHeight,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
edge="start"
|
||||
color="inherit"
|
||||
aria-label="menu"
|
||||
onClick={handleDrawerToggle}
|
||||
sx={{ display: { sm: "block", md: "none" }, }}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
{darkMode ?
|
||||
<img src="/logo_dark.png" alt="Logo" style={{ maxHeight: '55px', marginRight: '8px' }} /> :
|
||||
<img src="/logo.png" alt="Logo" style={{ maxHeight: '55px', marginRight: '8px' }} />
|
||||
}
|
||||
<Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}>
|
||||
Публикатор 1с.
|
||||
</Typography>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
|
||||
<Button color="inherit" onClick={handleServerControlClick} startIcon={<SettingsEthernetIcon />}>
|
||||
<Hidden mdDown>Управление веб-сервером</Hidden>
|
||||
</Button>
|
||||
<Menu
|
||||
anchorEl={serverControlAnchorEl}
|
||||
open={Boolean(serverControlAnchorEl)}
|
||||
onClose={handleServerControlClose}
|
||||
>
|
||||
<DropdownMenu />
|
||||
</Menu>
|
||||
<Button color="inherit" onClick={handleSettingsOpen} startIcon={<SettingsIcon />}>
|
||||
<Hidden mdDown>Настройки</Hidden>
|
||||
</Button>
|
||||
<Button color="inherit" onClick={handleInfoModalOpen} startIcon={<InfoIcon />}>
|
||||
<Hidden mdDown>Информация</Hidden>
|
||||
</Button>
|
||||
<Tooltip title={darkMode ? "Светлая тема" : "Темная тема"}>
|
||||
<IconButton onClick={toggleDarkMode} color="inherit">
|
||||
{darkMode ? <Brightness7Icon /> : <Brightness4Icon />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<SettingsModal open={settingsOpen} onClose={handleSettingsClose} />
|
||||
<InfoModal open={infoModalOpen} onClose={handleInfoModalClose} />
|
||||
</Box>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
);
|
||||
};
|
||||
|
||||
// AppBarMenu.js
|
||||
import React, { useState, useContext } from "react";
|
||||
import { AppBar, Toolbar, IconButton, Button, Menu, Box, Typography, Hidden, Tooltip } from "@mui/material";
|
||||
import MenuIcon from "@mui/icons-material/Menu";
|
||||
import SettingsIcon from "@mui/icons-material/Settings";
|
||||
import SettingsModal from "./components/SettingsModal";
|
||||
import InfoModal from "./components/InfoModal";
|
||||
import DropdownMenu from "./components/DropdownMenu";
|
||||
import SettingsEthernetIcon from "@mui/icons-material/SettingsEthernet";
|
||||
import InfoIcon from "@mui/icons-material/Info";
|
||||
import Brightness4Icon from "@mui/icons-material/Brightness4";
|
||||
import Brightness7Icon from "@mui/icons-material/Brightness7";
|
||||
import ThemeContext from ".//../../ThemeContext";
|
||||
|
||||
const AppBarMenu = ({ handleDrawerToggle }) => {
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
const [infoModalOpen, setInfoModalOpen] = useState(false);
|
||||
const [serverControlAnchorEl, setServerControlAnchorEl] = useState(null);
|
||||
const { darkMode, toggleDarkMode } = useContext(ThemeContext);
|
||||
|
||||
const handleSettingsOpen = (event) => {
|
||||
setSettingsOpen(true);
|
||||
};
|
||||
|
||||
const handleSettingsClose = () => {
|
||||
setSettingsOpen(false);
|
||||
};
|
||||
|
||||
const handleServerControlClick = (event) => {
|
||||
setServerControlAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleServerControlClose = () => {
|
||||
setServerControlAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleInfoModalOpen = () => {
|
||||
setInfoModalOpen(true);
|
||||
};
|
||||
|
||||
const handleInfoModalClose = () => {
|
||||
setInfoModalOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<AppBar position="static" sx={{ zIndex: (theme) => theme.zIndex.drawer + 1, Height: (theme) => theme.mixins.toolbar.minHeight }}>
|
||||
<Toolbar>
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: (theme) => theme.mixins.toolbar.minHeight,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
edge="start"
|
||||
color="inherit"
|
||||
aria-label="menu"
|
||||
onClick={handleDrawerToggle}
|
||||
sx={{ display: { sm: "block", md: "none" }, }}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
{darkMode ?
|
||||
<img src="/logo_dark.png" alt="Logo" style={{ maxHeight: '55px', marginRight: '8px' }} /> :
|
||||
<img src="/logo.png" alt="Logo" style={{ maxHeight: '55px', marginRight: '8px' }} />
|
||||
}
|
||||
<Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}>
|
||||
Публикатор 1с.
|
||||
</Typography>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
|
||||
<Button color="inherit" onClick={handleServerControlClick} startIcon={<SettingsEthernetIcon />}>
|
||||
<Hidden mdDown>Управление веб-сервером</Hidden>
|
||||
</Button>
|
||||
<Menu
|
||||
anchorEl={serverControlAnchorEl}
|
||||
open={Boolean(serverControlAnchorEl)}
|
||||
onClose={handleServerControlClose}
|
||||
>
|
||||
<DropdownMenu />
|
||||
</Menu>
|
||||
<Button color="inherit" onClick={handleSettingsOpen} startIcon={<SettingsIcon />}>
|
||||
<Hidden mdDown>Настройки</Hidden>
|
||||
</Button>
|
||||
<Button color="inherit" onClick={handleInfoModalOpen} startIcon={<InfoIcon />}>
|
||||
<Hidden mdDown>Информация</Hidden>
|
||||
</Button>
|
||||
<Tooltip title={darkMode ? "Светлая тема" : "Темная тема"}>
|
||||
<IconButton onClick={toggleDarkMode} color="inherit">
|
||||
{darkMode ? <Brightness7Icon /> : <Brightness4Icon />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<SettingsModal open={settingsOpen} onClose={handleSettingsClose} />
|
||||
<InfoModal open={infoModalOpen} onClose={handleInfoModalClose} />
|
||||
</Box>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppBarMenu;
|
@ -1,31 +1,31 @@
|
||||
// components/DropdownMenu.js
|
||||
import React, { useContext } from "react";
|
||||
import { MenuItem } from "@mui/material";
|
||||
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
|
||||
import StopIcon from "@mui/icons-material/Stop";
|
||||
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||
import AppContext from "../../../context/AppContext";
|
||||
import { startServer, stopServer, restartServer } from "../../../ApiProcessor";
|
||||
|
||||
const DropdownMenu = () => {
|
||||
const { showSnackbar } = useContext(AppContext);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuItem onClick={() => startServer(showSnackbar)}>
|
||||
<PlayArrowIcon />
|
||||
Старт сервера
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => stopServer(showSnackbar)}>
|
||||
<StopIcon />
|
||||
Стоп сервера
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => restartServer(showSnackbar)}>
|
||||
<RefreshIcon />
|
||||
Рестарт сервера
|
||||
</MenuItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DropdownMenu;
|
||||
// components/DropdownMenu.js
|
||||
import React, { useContext } from "react";
|
||||
import { MenuItem } from "@mui/material";
|
||||
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
|
||||
import StopIcon from "@mui/icons-material/Stop";
|
||||
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||
import AppContext from "../../../context/AppContext";
|
||||
import { startServer, stopServer, restartServer } from "../../../ApiProcessor";
|
||||
|
||||
const DropdownMenu = () => {
|
||||
const { showSnackbar } = useContext(AppContext);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuItem onClick={() => startServer(showSnackbar)}>
|
||||
<PlayArrowIcon />
|
||||
Старт сервера
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => stopServer(showSnackbar)}>
|
||||
<StopIcon />
|
||||
Стоп сервера
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => restartServer(showSnackbar)}>
|
||||
<RefreshIcon />
|
||||
Рестарт сервера
|
||||
</MenuItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DropdownMenu;
|
||||
|
@ -1,43 +1,43 @@
|
||||
import * as React from 'react';
|
||||
import { Typography, Button, Dialog, DialogTitle, DialogContent, DialogActions, Box, Link } from '@mui/material';
|
||||
import TelegramIcon from '@mui/icons-material/Telegram';
|
||||
import GitHubIcon from '@mui/icons-material/GitHub';
|
||||
|
||||
|
||||
function ProjectInfoForm({ open, onClose }) {
|
||||
const projectName = 'Публикатор 1С';
|
||||
const projectVersion = '1.1.0';
|
||||
const projectDescription = 'Проект преднозначен для удобной работы с публикациями на Веб-сервере. Код полностью открыт и доступен! \n Присоединяйтесь в катанл в телеграм, чтобы не пропутсить критичные обновления.';
|
||||
const telegramUrl = 'https://t.me/DevOps_onec';
|
||||
const githubUrl = 'https://github.com/Segate-ekb/publicator';
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>Информация о проекте</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box display="flex" alignItems="center" justifyContent="center" mb={2}>
|
||||
<img src="/logo.png" alt="Logo" style={{ maxHeight: '80px', marginRight: '8px' }} />
|
||||
<Typography variant="h5">{projectName}</Typography>
|
||||
</Box>
|
||||
<Typography variant="subtitle1">Версия: {projectVersion}</Typography>
|
||||
<Typography variant="body1" mt={2}>{projectDescription}</Typography>
|
||||
<Box mt={2}>
|
||||
<Typography variant="subtitle2">Ссылки:</Typography>
|
||||
<Box display="flex" alignItems="center">
|
||||
<Link href={telegramUrl} target="_blank" rel="noopener noreferrer" sx={{ marginRight: 2 }}>
|
||||
<TelegramIcon color="primary" fontSize="large" />
|
||||
</Link>
|
||||
<Link href={githubUrl} target="_blank" rel="noopener noreferrer">
|
||||
<GitHubIcon color="action" fontSize="large" />
|
||||
</Link>
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>Ок</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
import * as React from 'react';
|
||||
import { Typography, Button, Dialog, DialogTitle, DialogContent, DialogActions, Box, Link } from '@mui/material';
|
||||
import TelegramIcon from '@mui/icons-material/Telegram';
|
||||
import GitHubIcon from '@mui/icons-material/GitHub';
|
||||
|
||||
|
||||
function ProjectInfoForm({ open, onClose }) {
|
||||
const projectName = 'Публикатор 1С';
|
||||
const projectVersion = '1.0.0';
|
||||
const projectDescription = 'Проект преднозначен для удобной работы с публикациями на Веб-сервере. Код полностью открыт и доступен! \n Присоединяйтесь в катанл в телеграм, чтобы не пропутсить критичные обновления.';
|
||||
const telegramUrl = 'https://t.me/DevOps_onec';
|
||||
const githubUrl = 'https://github.com/Segate-ekb/publicator';
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>Информация о проекте</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box display="flex" alignItems="center" justifyContent="center" mb={2}>
|
||||
<img src="/logo.png" alt="Logo" style={{ maxHeight: '80px', marginRight: '8px' }} />
|
||||
<Typography variant="h5">{projectName}</Typography>
|
||||
</Box>
|
||||
<Typography variant="subtitle1">Версия: {projectVersion}</Typography>
|
||||
<Typography variant="body1" mt={2}>{projectDescription}</Typography>
|
||||
<Box mt={2}>
|
||||
<Typography variant="subtitle2">Ссылки:</Typography>
|
||||
<Box display="flex" alignItems="center">
|
||||
<Link href={telegramUrl} target="_blank" rel="noopener noreferrer" sx={{ marginRight: 2 }}>
|
||||
<TelegramIcon color="primary" fontSize="large" />
|
||||
</Link>
|
||||
<Link href={githubUrl} target="_blank" rel="noopener noreferrer">
|
||||
<GitHubIcon color="action" fontSize="large" />
|
||||
</Link>
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>Ок</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProjectInfoForm;
|
@ -1,78 +1,78 @@
|
||||
import React, { useContext, useState, useEffect } from "react";
|
||||
import { Dialog, DialogTitle, DialogContent, DialogActions, Button, TextField } from "@mui/material";
|
||||
import AppContext from "../../../context/AppContext";
|
||||
|
||||
const SettingsModal = ({ open, onClose }) => {
|
||||
const { state, updateSettings } = useContext(AppContext);
|
||||
const [settings, setSettings] = useState(state.settings);
|
||||
const [errors, setErrors] = useState({ onecVersion: false });
|
||||
|
||||
const handleChange = (e) => {
|
||||
setSettings({ ...settings, [e.target.name]: e.target.value });
|
||||
};
|
||||
|
||||
const validateOneCVersion = (value) => {
|
||||
const pattern = /^8\.3\.\d{2}\.\d{4}$/;
|
||||
return pattern.test(value);
|
||||
};
|
||||
|
||||
const handleOneCVersionChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
const isValid = validateOneCVersion(value);
|
||||
setErrors({ ...errors, onecVersion: !isValid });
|
||||
handleChange(e);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (!errors.onecVersion) {
|
||||
updateSettings(settings);
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSettings(state.settings);
|
||||
}, [state.settings]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
aria-labelledby="settings-dialog-title"
|
||||
>
|
||||
<DialogTitle id="settings-dialog-title">Настройки</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
label="Адрес сервера публикации"
|
||||
name="publicationServerUrl"
|
||||
value={settings.publicationServerUrl}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
style={{ marginTop: '16px' }}
|
||||
/>
|
||||
<TextField
|
||||
label="Версия 1С"
|
||||
name="onecVersion"
|
||||
value={settings.onecVersion}
|
||||
onChange={handleOneCVersionChange}
|
||||
fullWidth
|
||||
style={{ marginTop: '16px' }}
|
||||
error={errors.onecVersion}
|
||||
helperText={errors.onecVersion && 'Введите версию 1С в формате 8.3.xx.xxxx'}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} color="primary">
|
||||
Закрыть
|
||||
</Button>
|
||||
<Button onClick={handleSave} color="primary" disabled={errors.onecVersion}>
|
||||
Сохранить
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsModal;
|
||||
import React, { useContext, useState, useEffect } from "react";
|
||||
import { Dialog, DialogTitle, DialogContent, DialogActions, Button, TextField } from "@mui/material";
|
||||
import AppContext from "../../../context/AppContext";
|
||||
|
||||
const SettingsModal = ({ open, onClose }) => {
|
||||
const { state, updateSettings } = useContext(AppContext);
|
||||
const [settings, setSettings] = useState(state.settings);
|
||||
const [errors, setErrors] = useState({ onecVersion: false });
|
||||
|
||||
const handleChange = (e) => {
|
||||
setSettings({ ...settings, [e.target.name]: e.target.value });
|
||||
};
|
||||
|
||||
const validateOneCVersion = (value) => {
|
||||
const pattern = /^8\.3\.\d{2}\.\d{4}$/;
|
||||
return pattern.test(value);
|
||||
};
|
||||
|
||||
const handleOneCVersionChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
const isValid = validateOneCVersion(value);
|
||||
setErrors({ ...errors, onecVersion: !isValid });
|
||||
handleChange(e);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (!errors.onecVersion) {
|
||||
updateSettings(settings);
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSettings(state.settings);
|
||||
}, [state.settings]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
aria-labelledby="settings-dialog-title"
|
||||
>
|
||||
<DialogTitle id="settings-dialog-title">Настройки</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
label="Адрес сервера публикации"
|
||||
name="publicationServerUrl"
|
||||
value={settings.publicationServerUrl}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
style={{ marginTop: '16px' }}
|
||||
/>
|
||||
<TextField
|
||||
label="Версия 1С"
|
||||
name="onecVersion"
|
||||
value={settings.onecVersion}
|
||||
onChange={handleOneCVersionChange}
|
||||
fullWidth
|
||||
style={{ marginTop: '16px' }}
|
||||
error={errors.onecVersion}
|
||||
helperText={errors.onecVersion && 'Введите версию 1С в формате 8.3.xx.xxxx'}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} color="primary">
|
||||
Закрыть
|
||||
</Button>
|
||||
<Button onClick={handleSave} color="primary" disabled={errors.onecVersion}>
|
||||
Сохранить
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsModal;
|
||||
|
@ -1,170 +1,289 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import {
|
||||
Box,
|
||||
Drawer,
|
||||
Hidden,
|
||||
List,
|
||||
ListItem,
|
||||
IconButton,
|
||||
ListItemButton,
|
||||
ListItemText,
|
||||
Fab,
|
||||
CssBaseline
|
||||
} from "@mui/material";
|
||||
import { styled } from "@mui/system";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import EditIcon from "@mui/icons-material/Edit";
|
||||
import AppContext from "../../context/AppContext";
|
||||
import BaseSettingsEdit from "./components/BaseSettingsEdit";
|
||||
|
||||
const drawerWidth = 360;
|
||||
|
||||
const AddFab = styled(Fab)(({ theme }) => ({
|
||||
position: "absolute",
|
||||
bottom: theme.spacing(9),
|
||||
right: theme.spacing(2),
|
||||
}));
|
||||
|
||||
export default function BasesMenu(props) {
|
||||
const { window } = props;
|
||||
const {
|
||||
state,
|
||||
addBase,
|
||||
updateBase,
|
||||
deleteBase,
|
||||
showSnackbar,
|
||||
setSelectedBaseId,
|
||||
} = useContext(AppContext);
|
||||
const { handleDrawerToggle, mobileOpen } = props;
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||
const [editingBase, setEditingBase] = useState(null);
|
||||
const container = window !== undefined ? () => window().document.body : undefined;
|
||||
|
||||
const handleEditDialogOpen = (base) => {
|
||||
setEditingBase(base);
|
||||
setEditDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleEditDialogClose = (base) => {
|
||||
setEditDialogOpen(false);
|
||||
|
||||
if (base) {
|
||||
if (editingBase) {
|
||||
updateBase(base);
|
||||
showSnackbar("База успешно обновлена", "success");
|
||||
} else {
|
||||
addBase(base);
|
||||
showSnackbar("База успешно добавлена", "success");
|
||||
}
|
||||
}
|
||||
|
||||
setEditingBase(null);
|
||||
};
|
||||
|
||||
const handleListItemClick = (event, id) => {
|
||||
setSelectedBaseId(id);
|
||||
};
|
||||
|
||||
const [drawerOpen, setDrawerOpen] = React.useState(false);
|
||||
|
||||
const toggleDrawer = (open) => (event) => {
|
||||
if (
|
||||
event.type === 'keydown' &&
|
||||
(event.key === 'Tab' || event.key === 'Shift')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setDrawerOpen(open);
|
||||
};
|
||||
|
||||
const drawerContent = (
|
||||
<Box>
|
||||
<List>
|
||||
{state.bases.length === 0 ? (
|
||||
<ListItem secondaryAction={<IconButton
|
||||
edge="end"
|
||||
aria-label="new"
|
||||
onClick={() => handleEditDialogOpen(null)}
|
||||
>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
}
|
||||
disablePadding
|
||||
>
|
||||
<ListItemText primary="Добаьте первую базу." style={{ marginLeft: "20px" }} />
|
||||
</ListItem>
|
||||
) : (
|
||||
state.bases.map((base) => (
|
||||
<ListItem
|
||||
key={base.id}
|
||||
secondaryAction={
|
||||
<IconButton
|
||||
edge="end"
|
||||
aria-label="edit"
|
||||
onClick={() => handleEditDialogOpen(base)}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
}
|
||||
disablePadding
|
||||
>
|
||||
<ListItemButton
|
||||
selected={state.selectedBaseId === base.id}
|
||||
onClick={(event) => handleListItemClick(event, base.id)}
|
||||
>
|
||||
<ListItemText primary={base.name} />
|
||||
</ListItemButton>
|
||||
|
||||
</ListItem>
|
||||
))
|
||||
)}
|
||||
</List>
|
||||
<AddFab color="primary" onClick={() => handleEditDialogOpen()}>
|
||||
<AddIcon />
|
||||
</AddFab>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<CssBaseline />
|
||||
<Drawer
|
||||
container={container}
|
||||
variant="temporary"
|
||||
open={mobileOpen}
|
||||
onClose={handleDrawerToggle}
|
||||
ModalProps={{
|
||||
keepMounted: true,
|
||||
}}
|
||||
sx={{
|
||||
display: { xs: "block", sm: "block", md: "none" },
|
||||
width: drawerWidth,
|
||||
marginTop: 6,
|
||||
flexShrink: 0,
|
||||
[`& .MuiDrawer-paper`]: { width: drawerWidth, marginTop: 8, boxSizing: 'border-box' },
|
||||
}}
|
||||
>
|
||||
{drawerContent}
|
||||
</Drawer>
|
||||
<Drawer
|
||||
variant="permanent"
|
||||
sx={{
|
||||
display: { xs: "none", sm: "none", md: "block" },
|
||||
width: drawerWidth,
|
||||
|
||||
flexShrink: 0,
|
||||
[`& .MuiDrawer-paper`]: { width: drawerWidth, marginTop: 8, boxSizing: 'border-box' },
|
||||
}}
|
||||
open
|
||||
>
|
||||
{drawerContent}
|
||||
</Drawer>
|
||||
<BaseSettingsEdit
|
||||
open={editDialogOpen}
|
||||
base={editingBase}
|
||||
onClose={handleEditDialogClose}
|
||||
deleteBase={deleteBase}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import React, { useContext, useState } from "react";
|
||||
import {
|
||||
Box,
|
||||
Drawer,
|
||||
List,
|
||||
ListItem,
|
||||
IconButton,
|
||||
ListItemButton,
|
||||
ListItemText,
|
||||
CssBaseline,
|
||||
Divider
|
||||
} from "@mui/material";
|
||||
import { styled } from "@mui/system";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import EditIcon from "@mui/icons-material/Edit";
|
||||
import StorageIcon from "@mui/icons-material/Storage";
|
||||
import SpeedDial from "@mui/material/SpeedDial";
|
||||
import SpeedDialIcon from "@mui/material/SpeedDialIcon";
|
||||
import SpeedDialAction from "@mui/material/SpeedDialAction";
|
||||
import AppContext from "../../context/AppContext";
|
||||
import BaseSettingsEdit from "./components/BaseSettingsEdit";
|
||||
import CrsServerEdit from "./components/CrsServerEdit";
|
||||
|
||||
const drawerWidth = 360;
|
||||
|
||||
const StyledSpeedDial = styled(SpeedDial)(({ theme }) => ({
|
||||
position: "absolute",
|
||||
bottom: theme.spacing(9),
|
||||
right: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const CrsList = ({ crs, handleEditDialogOpen }) => {
|
||||
const { showSnackbar } = useContext(AppContext);
|
||||
|
||||
|
||||
const handleCopy = async (server) => {
|
||||
if (!navigator.clipboard) {
|
||||
showSnackbar('Копирование в буфер обмена не поддерживается в вашем браузере.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const serverAddress = `${server.tcpAddress}/${server.name}/repo.1ccr`;
|
||||
await navigator.clipboard.writeText(serverAddress);
|
||||
showSnackbar('Адрес сервера скопирован в буфер обмена.', 'success');
|
||||
} catch (err) {
|
||||
showSnackbar('Ошибка при копировании адреса сервера в буфер обмена.', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
if (crs.length === 0) {
|
||||
return (
|
||||
<ListItem
|
||||
secondaryAction={
|
||||
<IconButton
|
||||
edge="end"
|
||||
aria-label="new"
|
||||
onClick={() => handleEditDialogOpen(null)}
|
||||
>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
}
|
||||
disablePadding
|
||||
>
|
||||
<ListItemText primary="Добавьте сервер хранилища." style={{ marginLeft: "20px" }} />
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
|
||||
return crs.map((server) => (
|
||||
<ListItem
|
||||
key={server.id}
|
||||
secondaryAction={
|
||||
<IconButton
|
||||
edge="end"
|
||||
aria-label="edit"
|
||||
onClick={() => handleEditDialogOpen(server)}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
}
|
||||
disablePadding
|
||||
>
|
||||
<ListItemButton onClick={() => handleCopy(server)}>
|
||||
<ListItemText primary={server.title} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
));
|
||||
};
|
||||
|
||||
export default function BasesMenu(props) {
|
||||
const { window } = props;
|
||||
const {
|
||||
state,
|
||||
addBase,
|
||||
updateBase,
|
||||
deleteBase,
|
||||
addServer,
|
||||
updateServer,
|
||||
deleteServer,
|
||||
showSnackbar,
|
||||
setSelectedBaseId,
|
||||
} = useContext(AppContext);
|
||||
const { handleDrawerToggle, mobileOpen } = props;
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||
const [serverDialogOpen, setServerDialogOpen] = useState(false);
|
||||
const [editingBase, setEditingBase] = useState(null);
|
||||
const [editingServer, setEditingServer] = useState(null);
|
||||
const container = window !== undefined ? () => window().document.body : undefined;
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const handleOpen = () => setOpen(true);
|
||||
const handleClose = () => setOpen(false);
|
||||
|
||||
const handleServerDialogOpen = (server) => {
|
||||
setEditingServer(server);
|
||||
setServerDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleServerDialogClose = (server) => {
|
||||
setServerDialogOpen(false);
|
||||
|
||||
if (server) {
|
||||
if (editingServer) {
|
||||
updateServer(server);
|
||||
showSnackbar("Сервер успешно обновлен", "success");
|
||||
} else {
|
||||
addServer(server);
|
||||
showSnackbar("Сервер успешно добавлен", "success");
|
||||
}
|
||||
}
|
||||
|
||||
setEditingServer(null);
|
||||
};
|
||||
|
||||
const handleEditDialogOpen = (base) => {
|
||||
setEditingBase(base);
|
||||
setEditDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleEditDialogClose = (base) => {
|
||||
setEditDialogOpen(false);
|
||||
|
||||
if (base) {
|
||||
if (editingBase) {
|
||||
updateBase(base);
|
||||
showSnackbar("База успешно обновлена", "success");
|
||||
} else {
|
||||
addBase(base);
|
||||
showSnackbar("База успешно добавлена", "success");
|
||||
}
|
||||
}
|
||||
|
||||
setEditingBase(null);
|
||||
};
|
||||
|
||||
const handleListItemClick = (event, id) => {
|
||||
setSelectedBaseId(id);
|
||||
};
|
||||
|
||||
const actions = [
|
||||
{ icon: <AddIcon />, name: 'Создать базу', action: () => handleEditDialogOpen(null) },
|
||||
{ icon: <StorageIcon />, name: 'Создать сервер хранилища', action: () => handleServerDialogOpen(null)}
|
||||
];
|
||||
|
||||
const [drawerOpen, setDrawerOpen] = React.useState(false);
|
||||
|
||||
const toggleDrawer = (open) => (event) => {
|
||||
if (
|
||||
event.type === 'keydown' &&
|
||||
(event.key === 'Tab' || event.key === 'Shift')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setDrawerOpen(open);
|
||||
};
|
||||
|
||||
const drawerContent = (
|
||||
<Box>
|
||||
<List>
|
||||
<Divider textAlign="left">Базы</Divider>
|
||||
{state.bases.length === 0 ? (
|
||||
<ListItem secondaryAction={<IconButton
|
||||
edge="end"
|
||||
aria-label="new"
|
||||
onClick={() => handleEditDialogOpen(null)}
|
||||
>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
}
|
||||
disablePadding
|
||||
>
|
||||
<ListItemText primary="Добавьте первую базу." style={{ marginLeft: "20px" }} />
|
||||
</ListItem>
|
||||
) : (
|
||||
state.bases.map((base) => (
|
||||
<ListItem
|
||||
key={base.id}
|
||||
secondaryAction={
|
||||
<IconButton
|
||||
edge="end"
|
||||
aria-label="edit"
|
||||
onClick={() => handleEditDialogOpen(base)}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
}
|
||||
disablePadding
|
||||
>
|
||||
<ListItemButton
|
||||
selected={state.selectedBaseId === base.id}
|
||||
onClick={(event) => handleListItemClick(event, base.id)}
|
||||
>
|
||||
<ListItemText primary={base.name} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))
|
||||
)}
|
||||
<Divider textAlign="left">Хранилище</Divider>
|
||||
<CrsList crs={state.crs} handleEditDialogOpen={handleServerDialogOpen} />
|
||||
</List>
|
||||
|
||||
<StyledSpeedDial
|
||||
ariaLabel="SpeedDial tooltip example"
|
||||
icon={<SpeedDialIcon />}
|
||||
onClose={handleClose}
|
||||
onOpen={handleOpen}
|
||||
open={open}
|
||||
>
|
||||
{actions.map((action) => (
|
||||
<SpeedDialAction
|
||||
key={action.name}
|
||||
icon={action.icon}
|
||||
tooltipTitle={action.name}
|
||||
// tooltipOpen
|
||||
onClick={action.action}
|
||||
/>
|
||||
))}
|
||||
</StyledSpeedDial>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<CssBaseline />
|
||||
<Drawer
|
||||
container={container}
|
||||
variant="temporary"
|
||||
open={mobileOpen}
|
||||
onClose={handleDrawerToggle}
|
||||
ModalProps={{
|
||||
keepMounted: true,
|
||||
}}
|
||||
sx={{
|
||||
display: { xs: "block", sm: "block", md: "none" },
|
||||
width: drawerWidth,
|
||||
marginTop: 6,
|
||||
flexShrink: 0,
|
||||
[`& .MuiDrawer-paper`]: { width: drawerWidth, marginTop: 8, boxSizing: 'border-box' },
|
||||
}}
|
||||
>
|
||||
{drawerContent}
|
||||
</Drawer>
|
||||
<Drawer
|
||||
variant="permanent"
|
||||
sx={{
|
||||
display: { xs: "none", sm: "none", md: "block" },
|
||||
width: drawerWidth,
|
||||
flexShrink: 0,
|
||||
[`& .MuiDrawer-paper`]: { width: drawerWidth, marginTop: 8, boxSizing: 'border-box' },
|
||||
}}
|
||||
open
|
||||
>
|
||||
{drawerContent}
|
||||
</Drawer>
|
||||
<BaseSettingsEdit
|
||||
open={editDialogOpen}
|
||||
base={editingBase}
|
||||
onClose={handleEditDialogClose}
|
||||
deleteBase={deleteBase}
|
||||
/>
|
||||
<CrsServerEdit
|
||||
open={serverDialogOpen}
|
||||
server={editingServer}
|
||||
onClose={handleServerDialogClose}
|
||||
deleteServer={deleteServer}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,230 +1,230 @@
|
||||
import React, { useState, useEffect, useContext } from "react";
|
||||
import { IconButton, Dialog, DialogTitle, DialogContent, DialogActions, TextField, Button, Grid, Tooltip } from "@mui/material";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import ConfirmationDialog from "../../ConfirmationDialog"
|
||||
import AppContext from "../../../context/AppContext";
|
||||
|
||||
export default function BaseSettingsEdit({
|
||||
open,
|
||||
base,
|
||||
onClose,
|
||||
deleteBase
|
||||
}) {
|
||||
const [newBase, setNewBase] = useState({name: "", srvr: "", ref: "" });
|
||||
const [errors, setErrors] = useState({ name: "", srvr: "", ref: "" });
|
||||
const [update, setUpdate] = useState(false);
|
||||
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
|
||||
const { state } = useContext(AppContext);
|
||||
const [isGeneralSettingsValid, setIsGeneralSettingsValid] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
if (!base) {
|
||||
setNewBase({name: "", srvr: "", ref: "" });
|
||||
setUpdate(false);
|
||||
} else {
|
||||
setNewBase(base);
|
||||
setUpdate(true);
|
||||
};
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
// Добавьте новый useEffect с зависимостью от состояния newPublication
|
||||
useEffect(() => {
|
||||
const nameError = validateName(newBase.name);
|
||||
const srvrRefError = validateSrvrRef(newBase.ref, newBase.srvr);
|
||||
const srvrError = validateServer(newBase.srvr);
|
||||
const refError = validateRef(newBase.ref);
|
||||
|
||||
setErrors({ ...errors, name: nameError, srvr: !srvrError ? srvrRefError : srvrError, ref: !refError ? srvrRefError : srvrError});
|
||||
|
||||
handleGeneralSettingsValidChange(
|
||||
!nameError && !srvrError && !refError
|
||||
);
|
||||
}, [newBase]);
|
||||
|
||||
const handleGeneralSettingsValidChange = (isValid) => {
|
||||
setIsGeneralSettingsValid(isValid);
|
||||
};
|
||||
|
||||
|
||||
const handleSave = () => {
|
||||
|
||||
onClose(newBase);
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { name, value } = event.target;
|
||||
setNewBase({ ...newBase, [name]: value });
|
||||
|
||||
};
|
||||
|
||||
const handleDeleteConfirmationOpen = () => {
|
||||
setConfirmationDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteConfirmationClose = (confirmed) => {
|
||||
if (confirmed) {
|
||||
deleteBase(newBase);
|
||||
onClose(null);
|
||||
}
|
||||
setConfirmationDialogOpen(false);
|
||||
};
|
||||
|
||||
const isValidCharacters = (str) => {
|
||||
const regex = /^[a-zA-Z0-9-_]+$/;
|
||||
return regex.test(str);
|
||||
};
|
||||
|
||||
const validateBase = (base, field) => {
|
||||
|
||||
|
||||
if (field) {
|
||||
if (field === "name") {
|
||||
return base[field].trim().length > 0 && isNameUnique(base[field]);
|
||||
} else if (field === "ref" || field === "srvr") {
|
||||
return base[field].trim().length > 0 && isRefSrvrUnique(base.ref, base.srvr);
|
||||
}
|
||||
} else {
|
||||
return (
|
||||
base.name.trim().length > 0 &&
|
||||
base.srvr.trim().length > 0 &&
|
||||
base.ref.trim().length > 0 &&
|
||||
isNameUnique(base.name) &&
|
||||
isRefSrvrUnique(base.ref, base.srvr)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const isNameUnique = (name) => {
|
||||
return !state.bases.some((b) => (!base || b.id !== base.id) && b.name === name);
|
||||
};
|
||||
|
||||
const isRefSrvrUnique = (ref, srvr) => {
|
||||
return !state.bases.some((b) => (!base || b.id !== base.id) && b.ref === ref && b.srvr === srvr);
|
||||
};
|
||||
|
||||
const validateName = (name) => {
|
||||
if (!isNameUnique(name)) {
|
||||
return "Введите уникальное название базы";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const validateSrvrRef = (ref, srvr) => {
|
||||
if (!isRefSrvrUnique(ref, srvr)) {
|
||||
return "Введите уникальное сочетание сервера и имени базы на сервере";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const validateServer = (srvr) => {
|
||||
if (!isValidCharacters(srvr)) {
|
||||
return "Сервер может содержать только латиницу, цифры и спецсимволы uri";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const validateRef = (ref) => {
|
||||
if (!isValidCharacters(ref)) {
|
||||
return "Имя базы на сервере может содержать только латиницу, цифры и спецсимволы uri";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dialog open={open} onClose={() => onClose(null)}>
|
||||
<DialogTitle>{update ? "Редактировать базу" : "Добавить базу"}</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
label="Название базы"
|
||||
name="name"
|
||||
value={newBase.name}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
margin="dense"
|
||||
required
|
||||
onBlur={() => setErrors({ ...errors, name: validateName(newBase.name) })}
|
||||
error={!validateBase(newBase, "name")}
|
||||
helperText={errors.name}
|
||||
/>
|
||||
<TextField
|
||||
label="Сервер"
|
||||
name="srvr"
|
||||
value={newBase.srvr}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
margin="dense"
|
||||
required
|
||||
onBlur={() =>
|
||||
setErrors({ ...errors, srvr: validateServer(newBase.srvr), ref: validateSrvrRef(newBase.ref, newBase.srvr) })}
|
||||
error={!validateBase(newBase, "srvr")}
|
||||
helperText={errors.srvr}
|
||||
/>
|
||||
<TextField
|
||||
label="Имя базы на сервере"
|
||||
name="ref"
|
||||
value={newBase.ref}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
margin="dense"
|
||||
required
|
||||
onBlur={() =>
|
||||
setErrors({ ...errors, srvr: validateRef(newBase.ref), ref: validateSrvrRef(newBase.ref, newBase.srvr) })}
|
||||
|
||||
error={!validateBase(newBase, "ref")}
|
||||
helperText={errors.ref}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Grid container justifyContent="space-between" alignItems="flex-start">
|
||||
{update && (
|
||||
<Grid item>
|
||||
<IconButton onClick={handleDeleteConfirmationOpen} color="secondary">
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs style={{ flexGrow: 1 }} />
|
||||
<Grid item>
|
||||
<Button onClick={() => onClose(null)} color="primary">
|
||||
Отмена
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Tooltip
|
||||
title={
|
||||
isGeneralSettingsValid
|
||||
? ""
|
||||
: "Пожалуйста, исправьте ошибки перед сохранением"
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
color="primary"
|
||||
disabled={!isGeneralSettingsValid}
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
<ConfirmationDialog
|
||||
open={confirmationDialogOpen}
|
||||
title="Подтверждение удаления"
|
||||
content="Вы уверены, что хотите удалить эту базу?"
|
||||
onClose={handleDeleteConfirmationClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
import React, { useState, useEffect, useContext } from "react";
|
||||
import { IconButton, Dialog, DialogTitle, DialogContent, DialogActions, TextField, Button, Grid, Tooltip } from "@mui/material";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import ConfirmationDialog from "../../ConfirmationDialog"
|
||||
import AppContext from "../../../context/AppContext";
|
||||
|
||||
export default function BaseSettingsEdit({
|
||||
open,
|
||||
base,
|
||||
onClose,
|
||||
deleteBase
|
||||
}) {
|
||||
const [newBase, setNewBase] = useState({name: "", srvr: "", ref: "" });
|
||||
const [errors, setErrors] = useState({ name: "", srvr: "", ref: "" });
|
||||
const [update, setUpdate] = useState(false);
|
||||
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
|
||||
const { state } = useContext(AppContext);
|
||||
const [isGeneralSettingsValid, setIsGeneralSettingsValid] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
if (!base) {
|
||||
setNewBase({name: "", srvr: "", ref: "" });
|
||||
setUpdate(false);
|
||||
} else {
|
||||
setNewBase(base);
|
||||
setUpdate(true);
|
||||
};
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
// Добавьте новый useEffect с зависимостью от состояния newPublication
|
||||
useEffect(() => {
|
||||
const nameError = validateName(newBase.name);
|
||||
const srvrRefError = validateSrvrRef(newBase.ref, newBase.srvr);
|
||||
const srvrError = validateServer(newBase.srvr);
|
||||
const refError = validateRef(newBase.ref);
|
||||
|
||||
setErrors({ ...errors, name: nameError, srvr: !srvrError ? srvrRefError : srvrError, ref: !refError ? srvrRefError : srvrError});
|
||||
|
||||
handleGeneralSettingsValidChange(
|
||||
!nameError && !srvrError && !refError
|
||||
);
|
||||
}, [newBase]);
|
||||
|
||||
const handleGeneralSettingsValidChange = (isValid) => {
|
||||
setIsGeneralSettingsValid(isValid);
|
||||
};
|
||||
|
||||
|
||||
const handleSave = () => {
|
||||
|
||||
onClose(newBase);
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { name, value } = event.target;
|
||||
setNewBase({ ...newBase, [name]: value });
|
||||
|
||||
};
|
||||
|
||||
const handleDeleteConfirmationOpen = () => {
|
||||
setConfirmationDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteConfirmationClose = (confirmed) => {
|
||||
if (confirmed) {
|
||||
deleteBase(newBase);
|
||||
onClose(null);
|
||||
}
|
||||
setConfirmationDialogOpen(false);
|
||||
};
|
||||
|
||||
const isValidCharacters = (str) => {
|
||||
const regex = /^[a-zA-Z0-9-_]+$/;
|
||||
return regex.test(str);
|
||||
};
|
||||
|
||||
const validateBase = (base, field) => {
|
||||
|
||||
|
||||
if (field) {
|
||||
if (field === "name") {
|
||||
return base[field].trim().length > 0 && isNameUnique(base[field]);
|
||||
} else if (field === "ref" || field === "srvr") {
|
||||
return base[field].trim().length > 0 && isRefSrvrUnique(base.ref, base.srvr);
|
||||
}
|
||||
} else {
|
||||
return (
|
||||
base.name.trim().length > 0 &&
|
||||
base.srvr.trim().length > 0 &&
|
||||
base.ref.trim().length > 0 &&
|
||||
isNameUnique(base.name) &&
|
||||
isRefSrvrUnique(base.ref, base.srvr)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const isNameUnique = (name) => {
|
||||
return !state.bases.some((b) => (!base || b.id !== base.id) && b.name === name);
|
||||
};
|
||||
|
||||
const isRefSrvrUnique = (ref, srvr) => {
|
||||
return !state.bases.some((b) => (!base || b.id !== base.id) && b.ref === ref && b.srvr === srvr);
|
||||
};
|
||||
|
||||
const validateName = (name) => {
|
||||
if (!isNameUnique(name)) {
|
||||
return "Введите уникальное название базы";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const validateSrvrRef = (ref, srvr) => {
|
||||
if (!isRefSrvrUnique(ref, srvr)) {
|
||||
return "Введите уникальное сочетание сервера и имени базы на сервере";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const validateServer = (srvr) => {
|
||||
if (!isValidCharacters(srvr)) {
|
||||
return "Сервер может содержать только латиницу, цифры и спецсимволы uri";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const validateRef = (ref) => {
|
||||
if (!isValidCharacters(ref)) {
|
||||
return "Имя базы на сервере может содержать только латиницу, цифры и спецсимволы uri";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dialog open={open} onClose={() => onClose(null)}>
|
||||
<DialogTitle>{update ? "Редактировать базу" : "Добавить базу"}</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
label="Название базы"
|
||||
name="name"
|
||||
value={newBase.name}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
margin="dense"
|
||||
required
|
||||
onBlur={() => setErrors({ ...errors, name: validateName(newBase.name) })}
|
||||
error={!validateBase(newBase, "name")}
|
||||
helperText={errors.name}
|
||||
/>
|
||||
<TextField
|
||||
label="Сервер"
|
||||
name="srvr"
|
||||
value={newBase.srvr}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
margin="dense"
|
||||
required
|
||||
onBlur={() =>
|
||||
setErrors({ ...errors, srvr: validateServer(newBase.srvr), ref: validateSrvrRef(newBase.ref, newBase.srvr) })}
|
||||
error={!validateBase(newBase, "srvr")}
|
||||
helperText={errors.srvr}
|
||||
/>
|
||||
<TextField
|
||||
label="Имя базы на сервере"
|
||||
name="ref"
|
||||
value={newBase.ref}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
margin="dense"
|
||||
required
|
||||
onBlur={() =>
|
||||
setErrors({ ...errors, srvr: validateRef(newBase.ref), ref: validateSrvrRef(newBase.ref, newBase.srvr) })}
|
||||
|
||||
error={!validateBase(newBase, "ref")}
|
||||
helperText={errors.ref}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Grid container justifyContent="space-between" alignItems="flex-start">
|
||||
{update && (
|
||||
<Grid item>
|
||||
<IconButton onClick={handleDeleteConfirmationOpen} color="secondary">
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs style={{ flexGrow: 1 }} />
|
||||
<Grid item>
|
||||
<Button onClick={() => onClose(null)} color="primary">
|
||||
Отмена
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Tooltip
|
||||
title={
|
||||
isGeneralSettingsValid
|
||||
? ""
|
||||
: "Пожалуйста, исправьте ошибки перед сохранением"
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
color="primary"
|
||||
disabled={!isGeneralSettingsValid}
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
<ConfirmationDialog
|
||||
open={confirmationDialogOpen}
|
||||
title="Подтверждение удаления"
|
||||
content="Вы уверены, что хотите удалить эту базу?"
|
||||
onClose={handleDeleteConfirmationClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,211 @@
|
||||
import React, { useState, useEffect, useContext } from "react";
|
||||
import { IconButton, Dialog, DialogTitle, DialogContent, DialogActions, TextField, Button, Grid, Tooltip, Switch, FormControlLabel } from "@mui/material";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import ConfirmationDialog from "../../ConfirmationDialog"
|
||||
import AppContext from "../../../context/AppContext";
|
||||
|
||||
export default function CrsServerEdit({
|
||||
open,
|
||||
server,
|
||||
onClose,
|
||||
deleteServer
|
||||
}) {
|
||||
const [newServer, setNewServer] = useState({ tcpAddress: "", name: "", title: "", active: true });
|
||||
const [errors, setErrors] = useState({ tcpAddress: "", name: "", title: "" });
|
||||
const [update, setUpdate] = useState(false);
|
||||
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
|
||||
const { state } = useContext(AppContext);
|
||||
const [isGeneralSettingsValid, setIsGeneralSettingsValid] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
if (!server) {
|
||||
setNewServer({ tcpAddress: "", name: "", title: "", active: true });
|
||||
setUpdate(false);
|
||||
} else {
|
||||
setNewServer({ ...server, active: server.active !== undefined ? server.active : true });
|
||||
setUpdate(true);
|
||||
};
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
const tcpAddressError = validateTcpAddress(newServer.tcpAddress);
|
||||
const nameError = validateName(newServer.name);
|
||||
const titleError = validateTitle(newServer.title);
|
||||
|
||||
setErrors({ ...errors, tcpAddress: tcpAddressError, name: nameError, title: titleError });
|
||||
|
||||
handleGeneralSettingsValidChange(
|
||||
!tcpAddressError && !nameError && !titleError
|
||||
);
|
||||
}, [newServer]);
|
||||
|
||||
const handleGeneralSettingsValidChange = (isValid) => {
|
||||
setIsGeneralSettingsValid(isValid);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
onClose(newServer);
|
||||
};
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { name, value } = event.target;
|
||||
setNewServer({ ...newServer, [name]: value });
|
||||
};
|
||||
|
||||
const handleToggleActive = () => {
|
||||
setNewServer({ ...newServer, active: !newServer.active });
|
||||
};
|
||||
|
||||
const handleDeleteConfirmationOpen = () => {
|
||||
setConfirmationDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteConfirmationClose = (confirmed) => {
|
||||
if (confirmed) {
|
||||
deleteServer(newServer);
|
||||
onClose(null);
|
||||
}
|
||||
setConfirmationDialogOpen(false);
|
||||
};
|
||||
|
||||
const validateTcpAddress = (tcpAddress) => {
|
||||
try {
|
||||
const url = new URL(tcpAddress);
|
||||
if (url.protocol !== 'tcp:') {
|
||||
return 'TCP address must start with "tcp://" protocol';
|
||||
}
|
||||
} catch (_) {
|
||||
return "TCP address must be a valid URL";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
|
||||
const validateName = (name) => {
|
||||
if (!name || !isValidCharacters(name)) {
|
||||
return "Сервер может содержать только латиницу, цифры и спецсимволы uri";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
const isValidCharacters = (str) => {
|
||||
const regex = /^[a-zA-Z0-9-_]+$/;
|
||||
return regex.test(str);
|
||||
};
|
||||
|
||||
const validateTitle = (title) => {
|
||||
if (!title || title.trim().length === 0) {
|
||||
return "Представление должно быть заполненно";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dialog open={open} onClose={() => onClose(null)}>
|
||||
<DialogTitle>{update ? "Изменить настройки публикации хранилища" : "Создать публикацию хранилища"}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} sm={8}>
|
||||
<TextField
|
||||
label="Название сервера"
|
||||
name="title"
|
||||
value={newServer.title}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
margin="dense"
|
||||
required
|
||||
onBlur={() => setErrors({ ...errors, title: validateTitle(newServer.title) })}
|
||||
error={errors.title !== ""}
|
||||
helperText={errors.title}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={4}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={newServer.active}
|
||||
onChange={handleToggleActive}
|
||||
name="active"
|
||||
/>
|
||||
}
|
||||
label="Активность публикации"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
label="Адрес сервера хранилища конфигурации (TCP)"
|
||||
name="tcpAddress"
|
||||
value={newServer.tcpAddress}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
margin="dense"
|
||||
required
|
||||
onBlur={() => setErrors({ ...errors, tcpAddress: validateTcpAddress(newServer.tcpAddress) })}
|
||||
error={errors.tcpAddress !== ""}
|
||||
helperText={errors.tcpAddress}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
label="Алиас публикации"
|
||||
name="name"
|
||||
value={newServer.name}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
margin="dense"
|
||||
required
|
||||
onBlur={() => setErrors({ ...errors, name: validateName(newServer.name) })}
|
||||
error={errors.name !== ""}
|
||||
helperText={errors.name}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Grid container justifyContent="space-between" alignItems="flex-start">
|
||||
{update && (
|
||||
<Grid item>
|
||||
<IconButton onClick={handleDeleteConfirmationOpen} color="secondary">
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs style={{ flexGrow: 1 }} />
|
||||
<Grid item>
|
||||
<Button onClick={() => onClose(null)} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Tooltip
|
||||
title={
|
||||
isGeneralSettingsValid
|
||||
? ""
|
||||
: "Please fix errors before saving"
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
color="primary"
|
||||
disabled={!isGeneralSettingsValid}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
<ConfirmationDialog
|
||||
open={confirmationDialogOpen}
|
||||
title="Удалить публикацию"
|
||||
content="Вы уверены, что хотите удалить публикацию этого сервера хранилища?"
|
||||
onClose={handleDeleteConfirmationClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
// AppContext.js
|
||||
|
||||
import { createContext } from 'react';
|
||||
|
||||
const AppContext = createContext();
|
||||
|
||||
// AppContext.js
|
||||
|
||||
import { createContext } from 'react';
|
||||
|
||||
const AppContext = createContext();
|
||||
|
||||
export default AppContext;
|
@ -1,275 +1,345 @@
|
||||
import React, { useReducer, useEffect, useState } from "react";
|
||||
import AppContext from "./AppContext";
|
||||
import * as ApiProcessor from "../ApiProcessor"
|
||||
import { useSnackbar } from "notistack";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
const AppContextProvider = ({ children }) => {
|
||||
const initialState = {
|
||||
bases: [],
|
||||
selectedBaseId: null,
|
||||
settings: {},
|
||||
};
|
||||
|
||||
const [baseUpdated, setBaseUpdated] = useState(false);
|
||||
const [settingsUpdated, setSettingsUpdated] = useState(false);
|
||||
|
||||
function appReducer(state, action) {
|
||||
switch (action.type) {
|
||||
case "ADD_BASES":
|
||||
return { ...state, bases: action.payload };
|
||||
case "ADD_BASE":
|
||||
return { ...state, bases: [...state.bases, action.payload] };
|
||||
case "UPDATE_BASE":
|
||||
return {
|
||||
...state,
|
||||
bases: updateItem(state.bases, action.payload.id, () => action.payload),
|
||||
};
|
||||
case 'DELETE_BASE':
|
||||
return {
|
||||
...state,
|
||||
bases: state.bases.filter(base => base.id !== action.payload),
|
||||
};
|
||||
case "UPDATE_PUBLICATION":
|
||||
const { publicationId, updatedPublication } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
bases: updateItem(
|
||||
state.bases,
|
||||
state.selectedBaseId,
|
||||
(base) => ({
|
||||
...base,
|
||||
publications: updateItem(
|
||||
[...base.publications],
|
||||
publicationId,
|
||||
() => updatedPublication
|
||||
),
|
||||
}),
|
||||
"id"
|
||||
),
|
||||
};
|
||||
case "ADD_PUBLICATION":
|
||||
const { newPublication } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
bases: updateItem(
|
||||
state.bases,
|
||||
state.selectedBaseId,
|
||||
(base) => ({
|
||||
...base,
|
||||
publications: [...base.publications, newPublication],
|
||||
}),
|
||||
"id"
|
||||
),
|
||||
};
|
||||
case "DELETE_PUBLICATION":
|
||||
const deletedPublicationId = action.payload;
|
||||
|
||||
return {
|
||||
...state,
|
||||
bases: updateItem
|
||||
(state.bases, state.selectedBaseId, (base) => {
|
||||
console.log(base.publications);
|
||||
console.log(deletedPublicationId);
|
||||
const newPublications = base.publications.filter((publication) => publication.id !== deletedPublicationId);
|
||||
console.log(newPublications);
|
||||
return { ...base, publications: newPublications };
|
||||
}, "id"),
|
||||
};
|
||||
case "SET_SELECTED_BASE_ID":
|
||||
return { ...state, selectedBaseId: action.payload };
|
||||
case "UPDATE_SETTINGS":
|
||||
return { ...state, settings: action.payload };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
const updateItem = (items, targetId, callback, idFieldName = "id") =>
|
||||
items.map((item) =>
|
||||
item[idFieldName] === targetId ? callback(item) : item
|
||||
);
|
||||
|
||||
const [state, dispatch] = useReducer(appReducer, initialState);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const configData = await ApiProcessor.getConfig(showSnackbar);
|
||||
if (configData) {
|
||||
addBases(configData.bases);
|
||||
}
|
||||
|
||||
const settingsData = await ApiProcessor.getSettings(showSnackbar);
|
||||
if (settingsData) {
|
||||
loadSettings(settingsData);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const updateBases = async () => {
|
||||
await ApiProcessor.updateConfig(state.bases, showSnackbar);
|
||||
setBaseUpdated(false);
|
||||
};
|
||||
|
||||
if (baseUpdated) {
|
||||
updateBases();
|
||||
}
|
||||
}, [state.bases, baseUpdated]);
|
||||
|
||||
useEffect(() => {
|
||||
const saveSettings = async () => {
|
||||
if (state.settings && Object.keys(state.settings).length > 0) {
|
||||
await ApiProcessor.updateSettings(state.settings, showSnackbar);
|
||||
setSettingsUpdated(false);
|
||||
}
|
||||
};
|
||||
if (settingsUpdated) {
|
||||
saveSettings();
|
||||
}
|
||||
}, [state.settings, settingsUpdated]);
|
||||
|
||||
|
||||
const addBases = (newBases) => {
|
||||
dispatch({ type: "ADD_BASES", payload: newBases });
|
||||
};
|
||||
|
||||
const addBase = (newBase) => {
|
||||
const payload = {
|
||||
id: uuidv4(),
|
||||
name: newBase.name,
|
||||
srvr: newBase.srvr,
|
||||
ref: newBase.ref,
|
||||
publications: [
|
||||
{
|
||||
id: uuidv4(),
|
||||
name: newBase.ref,
|
||||
title: newBase.name,
|
||||
enable: true,
|
||||
active: false,
|
||||
usr: "",
|
||||
pwd: "",
|
||||
enableStandardOData: false,
|
||||
ws: {
|
||||
publishExtensionsByDefault: true,
|
||||
wsList: []
|
||||
},
|
||||
httpServices: {
|
||||
publishExtensionsByDefault: true,
|
||||
publishByDefault: true,
|
||||
hsList: []
|
||||
}
|
||||
}
|
||||
],
|
||||
};
|
||||
dispatch({
|
||||
type: "ADD_BASE",
|
||||
payload: payload,
|
||||
});
|
||||
setBaseUpdated(true);
|
||||
setSelectedBaseId(payload.id);
|
||||
};
|
||||
|
||||
const updateBase = (updatedBase) => {
|
||||
dispatch({ type: "UPDATE_BASE", payload: updatedBase });
|
||||
setBaseUpdated(true);
|
||||
};
|
||||
|
||||
|
||||
const deleteBase = (deletedBase) => {
|
||||
dispatch({ type: 'DELETE_BASE', payload: deletedBase.id });
|
||||
setBaseUpdated(true);
|
||||
};
|
||||
|
||||
const updatePublication = (publicationId, updatedPublication) => {
|
||||
dispatch({
|
||||
type: "UPDATE_PUBLICATION",
|
||||
payload: { publicationId, updatedPublication },
|
||||
});
|
||||
setBaseUpdated(true);
|
||||
};
|
||||
|
||||
const addPublication = (newPublication) => {
|
||||
const publicationWithId = {
|
||||
...newPublication,
|
||||
id: uuidv4(),
|
||||
};
|
||||
|
||||
dispatch({
|
||||
type: "ADD_PUBLICATION",
|
||||
payload: { newPublication: publicationWithId },
|
||||
});
|
||||
setBaseUpdated(true);
|
||||
};
|
||||
|
||||
const deletePublication = (deletedPublication) => {
|
||||
const deletedPublicationId = deletedPublication.id;
|
||||
dispatch({ type: 'DELETE_PUBLICATION', payload: deletedPublicationId });
|
||||
setBaseUpdated(true);
|
||||
};
|
||||
|
||||
const setSelectedBaseId = (id) => {
|
||||
dispatch({ type: "SET_SELECTED_BASE_ID", payload: id });
|
||||
};
|
||||
|
||||
const getUniqueTitles = () => {
|
||||
const selectedBase = state.bases.find(base => base.id === state.selectedBaseId);
|
||||
if (!selectedBase) return [];
|
||||
const titles = selectedBase.publications.map(publication => publication.title);
|
||||
return [...new Set(titles)];
|
||||
};
|
||||
|
||||
const getUniqueEndpoints = () => {
|
||||
const allPublications = state.bases.flatMap(base => base.publications);
|
||||
const endpoints = allPublications.map(publication => publication.name);
|
||||
return [...new Set(endpoints)];
|
||||
};
|
||||
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const showSnackbar = (message, variant = "default") => {
|
||||
enqueueSnackbar(message, {
|
||||
variant: variant,
|
||||
anchorOrigin: {
|
||||
vertical: "bottom",
|
||||
horizontal: "right",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const updateSettings = (newSettings) => {
|
||||
dispatch({ type: "UPDATE_SETTINGS", payload: newSettings });
|
||||
setSettingsUpdated(true);
|
||||
};
|
||||
|
||||
const loadSettings = (newSettings) => {
|
||||
dispatch({ type: "UPDATE_SETTINGS", payload: newSettings });
|
||||
};
|
||||
|
||||
return (
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
state,
|
||||
addBases,
|
||||
addBase,
|
||||
updateBase,
|
||||
deleteBase,
|
||||
updatePublication,
|
||||
addPublication,
|
||||
showSnackbar,
|
||||
setSelectedBaseId,
|
||||
getUniqueTitles,
|
||||
getUniqueEndpoints,
|
||||
deletePublication,
|
||||
updateSettings
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AppContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
import React, { useReducer, useEffect, useState } from "react";
|
||||
import AppContext from "./AppContext";
|
||||
import * as ApiProcessor from "../ApiProcessor"
|
||||
import { useSnackbar } from "notistack";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
const AppContextProvider = ({ children }) => {
|
||||
const initialState = {
|
||||
bases: [],
|
||||
selectedBaseId: null,
|
||||
settings: {},
|
||||
crs: []
|
||||
};
|
||||
|
||||
const [baseUpdated, setBaseUpdated] = useState(false);
|
||||
const [crsUpdated, setCrsUpdated] = useState(false);
|
||||
const [settingsUpdated, setSettingsUpdated] = useState(false);
|
||||
|
||||
function appReducer(state, action) {
|
||||
switch (action.type) {
|
||||
case "ADD_BASES":
|
||||
return { ...state, bases: action.payload };
|
||||
case "ADD_BASE":
|
||||
return { ...state, bases: [...state.bases, action.payload] };
|
||||
case "UPDATE_BASE":
|
||||
return {
|
||||
...state,
|
||||
bases: updateItem(state.bases, action.payload.id, () => action.payload),
|
||||
};
|
||||
case 'DELETE_BASE':
|
||||
return {
|
||||
...state,
|
||||
bases: state.bases.filter(base => base.id !== action.payload),
|
||||
};
|
||||
case "UPDATE_PUBLICATION":
|
||||
const { publicationId, updatedPublication } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
bases: updateItem(
|
||||
state.bases,
|
||||
state.selectedBaseId,
|
||||
(base) => ({
|
||||
...base,
|
||||
publications: updateItem(
|
||||
[...base.publications],
|
||||
publicationId,
|
||||
() => updatedPublication
|
||||
),
|
||||
}),
|
||||
"id"
|
||||
),
|
||||
};
|
||||
case "ADD_PUBLICATION":
|
||||
const { newPublication } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
bases: updateItem(
|
||||
state.bases,
|
||||
state.selectedBaseId,
|
||||
(base) => ({
|
||||
...base,
|
||||
publications: [...base.publications, newPublication],
|
||||
}),
|
||||
"id"
|
||||
),
|
||||
};
|
||||
case "DELETE_PUBLICATION":
|
||||
const deletedPublicationId = action.payload;
|
||||
|
||||
return {
|
||||
...state,
|
||||
bases: updateItem
|
||||
(state.bases, state.selectedBaseId, (base) => {
|
||||
console.log(base.publications);
|
||||
console.log(deletedPublicationId);
|
||||
const newPublications = base.publications.filter((publication) => publication.id !== deletedPublicationId);
|
||||
console.log(newPublications);
|
||||
return { ...base, publications: newPublications };
|
||||
}, "id"),
|
||||
};
|
||||
case "SET_SELECTED_BASE_ID":
|
||||
return { ...state, selectedBaseId: action.payload };
|
||||
case "UPDATE_SETTINGS":
|
||||
return { ...state, settings: action.payload };
|
||||
case "ADD_SERVERS":
|
||||
return { ...state, crs: action.payload };
|
||||
case "ADD_SERVER":
|
||||
return { ...state, crs: [...state.crs, action.payload] };
|
||||
case "UPDATE_SERVER":
|
||||
return {
|
||||
...state,
|
||||
crs: updateItem(state.crs, action.payload.id, () => action.payload),
|
||||
};
|
||||
case 'DELETE_SERVER':
|
||||
return {
|
||||
...state,
|
||||
crs: state.crs.filter(server => server.id !== action.payload),
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
const updateItem = (items, targetId, callback, idFieldName = "id") =>
|
||||
items.map((item) =>
|
||||
item[idFieldName] === targetId ? callback(item) : item
|
||||
);
|
||||
|
||||
const [state, dispatch] = useReducer(appReducer, initialState);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const configData = await ApiProcessor.getConfig(showSnackbar);
|
||||
if (configData) {
|
||||
if (!configData.crs || configData.crs.length === 0) {
|
||||
configData.crs = [{
|
||||
id: uuidv4(),
|
||||
name: "repository",
|
||||
active: false,
|
||||
title: "Репозиторий по умолчанию",
|
||||
tcpAddress: "tcp://localhost"
|
||||
}];
|
||||
}
|
||||
|
||||
addBases(configData.bases);
|
||||
addCrs(configData.crs);
|
||||
}
|
||||
|
||||
const settingsData = await ApiProcessor.getSettings(showSnackbar);
|
||||
if (settingsData) {
|
||||
loadSettings(settingsData);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const updateBases = async () => {
|
||||
await ApiProcessor.updateConfig(state.bases, state.crs, showSnackbar);
|
||||
setBaseUpdated(false);
|
||||
};
|
||||
|
||||
if (baseUpdated) {
|
||||
updateBases();
|
||||
}
|
||||
}, [state.bases, baseUpdated]);
|
||||
|
||||
useEffect(() => {
|
||||
const updateCrs = async () => {
|
||||
await ApiProcessor.updateConfig(state.bases, state.crs, showSnackbar);
|
||||
setCrsUpdated(false);
|
||||
};
|
||||
|
||||
if (crsUpdated) {
|
||||
updateCrs();
|
||||
}
|
||||
}, [state.crs, crsUpdated]);
|
||||
|
||||
useEffect(() => {
|
||||
const saveSettings = async () => {
|
||||
if (state.settings && Object.keys(state.settings).length > 0) {
|
||||
await ApiProcessor.updateSettings(state.settings, showSnackbar);
|
||||
setSettingsUpdated(false);
|
||||
}
|
||||
};
|
||||
if (settingsUpdated) {
|
||||
saveSettings();
|
||||
}
|
||||
}, [state.settings, settingsUpdated]);
|
||||
|
||||
|
||||
const addBases = (newBases) => {
|
||||
dispatch({ type: "ADD_BASES", payload: newBases });
|
||||
};
|
||||
|
||||
const addBase = (newBase) => {
|
||||
const payload = {
|
||||
id: uuidv4(),
|
||||
name: newBase.name,
|
||||
srvr: newBase.srvr,
|
||||
ref: newBase.ref,
|
||||
publications: [
|
||||
{
|
||||
id: uuidv4(),
|
||||
name: newBase.ref,
|
||||
title: newBase.name,
|
||||
enable: true,
|
||||
active: false,
|
||||
usr: "",
|
||||
pwd: "",
|
||||
enableStandardOData: false,
|
||||
ws: {
|
||||
publishExtensionsByDefault: true,
|
||||
wsList: []
|
||||
},
|
||||
httpServices: {
|
||||
publishExtensionsByDefault: true,
|
||||
publishByDefault: true,
|
||||
hsList: []
|
||||
}
|
||||
}
|
||||
],
|
||||
};
|
||||
dispatch({
|
||||
type: "ADD_BASE",
|
||||
payload: payload,
|
||||
});
|
||||
setBaseUpdated(true);
|
||||
setSelectedBaseId(payload.id);
|
||||
};
|
||||
|
||||
const updateBase = (updatedBase) => {
|
||||
dispatch({ type: "UPDATE_BASE", payload: updatedBase });
|
||||
setBaseUpdated(true);
|
||||
};
|
||||
|
||||
|
||||
const deleteBase = (deletedBase) => {
|
||||
dispatch({ type: 'DELETE_BASE', payload: deletedBase.id });
|
||||
setBaseUpdated(true);
|
||||
};
|
||||
|
||||
const addCrs = (newServers) => {
|
||||
dispatch({ type: "ADD_SERVERS", payload: newServers });
|
||||
};
|
||||
|
||||
const addServer = (newServer) => {
|
||||
const payload = {
|
||||
id: uuidv4(),
|
||||
name: newServer.name,
|
||||
active: newServer.active,
|
||||
title: newServer.title,
|
||||
tcpAddress: newServer.tcpAddress
|
||||
};
|
||||
dispatch({
|
||||
type: "ADD_SERVER",
|
||||
payload: payload,
|
||||
});
|
||||
setCrsUpdated(true);
|
||||
};
|
||||
|
||||
const updateServer = (updatedServer) => {
|
||||
dispatch({ type: "UPDATE_SERVER", payload: updatedServer });
|
||||
setCrsUpdated(true);
|
||||
};
|
||||
|
||||
const deleteServer = (deletedServer) => {
|
||||
dispatch({ type: 'DELETE_SERVER', payload: deletedServer.id });
|
||||
setCrsUpdated(true);
|
||||
};
|
||||
|
||||
const updatePublication = (publicationId, updatedPublication) => {
|
||||
dispatch({
|
||||
type: "UPDATE_PUBLICATION",
|
||||
payload: { publicationId, updatedPublication },
|
||||
});
|
||||
setBaseUpdated(true);
|
||||
};
|
||||
|
||||
const addPublication = (newPublication) => {
|
||||
const publicationWithId = {
|
||||
...newPublication,
|
||||
id: uuidv4(),
|
||||
};
|
||||
|
||||
dispatch({
|
||||
type: "ADD_PUBLICATION",
|
||||
payload: { newPublication: publicationWithId },
|
||||
});
|
||||
setBaseUpdated(true);
|
||||
};
|
||||
|
||||
const deletePublication = (deletedPublication) => {
|
||||
const deletedPublicationId = deletedPublication.id;
|
||||
dispatch({ type: 'DELETE_PUBLICATION', payload: deletedPublicationId });
|
||||
setBaseUpdated(true);
|
||||
};
|
||||
|
||||
const setSelectedBaseId = (id) => {
|
||||
dispatch({ type: "SET_SELECTED_BASE_ID", payload: id });
|
||||
};
|
||||
|
||||
const getUniqueTitles = () => {
|
||||
const selectedBase = state.bases.find(base => base.id === state.selectedBaseId);
|
||||
if (!selectedBase) return [];
|
||||
const titles = selectedBase.publications.map(publication => publication.title);
|
||||
return [...new Set(titles)];
|
||||
};
|
||||
|
||||
const getUniqueEndpoints = () => {
|
||||
const allPublications = state.bases.flatMap(base => base.publications);
|
||||
const endpoints = allPublications.map(publication => publication.name);
|
||||
return [...new Set(endpoints)];
|
||||
};
|
||||
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const showSnackbar = (message, variant = "default") => {
|
||||
enqueueSnackbar(message, {
|
||||
variant: variant,
|
||||
anchorOrigin: {
|
||||
vertical: "bottom",
|
||||
horizontal: "right",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const updateSettings = (newSettings) => {
|
||||
dispatch({ type: "UPDATE_SETTINGS", payload: newSettings });
|
||||
setSettingsUpdated(true);
|
||||
};
|
||||
|
||||
const loadSettings = (newSettings) => {
|
||||
dispatch({ type: "UPDATE_SETTINGS", payload: newSettings });
|
||||
};
|
||||
|
||||
return (
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
state,
|
||||
addBases,
|
||||
addBase,
|
||||
updateBase,
|
||||
deleteBase,
|
||||
updatePublication,
|
||||
addPublication,
|
||||
showSnackbar,
|
||||
setSelectedBaseId,
|
||||
getUniqueTitles,
|
||||
getUniqueEndpoints,
|
||||
deletePublication,
|
||||
updateSettings,
|
||||
addServer,
|
||||
updateServer,
|
||||
deleteServer,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AppContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppContextProvider;
|
@ -4,7 +4,7 @@ import App from "./App";
|
||||
import { ThemeProviderWrapper } from "./ThemeContext";
|
||||
import AppContextProvider from "./context/AppContextProvider.js";
|
||||
import { SnackbarProvider } from "notistack";
|
||||
import * as serviceWorker from "./serviceWorker";
|
||||
import * as serviceWorkerRegistration from './serviceWorkerRegistration';
|
||||
|
||||
const rootElement = document.getElementById("root");
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
@ -19,15 +19,5 @@ root.render(
|
||||
</ThemeProviderWrapper>
|
||||
);
|
||||
|
||||
if ("serviceWorker" in navigator) {
|
||||
window.addEventListener("load", () => {
|
||||
navigator.serviceWorker
|
||||
.register("/service-worker.js")
|
||||
.then((registration) => {
|
||||
console.log("SW registered: ", registration);
|
||||
})
|
||||
.catch((registrationError) => {
|
||||
console.log("SW registration failed: ", registrationError);
|
||||
});
|
||||
});
|
||||
}
|
||||
// Register the service worker
|
||||
serviceWorkerRegistration.register();
|
@ -1,98 +0,0 @@
|
||||
const isLocalhost = Boolean(
|
||||
window.location.hostname === "localhost" ||
|
||||
window.location.hostname === "[::1]" ||
|
||||
window.location.hostname.match(
|
||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||
)
|
||||
);
|
||||
|
||||
export function register(config) {
|
||||
if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
|
||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
|
||||
if (publicUrl.origin !== window.location.origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||
|
||||
if (isLocalhost) {
|
||||
checkValidServiceWorker(swUrl, config);
|
||||
|
||||
navigator.serviceWorker.ready.then(() => {
|
||||
console.log("This web app is being served cache-first by a service worker.");
|
||||
});
|
||||
} else {
|
||||
registerValidSW(swUrl, config);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function registerValidSW(swUrl, config) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then((registration) => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
if (installingWorker == null) {
|
||||
return;
|
||||
}
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === "installed") {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
if (config && config.onUpdate) {
|
||||
config.onUpdate(registration);
|
||||
}
|
||||
} else {
|
||||
console.log("Content is cached for offline use.");
|
||||
|
||||
if (config && config.onSuccess) {
|
||||
config.onSuccess(registration);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error during service worker registration:", error);
|
||||
});
|
||||
}
|
||||
|
||||
function checkValidServiceWorker(swUrl, config) {
|
||||
fetch(swUrl, {
|
||||
headers: { "Service-Worker": "script" },
|
||||
})
|
||||
.then((response) => {
|
||||
const contentType = response.headers.get("content-type");
|
||||
if (
|
||||
response.status === 404 ||
|
||||
(contentType != null && contentType.indexOf("javascript") === -1)
|
||||
) {
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
registerValidSW(swUrl, config);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.log("No internet connection found. App is running in offline mode.");
|
||||
});
|
||||
}
|
||||
|
||||
export function unregister() {
|
||||
if ("serviceWorker" in navigator) {
|
||||
navigator.serviceWorker.ready
|
||||
.then((registration) => {
|
||||
registration.unregister();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
12
react_frontend/src/serviceWorkerRegistration.js
Normal file
12
react_frontend/src/serviceWorkerRegistration.js
Normal file
@ -0,0 +1,12 @@
|
||||
export function register() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('/service-worker.js')
|
||||
.then(registration => {
|
||||
console.log('SW registered: ', registration);
|
||||
}).catch(registrationError => {
|
||||
console.log('SW registration failed: ', registrationError);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
1
volumes/config/config.json
Normal file
1
volumes/config/config.json
Normal file
@ -0,0 +1 @@
|
||||
{"bases":[{"id":"7f0b7c34-3b64-4c2d-a8f4-30495f211221","name":"test_test23","srvr":"test_test","ref":"test_test","publications":[{"id":"622a81fa-2737-49fa-b5aa-17b68afa1881","name":"test_test","title":"test_test","enable":true,"active":false,"usr":"","pwd":"","enableStandardOData":false,"ws":{"publishExtensionsByDefault":true,"wsList":[]},"httpServices":{"publishExtensionsByDefault":true,"publishByDefault":true,"hsList":[]}}]},{"id":"d6cce2b4-075d-46e6-b53f-503d6dde0e75","name":"test23","srvr":"sdffsdfds","ref":"sdfdsffds","publications":[{"id":"d84560f6-e392-46a5-b4d4-aa0fbe5656ae","name":"sdfdsffds","title":"test23","enable":true,"active":false,"usr":"","pwd":"","enableStandardOData":false,"ws":{"publishExtensionsByDefault":true,"wsList":[]},"httpServices":{"publishExtensionsByDefault":true,"publishByDefault":true,"hsList":[]}}]}],"crs":[{"id":"03155794-78b9-4a48-b7dc-daae2a2eff8b","name":"repository","active":false,"title":"Репозиторий по умолчанию","tcpAddress":"tcp://localhost"}]}
|
@ -3,7 +3,7 @@ FROM alpine:latest as downloader
|
||||
ARG ONEC_USERNAME
|
||||
ARG ONEC_PASSWORD
|
||||
ARG ONEC_VERSION
|
||||
ENV installer_type=server-crs-ws
|
||||
ENV installer_type=server-crs
|
||||
ENV downloads=downloads/platform83/${ONEC_VERSION}
|
||||
ENV RAS_PORT=1545
|
||||
ENV RAS_CLUSTER_ADDRESS=''
|
||||
@ -54,6 +54,12 @@ RUN apt-get install -yq geoclue-2.0 gstreamer1.0-plugins-bad
|
||||
# Правим локаль на русскую
|
||||
RUN export LANG=ru_RU.UTF-8
|
||||
|
||||
ARG ONEC_VERSION
|
||||
ENV installer_type=server-crs
|
||||
ARG gosu_ver=1.11
|
||||
ARG nls_enabled=false
|
||||
ENV nls=$nls_enabled
|
||||
|
||||
|
||||
COPY ./webserver/scripts/install.sh /install.sh
|
||||
COPY --from=downloader /tmp/*.* /tmp/
|
||||
@ -146,6 +152,9 @@ RUN opm install opm \
|
||||
COPY ./webserver/docker-entrypoint.sh /winow/
|
||||
COPY ./webserver/app /opt/app/winow/
|
||||
|
||||
RUN mkdir -p /opt/app/repo \
|
||||
&& chmod -R 777 /opt/app/repo
|
||||
|
||||
RUN chmod 755 /winow/docker-entrypoint.sh \
|
||||
&& chmod +x /winow/docker-entrypoint.sh
|
||||
|
||||
|
@ -39,7 +39,7 @@
|
||||
КонецЕсли;
|
||||
|
||||
УправлениеКонфигурацией.ЗаписатьКонфиг(Запрос.Тело);
|
||||
АпачМодификаторКонфига.ОпубликоватьБазы(УправлениеКонфигурацией.ПрочитатьКонфиг());
|
||||
АпачМодификаторКонфига.Опубликовать(УправлениеКонфигурацией.ПрочитатьКонфиг());
|
||||
Ответ.ТелоТекст = "{""success"": true, ""message"": ""Конфигурация успешно обновлена""}";
|
||||
Ответ.УстановитьСостояниеОК();
|
||||
КонецПроцедуры
|
||||
|
@ -1,10 +1,10 @@
|
||||
{
|
||||
"files": {
|
||||
"main.js": "/static/js/main.64d8a4e2.js",
|
||||
"main.js": "/static/js/main.3272b5e3.js",
|
||||
"index.html": "/index.html",
|
||||
"main.64d8a4e2.js.map": "/static/js/main.64d8a4e2.js.map"
|
||||
"main.3272b5e3.js.map": "/static/js/main.3272b5e3.js.map"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/js/main.64d8a4e2.js"
|
||||
"static/js/main.3272b5e3.js"
|
||||
]
|
||||
}
|
@ -1 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="shortcut icon" href="/favicon.ico"/><meta name="viewport" content="initial-scale=1,width=device-width,shrink-to-fit=no"/><meta name="theme-color" content="#000000"/><link rel="apple-touch-icon" href="/icon-192x192.png"/><link rel="icon" href="/icon-512x512.png" sizes="512x512" type="image/png"/><link rel="manifest" href="/manifest.json"/><title>Публикатор 1с</title><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"/><script defer="defer" src="/static/js/main.64d8a4e2.js"></script></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="shortcut icon" href="/favicon.ico"/><meta name="viewport" content="initial-scale=1,width=device-width,shrink-to-fit=no"/><meta name="theme-color" content="#000000"/><link rel="apple-touch-icon" href="/icon-192x192.png"/><link rel="icon" href="/icon-512x512.png" sizes="512x512" type="image/png"/><link rel="manifest" href="/manifest.json"/><title>Публикатор 1с</title><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"/><script defer="defer" src="/static/js/main.3272b5e3.js"></script></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
3
webserver/app/view/static/js/main.3272b5e3.js
Normal file
3
webserver/app/view/static/js/main.3272b5e3.js
Normal file
File diff suppressed because one or more lines are too long
1
webserver/app/view/static/js/main.3272b5e3.js.map
Normal file
1
webserver/app/view/static/js/main.3272b5e3.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -26,19 +26,40 @@
|
||||
КонецЕсли;
|
||||
КонецПроцедуры
|
||||
|
||||
Процедура ОпубликоватьБазы(ОписаниеБаз) Экспорт
|
||||
Лог.Информация("Приступаем к публикации баз");
|
||||
Процедура Опубликовать(Описание) Экспорт
|
||||
Лог.Информация("Приступаем к публикации");
|
||||
СоздатьОбновитьbase_onec_implment();
|
||||
Если не ОписаниеБаз.Свойство("bases") Тогда
|
||||
|
||||
ОпубликоватьБазы(Описание);
|
||||
ОпубликоватьСервераХранилища(Описание);
|
||||
|
||||
КонецПроцедуры
|
||||
|
||||
Процедура ОпубликоватьБазы(Описание)
|
||||
Лог.Информация("Приступаем к публикации баз");
|
||||
// Публикуем базы
|
||||
Если не Описание.Свойство("bases") Тогда
|
||||
Лог.Информация("Не задано ни одной базы для публикации.");
|
||||
Возврат;
|
||||
КонецЕсли;
|
||||
|
||||
Для каждого ОписаниеБазы Из ОписаниеБаз.bases Цикл
|
||||
Для каждого ОписаниеБазы Из Описание.bases Цикл
|
||||
ОпубликоватьБазу(ОписаниеБазы);
|
||||
КонецЦикла;
|
||||
КонецПроцедуры
|
||||
|
||||
Процедура ОпубликоватьСервераХранилища(Описание)
|
||||
Лог.Информация("Приступаем к публикации серверов хранилища");
|
||||
|
||||
Если не Описание.Свойство("crs") Тогда
|
||||
Лог.Информация("Не задано ни одного сервера хранилища для публикации.");
|
||||
Возврат;
|
||||
КонецЕсли;
|
||||
|
||||
Для каждого ОписаниеСервера Из Описание.crs Цикл
|
||||
ОпубликоватьСерверХранилища(ОписаниеСервера);
|
||||
КонецЦикла;
|
||||
КонецПроцедуры
|
||||
|
||||
Процедура ОпубликоватьБазу(СтруктураБазыДляПубликации) Экспорт
|
||||
СтрокаПодключенияКБД = СтрШаблон("Srvr=%1;Ref=%2;",
|
||||
СтруктураБазыДляПубликации.Srvr,
|
||||
@ -58,10 +79,21 @@
|
||||
КонецЕсли;
|
||||
|
||||
СоздатьDefaultVrd(Публикация, СтрокаПодключенияКБД);
|
||||
СоздатьКонфигПубликации(Публикация.name);
|
||||
СоздатьКонфигПубликацииБазы(Публикация.name);
|
||||
КонецЦикла;
|
||||
КонецПроцедуры
|
||||
|
||||
Процедура ОпубликоватьСерверХранилища(ОписаниеСервера)
|
||||
Лог.Информация("Обрабатываем сервер хранилища ""%1""", ОписаниеСервера.title);
|
||||
Если ОписаниеСервера.Свойство("active") и не ОписаниеСервера.active Тогда
|
||||
Лог.Отладка("Публикация сервера %1 с location %2 отключена и не публикуется.", ОписаниеСервера.title, ОписаниеСервера.name);
|
||||
Возврат;
|
||||
КонецЕсли;
|
||||
|
||||
Создатьrepo(ОписаниеСервера);
|
||||
СоздатьКонфигПубликацииСервераХранилища(ОписаниеСервера.name);
|
||||
КонецПроцедуры
|
||||
|
||||
Процедура СоздатьОбновитьbase_onec_implment()
|
||||
Лог.Отладка("Проверим существование base_onec_implment.conf в текущей конфигурации апач");
|
||||
|
||||
@ -106,7 +138,19 @@
|
||||
|
||||
КонецПроцедуры
|
||||
|
||||
Процедура СоздатьКонфигПубликации(ИмяПубликации)
|
||||
Процедура Создатьrepo(ОписаниеСервера)
|
||||
Лог.Отладка("Приступаем к созданию 1ccr файла");
|
||||
Шаблонrepo = ШаблоныФайловКонфига.crsPublication();
|
||||
Текстrepo = СтрШаблон(Шаблонrepo, ОписаниеСервера.tcpAddress);
|
||||
|
||||
ИмяКаталога = "/"+ОбъединитьПути(НастройкиПубликатора.КаталогПубликаций, ОписаниеСервера.name);
|
||||
СоздатьКаталог(ИмяКаталога);
|
||||
ПутьКФайлу = "/"+ОбъединитьПути(ИмяКаталога, "repo.1ccr");
|
||||
ФайловыеОперации.ЗаписатьТекстФайла(ПутьКФайлу, Текстrepo);
|
||||
Лог.Информация("Создан 1ccr файл публикации. Путь к файлу: %1", ПутьКФайлу);
|
||||
КонецПроцедуры
|
||||
|
||||
Процедура СоздатьКонфигПубликацииБазы(ИмяПубликации)
|
||||
Лог.Отладка("Приступаем к созданию conf файла");
|
||||
КаталогПубликации = ОбъединитьПути(НастройкиПубликатора.КаталогПубликаций, ИмяПубликации);
|
||||
ПутьКDefaultVrd = ОбъединитьПути(КаталогПубликации, "default.vrd");
|
||||
@ -122,6 +166,19 @@
|
||||
ДобавитьЗаписьBase_onec_implement(ОтносительныйКусокПути, ИмяФайла);
|
||||
КонецПроцедуры
|
||||
|
||||
Процедура СоздатьКонфигПубликацииСервераХранилища(ИмяПубликации)
|
||||
Лог.Отладка("Приступаем к созданию conf файла");
|
||||
КаталогПубликации = ОбъединитьПути(НастройкиПубликатора.КаталогПубликаций, ИмяПубликации);
|
||||
ТекстКонфига = СтрШаблон(ШаблоныФайловКонфига.КонфигПубликацииСервераХранилища(), ИмяПубликации, КаталогПубликации);
|
||||
ИмяФайла = СтрШаблон("%1.conf", ИмяПубликации);
|
||||
ОтносительныйКусокПути = ОбъединитьПути("conf","1C");
|
||||
ПутьКФайлу = ОбъединитьПути(НастройкиПубликатора.КаталогАпач, ОтносительныйКусокПути,ИмяФайла);
|
||||
ФайловыеОперации.ЗаписатьТекстФайла(ПутьКФайлу, ТекстКонфига);
|
||||
Лог.Информация("Создан conf файл публикации. Путь к файлу: %1", ПутьКФайлу);
|
||||
|
||||
ДобавитьЗаписьBase_onec_implement(ОтносительныйКусокПути, ИмяФайла);
|
||||
КонецПроцедуры
|
||||
|
||||
Процедура ДобавитьЗаписьBase_onec_implement(ОтносительныйКусокПути, ИмяФайла)
|
||||
Лог.Отладка("Приступаем к модификации файла со списком конфигураций");
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
Процедура ПриЗапускеПриложения() Экспорт
|
||||
Лог.Отладка("Запускаем приложение.");
|
||||
АпачМодификаторКонфига.ПервоначальныеНастройкиАпач();
|
||||
АпачМодификаторКонфига.ОпубликоватьБазы(УправлениеКонфигурацией.ПрочитатьКонфиг());
|
||||
АпачМодификаторКонфига.Опубликовать(УправлениеКонфигурацией.ПрочитатьКонфиг());
|
||||
АпачУправлятор.ЗапуститьАпач();
|
||||
КонецПроцедуры
|
||||
|
||||
|
@ -40,7 +40,7 @@
|
||||
|
||||
Функция ВернутьТекстКонфига() Экспорт
|
||||
ТекстКонфига = ФайловыеОперации.ПрочитатьТекстФайла(НастройкиПубликатора.ПутьККонфигу);
|
||||
Возврат ?(ЗначениеЗаполнено(ТекстКонфига), ТекстКонфига, "{""bases"":[]}");
|
||||
Возврат ?(ЗначениеЗаполнено(ТекстКонфига), ТекстКонфига, "{""bases"":[], ""crs"":[]}");
|
||||
КонецФункции
|
||||
|
||||
Функция ВернутьТекстНастроек() Экспорт
|
||||
|
@ -27,6 +27,18 @@
|
||||
|</Directory>";
|
||||
КонецФункции
|
||||
|
||||
Функция КонфигПубликацииСервераХранилища() Экспорт
|
||||
Возврат "Alias ""/%1"" ""/%2/""
|
||||
|<Directory ""/%2/"">
|
||||
| AllowOverride All
|
||||
| Require all granted
|
||||
| Order allow,deny
|
||||
| Allow from all
|
||||
| SetHandler 1cws-process
|
||||
|</Directory>";
|
||||
|
||||
КонецФункции
|
||||
|
||||
Функция СекцияPoint() Экспорт
|
||||
Возврат "<point name=""%1""
|
||||
| alias=""%2""
|
||||
@ -59,6 +71,12 @@
|
||||
|
||||
Функция base_onec_config() Экспорт
|
||||
Возврат "LoadModule _1cws_module %1
|
||||
|AddHandler 1cws-process .1ccr
|
||||
|
|
||||
|# Тут будут описания всех подключенных конфигураций";
|
||||
КонецФункции
|
||||
|
||||
Функция crsPublication() Экспорт
|
||||
Возврат "<?xml version=""1.0"" encoding=""UTF-8""?>
|
||||
| <repository connectString=""%1""/>";
|
||||
КонецФункции
|
@ -2,7 +2,7 @@
|
||||
|
||||
echo "starting crserver"
|
||||
|
||||
exec gosu usr1cv8 /opt/1cv8/current/crserver -port 1542 -d /home/usr1cv8/.1cv8/repo -daemon &
|
||||
exec gosu usr1cv8 /opt/1cv8/current/crserver -port 1542 -d /opt/app/repo -daemon &
|
||||
|
||||
echo "starting winow"
|
||||
|
||||
|
@ -58,11 +58,11 @@ else
|
||||
echo $nls_install
|
||||
./setup-full-${ONEC_VERSION}-x86_64.run --mode unattended --enable-components server,ws,$nls_install
|
||||
;;
|
||||
server-crs-ws)
|
||||
server-crs)
|
||||
set -x
|
||||
echo $nls_install
|
||||
./setup-full-${ONEC_VERSION}-x86_64.run --mode unattended --enable-components server,ws,config_storage_server,$nls_install
|
||||
;;
|
||||
./setup-full-${ONEC_VERSION}-x86_64.run --mode unattended --enable-components server,ws,config_storage_server,$nls_install
|
||||
;;
|
||||
server32)
|
||||
./setup-full-${ONEC_VERSION}-i386.run --mode unattended --enable-components server,ws,config_storage_server,$nls_install
|
||||
;;
|
||||
|
Loading…
Reference in New Issue
Block a user