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

Заливаю первую итерацию нового фронта

This commit is contained in:
ivanovEV 2023-03-19 11:42:18 +03:00
parent 412698851e
commit afc2813912
54 changed files with 22777 additions and 230 deletions

5
.gitignore vendored
View File

@ -1,4 +1,7 @@
*.env
/webserver/distr/*
!/webserver/distr/.gitkeep
/volumes/config.json
/volumes/config.json
node_modules
/react_frontend/build

27
react_frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,27 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# IDEs and editors
/.idea
/.vscode
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

44
react_frontend/README.md Normal file
View File

@ -0,0 +1,44 @@
# Material UI - Create React App example
## How to use
Download the example [or clone the repo](https://github.com/mui/material-ui):
<!-- #default-branch-switch -->
```sh
curl https://codeload.github.com/mui/material-ui/tar.gz/master | tar -xz --strip=2 material-ui-master/examples/material-cra
cd material-cra
```
Install it and run:
```sh
npm install
npm start
```
or:
<!-- #default-branch-switch -->
[![Edit on CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/mui/material-ui/tree/master/examples/material-cra)
<!-- #default-branch-switch -->
[![Edit on StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/mui/material-ui/tree/master/examples/material-cra)
## The idea behind the example
<!-- #default-branch-switch -->
This example demonstrates how you can use [Create React App](https://github.com/facebookincubator/create-react-app) with Material UI.
It includes `@mui/material` and its peer dependencies, including [Emotion](https://emotion.sh/docs/introduction), the default style engine in Material UI v5.
If you prefer, you can [use styled-components instead](https://mui.com/material-ui/guides/interoperability/#styled-components).
## What's next?
<!-- #default-branch-switch -->
You now have a working example project.
You can head back to the documentation, continuing browsing it from the [templates](https://mui.com/material-ui/getting-started/templates/) section.

19801
react_frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,34 @@
{
"name": "material-cra",
"version": "5.0.0",
"private": true,
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"dependencies": {
"@emotion/react": "latest",
"@emotion/styled": "latest",
"@mui/icons-material": "^5.11.11",
"@mui/material": "latest",
"install": "^0.13.0",
"npm": "^9.6.2",
"react": "latest",
"react-dom": "latest",
"react-scripts": "latest"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,44 @@
<!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" />
<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" />
<!--
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.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
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>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

@ -0,0 +1,15 @@
{
"short_name": "Your Orders",
"name": "Your Orders",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -0,0 +1,95 @@
import React, { useState, useEffect } from 'react';
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, TextField } from '@mui/material';
const AddPublicationForm = ({ open, handleClose, handleAddPublication }) => {
const [name, setName] = useState('');
const [title, setTitle] = useState('');
const [nameError, setNameError] = useState(false);
const [titleError, setTitleError] = useState(false);
const [nameErrorText, setNameErrorText] = useState('');
const [titleErrorText, setTitleErrorText] = useState('');
useEffect(() => {
if (!open) {
resetForm();
}
}, [open]);
const resetForm = () => {
setName('');
setTitle('');
setNameError(false);
setTitleError(false);
setNameErrorText('');
setTitleErrorText('');
};
const validateInput = () => {
const nameValid = /^[a-zA-Z0-9-_]+$/.test(name) && name.length > 0;
const titleValid = title.length > 0;
setNameError(!nameValid);
setTitleError(!titleValid);
setNameErrorText(nameValid ? '' : 'Имя должно содержать только латинские символы, цифры и спецсимволы URL');
setTitleErrorText(titleValid ? '' : 'Название не может быть пустым');
return nameValid && titleValid;
};
const handleSubmit = () => {
if (validateInput()) {
handleAddPublication({ name, title });
handleClose();
}
};
return (
<Dialog
open={open}
onClose={handleClose}
aria-labelledby="form-dialog-title"
>
<DialogTitle id="form-dialog-title">Добавить публикацию</DialogTitle>
<DialogContent>
<TextField
margin="dense"
label="Название публикации"
value={title}
onChange={(e) => setTitle(e.target.value)}
fullWidth
error={titleError}
helperText={titleErrorText}
/>
<TextField
autoFocus
margin="dense"
label="Корневой url"
value={name}
onChange={(e) => setName(e.target.value)}
fullWidth
error={nameError}
helperText={nameErrorText}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
Отмена
</Button>
<Button
onClick={() => {
if (handleAddPublication({ name, title })) {
handleClose();
}
}}
color="primary"
>
Добавить
</Button>
</DialogActions>
</Dialog>
);
};
export default AddPublicationForm;

50
react_frontend/src/App.js Normal file
View File

@ -0,0 +1,50 @@
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';
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>
);
}
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>
<Copyright />
</Box>
</DataProvider>
);
}

View File

@ -0,0 +1,233 @@
import * as React from 'react';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import MenuIcon from '@mui/icons-material/Menu';
import RefreshIcon from '@mui/icons-material/Refresh';
import SaveIcon from '@mui/icons-material/Save';
import BuildIcon from '@mui/icons-material/Build';
import RestartAltIcon from '@mui/icons-material/RestartAlt';
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import StopIcon from '@mui/icons-material/Stop';
import {
AppBar,
Toolbar,
Typography,
IconButton,
Button,
Menu,
ListItemIcon,
ListItemText,
Snackbar,
Alert,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
DialogContentText,
} from '@mui/material';
import MenuItem from '@mui/material/MenuItem';
import { useBasesData } from './DataContext';
import { restartServer, stopServer, startServer, saveToServer } from './serverManagement';
import ProjectInfoForm from './ProjectInfoForm';
import InfoIcon from '@mui/icons-material/Info';
const darkTheme = createTheme({
palette: {
mode: 'dark',
primary: {
main: '#4caf50',
},
secondary: {
main: '#f44336',
},
},
});
export default function AppBarComponent(props) {
const { handleDrawerToggle } = props;
const [anchorEl, setAnchorEl] = React.useState(null);
const [snackbarOpen, setSnackbarOpen] = React.useState(false);
const [snackbarMessage, setSnackbarMessage] = React.useState('');
const [operationResult, setOperationResult] = React.useState({ success: true, message: '' });
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const { refreshData } = useBasesData();
const [projectInfoFormOpen, setProjectInfoFormOpen] = React.useState(false);
const handleProjectInfoFormOpen = () => {
setProjectInfoFormOpen(true);
};
const handleProjectInfoFormClose = () => {
setProjectInfoFormOpen(false);
};
const handleRefresh = async () => {
if (window.confirm('Вы действительно хотите обновить данные из базы?')) {
await refreshData();
}
};
const handleRestart = async () => {
const result = await restartServer();
setOperationResult(result);
setSnackbarOpen(true);
};
const handleStop = async () => {
const result = await stopServer();
setOperationResult(result);
setSnackbarOpen(true);
};
const handleStart = async () => {
const result = await startServer();
setOperationResult(result);
setSnackbarOpen(true);
};
const handleSnackbarClose = (event, reason) => {
if (reason === 'clickaway') {
return;
}
setSnackbarOpen(false);
};
const [saveDialogOpen, setSaveDialogOpen] = React.useState(false);
const handleSaveDialogOpen = () => {
setSaveDialogOpen(true);
};
const handleSaveDialogClose = () => {
setSaveDialogOpen(false);
};
const { jsonData } = useBasesData();
const handleSaveAndClose = async () => {
const result = await saveToServer(jsonData);
setOperationResult(result);
setSnackbarOpen(true);
if (result.success) {
setSaveDialogOpen(false); // закрыть диалог, если операция выполнена успешно
}
};
const handleSave = async () => {
handleSaveDialogOpen();
};
return (
<ThemeProvider theme={darkTheme}>
<AppBar position="fixed" sx={{ height: "64px", zIndex: (theme) => theme.zIndex.drawer + 1 }}>
<Toolbar sx={{ color: 'inherit' }}>
<IconButton
color="inherit"
aria-label="open drawer"
edge="start"
onClick={handleDrawerToggle}
sx={{ mr: 2, display: { xs: "block", sm: "none" }, }}
>
<MenuIcon />
</IconButton>
<img src="/logo.png" alt="Logo" style={{ maxHeight: '40px', marginRight: '8px' }} />
<Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}>
Публикатор 1с.
</Typography>
<Button color="inherit" onClick={handleRefresh}>
<RefreshIcon sx={{ mr: 1 }} />
<Typography component="span" sx={{ display: { xs: "none", lg: "inline" } }}>
Обновить из базы
</Typography>
</Button>
<Button color="inherit" onClick={handleSave}>
<SaveIcon sx={{ mr: 1 }} />
<Typography component="span" sx={{ display: { xs: "none", lg: "inline" } }}>
Сохранить
</Typography>
</Button>
<Button color="inherit" onClick={handleClick}>
<BuildIcon sx={{ mr: 1 }} />
<Typography component="span" sx={{ display: { xs: "none", lg: "inline" } }}>
Управление Веб-сервером
</Typography>
</Button>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleClose}
>
<MenuItem onClick={handleRestart}>
<ListItemIcon>
<RestartAltIcon />
</ListItemIcon>
<ListItemText>Перезапустить</ListItemText>
</MenuItem>
<MenuItem onClick={handleStart}>
<ListItemIcon>
<PlayArrowIcon />
</ListItemIcon>
<ListItemText>Запустить</ListItemText>
</MenuItem>
<MenuItem onClick={handleStop}>
<ListItemIcon>
<StopIcon />
</ListItemIcon>
<ListItemText>Остановить</ListItemText>
</MenuItem>
</Menu>
<Button color="inherit" onClick={handleProjectInfoFormOpen}>
<InfoIcon sx={{ mr: 1 }} />
<Typography component="span" sx={{ display: { xs: "none", lg: "inline" } }}>
Информация о проекте
</Typography>
</Button>
</Toolbar>
</AppBar>
<Snackbar
open={snackbarOpen}
autoHideDuration={6000}
onClose={handleSnackbarClose}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
>
<Alert onClose={handleSnackbarClose} severity={operationResult.success ? 'success' : 'error'} sx={{ width: '100%' }}>
{operationResult.message}
</Alert>
</Snackbar>
<ProjectInfoForm open={projectInfoFormOpen} onClose={handleProjectInfoFormClose} />
<Dialog
open={saveDialogOpen}
onClose={handleSaveDialogClose}
>
<DialogTitle>
Сохранить данные на сервере
</DialogTitle>
<DialogContent>
<DialogContentText>
Вы действительно хотите сохранить данные на сервере? Это перезапишет текущую конфигурацию.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleSaveDialogClose}>
Отмена
</Button>
<Button onClick={handleSaveAndClose} color="primary">
Сохранить
</Button>
</DialogActions>
</Dialog>
</ThemeProvider>
);
}

368
react_frontend/src/Bases.js Normal file
View File

@ -0,0 +1,368 @@
import * as React from 'react';
import IconButton from '@mui/material/IconButton';
import Drawer from '@mui/material/Drawer';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemText from '@mui/material/ListItemText';
import ListSubheader from '@mui/material/ListSubheader';
import Button from '@mui/material/Button';
import Fab from '@mui/material/Fab';
import EditIcon from '@mui/icons-material/Edit';
import AddIcon from '@mui/icons-material/Add';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import TextField from '@mui/material/TextField';
import Box from '@mui/material/Box';
import CssBaseline from '@mui/material/CssBaseline';
import { useBasesData } from './DataContext';
const drawerWidth = 360;
export default function BasesOptions(props) {
const { window } = props;
const { handleDrawerToggle, mobileOpen } = props;
const { basesData, setBasesData } = useBasesData();
const [selectedIndex, setSelectedIndex] = React.useState(1);
const handleListItemClick = (event, index) => {
setSelectedIndex(index);
props.setSelectedIndex(index);
};
const [open, setOpen] = React.useState(false);
const [editingItemIndex, setEditingItemIndex] = React.useState(-1);
const [editedItem, setEditedItem] = React.useState(null);
const handleClickOpen = (index) => {
setEditingItemIndex(index);
setEditedItem(basesData[index]);
setOpen(true);
};
React.useEffect(() => {
if (basesData.length === 0) {
setSelectedIndex(null);
}
}, [basesData]);
const handleClose = () => {
setOpen(false);
};
const handleSave = () => {
const errorMessage = validate(editedItem.name, editedItem.Srvr, editedItem.Ref, editingItemIndex);
if (errorMessage) {
alert(errorMessage);
return;
}
const newBasesData = [...basesData];
newBasesData[editingItemIndex] = editedItem;
setBasesData(newBasesData);
setOpen(false);
};
const validate = (name, Srvr, Ref, currentIndex) => {
if (!name.trim() || !Srvr.trim() || !Ref.trim()) {
return 'Все поля должны быть заполнены';
}
if (!isUnique(name, Srvr, Ref, currentIndex)) {
return 'База с таким наименованием, или парой "Сервер-База на сервере" уже есть';
}
return null;
};
const isUnique = (name, Srvr, Ref, currentIndex = -1) => {
let uniqueName = true;
let uniqueSrvrRef = true;
const lowerName = name.toLowerCase();
const lowerSrvr = Srvr.toLowerCase();
const lowerRef = Ref.toLowerCase();
basesData.forEach((base, index) => {
if (index === currentIndex) {
return;
}
if (base.name.toLowerCase() === lowerName) {
uniqueName = false;
}
if (base.Srvr.toLowerCase() === lowerSrvr && base.Ref.toLowerCase() === lowerRef) {
uniqueSrvrRef = false;
}
});
return uniqueName && uniqueSrvrRef;
};
const [addingNewItem, setAddingNewItem] = React.useState(false);
const handleAddNewItem = () => {
setEditingItemIndex(-1);
setEditedItem({ name: "", Srvr: "", Ref: "", active: false });
setAddingNewItem(true);
};
const handleChange = (event, field) => {
setEditedItem({ ...editedItem, [field]: event.target.value });
};
const handleAddClose = () => {
setAddingNewItem(false);
};
const handleAddSave = () => {
const errorMessage = validate(editedItem.name, editedItem.Srvr, editedItem.Ref);
if (errorMessage) {
alert(errorMessage);
return;
}
const newBase = {
name: editedItem.name,
Srvr: editedItem.Srvr,
Ref: editedItem.Ref,
active: false,
publications: [
{
name: editedItem.Ref,
title: editedItem.name,
enable: true,
activated: false,
ws: {
publishExtensionsByDefault: true
},
httpServices: {
publishExtensionsByDefault: true,
publishByDefault: true
}
}
]
};
setBasesData([...basesData, newBase]);
setAddingNewItem(false);
};
const [drawerOpen, setDrawerOpen] = React.useState(false);
const toggleDrawer = (open) => (event) => {
if (
event.type === 'keydown' &&
(event.key === 'Tab' || event.key === 'Shift')
) {
return;
}
setDrawerOpen(open);
};
const drawerList = (
<List
sx={{
width: '100%',
height: '100%',
maxWidth: 360,
bgcolor: 'background.paper',
paddingTop: '64px',
}}
subheader={<ListSubheader>Список баз</ListSubheader>}
>
{basesData.map((value, index) => {
const labelId = `checkbox-list-label-${value.name}`;
return (
<ListItem
key={value.name}
secondaryAction={
<IconButton
edge="end"
aria-label="edit"
onClick={() => handleClickOpen(index)}
>
<EditIcon />
</IconButton>
}
disablePadding
>
<ListItemButton
selected={selectedIndex === index}
onClick={(event) =>
handleListItemClick(event, index)
}
>
<ListItemText id={labelId} primary={value.name} />
</ListItemButton>
</ListItem>
);
})}
{basesData.length === 0 && (
<ListItem>
<ListItemText primary="Нет доступных баз данных" />
</ListItem>
)}
<Fab
color="primary"
aria-label="add"
onClick={handleAddNewItem}
sx={{
position: 'absolute',
bottom: 16,
right: 16,
}}
>
<AddIcon />
</Fab>
</List>
);
const container = window !== undefined ? () => window().document.body : undefined;
const handleDeleteClick = () => {
setOpenDeleteConfirmation(true);
}
const [openDeleteConfirmation, setOpenDeleteConfirmation] = React.useState(false);
const handleDeleteConfirmationClose = (deleteConfirmed) => {
if (deleteConfirmed) {
const newBasesData = [...basesData];
newBasesData.splice(editingItemIndex, 1);
setBasesData(newBasesData);
setOpenDeleteConfirmation(false);
handleClose(); // Закрыть диалоговое окно после успешного удаления
} else {
setOpenDeleteConfirmation(false);
}
}
return (
<div>
<CssBaseline />
<Box component="main" sx={{ flexGrow: 1, paddingTop: "64px" }}>
<Drawer
container={container}
variant="temporary"
open={mobileOpen}
onClose={handleDrawerToggle}
ModalProps={{
keepMounted: true, // Better open performance on mobile.
}}
sx={{
display: { xs: "block", sm: "none" },
width: drawerWidth,
flexShrink: 0,
[`& .MuiDrawer-paper`]: { width: drawerWidth, boxSizing: 'border-box' },
}}
>
{drawerList}
</Drawer>
<Drawer
variant="permanent"
sx={{
display: { xs: "none", sm: "block" },
width: drawerWidth,
flexShrink: 0,
[`& .MuiDrawer-paper`]: { width: drawerWidth, boxSizing: 'border-box' },
}}
open
>
{drawerList}
</Drawer>
<Box component="section" sx={{ width: "100%", marginLeft: { sm: `${drawerWidth}px` }, padding: "16px", }}>
<Dialog open={open} onClose={handleClose}>
<DialogTitle>EditItem</DialogTitle>
<DialogContent>
<DialogContentText>
Внесите изменения в информацию о базе данных
</DialogContentText>
{editedItem && (
<>
<TextField
autoFocus
margin="dense"
label="Название базы"
type="text"
fullWidth
variant="outlined"
value={editedItem.name}
onChange={(e) => handleChange(e, 'name')}
/>
<TextField
margin="dense"
label="Сервер"
type="text"
fullWidth
variant="outlined"
value={editedItem.Srvr}
onChange={(e) => handleChange(e, 'Srvr')}
/>
<TextField
margin="dense"
label="Имя базы на сервере"
type="text"
fullWidth
variant="outlined"
value={editedItem.Ref}
onChange={(e) => handleChange(e, 'Ref')}
/>
</>
)}
</DialogContent>
<DialogActions>
<Button onClick={handleDeleteClick}>Удалить базу</Button>
<Button onClick={handleClose}>Отменить</Button>
<Button onClick={handleSave}>Сохранить</Button>
</DialogActions>
</Dialog>
<Dialog open={addingNewItem} onClose={handleAddClose}>
<DialogTitle>Add New Base</DialogTitle>
<DialogContent>
<DialogContentText>
Заполните информацию о новой базе
</DialogContentText>
{editedItem && (
<>
<TextField
autoFocus
margin="dense"
label="Название базы"
type="text"
fullWidth
variant="outlined"
value={editedItem.name}
onChange={(e) => handleChange(e, 'name')}
/>
<TextField
margin="dense"
label="Сервер"
type="text"
fullWidth
variant="outlined"
value={editedItem.Srvr}
onChange={(e) => handleChange(e, 'Srvr')}
/>
<TextField
margin="dense"
label="Имя базы на сервере"
type="text"
fullWidth
variant="outlined"
value={editedItem.Ref}
onChange={(e) => handleChange(e, 'Ref')}
/>
</>
)}
</DialogContent>
<DialogActions>
<Button onClick={handleAddClose}>Отменить</Button>
<Button onClick={handleAddSave}>Сохранить</Button>
</DialogActions>
</Dialog>
<Dialog open={openDeleteConfirmation} onClose={() => handleDeleteConfirmationClose(false)}>
<DialogTitle>Delete Base</DialogTitle>
<DialogContent>
<DialogContentText>
Уверены, что хотите удалить базу?
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => handleDeleteConfirmationClose(false)}>Отмена</Button>
<Button onClick={() => handleDeleteConfirmationClose(true)}>Удалить базу</Button>
</DialogActions>
</Dialog>
</Box>
</Box>
</div >
);
}

View File

@ -0,0 +1,39 @@
import React from 'react';
import Bases from './Bases';
import Publications from './Publications';
import ExportJson from './ExportJson';
import { useBasesData } from './DataContext';
import styles from './css/Content.module.css';
import SelectedIndexContext from './SelectedIndexContext';
function Content(props) {
const { jsonData, basesData } = useBasesData();
const { handleDrawerToggle, mobileOpen } = props;
const [selectedIndex, setSelectedIndex] = React.useState(0);
if (!basesData) {
return <div>Загружается...</div>;
}
return (
<div>
<div className={styles.container}>
<Bases handleDrawerToggle={handleDrawerToggle} mobileOpen={mobileOpen} setSelectedIndex={setSelectedIndex} />
<div className={styles.innerContainer}>
<SelectedIndexContext.Provider value={selectedIndex}>
<div className={styles.publicationsContainer}>
<Publications />
</div>
</SelectedIndexContext.Provider>
<div className={styles.exportJsonContainer}>
<div className={styles.exportJsonButton}>
{/* <ExportJson data={jsonData} /> */}
</div>
</div>
</div>
</div>
</div>
);
}
export default Content;

View File

@ -0,0 +1,53 @@
import { createContext, useContext, useState, useEffect } from 'react';
const DataContext = createContext();
export const useBasesData = () => {
return useContext(DataContext);
};
export const DataProvider = ({ children }) => {
const [jsonData, setJsonData] = useState({ bases: [] });
const [basesData, setBasesData] = useState([]);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('https://1cdevelopers.ru/api/v1/getconfig.json');
const strResponce = await response.text();
const data = JSON.parse(strResponce);
setJsonData(data);
setBasesData(data.bases);
} catch (error) {
console.error('Error fetching data:', error);
}
};
fetchData();
}, []);
useEffect(() => {
setJsonData((prevJsonData) => ({
...prevJsonData,
bases: basesData,
}));
}, [basesData]);
const refreshData = async () => {
try {
const response = await fetch('https://1cdevelopers.ru/api/v1/getconfig.json');
const strResponce = await response.text();
const data = JSON.parse(strResponce);
setJsonData(data);
setBasesData(data.bases);
} catch (error) {
console.error('Error fetching data:', error);
}
};
return (
<DataContext.Provider value={{ basesData, setBasesData, jsonData, refreshData }}>
{children}
</DataContext.Provider>
);
};

View File

@ -0,0 +1,128 @@
// EditPublicationForm.js
import React, { useState, useEffect } from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
DialogContentText,
Button,
Tab,
Tabs,
TextField,
FormControlLabel,
Switch,
Grid,
} from '@mui/material';
import BasicParametersTab from './ediPublicationFormTabs/BasicParametersTab';
import HttpServicesTab from './ediPublicationFormTabs/HttpServicesTab';
import WebServicesTab from './ediPublicationFormTabs/WebServicesTab';
import OpenIDConnectSettingsTab from './ediPublicationFormTabs/OpenIDConnectSettingsTab';
function EditPublicationForm({ open, handleClose, publicationIndex, publication, handleUpdatePublication, handleDeletePublication}) {
const [selectedTab, setSelectedTab] = React.useState(0);
const [editedPublication, setEditedPublication] = useState({ ...publication });
const [publications, setPublications] = useState([]);
useEffect(() => {
setEditedPublication({ ...publication });
}, [publication]);
const handleSave = () => {
handleUpdatePublication(editedPublication);
handleClose();
};
const handleChange = (event, newValue) => {
setSelectedTab(newValue);
};
const handleInputChange = (event) => {
setEditedPublication({
...editedPublication,
[event.target.name]: event.target.value,
});
};
const handleSwitchChange = (event) => {
setEditedPublication({
...editedPublication,
[event.target.name]: event.target.checked,
});
};
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
const onDeletePublication = () => {
// вызовите переданную функцию handleDeletePublication с id текущей публикации
handleDeletePublication(publication);
// Закройте диалог подтверждения и основной диалог
setConfirmationDialogOpen(false);
handleClose();
};
const handleConfirmationDialogOpen = () => {
setConfirmationDialogOpen(true);
};
const handleConfirmationDialogClose = () => {
setConfirmationDialogOpen(false);
};
return (
<>
<Dialog open={open} onClose={handleClose} maxWidth="md" fullWidth>
<DialogTitle>Изменить публикацию</DialogTitle>
<DialogContent>
<Tabs
value={selectedTab}
onChange={handleChange}
indicatorColor="primary"
textColor="primary"
variant="fullWidth"
>
<Tab label="Основные параметры" />
<Tab label="HTTP-сервисы" />
<Tab label="Web-сервисы" />
<Tab label="OpenID Connect Settings" />
</Tabs>
{selectedTab === 0 && (<BasicParametersTab editedPublication={editedPublication} handleInputChange={handleInputChange} handleSwitchChange={handleSwitchChange} />)}
{selectedTab === 1 && <HttpServicesTab editedPublication={editedPublication} setEditedPublication={setEditedPublication} />}
{selectedTab === 2 && <WebServicesTab editedPublication={editedPublication} setEditedPublication={setEditedPublication} />}
{selectedTab === 3 && <OpenIDConnectSettingsTab editedPublication={editedPublication} setEditedPublication={setEditedPublication} />}
</DialogContent>
<DialogActions>
<Button onClick={handleConfirmationDialogOpen} color="secondary">
Удалить публикацию
</Button>
<Button onClick={handleClose} color="primary">
Отменить
</Button>
<Button onClick={handleSave} color="primary">
Сохранить
</Button>
</DialogActions>
</Dialog>
<Dialog open={confirmationDialogOpen}onClose={handleConfirmationDialogClose}>
<DialogTitle>Удалить публикацию</DialogTitle>
<DialogContent>
<DialogContentText>
Вы уверены, что хотите удалить эту публикацию? Это действие нельзя будет отменить.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleConfirmationDialogClose} color="primary">
Отменить
</Button>
<Button onClick={onDeletePublication} color="secondary">
Удалить
</Button>
</DialogActions>
</Dialog>
</>
);
}
export default EditPublicationForm;

View File

@ -0,0 +1,20 @@
import React from 'react';
import Button from '@mui/material/Button';
export default function ExportButton({ data }) {
const exportToJson = () => {
const dataStr = JSON.stringify(data, null, 2);
const dataUri =
'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr);
const link = document.createElement('a');
link.href = dataUri;
link.download = 'bases_data.json';
link.click();
};
return (
<Button onClick={exportToJson} variant="contained" color="primary">
Export to JSON
</Button>
);
}

View File

@ -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 = '0.8.2';
const projectDescription = 'Проект преднозначен для удобной работы с публикациями на Веб-сервере. Код полностью открыт и доступен! \n Присоединяйтесь в катанл в телеграм, чтобы не пропутсить критичные обновления.';
const telegramUrl = 'https://t.me/DevOps_onec';
const githubUrl = 'https://github.com/Segate-ekb/publicator';
return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle>Информация о проекте</DialogTitle>
<DialogContent>
<Box display="flex" alignItems="center" justifyContent="center" mb={2}>
<img src="/logo.png" alt="Logo" style={{ maxHeight: '80px', marginRight: '8px' }} />
<Typography variant="h5">{projectName}</Typography>
</Box>
<Typography variant="subtitle1">Версия: {projectVersion}</Typography>
<Typography variant="body1" mt={2}>{projectDescription}</Typography>
<Box mt={2}>
<Typography variant="subtitle2">Ссылки:</Typography>
<Box display="flex" alignItems="center">
<Link href={telegramUrl} target="_blank" rel="noopener noreferrer" sx={{ marginRight: 2 }}>
<TelegramIcon color="primary" fontSize="large" />
</Link>
<Link href={githubUrl} target="_blank" rel="noopener noreferrer">
<GitHubIcon color="action" fontSize="large" />
</Link>
</Box>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>Ок</Button>
</DialogActions>
</Dialog>
);
}
export default ProjectInfoForm;

View File

@ -0,0 +1,245 @@
// Publications.js
import React, { useContext, useState } from 'react';
import {
Card,
CardActions,
CardContent,
Typography,
Switch,
IconButton,
Link,
Grid,
Breadcrumbs,
Fab
} from '@mui/material';
import { useBasesData } from './DataContext';
import SelectedIndexContext from './SelectedIndexContext';
import styles from './css/Publications.module.css';
import EditIcon from '@mui/icons-material/Edit';
import NavigateNextIcon from '@mui/icons-material/NavigateNext';
import AddPublicationForm from './AddPublicationForm';
import EditPublicationForm from './EditPublicationForm'
import AddIcon from '@mui/icons-material/Add';
function Publications() {
const { basesData, setBasesData } = useBasesData();
const selectedIndex = useContext(SelectedIndexContext);
const selectedBase = basesData[selectedIndex];
if (!selectedBase) {
return <div>Загружается...</div>;
}
const publications = selectedBase.publications;
const handleBreadcrumbClick = () => {
// handle click on breadcrumb
}
if (!basesData || !basesData[selectedIndex]) {
return <div>Не выбрана активная база!</div>;
}
const handleToggle = (index) => () => {
const updatedPublications = publications.map((publication, i) => {
if (i === index) {
return { ...publication, active: !publication.active };
}
return publication;
});
const newChecked = updatedPublications
.filter((publication) => publication.active)
.reduce((acc, curr) => acc || curr.active, false);
setBasesData((prevBasesData) => {
const updatedBasesData = prevBasesData.map((base, i) =>
i === selectedIndex
? { ...base, publications: updatedPublications }
: base
);
return updatedBasesData;
});
};
const [openAddForm, setOpenAddForm] = useState(false);
const [openEditForm, setOpenEditForm] = useState(false);
const [selectedPublicationIndex, setSelectedPublicationIndex] = useState(null);
const handleOpenAddForm = () => {
setOpenAddForm(true);
};
const handleCloseAddForm = () => {
setOpenAddForm(false);
};
const handleAddPublication = ({ name, title }, closeForm )=> {
if (!isUnique(name, title)) {
alert("Name и Title должны быть уникальными в пределах базы данных.");
return false;
}
const newPublication = {
name,
title,
actived: true,
enable: true,
ws: {
publishExtensionsByDefault: true,
wsList: [],
},
httpServices: {
publishExtensionsByDefault: true,
publishByDefault: true,
hsList: [],
},
};
setBasesData((prevBasesData) => {
const newPublications = [...prevBasesData[selectedIndex].publications, newPublication];
const newBasesData = prevBasesData.map((base, index) =>
index === selectedIndex ? { ...base, publications: newPublications } : base
);
return newBasesData;
});
closeForm();
return true;
};
const handleUpdatePublication = (index, updatedPublication, closeForm) => {
if (!isUnique(updatedPublication.name, updatedPublication.title, index)) {
alert("Name и Title должны быть уникальными в пределах базы данных.");
return false;
}
setBasesData((prevBasesData) => {
const newPublications = prevBasesData[selectedIndex].publications.map((publication, i) =>
i === index ? updatedPublication : publication
);
const newBasesData = prevBasesData.map((base, i) =>
i === selectedIndex ? { ...base, publications: newPublications } : base
);
return newBasesData;
});
closeForm();
return true;
};
const handleOpenEditForm = (index) => {
setSelectedPublicationIndex(index);
setOpenEditForm(true);
};
const handleCloseEditForm = () => {
setOpenEditForm(false);
};
const handleDeletePublication = (publication_income) => {
// Удалите публикацию с заданным id из массива publications
console.log(publication_income);
publications.forEach((publication) => {
console.log (publication.name == publication_income.name);
console.log (publication);
})
const updatedPublications = publications.filter((publication) => publication.name != publication_income.name);
console.log(updatedPublications);
// Обновите состояние basesData с помощью setBasesData
setBasesData((prevBasesData) => {
const updatedBasesData = prevBasesData.map((base, i) =>
i === selectedIndex
? { ...base, publications: updatedPublications }
: base
);
return updatedBasesData;
});
};
const isUnique = (name, title, ignoreIndex) => {
return !publications.some((pub, index) => {
if (index === ignoreIndex) {
return false;
}
return pub.name === name || pub.title === title;
});
};
return (
<div className={styles.publicationsWrapper}>
<div className={styles.publicationsContainer}>
<Breadcrumbs
separator={<NavigateNextIcon fontSize="small" />}
aria-label="breadcrumb"
style={{ marginLeft: '16px' }}
>
<Typography color="textPrimary">{selectedBase.name}</Typography>
<Typography color="textPrimary">Публикации</Typography>
</Breadcrumbs>
<Typography color="textSecondary">
Srvr: {selectedBase.Srvr} | Ref: {selectedBase.Ref}
</Typography>
<Grid container spacing={3}>
{publications.map((publication, index) => (
<Grid item xs={12} sm={4} md={3} key={index}>
<Card key={index} className={`${styles.publicationCard} ${styles.fixedSizeCard}`}>
<CardContent>
<Typography variant="h5">{publication.title}</Typography>
<Typography
color="text.secondary"
display="block"
variant="caption"
>{publication.name}</Typography>
</CardContent>
<CardActions style={{ display: 'flex', justifyContent: 'space-between' }}>
<Switch
checked={publication.active}
onChange={handleToggle(index)}
inputProps={{ 'aria-label': 'Toggle publication' }}
/>
<IconButton onClick={() => handleOpenEditForm(index)} color="primary" style={{ marginLeft: '8px' }}>
<EditIcon />
</IconButton>
</CardActions>
</Card>
</Grid>
))}
</Grid>
<Fab
color="primary"
onClick={handleOpenAddForm}
className={styles.addPublicationButton}
>
<AddIcon />
</Fab>
<AddPublicationForm
open={openAddForm}
handleClose={handleCloseAddForm}
handleAddPublication={({ name, title }) => handleAddPublication({ name, title }, handleCloseAddForm)}
/>
<EditPublicationForm
open={openEditForm}
handleClose={handleCloseEditForm}
publicationIndex={selectedPublicationIndex}
publication={publications[selectedPublicationIndex]}
handleDeletePublication={handleDeletePublication}
handleUpdatePublication={(updatedPublication) =>
handleUpdatePublication(selectedPublicationIndex, updatedPublication, handleCloseEditForm)
}
/>
</div>
</div>
);
}
export default Publications;

View File

@ -0,0 +1,5 @@
import { createContext } from 'react';
const SelectedIndexContext = createContext(null);
export default SelectedIndexContext;

View File

@ -0,0 +1,29 @@
.container {
display: flex;
height: 100%;
padding-top: 64px;
align-items: left;
}
.innerContainer {
flex: 1;
overflow: auto;
flex-direction: column;
}
.publicationsContainer {
flex: 1;
overflow: auto;
}
.exportJsonContainer {
flex: 1;
display: flex;
overflow: auto;
}
.exportJsonButton {
margin-top: auto;
}

View File

@ -0,0 +1,20 @@
.publicationsWrapper {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.publicationsContainer {
flex-grow: 1;
margin-bottom: 40px; /* или другое значение в зависимости от размера кнопки */
}
.addPublicationButton {
margin-bottom: -80px;
align-self: flex-end;
}
.publicationCard {
height: 200px;
width: 100%;
}

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

View File

@ -0,0 +1,89 @@
// AddHsItemDialog.js
import React, { useState } from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
TextField,
Grid,
} from '@mui/material';
function AddHsItemDialog({ open, handleClose, handleAddHsItem }) {
const [name, setName] = useState('');
const [rootUrl, setRootUrl] = useState('');
const handleSave = () => {
handleAddHsItem({
name,
rootUrl,
enable: true,
reuseSessions: 'dontuse',
sessionMaxAge: 20,
poolSize: 10,
poolTimeout: 5,
});
setName('');
setRootUrl('');
handleClose();
};
const handleChange = (event, setter) => {
setter(event.target.value);
};
const isValidName = () => {
const regex = /^[a-zA-Z0-9-_]+$/;
return regex.test(name);
};
const isValidRootUrl = () => {
const regex = /^[a-zA-Z0-9-_.:/]+$/;
return regex.test(rootUrl);
};
const isFormValid = () => {
return isValidName() && isValidRootUrl();
};
return (
<Dialog open={open} onClose={handleClose} maxWidth="xs" fullWidth>
<DialogTitle>Добавить новый элемент</DialogTitle>
<DialogContent>
<Grid container spacing={2}>
<Grid item xs={12}>
<TextField
fullWidth
label="Name"
value={name}
onChange={(event) => handleChange(event, setName)}
error={!isValidName() && name !== ''}
helperText={!isValidName() && name !== '' && 'Только латинские символы, цифры, дефисы и подчеркивания.'}
/>
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
label="Root URL"
value={rootUrl}
onChange={(event) => handleChange(event, setRootUrl)}
error={!isValidRootUrl() && rootUrl !== ''}
helperText={!isValidRootUrl() && rootUrl !== '' && 'Введите корректный URL.'}
/>
</Grid>
</Grid>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
Отменить
</Button>
<Button onClick={handleSave} color="primary" disabled={!isFormValid()}>
Сохранить
</Button>
</DialogActions>
</Dialog>
);
}
export default AddHsItemDialog;

View File

@ -0,0 +1,89 @@
// AddWsItemDialog.js
import React, { useState } from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
TextField,
Grid,
} from '@mui/material';
function AddWsItemDialog({ open, handleClose, handleAddWsItem }) {
const [name, setName] = useState('');
const [alias, setAlias] = useState('');
const handleSave = () => {
handleAddWsItem({
name,
alias,
enable: true,
reuseSessions: 'dontuse',
sessionMaxAge: 20,
poolSize: 10,
poolTimeout: 5,
});
setName('');
setAlias('');
handleClose();
};
const handleChange = (event, setter) => {
setter(event.target.value);
};
const isValidName = () => {
const regex = /^[a-zA-Z0-9-_]+$/;
return regex.test(name);
};
const isValidAlias = () => {
const regex = /^[a-zA-Z0-9-_.:/]+.1cws$/;
return regex.test(alias);
};
const isFormValid = () => {
return isValidName() && isValidAlias();
};
return (
<Dialog open={open} onClose={handleClose} maxWidth="xs" fullWidth>
<DialogTitle>Добавить новый элемент</DialogTitle>
<DialogContent>
<Grid container spacing={2}>
<Grid item xs={12}>
<TextField
fullWidth
label="Name"
value={name}
onChange={(event) => handleChange(event, setName)}
error={!isValidName() && name !== ''}
helperText={!isValidName() && name !== '' && 'Только латинские символы, цифры, дефисы и подчеркивания.'}
/>
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
label="Алиас"
value={alias}
onChange={(event) => handleChange(event, setAlias)}
error={!isValidAlias() && alias !== ''}
helperText={!isValidAlias() && alias !== '' && 'Введите корректный URL.'}
/>
</Grid>
</Grid>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
Отменить
</Button>
<Button onClick={handleSave} color="primary" disabled={!isFormValid()}>
Сохранить
</Button>
</DialogActions>
</Dialog>
);
}
export default AddWsItemDialog;

View File

@ -0,0 +1,99 @@
import React from 'react';
import {
TextField,
FormControlLabel,
Switch,
Grid,
} from '@mui/material';
import styles from './EditPublicationForm.module.css';
function BasicParametersTab(props) {
return (
<div>
<Grid container spacing={3} className={styles.grid}>
<Grid item xs={8}>
<TextField
label="Наименование подключения"
name="title"
value={props.editedPublication.title}
onChange={props.handleInputChange}
fullWidth
margin="normal"
/>
</Grid>
<Grid item xs={4}>
<FormControlLabel
control={
<Switch
checked={props.editedPublication.active}
onChange={props.handleSwitchChange}
name="active"
color="primary"
/>
}
label="Активно ли подключение"
/>
</Grid>
<Grid item xs={12}>
<TextField
label="location"
name="name"
value={props.editedPublication.name}
onChange={props.handleInputChange}
fullWidth
margin="normal"
/>
</Grid>
<Grid item xs={6}>
<TextField
label="Имя пользователя"
name="usr"
value={props.editedPublication.usr}
onChange={props.handleInputChange}
fullWidth
margin="normal"
/>
</Grid>
<Grid item xs={6}>
<TextField
label="Пароль"
name="pwd"
type="password"
value={props.editedPublication.pwd}
onChange={props.handleInputChange}
fullWidth
margin="normal"
/>
</Grid>
<Grid item xs={12}>
<FormControlLabel
control={
<Switch
checked={props.editedPublication.enable}
onChange={props.handleSwitchChange}
name="enable"
color="primary"
/>
}
label="Разрешен ли вход пользователей"
/>
</Grid>
<Grid item xs={12}>
<FormControlLabel
control={
<Switch
checked={props.editedPublication.enableStandardOData}
onChange={props.handleSwitchChange}
name="enableStandardOData"
color="primary"
/>
}
label="Разрешить OData"
/>
</Grid>
</Grid>
</div>
);
}
export default BasicParametersTab;

View File

@ -0,0 +1,5 @@
.grid {
display: flex;
justify-content: center;
align-items: center;
}

View File

@ -0,0 +1,293 @@
// HttpServicesTab.js
import React, { useState } from 'react';
import {
Accordion,
AccordionSummary,
AccordionDetails,
Typography,
FormControlLabel,
Switch,
Box,
TextField,
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
DialogActions,
Button,
Grid
} from '@mui/material';
import { ExpandMore } from '@mui/icons-material';
import AddItemDialog from './AddHsItemDialog';
import Fab from '@mui/material/Fab';
import AddIcon from '@mui/icons-material/Add';
import IconButton from '@mui/material/IconButton';
import DeleteIcon from '@mui/icons-material/Delete';
import Divider from '@mui/material/Divider';
function HttpServicesTab({ editedPublication = {
httpServices: {
publishExtensionsByDefault: false,
publishByDefault: false,
hsList: []
}
}, setEditedPublication }) {
const [addDialogOpen, setAddDialogOpen] = useState(false);
const handleSwitchChange = (event) => {
setEditedPublication({
...editedPublication,
httpServices: {
...editedPublication.httpServices,
[event.target.name]: event.target.checked,
},
});
};
const handleAccordionChange = (index, event) => {
const newHsList = editedPublication.httpServices.hsList.map((item, idx) => {
if (idx === index) {
return { ...item, [event.target.name]: event.target.value };
}
return item;
});
setEditedPublication({
...editedPublication,
httpServices: { ...editedPublication.httpServices, hsList: newHsList },
});
};
const handleAddDialogOpen = () => {
setAddDialogOpen(true);
};
const handleAddDialogClose = () => {
setAddDialogOpen(false);
};
const handleAddHsItem = (newHsItem) => {
setEditedPublication((prevPublication) => {
const prevHsList = prevPublication.httpServices?.hsList ?? [];
return {
...prevPublication,
httpServices: {
...prevPublication.httpServices,
hsList: [...prevHsList, newHsItem],
},
};
});
};
const handleDeleteHsItem = (index) => {
const newHsList = editedPublication.httpServices.hsList.filter(
(_, idx) => idx !== index
);
setEditedPublication({
...editedPublication,
httpServices: { ...editedPublication.httpServices, hsList: newHsList },
});
};
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [itemToDelete, setItemToDelete] = useState(null);
const handleDeleteDialogOpen = (index) => {
setItemToDelete(index);
setDeleteDialogOpen(true);
};
const handleDeleteDialogClose = () => {
setItemToDelete(null);
setDeleteDialogOpen(false);
};
const handleConfirmDelete = () => {
handleDeleteHsItem(itemToDelete);
handleDeleteDialogClose();
};
return (
<Box>
<FormControlLabel
control={
<Switch
checked={editedPublication.httpServices.publishExtensionsByDefault}
onChange={handleSwitchChange}
name="publishExtensionsByDefault"
/>
}
label="Разрешить публиковать http-сервисы расширений по умолчанию"
/>
<FormControlLabel
control={
<Switch
checked={editedPublication.httpServices.publishByDefault}
onChange={handleSwitchChange}
name="publishByDefault"
/>
}
label="Разрешить публиковать http-сервисы основной конфигурации по умолчанию"
/>
<Divider textAlign="left">Http-Сервисы</Divider>
{(editedPublication.httpServices?.hsList ?? []).map((hsItem, index) => (
<Accordion key={index}>
<AccordionSummary expandIcon={<ExpandMore />}>
<Typography variant="h4">{hsItem.name}</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={2}>
<Grid item xs={2} sm={4}>
<TextField
label="Наименование"
value={hsItem.name}
name="name"
onChange={(event) => handleAccordionChange(index, event)}
required
/>
</Grid>
<Grid item xs={2} sm={4}>
<TextField
label="Root URL"
value={hsItem.rootUrl}
name="rootUrl"
onChange={(event) => handleAccordionChange(index, event)}
required
/>
</Grid>
<Grid item xs={2} sm={4}>
<FormControlLabel
control={
<Switch
checked={hsItem.enable}
onChange={() =>
handleAccordionChange(index, { target: { name: 'enable', value: !hsItem.enable } })
}
name="enable"
/>
}
label="Использование"
/>
</Grid>
<Grid item xs={2} sm={3}>
<TextField
select
label="Reuse Sessions"
value={hsItem.reuseSessions}
onChange={(event) => handleAccordionChange(index, event)}
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={2} sm={3}>
<TextField
label="Session Max Age"
value={hsItem.sessionMaxAge}
name="sessionMaxAge"
onChange={(event) => handleAccordionChange(index, event)}
type="number"
/>
</Grid>
<Grid item xs={2} sm={3}>
<TextField
label="Pool Size"
value={hsItem.poolSize}
name="poolSize"
onChange={(event) => handleAccordionChange(index, event)}
type="number"
/>
</Grid>
<Grid item xs={2} sm={3}>
<TextField
label="Pool Timeout"
value={hsItem.poolTimeout}
name="poolTimeout"
onChange={(event) => handleAccordionChange(index, event)}
type="number"
/>
</Grid>
<Grid item xs={12} textAlign="right">
<IconButton
edge="end"
color="inherit"
onClick={() => handleDeleteDialogOpen(index)}
>
<DeleteIcon />
</IconButton>
</Grid>
</Grid>
</AccordionDetails>
</Accordion>
))}
<Fab
color="inherit"
aria-label="add"
size="small"
onClick={handleAddDialogOpen}
sx={{
position: 'relative',
top: 10,
left: 800,
}}
>
<AddIcon />
</Fab>
<AddItemDialog
open={addDialogOpen}
handleClose={handleAddDialogClose}
handleAddHsItem={handleAddHsItem}
/>
<Dialog
open={deleteDialogOpen}
onClose={handleDeleteDialogClose}
maxWidth="xs"
fullWidth
>
<DialogTitle>Подтверждение удаления</DialogTitle>
<DialogContent>
<DialogContentText>
Вы уверены, что хотите удалить данный элемент?
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleDeleteDialogClose} color="primary">
Отменить
</Button>
<Button onClick={handleConfirmDelete} color="primary" autoFocus>
Удалить
</Button>
</DialogActions>
</Dialog>
</Box>
);
}
export default HttpServicesTab;

View File

@ -0,0 +1,91 @@
import React, { useState } from 'react';
import { Grid, Typography, Accordion, AccordionDetails, AccordionSummary, Tabs, Tab } from '@mui/material';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import GeneralSettingsTab from './openid_parts/GeneralSettingsTab';
import ProviderSettingsTab from './openid_parts/ProviderSettingsTab';
import ClientSettingsTab from './openid_parts/ClientSettingsTab';
import Fab from '@mui/material/Fab';
import AddIcon from '@mui/icons-material/Add';
import IconButton from '@mui/material/IconButton';
import DeleteIcon from '@mui/icons-material/Delete';
import Divider from '@mui/material/Divider';
import AddProviderDialog from './openid_parts/AddProviderDialog';
function OpenIDConnectSettingsTab({ editedPublication, handleInputChange }) {
const [providers, setProviders] = useState(editedPublication?.oidc?.providers || []);
const [selectedTab, setSelectedTab] = useState(0);
const [addProviderDialogOpen, setAddProviderDialogOpen] = useState(false);
const handleChange = (event, newValue) => {
setSelectedTab(newValue);
};
const handleAddProvider = (newProvider) => {
const newProviders = [...providers, newProvider];
setProviders(newProviders);
setSelectedTab(0);
setAddProviderDialogOpen(false);
};
const handleDeleteProvider = (providerIndex) => {
const newProviders = [...providers];
newProviders.splice(providerIndex, 1);
setProviders(newProviders);
};
const handleProviderChange = (index, newProvider) => {
const newProviders = [...providers];
newProviders[index] = newProvider;
setProviders(newProviders);
};
return (
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography variant="h6" gutterBottom>
OpenID Connect Settings
</Typography>
</Grid>
{providers && providers.map((provider, index) => (
<Grid item xs={12} key={index}>
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />} aria-controls={`panel${index}-content`} id={`panel${index}-header`}>
<Typography>{provider.title}</Typography>
</AccordionSummary>
<AccordionDetails>
<Tabs value={selectedTab} onChange={handleChange}>
<Tab label="General" />
<Tab label="Provider" />
<Tab label="Client" />
</Tabs>
selectedTab === 0 && <GeneralSettingsTab provider={provider} handleInputChange={handleInputChange} handleProviderChange={(newProvider) => handleProviderChange(index, newProvider)} />}
{selectedTab === 1 && <ProviderSettingsTab provider={provider} handleInputChange={handleInputChange} handleProviderChange={(newProvider) => handleProviderChange(index, newProvider)} />}
{selectedTab === 2 && <ClientSettingsTab provider={provider} handleInputChange={handleInputChange} handleProviderChange={(newProvider) => handleProviderChange(index, newProvider)} />}
</AccordionDetails>
<Divider />
<IconButton aria-label="delete provider" onClick={() => handleDeleteProvider(index)}>
<DeleteIcon />
</IconButton>
</Accordion>
</Grid>
))}
<Grid item xs={12}>
<Fab
color="primary"
aria-label="add"
size="small"
onClick={() => setAddProviderDialogOpen(true)}
>
<AddIcon />
</Fab>
</Grid>
<AddProviderDialog
open={addProviderDialogOpen}
handleClose={() => setAddProviderDialogOpen(false)}
handleAddProvider={handleAddProvider}
/>
</Grid>
);
}
export default OpenIDConnectSettingsTab;

View File

@ -0,0 +1,275 @@
// WebServicesTab.js
import React, { useState } from 'react';
import {
Accordion,
AccordionSummary,
AccordionDetails,
Typography,
FormControlLabel,
Switch,
Box,
TextField,
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
DialogActions,
Button,
Grid
} from '@mui/material';
import { ExpandMore } from '@mui/icons-material';
import AddWsItemDialog from './AddWsItemDialog';
import Fab from '@mui/material/Fab';
import AddIcon from '@mui/icons-material/Add';
import IconButton from '@mui/material/IconButton';
import DeleteIcon from '@mui/icons-material/Delete';
import Divider from '@mui/material/Divider';
function WebServicesTab({ editedPublication, setEditedPublication }) {
const [addDialogOpen, setAddDialogOpen] = useState(false);
const handleSwitchChange = (event) => {
setEditedPublication({
...editedPublication,
ws: {
...editedPublication.ws,
[event.target.name]: event.target.checked,
},
});
};
const handleAccordionChange = (index, event) => {
const newWsList = editedPublication.ws.wsList.map((item, idx) => {
if (idx === index) {
return { ...item, [event.target.name]: event.target.value };
}
return item;
});
setEditedPublication({
...editedPublication,
ws: { ...editedPublication.ws, wsList: newWsList },
});
};
const handleAddDialogOpen = () => {
setAddDialogOpen(true);
};
const handleAddDialogClose = () => {
setAddDialogOpen(false);
};
const handleAddWsItem = (newWsItem) => {
setEditedPublication((prevPublication) => {
const prevWsList = prevPublication.ws?.wsList ?? [];
return {
...prevPublication,
ws: {
...prevPublication.ws,
wsList: [...prevWsList, newWsItem],
},
};
});
};
const handleDeleteWsItem = (index) => {
const newWsList = editedPublication.ws.wsList.filter(
(_, idx) => idx !== index
);
setEditedPublication({
...editedPublication,
ws: { ...editedPublication.ws, wsList: newWsList },
});
};
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [itemToDelete, setItemToDelete] = useState(null);
const handleDeleteDialogOpen = (index) => {
setItemToDelete(index);
setDeleteDialogOpen(true);
};
const handleDeleteDialogClose = () => {
setItemToDelete(null);
setDeleteDialogOpen(false);
};
const handleConfirmDelete = () => {
handleDeleteWsItem(itemToDelete);
handleDeleteDialogClose();
};
return (
<Box>
<FormControlLabel
control={
<Switch
checked={editedPublication.ws.publishExtensionsByDefault}
onChange={handleSwitchChange}
name="publishExtensionsByDefault"
/>
}
label="Разрешить публиковать Web-сервисы расширений по умолчанию"
/>
<Divider textAlign="left">Web-Сервисы</Divider>
{(editedPublication.ws?.wsList ?? []).map((wsItem, index) => (
<Accordion key={index}>
<AccordionSummary expandIcon={<ExpandMore />}>
<Typography variant="h4">{wsItem.name}</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={2}>
<Grid item xs={2} sm={4}>
<TextField
label="Наименование"
value={wsItem.name}
name="name"
onChange={(event) => handleAccordionChange(index, event)}
required
/>
</Grid>
<Grid item xs={2} sm={4}>
<TextField
label="Алиас"
value={wsItem.alias}
name="alias"
onChange={(event) => handleAccordionChange(index, event)}
required
/>
</Grid>
<Grid item xs={2} sm={4}>
<FormControlLabel
control={
<Switch
checked={wsItem.enable}
onChange={() =>
handleAccordionChange(index, { target: { name: 'enable', value: !wsItem.enable } })
}
name="enable"
/>
}
label="Использование"
/>
</Grid>
<Grid item xs={2} sm={3}>
<TextField
select
label="Reuse Sessions"
value={wsItem.reuseSessions}
onChange={(event) => handleAccordionChange(index, event)}
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={2} sm={3}>
<TextField
label="Session Max Age"
value={wsItem.sessionMaxAge}
name="sessionMaxAge"
onChange={(event) => handleAccordionChange(index, event)}
type="number"
/>
</Grid>
<Grid item xs={2} sm={3}>
<TextField
label="Pool Size"
value={wsItem.poolSize}
name="poolSize"
onChange={(event) => handleAccordionChange(index, event)}
type="number"
/>
</Grid>
<Grid item xs={2} sm={3}>
<TextField
label="Pool Timeout"
value={wsItem.poolTimeout}
name="poolTimeout"
onChange={(event) => handleAccordionChange(index, event)}
type="number"
/>
</Grid>
<Grid item xs={12} textAlign="right">
<IconButton
edge="end"
color="inherit"
onClick={() => handleDeleteDialogOpen(index)}
>
<DeleteIcon />
</IconButton>
</Grid>
</Grid>
</AccordionDetails>
</Accordion>
))}
<Fab
color="inherit"
aria-label="add"
size="small"
onClick={handleAddDialogOpen}
sx={{
position: 'relative',
top: 10,
left: 800,
}}
>
<AddIcon />
</Fab>
<AddWsItemDialog
open={addDialogOpen}
handleClose={handleAddDialogClose}
handleAddWsItem={handleAddWsItem}
/>
<Dialog
open={deleteDialogOpen}
onClose={handleDeleteDialogClose}
maxWidth="xs"
fullWidth
>
<DialogTitle>Подтверждение удаления</DialogTitle>
<DialogContent>
<DialogContentText>
Вы уверены, что хотите удалить данный элемент?
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleDeleteDialogClose} color="primary">
Отменить
</Button>
<Button onClick={handleConfirmDelete} color="primary" autoFocus>
Удалить
</Button>
</DialogActions>
</Dialog>
</Box>
);
}
export default WebServicesTab;

View File

@ -0,0 +1,127 @@
import React, { useState } from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
FormControl,
InputLabel,
Select,
MenuItem,
TextField,
Box,
} from '@mui/material';
function AddProviderDialog({ open, handleClose, handleAddProvider, providers }) {
const [selectedProviderIndex, setSelectedProviderIndex] = useState(0);
const [newProvider, setNewProvider] = useState({
name: '',
title: '',
authenticationClaimName: 'email',
authenticationUserPropertyName: 'email',
icon: '',
});
const handleAdd = () => {
handleAddProvider(newProvider);
handleClose();
};
const handleProviderChange = (event) => {
setSelectedProviderIndex(event.target.value);
};
const handleChange = (event) => {
setNewProvider({
...newProvider,
[event.target.name]: event.target.value,
});
};
const handleImageUpload = (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
setNewProvider({ ...newProvider, icon: reader.result });
};
reader.readAsDataURL(file);
}
};
return (
<Dialog open={open} onClose={handleClose}>
<DialogTitle>Add Provider</DialogTitle>
<DialogContent>
<form>
<TextField
label="Name"
name="name"
value={newProvider.name}
onChange={handleChange}
required
fullWidth
margin="normal"
/>
<TextField
label="Title"
name="title"
value={newProvider.title}
onChange={handleChange}
required
fullWidth
margin="normal"
/>
<TextField
label="Authentication Claim Name"
name="authenticationClaimName"
value={newProvider.authenticationClaimName}
onChange={handleChange}
required
fullWidth
margin="normal"
/>
<FormControl fullWidth margin="normal">
<InputLabel>Authentication User Property Name</InputLabel>
<Select
name="authenticationUserPropertyName"
value={newProvider.authenticationUserPropertyName}
onChange={handleChange}
>
<MenuItem value="name">name</MenuItem>
<MenuItem value="OSUser">OSUser</MenuItem>
<MenuItem value="email">email</MenuItem>
<MenuItem value="matchingKey">matchingKey</MenuItem>
</Select>
</FormControl>
<Box mb={2}>
<input
accept="image/*"
style={{ display: 'none' }}
id="icon-upload"
type="file"
onChange={handleImageUpload}
/>
<label htmlFor="icon-upload">
<Button variant="contained" component="span">
Upload Icon
</Button>
</label>
</Box>
{newProvider.icon && (
<Box mb={2}>
<img src={newProvider.icon} alt="Provider Icon" style={{ maxHeight: '100px', maxWidth: '100px' }} />
</Box> )}
</form>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Cancel</Button>
<Button onClick={handleAdd} disabled={!newProvider.name || !newProvider.title}>
Add
</Button>
</DialogActions>
</Dialog>);
}
export default AddProviderDialog;

View File

@ -0,0 +1,47 @@
import React from 'react';
import { Grid, Typography, TextField } from '@mui/material';
function ClientSettingsTab({ provider, handleInputChange }) {
return (
<Grid container spacing={2}>
<Grid item xs={12}>
<TextField
fullWidth
name={`client_id`}
label="Client ID"
value={provider.clientconfig?.client_id || ''}
onChange={handleInputChange}
/>
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
name={`redirect_uri`}
label="Redirect URI"
value={provider.clientconfig?.redirect_uri || ''}
onChange={handleInputChange}
/>
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
name={`response_type`}
label="Response Type"
value={provider.clientconfig?.response_type || ''}
onChange={handleInputChange}
/>
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
name={`scope`}
label="Scope"
value={provider.clientconfig?.scope || ''}
onChange={handleInputChange}
/>
</Grid>
</Grid>
);
}
export default ClientSettingsTab;

View File

@ -0,0 +1,71 @@
import React, { useState, useEffect } from 'react';
import { Grid, TextField, FormControl, InputLabel, Select, MenuItem, Typography, Box } from '@mui/material';
function GeneralSettingsTab({ provider, handleInputChange, handleProviderChange }) {
const [name, setName] = useState(provider.name || '');
const [title, setTitle] = useState(provider.title || '');
const [authenticationUserPropertyName, setAuthenticationUserPropertyName] = useState(provider.authenticationUserPropertyName || 'email');
const [authenticationClaimName, setAuthenticationClaimName] = useState(provider.authenticationClaimName || 'email');
const [imagePreview, setImagePreview] = useState(provider.icon || null);
useEffect(() => {
const updatedProvider = {
...provider,
name,
title,
authenticationUserPropertyName,
authenticationClaimName,
icon: imagePreview
};
handleProviderChange(updatedProvider);
}, [name, title, authenticationUserPropertyName, authenticationClaimName, imagePreview]);
const handleImageChange = (event) => {
const file = event.target.files[0];
const reader = new FileReader();
reader.onloadend = () => {
setImagePreview(reader.result);
};
reader.readAsDataURL(file);
};
return (
<Grid container spacing={2}>
<Grid item xs={12}>
<TextField fullWidth label="Name" value={name} onChange={(e) => setName(e.target.value)} />
</Grid>
<Grid item xs={12}>
<TextField fullWidth label="Title" value={title} onChange={(e) => setTitle(e.target.value)} />
</Grid>
<Grid item xs={12}>
<FormControl fullWidth>
<InputLabel>Authentication User Property Name</InputLabel>
<Select value={authenticationUserPropertyName} onChange={(e) => setAuthenticationUserPropertyName(e.target.value)}>
<MenuItem value="name">Name</MenuItem>
<MenuItem value="OSUser">OSUser</MenuItem>
<MenuItem value="email">Email</MenuItem>
<MenuItem value="matchingKey">Matching Key</MenuItem>
</Select>
</FormControl>
</Grid>
<Grid item xs={12}>
<TextField fullWidth label="Authentication Claim Name" value={authenticationClaimName} onChange={(e) => setAuthenticationClaimName(e.target.value)} />
</Grid>
<Grid item xs={12}>
<input accept="image/*" style={{ display: 'none' }} id="icon-upload" type="file" onChange={handleImageChange} />
<label htmlFor="icon-upload">
<Typography>Upload Icon</Typography>
</label>
</Grid>
{imagePreview && (
<Grid item xs={12}>
<Box>
<img src={imagePreview} alt="Preview" style={{ width: '100px', height: 'auto' }} />
</Box>
</Grid>
)}
</Grid>
);
}
export default GeneralSettingsTab;

View File

@ -0,0 +1,65 @@
import React from 'react';
import { Grid, Typography, TextField } from '@mui/material';
function GeneralSettingsTab({ provider, handleInputChange }) {
return (
<Grid container spacing={2}>
<Grid item xs={12}>
<TextField
fullWidth
name={`issuer`}
label="Issuer"
value={provider.providerconfig?.issuer || ''}
onChange={handleInputChange}
/>
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
name={`authorization_endpoint`}
label="Authorization Endpoint"
value={provider.providerconfig?.authorization_endpoint || ''}
onChange={handleInputChange}
/>
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
name={`token_endpoint`}
label="Token Endpoint"
value={provider.providerconfig?.token_endpoint || ''}
onChange={handleInputChange}
/>
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
name={`jwks_uri`}
label="JWKS URI"
value={provider.providerconfig?.jwks_uri || ''}
onChange={handleInputChange}
/>
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
name={`userinfo_endpoint`}
label="Userinfo Endpoint"
value={provider.providerconfig?.userinfo_endpoint || ''}
onChange={handleInputChange}
/>
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
name={`scopes_supported`}
label="Scopes Supported"
value={provider.providerconfig?.scopes_supported?.join(', ') || ''}
onChange={handleInputChange}
/>
</Grid>
</Grid>
);
}
export default GeneralSettingsTab;

View File

@ -0,0 +1,17 @@
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';
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>,
);

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

View File

@ -0,0 +1,69 @@
// serverManagement.js
export const restartServer = async () => {
try {
const response = await fetch(`/api/v1/ws/restart`);
if (response.ok) {
const data = await response.json();
return { success: true, message: 'Сервер успешно перезапущен' };
} else {
return { success: false, message: `Ошибка при перезапуске сервера: ${response.statusText}` };
}
} catch (error) {
return { success: false, message: `Ошибка при перезапуске сервера: ${error.message}` };
}
};
export const stopServer = async () => {
try {
const response = await fetch(`/api/v1/ws/stop`);
if (response.ok) {
const data = await response.json();
return { success: true, message: 'Сервер успешно остановлен' };
} else {
return { success: false, message: `Ошибка при остановке сервера: ${response.statusText}` };
}
} catch (error) {
return { success: false, message: `Ошибка при остановке сервера: ${error.message}` };
}
};
export const startServer = async () => {
try {
const response = await fetch(`/api/v1/ws/start`);
if (response.ok) {
const data = await response.json();
return { success: true, message: 'Сервер успешно запущен' };
} else {
return { success: false, message: `Ошибка при запуске сервера: ${response.statusText}` };
}
} catch (error) {
return { success: false, message: `Ошибка при запуске сервера: ${error.message}` };
}
};
export const saveToServer = async (jsonData) => {
try {
const response = await fetch('/api/v1/updateconfig', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(jsonData, null, 2)
});
if (response.ok) {
const data = await response.json();
return { success: true, message: 'Уонфигурация успешно записана' };
} else {
return { success: false, message: `Ошибка при запуске сервера: ${response.statusText}` };
}
} catch (error) {
return { success: false, message: `Ошибка при запуске сервера: ${error.message}` };
}
};

View File

@ -4,7 +4,7 @@
"Порт": 3333,
"ЗадержкаПередЧтениемСокета": 65,
"КаталогиСФайлами": {
"/assets": "./view/assets"
"/": "./view/"
}
},
"Публикатор": {

View File

@ -0,0 +1,13 @@
{
"files": {
"main.css": "/static/css/main.b72ea8bc.css",
"main.js": "/static/js/main.134517c4.js",
"index.html": "/index.html",
"main.b72ea8bc.css.map": "/static/css/main.b72ea8bc.css.map",
"main.134517c4.js.map": "/static/js/main.134517c4.js.map"
},
"entrypoints": [
"static/css/main.b72ea8bc.css",
"static/js/main.134517c4.js"
]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -1,206 +1 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<title>1С Публикатор</title>
<link rel="icon" type="image/x-icon" href="/assets/dist/img/favicon.ico">
<link href="/assets/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.0/css/all.min.css">
<style>
html,
body{
height: 100%;
margin: 0;
padding: 0;
}
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
.b-example-divider {
height: 3rem;
background-color: rgba(0, 0, 0, .1);
border: solid rgba(0, 0, 0, .15);
border-width: 1px 0;
box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15);
}
.b-example-vr {
flex-shrink: 0;
width: 1.5rem;
height: 100vh;
}
.bi {
vertical-align: -.125em;
fill: currentColor;
}
.nav-scroller {
position: relative;
z-index: 2;
height: 2.75rem;
overflow-y: hidden;
}
.nav-scroller .nav {
display: flex;
flex-wrap: nowrap;
padding-bottom: 1rem;
margin-top: -1px;
overflow-x: auto;
text-align: center;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
}
#logo {
height: 40px;
}
#editor {
display: flex;
height: 100%;
width: 100%;
}
</style>
</head>
<body>
<main>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark" aria-label="Fifth navbar example">
<div class="container-fluid">
<img id="logo" src="/assets/dist/img/logo.png"
alt="1C Logo">
<a class="navbar-brand">Публикатор 1с</a>
<div class="collapse navbar-collapse" id="navbarsExample05">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" id="reload"><i class="fas fa-redo"></i> Обновить</a>
</li>
<li class="nav-item">
<a class="nav-link" id="submit"><i class="fas fa-save"></i> Сохранить конфигурацию</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" data-bs-toggle="dropdown"
aria-expanded="false">Управление web-сервером</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" id="restart"><i class="fas fa-sync-alt"></i>
Перезапустить</a></li>
<li><a class="dropdown-item" id="start"><i class="fas fa-play"></i> Запустить</a></li>
<li><a class="dropdown-item" id="stop"> <i class="fas fa-stop"></i> Остановить</a></li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
</main>
<div id="editor"></div>
<script src="/assets/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://unpkg.com/monaco-editor@0.28.0/min/vs/loader.js"></script>
<script>
require.config({
paths: {
'vs': 'https://unpkg.com/monaco-editor@0.28.0/min/vs'
}
});
require(['vs/editor/editor.main'], function () {
// Initialize the Monaco Editor
const editor = monaco.editor.create(document.getElementById('editor'), {
language: 'json',
theme: 'vs-dark'
});
// Load JSON from the API
async function loadJSON() {
const response = await fetch('./api/v1/getconfig.json');
const data = await response.text();
editor.setValue(data);
}
// Post JSON to the API
async function postJSON(json) {
const response = await fetch('./api/v1/updateconfig', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: json
});
return response;
}
// Add click event listener to the submit button
document.getElementById('submit').addEventListener('click', async () => {
if (confirm('Вы уверены, что хотите сохранить конфигурацию?')) {
const json = editor.getValue();
try {
const response = await postJSON(json);
if (response.ok) {
if (confirm('Конфигурация успешно сохранена! \n Перезапустить веб-сервер?')) {
await fetch('./api/v1/ws/restart');
}
} else {
alert('Ошибка при сохранении конфигурации!');
}
} catch (error) {
console.error('Error:', error);
alert('Ошибка при сохранении конфигурации!');
}
}
});
// Add click event listener to the reload button
document.getElementById('reload').addEventListener('click', async () => {
if (confirm('Вы уверены, что хотите обновить конфигурацию? Это заменит текущую конфигурацию в редакторе.')) {
await loadJSON();
}
});
// Add click event listeners to the web server control buttons
document.getElementById('restart').addEventListener('click', async () => {
await fetch('./api/v1/ws/restart');
});
document.getElementById('start').addEventListener('click', async () => {
await fetch('./api/v1/ws/start');
});
document.getElementById('stop').addEventListener('click', async () => {
await fetch('./api/v1/ws/stop');
});
// Load JSON on startup
loadJSON();
});
</script>
</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"/><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.134517c4.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>

BIN
webserver/app/view/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@ -0,0 +1,15 @@
{
"short_name": "Your Orders",
"name": "Your Orders",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -0,0 +1,2 @@
.Publications_publicationsWrapper__JNEwF{display:flex;flex-direction:column;min-height:100vh}.Publications_publicationsContainer__WBZVM{flex-grow:1;margin-bottom:40px}.Publications_addPublicationButton__PNBAc{align-self:flex-end;margin-bottom:-80px}.Publications_publicationCard__X58tU{height:200px;width:100%}.EditPublicationForm_grid__ds-Pi{align-items:center;display:flex;justify-content:center}.Content_container__pHxpH{align-items:left;display:flex;height:100%;padding-top:64px}.Content_innerContainer__-G-LS{flex:1 1;flex-direction:column;overflow:auto}.Content_publicationsContainer__2CUys{flex:1 1;overflow:auto}.Content_exportJsonContainer__5vo0J{display:flex;flex:1 1;overflow:auto}.Content_exportJsonButton__R7pbR{margin-top:auto}
/*# sourceMappingURL=main.b72ea8bc.css.map*/

View File

@ -0,0 +1 @@
{"version":3,"file":"static/css/main.b72ea8bc.css","mappings":"AAAA,yCACE,YAAa,CACb,qBAAsB,CACtB,gBACF,CAEA,2CACE,WAAY,CACZ,kBACF,CAEA,0CAEE,mBAAoB,CADpB,mBAEF,CAEA,qCACE,YAAa,CACb,UACF,CCnBA,iCAGE,kBAAmB,CAFnB,YAAa,CACb,sBAEF,CCFA,0BAIE,gBAAiB,CAHjB,YAAa,CACb,WAAY,CACZ,gBAEF,CAGA,+BACE,QAAO,CAEP,qBAAsB,CADtB,aAEF,CACA,sCACE,QAAO,CACP,aACF,CAEA,oCAEE,YAAa,CADb,QAAO,CAEP,aACF,CAEA,iCACE,eACF","sources":["css/Publications.module.css","ediPublicationFormTabs/EditPublicationForm.module.css","css/Content.module.css"],"sourcesContent":[".publicationsWrapper {\r\n display: flex;\r\n flex-direction: column;\r\n min-height: 100vh;\r\n}\r\n\r\n.publicationsContainer {\r\n flex-grow: 1;\r\n margin-bottom: 40px; /* или другое значение в зависимости от размера кнопки */\r\n}\r\n\r\n.addPublicationButton {\r\n margin-bottom: -80px;\r\n align-self: flex-end;\r\n}\r\n\r\n.publicationCard {\r\n height: 200px;\r\n width: 100%;\r\n}",".grid {\r\n display: flex;\r\n justify-content: center;\r\n align-items: center;\r\n}","\r\n\r\n.container {\r\n display: flex;\r\n height: 100%;\r\n padding-top: 64px;\r\n align-items: left;\r\n}\r\n\r\n\r\n.innerContainer {\r\n flex: 1;\r\n overflow: auto;\r\n flex-direction: column;\r\n}\r\n.publicationsContainer {\r\n flex: 1;\r\n overflow: auto;\r\n}\r\n\r\n.exportJsonContainer {\r\n flex: 1;\r\n display: flex;\r\n overflow: auto;\r\n}\r\n\r\n.exportJsonButton {\r\n margin-top: auto;\r\n}"],"names":[],"sourceRoot":""}

File diff suppressed because one or more lines are too long

View 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.
*/

File diff suppressed because one or more lines are too long