@@ -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 && (
-
-
-
-
+
)}