WEBAPP: Редактор панелей (PoC)
This commit is contained in:
parent
c734b62ba0
commit
4d59203604
52
app/panels/panels_editor/component_editor.js
Normal file
52
app/panels/panels_editor/component_editor.js
Normal file
@ -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 (
|
||||
<Box className={"component-editor__wrap"}>
|
||||
{haveComponent && init && (
|
||||
<ComponentEditor.default id={id} {...settings} valueProviders={valueProviders} onSettingsChange={onSettingsChange} />
|
||||
)}
|
||||
{!haveComponent && <Typography align={"center"}>Компонент не определён</Typography>}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - редактор свойств компонента панели
|
||||
ComponentEditor.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
settings: PropTypes.object,
|
||||
valueProviders: PropTypes.object,
|
||||
onSettingsChange: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { ComponentEditor };
|
72
app/panels/panels_editor/component_view.js
Normal file
72
app/panels/panels_editor/component_view.js
Normal file
@ -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 (
|
||||
<Box className={"component-view__wrap"}>
|
||||
{haveComponent && init && <ComponentView.default id={id} {...settings} values={values} onValuesChange={handleValuesChange} />}
|
||||
{!haveComponent && <Typography align={"center"}>Компонент не определён</Typography>}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - компонент панели
|
||||
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 (
|
||||
<Paper sx={STYLES.CONTAINER}>
|
||||
{haveComponent && (<Suspense fallback={null}><ComponentView {...props}/></Suspense>)}
|
||||
{!haveComponent && <Typography align={"center"}>Компонент не определён</Typography>}
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
*/
|
56
app/panels/panels_editor/components/chart/editor.js
Normal file
56
app/panels/panels_editor/components/chart/editor.js
Normal file
@ -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 (
|
||||
<EditorBox title={"Параметры графика"} onSave={handleSave}>
|
||||
<EditorSubHeader title={"Источник данных"} />
|
||||
<DataSource dataSource={settings?.dataSource} valueProviders={valueProviders} onChange={handleDataSourceChange} />
|
||||
</EditorBox>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - График (редактор настроек)
|
||||
ChartEditor.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
dataSource: DATA_SOURCE_SHAPE,
|
||||
valueProviders: PropTypes.object,
|
||||
onSettingsChange: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export default ChartEditor;
|
79
app/panels/panels_editor/components/chart/view.js
Normal file
79
app/panels/panels_editor/components/chart/view.js
Normal file
@ -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 (
|
||||
<Paper className={"component-view__container component-view__container__empty"} elevation={6}>
|
||||
{haveConfing && haveData ? (
|
||||
<P8PChart style={STYLES.CHART} {...chart} />
|
||||
) : (
|
||||
<ComponentInlineMessage
|
||||
icon={COMPONENT_ICON}
|
||||
name={COMPONENT_NAME}
|
||||
message={!haveConfing ? COMPONENT_MESSAGES.NO_SETTINGS : error ? error : COMPONENT_MESSAGES.NO_DATA_FOUND}
|
||||
type={error ? COMPONENT_MESSAGE_TYPE.ERROR : COMPONENT_MESSAGE_TYPE.COMMON}
|
||||
/>
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - График (представление)
|
||||
Chart.propTypes = {
|
||||
dataSource: DATA_SOURCE_SHAPE,
|
||||
values: PropTypes.object
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export default Chart;
|
129
app/panels/panels_editor/components/components.js
Normal file
129
app/panels/panels_editor/components/components.js
Normal file
@ -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 };
|
174
app/panels/panels_editor/components/components_hooks.js
Normal file
174
app/panels/panels_editor/components/components_hooks.js
Normal file
@ -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 };
|
434
app/panels/panels_editor/components/editors_common.js
Normal file
434
app/panels/panels_editor/components/editors_common.js
Normal file
@ -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 (
|
||||
<Box className={"component-editor__container"}>
|
||||
<Divider>{title}</Divider>
|
||||
<Stack direction={"column"} spacing={1}>
|
||||
{children}
|
||||
</Stack>
|
||||
<Stack direction={"row"} justifyContent={"right"} p={1}>
|
||||
<IconButton onClick={() => handleSaveClick(false)} title={BUTTONS.APPLY}>
|
||||
<Icon>done</Icon>
|
||||
</IconButton>
|
||||
<IconButton onClick={() => handleSaveClick(true)} title={BUTTONS.SAVE}>
|
||||
<Icon>done_all</Icon>
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - контейнер редактора
|
||||
EditorBox.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
|
||||
onSave: PropTypes.func
|
||||
};
|
||||
|
||||
//Заголовок раздела редактора
|
||||
const EditorSubHeader = ({ title }) => {
|
||||
//Формирование представления
|
||||
return (
|
||||
<Divider className={"component-editor__divider"}>
|
||||
<Chip label={title} size={"small"} />
|
||||
</Divider>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - заголовок раздела редактора
|
||||
EditorSubHeader.propTypes = {
|
||||
title: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
//Диалог настройки
|
||||
const ConfigDialog = ({ title, children, onOk, onCancel }) => {
|
||||
//Формирование представления
|
||||
return (
|
||||
<Dialog onClose={onCancel} open>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogContent>{children}</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => onOk && onOk()}>{BUTTONS.OK}</Button>
|
||||
<Button onClick={() => onCancel && onCancel()}>{BUTTONS.CANCEL}</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - диалог настройки
|
||||
ConfigDialog.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
|
||||
onOk: PropTypes.func,
|
||||
onCancel: PropTypes.func
|
||||
};
|
||||
|
||||
//Диалог настройки источника данных
|
||||
const ConfigDataSourceDialog = ({ dataSource = null, valueProviders = {}, onOk = null, onCancel = null } = {}) => {
|
||||
//Собственное состояние - параметры элемента формы
|
||||
const [state, setState] = useState({ ...DATA_SOURCE_INITIAL, ...dataSource });
|
||||
|
||||
//Собственное состояние - флаги обновление данных
|
||||
const [refresh, setRefresh] = useState({ userProcDesc: 0 });
|
||||
|
||||
//Собственное состояние - элемент привязки меню выбора источника
|
||||
const [valueProvidersMenuAnchorEl, setValueProvidersMenuAnchorEl] = useState(null);
|
||||
|
||||
//Описание выбранной пользовательской процедуры
|
||||
const [userProcDesc] = useUserProcDesc({ code: state.userProc, refresh: refresh.userProcDesc });
|
||||
|
||||
//Подключение к контексту приложения
|
||||
const { pOnlineShowDictionary } = useContext(ApplicationСtx);
|
||||
|
||||
//Установка значения/привязки аргумента
|
||||
const setArgumentValueSource = (index, value, valueSource) =>
|
||||
setState(pv => ({
|
||||
...pv,
|
||||
arguments: pv.arguments.map((argument, i) => ({ ...argument, ...(i == index ? { value, valueSource } : {}) }))
|
||||
}));
|
||||
|
||||
//Открытие/сокрытие меню выбора источника
|
||||
const toggleValueProvidersMenu = target => setValueProvidersMenuAnchorEl(target instanceof Element ? target : null);
|
||||
|
||||
//При нажатии на очистку наименования пользовательской процедуры
|
||||
const handleUserProcClearClick = () => setState({ ...DATA_SOURCE_INITIAL });
|
||||
|
||||
//При нажатии на выбор пользовательской процедуры в качестве источника данных
|
||||
const handleUserProcSelectClick = () => {
|
||||
pOnlineShowDictionary({
|
||||
unitCode: "UserProcedures",
|
||||
showMethod: "main",
|
||||
inputParameters: [{ name: "in_CODE", value: state.userProc }],
|
||||
callBack: res => {
|
||||
if (res.success) {
|
||||
setState(pv => ({ ...pv, type: DATA_SOURCE_TYPE.USER_PROC, userProc: res.outParameters.out_CODE }));
|
||||
setRefresh(pv => ({ ...pv, userProcDesc: pv.userProcDesc + 1 }));
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
//При закрытии дилога с сохранением
|
||||
const handleOk = () => onOk && onOk({ ...state });
|
||||
|
||||
//При закртии диалога отменой
|
||||
const handleCancel = () => onCancel && onCancel();
|
||||
|
||||
//При очистке значения/связывания аргумента
|
||||
const handleArgumentClearClick = index => setArgumentValueSource(index, "", "");
|
||||
|
||||
//При отображении меню связывания аргумента с поставщиком данных
|
||||
const handleArgumentLinkMenuClick = e => setValueProvidersMenuAnchorEl(e.currentTarget);
|
||||
|
||||
//При выборе элемента меню связывания аргумента с поставщиком данных
|
||||
const handleArgumentLinkClick = valueSource => {
|
||||
setArgumentValueSource(valueProvidersMenuAnchorEl.id, "", valueSource);
|
||||
toggleValueProvidersMenu();
|
||||
};
|
||||
|
||||
//При вводе значения аргумента
|
||||
const handleArgumentChange = (index, value) => setArgumentValueSource(index, value, "");
|
||||
|
||||
//При изменении описания пользовательской процедуры
|
||||
useEffect(() => {
|
||||
if (userProcDesc)
|
||||
setState(pv => ({
|
||||
...pv,
|
||||
stored: userProcDesc?.stored?.name,
|
||||
respArg: userProcDesc?.stored?.respArg,
|
||||
arguments: (userProcDesc?.arguments || []).map(argument => ({ ...DATA_SOURCE_ARGUMENT_INITIAL, ...argument }))
|
||||
}));
|
||||
}, [userProcDesc]);
|
||||
|
||||
//Список значений
|
||||
const values = Object.keys(valueProviders).reduce((res, key) => [...res, ...Object.keys(valueProviders[key])], []);
|
||||
|
||||
//Наличие значений
|
||||
const isValues = values && values.length > 0 ? true : false;
|
||||
|
||||
//Меню привязки к поставщикам значений
|
||||
const valueProvidersMenu = isValues && (
|
||||
<Menu anchorEl={valueProvidersMenuAnchorEl} open={Boolean(valueProvidersMenuAnchorEl)} onClose={toggleValueProvidersMenu}>
|
||||
{values.map((value, i) => (
|
||||
<MenuItem key={i} onClick={() => handleArgumentLinkClick(value)}>
|
||||
{value}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
//Формирование представления
|
||||
return (
|
||||
<ConfigDialog title="Настройка источника данных" onOk={handleOk} onCancel={handleCancel}>
|
||||
<Stack direction={"column"} spacing={1}>
|
||||
{valueProvidersMenu}
|
||||
<TextField
|
||||
type={"text"}
|
||||
variant={"standard"}
|
||||
value={state.userProc}
|
||||
label={"Пользовательская процедура"}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
InputProps={{
|
||||
readOnly: true,
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={handleUserProcClearClick}>
|
||||
<Icon>clear</Icon>
|
||||
</IconButton>
|
||||
<IconButton onClick={handleUserProcSelectClick}>
|
||||
<Icon>list</Icon>
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{Array.isArray(state?.arguments) &&
|
||||
state.arguments.map((argument, i) => (
|
||||
<TextField
|
||||
key={i}
|
||||
type={"text"}
|
||||
variant={"standard"}
|
||||
value={argument.value || argument.valueSource}
|
||||
label={argument.caption}
|
||||
onChange={e => handleArgumentChange(i, e.target.value)}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={() => handleArgumentClearClick(i)}>
|
||||
<Icon>clear</Icon>
|
||||
</IconButton>
|
||||
{isValues && (
|
||||
<IconButton id={i} onClick={handleArgumentLinkMenuClick}>
|
||||
<Icon>settings_ethernet</Icon>
|
||||
</IconButton>
|
||||
)}
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</ConfigDialog>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - Диалог настройки источника данных
|
||||
ConfigDataSourceDialog.propTypes = {
|
||||
dataSource: DATA_SOURCE_SHAPE,
|
||||
valueProviders: PropTypes.object,
|
||||
onOk: PropTypes.func,
|
||||
onCancel: PropTypes.func
|
||||
};
|
||||
|
||||
//Источник данных
|
||||
const DataSource = ({ dataSource = null, valueProviders = {}, onChange = null } = {}) => {
|
||||
//Собственное состояние - отображение диалога настройки
|
||||
const [configDlg, setConfigDlg] = useState(false);
|
||||
|
||||
//Уведомление родителя о смене настроек источника данных
|
||||
const notifyChange = settings => onChange && onChange(settings);
|
||||
|
||||
//При нажатии на настройку источника данных
|
||||
const handleSetup = () => setConfigDlg(true);
|
||||
|
||||
//При нажатии на настройку источника данных
|
||||
const handleSetupOk = dataSource => {
|
||||
setConfigDlg(false);
|
||||
notifyChange(dataSource);
|
||||
};
|
||||
|
||||
//При нажатии на настройку источника данных
|
||||
const handleSetupCancel = () => setConfigDlg(false);
|
||||
|
||||
//При удалении настроек источника данных
|
||||
const handleDelete = () => notifyChange({ ...DATA_SOURCE_INITIAL });
|
||||
|
||||
//Расчет флага "настроенности"
|
||||
const configured = dataSource?.type ? true : false;
|
||||
|
||||
//Список аргументов
|
||||
const args =
|
||||
configured &&
|
||||
dataSource.arguments.map((argument, i) => (
|
||||
<Chip
|
||||
key={i}
|
||||
label={`:${argument.name} = ${argument.valueSource || argument.value || "NULL"}`}
|
||||
variant={"outlined"}
|
||||
sx={STYLES.CHIP(true)}
|
||||
/>
|
||||
));
|
||||
|
||||
//Формирование представления
|
||||
return (
|
||||
<>
|
||||
{configDlg && (
|
||||
<ConfigDataSourceDialog dataSource={dataSource} valueProviders={valueProviders} onOk={handleSetupOk} onCancel={handleSetupCancel} />
|
||||
)}
|
||||
{configured && (
|
||||
<Card variant={"outlined"}>
|
||||
<CardActionArea onClick={handleSetup}>
|
||||
<CardContent>
|
||||
<Typography variant={"subtitle1"} noWrap={true}>
|
||||
{dataSource.type === DATA_SOURCE_TYPE.USER_PROC ? dataSource.userProc : "Источник без наименования"}
|
||||
</Typography>
|
||||
<Typography variant={"caption"} color={"text.secondary"} noWrap={true}>
|
||||
{DATA_SOURCE_TYPE_NAME[dataSource.type] || "Неизвестный тип источника"}
|
||||
</Typography>
|
||||
<Stack direction={"column"} spacing={1} pt={2}>
|
||||
{args}
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
<CardActions>
|
||||
<IconButton onClick={handleDelete}>
|
||||
<Icon>delete</Icon>
|
||||
</IconButton>
|
||||
</CardActions>
|
||||
</Card>
|
||||
)}
|
||||
{!configured && (
|
||||
<Button startIcon={<Icon>build</Icon>} onClick={handleSetup}>
|
||||
Настроить
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - Источник данных
|
||||
DataSource.propTypes = {
|
||||
dataSource: DATA_SOURCE_SHAPE,
|
||||
valueProviders: PropTypes.object,
|
||||
onChange: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { STYLES, ARGUMENT_DATA_TYPE, DATA_SOURCE_TYPE, DATA_SOURCE_SHAPE, DATA_SOURCE_INITIAL, EditorBox, EditorSubHeader, ConfigDialog, DataSource };
|
49
app/panels/panels_editor/components/form/common.js
Normal file
49
app/panels/panels_editor/components/form/common.js
Normal file
@ -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"
|
||||
};
|
306
app/panels/panels_editor/components/form/editor.js
Normal file
306
app/panels/panels_editor/components/form/editor.js
Normal file
@ -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 (
|
||||
<ConfigDialog title={`${item ? "Изменение" : "Добавление"} элемента`} onOk={handleOk} onCancel={handleCancel}>
|
||||
<Stack direction={"column"} spacing={1}>
|
||||
<TextField type={"text"} variant={"standard"} value={state.name} label={"Имя"} id={"name"} onChange={handleChange} />
|
||||
<TextField type={"text"} variant={"standard"} value={state.caption} label={"Приглашение"} id={"caption"} onChange={handleChange} />
|
||||
<TextField
|
||||
type={"text"}
|
||||
variant={"standard"}
|
||||
value={state.unitName}
|
||||
label={"Раздел"}
|
||||
InputLabelProps={{ shrink: state.unitName ? true : false }}
|
||||
InputProps={{
|
||||
readOnly: true,
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={handleClearUnitClick}>
|
||||
<Icon>clear</Icon>
|
||||
</IconButton>
|
||||
<IconButton onClick={handleSelectUnitClick}>
|
||||
<Icon>list</Icon>
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
type={"text"}
|
||||
variant={"standard"}
|
||||
value={state.showMethodName}
|
||||
label={"Метод вызова"}
|
||||
InputLabelProps={{ shrink: state.showMethodName ? true : false }}
|
||||
InputProps={{ readOnly: true }}
|
||||
/>
|
||||
<TextField
|
||||
type={"text"}
|
||||
variant={"standard"}
|
||||
value={state.parameter}
|
||||
label={"Параметр"}
|
||||
InputLabelProps={{ shrink: state.parameter ? true : false }}
|
||||
InputProps={{
|
||||
readOnly: true,
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={handleSelectUnitParameterClick}>
|
||||
<Icon>list</Icon>
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigDialog>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - редактор элемента
|
||||
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 && (
|
||||
<EditorBox title={"Параметры формы"} onSave={handleSave}>
|
||||
{itemEditor.display && (
|
||||
<ItemEditor
|
||||
item={itemEditor.index !== null ? { ...settings.items[itemEditor.index] } : null}
|
||||
onCancel={handleItemCancel}
|
||||
onOk={handleItemSave}
|
||||
/>
|
||||
)}
|
||||
<EditorSubHeader title={"Общие"} />
|
||||
<TextField type={"text"} variant={"standard"} value={settings.title} label={"Заголовок"} name={"title"} onChange={handleChange} />
|
||||
<FormControl variant={"standard"}>
|
||||
<InputLabel id={"orientation-label"}>Ориентация</InputLabel>
|
||||
<Select
|
||||
name={"orientation"}
|
||||
value={settings.orientation}
|
||||
labelId={"orientation-label"}
|
||||
label={"Ориентация"}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<MenuItem value={ORIENTATION.V}>Вертикально</MenuItem>
|
||||
<MenuItem value={ORIENTATION.H}>Горизонтально</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControlLabel
|
||||
control={<Switch name={"autoApply"} checked={settings.autoApply} onChange={handleChange} />}
|
||||
label={"Автоподтверждение"}
|
||||
/>
|
||||
<EditorSubHeader title={"Элементы"} />
|
||||
{Array.isArray(settings?.items) &&
|
||||
settings.items.length > 0 &&
|
||||
settings.items.map((item, i) => (
|
||||
<Chip
|
||||
key={i}
|
||||
label={item.caption}
|
||||
variant={"outlined"}
|
||||
onClick={() => handleItemClick(i)}
|
||||
onDelete={() => handleItemDelete(i)}
|
||||
sx={STYLES.CHIP_ITEM}
|
||||
/>
|
||||
))}
|
||||
<Button startIcon={<Icon>add</Icon>} onClick={handleItemAdd}>
|
||||
Добавить элемент
|
||||
</Button>
|
||||
</EditorBox>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - Форма (редактор настроек)
|
||||
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;
|
168
app/panels/panels_editor/components/form/view.js
Normal file
168
app/panels/panels_editor/components/form/view.js
Normal file
@ -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 && (
|
||||
<TextField
|
||||
fullWidth={fullWidth}
|
||||
type={"text"}
|
||||
variant={"standard"}
|
||||
value={value}
|
||||
label={item.caption}
|
||||
id={item.name}
|
||||
onChange={handleChange}
|
||||
{...(item.unitCode && {
|
||||
InputLabelProps: { shrink: true },
|
||||
InputProps: {
|
||||
readOnly: true,
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={handleClear}>
|
||||
<Icon>clear</Icon>
|
||||
</IconButton>
|
||||
<IconButton onClick={handleDictionary}>
|
||||
<Icon>list</Icon>
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}
|
||||
})}
|
||||
/>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - элемент формы
|
||||
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 (
|
||||
<Paper className={`component-view__container ${!haveConfing && "component-view__container__empty"}`} elevation={6}>
|
||||
{haveConfing ? (
|
||||
<Stack direction={"column"}>
|
||||
<Stack direction={"row"} justifyContent={"space-between"} alignItems={"center"}>
|
||||
{title && (
|
||||
<Typography align={"left"} color={"text.primary"} variant={"subtitle2"} noWrap={true}>
|
||||
{title}
|
||||
</Typography>
|
||||
)}
|
||||
{!autoApply && (
|
||||
<IconButton onClick={handleOkClick}>
|
||||
<Icon>done</Icon>
|
||||
</IconButton>
|
||||
)}
|
||||
</Stack>
|
||||
<Stack direction={orientation == ORIENTATION.V ? "column" : "row"} spacing={1} pt={1} pb={1}>
|
||||
{items.map((item, i) => (
|
||||
<FormItem
|
||||
key={i}
|
||||
item={item}
|
||||
value={selfValues?.[item.name] || ""}
|
||||
onChange={handleItemChange}
|
||||
fullWidth={orientation == ORIENTATION.V}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
) : (
|
||||
<ComponentInlineMessage icon={COMPONENT_ICON} name={COMPONENT_NAME} message={COMPONENT_MESSAGES.NO_SETTINGS} />
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - Форма (представление)
|
||||
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;
|
56
app/panels/panels_editor/components/indicator/editor.js
Normal file
56
app/panels/panels_editor/components/indicator/editor.js
Normal file
@ -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 (
|
||||
<EditorBox title={"Параметры индикатора"} onSave={handleSave}>
|
||||
<EditorSubHeader title={"Источник данных"} />
|
||||
<DataSource dataSource={settings?.dataSource} valueProviders={valueProviders} onChange={handleDataSourceChange} />
|
||||
</EditorBox>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - Индикатор (редактор настроек)
|
||||
IndicatorEditor.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
dataSource: DATA_SOURCE_SHAPE,
|
||||
valueProviders: PropTypes.object,
|
||||
onSettingsChange: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export default IndicatorEditor;
|
131
app/panels/panels_editor/components/indicator/view.js
Normal file
131
app/panels/panels_editor/components/indicator/view.js
Normal file
@ -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 (
|
||||
<Paper sx={STYLES.CONTAINER(indicator?.state)} elevation={6}>
|
||||
{haveConfing && haveData ? (
|
||||
<Stack direction={"row"} alignItems={"center"} justifyContent={"space-between"} style={STYLES.STACK}>
|
||||
<Stack direction={"column"}>
|
||||
<Typography variant={"h4"}>{[undefined, null, ""].includes(indicator.value) ? "н.д." : indicator.value}</Typography>
|
||||
<Typography>{indicator.caption}</Typography>
|
||||
</Stack>
|
||||
{indicator.icon ? <Icon sx={STYLES.ICON(indicator.state)}>{indicator.icon}</Icon> : null}
|
||||
</Stack>
|
||||
) : (
|
||||
<ComponentInlineMessage
|
||||
icon={COMPONENT_ICON}
|
||||
name={COMPONENT_NAME}
|
||||
message={!haveConfing ? COMPONENT_MESSAGES.NO_SETTINGS : error ? error : COMPONENT_MESSAGES.NO_DATA_FOUND}
|
||||
type={error ? COMPONENT_MESSAGE_TYPE.ERROR : COMPONENT_MESSAGE_TYPE.COMMON}
|
||||
/>
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - Индикатор (представление)
|
||||
Indicator.propTypes = {
|
||||
dataSource: DATA_SOURCE_SHAPE,
|
||||
values: PropTypes.object
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export default Indicator;
|
56
app/panels/panels_editor/components/table/editor.js
Normal file
56
app/panels/panels_editor/components/table/editor.js
Normal file
@ -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 (
|
||||
<EditorBox title={"Параметры таблицы"} onSave={handleSave}>
|
||||
<EditorSubHeader title={"Источник данных"} />
|
||||
<DataSource dataSource={settings?.dataSource} valueProviders={valueProviders} onChange={handleDataSourceChange} />
|
||||
</EditorBox>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - Таблица (редактор настроек)
|
||||
TableEditor.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
dataSource: DATA_SOURCE_SHAPE,
|
||||
valueProviders: PropTypes.object,
|
||||
onSettingsChange: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export default TableEditor;
|
96
app/panels/panels_editor/components/table/view.js
Normal file
96
app/panels/panels_editor/components/table/view.js
Normal file
@ -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 (
|
||||
<Paper
|
||||
{...(haveConfing && haveData
|
||||
? { sx: { ...STYLES.CONTAINER } }
|
||||
: { className: "component-view__container component-view__container__empty" })}
|
||||
elevation={6}
|
||||
>
|
||||
{haveConfing && haveData ? (
|
||||
<P8PDataGrid
|
||||
{...P8P_DATA_GRID_CONFIG_PROPS}
|
||||
{...dataGrid}
|
||||
style={STYLES.DATA_GRID}
|
||||
containerComponentProps={{ sx: STYLES.DATA_GRID_CONTAINER, elevation: 0 }}
|
||||
/>
|
||||
) : (
|
||||
<ComponentInlineMessage
|
||||
icon={COMPONENT_ICON}
|
||||
name={COMPONENT_NAME}
|
||||
message={!haveConfing ? COMPONENT_MESSAGES.NO_SETTINGS : error ? error : COMPONENT_MESSAGES.NO_DATA_FOUND}
|
||||
type={error ? COMPONENT_MESSAGE_TYPE.ERROR : COMPONENT_MESSAGE_TYPE.COMMON}
|
||||
/>
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - Таблица (представление)
|
||||
Table.propTypes = {
|
||||
dataSource: DATA_SOURCE_SHAPE,
|
||||
values: PropTypes.object
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export default Table;
|
67
app/panels/panels_editor/components/views_common.js
Normal file
67
app/panels/panels_editor/components/views_common.js
Normal file
@ -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 (
|
||||
<Stack direction={"column"}>
|
||||
<Stack direction={"row"} justifyContent={"center"} alignItems={"center"}>
|
||||
{icon && <Icon color={"disabled"}>{icon}</Icon>}
|
||||
{name && (
|
||||
<Typography align={"center"} color={"text.secondary"} variant={"button"}>
|
||||
{name}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
<Typography align={"center"} color={type != COMPONENT_MESSAGE_TYPE.ERROR ? "text.secondary" : "error.dark"} variant={"caption"}>
|
||||
{message}
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Информационное сообщение внутри компонента
|
||||
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 };
|
16
app/panels/panels_editor/index.js
Normal file
16
app/panels/panels_editor/index.js
Normal file
@ -0,0 +1,16 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редактор панелей
|
||||
Редактор панелей: точка входа
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import { PanelsEditor } from "./panels_editor"; //Корневая панель редактора
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export const RootClass = PanelsEditor;
|
87
app/panels/panels_editor/layout_item.js
Normal file
87
app/panels/panels_editor/layout_item.js
Normal file
@ -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 (
|
||||
<div
|
||||
style={{ ...style, ...STYLES.CONTAINER(selected) }}
|
||||
className={`${className} layout-item__container`}
|
||||
ref={ref}
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseUp={onMouseUp}
|
||||
onTouchEnd={onTouchEnd}
|
||||
>
|
||||
{editMode && (
|
||||
<Stack direction={"column"} sx={STYLES.STACK_TOOLS}>
|
||||
<IconButton onClick={handleSettingsClick}>
|
||||
<Icon>settings</Icon>
|
||||
</IconButton>
|
||||
<IconButton onClick={handleDeleteClick}>
|
||||
<Icon>delete</Icon>
|
||||
</IconButton>
|
||||
</Stack>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
//Контроль свойств компонента - элемент макета
|
||||
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 };
|
40
app/panels/panels_editor/panels_editor.css
Normal file
40
app/panels/panels_editor/panels_editor.css
Normal file
@ -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;
|
||||
}
|
239
app/panels/panels_editor/panels_editor.js
Normal file
239
app/panels/panels_editor/panels_editor.js
Normal file
@ -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 = (
|
||||
<Menu anchorEl={addMenuAnchorEl} open={Boolean(addMenuAnchorEl)} onClose={toggleAddMenu}>
|
||||
{COMPONETNS.map((comp, i) => (
|
||||
<MenuItem key={i} onClick={() => handleAddMenuItemClick(comp)}>
|
||||
{comp.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
//Кнопка редактирования
|
||||
const editButton = !editMode && (
|
||||
<Fab sx={STYLES.FAB_EDIT} size={"small"} color={"grey.700"} title={"Редактировать"} onClick={toggleEditMode}>
|
||||
<Icon>edit</Icon>
|
||||
</Fab>
|
||||
);
|
||||
|
||||
//Панель инструмментов
|
||||
const toolBar = (
|
||||
<Stack direction={"row"} p={1}>
|
||||
<IconButton onClick={toggleEditMode} title={"Запустить"}>
|
||||
<Icon>play_arrow</Icon>
|
||||
</IconButton>
|
||||
<IconButton onClick={handleAddClick} title={"Добавить элемент"}>
|
||||
<Icon>add</Icon>
|
||||
</IconButton>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<Box sx={STYLES.CONTAINER}>
|
||||
{editButton}
|
||||
{addMenu}
|
||||
<Grid container sx={STYLES.GRID_CONTAINER} columns={25}>
|
||||
<Grid item xs={editMode ? 20 : 25}>
|
||||
<ResponsiveGridLayout
|
||||
rowHeight={5}
|
||||
className={"layout"}
|
||||
layouts={layouts}
|
||||
breakpoints={{ lg: 1200 }}
|
||||
cols={{ lg: 12 }}
|
||||
onBreakpointChange={handleBreakpointChange}
|
||||
onLayoutChange={handleLayoutChange}
|
||||
useCSSTransforms={true}
|
||||
compactType={"vertical"}
|
||||
isDraggable={editMode}
|
||||
isResizable={editMode}
|
||||
>
|
||||
{layouts[breakpoint].map(item => (
|
||||
<LayoutItem
|
||||
key={item.i}
|
||||
onSettingsClick={handleComponentSettingsClick}
|
||||
onDeleteClick={handleComponentDeleteClick}
|
||||
item={item}
|
||||
editMode={editMode}
|
||||
selected={editMode && editComponent === item.i}
|
||||
>
|
||||
<ComponentView
|
||||
id={item.i}
|
||||
path={components[item.i]?.path}
|
||||
settings={components[item.i]?.settings}
|
||||
values={values}
|
||||
onValuesChange={handleComponentValuesChange}
|
||||
/>
|
||||
</LayoutItem>
|
||||
))}
|
||||
</ResponsiveGridLayout>
|
||||
</Grid>
|
||||
{editMode && (
|
||||
<Grid item xs={5} sx={STYLES.GRID_ITEM_INSPECTOR}>
|
||||
{toolBar}
|
||||
{editComponent && (
|
||||
<>
|
||||
<ComponentEditor
|
||||
id={editComponent}
|
||||
path={components[editComponent].path}
|
||||
settings={components[editComponent].settings}
|
||||
valueProviders={valueProviders}
|
||||
onSettingsChange={handleComponentSettingsChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { PanelsEditor };
|
127
db/PKG_P8PANELS_EDITOR.pck
Normal file
127
db/PKG_P8PANELS_EDITOR.pck
Normal file
@ -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;
|
||||
/
|
Loading…
x
Reference in New Issue
Block a user