From 078a81038f60d1a220e26b78b67156ff406a484d Mon Sep 17 00:00:00 2001 From: tristanliu <37809633+sevenliu1896@users.noreply.github.com> Date: Fri, 21 Jul 2023 15:30:38 +0800 Subject: [PATCH] [improvement][semantic-fe] Added an editing component to set filtering rules for Q&A. Now, the SQL editor will be accompanied by a list for display and control, to resolve ambiguity when using comma-separated values. [improvement][semantic-fe] Improved validation logic and prompt copywriting for data source/dimension/metric editing and creation. [improvement][semantic-fe] Improved user experience for visual modeling. Now, when using the legend to control the display/hide of data sources and their associated metric dimensions, the canvas will be re-layout based on the activated data source in the legend. Co-authored-by: tristanliu --- webapp/packages/supersonic-fe/package.json | 1 + webapp/packages/supersonic-fe/src/global.less | 38 +- .../supersonic-fe/src/locales/zh-CN/menu.ts | 2 +- .../components/DataSourceCreateForm.tsx | 20 +- .../components/DataSourceFieldForm.tsx | 98 ++- .../pages/SemanticModel/Datasource/style.less | 14 + .../pages/SemanticModel/ProjectManager.tsx | 21 +- .../XflowJsonSchemaFormDrawerForm.tsx | 5 +- .../SemanticModel/SemanticFlows/service.ts | 9 +- .../SemanticGraph/components/ContextMenu.tsx | 71 +- .../components/DeleteConfirmModal.tsx | 61 ++ .../SemanticGraph/components/Legend.tsx | 50 ++ .../SemanticGraph/components/ToolBar.tsx | 105 ++- .../SemanticGraph/components/ToolTips.tsx | 14 +- .../SemanticModel/SemanticGraph/index.tsx | 696 ++++++++++-------- .../SemanticModel/SemanticGraph/utils.ts | 205 ++++-- .../SemanticModel/SemanticGraphCanvas.tsx | 51 ++ .../components/AntdComponentDom/Tag.ts | 3 + .../components/ClassDataSourceTable.tsx | 3 + .../components/ClassDataSourceTypeModal.tsx | 112 ++- .../components/ClassDimensionTable.tsx | 56 +- .../components/ClassMetricTable.tsx | 75 +- .../components/CommonEditList/index.tsx | 164 +++++ .../components/CommonEditList/style.less | 10 + .../components/DimensionInfoModal.tsx | 68 +- .../Entity/DimensionMetricVisibleModal.tsx | 139 ---- .../DimensionMetricVisibleTableTransfer.tsx | 14 +- .../Entity/DimensionSearchVisibleModal.tsx | 138 ---- .../Entity/DimensionValueSettingForm.tsx | 9 +- .../components/Entity/MetricSettingForm.tsx | 195 ----- .../components/FormLabelRequire.tsx | 25 + .../SemanticModel/components/InfoTagList.tsx | 1 - .../components/MetricInfoCreateForm.tsx | 128 +++- .../components/MetricMeasuresFormTable.tsx | 20 +- .../components/TableTitleTooltips.tsx | 23 + .../pages/SemanticModel/components/style.less | 16 +- .../src/pages/SemanticModel/constant.ts | 20 + .../src/pages/SemanticModel/data.d.ts | 16 +- .../src/pages/SemanticModel/enum.ts | 6 + 39 files changed, 1541 insertions(+), 1161 deletions(-) create mode 100644 webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/components/DeleteConfirmModal.tsx create mode 100644 webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/components/Legend.tsx create mode 100644 webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraphCanvas.tsx create mode 100644 webapp/packages/supersonic-fe/src/pages/SemanticModel/components/AntdComponentDom/Tag.ts create mode 100644 webapp/packages/supersonic-fe/src/pages/SemanticModel/components/CommonEditList/index.tsx create mode 100644 webapp/packages/supersonic-fe/src/pages/SemanticModel/components/CommonEditList/style.less delete mode 100644 webapp/packages/supersonic-fe/src/pages/SemanticModel/components/Entity/DimensionMetricVisibleModal.tsx delete mode 100644 webapp/packages/supersonic-fe/src/pages/SemanticModel/components/Entity/DimensionSearchVisibleModal.tsx delete mode 100644 webapp/packages/supersonic-fe/src/pages/SemanticModel/components/Entity/MetricSettingForm.tsx create mode 100644 webapp/packages/supersonic-fe/src/pages/SemanticModel/components/FormLabelRequire.tsx create mode 100644 webapp/packages/supersonic-fe/src/pages/SemanticModel/components/TableTitleTooltips.tsx diff --git a/webapp/packages/supersonic-fe/package.json b/webapp/packages/supersonic-fe/package.json index 9a3620754..2136a0009 100644 --- a/webapp/packages/supersonic-fe/package.json +++ b/webapp/packages/supersonic-fe/package.json @@ -91,6 +91,7 @@ "react-ace": "^9.4.1", "react-dev-inspector": "^1.8.4", "react-dom": "^17.0.2", + "react-fast-marquee": "^1.6.0", "react-helmet-async": "^1.0.4", "react-spinners": "^0.10.6", "react-split-pane": "^2.0.3", diff --git a/webapp/packages/supersonic-fe/src/global.less b/webapp/packages/supersonic-fe/src/global.less index a319311e3..bbce2f2af 100644 --- a/webapp/packages/supersonic-fe/src/global.less +++ b/webapp/packages/supersonic-fe/src/global.less @@ -216,20 +216,30 @@ ol { right: 240px !important; } + .g6ContextMenuContainer { font-size: 12px; color: #545454; -} -.g6ContextMenuContainer li { - cursor: pointer; - list-style-type:none; - list-style: none; - margin-left: 0; -} -.g6ContextMenuContainer ul { - width: 100%; - padding: 0; -} -.g6ContextMenuContainer li:hover { - color: #aaa; -} \ No newline at end of file + min-width: 100px; + h3 { + padding-bottom: 5px; + border-bottom: 1px solid #4E86F5; + } + li { + cursor: pointer; + list-style-type:none; + // list-style: none; + margin-left: 0; + &:hover { + color: #4E86F5; + } + } + ul { + width: 100%; + padding: 0; + margin: 0; + } + .ant-tag { + transition: none; + } +} \ No newline at end of file diff --git a/webapp/packages/supersonic-fe/src/locales/zh-CN/menu.ts b/webapp/packages/supersonic-fe/src/locales/zh-CN/menu.ts index b78d31897..75179dcdf 100644 --- a/webapp/packages/supersonic-fe/src/locales/zh-CN/menu.ts +++ b/webapp/packages/supersonic-fe/src/locales/zh-CN/menu.ts @@ -7,7 +7,7 @@ export default { 'menu.exception.not-permission': '403', 'menu.exception.not-find': '404', 'menu.exception.server-error': '500', - 'menu.semanticModel': '语义建模', + 'menu.semanticModel': '模型管理', 'menu.chatSetting': '问答设置', 'menu.login': '登录', 'menu.chat': '问答对话', diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/Datasource/components/DataSourceCreateForm.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Datasource/components/DataSourceCreateForm.tsx index 4936e3732..4b41a4b35 100644 --- a/webapp/packages/supersonic-fe/src/pages/SemanticModel/Datasource/components/DataSourceCreateForm.tsx +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Datasource/components/DataSourceCreateForm.tsx @@ -10,6 +10,7 @@ import { createDatasource, updateDatasource, getColumns } from '../../service'; import type { Dispatch } from 'umi'; import type { StateType } from '../../model'; import { connect } from 'umi'; +import { isUndefined } from 'lodash'; export type CreateFormProps = { domainManger: StateType; @@ -47,6 +48,7 @@ const DataSourceCreateForm: React.FC = ({ const [fields, setFields] = useState([]); const [currentStep, setCurrentStep] = useState(0); const [saveLoading, setSaveLoading] = useState(false); + const [hasEmptyNameField, setHasEmptyNameField] = useState(false); const formValRef = useRef(initFormVal as any); const [form] = Form.useForm(); const { dataBaseConfig } = domainManger; @@ -54,6 +56,17 @@ const DataSourceCreateForm: React.FC = ({ formValRef.current = val; }; + useEffect(() => { + const hasEmpty = fields.some((item) => { + const { name, isCreateDimension, isCreateMetric } = item; + if ((isCreateMetric || isCreateDimension) && !name) { + return true; + } + return false; + }); + setHasEmptyNameField(hasEmpty); + }, [fields]); + const [fieldColumns, setFieldColumns] = useState(scriptColumns || []); useEffect(() => { if (scriptColumns) { @@ -310,7 +323,12 @@ const DataSourceCreateForm: React.FC = ({ 上一步 - diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/Datasource/components/DataSourceFieldForm.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Datasource/components/DataSourceFieldForm.tsx index 5ec0645ab..e81296a27 100644 --- a/webapp/packages/supersonic-fe/src/pages/SemanticModel/Datasource/components/DataSourceFieldForm.tsx +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Datasource/components/DataSourceFieldForm.tsx @@ -1,8 +1,21 @@ import React from 'react'; -import { Table, Select, Checkbox, Input } from 'antd'; -import type { FieldItem } from '../data'; +import { Table, Select, Checkbox, Input, Alert, Space, Tooltip } from 'antd'; +import TableTitleTooltips from '../../components/TableTitleTooltips'; import { isUndefined } from 'lodash'; +import { ExclamationCircleOutlined } from '@ant-design/icons'; +import Marquee from 'react-fast-marquee'; import { TYPE_OPTIONS, DATE_FORMATTER, AGG_OPTIONS, EnumDataSourceType } from '../constants'; +import styles from '../style.less'; + +type FieldItem = { + bizName: string; + sqlType: string; + name: string; + type: EnumDataSourceType; + agg?: string; + checked?: number; + dateFormat?: string; +}; type Props = { fields: FieldItem[]; @@ -11,6 +24,16 @@ type Props = { const { Option } = Select; +const getCreateFieldName = (type: EnumDataSourceType) => { + const isCreateName = [EnumDataSourceType.CATEGORICAL, EnumDataSourceType.TIME].includes( + type as EnumDataSourceType, + ) + ? 'isCreateDimension' + : 'isCreateMetric'; + return isCreateName; + // const editState = !isUndefined(record[isCreateName]) ? !!record[isCreateName] : true; +}; + const FieldForm: React.FC = ({ fields, onFieldChange }) => { const handleFieldChange = (record: FieldItem, fieldName: string, value: any) => { onFieldChange(record.bizName, { @@ -58,10 +81,14 @@ const FieldForm: React.FC = ({ fields, onFieldChange }) => { timeGranularity: undefined, }; } + const isCreateName = getCreateFieldName(value); + const editState = !isUndefined(record[isCreateName]) ? !!record[isCreateName] : true; // handleFieldChange(record, 'type', value); onFieldChange(record.bizName, { ...record, type: value, + name: '', + [isCreateName]: editState, ...defaultParams, }); }} @@ -105,28 +132,38 @@ const FieldForm: React.FC = ({ fields, onFieldChange }) => { if (type === EnumDataSourceType.TIME) { const dateFormat = fields.find((field) => field.bizName === record.bizName)?.dateFormat; return ( - + + + + + + ); } return <>; }, }, { - title: '快速创建', + title: ( + + ), dataIndex: 'fastCreate', width: 100, render: (_: any, record: FieldItem) => { @@ -140,11 +177,7 @@ const FieldForm: React.FC = ({ fields, onFieldChange }) => { EnumDataSourceType.MEASURES, ].includes(type as EnumDataSourceType) ) { - const isCreateName = [EnumDataSourceType.CATEGORICAL, EnumDataSourceType.TIME].includes( - type as EnumDataSourceType, - ) - ? 'isCreateDimension' - : 'isCreateMetric'; + const isCreateName = getCreateFieldName(type); const editState = !isUndefined(record[isCreateName]) ? !!record[isCreateName] : true; return ( = ({ fields, onFieldChange }) => { onFieldChange(record.bizName, { ...record, name: '', + checked: value, [isCreateName]: value, }); } else { - handleFieldChange(record, isCreateName, value); + // handleFieldChange(record, isCreateName, value); + onFieldChange(record.bizName, { + ...record, + checked: value, + [isCreateName]: value, + }); } }} > { @@ -186,10 +226,18 @@ const FieldForm: React.FC = ({ fields, onFieldChange }) => { return ( <> + + 为了保障同一个主题域下维度/指标列表唯一,消除歧义,若本主题域下的多个数据源存在相同的字段名并且都勾选了快速创建,系统默认这些相同字段的指标维度是同一个,同时列表中将只显示最后一次创建的指标/维度。 + + } + /> dataSource={fields} columns={columns} - className="fields-table" rowKey="bizName" pagination={false} scroll={{ y: 500 }} diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/Datasource/style.less b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Datasource/style.less index b15411466..eccb672d7 100644 --- a/webapp/packages/supersonic-fe/src/pages/SemanticModel/Datasource/style.less +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Datasource/style.less @@ -757,3 +757,17 @@ height: 16px !important; margin: 0 3px 4px; } + +.dataSourceFieldsName { + background: #fff; + border-color: #ff4d4f; + &:hover { + border-color: #ff4d4f; + } + &:focus { + border-color: #ff7875; + box-shadow: 0 0 0 2px #ff4d4f33; + border-right-width: 1px; + outline: 0; + } +} \ No newline at end of file diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/ProjectManager.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/ProjectManager.tsx index 9553d3d71..7b7541382 100644 --- a/webapp/packages/supersonic-fe/src/pages/SemanticModel/ProjectManager.tsx +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/ProjectManager.tsx @@ -11,10 +11,9 @@ import OverView from './components/OverView'; import styles from './components/style.less'; import type { StateType } from './model'; import { DownOutlined } from '@ant-design/icons'; -import SemanticFlow from './SemanticFlows'; import { ISemantic } from './data'; import { findLeafNodesFromDomainList } from './utils'; -import SemanticGraph from './SemanticGraph'; +import SemanticGraphCanvas from './SemanticGraphCanvas'; import { getDomainList } from './service'; import type { Dispatch } from 'umi'; @@ -35,6 +34,10 @@ const DomainManger: React.FC = ({ domainManger, dispatch }) => { const [open, setOpen] = useState(false); const [activeKey, setActiveKey] = useState(menuKey); + useEffect(() => { + setActiveKey(menuKey); + }, [menuKey]); + const initSelectedDomain = (domainList: ISemantic.IDomainItem[]) => { const targetNode = domainList.filter((item: any) => { return `${item.id}` === modelId; @@ -145,21 +148,13 @@ const DomainManger: React.FC = ({ domainManger, dispatch }) => { ]; const isModelItem = [ - // { - // label: '关系可视化', - // key: 'graph', - // children: ( - //
- // - //
- // ), - // }, { label: '可视化建模', key: 'xflow', children: (
- + {/* */} +
), }, @@ -192,7 +187,7 @@ const DomainManger: React.FC = ({ domainManger, dispatch }) => { return (
- +

= (props) => { ...targetData, label: dataSourceInfo.name, payload: dataSourceInfo, - id: `dataSource-${dataSourceInfo.id}`, + id: `${SemanticNodeType.DATASOURCE}-${dataSourceInfo.id}`, }); setDataSourceItem(undefined); commandService.executeCommand(XFlowGraphCommands.SAVE_GRAPH_DATA.id, { @@ -144,7 +145,7 @@ const XflowJsonSchemaFormDrawerForm: React.FC = (props) => { ...targetData, label: dataSourceInfo.name, payload: dataSourceInfo, - id: `dataSource-${dataSourceInfo.id}`, + id: `${SemanticNodeType.DATASOURCE}-${dataSourceInfo.id}`, }); setDataSourceItem(undefined); commandService.executeCommand(XFlowGraphCommands.SAVE_GRAPH_DATA.id, { diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticFlows/service.ts b/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticFlows/service.ts index e4bb9c33c..a3d5f0b1f 100644 --- a/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticFlows/service.ts +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticFlows/service.ts @@ -7,6 +7,7 @@ import type { NsDeployDagCmd } from './CmdExtensions/CmdDeploy'; import { getRelationConfigInfo, addClassInfoAsDataSourceParents } from './utils'; import { cloneDeep } from 'lodash'; import type { IDataSource } from '../data'; +import { SemanticNodeType } from '../enum'; import { getDatasourceList, deleteDatasource, @@ -57,7 +58,7 @@ export namespace GraphApi { export const createDataSourceNode = (dataSourceItem: IDataSource.IDataSourceItem) => { const { id, name } = dataSourceItem; - const nodeId = `dataSource-${id}`; + const nodeId = `${SemanticNodeType.DATASOURCE}-${id}`; return { ...NODE_COMMON_PROPS, id: nodeId, @@ -89,7 +90,7 @@ export namespace GraphApi { const dataSourceMap = data.reduce( (itemMap: Record, item: IDataSource.IDataSourceItem) => { const { id, name } = item; - itemMap[`dataSource-${id}`] = item; + itemMap[`${SemanticNodeType.DATASOURCE}-${id}`] = item; itemMap[name] = item; return itemMap; @@ -114,7 +115,7 @@ export namespace GraphApi { mergeNodes = data.reduce( (mergeNodeList: NsGraph.INodeConfig[], item: IDataSource.IDataSourceItem) => { const { id } = item; - const targetDataSourceItem = nodesMap[`dataSource-${id}`]; + const targetDataSourceItem = nodesMap[`${SemanticNodeType.DATASOURCE}-${id}`]; if (targetDataSourceItem) { mergeNodeList.push({ ...targetDataSourceItem, @@ -166,7 +167,7 @@ export namespace GraphApi { const { list } = data; const nodes: NsGraph.INodeConfig[] = list.map((item: any) => { const { id, name } = item; - const nodeId = `dimension-${id}`; + const nodeId = `${SemanticNodeType.DIMENSION}-${id}`; return { ...NODE_COMMON_PROPS, id: nodeId, diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/components/ContextMenu.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/components/ContextMenu.tsx index 6166fbeee..23ca7d24a 100644 --- a/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/components/ContextMenu.tsx +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/components/ContextMenu.tsx @@ -1,52 +1,46 @@ import G6 from '@antv/g6'; import '../style.less'; -// define the CSS with the id of your menu +import { Item } from '@antv/g6-core'; +import { presetsTagDomString } from '../../components/AntdComponentDom/Tag'; +import { SemanticNodeType } from '../../enum'; +import { SEMANTIC_NODE_TYPE_CONFIG } from '../../constant'; -// insertCss(` -// #contextMenu { -// position: absolute; -// list-style-type: none; -// padding: 10px 8px; -// left: -150px; -// background-color: rgba(255, 255, 255, 0.9); -// border: 1px solid #e2e2e2; -// border-radius: 4px; -// font-size: 12px; -// color: #545454; -// } -// #contextMenu li { -// cursor: pointer; -// list-style-type:none; -// list-style: none; -// margin-left: 0px; -// } -// #contextMenu li:hover { -// color: #aaa; -// } -// `); +type InitContextMenuProps = { + graphShowType: string; + onMenuClick?: (key: string, item: Item) => void; +}; -const initContextMenu = () => { - const contextMenu = new G6.Menu({ +export const getMenuConfig = (props?: InitContextMenuProps) => { + const { graphShowType, onMenuClick } = props || {}; + return { getContent(evt) { - const itemType = evt!.item!.getType(); - console.log(this, evt?.item?._cfg, 333); const nodeData = evt?.item?._cfg?.model; - const { name } = nodeData as any; + const { name, nodeType } = nodeData as any; if (nodeData) { + const nodeTypeConfig = SEMANTIC_NODE_TYPE_CONFIG[nodeType] || {}; + let ulNode = `
    +
  • 编辑
  • +
  • 删除
  • +
`; + if (nodeType === SemanticNodeType.DATASOURCE) { + if (graphShowType) { + const typeString = graphShowType === SemanticNodeType.DIMENSION ? '维度' : '指标'; + ulNode = `
    +
  • 新增${typeString}
  • +
`; + } + } const header = `${name}`; return `
-

${header}

-
    -
  • 编辑
  • -
  • 删除
  • -
+

${presetsTagDomString(nodeTypeConfig.label, nodeTypeConfig.color)}${header}

+ ${ulNode}
`; } return `
当前节点信息获取失败
`; }, handleMenuClick(target, item) { - console.log(contextMenu, target, item); - const graph = contextMenu._cfgs.graph; + const targetKey = target.getAttribute('key') || ''; + onMenuClick?.(targetKey, item); }, // offsetX and offsetY include the padding of the parent container // 需要加上父级容器的 padding-left 16 与自身偏移量 10 @@ -56,7 +50,12 @@ const initContextMenu = () => { // the types of items that allow the menu show up // 在哪些类型的元素上响应 itemTypes: ['node'], - }); + }; +}; + +const initContextMenu = (props?: InitContextMenuProps) => { + const config = getMenuConfig(props); + const contextMenu = new G6.Menu(config); return contextMenu; }; diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/components/DeleteConfirmModal.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/components/DeleteConfirmModal.tsx new file mode 100644 index 000000000..131132876 --- /dev/null +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/components/DeleteConfirmModal.tsx @@ -0,0 +1,61 @@ +import { Button, Modal, message } from 'antd'; +import React, { useState } from 'react'; +import { SemanticNodeType } from '../../enum'; +import { deleteDimension, deleteMetric } from '../../service'; + +type Props = { + nodeData: any; + nodeType: SemanticNodeType; + onOkClick: () => void; + onCancelClick: () => void; + open: boolean; +}; + +const DeleteConfirmModal: React.FC = ({ + nodeData, + nodeType, + onOkClick, + onCancelClick, + open = false, +}) => { + const [confirmLoading, setConfirmLoading] = useState(false); + const deleteNode = async () => { + setConfirmLoading(true); + const { id } = nodeData; + let deleteQuery = deleteDimension; + if (nodeType === SemanticNodeType.METRIC) { + deleteQuery = deleteMetric; + } + const { code, msg } = await deleteQuery(id); + setConfirmLoading(false); + if (code === 200) { + onOkClick(); + message.success('删除成功!'); + } else { + message.error(msg); + } + }; + + const handleOk = () => { + deleteNode(); + }; + + return ( + <> + + <> + {nodeData?.name} + 将被删除,是否确认? + + + + ); +}; + +export default DeleteConfirmModal; diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/components/Legend.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/components/Legend.tsx new file mode 100644 index 000000000..27c948fa9 --- /dev/null +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/components/Legend.tsx @@ -0,0 +1,50 @@ +import G6 from '@antv/g6'; + +const initLegend = ({ nodeData, filterFunctions }) => { + const legend = new G6.Legend({ + data: { + nodes: nodeData, + }, + align: 'center', + layout: 'horizontal', // vertical + position: 'bottom-right', + vertiSep: 12, + horiSep: 24, + offsetY: -24, + padding: [10, 50, 10, 50], + containerStyle: { + fill: '#a6ccff', + lineWidth: 1, + }, + title: '可见数据源', + titleConfig: { + position: 'center', + offsetX: 0, + offsetY: 12, + + style: { + fontSize: 12, + fontWeight: 500, + fill: '#000', + }, + }, + filter: { + enable: true, + multiple: true, + trigger: 'click', + graphActiveState: 'activeByLegend', + graphInactiveState: 'inactiveByLegend', + filterFunctions, + legendStateStyles: { + active: { + lineWidth: 2, + fill: '#f0f7ff', + stroke: '#a6ccff', + }, + }, + }, + }); + + return legend; +}; +export default initLegend; diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/components/ToolBar.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/components/ToolBar.tsx index fb3a600e0..c7c92eb89 100644 --- a/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/components/ToolBar.tsx +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/components/ToolBar.tsx @@ -1,45 +1,90 @@ -import G6 from '@antv/g6'; +import G6, { Graph } from '@antv/g6'; import { createDom } from '@antv/dom-util'; - +import { RefreshGraphData } from '../../data'; const searchIconSvgPath = ``; -const searchNode = (graph) => { +// const searchNode = (graph) => { +// const toolBarSearchInput = document.getElementById('toolBarSearchInput') as HTMLInputElement; +// const searchText = toolBarSearchInput.value.trim(); +// let lastFoundNode = null; +// graph.getNodes().forEach((node) => { +// const model = node.getModel(); +// const isFound = searchText && model.label.includes(searchText); +// if (isFound) { +// graph.setItemState(node, 'active', true); +// lastFoundNode = node; +// } else { +// graph.setItemState(node, 'active', false); +// } +// }); + +// if (lastFoundNode) { +// // 将视图移动到找到的节点位置 +// graph.focusItem(lastFoundNode, true, { +// duration: 300, +// easing: 'easeCubic', +// }); +// } +// }; + +interface Node { + label: string; + children?: Node[]; +} + +function findNodesByLabel(query: string, nodes: Node[]): Node[] { + const result: Node[] = []; + + for (const node of nodes) { + let match = false; + let children: Node[] = []; + + // 如果节点的label包含查询字符串,我们将其标记为匹配 + if (node.label.includes(query)) { + match = true; + } + + // 我们还需要在子节点中进行搜索 + if (node.children) { + children = findNodesByLabel(query, node.children); + if (children.length > 0) { + match = true; + } + } + + // 如果节点匹配或者其子节点匹配,我们将其添加到结果中 + if (match) { + result.push({ ...node, children }); + } + } + + return result; +} + +const searchNode = (graph: Graph, refreshGraphData?: RefreshGraphData) => { const toolBarSearchInput = document.getElementById('toolBarSearchInput') as HTMLInputElement; const searchText = toolBarSearchInput.value.trim(); - let lastFoundNode = null; - graph.getNodes().forEach((node) => { - const model = node.getModel(); - const isFound = searchText && model.label.includes(searchText); - if (isFound) { - graph.setItemState(node, 'active', true); - lastFoundNode = node; - } else { - graph.setItemState(node, 'active', false); - } + const graphData = graph.get('initGraphData'); + const filterChildrenData = findNodesByLabel(searchText, graphData.children); + refreshGraphData?.({ + ...graphData, + children: filterChildrenData, }); - - if (lastFoundNode) { - // 将视图移动到找到的节点位置 - graph.focusItem(lastFoundNode, true, { - duration: 300, - easing: 'easeCubic', - }); - } }; -const generatorSearchInputDom = (graph) => { +const generatorSearchInputDom = (graph: Graph, refreshGraphData: RefreshGraphData) => { const domString = ''; const searchInputDom = createDom(domString); searchInputDom.addEventListener('keydown', (event) => { if (event.key === 'Enter') { - searchNode(graph); + searchNode(graph, refreshGraphData); } }); return searchInputDom; }; -const generatorSearchBtnDom = (graph) => { +const generatorSearchBtnDom = (graph: Graph) => { const domString = ` + } + /> + )} ); }; -export default ClassDataSourceTypeModal; + +export default connect(({ domainManger }: { domainManger: StateType }) => ({ + domainManger, +}))(ClassDataSourceTypeModal); diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/components/ClassDimensionTable.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/components/ClassDimensionTable.tsx index d4319d877..0cd609945 100644 --- a/webapp/packages/supersonic-fe/src/pages/SemanticModel/components/ClassDimensionTable.tsx +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/components/ClassDimensionTable.tsx @@ -6,14 +6,9 @@ import type { Dispatch } from 'umi'; import { connect } from 'umi'; import type { StateType } from '../model'; import { SENSITIVE_LEVEL_ENUM } from '../constant'; -import { - getDatasourceList, - getDimensionList, - createDimension, - updateDimension, - deleteDimension, -} from '../service'; +import { getDatasourceList, getDimensionList, deleteDimension } from '../service'; import DimensionInfoModal from './DimensionInfoModal'; +import { ISemantic } from '../data'; import moment from 'moment'; import styles from './style.less'; @@ -25,7 +20,7 @@ type Props = { const ClassDimensionTable: React.FC = ({ domainManger, dispatch }) => { const { selectDomainId } = domainManger; const [createModalVisible, setCreateModalVisible] = useState(false); - const [dimensionItem, setDimensionItem] = useState(); + const [dimensionItem, setDimensionItem] = useState(); const [dataSourceList, setDataSourceList] = useState([]); const [pagination, setPagination] = useState({ current: 1, @@ -45,7 +40,7 @@ const ClassDimensionTable: React.FC = ({ domainManger, dispatch }) => { let resData: any = {}; if (code === 200) { setPagination({ - pageSize, + pageSize: Math.min(pageSize, 100), current, total, }); @@ -175,36 +170,6 @@ const ClassDimensionTable: React.FC = ({ domainManger, dispatch }) => { }, ]; - const saveDimension = async (fieldsValue: any, reloadState: boolean = true) => { - const queryParams = { - domainId: selectDomainId, - type: 'categorical', - ...fieldsValue, - }; - let saveDimensionQuery = createDimension; - if (queryParams.id) { - saveDimensionQuery = updateDimension; - } - - const { code, msg } = await saveDimensionQuery(queryParams); - - if (code === 200) { - setCreateModalVisible(false); - if (reloadState) { - message.success('编辑维度成功'); - actionRef?.current?.reload(); - } - dispatch({ - type: 'domainManger/queryDimensionList', - payload: { - domainId: selectDomainId, - }, - }); - return; - } - message.error(msg); - }; - return ( <> = ({ domainManger, dispatch }) => { {createModalVisible && ( { + setCreateModalVisible(false); + actionRef?.current?.reload(); + dispatch({ + type: 'domainManger/queryDimensionList', + payload: { + domainId: selectDomainId, + }, + }); + return; + }} onCancel={() => { setCreateModalVisible(false); }} diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/components/ClassMetricTable.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/components/ClassMetricTable.tsx index a77e389e0..1a87709d3 100644 --- a/webapp/packages/supersonic-fe/src/pages/SemanticModel/components/ClassMetricTable.tsx +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/components/ClassMetricTable.tsx @@ -6,7 +6,7 @@ import type { Dispatch } from 'umi'; import { connect } from 'umi'; import type { StateType } from '../model'; import { SENSITIVE_LEVEL_ENUM } from '../constant'; -import { creatExprMetric, updateExprMetric, queryMetric, deleteMetric } from '../service'; +import { queryMetric, deleteMetric } from '../service'; import MetricInfoCreateForm from './MetricInfoCreateForm'; @@ -39,7 +39,7 @@ const ClassMetricTable: React.FC = ({ domainManger, dispatch }) => { let resData: any = {}; if (code === 200) { setPagination({ - pageSize, + pageSize: Math.min(pageSize, 100), current, total, }); @@ -166,36 +166,36 @@ const ClassMetricTable: React.FC = ({ domainManger, dispatch }) => { }, ]; - const saveMetric = async (fieldsValue: any, reloadState: boolean = true) => { - const queryParams = { - domainId: selectDomainId, - ...fieldsValue, - }; - if (queryParams.typeParams && !queryParams.typeParams.expr) { - message.error('度量表达式不能为空'); - return; - } - let saveMetricQuery = creatExprMetric; - if (queryParams.id) { - saveMetricQuery = updateExprMetric; - } - const { code, msg } = await saveMetricQuery(queryParams); - if (code === 200) { - message.success('编辑指标成功'); - setCreateModalVisible(false); - if (reloadState) { - actionRef?.current?.reload(); - } - dispatch({ - type: 'domainManger/queryMetricList', - payload: { - domainId: selectDomainId, - }, - }); - return; - } - message.error(msg); - }; + // const saveMetric = async (fieldsValue: any, reloadState: boolean = true) => { + // const queryParams = { + // domainId: selectDomainId, + // ...fieldsValue, + // }; + // if (queryParams.typeParams && !queryParams.typeParams.expr) { + // message.error('度量表达式不能为空'); + // return; + // } + // let saveMetricQuery = creatExprMetric; + // if (queryParams.id) { + // saveMetricQuery = updateExprMetric; + // } + // const { code, msg } = await saveMetricQuery(queryParams); + // if (code === 200) { + // message.success('编辑指标成功'); + // setCreateModalVisible(false); + // if (reloadState) { + // actionRef?.current?.reload(); + // } + // dispatch({ + // type: 'domainManger/queryMetricList', + // payload: { + // domainId: selectDomainId, + // }, + // }); + // return; + // } + // message.error(msg); + // }; return ( <> @@ -246,8 +246,15 @@ const ClassMetricTable: React.FC = ({ domainManger, dispatch }) => { domainId={Number(selectDomainId)} createModalVisible={createModalVisible} metricItem={metricItem} - onSubmit={(values) => { - saveMetric(values); + onSubmit={() => { + setCreateModalVisible(false); + actionRef?.current?.reload(); + dispatch({ + type: 'domainManger/queryMetricList', + payload: { + domainId: selectDomainId, + }, + }); }} onCancel={() => { setCreateModalVisible(false); diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/components/CommonEditList/index.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/components/CommonEditList/index.tsx new file mode 100644 index 000000000..6b5128a57 --- /dev/null +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/components/CommonEditList/index.tsx @@ -0,0 +1,164 @@ +import React, { useState, useEffect } from 'react'; +import { List, Collapse, Button } from 'antd'; +import { uuid } from '@/utils/utils'; +import SqlEditor from '@/components/SqlEditor'; + +import styles from './style.less'; + +const { Panel } = Collapse; + +type Props = { + title?: string; + defaultCollapse?: boolean; + value?: string[]; + onChange?: (list: string[]) => void; +}; + +type ListItem = { + id: string; + sql: string; +}; + +type List = ListItem[]; + +const CommonEditList: React.FC = ({ title, defaultCollapse = false, value, onChange }) => { + const [listDataSource, setListDataSource] = useState([]); + const [currentSql, setCurrentSql] = useState(''); + const [activeKey, setActiveKey] = useState(); + const [currentRecord, setCurrentRecord] = useState(); + + useEffect(() => { + if (Array.isArray(value)) { + const list = value.map((sql: string) => { + return { + id: uuid(), + sql, + }; + }); + setListDataSource(list); + } + }, [value]); + + const handleListChange = (listDataSource: List) => { + const sqlList = listDataSource.map((item) => { + return item.sql; + }); + onChange?.(sqlList); + }; + + return ( +
+ {}} + ghost + > + { + if (!currentRecord && !currentSql) { + setActiveKey(undefined); + return; + } + if (currentRecord) { + const list = [...listDataSource].map((item) => { + if (item.id === currentRecord.id) { + return { + ...item, + sql: currentSql, + }; + } + return item; + }); + setListDataSource(list); + handleListChange(list); + } else { + const list = [ + ...listDataSource, + { + id: uuid(), + sql: currentSql, + }, + ]; + setListDataSource(list); + handleListChange(list); + } + + setActiveKey(undefined); + }} + > + 确认 + + ) : ( + + ) + } + showArrow={false} + > +
+ { + setCurrentSql(sql); + }} + /> +
+
+
+ ( + { + setCurrentSql(item.sql); + setCurrentRecord(item); + setActiveKey('editor'); + }} + > + 编辑 + , + { + const list = [...listDataSource].filter(({ id }) => { + return item.id !== id; + }); + handleListChange(list); + setListDataSource(list); + }} + > + 删除 + , + ]} + > + + + )} + /> +
+ ); +}; + +export default CommonEditList; diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/components/CommonEditList/style.less b/webapp/packages/supersonic-fe/src/pages/SemanticModel/components/CommonEditList/style.less new file mode 100644 index 000000000..9bb2b1c47 --- /dev/null +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/components/CommonEditList/style.less @@ -0,0 +1,10 @@ +.commonEditList { + :global { + .ant-collapse-header { + padding: 0 !important; + } + .ant-collapse { + padding-bottom: 20px; + } + } +} \ No newline at end of file diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/components/DimensionInfoModal.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/components/DimensionInfoModal.tsx index 9cb2f8fcb..efddaee79 100644 --- a/webapp/packages/supersonic-fe/src/pages/SemanticModel/components/DimensionInfoModal.tsx +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/components/DimensionInfoModal.tsx @@ -1,17 +1,20 @@ -import React, { useEffect, useState } from 'react'; -import { Button, Form, Input, Modal, Select, List } from 'antd'; +import React, { useEffect } from 'react'; +import { Button, Form, Input, Modal, Select } from 'antd'; import { SENSITIVE_LEVEL_OPTIONS } from '../constant'; import { formLayout } from '@/components/FormHelper/utils'; import SqlEditor from '@/components/SqlEditor'; import InfoTagList from './InfoTagList'; +import { ISemantic } from '../data'; +import { createDimension, updateDimension } from '../service'; import { message } from 'antd'; export type CreateFormProps = { - dimensionItem: any; + domainId: number; + dimensionItem?: ISemantic.IDimensionItem; onCancel: () => void; bindModalVisible: boolean; dataSourceList: any[]; - onSubmit: (values: any) => Promise; + onSubmit: (values?: any) => void; }; const FormItem = Form.Item; @@ -20,42 +23,56 @@ const { Option } = Select; const { TextArea } = Input; const DimensionInfoModal: React.FC = ({ + domainId, onCancel, bindModalVisible, dimensionItem, dataSourceList, onSubmit: handleUpdate, }) => { - const isEdit = dimensionItem?.id; - const [formVals, setFormVals] = useState({ - roleCode: '', - users: [], - effectiveTime: 1, - }); + const isEdit = !!dimensionItem?.id; const [form] = Form.useForm(); - const { setFieldsValue } = form; + const { setFieldsValue, resetFields } = form; const handleSubmit = async () => { const fieldsValue = await form.validateFields(); - setFormVals({ ...fieldsValue }); - try { - await handleUpdate(fieldsValue); - } catch (error) { - message.error('保存失败,接口调用出错'); + await saveDimension(fieldsValue); + }; + + const saveDimension = async (fieldsValue: any) => { + const queryParams = { + domainId, + type: 'categorical', + ...fieldsValue, + }; + let saveDimensionQuery = createDimension; + if (queryParams.id) { + saveDimensionQuery = updateDimension; } + const { code, msg } = await saveDimensionQuery(queryParams); + if (code === 200) { + message.success('编辑维度成功'); + handleUpdate(fieldsValue); + return; + } + message.error(msg); }; const setFormVal = () => { - console.log(dimensionItem, 'dimensionItem'); setFieldsValue(dimensionItem); }; useEffect(() => { if (dimensionItem) { setFormVal(); + } else { + resetFields(); } - }, [dimensionItem]); + if (!isEdit && Array.isArray(dataSourceList) && dataSourceList[0]?.id) { + setFieldsValue({ datasourceId: dataSourceList[0].id }); + } + }, [dimensionItem, dataSourceList]); const renderFooter = () => { return ( @@ -141,7 +158,12 @@ const DimensionInfoModal: React.FC = ({ >