diff --git a/app/panels/query_editor/components/attribute/attribute.js b/app/panels/query_editor/components/attribute/attribute.js index f1d36ef..cfb4b9a 100644 --- a/app/panels/query_editor/components/attribute/attribute.js +++ b/app/panels/query_editor/components/attribute/attribute.js @@ -101,4 +101,4 @@ Attribute.propTypes = { //Интерфейс модуля //---------------- -export { Attribute }; +export { Attribute, ATTRIBUTE_DATA_SHAPE }; diff --git a/app/panels/query_editor/components/entity/entity.js b/app/panels/query_editor/components/entity/entity.js index 89b9797..9750ec1 100644 --- a/app/panels/query_editor/components/entity/entity.js +++ b/app/panels/query_editor/components/entity/entity.js @@ -17,6 +17,7 @@ import "./entity.css"; //Стили компомнента //Структура данных о сущности запроса const ENTITY_DATA_SHAPE = PropTypes.shape({ + id: PropTypes.string.isRequired, name: PropTypes.string.isRequired, title: PropTypes.string.isRequired }); @@ -26,7 +27,7 @@ const ENTITY_DATA_SHAPE = PropTypes.shape({ //----------- //Сущность запроса -const Entity = ({ data, selected }) => { +const Entity = ({ data, selected = false }) => { return (
@@ -47,4 +48,4 @@ Entity.propTypes = { //Интерфейс модуля //---------------- -export { Entity }; +export { Entity, ENTITY_DATA_SHAPE }; diff --git a/app/panels/query_editor/components/entity_attrs_dialog/entity_attrs_dialog.js b/app/panels/query_editor/components/entity_attrs_dialog/entity_attrs_dialog.js new file mode 100644 index 0000000..972d74a --- /dev/null +++ b/app/panels/query_editor/components/entity_attrs_dialog/entity_attrs_dialog.js @@ -0,0 +1,42 @@ +/* + Парус 8 - Панели мониторинга - Редактор запросов + Компонент: Диалог настройки атрибутов сущности +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +import React from "react"; //Классы React +import PropTypes from "prop-types"; //Контроль свойств компонента +import { P8PDialog } from "../../../../components/p8p_dialog"; //Типовой диалог + +//----------- +//Тело модуля +//----------- + +//Диалог настройки атрибутов сущности +const EntityAttrsDialog = ({ id, title, onOk, onCancel }) => { + //Нажатие на кнопку "Ok" + const handleOk = values => onOk && onOk({ ...values }); + + //Нажатие на кнопку "Отмена" + const handleCancel = () => onCancel && onCancel(); + + //Генерация содержимого + return ; +}; + +//Контроль свойств - Диалог настройки атрибутов сущности +EntityAttrsDialog.propTypes = { + id: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + onOk: PropTypes.func, + onCancel: PropTypes.func +}; + +//---------------- +//Интерфейс модуля +//---------------- + +export { EntityAttrsDialog }; diff --git a/app/panels/query_editor/components/queries_manager/queries_list.js b/app/panels/query_editor/components/queries_manager/queries_list.js index f3347e9..a741004 100644 --- a/app/panels/query_editor/components/queries_manager/queries_list.js +++ b/app/panels/query_editor/components/queries_manager/queries_list.js @@ -10,6 +10,7 @@ import React from "react"; //Классы React import PropTypes from "prop-types"; //Контроль свойств компонента import { Stack, List, ListItem, IconButton, Icon, ListItemButton, ListItemText, Typography } from "@mui/material"; //Интерфейсные компоненты MUI +import { APP_STYLES } from "../../../../../app.styles"; //Общие стили приложения import { BUTTONS } from "../../../../../app.text"; //Общие текстовые ресурсы приложения //--------- @@ -20,7 +21,8 @@ import { BUTTONS } from "../../../../../app.text"; //Общие текстовы const STYLES = { SMALL_TOOL_ICON: { fontSize: 20 - } + }, + LIST: { height: "500px", width: "360px", bgcolor: "background.paper", overflowY: "auto", ...APP_STYLES.SCROLL } }; //--------- @@ -76,7 +78,7 @@ const QueriesList = ({ queries = [], current = null, onSelect = null, onPbl = nu //Формирование представления return ( - + {queries.map((query, i) => { const selected = query.rn === current; const disabled = !query.modify; diff --git a/app/panels/query_editor/components/queries_manager/queries_manager.js b/app/panels/query_editor/components/queries_manager/queries_manager.js index fe31615..3491408 100644 --- a/app/panels/query_editor/components/queries_manager/queries_manager.js +++ b/app/panels/query_editor/components/queries_manager/queries_manager.js @@ -36,7 +36,7 @@ const QueriesManager = ({ current = null, onQuerySelect = null, onCancel = null const handleQueryAdd = () => setModQuery(true); //При выборе запроса - const handleQuerySelect = query => onQuerySelect && onQuerySelect(query.rn); + const handleQuerySelect = query => onQuerySelect && onQuerySelect({ ...query }); //При установке признака публичности const handleQueryPblSet = query => setQueryPbl(query.rn, query.pbl === 1 ? 0 : 1); diff --git a/app/panels/query_editor/components/query_diagram/query_diagram.js b/app/panels/query_editor/components/query_diagram/query_diagram.js index 2715653..bf8f6dd 100644 --- a/app/panels/query_editor/components/query_diagram/query_diagram.js +++ b/app/panels/query_editor/components/query_diagram/query_diagram.js @@ -8,10 +8,11 @@ //--------------------- import React, { useState, useCallback, useEffect } from "react"; //Классы React +import PropTypes from "prop-types"; //Контроль свойств компонента import ReactFlow, { addEdge, Controls, getOutgoers, applyNodeChanges, applyEdgeChanges } from "reactflow"; //Библиотека редактора диаграмм import { NODE_TYPE } from "../../common"; //Общие ресурсы и константы редактора -import { Entity } from "../entity/entity"; //Сущность запроса -import { Attribute } from "../attribute/attribute"; //Атрибут сущности +import { Entity, ENTITY_DATA_SHAPE } from "../entity/entity"; //Сущность запроса +import { Attribute, ATTRIBUTE_DATA_SHAPE } from "../attribute/attribute"; //Атрибут сущности import "reactflow/dist/style.css"; //Типовые стили библиотеки редактора диаграмм import "./query_diagram.css"; //Стили компонента @@ -36,42 +37,44 @@ const NODE_TYPES_COMPONENTS = { [NODE_TYPE.ATTRIBUTE]: Attribute }; +//Структура сущности запроса +const ENTITY_SHAPE = PropTypes.shape({ + id: PropTypes.string.isRequired, + type: PropTypes.oneOf([NODE_TYPE.ENTITY, NODE_TYPE.ATTRIBUTE]).isRequired, + style: PropTypes.object, + position: PropTypes.shape({ x: PropTypes.number.isRequired, y: PropTypes.number.isRequired }), + draggable: PropTypes.bool.isRequired, + data: PropTypes.oneOfType([ENTITY_DATA_SHAPE, ATTRIBUTE_DATA_SHAPE]) +}); + +//Структура связи запроса +const RELATION_SHAPE = PropTypes.shape({ id: PropTypes.string.isRequired, source: PropTypes.string.isRequired, target: PropTypes.string.isRequired }); + //------------------------------------ //Вспомогательные функции и компоненты //------------------------------------ +//Проверка зацикленности связи const hasCycle = (connection, target, nodes, edges, visited = new Set()) => { - if (visited.has(target.id)) { - return false; - } - + if (visited.has(target.id)) return false; visited.add(target.id); - - for (const outgoer of getOutgoers(target, nodes, edges)) { - if (outgoer.id === connection.source || hasCycle(connection, outgoer, nodes, edges, visited)) { - return true; - } - } - + for (const outgoer of getOutgoers(target, nodes, edges)) + if (outgoer.id === connection.source || hasCycle(connection, outgoer, nodes, edges, visited)) return true; return false; }; +//Проверка корректности связи const isValidConnection = (connection, nodes, edges) => { - if (!connection.source || !connection.target) { - return false; - } - - const tableId = connection.source.split("-")[0]; + //Должны быть заданы источник и приёмник + if (!connection.source || !connection.target) return false; + //Нельзя ссылаться на самого себя + const tableId = connection.source.split(".")[0]; const isSameTable = connection.target.startsWith(tableId); - if (isSameTable) { - return false; - } - + if (isSameTable) return false; + //Приёмник должен быть среди элементов диаграммы и не должен быть источником const target = nodes.find(node => node.id === connection.target); - if (!target || target.id === connection.source) { - return false; - } - + if (!target || target.id === connection.source) return false; + //Нельзя зацикливаться return !hasCycle(connection, target, nodes, edges); }; @@ -80,7 +83,16 @@ const isValidConnection = (connection, nodes, edges) => { //----------- //Диаграмма запроса -const QueryDiagram = ({ entities, relations, onEntityPositionChange, onEntityRemove, onRelationAdd, onRelationRemove }) => { +const QueryDiagram = ({ + entities = [], + relations = [], + onEntityClick, + onEntityPositionChange, + onEntityRemove, + onRelactionClick, + onRelationAdd, + onRelationRemove +}) => { //Собственное состояние - элементы const [nodes, setNodes] = useState(entities); @@ -98,20 +110,27 @@ const QueryDiagram = ({ entities, relations, onEntityPositionChange, onEntityRem setMovedNode({ id: changes[0].id, position: { ...changes[0].position } }); if (changes.length == 1 && changes[0].type == "position" && !changes[0].dragging && movedNode) { if (onEntityPositionChange) onEntityPositionChange(movedNode.id, movedNode.position); + if (onEntityClick) onEntityClick(movedNode.id); setMovedNode(null); } if (changes[0].type == "remove" && entities.find(e => e.id == changes[0].id && e.type == NODE_TYPE.ENTITY) && onEntityRemove) onEntityRemove(changes[0].id); }, - [movedNode, entities, onEntityPositionChange, onEntityRemove] + [movedNode, entities, onEntityClick, onEntityPositionChange, onEntityRemove] ); + //При выборе элемента диаграммы + const handleNodeClick = useCallback((e, node) => onEntityClick && onEntityClick(node.id), [onEntityClick]); + //При связывании элементов на диаграмме const handleConnect = connection => { setEdges(state => addEdge({ ...connection, id: `${connection.source}-${connection.target}` }, state)); onRelationAdd && onRelationAdd(connection.source, connection.target); }; + //При выборе связи диаграммы + const handleEdgeClick = useCallback((e, edge) => onRelactionClick && onRelactionClick(edge.id), [onRelactionClick]); + //При изменении связей на диаграмме const handleEdgesChange = useCallback( changes => { @@ -121,9 +140,8 @@ const QueryDiagram = ({ entities, relations, onEntityPositionChange, onEntityRem [onRelationRemove] ); - const validateConnection = connection => { - return isValidConnection(connection, nodes, edges); - }; + //Валидация связи + const validateConnection = connection => isValidConnection(connection, nodes, edges); //При изменении состава сущностей useEffect(() => setNodes(entities), [entities]); @@ -137,7 +155,9 @@ const QueryDiagram = ({ entities, relations, onEntityPositionChange, onEntityRem nodes={nodes} nodeTypes={NODE_TYPES_COMPONENTS} edges={edges} + onNodeClick={handleNodeClick} onNodesChange={handleNodesChange} + onEdgeClick={handleEdgeClick} onEdgesChange={handleEdgesChange} defaultEdgeOptions={{ animated: true, @@ -153,6 +173,18 @@ const QueryDiagram = ({ entities, relations, onEntityPositionChange, onEntityRem ); }; +//Контроль свойств компонента - Диаграмма запроса +QueryDiagram.propTypes = { + entities: PropTypes.arrayOf(ENTITY_SHAPE), + relations: PropTypes.arrayOf(RELATION_SHAPE), + onEntityClick: PropTypes.func, + onEntityPositionChange: PropTypes.func, + onEntityRemove: PropTypes.func, + onRelactionClick: PropTypes.func, + onRelationAdd: PropTypes.func, + onRelationRemove: PropTypes.func +}; + //---------------- //Интерфейс модуля //---------------- diff --git a/app/panels/query_editor/components/query_options/query_options.js b/app/panels/query_editor/components/query_options/query_options.js new file mode 100644 index 0000000..d3c9f9d --- /dev/null +++ b/app/panels/query_editor/components/query_options/query_options.js @@ -0,0 +1,117 @@ +/* + Парус 8 - Панели мониторинга - Редактор запросов + Свойства запроса +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +import React, { useState, useContext } from "react"; //Классы React +import PropTypes from "prop-types"; //Контроль свойств компонента +import { Button, Icon } from "@mui/material"; //Интерфейсные компоненты MUI +import { MessagingСtx } from "../../../../context/messaging"; //Контекст сообщений приложения +import { BUTTONS } from "../../../../../app.text"; //Общие текстовые ресурсы приложения +import { EntityAddDialog } from "../entity_add_dialog/entity_add_dialog"; //Диалог добавления сущности +import { EntityAttrsDialog } from "../entity_attrs_dialog/entity_attrs_dialog"; //Диалог настройки атрибутов сущности +import { P8PEditorBox } from "../../../../components/editors/p8p_editor_box"; //Контейнер параметров редактора +import { P8PEditorSubHeader } from "../../../../components/editors/p8p_editor_sub_header"; //Подзаголовок группы параметров редактора +import { ENTITY_DATA_SHAPE } from "../entity/entity"; //Описание сущности +import { RELATION_DATA_SHAPE } from "../relation/relation"; //Описание связи + +//----------- +//Тело модуля +//----------- + +//Свойства запроса +const QueryOptions = ({ onEntityAdd, onEntityRemove, onRelationRemove, onQueryOptionsChanged, entity, relation }) => { + //Отображение диалога добавления сущности + const [openEntityAddDialog, setOpenEntityAddDialog] = useState(false); + + //Отображение диалога настройки атрибутов сущности + const [openEntityAttrsDialog, setOpenEntityAttrsDialog] = useState(false); + + //Подключение к контексту сообщений + const { showMsgWarn } = useContext(MessagingСtx); + + //При нажатии на кнопку добавлении сущности в запрос + const handleEntityAddClick = () => setOpenEntityAddDialog(true); + + //При нажатии на кнопку настройки атрибутов сущности + const handleEntityAttrsClick = () => setOpenEntityAttrsDialog(true); + + //При нажатии на кнопку даления сущности из запроса + const handleEntityRemoveClick = () => + showMsgWarn(`Удалить сущность "${entity.title}"?`, () => entity?.id && onEntityRemove && onEntityRemove(entity.id)); + + //При нажатии на кнопку даления связи из запроса + const handleRelationRemoveClick = () => + showMsgWarn( + `Удалить связь "${relation.source}" - "${relation.target}"?`, + () => relation?.id && onRelationRemove && onRelationRemove(relation.id) + ); + + //Закрытие диалога добавления сущности по "Отмена" + const handleEntityAddDialogCancel = () => setOpenEntityAddDialog(false); + + //Закрытие диалога добавления сущности по "ОК" + const handleEntityAddDialogOk = values => onEntityAdd && onEntityAdd(values.name, res => res && setOpenEntityAddDialog(false)); + + //Закрытие диалога настройки атрибутов сущности по "Отмена" + const handleEntityAttrsDialogCancel = () => setOpenEntityAttrsDialog(false); + + //Закрытие диалога настройки атрибутов сущности по "ОК" + const handleEntityAttrsDialogOk = () => { + onQueryOptionsChanged && onQueryOptionsChanged(); + setOpenEntityAttrsDialog(); + }; + + //Генерация содержимого + return ( + <> + {openEntityAddDialog && } + {openEntityAttrsDialog && } + + + + {entity && ( + <> + + + + + )} + {relation && ( + <> + + + + )} + + + ); +}; + +//Контроль свойств компонента - Свойства запроса +QueryOptions.propTypes = { + onEntityAdd: PropTypes.func, + onEntityRemove: PropTypes.func, + onRelationRemove: PropTypes.func, + onQueryOptionsChanged: PropTypes.func, + entity: ENTITY_DATA_SHAPE, + relation: RELATION_DATA_SHAPE +}; + +//---------------- +//Интерфейс модуля +//---------------- + +export { QueryOptions }; diff --git a/app/panels/query_editor/components/relation/relation.js b/app/panels/query_editor/components/relation/relation.js new file mode 100644 index 0000000..dfe8697 --- /dev/null +++ b/app/panels/query_editor/components/relation/relation.js @@ -0,0 +1,27 @@ +/* + Парус 8 - Панели мониторинга - Редактор запросов + Компоненты: Связь сущностей запроса +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +import PropTypes from "prop-types"; //Контроль свойств компонента + +//--------- +//Константы +//--------- + +//Структура данных о связи сущностей запроса +const RELATION_DATA_SHAPE = PropTypes.shape({ + id: PropTypes.string.isRequired, + source: PropTypes.string.isRequired, + target: PropTypes.string.isRequired +}); + +//---------------- +//Интерфейс модуля +//---------------- + +export { RELATION_DATA_SHAPE }; diff --git a/app/panels/query_editor/query_editor.js b/app/panels/query_editor/query_editor.js index 22e4069..c22b09c 100644 --- a/app/panels/query_editor/query_editor.js +++ b/app/panels/query_editor/query_editor.js @@ -7,22 +7,23 @@ //Подключение библиотек //--------------------- -import React, { useState } from "react"; //Классы React -import { Box, Grid, Button, Icon } from "@mui/material"; //Интерфейсные компоненты MUI +import React, { useState, useContext } from "react"; //Классы React +import { Box, Grid } from "@mui/material"; //Интерфейсные компоненты MUI +import { ApplicationСtx } from "../../context/application"; //Контекст взаимодействия с приложением import { APP_BAR_HEIGHT } from "../../components/p8p_app_workspace"; //Компоненты рабочего стола import { P8PEditorToolBar } from "../../components/editors/p8p_editor_toolbar"; //Панель инструментов редактора -import { BUTTONS } from "../../../app.text"; //Общие текстовые ресурсы приложения import { QueryDiagram } from "./components/query_diagram/query_diagram"; //Диаграмма запроса +import { QueryOptions } from "./components/query_options/query_options"; //Свойства запроса import { QueriesManager } from "./components/queries_manager/queries_manager"; //Менеджер запросов -import { EntityAddDialog } from "./components/entity_add_dialog/entity_add_dialog"; //Диалог добавления сущности -import { P8PEditorBox } from "../../components/editors/p8p_editor_box"; //Контейнер параметров редактора -import { P8PEditorSubHeader } from "../../components/editors/p8p_editor_sub_header"; //Подзаголовок группы параметров редактора import { useQueryDesc } from "./hooks"; //Пользовательские хуки //--------- //Константы //--------- +//Заголовок панели по умолчанию +const APP_BAR_TITLE_DEFAULT = "Редактор запросов"; + //Стили const STYLES = { CONTAINER: { display: "flex" }, @@ -39,26 +40,62 @@ const QueryEditor = () => { //Текущий запрос const [query, setQuery] = useState(null); + //Текущая сущность + const [entity, setEntity] = useState(null); + + //Текущая связь + const [relation, setRelation] = useState(null); + //Отображения менеджера запросов const [openQueriesManager, setOpenQueriesManager] = useState(true); - //Отображение диалога добавления сущности - const [openEntityAddDialog, setOpenEntityAddDialog] = useState(false); - //Получение данных запроса - const [queryDiagram, addEnt, removeEnt, setEntPosition, addRl, removeRl] = useQueryDesc(query); + const [queryDiagram, addEnt, removeEnt, setEntPosition, addRl, removeRl, doRefresh] = useQueryDesc(query); + + //Подключение к контексту приложения + const { setAppBarTitle } = useContext(ApplicationСtx); //Обработка изменения положения сущности на диаграмме const handleEntityPositionChange = (ent, position) => setEntPosition(ent, position.x, position.y); + //Обработка добавления сущности в запрос + const handleEntityAdd = async (entName, cb) => { + await addEnt(entName, "VIEW"); + cb(true); + }; + //Обработка удаления сущности из запроса - const handleEntityRemove = ent => removeEnt(ent); + const handleEntityRemove = async ent => { + await removeEnt(ent); + if (entity && entity?.id === ent) setEntity(null); + }; + + //Обработка выделения сущности + const handleEntityClick = ent => { + setRelation(null); + const queryEnt = queryDiagram.entities.find(e => e.id === ent); + if (queryEnt) + if (entity?.id == queryEnt.id) setEntity(null); + else setEntity({ ...queryEnt.data }); + }; + + //Обработка выделения связи + const handleRelationClick = rl => { + setEntity(null); + const queryRl = queryDiagram.relations.find(r => r.id === rl); + if (queryRl) + if (relation?.id == queryRl.id) setRelation(null); + else setRelation({ ...queryRl }); + }; //Обработка добавления отношения cущностей const handleRelationAdd = (source, target) => addRl(source, target); //Обработка удаления отношения cущностей - const handleRelationRemove = rl => removeRl(rl); + const handleRelationRemove = async rl => { + await removeRl(rl); + if (relation && relation?.id === rl) setRelation(null); + }; //Открытие менеджера запросов const handleOpenQueriesManager = () => setOpenQueriesManager(true); @@ -67,25 +104,22 @@ const QueryEditor = () => { const handleCancelQueriesManager = () => setOpenQueriesManager(false); //Закрытие запроса - const handleQueryClose = () => setQuery(null); + const handleQueryClose = () => { + setAppBarTitle(APP_BAR_TITLE_DEFAULT); + setEntity(null); + setRelation(null); + setQuery(null); + }; //При выборе запроса - const handleQuerySelect = query => { - setQuery(query); + const handleQuerySelect = ({ rn, name }) => { + setAppBarTitle(`Запрос [${name}]`); + setQuery(rn); setOpenQueriesManager(false); }; - //При добавлении сущности в запрос - const handleEntityAdd = () => setOpenEntityAddDialog(true); - - //Закрытие диалога добавления сущности по "ОК" - const handleEntityAddDialogOk = async values => { - await addEnt(values.name, "VIEW"); - setOpenEntityAddDialog(false); - }; - - //Закрытие диалога добавления сущности по "ОК" - const handleEntityAddDialogCancel = () => setOpenEntityAddDialog(false); + //При изменении свойств запроса + const handleQueryOptionsChanged = () => doRefresh(); //Панель инструмментов const toolBar = ( @@ -101,14 +135,15 @@ const QueryEditor = () => { return ( {openQueriesManager && } - {openEntityAddDialog && } {queryDiagram && ( @@ -117,12 +152,14 @@ const QueryEditor = () => { {toolBar} {query && ( - - - - + )}