/* Парус 8 - Панели мониторинга - Редактор панелей Компоненты: Хуки компонентов */ //--------------------- //Подключение библиотек //--------------------- import { useState, useEffect, useContext, useCallback, useLayoutEffect } from "react"; //Классы React import { BackEndСtx } from "../../../context/backend"; //Контекст взаимодействия с сервером import { MessagingСtx } from "../../../context/messaging"; //Контекст сообщений import { object2Base64XML, genUID, xml2JSON } from "../../../core/utils"; //Вспомогательные функции import { exportXMLFile } from "../layouts"; //Дополнительная разметка и вёрстка клиентских элементов import { P8P_COMPONENT_SETTINGS_PATHS } from "../../../components/editors/p8p_component_settings"; //Дополнительные настройки источников import { getActionsVariables } from "../../../components/editors/p8p_component_action/util"; //Вспомогательный функционал действий компонентов import { P8P_CA_TYPE } from "../../../components/editors/p8p_component_action/common"; //Общие ресурсы действий //--------- //Константы //--------- //Начальное состояние размера макета const INITIAL_BREAKPOINT = "lg"; //Начальное состояние макета const INITIAL_LAYOUTS = { [INITIAL_BREAKPOINT]: [] }; //--------- //Константы расчета высоты строки ResponsiveGridLayout //--------- //Структура параметров для расчета высоты строки ResponsiveGridLayout const GRID_LAYOUT_PARAMS = { //Высота главного меню (px) APP_BAR_HEIGHT: 64, //Дополнительный отступ (px) SAFE_OFFSET: 20, //Максимальное количество строк (число) MAX_ROWS: 68, //Высота отступа (px) MARGIN: 10 }; //------------------------------------ //Вспомогательные функции и компоненты //------------------------------------ //Проверка на массив загружаемых данных панели const isArrayPanelDesc = (name, jPath) => ["items", "arguments", "conditions", "actions", "dependencies", "inputParams"].includes(name) || /(.*)XLAYOUTS\.[^.]*$/.test(jPath); //Обработка значений тэгов загружаемых данных панели const tagValueProcessorPanelDesc = (name, val, jPath) => ["condValue", "resValue", "description"].includes(name) ? undefined : /(.*)dataSource.arguments.value$/.test(jPath) || /(.*)XVALUE_PROVIDERS(.*)$/.test(jPath) ? undefined : val; //Конвертация серверного описания компонентов в данные для редактора панелей const convertServerData2Components = components => { //Корректировка информации о действия (Для типа "setVariable" значение ключа "params" - обязательно массив, в иных случаях - объект) const correctionActions = actions => { return actions.reduce( (prevActions, action) => [ ...prevActions, { ...(action.type === P8P_CA_TYPE.setVariable.code && !Array.isArray(action.params) ? { ...action, params: [{ ...action.params }] } : { ...action }) } ], [] ); }; //Форматируем устанавливая пустой объект для dataSource, если он пуст return Object.keys(components).reduce( (prev, cur) => ({ ...prev, [cur]: { ...components[cur], settings: { ...components[cur].settings, //dataSource - обязательно объект ...(components[cur].settings?.dataSource ? { dataSource: components[cur].settings.dataSource || {} } : {}), //actions - требуют корректировки ...(components[cur].settings?.actions ? { actions: correctionActions(components[cur].settings.actions) } : {}) } } }), {} ); }; //Конвертация серверного описания проводников значений в данные для редактора панелей const convertServerData2ValueProviders = valueProviders => { //Форматируем инициализируя dependencies, если требуется return Object.keys(valueProviders).reduce( (prev, cur) => ({ ...prev, [cur]: { ...valueProviders[cur], dependencies: valueProviders[cur].dependencies || [] } }), {} ); }; //Конвертация серверного описания панели в данные для редактора панелей const serverPanelData2PanelDesc = (components, valueProviders, layouts, breakpoint) => { //Возвращаем информацию о панеле с учетом конвертаций return { components: convertServerData2Components(components), valueProviders: convertServerData2ValueProviders(valueProviders), layouts: layouts[breakpoint] ? { [breakpoint]: [...layouts[breakpoint]] } : INITIAL_LAYOUTS, breakpoint: breakpoint }; }; //Считывание общего списка зависимостей от проводника значений const getValueProvidersLinks = (componentPath, settings) => { //Если это индикатор/график/таблица if ([P8P_COMPONENT_SETTINGS_PATHS.INDICATOR, P8P_COMPONENT_SETTINGS_PATHS.CHART, P8P_COMPONENT_SETTINGS_PATHS.TABLE].includes(componentPath)) { //Собираем зависимости из настройки источника let argumentsSources = Array.isArray(settings?.dataSource?.arguments) ? settings.dataSource.arguments.reduce((prev, cur) => (cur.valueSource ? [...prev, cur.valueSource] : [...prev]), []) : []; //Собираем зависимости из параметров действий let actionsSources = Array.isArray(settings?.actions) ? getActionsVariables(settings.actions) : []; //Возвращаем зависимости компонента return [...new Set([...argumentsSources, ...actionsSources])]; } //Если это форма if (P8P_COMPONENT_SETTINGS_PATHS.FORM === componentPath) { //Собираем зависимости из элементов формы let items = Array.isArray(settings?.items) ? settings.items.map(item => item.name) : []; //Возвращаем зависимости компонента return [...new Set(items)]; } }; //----------- //Тело модуля //----------- //Отложенная загрузка модуля компонента (как альтернативу можно применять 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 usePanel = () => { //Собственное состояние - рабочая панель const [panel, setPanel] = useState(); //Собственное состояние - имя рабочей панели const [panelName, setPanelName] = useState(null); //Собственное состояние - наличие изменений рабочей панели const [isPanelChanged, setIsPanelChanged] = useState(false); //Собственное состояние - возможность редактирования const [isEditAvaliable, setIsEditAvaliable] = useState(true); //Собственное состояние - режим редактирования const [editMode, setEditMode] = useState(true); //Подключение к контексту взаимодействия с сервером const { executeStored } = useContext(BackEndСtx); //При необходимости загрузки информации о панели const loadPanel = async panel => { //Считываем информацию с сервера const res = await executeStored({ stored: "PKG_P8PANELS_PE.PANEL_ATTRS_GET_BY_CODE", args: { SCODE: panel }, respArg: "COUT", loader: true }); //Устанавливаем рабочую панель setPanel(res.XPANEL_ATTRS.rn); //Устанавливаем наименование рабочей панели setPanelName(res.XPANEL_ATTRS.name); //Сбрасываем наличие изменений рабочей панели setIsPanelChanged(false); //Загружаемую панель запрещено редактировать setIsEditAvaliable(false); setEditMode(false); }; //При выборе панели const selectPanel = (panel, panelName, isPanelEditAvaliable) => { //Обновляем информацию о панели setPanel(panel); setPanelName(panelName); setIsEditAvaliable(isPanelEditAvaliable); setEditMode(isPanelEditAvaliable); setIsPanelChanged(false); }; //При закрытии панели const closePanel = () => { setPanel(null); setPanelName(null); setIsPanelChanged(false); }; //При установке признака изменений панели const setPanelChanged = isChanged => setIsPanelChanged(isChanged); //При установке признака режима редактирования const changeEditMode = isEditMode => setEditMode(isEditMode); //Возвращаем интерфейс хука return [panel, panelName, editMode, isEditAvaliable, isPanelChanged, loadPanel, selectPanel, closePanel, changeEditMode, setPanelChanged]; }; //Работа с менеджером панелей const usePanelManager = () => { //Собственное состояние - флаг необходимости обновления const [refresh, setRefresh] = useState(true); //Собственное состояние - данные const [data, setData] = useState(null); //Подключение к контексту взаимодействия с сервером const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndСtx); //Добавление панели const insertPanel = useCallback( async (code, name) => { await executeStored({ stored: "PKG_P8PANELS_PE.PANEL_INSERT", args: { SCODE: code, SNAME: name }, loader: false }); setRefresh(true); }, [executeStored] ); //Изменение панели const updatePanel = useCallback( async (query, code, name) => { await executeStored({ stored: "PKG_P8PANELS_PE.PANEL_UPDATE", args: { NRN: query, SCODE: code, SNAME: name }, loader: false }); setRefresh(true); }, [executeStored] ); //Удаление панели const deletePanel = useCallback( async query => { await executeStored({ stored: "PKG_P8PANELS_PE.PANEL_DELETE", args: { NRN: query }, loader: false }); setRefresh(true); }, [executeStored] ); //Установка флага готовности панели const setPanelReady = useCallback( async (query, ready) => { await executeStored({ stored: "PKG_P8PANELS_PE.PANEL_READY_SET", args: { NRN: query, NREADY: ready }, loader: false }); setRefresh(true); }, [executeStored] ); //Установка флага публичности панели const setPanelPbl = useCallback( async (query, pbl) => { await executeStored({ stored: "PKG_P8PANELS_PE.PANEL_PBL_SET", args: { NRN: query, NPBL: pbl }, loader: false }); setRefresh(true); }, [executeStored] ); //Импорт новой панели const importPanel = useCallback( async fileData => { await executeStored({ stored: "PKG_P8PANELS_PE.PANEL_IMPORT", args: { CPANEL: { //Форматируем данные документа в base64 VALUE: btoa(unescape(encodeURIComponent(fileData))), SDATA_TYPE: SERV_DATA_TYPE_CLOB } }, loader: true }); setRefresh(true); }, [SERV_DATA_TYPE_CLOB, executeStored] ); //При необходимости получить/обновить данные useEffect(() => { //Загрузка данных с сервера const loadData = async () => { try { const data = await executeStored({ stored: "PKG_P8PANELS_PE.PANEL_LIST", respArg: "COUT", isArray: name => ["XPANEL"].includes(name), attributeValueProcessor: (name, val) => (["code", "name"].includes(name) ? undefined : val), loader: true }); setData(data?.XPANELS?.XPANEL || []); } finally { setRefresh(false); } }; //Если надо обновить if (refresh) //Получим данные loadData(); }, [refresh, executeStored]); //Возвращаем интерфейс хука return [data, insertPanel, updatePanel, deletePanel, setPanelReady, setPanelPbl, importPanel]; }; //Работа с содержимым панели const usePanelDesc = panel => { //Собственное состояние - флаг инициализированности const [isInit, setInit] = useState(false); //Собственное состояние - флаг необходимости обновления const [refresh, setRefresh] = useState(true); //Собственное состояние - данные const [data, setData] = useState({ components: {}, valueProviders: {}, layouts: INITIAL_LAYOUTS, breakpoint: INITIAL_BREAKPOINT }); //Подключение к контексту взаимодействия с сервером const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndСtx); //Подключение к контексту сообщений const { showMsgErr } = useContext(MessagingСtx); //Считывание базовой информации о панели const getPanelInfo = useCallback(async () => { //Считываем информацию с сервера const res = await executeStored({ stored: "PKG_P8PANELS_PE.PANEL_ATTRS_GET", args: { NRN: panel }, respArg: "COUT", loader: true }); //Забираем все без RN // eslint-disable-next-line no-unused-vars const { rn, ...panelAttrs } = res.XPANEL_ATTRS; //Возвращаем результат return panelAttrs; }, [executeStored, panel]); //Формирования данных панели для выгрузки const makeXMLPanelDesc = useCallback( async (includePanelInfo = false, isBase64 = true) => { //Считываем информацию о выгружаемой панели const panelInfo = includePanelInfo ? await getPanelInfo() : {}; //Сформируем данные в формат XML const xmlData = object2Base64XML( { XPANEL: { ...(includePanelInfo ? { XPANEL_INFO: panelInfo } : {}), XCOMPONENTS: data.components, XVALUE_PROVIDERS: data.valueProviders, XLAYOUTS: data.layouts, XOPTIONS: { breakpoint: data.breakpoint } } }, { suppressEmptyNode: false } ); //Возвращаем данные в формате XML (формат исходит от признака) return isBase64 ? xmlData : decodeURIComponent(escape(atob(xmlData))); }, [data.breakpoint, data.components, data.layouts, data.valueProviders, getPanelInfo] ); //Добавление компонента в макет const addComponent = component => { //Генерируем ID const id = genUID(component.path); //Добавляем компонент и его макет setData(pv => ({ ...pv, components: { ...pv.components, [id]: { ...component, settings: { ...component.settings, id: id } } }, layouts: { ...pv.layouts, [data.breakpoint]: [...pv.layouts[data.breakpoint], { i: id, x: 0, y: 0, w: 4, h: 10 }] } })); }; //Удаление компонента из макета const deleteComponent = id => { //Удаляем все старые зависимости от компонента const newValueProviders = Object.keys(data.valueProviders).reduce( (prev, cur) => ({ ...prev, [cur]: { ...data.valueProviders[cur], dependencies: data.valueProviders[cur].dependencies.filter(el => el !== id) } }), {} ); //Обновляем данные setData(pv => ({ ...pv, layouts: { ...pv.layouts, [data.breakpoint]: data.layouts[data.breakpoint].filter(item => item.i !== id) }, components: { ...pv.components, [id]: { ...pv.components[id], deleted: true } }, valueProviders: { ...newValueProviders } })); }; //Изменение размера холста const breakpointChange = breakpoint => setData(pv => ({ ...pv, breakpoint: breakpoint })); //Изменение состояния макета const layoutsChange = layouts => setData(pv => ({ ...pv, layouts: layouts })); //Изменение значений в компоненте const changeValueProviders = values => { //Считываем проводники const newValueProviders = { ...data.valueProviders }; //Переносим новые значения в проводники Object.keys(values).map(el => (newValueProviders[el].value = values[el])); //Обновляем проводники setData(pv => ({ ...pv, valueProviders: { ...newValueProviders } })); }; //Изменение настроек компонента const changeComponentSettings = (id = null, settings = {}, onChanged = null) => { //Считываем новые зависимости компонента const providedValues = getValueProvidersLinks(data.components[id].path, settings); //Удаляем все старые зависимости от компонента const newValueProviders = Object.keys(data.valueProviders).reduce( (prev, cur) => ({ ...prev, [cur]: { ...data.valueProviders[cur], dependencies: data.valueProviders[cur].dependencies.filter(el => el !== id) } }), {} ); //Добавляем новые зависимости providedValues.map(providerName => newValueProviders[providerName].dependencies.push(id)); //Обновляем данные setData(pv => ({ ...pv, components: { ...pv.components, [id]: { ...pv.components[id], settings: { ...settings } } }, valueProviders: { ...newValueProviders } })); //Выполняем действия после изменения onChanged && onChanged(); }; //Изменение настроек панели const changePanelSettings = valueProviders => { //Обновляем данные setData(pv => ({ ...pv, valueProviders: { ...valueProviders } })); }; //Сохранение описания панели const savePanelDesc = useCallback( async (callBack = null) => { try { await executeStored({ stored: "PKG_P8PANELS_PE.PANEL_DESC_SET", args: { NRN: panel, CPANEL: { VALUE: await makeXMLPanelDesc(), SDATA_TYPE: SERV_DATA_TYPE_CLOB } }, loader: true }); callBack && callBack(false); } catch (e) { callBack && callBack(true); } }, [SERV_DATA_TYPE_CLOB, executeStored, makeXMLPanelDesc, panel] ); //Загрузка описания панели из файла const importPanelDesc = async (fileData, callBack = null) => { //Формируем данные панели const panelData = await xml2JSON({ xmlDoc: fileData, isArray: isArrayPanelDesc, tagValueProcessor: tagValueProcessorPanelDesc }); //Если файл содержит тэг XPANEL if (panelData.XPANEL) { setData( serverPanelData2PanelDesc( panelData.XPANEL?.XCOMPONENTS || {}, panelData.XPANEL?.XVALUE_PROVIDERS || {}, panelData.XPANEL?.XLAYOUTS || {}, panelData.XPANEL?.XOPTIONS?.breakpoint || INITIAL_BREAKPOINT ) ); callBack && callBack(true); } else { showMsgErr("Загружаемые данные не соответствуют формату настройки панели."); callBack && callBack(false); } }; //Выгрузка панели в файл const exportPanelDesc = async panelName => { //Формируем XML-представление панели const xmlPanelDesc = await makeXMLPanelDesc(true, false); //Выгружаем в файл exportXMLFile(xmlPanelDesc, panelName); }; //При необходимости получить/обновить данные useEffect(() => { //Загрузка данных с сервера const loadData = async () => { try { const data = await executeStored({ stored: "PKG_P8PANELS_PE.PANEL_DESC_GET", args: { NRN: panel }, respArg: "COUT", isArray: isArrayPanelDesc, tagValueProcessor: tagValueProcessorPanelDesc, loader: true }); setData( serverPanelData2PanelDesc( data?.XCOMPONENTS || {}, data?.XVALUE_PROVIDERS || {}, data?.XLAYOUTS || {}, data?.XOPTIONS?.breakpoint || INITIAL_BREAKPOINT ) ); setInit(true); } finally { setRefresh(false); } }; //Если надо обновить if (refresh) if (panel) //Если есть для чего получать данные loadData(); //Нет идентификатора запроса - нет данных else setData({ components: {}, valueProviders: {}, layouts: INITIAL_LAYOUTS, breakpoint: INITIAL_BREAKPOINT }); }, [refresh, panel, executeStored]); //При изменении входных свойств - поднимаем флаг обновления useEffect(() => { setInit(false); setRefresh(true); }, [panel]); //Возвращаем интерфейс хука return [ data, isInit, addComponent, deleteComponent, breakpointChange, layoutsChange, changeValueProviders, changeComponentSettings, changePanelSettings, savePanelDesc, importPanelDesc, exportPanelDesc ]; }; //Работа с соотношением размеров const useWindowResize = () => { //Состояние высоты строки ResponsiveGridLayout const [rowHeight, setRowHeight] = useState(1); //При изменении размера useLayoutEffect(() => { //Расчет высоты строки const updateRowHeight = () => { //Определяем доступную область const availableHeight = window.innerHeight - GRID_LAYOUT_PARAMS.APP_BAR_HEIGHT - GRID_LAYOUT_PARAMS.SAFE_OFFSET; //Промежутки между рядами const totalMargins = (GRID_LAYOUT_PARAMS.MAX_ROWS - 1) * GRID_LAYOUT_PARAMS.MARGIN; //Рассчитываем высоту строки const result = (availableHeight - totalMargins) / GRID_LAYOUT_PARAMS.MAX_ROWS; //Устанавливаем setRowHeight(result); }; //Запускаем при открытии панели updateRowHeight(); //Устанавливаем обработчик на событие window.addEventListener("resize", updateRowHeight); //Удаляем обработчик на событие return () => window.removeEventListener("resize", updateRowHeight); }, []); //Возвращаем интерфейс хука return [rowHeight]; }; //---------------- //Интерфейс модуля //---------------- export { useComponentModule, usePanel, usePanelManager, usePanelDesc, useWindowResize };