diff --git a/app/panels/panels_editor/component_editor.js b/app/panels/panels_editor/component_editor.js
new file mode 100644
index 0000000..ce3bc8d
--- /dev/null
+++ b/app/panels/panels_editor/component_editor.js
@@ -0,0 +1,52 @@
+/*
+ Парус 8 - Панели мониторинга - Редактор панелей
+ Редактор свойств компонента панели
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+import React from "react"; //Классы React
+import PropTypes from "prop-types"; //Контроль свойств компонента
+import { Box, Typography } from "@mui/material"; //Интерфейсные элементы
+import { useComponentModule } from "./components/components_hooks"; //Хуки компонентов
+import "./panels_editor.css"; //Стили редактора
+
+//-----------
+//Тело модуля
+//-----------
+
+//Редактор свойств компонента панели
+const ComponentEditor = ({ id, path, settings = {}, valueProviders = {}, onSettingsChange = null } = {}) => {
+ //Подгрузка модуля редактора компонента (lazy здесь постоянно обновлялся при смене props, поэтому на хуке, от props независимого)
+ const [ComponentEditor, init] = useComponentModule({ path, module: "editor" });
+
+ //Расчёт флага наличия компонента
+ const haveComponent = path ? true : false;
+
+ //Формирование представления
+ return (
+
+ {haveComponent && init && (
+
+ )}
+ {!haveComponent && Компонент не определён}
+
+ );
+};
+
+//Контроль свойств компонента - редактор свойств компонента панели
+ComponentEditor.propTypes = {
+ id: PropTypes.string.isRequired,
+ path: PropTypes.string.isRequired,
+ settings: PropTypes.object,
+ valueProviders: PropTypes.object,
+ onSettingsChange: PropTypes.func
+};
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+export { ComponentEditor };
diff --git a/app/panels/panels_editor/component_view.js b/app/panels/panels_editor/component_view.js
new file mode 100644
index 0000000..c914010
--- /dev/null
+++ b/app/panels/panels_editor/component_view.js
@@ -0,0 +1,72 @@
+/*
+ Парус 8 - Панели мониторинга - Редактор панелей
+ Представление компонента панели
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+import React from "react"; //Классы React
+import PropTypes from "prop-types"; //Контроль свойств компонента
+import { Box, Typography } from "@mui/material"; //Интерфейсные элементы
+import { useComponentModule } from "./components/components_hooks"; //Хуки компонентов
+import "./panels_editor.css"; //Стили редактора
+
+//-----------
+//Тело модуля
+//-----------
+
+//Представление компонента панели
+const ComponentView = ({ id, path, settings = {}, values = {}, onValuesChange = null } = {}) => {
+ //Подгрузка модуля представления компонента (lazy здесь постоянно обновлялся при смене props, поэтому на хуке, от props независимого)
+ const [ComponentView, init] = useComponentModule({ path, module: "view" });
+
+ //При смене значений
+ const handleValuesChange = values => onValuesChange && onValuesChange(id, values);
+
+ //Расчёт флага наличия компонента
+ const haveComponent = path ? true : false;
+
+ //Формирование представления
+ return (
+
+ {haveComponent && init && }
+ {!haveComponent && Компонент не определён}
+
+ );
+};
+
+//Контроль свойств компонента - компонент панели
+ComponentView.propTypes = {
+ id: PropTypes.string.isRequired,
+ path: PropTypes.string.isRequired,
+ settings: PropTypes.object,
+ values: PropTypes.object,
+ onValuesChange: PropTypes.func
+};
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+export { ComponentView };
+
+//--------------------------
+//ВАЖНО: Можно на React.lazy
+//--------------------------
+
+//ПРИМЕР:
+/*
+import React, { Suspense, lazy } from "react"; //Классы React
+const ComponentView = ({ path = null, props = {} } = {}) => {
+ const haveComponent = path ? true : false;
+ const ComponentView = haveComponent ? lazy(() => import(`./components/${path}/view`)) : null;
+ return (
+
+ {haveComponent && ()}
+ {!haveComponent && Компонент не определён}
+
+ );
+};
+*/
diff --git a/app/panels/panels_editor/components/chart/editor.js b/app/panels/panels_editor/components/chart/editor.js
new file mode 100644
index 0000000..b6aa7f3
--- /dev/null
+++ b/app/panels/panels_editor/components/chart/editor.js
@@ -0,0 +1,56 @@
+/*
+ Парус 8 - Панели мониторинга - Редактор панелей
+ Компоненты: График (редактор настроек)
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+import React, { useEffect, useState } from "react"; //Классы React
+import PropTypes from "prop-types"; //Контроль свойств компонента
+import { DATA_SOURCE_SHAPE, DataSource, EditorBox, EditorSubHeader } from "../editors_common"; //Общие компоненты редакторов
+import "../../panels_editor.css"; //Стили редактора
+
+//-----------
+//Тело модуля
+//-----------
+
+//График (редактор настроек)
+const ChartEditor = ({ id, dataSource = null, valueProviders = {}, onSettingsChange = null } = {}) => {
+ //Собственное состояние - текущие настройки
+ const [settings, setSettings] = useState(null);
+
+ //При изменении компонента
+ useEffect(() => {
+ settings?.id != id && setSettings({ id, dataSource });
+ }, [settings, id, dataSource]);
+
+ //При сохранении изменений элемента
+ const handleDataSourceChange = dataSource => setSettings(pv => ({ ...pv, dataSource: { ...dataSource } }));
+
+ //При сохранении настроек
+ const handleSave = (closeEditor = false) => onSettingsChange && onSettingsChange({ id, settings, closeEditor });
+
+ //Формирование представления
+ return (
+
+
+
+
+ );
+};
+
+//Контроль свойств компонента - График (редактор настроек)
+ChartEditor.propTypes = {
+ id: PropTypes.string.isRequired,
+ dataSource: DATA_SOURCE_SHAPE,
+ valueProviders: PropTypes.object,
+ onSettingsChange: PropTypes.func
+};
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+export default ChartEditor;
diff --git a/app/panels/panels_editor/components/chart/view.js b/app/panels/panels_editor/components/chart/view.js
new file mode 100644
index 0000000..273da15
--- /dev/null
+++ b/app/panels/panels_editor/components/chart/view.js
@@ -0,0 +1,79 @@
+/*
+ Парус 8 - Панели мониторинга - Редактор панелей
+ Компоненты: График (представление)
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+import React from "react"; //Классы React
+import PropTypes from "prop-types"; //Контроль свойств компонента
+import { Paper } from "@mui/material"; //Интерфейсные элементы
+import { P8PChart } from "../../../../components/p8p_chart"; //График
+import { useComponentDataSource } from "../components_hooks"; //Хуки для данных
+import { DATA_SOURCE_SHAPE } from "../editors_common"; //Общие объекты компонентов
+import { COMPONENT_MESSAGE_TYPE, COMPONENT_MESSAGES, ComponentInlineMessage } from "../views_common"; //Общие компоненты представлений
+import "../../panels_editor.css"; //Стили редактора
+
+//---------
+//Константы
+//---------
+
+//Иконка компонента
+const COMPONENT_ICON = "bar_chart";
+
+//Наименование компонента
+const COMPONENT_NAME = "График";
+
+//Стили
+const STYLES = {
+ CHART: { width: "100%", height: "100%", alignItems: "center", justifyContent: "center", display: "flex" }
+};
+
+//-----------
+//Тело модуля
+//-----------
+
+//График (представление)
+const Chart = ({ dataSource = null, values = {} } = {}) => {
+ //Собственное состояние - данные
+ const [data, error] = useComponentDataSource({ dataSource, values });
+
+ //Флаг настроенности графика
+ const haveConfing = dataSource?.stored ? true : false;
+
+ //Флаг наличия данных
+ const haveData = data?.init === true && !error ? true : false;
+
+ //Данные графика
+ const chart = data?.XCHART || {};
+
+ //Формирование представления
+ return (
+
+ {haveConfing && haveData ? (
+
+ ) : (
+
+ )}
+
+ );
+};
+
+//Контроль свойств компонента - График (представление)
+Chart.propTypes = {
+ dataSource: DATA_SOURCE_SHAPE,
+ values: PropTypes.object
+};
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+export default Chart;
diff --git a/app/panels/panels_editor/components/components.js b/app/panels/panels_editor/components/components.js
new file mode 100644
index 0000000..632cdf6
--- /dev/null
+++ b/app/panels/panels_editor/components/components.js
@@ -0,0 +1,129 @@
+/*
+ Парус 8 - Панели мониторинга - Редактор панелей
+ Компоненты: Описание
+*/
+
+//---------
+//Константы
+//---------
+
+const COMPONETNS = [
+ {
+ name: "Форма",
+ path: "form",
+ settings2: {
+ title: "Параметры формирования",
+ autoApply: true,
+ items: [
+ {
+ name: "AGENT",
+ caption: "Контрагент",
+ unitCode: "AGNLIST",
+ unitName: "Контрагенты",
+ showMethod: "main",
+ showMethodName: "main",
+ parameter: "Мнемокод",
+ inputParameter: "in_AGNABBR",
+ outputParameter: "out_AGNABBR"
+ },
+ {
+ name: "DOC_TYPE",
+ caption: "Тип документа",
+ unitCode: "DOCTYPES",
+ unitName: "Типы документов",
+ showMethod: "main",
+ showMethodName: "main",
+ parameter: "Мнемокод",
+ inputParameter: "in_DOCCODE",
+ outputParameter: "out_DOCCODE"
+ }
+ ]
+ }
+ },
+ {
+ name: "График",
+ path: "chart",
+ settings2: {
+ dataSource: {
+ type: "USER_PROC",
+ userProc: "ГрафТоп5ДогКонтрТип",
+ stored: "UDO_P_P8P_AGNCONTR_CHART",
+ respArg: "COUT",
+ arguments: [
+ { name: "SAGENT", caption: "Контрагент", dataType: "STR", req: false, value: "", valueSource: "AGENT" },
+ { name: "SDOC_TYPE", caption: "Тип документа", dataType: "STR", req: false, value: "", valueSource: "DOC_TYPE" }
+ ]
+ }
+ }
+ },
+ {
+ name: "Таблица",
+ path: "table",
+ settings2: {
+ dataSource: {
+ type: "USER_PROC",
+ userProc: "ТаблицаДогКонтрТип",
+ stored: "UDO_P_P8P_AGNCONTR_TABLE",
+ respArg: "COUT",
+ arguments: [
+ { name: "SAGENT", caption: "Контрагент", dataType: "STR", req: false, value: "", valueSource: "AGENT" },
+ { name: "SDOC_TYPE", caption: "Тип документа", dataType: "STR", req: false, value: "", valueSource: "DOC_TYPE" }
+ ]
+ }
+ }
+ },
+ {
+ name: "Индикатор",
+ path: "indicator",
+ settings2: {
+ dataSource: {
+ type: "USER_PROC",
+ userProc: "ИндКолДогКонтрТип sdfg sdfg sfdg sdfg sdfg sdfg ",
+ stored: "UDO_P_P8P_AGNCONTR_IND",
+ respArg: "COUT",
+ arguments: [
+ { name: "SAGENT", caption: "Контрагент", dataType: "STR", req: false, value: "", valueSource: "AGENT" },
+ { name: "SDOC_TYPE", caption: "Тип документа", dataType: "STR", req: false, value: "", valueSource: "DOC_TYPE" },
+ {
+ name: "NIND_TYPE",
+ caption: "Тип индикатора (0 - все, 1 - неутвержденные)",
+ dataType: "NUMB",
+ req: true,
+ value: "0",
+ valueSource: ""
+ }
+ ]
+ }
+ }
+ } /*,
+ {
+ name: "Индикатор2",
+ path: "indicator",
+ settings: {
+ dataSource: {
+ type: "USER_PROC",
+ userProc: "ИндКолДогКонтрТип",
+ stored: "UDO_P_P8P_AGNCONTR_IND",
+ respArg: "COUT",
+ arguments: [
+ { name: "SAGENT", caption: "Контрагент", dataType: "STR", req: false, value: "", valueSource: "AGENT" },
+ { name: "SDOC_TYPE", caption: "Тип документа", dataType: "STR", req: false, value: "", valueSource: "DOC_TYPE" },
+ {
+ name: "NIND_TYPE",
+ caption: "Тип индикатора (0 - все, 1 - неутвержденные)",
+ dataType: "NUMB",
+ req: true,
+ value: "1",
+ valueSource: ""
+ }
+ ]
+ }
+ }
+ }*/
+];
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+export { COMPONETNS };
diff --git a/app/panels/panels_editor/components/components_hooks.js b/app/panels/panels_editor/components/components_hooks.js
new file mode 100644
index 0000000..a23fce5
--- /dev/null
+++ b/app/panels/panels_editor/components/components_hooks.js
@@ -0,0 +1,174 @@
+/*
+ Парус 8 - Панели мониторинга - Редактор панелей
+ Компоненты: Хуки компонентов
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+import { useState, useContext, useEffect, useRef } from "react"; //Классы React
+import client from "../../../core/client"; //Клиент взаимодействия с сервером приложений
+import { formatErrorMessage } from "../../../core/utils"; //Общие вспомогательные функции
+import { BackEndСtx } from "../../../context/backend"; //Контекст взаимодействия с сервером
+import { DATA_SOURCE_TYPE, ARGUMENT_DATA_TYPE } from "./editors_common"; //Общие объекты редакторов
+
+//-----------
+//Тело модуля
+//-----------
+
+//Загрузка модуля компонента из модуля (можно применять как альтернативу React.lazy)
+const useComponentModule = ({ path = null, module = "view" } = {}) => {
+ //Собственное состояние - импортированный модуль компонента
+ const [componentModule, setComponentModule] = useState(null);
+
+ //Собственное состояние - флаг готовности
+ const [init, setInit] = useState(false);
+
+ //При подмонтировании к странице
+ useEffect(() => {
+ //Динамическая загрузка модуля компонента из библиотеки
+ const importComponentModule = async () => {
+ setInit(false);
+ const moduleContent = await import(`./${path}/${module}`);
+ setComponentModule(moduleContent);
+ setInit(true);
+ };
+ if (path) importComponentModule();
+ }, [path, module]);
+
+ //Возвращаем интерфейс хука
+ return [componentModule, init];
+};
+
+//Описание пользовательской процедуры
+const useUserProcDesc = ({ code, refresh }) => {
+ //Собственное состояние - флаг загрузки
+ const [isLoading, setLoading] = useState(false);
+
+ //Собственное состояние - данные
+ const [data, setData] = useState(null);
+
+ //Подключение к контексту взаимодействия с сервером
+ const { executeStored } = useContext(BackEndСtx);
+
+ //При необходимости обновить данные компонента
+ useEffect(() => {
+ //Загрузка данных с сервера
+ const loadData = async () => {
+ try {
+ setLoading(true);
+ const data = await executeStored({
+ stored: "PKG_P8PANELS_EDITOR.USERPROCS_DESC",
+ args: { SCODE: code },
+ respArg: "COUT",
+ isArray: name => name === "arguments",
+ loader: false
+ });
+ setData(data?.XUSERPROC || null);
+ } finally {
+ setLoading(false);
+ }
+ };
+ //Если надо обновить и есть для чего получать данные
+ if (refresh > 0)
+ if (code) loadData();
+ else setData(null);
+ }, [refresh, code, executeStored]);
+
+ //Возвращаем интерфейс хука
+ return [data, isLoading];
+};
+
+//Получение данных компонента из источника
+const useComponentDataSource = ({ dataSource, values }) => {
+ //Контроллер для прерывания запросов
+ const abortController = useRef(null);
+
+ //Собственное состояние - параметры исполнения
+ const [state, setState] = useState({ stored: null, storedArgs: [], respArg: null, reqSet: false });
+
+ //Собственное состояние - флаг загрузки
+ const [isLoading, setLoading] = useState(false);
+
+ //Собственное состояние - данные
+ const [data, setData] = useState({ init: false });
+
+ //Собственное состояние - ошибка получения данных
+ const [error, setError] = useState(null);
+
+ //Подключение к контексту взаимодействия с сервером
+ const { executeStored } = useContext(BackEndСtx);
+
+ //При необходимости обновить данные
+ useEffect(() => {
+ //Загрузка данных с сервера
+ const loadData = async () => {
+ try {
+ setLoading(true);
+ abortController.current?.abort?.();
+ abortController.current = new AbortController();
+ const data = await executeStored({
+ stored: state.stored,
+ args: { ...(state.storedArgs ? state.storedArgs : {}) },
+ respArg: state.respArg,
+ loader: false,
+ signal: abortController.current.signal,
+ showErrorMessage: false
+ });
+ setError(null);
+ setData({ ...data, init: true });
+ } catch (e) {
+ if (e.message !== client.ERR_ABORTED) {
+ setError(formatErrorMessage(e.message).text);
+ setData({ init: false });
+ }
+ } finally {
+ setLoading(false);
+ }
+ };
+ if (state.reqSet) {
+ if (state.stored) loadData();
+ } else setData({ init: false });
+ return () => abortController.current?.abort?.();
+ }, [state.stored, state.storedArgs, state.respArg, state.reqSet, executeStored]);
+
+ //При изменении свойств
+ useEffect(() => {
+ setState(pv => {
+ if (dataSource?.type == DATA_SOURCE_TYPE.USER_PROC) {
+ const { stored, respArg } = dataSource;
+ let reqSet = true;
+ const storedArgs = {};
+ dataSource.arguments.forEach(argument => {
+ let v = argument.valueSource ? values[argument.valueSource] : argument.value;
+ storedArgs[argument.name] =
+ argument.dataType == ARGUMENT_DATA_TYPE.NUMB
+ ? isNaN(parseFloat(v))
+ ? null
+ : parseFloat(v)
+ : argument.dataType == ARGUMENT_DATA_TYPE.DATE
+ ? new Date(v)
+ : String(v === undefined ? "" : v);
+ if (argument.req === true && [undefined, null, ""].includes(storedArgs[argument.name])) reqSet = false;
+ });
+ if (pv.stored != stored || pv.respArg != respArg || JSON.stringify(pv.storedArgs) != JSON.stringify(storedArgs)) {
+ if (!reqSet) {
+ setError("Не заданы обязательные параметры источника данных");
+ setData({ init: false });
+ }
+ return { stored, respArg, storedArgs, reqSet };
+ } else return pv;
+ } else return pv;
+ });
+ }, [dataSource, values]);
+
+ //Возвращаем интерфейс хука
+ return [data, error, isLoading];
+};
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+export { useComponentModule, useUserProcDesc, useComponentDataSource };
diff --git a/app/panels/panels_editor/components/editors_common.js b/app/panels/panels_editor/components/editors_common.js
new file mode 100644
index 0000000..2c2f172
--- /dev/null
+++ b/app/panels/panels_editor/components/editors_common.js
@@ -0,0 +1,434 @@
+/*
+ Парус 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 (
+
+ {title}
+
+ {children}
+
+
+ handleSaveClick(false)} title={BUTTONS.APPLY}>
+ done
+
+ handleSaveClick(true)} title={BUTTONS.SAVE}>
+ done_all
+
+
+
+ );
+};
+
+//Контроль свойств компонента - контейнер редактора
+EditorBox.propTypes = {
+ title: PropTypes.string.isRequired,
+ children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
+ onSave: PropTypes.func
+};
+
+//Заголовок раздела редактора
+const EditorSubHeader = ({ title }) => {
+ //Формирование представления
+ return (
+
+
+
+ );
+};
+
+//Контроль свойств компонента - заголовок раздела редактора
+EditorSubHeader.propTypes = {
+ title: PropTypes.string.isRequired
+};
+
+//Диалог настройки
+const ConfigDialog = ({ title, children, onOk, onCancel }) => {
+ //Формирование представления
+ return (
+
+ );
+};
+
+//Контроль свойств компонента - диалог настройки
+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 && (
+
+ );
+
+ //Формирование представления
+ return (
+
+
+ {valueProvidersMenu}
+
+
+ clear
+
+
+ list
+
+
+ )
+ }}
+ />
+ {Array.isArray(state?.arguments) &&
+ state.arguments.map((argument, i) => (
+ handleArgumentChange(i, e.target.value)}
+ InputLabelProps={{ shrink: true }}
+ InputProps={{
+ endAdornment: (
+
+ handleArgumentClearClick(i)}>
+ clear
+
+ {isValues && (
+
+ settings_ethernet
+
+ )}
+
+ )
+ }}
+ />
+ ))}
+
+
+ );
+};
+
+//Контроль свойств компонента - Диалог настройки источника данных
+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) => (
+
+ ));
+
+ //Формирование представления
+ return (
+ <>
+ {configDlg && (
+
+ )}
+ {configured && (
+
+
+
+
+ {dataSource.type === DATA_SOURCE_TYPE.USER_PROC ? dataSource.userProc : "Источник без наименования"}
+
+
+ {DATA_SOURCE_TYPE_NAME[dataSource.type] || "Неизвестный тип источника"}
+
+
+ {args}
+
+
+
+
+
+ delete
+
+
+
+ )}
+ {!configured && (
+
+ )}
+ >
+ );
+};
+
+//Контроль свойств компонента - Источник данных
+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 };
diff --git a/app/panels/panels_editor/components/form/common.js b/app/panels/panels_editor/components/form/common.js
new file mode 100644
index 0000000..c076bfb
--- /dev/null
+++ b/app/panels/panels_editor/components/form/common.js
@@ -0,0 +1,49 @@
+/*
+ Парус 8 - Панели мониторинга - Редактор панелей
+ Компоненты: Форма (общие константы)
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+import PropTypes from "prop-types"; //Контроль свойств компонента
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+//Структура элемента формы
+export const ITEM_SHAPE = PropTypes.shape({
+ name: PropTypes.string.isRequired,
+ caption: PropTypes.string.isRequired,
+ unitCode: PropTypes.string,
+ unitName: PropTypes.string,
+ showMethod: PropTypes.string,
+ showMethodName: PropTypes.string,
+ parameter: PropTypes.string,
+ inputParameter: PropTypes.string,
+ outputParameter: PropTypes.string
+});
+
+//Начальное состояние элемента формы
+export const ITEM_INITIAL = {
+ name: "",
+ caption: "",
+ unitCode: "",
+ unitName: "",
+ showMethod: "",
+ showMethodName: "",
+ parameter: "",
+ inputParameter: "",
+ outputParameter: ""
+};
+
+//Начальное состояние элементов формы
+export const ITEMS_INITIAL = [];
+
+//Ориентация элементов формы
+export const ORIENTATION = {
+ H: "H",
+ V: "v"
+};
diff --git a/app/panels/panels_editor/components/form/editor.js b/app/panels/panels_editor/components/form/editor.js
new file mode 100644
index 0000000..719a82b
--- /dev/null
+++ b/app/panels/panels_editor/components/form/editor.js
@@ -0,0 +1,306 @@
+/*
+ Парус 8 - Панели мониторинга - Редактор панелей
+ Компоненты: Форма (редактор настроек)
+*/
+
+//TODO: Контроль уникальности имени элемента формы
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+import React, { useEffect, useState, useContext } from "react"; //Классы React
+import PropTypes from "prop-types"; //Контроль свойств компонента
+import {
+ TextField,
+ Button,
+ Icon,
+ Select,
+ MenuItem,
+ FormControl,
+ InputLabel,
+ FormControlLabel,
+ Switch,
+ Chip,
+ Stack,
+ InputAdornment,
+ IconButton
+} from "@mui/material"; //Интерфейсные элементы
+import { ApplicationСtx } from "../../../../context/application"; //Контекст приложения
+import { STYLES as COMMON_STYLES, EditorBox, EditorSubHeader, ConfigDialog } from "../editors_common"; //Общие компоненты редакторов
+import { ITEM_SHAPE, ITEM_INITIAL, ITEMS_INITIAL, ORIENTATION } from "./common"; //Общие ресурсы и константы формы
+
+//---------
+//Константы
+//---------
+
+//Стили
+const STYLES = {
+ CHIP_ITEM: { ...COMMON_STYLES.CHIP(true, false) }
+};
+
+//------------------------------------
+//Вспомогательные функции и компоненты
+//------------------------------------
+
+//Редактор элемента
+const ItemEditor = ({ item = null, onOk = null, onCancel = null } = {}) => {
+ //Собственное состояние - параметры элемента формы
+ const [state, setState] = useState({ ...ITEM_INITIAL, ...item });
+
+ //Подключение к контексту приложения
+ const { pOnlineShowDictionary } = useContext(ApplicationСtx);
+
+ //При закрытии редактора с сохранением
+ const handleOk = () => onOk && onOk({ ...state });
+
+ //При закрытии редактора с отменой
+ const handleCancel = () => onCancel && onCancel();
+
+ //При изменении параметра элемента
+ const handleChange = e => setState(pv => ({ ...pv, [e.target.id]: e.target.value }));
+
+ //При нажатии на очистку раздела
+ const handleClearUnitClick = () =>
+ setState(pv => ({
+ ...pv,
+ unitCode: "",
+ unitName: "",
+ showMethod: "",
+ showMethodName: "",
+ parameter: "",
+ inputParameter: "",
+ outputParameter: ""
+ }));
+
+ //При нажатии на выбор раздела
+ const handleSelectUnitClick = () => {
+ pOnlineShowDictionary({
+ unitCode: "Units",
+ showMethod: "methods",
+ inputParameters: [
+ { name: "pos_unit_name", value: state.unitName },
+ { name: "pos_method_name", value: state.showMethodName }
+ ],
+ callBack: res =>
+ res.success &&
+ setState(pv => ({
+ ...pv,
+ unitCode: res.outParameters.unit_code,
+ unitName: res.outParameters.unit_name,
+ showMethod: res.outParameters.method_code,
+ showMethodName: res.outParameters.method_name,
+ parameter: "",
+ inputParameter: "",
+ outputParameter: ""
+ }))
+ });
+ };
+
+ //При нажатии на выбор параметра метода вызова
+ const handleSelectUnitParameterClick = () => {
+ state.unitCode &&
+ state.showMethod &&
+ pOnlineShowDictionary({
+ unitCode: "UnitParams",
+ showMethod: "main",
+ inputParameters: [
+ { name: "in_UNITCODE", value: state.unitCode },
+ { name: "in_PARENT_METHOD_CODE", value: state.showMethod },
+ { name: "in_PARAMNAME", value: state.parameter }
+ ],
+ callBack: res =>
+ res.success &&
+ setState(pv => ({
+ ...pv,
+ parameter: res.outParameters.out_PARAMNAME,
+ inputParameter: res.outParameters.out_IN_CODE,
+ outputParameter: res.outParameters.out_OUT_CODE
+ }))
+ });
+ };
+
+ //Формирование представления
+ return (
+
+
+
+
+
+
+ clear
+
+
+ list
+
+
+ )
+ }}
+ />
+
+
+
+ list
+
+
+ )
+ }}
+ />
+
+
+ );
+};
+
+//Контроль свойств - редактор элемента
+ItemEditor.propTypes = {
+ item: ITEM_SHAPE,
+ onOk: PropTypes.func,
+ onCancel: PropTypes.func
+};
+
+//-----------
+//Тело модуля
+//-----------
+
+//Форма (редактор настроек)
+const FormEditor = ({ id, title = "", orientation = ORIENTATION.V, autoApply = false, items = ITEMS_INITIAL, onSettingsChange = null } = {}) => {
+ //Собственное состояние - текущие настройки
+ const [settings, setSettings] = useState(null);
+
+ //Собственное состояние - предоставляемые в панель значения
+ const [providedValues, setProvidedValues] = useState([]);
+
+ //Собственное состояние - редактор элементов формы
+ const [itemEditor, setItemEditor] = useState({ display: false, index: null });
+
+ //При изменении значения настройки
+ const handleChange = e => setSettings({ ...settings, [e.target.name]: e.target.type === "checkbox" ? e.target.checked : e.target.value });
+
+ //При добавлении нового элемента
+ const handleItemAdd = () => setItemEditor({ display: true, index: null });
+
+ //При нажатии на элемент
+ const handleItemClick = i => setItemEditor({ display: true, index: i });
+
+ //При удалении элемента
+ const handleItemDelete = i => {
+ const items = [...settings.items];
+ items.splice(i, 1);
+ setSettings(pv => ({ ...pv, items }));
+ };
+
+ //При сохранении изменений элемента
+ const handleItemSave = item => {
+ const items = [...settings.items];
+ itemEditor.index == null ? items.push({ ...item }) : (items[itemEditor.index] = { ...item });
+ setSettings(pv => ({ ...pv, items }));
+ setItemEditor({ display: false, index: null });
+ };
+
+ //При отмене сохранения изменений элемента
+ const handleItemCancel = () => setItemEditor({ display: false, index: null });
+
+ //При сохранении настроек
+ const handleSave = (closeEditor = false) => onSettingsChange && onSettingsChange({ id, settings, providedValues, closeEditor });
+
+ //При изменении компонента
+ useEffect(() => {
+ settings?.id != id && setSettings({ id, title, orientation, autoApply, items });
+ }, [settings, id, title, orientation, autoApply, items]);
+
+ //При изменении состава элементов формы
+ useEffect(() => {
+ Array.isArray(settings?.items) && setProvidedValues(settings.items.map(item => item.name));
+ }, [settings?.items]);
+
+ //Формирование представления
+ return (
+ settings && (
+
+ {itemEditor.display && (
+
+ )}
+
+
+
+ Ориентация
+
+
+ }
+ label={"Автоподтверждение"}
+ />
+
+ {Array.isArray(settings?.items) &&
+ settings.items.length > 0 &&
+ settings.items.map((item, i) => (
+ handleItemClick(i)}
+ onDelete={() => handleItemDelete(i)}
+ sx={STYLES.CHIP_ITEM}
+ />
+ ))}
+
+
+ )
+ );
+};
+
+//Контроль свойств компонента - Форма (редактор настроек)
+FormEditor.propTypes = {
+ id: PropTypes.string.isRequired,
+ title: PropTypes.string,
+ orientation: PropTypes.oneOf(Object.values(ORIENTATION)),
+ autoApply: PropTypes.bool,
+ items: PropTypes.arrayOf(ITEM_SHAPE),
+ onSettingsChange: PropTypes.func
+};
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+export default FormEditor;
diff --git a/app/panels/panels_editor/components/form/view.js b/app/panels/panels_editor/components/form/view.js
new file mode 100644
index 0000000..77965c2
--- /dev/null
+++ b/app/panels/panels_editor/components/form/view.js
@@ -0,0 +1,168 @@
+/*
+ Парус 8 - Панели мониторинга - Редактор панелей
+ Компоненты: Форма (представление)
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+import React, { useEffect, useState, useContext } from "react"; //Классы React
+import PropTypes from "prop-types"; //Контроль свойств компонента
+import { Paper, Stack, Typography, Icon, TextField, IconButton, InputAdornment } from "@mui/material"; //Интерфейсные элементы
+import { ApplicationСtx } from "../../../../context/application"; //Контекст приложения
+import { COMPONENT_MESSAGES, ComponentInlineMessage } from "../views_common"; //Общие компоненты представлений
+import { ITEM_SHAPE, ITEMS_INITIAL, ORIENTATION } from "./common"; //Общие ресурсы и константы формы
+import "../../panels_editor.css"; //Стили редактора
+
+//---------
+//Константы
+//---------
+
+//Иконка компонента
+const COMPONENT_ICON = "fact_check";
+
+//Наименование компонента
+const COMPONENT_NAME = "Форма";
+
+//------------------------------------
+//Вспомогательные функции и компоненты
+//------------------------------------
+
+//Элемент формы
+const FormItem = ({ item = null, fullWidth = false, value = "", onChange = null } = {}) => {
+ //Подключение к контексту приложения
+ const { pOnlineShowDictionary } = useContext(ApplicationСtx);
+
+ //При изменении значения элемента
+ const handleChange = e => onChange && onChange(e.target.id, e.target.value);
+
+ //При очистке значения элемента
+ const handleClear = () => onChange(item.name, "");
+
+ //При выборе значения из словаря
+ const handleDictionary = () =>
+ item.unitCode &&
+ item.showMethod &&
+ pOnlineShowDictionary({
+ unitCode: item.unitCode,
+ showMethod: item.showMethod,
+ inputParameters: [{ name: item.inputParameter, value }],
+ callBack: res => res.success && onChange && onChange(item.name, res.outParameters[item.outputParameter])
+ });
+ //Формирование представления
+ return (
+ item && (
+
+
+ clear
+
+
+ list
+
+
+ )
+ }
+ })}
+ />
+ )
+ );
+};
+
+//Контроль свойств - элемент формы
+FormItem.propTypes = {
+ item: ITEM_SHAPE,
+ fullWidth: PropTypes.bool,
+ value: PropTypes.any,
+ onChange: PropTypes.func
+};
+
+//-----------
+//Тело модуля
+//-----------
+
+//Форма (представление)
+const Form = ({ title = null, orientation = ORIENTATION.V, autoApply = false, items = ITEMS_INITIAL, values = {}, onValuesChange = null } = {}) => {
+ //Собственное состояние - значения элементов
+ const [selfValues, setSelfValues] = useState({});
+
+ //При изменении состава элементов или значений
+ useEffect(() => setSelfValues(items.reduce((sV, item) => ({ ...sV, [item.name]: values[item.name] }), {})), [items, values]);
+
+ //При изменении значения элемента формы
+ const handleItemChange = (name, value) => {
+ setSelfValues(pv => ({ ...pv, [name]: value }));
+ autoApply && onValuesChange && onValuesChange({ ...selfValues, [name]: value });
+ };
+
+ //При подтверждении изменений формы
+ const handleOkClick = () => onValuesChange && onValuesChange({ ...selfValues });
+
+ //Флаг настроенности формы
+ const haveConfing = items && Array.isArray(items) && items.length > 0;
+
+ //Формирование представления
+ return (
+
+ {haveConfing ? (
+
+
+ {title && (
+
+ {title}
+
+ )}
+ {!autoApply && (
+
+ done
+
+ )}
+
+
+ {items.map((item, i) => (
+
+ ))}
+
+
+ ) : (
+
+ )}
+
+ );
+};
+
+//Контроль свойств компонента - Форма (представление)
+Form.propTypes = {
+ id: PropTypes.string.isRequired,
+ title: PropTypes.string,
+ orientation: PropTypes.oneOf(Object.values(ORIENTATION)),
+ autoApply: PropTypes.bool,
+ items: PropTypes.arrayOf(ITEM_SHAPE),
+ values: PropTypes.object,
+ onValuesChange: PropTypes.func
+};
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+export default Form;
diff --git a/app/panels/panels_editor/components/indicator/editor.js b/app/panels/panels_editor/components/indicator/editor.js
new file mode 100644
index 0000000..5d8a3f4
--- /dev/null
+++ b/app/panels/panels_editor/components/indicator/editor.js
@@ -0,0 +1,56 @@
+/*
+ Парус 8 - Панели мониторинга - Редактор панелей
+ Компоненты: Индикатор (редактор настроек)
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+import React, { useEffect, useState } from "react"; //Классы React
+import PropTypes from "prop-types"; //Контроль свойств компонента
+import { DATA_SOURCE_SHAPE, DataSource, EditorBox, EditorSubHeader } from "../editors_common"; //Общие компоненты редакторов
+import "../../panels_editor.css"; //Стили редактора
+
+//-----------
+//Тело модуля
+//-----------
+
+//Индикатор (редактор настроек)
+const IndicatorEditor = ({ id, dataSource = null, valueProviders = {}, onSettingsChange = null } = {}) => {
+ //Собственное состояние - текущие настройки
+ const [settings, setSettings] = useState(null);
+
+ //При изменении компонента
+ useEffect(() => {
+ settings?.id != id && setSettings({ id, dataSource });
+ }, [settings, id, dataSource]);
+
+ //При сохранении изменений элемента
+ const handleDataSourceChange = dataSource => setSettings(pv => ({ ...pv, dataSource: { ...dataSource } }));
+
+ //При сохранении настроек
+ const handleSave = (closeEditor = false) => onSettingsChange && onSettingsChange({ id, settings, closeEditor });
+
+ //Формирование представления
+ return (
+
+
+
+
+ );
+};
+
+//Контроль свойств компонента - Индикатор (редактор настроек)
+IndicatorEditor.propTypes = {
+ id: PropTypes.string.isRequired,
+ dataSource: DATA_SOURCE_SHAPE,
+ valueProviders: PropTypes.object,
+ onSettingsChange: PropTypes.func
+};
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+export default IndicatorEditor;
diff --git a/app/panels/panels_editor/components/indicator/view.js b/app/panels/panels_editor/components/indicator/view.js
new file mode 100644
index 0000000..509075f
--- /dev/null
+++ b/app/panels/panels_editor/components/indicator/view.js
@@ -0,0 +1,131 @@
+/*
+ Парус 8 - Панели мониторинга - Редактор панелей
+ Компоненты: Индикатор (представление)
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+import React from "react"; //Классы React
+import PropTypes from "prop-types"; //Контроль свойств компонента
+import { Paper, Stack, Typography, Icon } from "@mui/material"; //Интерфейсные элементы
+import { useComponentDataSource } from "../components_hooks"; //Хуки для данных
+import { DATA_SOURCE_SHAPE } from "../editors_common"; //Общие объекты компонентов
+import { COMPONENT_MESSAGE_TYPE, COMPONENT_MESSAGES, ComponentInlineMessage } from "../views_common"; //Общие компоненты представлений
+import "../../panels_editor.css"; //Стили редактора
+
+//---------
+//Константы
+//---------
+
+//Иконка компонента
+const COMPONENT_ICON = "speed";
+
+//Наименование компонента
+const COMPONENT_NAME = "Индикатор";
+
+//Типовые цвета
+const COLOR = {
+ OK: "#00ffaaa0",
+ OK_CONTR: "white",
+ ERR: "#ff0000a0",
+ ERR_CONTR: "white",
+ WARN: "orange",
+ WARN_CONTR: "white",
+ UNDEFINED: "#dcdcdca0",
+ UNDEFINED_CONTR: "black",
+ INACTIVE: "#A9A9A9",
+ INACTIVE_CONTR: "black"
+};
+
+//Состояния
+const INDICATOR_STATE = {
+ UNDEFINED: "UNDEFINED",
+ OK: "OK",
+ ERR: "ERR",
+ WARN: "WARN"
+};
+
+//Цвета заливки
+const BG_COLOR = {
+ [INDICATOR_STATE.OK]: COLOR.OK,
+ [INDICATOR_STATE.ERR]: COLOR.ERR,
+ [INDICATOR_STATE.WARN]: COLOR.WARN
+};
+
+//Цвета иконок
+const ICON_COLOR = {
+ [INDICATOR_STATE.OK]: COLOR.OK_CONTR,
+ [INDICATOR_STATE.ERR]: COLOR.ERR_CONTR,
+ [INDICATOR_STATE.WARN]: COLOR.WARN_CONTR
+};
+
+//Стили
+const STYLES = {
+ CONTAINER: state => ({
+ ...(BG_COLOR[state] ? { backgroundColor: BG_COLOR[state] } : {}),
+ height: "100%",
+ display: "flex",
+ flexDirection: "column",
+ justifyContent: "center",
+ alignItems: "center",
+ overflow: "hidden",
+ padding: "10px"
+ }),
+ STACK: { padding: "10px", width: "100%" },
+ ICON: state => ({ fontSize: "100px", ...(ICON_COLOR[state] ? { color: ICON_COLOR[state] } : {}) })
+};
+
+//-----------
+//Тело модуля
+//-----------
+
+//Индикатор (представление)
+const Indicator = ({ dataSource = null, values = {} } = {}) => {
+ //Собственное состояние - данные
+ const [data, error] = useComponentDataSource({ dataSource, values });
+
+ //Флаг настроенности индикатора
+ const haveConfing = dataSource?.stored ? true : false;
+
+ //Флаг наличия данных
+ const haveData = data?.init === true && !error ? true : false;
+
+ //Данные индикатора
+ const indicator = data?.XINDICATOR || {};
+
+ //Формирование представления
+ return (
+
+ {haveConfing && haveData ? (
+
+
+ {[undefined, null, ""].includes(indicator.value) ? "н.д." : indicator.value}
+ {indicator.caption}
+
+ {indicator.icon ? {indicator.icon} : null}
+
+ ) : (
+
+ )}
+
+ );
+};
+
+//Контроль свойств компонента - Индикатор (представление)
+Indicator.propTypes = {
+ dataSource: DATA_SOURCE_SHAPE,
+ values: PropTypes.object
+};
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+export default Indicator;
diff --git a/app/panels/panels_editor/components/table/editor.js b/app/panels/panels_editor/components/table/editor.js
new file mode 100644
index 0000000..d0dba28
--- /dev/null
+++ b/app/panels/panels_editor/components/table/editor.js
@@ -0,0 +1,56 @@
+/*
+ Парус 8 - Панели мониторинга - Редактор панелей
+ Компоненты: Таблица (редактор настроек)
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+import React, { useEffect, useState } from "react"; //Классы React
+import PropTypes from "prop-types"; //Контроль свойств компонента
+import { DATA_SOURCE_SHAPE, DataSource, EditorBox, EditorSubHeader } from "../editors_common"; //Общие компоненты редакторов
+import "../../panels_editor.css"; //Стили редактора
+
+//-----------
+//Тело модуля
+//-----------
+
+//Таблица (редактор настроек)
+const TableEditor = ({ id, dataSource = null, valueProviders = {}, onSettingsChange = null } = {}) => {
+ //Собственное состояние - текущие настройки
+ const [settings, setSettings] = useState(null);
+
+ //При изменении компонента
+ useEffect(() => {
+ settings?.id != id && setSettings({ id, dataSource });
+ }, [settings, id, dataSource]);
+
+ //При сохранении изменений элемента
+ const handleDataSourceChange = dataSource => setSettings(pv => ({ ...pv, dataSource: { ...dataSource } }));
+
+ //При сохранении настроек
+ const handleSave = (closeEditor = false) => onSettingsChange && onSettingsChange({ id, settings, closeEditor });
+
+ //Формирование представления
+ return (
+
+
+
+
+ );
+};
+
+//Контроль свойств компонента - Таблица (редактор настроек)
+TableEditor.propTypes = {
+ id: PropTypes.string.isRequired,
+ dataSource: DATA_SOURCE_SHAPE,
+ valueProviders: PropTypes.object,
+ onSettingsChange: PropTypes.func
+};
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+export default TableEditor;
diff --git a/app/panels/panels_editor/components/table/view.js b/app/panels/panels_editor/components/table/view.js
new file mode 100644
index 0000000..4b86d1c
--- /dev/null
+++ b/app/panels/panels_editor/components/table/view.js
@@ -0,0 +1,96 @@
+/*
+ Парус 8 - Панели мониторинга - Редактор панелей
+ Компоненты: Таблица (представление)
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+import React from "react"; //Классы React
+import PropTypes from "prop-types"; //Контроль свойств компонента
+import { Paper } from "@mui/material"; //Интерфейсные элементы
+import { APP_STYLES } from "../../../../../app.styles"; //Типовые стили
+import { P8PDataGrid } from "../../../../components/p8p_data_grid"; //Таблица данных
+import { P8P_DATA_GRID_CONFIG_PROPS } from "../../../../config_wrapper"; //Подключение компонентов к настройкам приложения
+import { useComponentDataSource } from "../components_hooks"; //Хуки для данных
+import { DATA_SOURCE_SHAPE } from "../editors_common"; //Общие объекты компонентов
+import { COMPONENT_MESSAGE_TYPE, COMPONENT_MESSAGES, ComponentInlineMessage } from "../views_common"; //Общие компоненты представлений
+import "../../panels_editor.css"; //Стили редактора
+
+//---------
+//Константы
+//---------
+
+//Иконка компонента
+const COMPONENT_ICON = "table_view";
+
+//Наименование компонента
+const COMPONENT_NAME = "Таблица";
+
+//Стили
+const STYLES = {
+ CONTAINER: { display: "flex", height: "100%", overflow: "hidden" },
+ DATA_GRID: { width: "100%" },
+ DATA_GRID_CONTAINER: {
+ height: `calc(100%)`,
+ ...APP_STYLES.SCROLL
+ }
+};
+
+//-----------
+//Тело модуля
+//-----------
+
+//Таблица (представление)
+const Table = ({ dataSource = null, values = {} } = {}) => {
+ //Собственное состояние - данные
+ const [data, error] = useComponentDataSource({ dataSource, values });
+
+ //Флаг настроенности таблицы
+ const haveConfing = dataSource?.stored ? true : false;
+
+ //Флаг наличия данных
+ const haveData = data?.init === true && !error ? true : false;
+
+ //Данные таблицы
+ const dataGrid = data?.XDATA_GRID || {};
+
+ //Формирование представления
+ return (
+
+ {haveConfing && haveData ? (
+
+ ) : (
+
+ )}
+
+ );
+};
+
+//Контроль свойств компонента - Таблица (представление)
+Table.propTypes = {
+ dataSource: DATA_SOURCE_SHAPE,
+ values: PropTypes.object
+};
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+export default Table;
diff --git a/app/panels/panels_editor/components/views_common.js b/app/panels/panels_editor/components/views_common.js
new file mode 100644
index 0000000..085ed0c
--- /dev/null
+++ b/app/panels/panels_editor/components/views_common.js
@@ -0,0 +1,67 @@
+/*
+ Парус 8 - Панели мониторинга - Редактор панелей
+ Общие компоненты представлений элементов панели
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+import React from "react"; //Классы React
+import PropTypes from "prop-types"; //Контроль свойств компонента
+import { Stack, Icon, Typography } from "@mui/material"; //Интерфейсные элементы
+import { TEXTS } from "../../../../app.text"; //Общие текстовые ресурсы
+
+//---------
+//Константы
+//---------
+
+//Типы сообщений
+const COMPONENT_MESSAGE_TYPE = {
+ COMMON: "COMMON",
+ ERROR: "ERROR"
+};
+
+//Типовые сообщения
+const COMPONENT_MESSAGES = {
+ NO_DATA_FOUND: TEXTS.NO_DATA_FOUND,
+ NO_SETTINGS: "Настройте компонент"
+};
+
+//-----------
+//Тело модуля
+//-----------
+
+//Информационное сообщение внутри компонента
+const ComponentInlineMessage = ({ icon, name, message, type = COMPONENT_MESSAGE_TYPE.COMMON }) => {
+ //Формирование представления
+ return (
+
+
+ {icon && {icon}}
+ {name && (
+
+ {name}
+
+ )}
+
+
+ {message}
+
+
+ );
+};
+
+//Контроль свойств - Информационное сообщение внутри компонента
+ComponentInlineMessage.propTypes = {
+ icon: PropTypes.string,
+ name: PropTypes.string,
+ message: PropTypes.string.isRequired,
+ type: PropTypes.oneOf(Object.values(COMPONENT_MESSAGE_TYPE))
+};
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+export { COMPONENT_MESSAGE_TYPE, COMPONENT_MESSAGES, ComponentInlineMessage };
diff --git a/app/panels/panels_editor/index.js b/app/panels/panels_editor/index.js
new file mode 100644
index 0000000..9517226
--- /dev/null
+++ b/app/panels/panels_editor/index.js
@@ -0,0 +1,16 @@
+/*
+ Парус 8 - Панели мониторинга - Редактор панелей
+ Редактор панелей: точка входа
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+import { PanelsEditor } from "./panels_editor"; //Корневая панель редактора
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+export const RootClass = PanelsEditor;
diff --git a/app/panels/panels_editor/layout_item.js b/app/panels/panels_editor/layout_item.js
new file mode 100644
index 0000000..db6f650
--- /dev/null
+++ b/app/panels/panels_editor/layout_item.js
@@ -0,0 +1,87 @@
+/*
+ Парус 8 - Панели мониторинга - Редактор панелей
+ Элемент макета
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+import React from "react"; //Классы React
+import PropTypes from "prop-types"; //Контроль свойств компонента
+import { IconButton, Icon, Stack } from "@mui/material"; //Интерфейсные элементы
+import "./panels_editor.css"; //Кастомные стили
+
+//---------
+//Константы
+//---------
+
+//Стили
+const STYLES = {
+ CONTAINER: selected => ({ zIndex: 1100, ...(selected ? { border: "2px dotted green" } : {}) }),
+ STACK_TOOLS: { position: "absolute", zIndex: 1200, height: "100%", backgroundColor: "#c0c0c07f" }
+};
+
+//-----------
+//Тело модуля
+//-----------
+
+//Элемент макета
+// eslint-disable-next-line react/display-name
+const LayoutItem = React.forwardRef(
+ (
+ { style, className, onMouseDown, onMouseUp, onTouchEnd, children, onSettingsClick, onDeleteClick, item, editMode = false, selected = false },
+ ref
+ ) => {
+ //При нажатии на настройки
+ const handleSettingsClick = () => onSettingsClick && onSettingsClick(item.i);
+
+ //При нажатии на удаление
+ const handleDeleteClick = () => onDeleteClick && onDeleteClick(item.i);
+
+ //Формирование представления
+ return (
+
+ {editMode && (
+
+
+ settings
+
+
+ delete
+
+
+ )}
+ {children}
+
+ );
+ }
+);
+
+//Контроль свойств компонента - элемент макета
+LayoutItem.propTypes = {
+ style: PropTypes.object,
+ className: PropTypes.string,
+ onMouseDown: PropTypes.func,
+ onMouseUp: PropTypes.func,
+ onTouchEnd: PropTypes.func,
+ children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
+ onSettingsClick: PropTypes.func,
+ onDeleteClick: PropTypes.func,
+ item: PropTypes.object.isRequired,
+ editMode: PropTypes.bool,
+ selected: PropTypes.bool
+};
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+export { LayoutItem };
diff --git a/app/panels/panels_editor/panels_editor.css b/app/panels/panels_editor/panels_editor.css
new file mode 100644
index 0000000..0729e89
--- /dev/null
+++ b/app/panels/panels_editor/panels_editor.css
@@ -0,0 +1,40 @@
+:root {
+ --border-color: #dee2e6;
+ --layout-bg: #ffffff;
+}
+
+.layout {
+ background-color: var(--layout-bg);
+}
+
+.layout-item__container {
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+}
+
+.component-editor__wrap {
+}
+
+.component-editor__container {
+ padding: 10px;
+}
+
+.component-editor__divider {
+ padding-top: 20px;
+}
+
+.component-view__wrap {
+ height: 100%;
+}
+
+.component-view__container {
+ height: 100%;
+ overflow: auto;
+ padding: 10px;
+}
+
+.component-view__container__empty {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
diff --git a/app/panels/panels_editor/panels_editor.js b/app/panels/panels_editor/panels_editor.js
new file mode 100644
index 0000000..92ca84d
--- /dev/null
+++ b/app/panels/panels_editor/panels_editor.js
@@ -0,0 +1,239 @@
+/*
+ Парус 8 - Панели мониторинга - Редактор панелей
+ Корневой компонент
+*/
+
+//TODO: Подчистка values после обновления имени элемента формы (и т.п.), удаления элемента формы
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+import React, { useEffect, useState } from "react"; //Классы React
+import { Responsive, WidthProvider } from "react-grid-layout"; //Адаптивный макет
+import { Box, Grid, Stack, Menu, MenuItem, IconButton, Icon, Fab } from "@mui/material"; //Интерфейсные элементы
+import { APP_BAR_HEIGHT } from "../../components/p8p_app_workspace"; //Рабочая область приложения
+import { genGUID } from "../../core/utils"; //Общие вспомогательные функции
+import { LayoutItem } from "./layout_item"; //Элемент макета
+import { ComponentView } from "./component_view"; //Представление компонента панели
+import { ComponentEditor } from "./component_editor"; //Редактор свойств компонента панели
+import { COMPONETNS } from "./components/components"; //Описание доступных компонентов
+import "react-grid-layout/css/styles.css"; //Стили для адаптивного макета
+import "react-resizable/css/styles.css"; //Стили для адаптивного макета
+
+//---------
+//Константы
+//---------
+
+//Стили
+const STYLES = {
+ CONTAINER: { display: "flex" },
+ GRID_CONTAINER: { height: `calc(100vh - ${APP_BAR_HEIGHT})` },
+ GRID_ITEM_INSPECTOR: { backgroundColor: "#e9ecef" },
+ FAB_EDIT: { position: "absolute", top: 12, right: 12, zIndex: 2000 }
+};
+
+//Начальное состояние размера макета
+const INITIAL_BREAKPOINT = "lg";
+
+//Начальное состояние макета
+const INITIAL_LAYOUTS = {
+ [INITIAL_BREAKPOINT]: []
+};
+
+//-----------
+//Тело модуля
+//-----------
+
+//Обёрдка для динамического макета
+const ResponsiveGridLayout = WidthProvider(Responsive);
+
+//Корневой компонент редактора панелей
+const PanelsEditor = () => {
+ //Собственное состояние
+ const [components, setComponents] = useState({});
+ const [valueProviders, setValueProviders] = useState({});
+ const [layouts, setLayouts] = useState(INITIAL_LAYOUTS);
+ const [breakpoint, setBreakpoint] = useState(INITIAL_BREAKPOINT);
+ const [editMode, setEditMode] = useState(true);
+ const [editComponent, setEditComponent] = useState(null);
+ const [addMenuAnchorEl, setAddMenuAnchorEl] = useState(null);
+
+ //Добвление компонента в макет
+ const addComponent = component => {
+ const id = genGUID();
+ setLayouts(pv => ({ ...pv, [breakpoint]: [...pv[breakpoint], { i: id, x: 0, y: 0, w: 4, h: 10 }] }));
+ setComponents(pv => ({ ...pv, [id]: { ...component } }));
+ };
+
+ //Удаление компонента из макета
+ const deleteComponent = id => {
+ setLayouts(pv => ({ ...pv, [breakpoint]: layouts[breakpoint].filter(item => item.i !== id) }));
+ setComponents(pv => ({ ...pv, [id]: { ...pv[id], deleted: true } }));
+ if (valueProviders[id]) {
+ const vPTmp = { ...valueProviders };
+ delete vPTmp[id];
+ setValueProviders(vPTmp);
+ }
+ editComponent === id && closeComponentSettingsEditor();
+ };
+
+ //Включение/выключение режима редиктирования
+ const toggleEditMode = () => setEditMode(!editMode);
+
+ //Открытие редактора настроек компонента
+ const openComponentSettingsEditor = id => setEditComponent(id);
+
+ //Закрытие реактора настроек компонента
+ const closeComponentSettingsEditor = () => setEditComponent(null);
+
+ //Открытие/сокрытие меню добавления
+ const toggleAddMenu = target => setAddMenuAnchorEl(target instanceof Element ? target : null);
+
+ //При изменении размера холста
+ const handleBreakpointChange = breakpoint => setBreakpoint(breakpoint);
+
+ //При изменении состояния макета
+ const handleLayoutChange = (currentLayout, layouts) => setLayouts(layouts);
+
+ //При нажатии на кнопку добалвения
+ const handleAddClick = e => toggleAddMenu(e.currentTarget);
+
+ //При выборе элемента меню добавления
+ const handleAddMenuItemClick = component => {
+ toggleAddMenu();
+ addComponent(component);
+ };
+
+ //При изменении значений в компоненте
+ const handleComponentValuesChange = (id, values) => setValueProviders(pv => ({ ...pv, [id]: { ...values } }));
+
+ //При нажатии на настройки компонента
+ const handleComponentSettingsClick = id => (editComponent === id ? closeComponentSettingsEditor() : openComponentSettingsEditor(id));
+
+ //При изменении настроек компонента
+ const handleComponentSettingsChange = ({ id = null, settings = {}, providedValues = [], closeEditor = false } = {}) => {
+ if (id && components[id]) {
+ const providedValuesInit = providedValues.reduce((res, providedValue) => ({ ...res, [providedValue]: undefined }), {});
+ if (valueProviders[id]) {
+ const vPTmp = { ...valueProviders[id] };
+ Object.keys(valueProviders[id]).forEach(key => !providedValues.includes(key) && delete vPTmp[key]);
+ setValueProviders(pv => ({ ...pv, [id]: { ...providedValuesInit, ...vPTmp } }));
+ } else setValueProviders(pv => ({ ...pv, [id]: providedValuesInit }));
+ setComponents(pv => ({ ...pv, [editComponent]: { ...pv[editComponent], settings: { ...settings } } }));
+ if (closeEditor === true) closeComponentSettingsEditor();
+ }
+ };
+
+ //При удалении компоненета
+ const handleComponentDeleteClick = id => deleteComponent(id);
+
+ //При подключении к странице
+ useEffect(() => {
+ //addComponent(COMPONETNS[0]);
+ //addComponent(COMPONETNS[3]);
+ //addComponent(COMPONETNS[4]);
+ //addComponent(COMPONETNS[1]);
+ //addComponent(COMPONETNS[2]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ //Текущие значения панели
+ const values = Object.keys(valueProviders).reduce((res, key) => ({ ...res, ...valueProviders[key] }), {});
+
+ //Меню добавления
+ const addMenu = (
+
+ );
+
+ //Кнопка редактирования
+ const editButton = !editMode && (
+
+ edit
+
+ );
+
+ //Панель инструмментов
+ const toolBar = (
+
+
+ play_arrow
+
+
+ add
+
+
+ );
+
+ //Генерация содержимого
+ return (
+
+ {editButton}
+ {addMenu}
+
+
+
+ {layouts[breakpoint].map(item => (
+
+
+
+ ))}
+
+
+ {editMode && (
+
+ {toolBar}
+ {editComponent && (
+ <>
+
+ >
+ )}
+
+ )}
+
+
+ );
+};
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+export { PanelsEditor };
diff --git a/db/PKG_P8PANELS_EDITOR.pck b/db/PKG_P8PANELS_EDITOR.pck
new file mode 100644
index 0000000..0a7a734
--- /dev/null
+++ b/db/PKG_P8PANELS_EDITOR.pck
@@ -0,0 +1,127 @@
+create or replace package PKG_P8PANELS_EDITOR as
+
+ /* Список аргументов пользовательской процедуры */
+ procedure USERPROCS_DESC
+ (
+ SCODE in varchar2, -- Мнемокод пользовательской процедуры
+ COUT out clob -- Сериализованный список аргументов
+ );
+
+end PKG_P8PANELS_EDITOR;
+/
+create or replace package body PKG_P8PANELS_EDITOR as
+
+ /* Описание пользовательской процедуры */
+ procedure USERPROCS_DESC
+ (
+ SCODE in varchar2, -- Мнемокод пользовательской процедуры
+ COUT out clob -- Сериализованный список аргументов
+ )
+ is
+ SRESP_ARG PKG_STD.TSTRING; -- Имя выходного визуализируемого параметра
+ begin
+ /* Обращаемся к процедуре */
+ for C in (select T.RN NRN,
+ T.PROCNAME SPROC_NAME,
+ T.PROCTYPE NPROC_TYPE
+ from USERPROCS T
+ where T.CODE = SCODE)
+ loop
+ /* Проверим возможность использования ПП в качестве источника данных */
+ if (C.NPROC_TYPE <> 0) then
+ P_EXCEPTION(0,
+ 'Пользовательская процедура "%s" не может быть использована в качестве источника данных: должна иметь тип "Хранимая процедура".',
+ SCODE);
+ end if;
+ if (C.SPROC_NAME is null) then
+ P_EXCEPTION(0,
+ 'Пользовательская процедура "%s" не может быть использована в качестве источника данных: не указана хранимая процедура.',
+ SCODE);
+ end if;
+ /* Начинаем формирование XML */
+ PKG_XFAST.PROLOGUE(ITYPE => PKG_XFAST.CONTENT_);
+ /* Открываем корень */
+ PKG_XFAST.DOWN_NODE(SNAME => 'XDATA');
+ /* Открываем описание процедуры */
+ PKG_XFAST.DOWN_NODE(SNAME => 'XUSERPROC');
+ /* Обходим параметры */
+ for P in (select T.PARAMTYPE NTYPE,
+ T.PARAMNAME SNAME,
+ T.NAME SCAPTION,
+ T.DATATYPE NDATA_TYPE,
+ case T.DATATYPE
+ when 0 then
+ 'STR'
+ when 1 then
+ 'NUMB'
+ when 2 then
+ 'DATE'
+ else
+ null
+ end SDATA_TYPE,
+ T.MANDATORY NREQ,
+ T.VISUALIZE NVISUALIZE
+ from USERPROCSPARAMS T
+ where T.PRN = C.NRN
+ order by T.POSITION)
+ loop
+ /* В результирующий список забираем только входные поддерживаемого типа */
+ if ((P.NTYPE = 0) and (P.SDATA_TYPE is not null)) then
+ /* Открываем описание аргумента */
+ PKG_XFAST.DOWN_NODE(SNAME => 'arguments');
+ /* Описываем аргумент */
+ PKG_XFAST.ATTR(SNAME => 'name', SVALUE => P.SNAME);
+ PKG_XFAST.ATTR(SNAME => 'caption', SVALUE => P.SCAPTION);
+ PKG_XFAST.ATTR(SNAME => 'dataType', SVALUE => P.SDATA_TYPE);
+ PKG_XFAST.ATTR(SNAME => 'req',
+ BVALUE => case P.NREQ
+ when 1 then
+ true
+ else
+ false
+ end);
+ /* Закрываем описание аргумента */
+ PKG_XFAST.UP();
+ end if;
+ /* Если встретился визуализируемый параметр типа CLOB */
+ if ((P.NVISUALIZE = 1) and (P.NDATA_TYPE = 4)) then
+ if (SRESP_ARG is null) then
+ SRESP_ARG := P.SNAME;
+ else
+ /* Это уже второй такой - ошибка */
+ P_EXCEPTION(0,
+ 'Пользовательская процедура "%s" не может быть использована в качестве источника данных: имеет более одного выходного параметра типа "Текстовые данные" с признаком "Визуализировать после выполнения".',
+ SCODE);
+ end if;
+ end if;
+ end loop;
+ /* Сформируем описание хранимой процедуры */
+ PKG_XFAST.DOWN_NODE(SNAME => 'stored');
+ PKG_XFAST.ATTR(SNAME => 'name', SVALUE => C.SPROC_NAME);
+ PKG_XFAST.ATTR(SNAME => 'respArg', SVALUE => SRESP_ARG);
+ PKG_XFAST.UP();
+ /* Закрываем описание процедуры */
+ PKG_XFAST.UP();
+ /* Закрываем описание корня */
+ PKG_XFAST.UP();
+ /* Сериализуем */
+ COUT := PKG_XFAST.SERIALIZE_TO_CLOB();
+ /* Завершаем формирование XML */
+ PKG_XFAST.EPILOGUE();
+ end loop;
+ /* Если ничего не нашли */
+ if (COUT is null) then
+ P_EXCEPTION(0,
+ 'Пользовательская процедура "%s" не определена.',
+ COALESCE(SCODE, '<НЕ УКАЗАНА>'));
+ end if;
+ /* Проверим возможность использования ПП в качестве источника данных - должен быть выходной визуализируемый параметр типа CLOB */
+ if (SRESP_ARG is null) then
+ P_EXCEPTION(0,
+ 'Пользовательская процедура "%s" не может быть использована в качестве источника данных: должна иметь выходной параметр типа "Текстовые данные" с признаком "Визуализировать после выполнения".',
+ SCODE);
+ end if;
+ end USERPROCS_DESC;
+
+end PKG_P8PANELS_EDITOR;
+/