1
0
mirror of https://github.com/Segate-ekb/publicator.git synced 2024-11-16 09:58:27 +02:00

Публикация хранилища (#2)

Добавил функционал публикации сервера хранилища.
Теперь данные о выбранной теме сохраняются для клиента
This commit is contained in:
Ivanov Egor 2023-05-23 12:57:58 +06:00 committed by GitHub
parent e3fba7afd9
commit 74a12be728
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 3563 additions and 3153 deletions

View File

@ -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"

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View 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"}]}

View File

@ -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

View File

@ -39,7 +39,7 @@
КонецЕсли;
УправлениеКонфигурацией.ЗаписатьКонфиг(Запрос.Тело);
АпачМодификаторКонфига.ОпубликоватьБазы(УправлениеКонфигурацией.ПрочитатьКонфиг());
АпачМодификаторКонфига.Опубликовать(УправлениеКонфигурацией.ПрочитатьКонфиг());
Ответ.ТелоТекст = "{""success"": true, ""message"": ""Конфигурация успешно обновлена""}";
Ответ.УстановитьСостояниеОК();
КонецПроцедуры

View File

@ -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"
]
}

View File

@ -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>

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

File diff suppressed because one or more lines are too long

View File

@ -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(ОтносительныйКусокПути, ИмяФайла)
Лог.Отладка("Приступаем к модификации файла со списком конфигураций");

View File

@ -7,7 +7,7 @@
Процедура ПриЗапускеПриложения() Экспорт
Лог.Отладка("Запускаем приложение.");
АпачМодификаторКонфига.ПервоначальныеНастройкиАпач();
АпачМодификаторКонфига.ОпубликоватьБазы(УправлениеКонфигурацией.ПрочитатьКонфиг());
АпачМодификаторКонфига.Опубликовать(УправлениеКонфигурацией.ПрочитатьКонфиг());
АпачУправлятор.ЗапуститьАпач();
КонецПроцедуры

View File

@ -40,7 +40,7 @@
Функция ВернутьТекстКонфига() Экспорт
ТекстКонфига = ФайловыеОперации.ПрочитатьТекстФайла(НастройкиПубликатора.ПутьККонфигу);
Возврат ?(ЗначениеЗаполнено(ТекстКонфига), ТекстКонфига, "{""bases"":[]}");
Возврат ?(ЗначениеЗаполнено(ТекстКонфига), ТекстКонфига, "{""bases"":[], ""crs"":[]}");
КонецФункции
Функция ВернутьТекстНастроек() Экспорт

View File

@ -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""/>";
КонецФункции

View File

@ -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"

View File

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