Merge branch 'new_frontend'
28
README.md
@ -41,38 +41,28 @@
|
||||
|
||||
Для удобства использования реализован веб интерфейс, доступный по порту :3333
|
||||
|
||||
![Веб-интерфейс](/docs/img/web.png)
|
||||
### Графический интерфейс
|
||||
|
||||
Здесь можно отредактировать/сохранить конфигурацию и перезапустить веб-сервер apache.
|
||||
Реализован графичекий интерфейс. Я постарался его сделать адаптивным и приятно выглядищим на всех видах устройств.
|
||||
![Веб интерфейс](./docs/img/web.png)
|
||||
|
||||
## Структура конфига
|
||||
Посмотреть пример конфига можно в файле [config.json.example](/volumes/config.json.example)
|
||||
### bases
|
||||
|
||||
Массив со списком информационных баз 1с. На жанный момент поддерживаются только серверные варианты баз
|
||||
### Редактирование карточки публикации
|
||||
|
||||
Потому обязаельно надо заполнить свойства `Srvr` и `Ref`
|
||||
### publications
|
||||
* Реализована графическая настройка и валидация таких групп параметров как http-сервисы, web-сервисы, общие настройки и oidc
|
||||
|
||||
Массив публикаций конкртеной базы. Их может быть несколько. Например в одна для использование веб-интерфейса, а другая для публикации анонимных http-сервисов
|
||||
![Редактирование публикации](./docs/img/%D0%A0%D0%B5%D0%B4%D0%B0%D0%BA%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5%20%D0%BF%D1%83%D0%B1%D0%BB%D0%B8%D0%BA%D0%B0%D1%86%D0%B8%D0%B8.png)
|
||||
|
||||
Можно указать логин и пароль для авторизации в БД, а так же признак `enable` разрешающий или запрещающий вход.
|
||||
### ws
|
||||
### Настройка OpenId Connect
|
||||
|
||||
Секция описывающая веб сервисы публикации. Состоит из массива `wsList`, а так же из опции `publishExtensionsByDefault`
|
||||
Реализован, и будет дополняться графический интерфейс для реализации oidc подключений к базе.
|
||||
|
||||
### httpServices
|
||||
![oidc](./docs/img/%D0%9D%D0%B0%D1%81%D1%82%D1%80%D0%BE%D0%B9%D0%BA%D0%B0%20oidc.png)
|
||||
|
||||
Секция описывающая http-сервисы публикации. Состоит из массива `hsList`, а так же свойств `publishExtensionsByDefault` и `publishByDefault`
|
||||
|
||||
### oidc
|
||||
|
||||
Секция описывающая подключение по openid connect. содерит в себе массив `providers` описание заполнения этого масссива можно найти на ИТС
|
||||
## To-do
|
||||
Что будет дорабатываться:
|
||||
|
||||
* Добавить схему json для подсказок и валидации
|
||||
* В ближайших планах, все таки понять как рисовать красивый фронт и сделать конструктор публикаций без прямого редактирования конфигурации
|
||||
|
||||
## Спасибо
|
||||
|
||||
|
@ -7,6 +7,6 @@ services:
|
||||
- 998:80
|
||||
- 3333:3333
|
||||
volumes:
|
||||
- "./volumes/config.json:/opt/app/winow/config/config.json"
|
||||
- "./volumes/config:/opt/app/winow/config"
|
||||
# - "./volumes/autumn-properties.json:/opt/app/winow/autumn-properties.json"
|
||||
|
BIN
docs/img/Настройка oidc.png
Normal file
After Width: | Height: | Size: 76 KiB |
BIN
docs/img/Редактирование публикации.png
Normal file
After Width: | Height: | Size: 58 KiB |
2912
react_frontend/package-lock.json
generated
@ -13,8 +13,9 @@
|
||||
"@emotion/styled": "latest",
|
||||
"@mui/icons-material": "^5.11.11",
|
||||
"@mui/material": "latest",
|
||||
"install": "^0.13.0",
|
||||
"npm": "^9.6.2",
|
||||
"@mui/system": "^5.11.13",
|
||||
"material-ui-popup-state": "^5.0.8",
|
||||
"notistack": "^3.0.1",
|
||||
"react": "latest",
|
||||
"react-dom": "latest",
|
||||
"react-scripts": "latest"
|
||||
@ -30,5 +31,8 @@
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"workbox-webpack-plugin": "^6.5.4"
|
||||
}
|
||||
}
|
||||
|
BIN
react_frontend/public/icon-192x192.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
react_frontend/public/icon-512x512.png
Normal file
After Width: | Height: | Size: 42 KiB |
@ -1,17 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="shortcut icon" href="%PUBLIC_URL%/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="%PUBLIC_URL%/icon-192x192.png" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/icon-512x512.png" sizes="512x512" type="image/png" />
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<!--
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
@ -20,16 +22,17 @@
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>Публикатор 1с</title>
|
||||
<!-- Fonts to support Material Design -->
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
<title>Публикатор 1с </title>
|
||||
<!-- Fonts to support Material Design -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
@ -39,6 +42,5 @@
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</body>
|
||||
</html>
|
||||
|
BIN
react_frontend/public/logo_dark.png
Normal file
After Width: | Height: | Size: 29 KiB |
@ -1,11 +1,21 @@
|
||||
{
|
||||
"short_name": "Your Orders",
|
||||
"name": "Your Orders",
|
||||
"short_name": "Публикатор 1с",
|
||||
"name": "Публикатор 1с",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
|
65
react_frontend/src/ApiProcessor.js
Normal file
@ -0,0 +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);
|
||||
};
|
@ -1,50 +1,37 @@
|
||||
import * as React from 'react';
|
||||
import Container from '@mui/material/Container';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Box from '@mui/material/Box';
|
||||
import Link from '@mui/material/Link';
|
||||
import AppBarComponent from './AppBarComponent';
|
||||
import Content from './Content';
|
||||
import { DataProvider } from './DataContext';
|
||||
import React, { useState, useContext } from "react";
|
||||
import {
|
||||
Box,
|
||||
Container,
|
||||
Typography,
|
||||
Link
|
||||
} from "@mui/material";
|
||||
|
||||
function Copyright() {
|
||||
return (
|
||||
<Typography variant="body2" color="text.secondary" align="center">
|
||||
{'Copyright © '}
|
||||
<Link color="inherit" href="https://1cdevelopers.ru/">
|
||||
1cDevelopers.ru
|
||||
</Link>{' '}
|
||||
{new Date().getFullYear()}
|
||||
{'.'}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
import AppBarMenu from "./components/appBarMenu/AppBarMenu";
|
||||
import BasesMenu from "./components/basesMenu/BasesMenu"
|
||||
import PublicationsInfo from "./components/PublicationsInfo/PublicationsInfo"
|
||||
import Copyright from "./components/Copyright";
|
||||
|
||||
export default function App() {
|
||||
const [mobileOpen, setMobileOpen] = React.useState(false);
|
||||
|
||||
|
||||
const handleDrawerToggle = () => {
|
||||
setMobileOpen(!mobileOpen);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<DataProvider>
|
||||
|
||||
<AppBarComponent handleDrawerToggle={handleDrawerToggle} />
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minHeight: '99vh',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Content handleDrawerToggle={handleDrawerToggle} mobileOpen={mobileOpen} />
|
||||
<>
|
||||
<Box sx={{ display: "flex", height: "100vh", flexDirection: "column" }}>
|
||||
<AppBarMenu handleDrawerToggle={handleDrawerToggle} position="fixed" />
|
||||
<Box sx={{ display: "flex", flexGrow: 1 }}>
|
||||
<BasesMenu handleDrawerToggle={handleDrawerToggle} mobileOpen={mobileOpen} />
|
||||
<Box component="main" sx={{ flexGrow: 1, p: 3, display: "flex", flexDirection: "column" }}>
|
||||
<Container maxWidth="lg">
|
||||
<PublicationsInfo />
|
||||
<Copyright sx={{ marginTop: "auto" }} />
|
||||
</Container>
|
||||
</Box>
|
||||
</Box>
|
||||
<Copyright />
|
||||
</Box>
|
||||
</DataProvider>
|
||||
</>
|
||||
);
|
||||
}
|
44
react_frontend/src/ThemeContext.js
Normal file
@ -0,0 +1,44 @@
|
||||
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;
|
31
react_frontend/src/components/ConfirmationDialog.js
Normal file
@ -0,0 +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>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmationDialog;
|
18
react_frontend/src/components/Copyright.js
Normal file
@ -0,0 +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>
|
||||
);
|
||||
}
|
||||
|
||||
export default Copyright;
|
@ -0,0 +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>
|
||||
);
|
||||
}
|
@ -0,0 +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>
|
||||
);
|
||||
}
|
@ -0,0 +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>
|
||||
);
|
||||
}
|
@ -0,0 +1,192 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Typography,
|
||||
TextField,
|
||||
Grid,
|
||||
Button,
|
||||
Box,
|
||||
} from '@mui/material';
|
||||
import { styled } from '@mui/system';
|
||||
import UploadIcon from '@mui/icons-material/Upload';
|
||||
|
||||
const InputImage = styled('input')({
|
||||
display: 'none',
|
||||
});
|
||||
|
||||
const ImagePreview = styled('img')({
|
||||
maxHeight: '100px',
|
||||
maxWidth: '100px',
|
||||
});
|
||||
|
||||
export default function GeneralSettings({ provider, onChange, providers }) {
|
||||
const [imagePreview, setImagePreview] = useState(provider.image);
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
validate(provider);
|
||||
}, [provider]);
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Grid container spacing={2} alignItems="center" sx={{ mt: 0.5 }}>
|
||||
<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}>
|
||||
<Box>
|
||||
<InputImage
|
||||
accept="image/*"
|
||||
id="contained-button-file"
|
||||
type="file"
|
||||
onChange={handleImageUpload}
|
||||
/>
|
||||
<label htmlFor="contained-button-file">
|
||||
<Button component="span" color="inherit" startIcon={<UploadIcon />}>
|
||||
Загрузить изображение
|
||||
</Button>
|
||||
</label>
|
||||
</Box>
|
||||
{imagePreview && (
|
||||
<ImagePreview
|
||||
src={imagePreview}
|
||||
alt="Предпросмотр изображения"
|
||||
/>
|
||||
)}
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +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>
|
||||
);
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
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 handleInputChange = (event) => {
|
||||
onChange({
|
||||
...provider,
|
||||
[event.target.name]: event.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
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}>
|
||||
<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>
|
||||
);
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Accordion, AccordionSummary, AccordionDetails, Typography, Button, Grid, Avatar } 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 = {
|
||||
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 } });
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid container direction="column" spacing={2}>
|
||||
{isProvidersUpdated &&
|
||||
providers.map((provider, index) => (
|
||||
<Grid item key={index}>
|
||||
<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>
|
||||
);
|
||||
}
|
@ -0,0 +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>
|
||||
);
|
||||
}
|
@ -0,0 +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>
|
||||
);
|
||||
}
|
@ -0,0 +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>
|
||||
);
|
||||
}
|
@ -0,0 +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}>
|
||||
<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>
|
||||
);
|
||||
}
|
@ -0,0 +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>
|
||||
);
|
||||
}
|
45
react_frontend/src/components/SnackbarHandler.js
Normal file
@ -0,0 +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;
|
||||
};
|
||||
|
||||
export default SnackbarHandler;
|
103
react_frontend/src/components/appBarMenu/AppBarMenu.js
Normal file
@ -0,0 +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>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppBarMenu;
|
@ -0,0 +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;
|
@ -0,0 +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.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;
|
@ -0,0 +1,53 @@
|
||||
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 handleChange = (e) => {
|
||||
setSettings({ ...settings, [e.target.name]: e.target.value });
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
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' }}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} color="primary">
|
||||
Закрыть
|
||||
</Button>
|
||||
<Button onClick={handleSave} color="primary">
|
||||
Сохранить
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsModal;
|
170
react_frontend/src/components/basesMenu/BasesMenu.js
Normal file
@ -0,0 +1,170 @@
|
||||
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>
|
||||
);
|
||||
}
|
@ -0,0 +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>
|
||||
);
|
||||
}
|
7
react_frontend/src/context/AppContext.js
Normal file
@ -0,0 +1,7 @@
|
||||
// AppContext.js
|
||||
|
||||
import { createContext } from 'react';
|
||||
|
||||
const AppContext = createContext();
|
||||
|
||||
export default AppContext;
|
275
react_frontend/src/context/AppContextProvider.js
Normal file
@ -0,0 +1,275 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppContextProvider;
|
@ -1,17 +1,33 @@
|
||||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom/client';
|
||||
import CssBaseline from '@mui/material/CssBaseline';
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
import App from './App';
|
||||
import theme from './darkTheme';
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
import { ThemeProviderWrapper } from "./ThemeContext";
|
||||
import AppContextProvider from "./context/AppContextProvider.js";
|
||||
import { SnackbarProvider } from "notistack";
|
||||
import * as serviceWorker from "./serviceWorker";
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
const rootElement = document.getElementById("root");
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
|
||||
root.render(
|
||||
<ThemeProvider theme={theme}>
|
||||
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
|
||||
<CssBaseline />
|
||||
<App />
|
||||
</ThemeProvider>,
|
||||
<ThemeProviderWrapper>
|
||||
<SnackbarProvider maxSnack={3}>
|
||||
<AppContextProvider>
|
||||
<App />
|
||||
</AppContextProvider>
|
||||
</SnackbarProvider>
|
||||
</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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
98
react_frontend/src/serviceWorker.js
Normal file
@ -0,0 +1,98 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
19
react_frontend/src/theme.js
Normal file
@ -0,0 +1,19 @@
|
||||
import { red } from '@mui/material/colors';
|
||||
import { createTheme } from '@mui/material/styles';
|
||||
|
||||
// A custom theme for this app
|
||||
const theme = createTheme({
|
||||
palette: {
|
||||
primary: {
|
||||
main: '#556cd6',
|
||||
},
|
||||
secondary: {
|
||||
main: '#19857b',
|
||||
},
|
||||
error: {
|
||||
main: red.A400,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default theme;
|
0
volumes/config/.gitkeep
Normal file
@ -11,6 +11,7 @@
|
||||
"КаталогПубликаций": "var/www/1C/",
|
||||
"КаталогАпач": "/usr/local/apache2/",
|
||||
"ПутьК1С": "/opt/1cv8/current/",
|
||||
"ПутьККонфигу": "./config/config.json"
|
||||
"ПутьККонфигу": "./config/config.json",
|
||||
"ПутьКНастройкам": "./config/settings.json"
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@
|
||||
|
||||
КонецПроцедуры
|
||||
|
||||
&ТочкаМаршрута("getconfig.json")
|
||||
&ТочкаМаршрута("config/getconfig")
|
||||
Процедура ПолучитьКонфиг(Ответ) Экспорт
|
||||
JsonСтрока = УправлениеКонфигурацией.ВернутьТекстКонфига();
|
||||
Ответ.УстановитьТипКонтента("json");
|
||||
@ -17,7 +17,16 @@
|
||||
Ответ.УстановитьСостояниеОК();
|
||||
КонецПроцедуры
|
||||
|
||||
&ТочкаМаршрута("updateconfig")
|
||||
&ТочкаМаршрута("config/getsettings")
|
||||
Процедура ПолучитьНастройки(Ответ) Экспорт
|
||||
JsonСтрока = УправлениеКонфигурацией.ВернутьТекстНастроек();
|
||||
Ответ.УстановитьТипКонтента("json");
|
||||
Ответ.ТелоТекст = JsonСтрока;
|
||||
Ответ.заголовки.Вставить("Access-Control-Allow-Origin", "*");
|
||||
Ответ.УстановитьСостояниеОК();
|
||||
КонецПроцедуры
|
||||
|
||||
&ТочкаМаршрута("config/updateconfig")
|
||||
Процедура ПерезаписатьКонфиг(Запрос, ТелоЗапросОбъект, Ответ) Экспорт
|
||||
Ответ.заголовки.Вставить("Access-Control-Allow-Origin", "*");
|
||||
Ответ.заголовки.Вставить("Access-Control-Allow-Headers", "Content-Type");
|
||||
@ -31,28 +40,48 @@
|
||||
|
||||
УправлениеКонфигурацией.ЗаписатьКонфиг(Запрос.Тело);
|
||||
АпачМодификаторКонфига.ОпубликоватьБазы(УправлениеКонфигурацией.ПрочитатьКонфиг());
|
||||
Ответ.ТелоТекст = "{""success"": true}";
|
||||
Ответ.ТелоТекст = "{""success"": true, ""message"": ""Конфигурация успешно обновлена""}";
|
||||
Ответ.УстановитьСостояниеОК();
|
||||
КонецПроцедуры
|
||||
|
||||
&ТочкаМаршрута("config/updatesettings")
|
||||
Процедура ПерезаписатьНастройки(Запрос, ТелоЗапросОбъект, Ответ) Экспорт
|
||||
Ответ.заголовки.Вставить("Access-Control-Allow-Origin", "*");
|
||||
Ответ.заголовки.Вставить("Access-Control-Allow-Headers", "Content-Type");
|
||||
Если не Запрос.Метод = "POST" Тогда
|
||||
Ответ.Модель = Новый Структура();
|
||||
Ответ.Модель.Вставить("КодСостояния", 405);
|
||||
Ответ.Модель.Вставить("ТекстСообщения", "Недопустимый метод!");
|
||||
Ответ.Модель.Вставить("Запрос", Запрос);
|
||||
Возврат;
|
||||
КонецЕсли;
|
||||
|
||||
УправлениеКонфигурацией.ЗаписатьНастройки(Запрос.Тело);
|
||||
Ответ.ТелоТекст = "{""success"": true, ""message"": ""Настройки успешно сохранены""}";
|
||||
Ответ.УстановитьСостояниеОК();
|
||||
КонецПроцедуры
|
||||
|
||||
&ТочкаМаршрута("ws/start")
|
||||
Процедура Старт(Ответ) Экспорт
|
||||
АпачУправлятор.ЗапуститьАпач();
|
||||
Ответ.ТелоТекст = "{""success"": true}";
|
||||
Ответ.ТелоТекст = "{""success"": true, ""message"": ""Сервер успешно запущен""}";
|
||||
Ответ.заголовки.Вставить("Access-Control-Allow-Origin", "*");
|
||||
Ответ.УстановитьСостояниеОК();
|
||||
КонецПроцедуры
|
||||
|
||||
&ТочкаМаршрута("ws/stop")
|
||||
Процедура Стоп(Ответ) Экспорт
|
||||
АпачУправлятор.ОстановитьАпач();
|
||||
Ответ.ТелоТекст = "{""success"": true}";
|
||||
Ответ.ТелоТекст = "{""success"": true, ""message"": ""Сервер успешно остановлен""}";
|
||||
Ответ.заголовки.Вставить("Access-Control-Allow-Origin", "*");
|
||||
Ответ.УстановитьСостояниеОК();
|
||||
КонецПроцедуры
|
||||
|
||||
&ТочкаМаршрута("ws/restart")
|
||||
Процедура Рестарт(Ответ) Экспорт
|
||||
АпачУправлятор.ПерезапуститьАпач();
|
||||
Ответ.ТелоТекст = "{""success"": true}";
|
||||
Ответ.ТелоТекст = "{""success"": true, ""message"": ""Сервер успешно перезапущен""}";
|
||||
Ответ.заголовки.Вставить("Access-Control-Allow-Origin", "*");
|
||||
Ответ.УстановитьСостояниеОК();
|
||||
КонецПроцедуры
|
||||
|
||||
|
@ -1,13 +1,10 @@
|
||||
{
|
||||
"files": {
|
||||
"main.css": "/static/css/main.b72ea8bc.css",
|
||||
"main.js": "/static/js/main.75b577f3.js",
|
||||
"main.js": "/static/js/main.270f6462.js",
|
||||
"index.html": "/index.html",
|
||||
"main.b72ea8bc.css.map": "/static/css/main.b72ea8bc.css.map",
|
||||
"main.75b577f3.js.map": "/static/js/main.75b577f3.js.map"
|
||||
"main.270f6462.js.map": "/static/js/main.270f6462.js.map"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.b72ea8bc.css",
|
||||
"static/js/main.75b577f3.js"
|
||||
"static/js/main.270f6462.js"
|
||||
]
|
||||
}
|
BIN
webserver/app/view/icon-192x192.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
webserver/app/view/icon-512x512.png
Normal file
After Width: | Height: | Size: 42 KiB |
@ -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"/><meta name="theme-color" content="#000000"/><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"/><link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons"/><script defer="defer" src="/static/js/main.75b577f3.js"></script><link href="/static/css/main.b72ea8bc.css" rel="stylesheet"></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.270f6462.js"></script></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
|
BIN
webserver/app/view/logo_dark.png
Normal file
After Width: | Height: | Size: 29 KiB |
@ -1,11 +1,21 @@
|
||||
{
|
||||
"short_name": "Your Orders",
|
||||
"name": "Your Orders",
|
||||
"short_name": "Публикатор 1с",
|
||||
"name": "Публикатор 1с",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
|
3
webserver/app/view/static/js/main.270f6462.js
Normal file
68
webserver/app/view/static/js/main.270f6462.js.LICENSE.txt
Normal file
@ -0,0 +1,68 @@
|
||||
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
|
||||
|
||||
/**
|
||||
* @license React
|
||||
* react-dom.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @license React
|
||||
* react-is.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @license React
|
||||
* react-jsx-runtime.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @license React
|
||||
* react.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @license React
|
||||
* scheduler.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @mui/styled-engine v5.11.11
|
||||
*
|
||||
* @license MIT
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/** @license React v16.13.1
|
||||
* react-is.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
1
webserver/app/view/static/js/main.270f6462.js.map
Normal file
@ -10,6 +10,9 @@
|
||||
&Деталька(Значение = "Публикатор.ПутьККонфигу", ЗначениеПоУмолчанию = "./config/config.json")
|
||||
Перем ПутьККонфигу Экспорт;
|
||||
|
||||
&Деталька(Значение = "Публикатор.ПутьКНастройкам", ЗначениеПоУмолчанию = "./config/settings.json")
|
||||
Перем ПутьКНастройкам Экспорт;
|
||||
|
||||
Перем ЭтоLinux Экспорт;
|
||||
|
||||
&Желудь
|
||||
|
@ -9,12 +9,13 @@
|
||||
|
||||
Функция ПрочитатьКонфиг() Экспорт
|
||||
Лог.Отладка("Приступаем к чтению Json c настройками.");
|
||||
Конфиг = ФайловыеОперации.ПрочитатьТекстФайла(НастройкиПубликатора.ПутьККонфигу);
|
||||
ПарсерJSON = Новый ПарсерJSON;
|
||||
Возврат ПарсерJSON.ПрочитатьJSON(ФайловыеОперации.ПрочитатьТекстФайла(НастройкиПубликатора.ПутьККонфигу),,,Истина);
|
||||
Возврат ПарсерJSON.ПрочитатьJSON(?(ЗначениеЗаполнено(Конфиг), Конфиг, "{}"),,,Истина);
|
||||
КонецФункции
|
||||
|
||||
Процедура ЗаписатьКонфиг(Знач Конфиг) Экспорт
|
||||
Лог.Отладка("Приступаем к обновлению Json c настройками.");
|
||||
Лог.Отладка("Приступаем к обновлению Json c конфигом.");
|
||||
|
||||
Если Не ТипЗнч(Конфиг) = Тип("Строка") Тогда
|
||||
Лог.Отладка("Передана не json строка. Сериализуем.");
|
||||
@ -25,8 +26,25 @@
|
||||
Лог.Информация("Файл конфигурации успешно обновлен.");
|
||||
КонецПроцедуры
|
||||
|
||||
Процедура ЗаписатьНастройки(Знач Настройки) Экспорт
|
||||
Лог.Отладка("Приступаем к обновлению Json c настройками.");
|
||||
|
||||
Если Не ТипЗнч(Настройки) = Тип("Строка") Тогда
|
||||
Лог.Отладка("Передана не json строка. Сериализуем.");
|
||||
ПарсерJSON = Новый ПарсерJSON;
|
||||
Настройки = ПарсерJSON.ЗаписатьJSON(Настройки);
|
||||
КонецЕсли;
|
||||
ФайловыеОперации.ЗаписатьТекстФайла(НастройкиПубликатора.ПутьКНастройкам, Настройки);
|
||||
Лог.Информация("Файл конфигурации успешно обновлен.");
|
||||
КонецПроцедуры
|
||||
|
||||
Функция ВернутьТекстКонфига() Экспорт
|
||||
Возврат ФайловыеОперации.ПрочитатьТекстФайла(НастройкиПубликатора.ПутьККонфигу);
|
||||
ТекстКонфига = ФайловыеОперации.ПрочитатьТекстФайла(НастройкиПубликатора.ПутьККонфигу);
|
||||
Возврат ?(ЗначениеЗаполнено(ТекстКонфига), ТекстКонфига, "{""bases"":[]}");
|
||||
КонецФункции
|
||||
|
||||
Функция ВернутьТекстНастроек() Экспорт
|
||||
ТекстКонфига = ФайловыеОперации.ПрочитатьТекстФайла(НастройкиПубликатора.ПутьКНастройкам);
|
||||
Возврат ?(ЗначениеЗаполнено(ТекстКонфига), ТекстКонфига, "{""publicationServerUrl"":""http://localhost""}");
|
||||
КонецФункции
|
||||
|
||||
|
@ -43,15 +43,22 @@
|
||||
//
|
||||
Функция ПрочитатьТекстФайла(ПутьКФайлу) Экспорт
|
||||
Лог.Отладка("Приступаем к чтению файла по пути: %1", ПутьКФайлу);
|
||||
Кодировка = КодировкаТекста.UTF8NoBOM;
|
||||
Текст = Новый ЧтениеТекста();
|
||||
Текст.Открыть(ПутьКФайлу, Кодировка);
|
||||
файл = НовыйФайл(ПутьКФайлу);
|
||||
Если файл.Существует() Тогда
|
||||
Кодировка = КодировкаТекста.UTF8NoBOM;
|
||||
Текст = Новый ЧтениеТекста();
|
||||
Текст.Открыть(ПутьКФайлу, Кодировка);
|
||||
|
||||
СодержимоеФайла = Текст.Прочитать();
|
||||
|
||||
Текст.Закрыть();
|
||||
|
||||
СодержимоеФайла = Текст.Прочитать();
|
||||
|
||||
Текст.Закрыть();
|
||||
Лог.Отладка("Файл успешно прочитан");
|
||||
Иначе
|
||||
СодержимоеФайла = "";
|
||||
Лог.Отладка("Файл не найден")
|
||||
КонецЕсли;
|
||||
|
||||
Лог.Отладка("Файл успешно прочитан");
|
||||
Возврат СодержимоеФайла;
|
||||
|
||||
КонецФункции // ПрочитатьТекстФайла
|
||||
|