640 lines
27 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
Парус 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 };