ЦИТК-979 - Компонент для настройки свойств запроса, настройка атрибутов сущностей (начало, клиент)

This commit is contained in:
Mikhail Chechnev 2025-08-08 19:16:30 +03:00
parent 836a3db5e2
commit f9b2f1ae34
9 changed files with 328 additions and 70 deletions

View File

@ -101,4 +101,4 @@ Attribute.propTypes = {
//Интерфейс модуля
//----------------
export { Attribute };
export { Attribute, ATTRIBUTE_DATA_SHAPE };

View File

@ -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 (
<div className="entity__wrapper" data-selected={selected}>
<div className="entity__title">
@ -47,4 +48,4 @@ Entity.propTypes = {
//Интерфейс модуля
//----------------
export { Entity };
export { Entity, ENTITY_DATA_SHAPE };

View File

@ -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 <P8PDialog title={`Атрибуты сущности "${title}"`} onOk={handleOk} onCancel={handleCancel} />;
};
//Контроль свойств - Диалог настройки атрибутов сущности
EntityAttrsDialog.propTypes = {
id: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
onOk: PropTypes.func,
onCancel: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { EntityAttrsDialog };

View File

@ -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 (
<List sx={{ height: "500px", width: "360px", bgcolor: "background.paper", overflowY: "auto" }}>
<List sx={STYLES.LIST}>
{queries.map((query, i) => {
const selected = query.rn === current;
const disabled = !query.modify;

View File

@ -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);

View File

@ -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
};
//----------------
//Интерфейс модуля
//----------------

View File

@ -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 && <EntityAddDialog onOk={handleEntityAddDialogOk} onCancel={handleEntityAddDialogCancel} />}
{openEntityAttrsDialog && <EntityAttrsDialog {...entity} onOk={handleEntityAttrsDialogOk} onCancel={handleEntityAttrsDialogCancel} />}
<P8PEditorBox title={"Параметры запроса"}>
<P8PEditorSubHeader title={"Сущности"} />
<Button startIcon={<Icon>add</Icon>} onClick={handleEntityAddClick}>
{BUTTONS.INSERT}
</Button>
{entity && (
<>
<P8PEditorSubHeader title={entity.title} />
<Button startIcon={<Icon>edit_attributes</Icon>} onClick={handleEntityAttrsClick}>
Атрибуты
</Button>
<Button startIcon={<Icon>delete</Icon>} onClick={handleEntityRemoveClick}>
{BUTTONS.DELETE}
</Button>
</>
)}
{relation && (
<>
<P8PEditorSubHeader title={"Связь"} />
<Button startIcon={<Icon>delete</Icon>} onClick={handleRelationRemoveClick}>
{BUTTONS.DELETE}
</Button>
</>
)}
</P8PEditorBox>
</>
);
};
//Контроль свойств компонента - Свойства запроса
QueryOptions.propTypes = {
onEntityAdd: PropTypes.func,
onEntityRemove: PropTypes.func,
onRelationRemove: PropTypes.func,
onQueryOptionsChanged: PropTypes.func,
entity: ENTITY_DATA_SHAPE,
relation: RELATION_DATA_SHAPE
};
//----------------
//Интерфейс модуля
//----------------
export { QueryOptions };

View File

@ -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 };

View File

@ -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 (
<Box sx={STYLES.CONTAINER}>
{openQueriesManager && <QueriesManager current={query} onQuerySelect={handleQuerySelect} onCancel={handleCancelQueriesManager} />}
{openEntityAddDialog && <EntityAddDialog onOk={handleEntityAddDialogOk} onCancel={handleEntityAddDialogCancel} />}
<Grid container sx={STYLES.GRID_CONTAINER} columns={25}>
<Grid item xs={20}>
{queryDiagram && (
<QueryDiagram
{...queryDiagram}
onEntityClick={handleEntityClick}
onEntityPositionChange={handleEntityPositionChange}
onEntityRemove={handleEntityRemove}
onRelactionClick={handleRelationClick}
onRelationAdd={handleRelationAdd}
onRelationRemove={handleRelationRemove}
/>
@ -117,12 +152,14 @@ const QueryEditor = () => {
<Grid item xs={5} sx={STYLES.GRID_ITEM_INSPECTOR}>
{toolBar}
{query && (
<P8PEditorBox title={"Параметры запроса"}>
<P8PEditorSubHeader title={"Сущности"} />
<Button startIcon={<Icon>add</Icon>} onClick={handleEntityAdd}>
{BUTTONS.INSERT}
</Button>
</P8PEditorBox>
<QueryOptions
onEntityAdd={handleEntityAdd}
onEntityRemove={handleEntityRemove}
onRelationRemove={handleRelationRemove}
onQueryOptionsChanged={handleQueryOptionsChanged}
entity={entity}
relation={relation}
/>
)}
</Grid>
</Grid>