From ef8caea9d2550074e59bf7eeb2d0806ec37593e3 Mon Sep 17 00:00:00 2001 From: tristanliu <37809633+sevenliu1896@users.noreply.github.com> Date: Fri, 15 Dec 2023 19:12:34 +0800 Subject: [PATCH] [improvement][semantic-fe] Optimizing the canvas functionality for better performance and user experience. (#517) * [improvement][semantic-fe] Add model alias setting & Add view permission restrictions to the model permission management tab. [improvement][semantic-fe] Add permission control to the action buttons for the main domain; apply high sensitivity filtering to the authorization of metrics/dimensions. [improvement][semantic-fe] Optimize the editing mode in the dimension/metric/datasource components to use the modelId stored in the database for data, instead of relying on the data from the state manager. * [improvement][semantic-fe] Add time granularity setting in the data source configuration. * [improvement][semantic-fe] Dictionary import for dimension values supported in Q&A visibility * [improvement][semantic-fe] Modification of data source creation prompt wording" * [improvement][semantic-fe] metric market experience optimization * [improvement][semantic-fe] enhance the analysis of metric trends * [improvement][semantic-fe] optimize the presentation of metric trend permissions * [improvement][semantic-fe] add metric trend download functionality * [improvement][semantic-fe] fix the dimension initialization issue in metric correlation * [improvement][semantic-fe] Fix the issue of database changes not taking effect when creating based on an SQL data source. * [improvement][semantic-fe] Optimizing pagination logic and some CSS styles * [improvement][semantic-fe] Fixing the API for the indicator list by changing "current" to "pageNum" * [improvement][semantic-fe] Fixing the default value setting for the indicator list * [improvement][semantic-fe] Adding batch operations for indicators/dimensions/models * [improvement][semantic-fe] Replacing the single status update API for indicators/dimensions with a batch update API * [improvement][semantic-fe] Redesigning the indicator homepage to incorporate trend charts and table functionality for indicators * [improvement][semantic-fe] Optimizing the logic for setting dimension values and editing data sources, and adding system settings functionality * [improvement][semantic-fe] Upgrading antd version to 5.x, extracting the batch operation button component, optimizing the interaction for system settings, and expanding the configuration generation types for list-to-select component. * [improvement][semantic-fe] Adding the ability to filter dimensions based on whether they are tags or not. * [improvement][semantic-fe] Adding the ability to edit relationships between models in the canvas. * [improvement][semantic-fe] Updating the datePicker component to use dayjs instead. * [improvement][semantic-fe] Fixing the issue with passing the model ID for dimensions in the indicator market. * [improvement][semantic-fe] Fixing the abnormal state of the popup when creating a model. * [improvement][semantic-fe] Adding permission logic for bulk operations in the indicator market. * [improvement][semantic-fe] Adding the ability to download and transpose data. * [improvement][semantic-fe] Fixing the initialization issue with the date selection component in the indicator details page when switching time granularity. * [improvement][semantic-fe] Fixing the logic error in the dimension value setting. * [improvement][semantic-fe] Fixing the synchronization issue with the question and answer settings information. * [improvement][semantic-fe] Optimizing the canvas functionality for better performance and user experience. --- .../components/ControlToolBar/index.tsx | 111 ++++++++++++++++++ .../components/ControlToolBar/style.less | 39 ++++++ .../components/GraphLegendVisibleModeItem.tsx | 10 +- .../SemanticModel/SemanticGraph/index.tsx | 71 +++++++---- .../SemanticModel/SemanticGraph/style.less | 5 - .../components/MetricInfoCreateForm.tsx | 2 +- 6 files changed, 208 insertions(+), 30 deletions(-) create mode 100644 webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/components/ControlToolBar/index.tsx create mode 100644 webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/components/ControlToolBar/style.less diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/components/ControlToolBar/index.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/components/ControlToolBar/index.tsx new file mode 100644 index 000000000..471357c19 --- /dev/null +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/components/ControlToolBar/index.tsx @@ -0,0 +1,111 @@ +import React from 'react'; +import { + ControlOutlined, + ZoomInOutlined, + ZoomOutOutlined, + SearchOutlined, + OneToOneOutlined, +} from '@ant-design/icons'; +import { FloatButton, Tooltip, Input } from 'antd'; +import GraphLegendVisibleModeItem from '../GraphLegendVisibleModeItem'; +import { SemanticNodeType } from '../../../enum'; +import styles from './style.less'; + +const { Search } = Input; + +type Props = { + graph: any; + onSearch?: (text: string) => void; + onShowTypeChange?: (nodeType: SemanticNodeType) => void; + onZoomIn?: () => void; + onZoomOut?: () => void; + onAutoZoom?: () => void; +}; + +function zoomGraph(graph, ratio: number) { + const width = graph.get('width'); + const height = graph.get('height'); + const centerX = width / 2; + const centerY = height / 2; + graph.zoom(ratio, { x: centerX, y: centerY }); +} + +const ControlToolBar: React.FC = ({ + graph, + onSearch, + onShowTypeChange, + onZoomIn, + onZoomOut, + onAutoZoom, +}) => { + const sensitivity = 0.1; // 设置缩放灵敏度,值越小,缩放越不敏感,默认值为 1 + const zoomOutRatio = 1 - sensitivity; + const zoomInRatio = 1 + sensitivity; + + return ( +
+ + { + onSearch?.(text); + }} + style={{ width: 250 }} + /> + } + placement="right" + > + } description="搜索" /> + + + { + onShowTypeChange?.(nodeType); + }} + /> + } + placement="right" + > + } description="模式" /> + + + } + description="放大" + onClick={() => { + zoomGraph(graph, zoomInRatio); + onZoomIn?.(); + }} + /> + } + description="缩小" + onClick={() => { + zoomGraph(graph, zoomOutRatio); + onZoomOut?.(); + }} + /> + } + description="重置" + onClick={() => { + graph.fitView(); + onAutoZoom?.(); + }} + /> + +
+ ); +}; + +export default ControlToolBar; diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/components/ControlToolBar/style.less b/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/components/ControlToolBar/style.less new file mode 100644 index 000000000..cf1d47aee --- /dev/null +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/components/ControlToolBar/style.less @@ -0,0 +1,39 @@ +.graphControlContent { + :global { + .ant-float-btn-group { + width: 55px; + } + .ant-float-btn { + width: 100%; + } + .ant-float-btn .ant-float-btn-body .ant-float-btn-content .ant-float-btn-icon { + font-size: 24px; + width: 26px; + } + .ant-float-btn-default .ant-float-btn-body .ant-float-btn-content .ant-float-btn-description { + margin-top: 5px; + } + .ant-float-btn-group-square-shadow .ant-float-btn-square .ant-float-btn-body { + width: inherit; + height: inherit; + padding: 2px; + } + } +} +.overlayClassName { + background-color: #fff; + max-width: unset; + :global { + .ant-tooltip-content { + .ant-tooltip-inner { + background-color: #fff; + } + } + .ant-tooltip-arrow { + &::before { + background-color: #fff; + } + } + + } +} \ No newline at end of file diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/components/GraphLegendVisibleModeItem.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/components/GraphLegendVisibleModeItem.tsx index 103147b7f..96aeb3377 100644 --- a/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/components/GraphLegendVisibleModeItem.tsx +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/components/GraphLegendVisibleModeItem.tsx @@ -1,6 +1,6 @@ import { Segmented } from 'antd'; -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { SemanticNodeType } from '../../enum'; import styles from '../style.less'; @@ -11,12 +11,18 @@ type Props = { }; const GraphLegendVisibleModeItem: React.FC = ({ value, onChange }) => { + const [nodeType, setNodeType] = useState(); + + useEffect(() => { + setNodeType(value); + }, [value]); + return (
{ onChange?.(changeValue as SemanticNodeType); }} diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/index.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/index.tsx index fb10d69e9..fe98c1133 100644 --- a/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/index.tsx +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/index.tsx @@ -35,6 +35,7 @@ import GraphToolBar from './components/GraphToolBar'; import GraphLegend from './components/GraphLegend'; import GraphLegendVisibleModeItem from './components/GraphLegendVisibleModeItem'; import ModelRelationFormDrawer from './components/ModelRelationFormDrawer'; +import ControlToolBar from './components/ControlToolBar'; type Props = { domainManger: StateType; @@ -181,10 +182,12 @@ const DomainManger: React.FC = ({ domainManger, dispatch }) => { if (target) { const { config } = target; const parseConfig = jsonParse(config, []); - setRelationConfig(target); - parseConfig.forEach((item) => { - graphRef?.current?.addItem('edge', item); - }); + if (Array.isArray(parseConfig)) { + setRelationConfig(target); + parseConfig.forEach((item) => { + graphRef?.current?.addItem('edge', item); + }); + } } } else { message.error(msg); @@ -478,11 +481,11 @@ const DomainManger: React.FC = ({ domainManger, dispatch }) => { if (!graph && graphData) { const graphNodeList = flatGraphDataNode(graphData.children); - // const graphConfigKey = graphNodeList.length > 20 ? 'dendrogram' : 'mindmap'; - const graphConfigKey = 'dendrogram'; + const graphConfigKey = graphNodeList.length > 20 ? 'dendrogram' : 'mindmap'; + // const graphConfigKey = 'mindmap'; // getLegendDataFilterFunctions(); - const toolbar = initToolBar({ onSearch: handleSeachNode, onClick: handleToolBarClick }); + // const toolbar = initToolBar({ onSearch: handleSeachNode, onClick: handleToolBarClick }); const tooltip = initTooltips(); const contextMenu = initContextMenu({ onMenuClick: handleContextMenuClick, @@ -532,7 +535,8 @@ const DomainManger: React.FC = ({ domainManger, dispatch }) => { .getContainer() .findAll((ele) => ele.get('name') === 'anchor-point'); anchorPoints.forEach((point) => { - if (value || point.get('links') > 0) point.show(); + // if (value || point.get('links') > 0) point.show(); + if (value) point.show(); else point.hide(); }); } @@ -655,6 +659,7 @@ const DomainManger: React.FC = ({ domainManger, dispatch }) => { return true; }, }, + 'scroll-canvas', 'drag-canvas', // config the shouldBegin and shouldEnd to make sure the create-edge is began and ended at anchor-point circles { @@ -679,10 +684,10 @@ const DomainManger: React.FC = ({ domainManger, dispatch }) => { return true; }, }, - { - type: 'zoom-canvas', - sensitivity: 0.3, // 设置缩放灵敏度,值越小,缩放越不敏感,默认值为 1 - }, + // { + // type: 'zoom-canvas', + // sensitivity: 0.3, // 设置缩放灵敏度,值越小,缩放越不敏感,默认值为 1 + // }, { type: 'activate-relations', trigger: 'mouseenter', // 触发方式,可以是 'mouseenter' 或 'click' @@ -693,7 +698,7 @@ const DomainManger: React.FC = ({ domainManger, dispatch }) => { layout: { ...graphConfigMap[graphConfigKey].layout, }, - plugins: [tooltip, toolbar, contextMenu], + plugins: [tooltip, contextMenu], defaultNode: { type: 'rect-node', @@ -751,8 +756,13 @@ const DomainManger: React.FC = ({ domainManger, dispatch }) => { label: '模型关系编辑', style: { stroke: '#296df3', + endArrow: true, }, }); + const sourceNode = e.edge.get('sourceNode'); + const targetnode = e.edge.get('targetNode'); + graphRef.current.setItemState(sourceNode, 'showAnchors', false); + graphRef.current.setItemState(targetnode, 'showAnchors', false); } // update the curveOffset for parallel edges @@ -804,6 +814,7 @@ const DomainManger: React.FC = ({ domainManger, dispatch }) => { label: '模型关系编辑', style: { stroke: '#296df3', + endArrow: true, }, }); } @@ -858,12 +869,12 @@ const DomainManger: React.FC = ({ domainManger, dispatch }) => { graphRef.current.data(graphData); graphRef.current.render(); - const nodeCount = graphRef.current.getNodes().length; - if (nodeCount < 10) { - lessNodeZoomRealAndMoveCenter(); - } else { - graphRef.current.fitView([80, 80]); - } + // const nodeCount = graphRef.current.getNodes().length; + // if (nodeCount < 10) { + lessNodeZoomRealAndMoveCenter(); + // } else { + // graphRef.current.fitView([80, 80]); + // } graphRef.current.on('node:click', (evt: any) => { const item = evt.item; // 被操作的节点 item @@ -934,7 +945,8 @@ const DomainManger: React.FC = ({ domainManger, dispatch }) => { graphRef.current.changeData(graphRootData); const rootNode = graphRef.current.findById('root'); graphRef.current.hideItem(rootNode); - graphRef.current.fitView(); + // graphRef.current.fitView(); + lessNodeZoomRealAndMoveCenter(); }; const saveModelRelationEdges = () => { @@ -948,7 +960,7 @@ const DomainManger: React.FC = ({ domainManger, dispatch }) => { return ( <> - { @@ -967,7 +979,22 @@ const DomainManger: React.FC = ({ domainManger, dispatch }) => { refreshGraphData(rootGraphData); }} /> - )} + )} */} + + { + handleSeachNode(text); + }} + onShowTypeChange={(showType) => { + graphShowTypeRef.current = showType; + setGraphShowTypeState(showType); + const rootGraphData = changeGraphData(dataSourceRef.current); + refreshGraphData(rootGraphData); + }} + onZoomIn={() => {}} + onZoomOut={() => {}} + /> { diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/style.less b/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/style.less index 7a780ddfe..16314e0ba 100644 --- a/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/style.less +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/style.less @@ -45,10 +45,5 @@ .graphLegendVisibleModeItem { padding: 3px; background: #fff; - position: absolute; - top: 58px; - left: 20px; - z-index: 1; - border: 1px solid #eee; min-width: 190px; } \ No newline at end of file diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/components/MetricInfoCreateForm.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/components/MetricInfoCreateForm.tsx index 31c5c9621..dbc09a872 100644 --- a/webapp/packages/supersonic-fe/src/pages/SemanticModel/components/MetricInfoCreateForm.tsx +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/components/MetricInfoCreateForm.tsx @@ -89,7 +89,7 @@ const MetricInfoCreateForm: React.FC = ({ const queryClassMeasureList = async () => { // const { code, data } = await getMeasureListByModelId(modelId); - const { code, data } = await getModelDetail({ modelId }); + const { code, data } = await getModelDetail({ modelId: modelId || metricItem?.modelId }); if (code === 200) { if (Array.isArray(data?.modelDetail?.measures)) { setClassMeasureList(data.modelDetail.measures);