forked from CITKParus/P8-Panels
252 lines
11 KiB
JavaScript
252 lines
11 KiB
JavaScript
/*
|
||
Парус 8 - Панели мониторинга - Редактор панелей
|
||
Корневой компонент
|
||
*/
|
||
|
||
//TODO: Подчистка values после обновления имени элемента формы (и т.п.), удаления элемента формы
|
||
|
||
//---------------------
|
||
//Подключение библиотек
|
||
//---------------------
|
||
|
||
import React, { useEffect, useState, useContext } from "react"; //Классы React
|
||
import { Responsive, WidthProvider } from "react-grid-layout"; //Адаптивный макет
|
||
import { Box, Grid, Stack, Menu, MenuItem, IconButton, Icon, Fab } from "@mui/material"; //Интерфейсные элементы
|
||
import { ApplicationСtx } from "../../context/application"; //Контекст приложения
|
||
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 PANEL_CAPTION_EDIT_MODE = "Редактор панелей";
|
||
const PANEL_CAPTION_EXECUTE_MODE = "Исполнение панели";
|
||
|
||
//Начальное состояние размера макета
|
||
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 { setAppBarTitle } = useContext(ApplicationСtx);
|
||
|
||
//Добвление компонента в макет
|
||
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 = () => {
|
||
if (!editMode) setAppBarTitle(PANEL_CAPTION_EDIT_MODE);
|
||
else setAppBarTitle(PANEL_CAPTION_EXECUTE_MODE);
|
||
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 };
|