435 lines
17 KiB
JavaScript
435 lines
17 KiB
JavaScript
/*
|
||
Парус 8 - Панели мониторинга - Редактор панелей
|
||
Общие компоненты редакторов свойств
|
||
*/
|
||
|
||
//---------------------
|
||
//Подключение библиотек
|
||
//---------------------
|
||
|
||
import React, { useState, useContext, useEffect } from "react"; //Классы React
|
||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||
import {
|
||
Box,
|
||
Stack,
|
||
IconButton,
|
||
Icon,
|
||
Typography,
|
||
Divider,
|
||
Chip,
|
||
Dialog,
|
||
DialogTitle,
|
||
DialogContent,
|
||
DialogActions,
|
||
Button,
|
||
TextField,
|
||
InputAdornment,
|
||
MenuItem,
|
||
Menu,
|
||
Card,
|
||
CardContent,
|
||
CardActions,
|
||
CardActionArea
|
||
} from "@mui/material"; //Интерфейсные элементы
|
||
import client from "../../../core/client"; //Клиент БД
|
||
import { ApplicationСtx } from "../../../context/application"; //Контекст приложения
|
||
import { BUTTONS } from "../../../../app.text"; //Общие текстовые ресурсы
|
||
import { useUserProcDesc } from "./components_hooks"; //Общие хуки компонентов
|
||
import "../panels_editor.css"; //Стили редактора
|
||
|
||
//---------
|
||
//Константы
|
||
//---------
|
||
|
||
//Стили
|
||
const STYLES = {
|
||
CHIP: (fullWidth = false, multiLine = false) => ({
|
||
...(multiLine ? { height: "auto" } : {}),
|
||
"& .MuiChip-label": {
|
||
...(multiLine
|
||
? {
|
||
display: "block",
|
||
whiteSpace: "normal"
|
||
}
|
||
: {}),
|
||
...(fullWidth ? { width: "100%" } : {})
|
||
}
|
||
})
|
||
};
|
||
|
||
//Типы даных аргументов
|
||
const ARGUMENT_DATA_TYPE = {
|
||
STR: client.SERV_DATA_TYPE_STR,
|
||
NUMB: client.SERV_DATA_TYPE_NUMB,
|
||
DATE: client.SERV_DATA_TYPE_DATE
|
||
};
|
||
|
||
//Типы источников данных
|
||
const DATA_SOURCE_TYPE = {
|
||
USER_PROC: "USER_PROC",
|
||
QUERY: "QUERY"
|
||
};
|
||
|
||
//Типы источников данных (наименования)
|
||
const DATA_SOURCE_TYPE_NAME = {
|
||
[DATA_SOURCE_TYPE.USER_PROC]: "Пользовательская процедура",
|
||
[DATA_SOURCE_TYPE.QUERY]: "Запрос"
|
||
};
|
||
|
||
//Структура аргумента источника данных
|
||
const DATA_SOURCE_ARGUMENT_SHAPE = PropTypes.shape({
|
||
name: PropTypes.string.isRequired,
|
||
caption: PropTypes.string.isRequired,
|
||
dataType: PropTypes.oneOf(Object.values(ARGUMENT_DATA_TYPE)),
|
||
req: PropTypes.bool.isRequired,
|
||
value: PropTypes.any,
|
||
valueSource: PropTypes.string
|
||
});
|
||
|
||
//Начальное состояние аргумента источника данных
|
||
const DATA_SOURCE_ARGUMENT_INITIAL = {
|
||
name: "",
|
||
caption: "",
|
||
dataType: "",
|
||
req: false,
|
||
value: "",
|
||
valueSource: ""
|
||
};
|
||
|
||
//Структура источника данных
|
||
const DATA_SOURCE_SHAPE = PropTypes.shape({
|
||
type: PropTypes.oneOf([...Object.values(DATA_SOURCE_TYPE), ""]),
|
||
userProc: PropTypes.string,
|
||
stored: PropTypes.string,
|
||
respArg: PropTypes.string,
|
||
arguments: PropTypes.arrayOf(DATA_SOURCE_ARGUMENT_SHAPE)
|
||
});
|
||
|
||
//Начальное состояние истоника данных
|
||
const DATA_SOURCE_INITIAL = {
|
||
type: "",
|
||
userProc: "",
|
||
stored: "",
|
||
respArg: "",
|
||
arguments: []
|
||
};
|
||
|
||
//-----------
|
||
//Тело модуля
|
||
//-----------
|
||
|
||
//Контейнер редактора
|
||
const EditorBox = ({ title, children, onSave }) => {
|
||
//При нажатии на "Сохранить"
|
||
const handleSaveClick = (closeEditor = false) => onSave && onSave(closeEditor);
|
||
|
||
//Формирование представления
|
||
return (
|
||
<Box className={"component-editor__container"}>
|
||
<Divider>{title}</Divider>
|
||
<Stack direction={"column"} spacing={1}>
|
||
{children}
|
||
</Stack>
|
||
<Stack direction={"row"} justifyContent={"right"} p={1}>
|
||
<IconButton onClick={() => handleSaveClick(false)} title={BUTTONS.APPLY}>
|
||
<Icon>done</Icon>
|
||
</IconButton>
|
||
<IconButton onClick={() => handleSaveClick(true)} title={BUTTONS.SAVE}>
|
||
<Icon>done_all</Icon>
|
||
</IconButton>
|
||
</Stack>
|
||
</Box>
|
||
);
|
||
};
|
||
|
||
//Контроль свойств компонента - контейнер редактора
|
||
EditorBox.propTypes = {
|
||
title: PropTypes.string.isRequired,
|
||
children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
|
||
onSave: PropTypes.func
|
||
};
|
||
|
||
//Заголовок раздела редактора
|
||
const EditorSubHeader = ({ title }) => {
|
||
//Формирование представления
|
||
return (
|
||
<Divider className={"component-editor__divider"}>
|
||
<Chip label={title} size={"small"} />
|
||
</Divider>
|
||
);
|
||
};
|
||
|
||
//Контроль свойств компонента - заголовок раздела редактора
|
||
EditorSubHeader.propTypes = {
|
||
title: PropTypes.string.isRequired
|
||
};
|
||
|
||
//Диалог настройки
|
||
const ConfigDialog = ({ title, children, onOk, onCancel }) => {
|
||
//Формирование представления
|
||
return (
|
||
<Dialog onClose={onCancel} open>
|
||
<DialogTitle>{title}</DialogTitle>
|
||
<DialogContent>{children}</DialogContent>
|
||
<DialogActions>
|
||
<Button onClick={() => onOk && onOk()}>{BUTTONS.OK}</Button>
|
||
<Button onClick={() => onCancel && onCancel()}>{BUTTONS.CANCEL}</Button>
|
||
</DialogActions>
|
||
</Dialog>
|
||
);
|
||
};
|
||
|
||
//Контроль свойств компонента - диалог настройки
|
||
ConfigDialog.propTypes = {
|
||
title: PropTypes.string.isRequired,
|
||
children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
|
||
onOk: PropTypes.func,
|
||
onCancel: PropTypes.func
|
||
};
|
||
|
||
//Диалог настройки источника данных
|
||
const ConfigDataSourceDialog = ({ dataSource = null, valueProviders = {}, onOk = null, onCancel = null } = {}) => {
|
||
//Собственное состояние - параметры элемента формы
|
||
const [state, setState] = useState({ ...DATA_SOURCE_INITIAL, ...dataSource });
|
||
|
||
//Собственное состояние - флаги обновление данных
|
||
const [refresh, setRefresh] = useState({ userProcDesc: 0 });
|
||
|
||
//Собственное состояние - элемент привязки меню выбора источника
|
||
const [valueProvidersMenuAnchorEl, setValueProvidersMenuAnchorEl] = useState(null);
|
||
|
||
//Описание выбранной пользовательской процедуры
|
||
const [userProcDesc] = useUserProcDesc({ code: state.userProc, refresh: refresh.userProcDesc });
|
||
|
||
//Подключение к контексту приложения
|
||
const { pOnlineShowDictionary } = useContext(ApplicationСtx);
|
||
|
||
//Установка значения/привязки аргумента
|
||
const setArgumentValueSource = (index, value, valueSource) =>
|
||
setState(pv => ({
|
||
...pv,
|
||
arguments: pv.arguments.map((argument, i) => ({ ...argument, ...(i == index ? { value, valueSource } : {}) }))
|
||
}));
|
||
|
||
//Открытие/сокрытие меню выбора источника
|
||
const toggleValueProvidersMenu = target => setValueProvidersMenuAnchorEl(target instanceof Element ? target : null);
|
||
|
||
//При нажатии на очистку наименования пользовательской процедуры
|
||
const handleUserProcClearClick = () => setState({ ...DATA_SOURCE_INITIAL });
|
||
|
||
//При нажатии на выбор пользовательской процедуры в качестве источника данных
|
||
const handleUserProcSelectClick = () => {
|
||
pOnlineShowDictionary({
|
||
unitCode: "UserProcedures",
|
||
showMethod: "main",
|
||
inputParameters: [{ name: "in_CODE", value: state.userProc }],
|
||
callBack: res => {
|
||
if (res.success) {
|
||
setState(pv => ({ ...pv, type: DATA_SOURCE_TYPE.USER_PROC, userProc: res.outParameters.out_CODE }));
|
||
setRefresh(pv => ({ ...pv, userProcDesc: pv.userProcDesc + 1 }));
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
//При закрытии дилога с сохранением
|
||
const handleOk = () => onOk && onOk({ ...state });
|
||
|
||
//При закртии диалога отменой
|
||
const handleCancel = () => onCancel && onCancel();
|
||
|
||
//При очистке значения/связывания аргумента
|
||
const handleArgumentClearClick = index => setArgumentValueSource(index, "", "");
|
||
|
||
//При отображении меню связывания аргумента с поставщиком данных
|
||
const handleArgumentLinkMenuClick = e => setValueProvidersMenuAnchorEl(e.currentTarget);
|
||
|
||
//При выборе элемента меню связывания аргумента с поставщиком данных
|
||
const handleArgumentLinkClick = valueSource => {
|
||
setArgumentValueSource(valueProvidersMenuAnchorEl.id, "", valueSource);
|
||
toggleValueProvidersMenu();
|
||
};
|
||
|
||
//При вводе значения аргумента
|
||
const handleArgumentChange = (index, value) => setArgumentValueSource(index, value, "");
|
||
|
||
//При изменении описания пользовательской процедуры
|
||
useEffect(() => {
|
||
if (userProcDesc)
|
||
setState(pv => ({
|
||
...pv,
|
||
stored: userProcDesc?.stored?.name,
|
||
respArg: userProcDesc?.stored?.respArg,
|
||
arguments: (userProcDesc?.arguments || []).map(argument => ({ ...DATA_SOURCE_ARGUMENT_INITIAL, ...argument }))
|
||
}));
|
||
}, [userProcDesc]);
|
||
|
||
//Список значений
|
||
const values = Object.keys(valueProviders).reduce((res, key) => [...res, ...Object.keys(valueProviders[key])], []);
|
||
|
||
//Наличие значений
|
||
const isValues = values && values.length > 0 ? true : false;
|
||
|
||
//Меню привязки к поставщикам значений
|
||
const valueProvidersMenu = isValues && (
|
||
<Menu anchorEl={valueProvidersMenuAnchorEl} open={Boolean(valueProvidersMenuAnchorEl)} onClose={toggleValueProvidersMenu}>
|
||
{values.map((value, i) => (
|
||
<MenuItem key={i} onClick={() => handleArgumentLinkClick(value)}>
|
||
{value}
|
||
</MenuItem>
|
||
))}
|
||
</Menu>
|
||
);
|
||
|
||
//Формирование представления
|
||
return (
|
||
<ConfigDialog title="Настройка источника данных" onOk={handleOk} onCancel={handleCancel}>
|
||
<Stack direction={"column"} spacing={1}>
|
||
{valueProvidersMenu}
|
||
<TextField
|
||
type={"text"}
|
||
variant={"standard"}
|
||
value={state.userProc}
|
||
label={"Пользовательская процедура"}
|
||
InputLabelProps={{ shrink: true }}
|
||
InputProps={{
|
||
readOnly: true,
|
||
endAdornment: (
|
||
<InputAdornment position="end">
|
||
<IconButton onClick={handleUserProcClearClick}>
|
||
<Icon>clear</Icon>
|
||
</IconButton>
|
||
<IconButton onClick={handleUserProcSelectClick}>
|
||
<Icon>list</Icon>
|
||
</IconButton>
|
||
</InputAdornment>
|
||
)
|
||
}}
|
||
/>
|
||
{Array.isArray(state?.arguments) &&
|
||
state.arguments.map((argument, i) => (
|
||
<TextField
|
||
key={i}
|
||
type={"text"}
|
||
variant={"standard"}
|
||
value={argument.value || argument.valueSource}
|
||
label={argument.caption}
|
||
onChange={e => handleArgumentChange(i, e.target.value)}
|
||
InputLabelProps={{ shrink: true }}
|
||
InputProps={{
|
||
endAdornment: (
|
||
<InputAdornment position="end">
|
||
<IconButton onClick={() => handleArgumentClearClick(i)}>
|
||
<Icon>clear</Icon>
|
||
</IconButton>
|
||
{isValues && (
|
||
<IconButton id={i} onClick={handleArgumentLinkMenuClick}>
|
||
<Icon>settings_ethernet</Icon>
|
||
</IconButton>
|
||
)}
|
||
</InputAdornment>
|
||
)
|
||
}}
|
||
/>
|
||
))}
|
||
</Stack>
|
||
</ConfigDialog>
|
||
);
|
||
};
|
||
|
||
//Контроль свойств компонента - Диалог настройки источника данных
|
||
ConfigDataSourceDialog.propTypes = {
|
||
dataSource: DATA_SOURCE_SHAPE,
|
||
valueProviders: PropTypes.object,
|
||
onOk: PropTypes.func,
|
||
onCancel: PropTypes.func
|
||
};
|
||
|
||
//Источник данных
|
||
const DataSource = ({ dataSource = null, valueProviders = {}, onChange = null } = {}) => {
|
||
//Собственное состояние - отображение диалога настройки
|
||
const [configDlg, setConfigDlg] = useState(false);
|
||
|
||
//Уведомление родителя о смене настроек источника данных
|
||
const notifyChange = settings => onChange && onChange(settings);
|
||
|
||
//При нажатии на настройку источника данных
|
||
const handleSetup = () => setConfigDlg(true);
|
||
|
||
//При нажатии на настройку источника данных
|
||
const handleSetupOk = dataSource => {
|
||
setConfigDlg(false);
|
||
notifyChange(dataSource);
|
||
};
|
||
|
||
//При нажатии на настройку источника данных
|
||
const handleSetupCancel = () => setConfigDlg(false);
|
||
|
||
//При удалении настроек источника данных
|
||
const handleDelete = () => notifyChange({ ...DATA_SOURCE_INITIAL });
|
||
|
||
//Расчет флага "настроенности"
|
||
const configured = dataSource?.type ? true : false;
|
||
|
||
//Список аргументов
|
||
const args =
|
||
configured &&
|
||
dataSource.arguments.map((argument, i) => (
|
||
<Chip
|
||
key={i}
|
||
label={`:${argument.name} = ${argument.valueSource || argument.value || "NULL"}`}
|
||
variant={"outlined"}
|
||
sx={STYLES.CHIP(true)}
|
||
/>
|
||
));
|
||
|
||
//Формирование представления
|
||
return (
|
||
<>
|
||
{configDlg && (
|
||
<ConfigDataSourceDialog dataSource={dataSource} valueProviders={valueProviders} onOk={handleSetupOk} onCancel={handleSetupCancel} />
|
||
)}
|
||
{configured && (
|
||
<Card variant={"outlined"}>
|
||
<CardActionArea onClick={handleSetup}>
|
||
<CardContent>
|
||
<Typography variant={"subtitle1"} noWrap={true}>
|
||
{dataSource.type === DATA_SOURCE_TYPE.USER_PROC ? dataSource.userProc : "Источник без наименования"}
|
||
</Typography>
|
||
<Typography variant={"caption"} color={"text.secondary"} noWrap={true}>
|
||
{DATA_SOURCE_TYPE_NAME[dataSource.type] || "Неизвестный тип источника"}
|
||
</Typography>
|
||
<Stack direction={"column"} spacing={1} pt={2}>
|
||
{args}
|
||
</Stack>
|
||
</CardContent>
|
||
</CardActionArea>
|
||
<CardActions>
|
||
<IconButton onClick={handleDelete}>
|
||
<Icon>delete</Icon>
|
||
</IconButton>
|
||
</CardActions>
|
||
</Card>
|
||
)}
|
||
{!configured && (
|
||
<Button startIcon={<Icon>build</Icon>} onClick={handleSetup}>
|
||
Настроить
|
||
</Button>
|
||
)}
|
||
</>
|
||
);
|
||
};
|
||
|
||
//Контроль свойств компонента - Источник данных
|
||
DataSource.propTypes = {
|
||
dataSource: DATA_SOURCE_SHAPE,
|
||
valueProviders: PropTypes.object,
|
||
onChange: PropTypes.func
|
||
};
|
||
|
||
//----------------
|
||
//Интерфейс модуля
|
||
//----------------
|
||
|
||
export { STYLES, ARGUMENT_DATA_TYPE, DATA_SOURCE_TYPE, DATA_SOURCE_SHAPE, DATA_SOURCE_INITIAL, EditorBox, EditorSubHeader, ConfigDialog, DataSource };
|