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 ( + + {title} + {children} + + + + + + ); +}; + +//Контроль свойств компонента - диалог настройки +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 && ( + + {values.map((value, i) => ( + handleArgumentLinkClick(value)}> + {value} + + ))} + + ); + + //Формирование представления + 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 = ( + + {COMPONETNS.map((comp, i) => ( + handleAddMenuItemClick(comp)}> + {comp.name} + + ))} + + ); + + //Кнопка редактирования + 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; +/