[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 <tristanliu@tencent.com>
This commit is contained in:
tristanliu
2023-07-21 15:30:38 +08:00
committed by GitHub
parent 6492316e23
commit 078a81038f
39 changed files with 1541 additions and 1161 deletions

View File

@@ -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",

View File

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

View File

@@ -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': '问答对话',

View File

@@ -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<CreateFormProps> = ({
const [fields, setFields] = useState<any[]>([]);
const [currentStep, setCurrentStep] = useState(0);
const [saveLoading, setSaveLoading] = useState(false);
const [hasEmptyNameField, setHasEmptyNameField] = useState<boolean>(false);
const formValRef = useRef(initFormVal as any);
const [form] = Form.useForm();
const { dataBaseConfig } = domainManger;
@@ -54,6 +56,17 @@ const DataSourceCreateForm: React.FC<CreateFormProps> = ({
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<CreateFormProps> = ({
</Button>
<Button onClick={onCancel}></Button>
<Button type="primary" loading={saveLoading} onClick={handleNext}>
<Button
type="primary"
loading={saveLoading}
onClick={handleNext}
disabled={hasEmptyNameField}
>
</Button>
</>

View File

@@ -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<Props> = ({ fields, onFieldChange }) => {
const handleFieldChange = (record: FieldItem, fieldName: string, value: any) => {
onFieldChange(record.bizName, {
@@ -58,10 +81,14 @@ const FieldForm: React.FC<Props> = ({ 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<Props> = ({ fields, onFieldChange }) => {
if (type === EnumDataSourceType.TIME) {
const dateFormat = fields.find((field) => field.bizName === record.bizName)?.dateFormat;
return (
<Select
placeholder="时间格式"
value={dateFormat}
onChange={(value) => {
handleFieldChange(record, 'dateFormat', value);
}}
defaultValue={DATE_FORMATTER[0]}
style={{ width: '100%' }}
>
{DATE_FORMATTER.map((item) => (
<Option key={item} value={item}>
{item}
</Option>
))}
</Select>
<Space>
<Select
placeholder="时间格式"
value={dateFormat}
onChange={(value) => {
handleFieldChange(record, 'dateFormat', value);
}}
defaultValue={DATE_FORMATTER[0]}
style={{ minWidth: 180 }}
>
{DATE_FORMATTER.map((item) => (
<Option key={item} value={item}>
{item}
</Option>
))}
</Select>
<Tooltip title="请选择数据库中时间字段对应格式">
<ExclamationCircleOutlined />
</Tooltip>
</Space>
);
}
return <></>;
},
},
{
title: '快速创建',
title: (
<TableTitleTooltips
title="快速创建"
tooltips="若勾选快速创建并填写名称,将会把该维度/指标直接创建到维度/指标列表"
/>
),
dataIndex: 'fastCreate',
width: 100,
render: (_: any, record: FieldItem) => {
@@ -140,11 +177,7 @@ const FieldForm: React.FC<Props> = ({ 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 (
<Checkbox
@@ -155,14 +188,21 @@ const FieldForm: React.FC<Props> = ({ 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,
});
}
}}
>
<Input
className={!name && styles.dataSourceFieldsName}
value={name}
disabled={!editState}
onChange={(e) => {
@@ -186,10 +226,18 @@ const FieldForm: React.FC<Props> = ({ fields, onFieldChange }) => {
return (
<>
<Alert
style={{ marginBottom: '10px' }}
banner
message={
<Marquee pauseOnHover gradient={false}>
//
</Marquee>
}
/>
<Table<FieldItem>
dataSource={fields}
columns={columns}
className="fields-table"
rowKey="bizName"
pagination={false}
scroll={{ y: 500 }}

View File

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

View File

@@ -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<Props> = ({ domainManger, dispatch }) => {
const [open, setOpen] = useState(false);
const [activeKey, setActiveKey] = useState<string>(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<Props> = ({ domainManger, dispatch }) => {
];
const isModelItem = [
// {
// label: '关系可视化',
// key: 'graph',
// children: (
// <div style={{ width: '100%', height: 'calc(100vh - 200px)' }}>
// <SemanticGraph domainId={selectDomainId} />
// </div>
// ),
// },
{
label: '可视化建模',
key: 'xflow',
children: (
<div style={{ width: '100%', height: 'calc(100vh - 200px)' }}>
<SemanticFlow />
{/* <SemanticFlow /> */}
<SemanticGraphCanvas />
</div>
),
},
@@ -192,7 +187,7 @@ const DomainManger: React.FC<Props> = ({ domainManger, dispatch }) => {
return (
<div className={styles.projectBody}>
<Helmet title={'语义建模-超音数'} />
<Helmet title={'模型管理-超音数'} />
<div className={styles.projectManger}>
<h2 className={styles.title}>
<Popover

View File

@@ -9,6 +9,7 @@ import DataSourceRelationFormDrawer from './DataSourceRelationFormDrawer';
import DataSourceCreateForm from '../../Datasource/components/DataSourceCreateForm';
import ClassDataSourceTypeModal from '../../components/ClassDataSourceTypeModal';
import { GraphApi } from '../service';
import { SemanticNodeType } from '../../enum';
import type { StateType } from '../../model';
import DataSource from '../../Datasource';
@@ -112,7 +113,7 @@ const XflowJsonSchemaFormDrawerForm: React.FC<CreateFormProps> = (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<CreateFormProps> = (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, {

View File

@@ -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<string, IDataSource.IDataSourceItem>, 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,

View File

@@ -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 = `<ul>
<li title='编辑' key='edit' >编辑</li>
<li title='删除' key='delete' >删除</li>
</ul>`;
if (nodeType === SemanticNodeType.DATASOURCE) {
if (graphShowType) {
const typeString = graphShowType === SemanticNodeType.DIMENSION ? '维度' : '指标';
ulNode = `<ul>
<li title='新增${typeString}' key='create' >新增${typeString}</li>
</ul>`;
}
}
const header = `${name}`;
return `<div class="g6ContextMenuContainer">
<h3>${header}</h3>
<ul>
<li title='2'>编辑</li>
<li title='1'>删除</li>
</ul>
<h3>${presetsTagDomString(nodeTypeConfig.label, nodeTypeConfig.color)}${header}</h3>
${ulNode}
</div>`;
}
return `<div>当前节点信息获取失败</div>`;
},
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;
};

View File

@@ -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<Props> = ({
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 (
<>
<Modal
title={'删除确认'}
open={open}
onOk={handleOk}
confirmLoading={confirmLoading}
onCancel={onCancelClick}
>
<>
<span style={{ color: '#296DF3', fontWeight: 'bold' }}>{nodeData?.name}</span>
</>
</Modal>
</>
);
};
export default DeleteConfirmModal;

View File

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

View File

@@ -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 = `<path d="M909.6 854.5L649.9 594.8C690.2 542.7 712 479 712 412c0-80.2-31.3-155.4-87.9-212.1-56.6-56.7-132-87.9-212.1-87.9s-155.5 31.3-212.1 87.9C143.2 256.5 112 331.8 112 412c0 80.1 31.3 155.5 87.9 212.1C256.5 680.8 331.8 712 412 712c67 0 130.6-21.8 182.7-62l259.7 259.6a8.2 8.2 0 0011.6 0l43.6-43.5a8.2 8.2 0 000-11.6zM570.4 570.4C528 612.7 471.8 636 412 636s-116-23.3-158.4-65.6C211.3 528 188 471.8 188 412s23.3-116.1 65.6-158.4C296 211.3 352.2 188 412 188s116.1 23.2 158.4 65.6S636 352.2 636 412s-23.3 116.1-65.6 158.4z" />`;
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 =
'<input placeholder="请输入指标/维度名称" class="ant-input" id="toolBarSearchInput" type="text" value="" />';
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 = `<button
id="toolBarSearchBtn"
type="button"
@@ -66,11 +111,11 @@ const generatorSearchBtnDom = (graph) => {
return searchBtnDom;
};
const searchInputDOM = (graph) => {
const searchInputDom = generatorSearchInputDom(graph);
const searchInputDOM = (graph: Graph, refreshGraphData: RefreshGraphData) => {
const searchInputDom = generatorSearchInputDom(graph, refreshGraphData);
const searchBtnDom = generatorSearchBtnDom(graph);
const searchInput = `
<div id="searchInputContent" class="g6-component-toolbar-search-input" style="position: absolute;top: 38px;width: 190px;left: 0;display:none">
<div id="searchInputContent" class="g6-component-toolbar-search-input" style="position: absolute;top: 38px;width: 190px;left: 0;">
<span class="ant-input-group-wrapper ant-input-search" >
<span class="ant-input-wrapper ant-input-group" id="toolBarSearchWrapper">
<span class="ant-input-group-addon"></span>
@@ -84,7 +129,7 @@ const searchInputDOM = (graph) => {
return searchDom;
};
const initToolBar = () => {
const initToolBar = ({ refreshGraphData }: { refreshGraphData: RefreshGraphData }) => {
const toolBarInstance = new G6.ToolBar();
const config = toolBarInstance._cfgs;
const defaultContentDomString = config.getContent();
@@ -108,12 +153,12 @@ const initToolBar = () => {
</svg>
</li>`;
defaultContentDom.insertAdjacentHTML('afterbegin', searchBtnDom);
let searchInputContentVisible = false;
let searchInputContentVisible = true;
const toolbar = new G6.ToolBar({
position: { x: 10, y: 10 },
className: 'semantic-graph-toolbar',
getContent: (graph) => {
const searchInput = searchInputDOM(graph);
const searchInput = searchInputDOM(graph as Graph, refreshGraphData);
const content = `<div class="g6-component-toolbar-content">${defaultContentDom.outerHTML}</div>`;
const contentDom = createDom(content);
contentDom.appendChild(searchInput);

View File

@@ -6,17 +6,14 @@ const initTooltips = () => {
offsetX: 10,
offsetY: 10,
fixToNode: [1, 0.5],
// the types of items that allow the tooltip show up
// 允许出现 tooltip 的 item 类型
// itemTypes: ['node', 'edge'],
itemTypes: ['node'],
// custom the tooltip's content
// 自定义 tooltip 内容
getContent: (e) => {
const outDiv = document.createElement('div');
outDiv.style.width = 'fit-content';
outDiv.style.height = 'fit-content';
const model = e.item.getModel();
const model = e!.item!.getModel();
const { name, bizName, createdBy, updatedAt, description } = model;
const list = [
@@ -54,16 +51,9 @@ const initTooltips = () => {
const html = `<div>
${listHtml}
</div>`;
if (e.item.getType() === 'node') {
if (e!.item!.getType() === 'node') {
outDiv.innerHTML = html;
}
// else {
// const source = e.item.getSource();
// const target = e.item.getTarget();
// outDiv.innerHTML = `来源:${source.getModel().name}<br/>去向:${
// target.getModel().name
// }`;
// }
return outDiv;
},
});

View File

@@ -1,137 +1,90 @@
import React, { useEffect, useState, useRef } from 'react';
import { connect } from 'umi';
import type { StateType } from '../model';
import { IGroup } from '@antv/g-base';
import type { Dispatch } from 'umi';
import { typeConfigs } from './utils';
import { message, Row, Col, Radio } from 'antd';
import { getDatasourceList, getDomainSchemaRela } from '../service';
import {
typeConfigs,
formatterRelationData,
loopNodeFindDataSource,
getNodeConfigByType,
flatGraphDataNode,
} from './utils';
import { message } from 'antd';
import { getDomainSchemaRela } from '../service';
import { Item, TreeGraphData, NodeConfig, IItemBaseConfig } from '@antv/g6-core';
import initToolBar from './components/ToolBar';
import initTooltips from './components/ToolTips';
import initContextMenu from './components/ContextMenu';
import initLegend from './components/Legend';
import { SemanticNodeType } from '../enum';
import G6 from '@antv/g6';
import { ISemantic, IDataSource } from '../data';
import DimensionInfoModal from '../components/DimensionInfoModal';
import MetricInfoCreateForm from '../components/MetricInfoCreateForm';
import DeleteConfirmModal from './components/DeleteConfirmModal';
import { cloneDeep } from 'lodash';
type Props = {
domainId: number;
graphShowType: SemanticNodeType;
domainManger: StateType;
dispatch: Dispatch;
};
const DomainManger: React.FC<Props> = ({ domainManger, domainId }) => {
const DomainManger: React.FC<Props> = ({
domainManger,
domainId,
graphShowType = SemanticNodeType.DIMENSION,
dispatch,
}) => {
const ref = useRef(null);
const [graphData, setGraphData] = useState<any>({});
const [dataSourceListData, setDataSourceListData] = useState<any[]>([]);
const [graphShowType, setGraphShowType] = useState<string>('dimension');
const [graphData, setGraphData] = useState<TreeGraphData>();
const [createDimensionModalVisible, setCreateDimensionModalVisible] = useState<boolean>(false);
const [createMetricModalVisible, setCreateMetricModalVisible] = useState<boolean>(false);
const legendDataRef = useRef<any[]>([]);
const graphRef = useRef<any>(null);
const legendDataFilterFunctions = useRef<any>({});
const [dimensionItem, setDimensionItem] = useState<ISemantic.IDimensionItem>();
// const { dimensionList } = domainManger;
const [metricItem, setMetricItem] = useState<ISemantic.IMetricItem>();
const toggleNodeVisibility = (graph, node, visible) => {
if (visible) {
graph.showItem(node);
} else {
graph.hideItem(node);
}
};
const [nodeDataSource, setNodeDataSource] = useState<any>();
const toggleChildrenVisibility = (graph, node, visible) => {
const model = node.getModel();
if (model.children) {
model.children.forEach((child) => {
const childNode = graph.findById(child.id);
toggleNodeVisibility(graph, childNode, visible);
toggleChildrenVisibility(graph, childNode, visible);
});
}
};
const { dimensionList, metricList } = domainManger;
const getDimensionChildren = (dimensions: any[], dataSourceId: string) => {
const dimensionChildrenList = dimensions.reduce((dimensionChildren: any[], dimension: any) => {
const {
id: dimensionId,
name: dimensionName,
bizName,
description,
createdBy,
updatedAt,
} = dimension;
// if (datasourceId === id) {
dimensionChildren.push({
nodeType: 'dimension',
legendType: dataSourceId,
id: `dimension-${dimensionId}`,
name: dimensionName,
bizName,
description,
createdBy,
updatedAt,
style: {
lineWidth: 2,
fill: '#f0f7ff',
stroke: '#a6ccff',
},
});
// }
return dimensionChildren;
}, []);
return dimensionChildrenList;
};
const dimensionListRef = useRef<ISemantic.IDimensionItem[]>([]);
const metricListRef = useRef<ISemantic.IMetricItem[]>([]);
const getMetricChildren = (metrics: any[], dataSourceId: string) => {
const metricsChildrenList = metrics.reduce((metricsChildren: any[], dimension: any) => {
const { id, name, bizName, description, createdBy, updatedAt } = dimension;
metricsChildren.push({
nodeType: 'metric',
legendType: dataSourceId,
id: `dimension-${id}`,
name,
bizName,
description,
createdBy,
updatedAt,
style: {
lineWidth: 2,
fill: '#f0f7ff',
stroke: '#a6ccff',
},
});
return metricsChildren;
}, []);
return metricsChildrenList;
};
const [confirmModalOpenState, setConfirmModalOpenState] = useState<boolean>(false);
const formatterRelationData = (dataSourceList: any[], type = graphShowType) => {
const relationData = dataSourceList.reduce((relationList: any[], item: any) => {
const { datasource, dimensions, metrics } = item;
const { id, name } = datasource;
const dataSourceId = `dataSource-${id}`;
let childrenList = [];
if (type === 'metric') {
childrenList = getMetricChildren(metrics, dataSourceId);
}
if (type === 'dimension') {
childrenList = getDimensionChildren(dimensions, dataSourceId);
}
relationList.push({
name,
legendType: dataSourceId,
id: dataSourceId,
nodeType: 'datasource',
size: 40,
children: [...childrenList],
style: {
lineWidth: 2,
fill: '#BDEFDB',
stroke: '#5AD8A6',
},
});
return relationList;
}, []);
return relationData;
};
// const toggleNodeVisibility = (graph: Graph, node: Item, visible: boolean) => {
// if (visible) {
// graph.showItem(node);
// } else {
// graph.hideItem(node);
// }
// };
const changeGraphData = (data: any, type?: string) => {
useEffect(() => {
dimensionListRef.current = dimensionList;
metricListRef.current = metricList;
}, [dimensionList, metricList]);
// const toggleChildrenVisibility = (graph: Graph, node: Item, visible: boolean) => {
// const model = node.getModel();
// if (Array.isArray(model.children)) {
// model.children.forEach((child) => {
// const childNode = graph.findById(child.id);
// toggleNodeVisibility(graph, childNode, visible);
// toggleChildrenVisibility(graph, childNode, visible);
// });
// }
// };
const changeGraphData = (data: IDataSource.IDataSourceItem[], type: SemanticNodeType) => {
const relationData = formatterRelationData(data, type);
const legendList = relationData.map((item: any) => {
const { id, name } = item;
@@ -148,31 +101,38 @@ const DomainManger: React.FC<Props> = ({ domainManger, domainId }) => {
name: domainManger.selectDomainName,
children: relationData,
};
setGraphData(graphRootData);
//
return graphRootData;
};
const queryDataSourceList = async (params: any) => {
const { code, data, msg } = await getDomainSchemaRela(params.domainId);
const queryDataSourceList = async (params: {
domainId: number;
graphShowType?: SemanticNodeType;
}) => {
const { code, data } = await getDomainSchemaRela(params.domainId);
if (code === 200) {
if (data) {
changeGraphData(data);
setDataSourceListData(data);
const graphRootData = changeGraphData(data, params.graphShowType || graphShowType);
setGraphData(graphRootData);
return graphRootData;
}
return false;
} else {
message.error(msg);
return false;
}
};
useEffect(() => {
graphRef.current = null;
queryDataSourceList({ domainId });
}, [domainId]);
}, [domainId, graphShowType]);
const getLegendDataFilterFunctions = () => {
legendDataRef.current.map((item: any) => {
const { id } = item;
legendDataFilterFunctions.current = {
...legendDataFilterFunctions.current,
[id]: (d) => {
[id]: (d: any) => {
if (d.legendType === id) {
return true;
}
@@ -184,6 +144,9 @@ const DomainManger: React.FC<Props> = ({ domainManger, domainId }) => {
const setAllActiveLegend = (legend: any) => {
const legendCanvas = legend._cfgs.legendCanvas;
if (!legendCanvas) {
return;
}
// 从图例中找出node-group节点;
const group = legendCanvas.find((e: any) => e.get('name') === 'node-group');
// 数据源的图例节点在node-group中的children中
@@ -195,211 +158,366 @@ const DomainManger: React.FC<Props> = ({ domainManger, domainId }) => {
legend.activateLegend(labelText);
});
};
// const [visible, setVisible] = useState(false);
useEffect(() => {
if (!(Array.isArray(graphData.children) && graphData.children.length > 0)) {
const handleContextMenuClickEdit = (item: IItemBaseConfig) => {
const targetData = item.model;
if (!targetData) {
return;
}
const container = document.getElementById('semanticGraph');
const width = container!.scrollWidth;
const height = container!.scrollHeight || 500;
const datasource = loopNodeFindDataSource(item);
if (datasource) {
setNodeDataSource({
id: datasource.uid,
name: datasource.name,
});
}
if (targetData.nodeType === SemanticNodeType.DIMENSION) {
const targetItem = dimensionListRef.current.find((item) => item.id === targetData.uid);
if (targetItem) {
setDimensionItem({ ...targetItem });
setCreateDimensionModalVisible(true);
} else {
message.error('获取维度初始化数据失败');
}
}
if (targetData.nodeType === SemanticNodeType.METRIC) {
const targetItem = metricListRef.current.find((item) => item.id === targetData.uid);
if (targetItem) {
setMetricItem({ ...targetItem });
setCreateMetricModalVisible(true);
} else {
message.error('获取指标初始化数据失败');
}
}
};
// if (!graphRef.current) {
getLegendDataFilterFunctions();
const toolbar = initToolBar();
const tooltip = initTooltips();
const contextMenu = initContextMenu();
const legend = new G6.Legend({
// container: 'legendContainer',
data: {
nodes: legendDataRef.current,
},
align: 'center',
layout: 'horizontal', // vertical
position: 'bottom-right',
vertiSep: 12,
horiSep: 24,
offsetY: -24,
padding: [4, 16, 8, 16],
containerStyle: {
fill: '#ccc',
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: {
...legendDataFilterFunctions.current,
},
},
const handleContextMenuClickCreate = (item: IItemBaseConfig) => {
const datasource = item.model;
if (!datasource) {
return;
}
setNodeDataSource({
id: datasource.uid,
name: datasource.name,
});
if (graphShowType === SemanticNodeType.DIMENSION) {
setCreateDimensionModalVisible(true);
}
if (graphShowType === SemanticNodeType.METRIC) {
setCreateMetricModalVisible(true);
}
setDimensionItem(undefined);
setMetricItem(undefined);
};
graphRef.current = new G6.TreeGraph({
container: 'semanticGraph',
width,
height,
linkCenter: true,
modes: {
default: [
{
type: 'collapse-expand',
onChange: function onChange(item, collapsed) {
const data = item.get('model');
data.collapsed = collapsed;
return true;
},
},
'drag-node',
'drag-canvas',
// 'activate-relations',
'zoom-canvas',
{
type: 'activate-relations',
trigger: 'mouseenter', // 触发方式,可以是 'mouseenter' 或 'click'
resetSelected: true, // 点击空白处时,是否取消高亮
},
],
},
defaultNode: {
size: 26,
labelCfg: {
position: 'right',
offset: 5,
style: {
stroke: '#fff',
lineWidth: 4,
},
},
},
const handleContextMenuClickDelete = (item: IItemBaseConfig) => {
const targetData = item.model;
if (!targetData) {
return;
}
if (targetData.nodeType === SemanticNodeType.DIMENSION) {
const targetItem = dimensionListRef.current.find((item) => item.id === targetData.uid);
if (targetItem) {
setDimensionItem({ ...targetItem });
setConfirmModalOpenState(true);
} else {
message.error('获取维度初始化数据失败');
}
}
if (targetData.nodeType === SemanticNodeType.METRIC) {
const targetItem = metricListRef.current.find((item) => item.id === targetData.uid);
if (targetItem) {
setMetricItem({ ...targetItem });
setConfirmModalOpenState(true);
} else {
message.error('获取指标初始化数据失败');
}
}
};
const handleContextMenuClick = (key: string, item: Item) => {
if (!item?._cfg) {
return;
}
switch (key) {
case 'edit':
handleContextMenuClickEdit(item._cfg);
break;
case 'delete':
handleContextMenuClickDelete(item._cfg);
break;
case 'create':
handleContextMenuClickCreate(item._cfg);
break;
default:
break;
}
};
const graphConfigMap = {
dendrogram: {
defaultEdge: {
type: 'cubic-horizontal',
// type: 'flow-line',
// type: 'polyline',
// type: 'line',
/* configure the bending radius and min distance to the end nodes */
style: {
radius: 10,
offset: 30,
endArrow: true,
/* and other styles */
// stroke: '#F6BD16',
},
// style: {
// stroke: '#A3B1BF',
// },
},
layout: {
// type: 'mindmap',
// direction: 'H',
// getId: function getId(d) {
// return d.id;
// },
// getHeight: function getHeight() {
// return 16;
// },
// getWidth: function getWidth() {
// return 16;
// },
// getVGap: function getVGap() {
// return 30;
// },
// getHGap: function getHGap() {
// return 100;
// },
type: 'dendrogram',
direction: 'LR',
animate: false,
nodeSep: 200,
rankSep: 300,
radial: true,
},
plugins: [legend, tooltip, toolbar, contextMenu],
});
},
mindmap: {
defaultEdge: {
type: 'polyline',
},
layout: {
type: 'mindmap',
animate: false,
direction: 'H',
getHeight: () => {
return 50;
},
getWidth: () => {
return 50;
},
getVGap: () => {
return 10;
},
getHGap: () => {
return 50;
},
},
},
};
const legendCanvas = legend._cfgs.legendCanvas;
// legend模式事件方法bindEvents会有点击图例空白清空选中的逻辑在注册click事件前先将click事件队列清空
legend._cfgs.legendCanvas._events.click = [];
legendCanvas.on('click', (e) => {
const shape = e.target;
const shapeGroup = shape.get('parent');
const shapeGroupId = shapeGroup?.cfg?.id;
if (shapeGroupId) {
const isActive = shapeGroup.get('active');
const targetNode = graph.findById(shapeGroupId);
// const model = targetNode.getModel();
toggleNodeVisibility(graph, targetNode, isActive);
toggleChildrenVisibility(graph, targetNode, isActive);
}
});
useEffect(() => {
if (!Array.isArray(graphData?.children)) {
return;
}
const container = document.getElementById('semanticGraph');
const width = container!.scrollWidth;
const height = container!.scrollHeight || 500;
const graph = graphRef.current;
graph.node(function (node) {
return {
label: node.name,
labelCfg: { style: { fill: '#3c3c3c' } },
};
});
// graph.data(graphData);
graph.changeData(graphData);
graph.render();
graph.fitView();
if (!graph && graphData) {
const graphNodeList = flatGraphDataNode(graphData.children);
const graphConfigKey = graphNodeList.length > 20 ? 'dendrogram' : 'mindmap';
setAllActiveLegend(legend);
getLegendDataFilterFunctions();
const toolbar = initToolBar({ refreshGraphData });
const tooltip = initTooltips();
const contextMenu = initContextMenu({
graphShowType,
onMenuClick: handleContextMenuClick,
});
const legend = initLegend({
nodeData: legendDataRef.current,
filterFunctions: { ...legendDataFilterFunctions.current },
});
const rootNode = graph.findById('root');
graph.hideItem(rootNode);
if (typeof window !== 'undefined')
window.onresize = () => {
if (!graph || graph.get('destroyed')) return;
if (!container || !container.scrollWidth || !container.scrollHeight) return;
graph.changeSize(container.scrollWidth, container.scrollHeight);
};
// }
graphRef.current = new G6.TreeGraph({
container: 'semanticGraph',
width,
height,
modes: {
default: [
{
type: 'collapse-expand',
onChange: function onChange(item, collapsed) {
const data = item!.get('model');
data.collapsed = collapsed;
return true;
},
},
'drag-node',
'drag-canvas',
// 'activate-relations',
'zoom-canvas',
{
type: 'activate-relations',
trigger: 'mouseenter', // 触发方式,可以是 'mouseenter' 或 'click'
resetSelected: true, // 点击空白处时,是否取消高亮
},
],
},
defaultNode: {
size: 26,
anchorPoints: [
[0, 0.5],
[1, 0.5],
],
labelCfg: {
position: 'right',
offset: 5,
style: {
stroke: '#fff',
lineWidth: 4,
},
},
},
defaultEdge: {
type: graphConfigMap[graphConfigKey].defaultEdge.type,
},
layout: {
...graphConfigMap[graphConfigKey].layout,
},
plugins: [legend, tooltip, toolbar, contextMenu],
});
graphRef.current.set('initGraphData', graphData);
const legendCanvas = legend._cfgs.legendCanvas;
// legend模式事件方法bindEvents会有点击图例空白清空选中的逻辑在注册click事件前先将click事件队列清空
legend._cfgs.legendCanvas._events.click = [];
// legendCanvas.on('click', (e) => {
// const shape = e.target;
// const shapeGroup = shape.get('parent');
// const shapeGroupId = shapeGroup?.cfg?.id;
// if (shapeGroupId) {
// const isActive = shapeGroup.get('active');
// const targetNode = graphRef.current.findById(shapeGroupId);
// toggleNodeVisibility(graphRef.current, targetNode, isActive);
// toggleChildrenVisibility(graphRef.current, targetNode, isActive);
// }
// });
legendCanvas.on('click', () => {
// @ts-ignore findLegendItemsByState为Legend的 private方法忽略ts校验
const activedNodeList = legend.findLegendItemsByState('active');
// 获取当前所有激活节点后进行数据遍历筛选;
const activedNodeIds = activedNodeList.map((item: IGroup) => {
return item.cfg.id;
});
const graphDataClone = cloneDeep(graphData);
const filterGraphDataChildren = Array.isArray(graphDataClone?.children)
? graphDataClone.children.reduce((children: TreeGraphData[], item: TreeGraphData) => {
if (activedNodeIds.includes(item.id)) {
children.push(item);
}
return children;
}, [])
: [];
graphDataClone.children = filterGraphDataChildren;
refreshGraphData(graphDataClone);
});
graphRef.current.node(function (node: NodeConfig) {
return getNodeConfigByType(node, {
label: node.name,
});
});
graphRef.current.data(graphData);
graphRef.current.render();
graphRef.current.fitView([80, 80]);
setAllActiveLegend(legend);
const rootNode = graphRef.current.findById('root');
graphRef.current.hideItem(rootNode);
if (typeof window !== 'undefined')
window.onresize = () => {
if (!graphRef.current || graphRef.current.get('destroyed')) return;
if (!container || !container.scrollWidth || !container.scrollHeight) return;
graphRef.current.changeSize(container.scrollWidth, container.scrollHeight);
};
}
}, [graphData]);
const updateGraphData = async () => {
const graphRootData = await queryDataSourceList({ domainId });
if (graphRootData) {
refreshGraphData(graphRootData);
}
};
const refreshGraphData = (graphRootData: TreeGraphData) => {
graphRef.current.changeData(graphRootData);
const rootNode = graphRef.current.findById('root');
graphRef.current.hideItem(rootNode);
graphRef.current.fitView();
};
return (
<>
<Row>
<Col flex="auto" />
<Col flex="100px">
<Radio.Group
buttonStyle="solid"
size="small"
value={graphShowType}
onChange={(e) => {
const { value } = e.target;
setGraphShowType(value);
changeGraphData(dataSourceListData, value);
}}
>
<Radio.Button value="dimension"></Radio.Button>
<Radio.Button value="metric"></Radio.Button>
</Radio.Group>
</Col>
</Row>
<div
ref={ref}
key={`${domainId}-${graphShowType}`}
id="semanticGraph"
style={{ width: '100%', height: '100%' }}
/>
{createDimensionModalVisible && (
<DimensionInfoModal
domainId={domainId}
bindModalVisible={createDimensionModalVisible}
dimensionItem={dimensionItem}
dataSourceList={nodeDataSource ? [nodeDataSource] : []}
onSubmit={() => {
setCreateDimensionModalVisible(false);
updateGraphData();
dispatch({
type: 'domainManger/queryDimensionList',
payload: {
domainId,
},
});
}}
onCancel={() => {
setCreateDimensionModalVisible(false);
}}
/>
)}
{createMetricModalVisible && (
<MetricInfoCreateForm
domainId={domainId}
key={metricItem?.id}
datasourceId={nodeDataSource.id}
createModalVisible={createMetricModalVisible}
metricItem={metricItem}
onSubmit={() => {
setCreateMetricModalVisible(false);
updateGraphData();
dispatch({
type: 'domainManger/queryMetricList',
payload: {
domainId,
},
});
}}
onCancel={() => {
setCreateMetricModalVisible(false);
}}
/>
)}
{
<DeleteConfirmModal
open={confirmModalOpenState}
onOkClick={() => {
setConfirmModalOpenState(false);
updateGraphData();
graphShowType === SemanticNodeType.DIMENSION
? dispatch({
type: 'domainManger/queryDimensionList',
payload: {
domainId,
},
})
: dispatch({
type: 'domainManger/queryMetricList',
payload: {
domainId,
},
});
}}
onCancelClick={() => {
setConfirmModalOpenState(false);
}}
nodeType={graphShowType}
nodeData={graphShowType === SemanticNodeType.DIMENSION ? dimensionItem : metricItem}
/>
}
</>
);
};

View File

@@ -1,80 +1,139 @@
import { ISemantic, IDataSource } from '../data';
import { SemanticNodeType } from '../enum';
export const typeConfigs = {
datasource: {
type: 'circle',
size: 5,
style: {
fill: '#5B8FF9',
},
size: 10,
},
dimension: {
type: 'circle',
size: 20,
style: {
fill: '#5AD8A6',
},
},
metric: {
type: 'rect',
size: [10, 10],
style: {
fill: '#5D7092',
},
},
// eType1: {
// type: 'line',
// style: {
// width: 20,
// stroke: '#F6BD16',
// },
// },
// eType2: {
// type: 'cubic',
// },
// eType3: {
// type: 'quadratic',
// style: {
// width: 25,
// stroke: '#6F5EF9',
// },
// },
};
export const legendData = {
nodes: [
{
id: 'type1',
label: 'node-type1',
order: 4,
...typeConfigs.datasource,
export const getDimensionChildren = (
dimensions: ISemantic.IDimensionItem[],
dataSourceNodeId: string,
) => {
const dimensionChildrenList = dimensions.reduce(
(dimensionChildren: any[], dimension: ISemantic.IDimensionItem) => {
const { id } = dimension;
dimensionChildren.push({
...dimension,
nodeType: SemanticNodeType.DIMENSION,
legendType: dataSourceNodeId,
id: `${SemanticNodeType.DIMENSION}-${id}`,
uid: id,
style: {
lineWidth: 2,
fill: '#f0f7ff',
stroke: '#a6ccff',
},
});
return dimensionChildren;
},
{
id: 'type2',
label: 'node-type2',
order: 0,
...typeConfigs.dimension,
},
{
id: 'type3',
label: 'node-type3',
order: 2,
...typeConfigs.metric,
},
],
// edges: [
// {
// id: 'eType1',
// label: 'edge-type1',
// order: 2,
// ...typeConfigs.eType1,
// },
// {
// id: 'eType2',
// label: 'edge-type2',
// ...typeConfigs.eType2,
// },
// {
// id: 'eType3',
// label: 'edge-type3',
// ...typeConfigs.eType3,
// },
// ],
[],
);
return dimensionChildrenList;
};
export const getMetricChildren = (metrics: ISemantic.IMetricItem[], dataSourceNodeId: string) => {
const metricsChildrenList = metrics.reduce(
(metricsChildren: any[], metric: ISemantic.IMetricItem) => {
const { id } = metric;
metricsChildren.push({
...metric,
nodeType: SemanticNodeType.METRIC,
legendType: dataSourceNodeId,
id: `${SemanticNodeType.METRIC}-${id}`,
uid: id,
style: {
lineWidth: 2,
fill: '#f0f7ff',
stroke: '#a6ccff',
},
});
return metricsChildren;
},
[],
);
return metricsChildrenList;
};
export const formatterRelationData = (
dataSourceList: IDataSource.IDataSourceItem[],
type: SemanticNodeType = SemanticNodeType.DIMENSION,
) => {
const relationData = dataSourceList.reduce((relationList: any[], item: any) => {
const { datasource, dimensions, metrics } = item;
const { id } = datasource;
const dataSourceNodeId = `${SemanticNodeType.DATASOURCE}-${id}`;
let childrenList = [];
if (type === SemanticNodeType.METRIC) {
childrenList = getMetricChildren(metrics, dataSourceNodeId);
}
if (type === SemanticNodeType.DIMENSION) {
childrenList = getDimensionChildren(dimensions, dataSourceNodeId);
}
relationList.push({
...datasource,
legendType: dataSourceNodeId,
id: dataSourceNodeId,
uid: id,
nodeType: SemanticNodeType.DATASOURCE,
size: 40,
children: [...childrenList],
style: {
lineWidth: 2,
fill: '#BDEFDB',
stroke: '#5AD8A6',
},
});
return relationList;
}, []);
return relationData;
};
export const loopNodeFindDataSource: any = (node: any) => {
const { model, parent } = node;
if (model?.nodeType === SemanticNodeType.DATASOURCE) {
return model;
}
const parentNode = parent?._cfg;
if (parentNode) {
return loopNodeFindDataSource(parentNode);
}
return false;
};
export const getNodeConfigByType = (nodeData: any, defaultConfig = {}) => {
const { nodeType } = nodeData;
const labelCfg = { style: { fill: '#3c3c3c' } };
switch (nodeType) {
case SemanticNodeType.DATASOURCE: {
return {
...defaultConfig,
labelCfg: { position: 'bottom', ...labelCfg },
};
}
case SemanticNodeType.DIMENSION:
return {
...defaultConfig,
labelCfg: { position: 'right', ...labelCfg },
};
case SemanticNodeType.METRIC:
return {
...defaultConfig,
labelCfg: { position: 'right', ...labelCfg },
};
default:
return defaultConfig;
}
};
export const flatGraphDataNode = (graphData: any[]) => {
return graphData.reduce((nodeList: any[], item: any) => {
const { children } = item;
if (Array.isArray(children)) {
nodeList.push(...children);
}
return nodeList;
}, []);
};

View File

@@ -0,0 +1,51 @@
import { Radio } from 'antd';
import React, { useState } from 'react';
import { connect } from 'umi';
import styles from './components/style.less';
import type { StateType } from './model';
import { SemanticNodeType } from './enum';
import SemanticFlow from './SemanticFlows';
import SemanticGraph from './SemanticGraph';
type Props = {
domainManger: StateType;
};
const SemanticGraphCanvas: React.FC<Props> = ({ domainManger }) => {
const [graphShowType, setGraphShowType] = useState<SemanticNodeType>(SemanticNodeType.DATASOURCE);
const { selectDomainId } = domainManger;
return (
<div className={styles.semanticGraphCanvas}>
<div className={styles.toolbar}>
<Radio.Group
buttonStyle="solid"
value={graphShowType}
onChange={(e) => {
const { value } = e.target;
setGraphShowType(value);
}}
>
<Radio.Button value={SemanticNodeType.DATASOURCE}></Radio.Button>
<Radio.Button value={SemanticNodeType.DIMENSION}></Radio.Button>
<Radio.Button value={SemanticNodeType.METRIC}></Radio.Button>
</Radio.Group>
</div>
<div className={styles.canvasContainer}>
{graphShowType === SemanticNodeType.DATASOURCE ? (
<div style={{ width: '100%', height: 'calc(100vh - 200px)' }}>
<SemanticFlow />
</div>
) : (
<div style={{ width: '100%', height: 'calc(100vh - 220px)' }}>
<SemanticGraph domainId={selectDomainId} graphShowType={graphShowType} />
</div>
)}
</div>
</div>
);
};
export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger,
}))(SemanticGraphCanvas);

View File

@@ -0,0 +1,3 @@
export const presetsTagDomString = (text: string, color: string = 'blue') => {
return `<span class="ant-tag ant-tag-${color}">${text}</span>`;
};

View File

@@ -176,6 +176,9 @@ const ClassDataSourceTable: React.FC<Props> = ({ dispatch, domainManger }) => {
{
<ClassDataSourceTypeModal
open={createDataSourceModalOpen}
onCancel={() => {
setCreateDataSourceModalOpen(false);
}}
onTypeChange={(type) => {
if (type === 'fast') {
setDataSourceModalVisible(true);

View File

@@ -1,14 +1,24 @@
import { Modal, Card, Row, Col } from 'antd';
import { Modal, Card, Row, Col, Result, Button } from 'antd';
import { ConsoleSqlOutlined, CoffeeOutlined } from '@ant-design/icons';
import React, { useState, useEffect } from 'react';
import { history, connect } from 'umi';
import type { Dispatch } from 'umi';
import type { StateType } from '../model';
const { Meta } = Card;
type Props = {
open: boolean;
domainManger: StateType;
onTypeChange: (type: 'fast' | 'normal') => void;
onCancel?: () => void;
};
const ClassDataSourceTypeModal: React.FC<Props> = ({ open, onTypeChange, onCancel }) => {
const ClassDataSourceTypeModal: React.FC<Props> = ({
open,
onTypeChange,
domainManger,
onCancel,
}) => {
const { selectDomainId, dataBaseConfig } = domainManger;
const [createDataSourceModalOpen, setCreateDataSourceModalOpen] = useState(false);
useEffect(() => {
setCreateDataSourceModalOpen(open);
@@ -26,45 +36,67 @@ const ClassDataSourceTypeModal: React.FC<Props> = ({ open, onTypeChange, onCance
centered
closable={false}
>
<Row gutter={16} style={{ marginTop: '0px' }}>
<Col span={12}>
<Card
hoverable
style={{ height: 220 }}
onClick={() => {
onTypeChange('fast');
setCreateDataSourceModalOpen(false);
}}
cover={
<CoffeeOutlined
width={240}
style={{ paddingTop: '45px', height: 120, fontSize: '48px', color: '#1890ff' }}
/>
}
>
<Meta title="快速创建" description="自动进行数据源可视化创建" />
</Card>
</Col>
<Col span={12}>
<Card
onClick={() => {
onTypeChange('normal');
setCreateDataSourceModalOpen(false);
}}
hoverable
style={{ height: 220 }}
cover={
<ConsoleSqlOutlined
style={{ paddingTop: '45px', height: 120, fontSize: '48px', color: '#1890ff' }}
/>
}
>
<Meta title="SQL脚本" description="自定义SQL脚本创建数据源" />
</Card>
</Col>
</Row>
{dataBaseConfig && dataBaseConfig.id ? (
<Row gutter={16} style={{ marginTop: '0px' }}>
<Col span={12}>
<Card
hoverable
style={{ height: 220 }}
onClick={() => {
onTypeChange('fast');
setCreateDataSourceModalOpen(false);
}}
cover={
<CoffeeOutlined
width={240}
style={{ paddingTop: '45px', height: 120, fontSize: '48px', color: '#1890ff' }}
/>
}
>
<Meta title="快速创建" description="自动进行数据源可视化创建" />
</Card>
</Col>
<Col span={12}>
<Card
onClick={() => {
onTypeChange('normal');
setCreateDataSourceModalOpen(false);
}}
hoverable
style={{ height: 220 }}
cover={
<ConsoleSqlOutlined
style={{ paddingTop: '45px', height: 120, fontSize: '48px', color: '#1890ff' }}
/>
}
>
<Meta title="SQL脚本" description="自定义SQL脚本创建数据源" />
</Card>
</Col>
</Row>
) : (
<Result
status="warning"
subTitle="创建数据源需要先完成数据库设置"
extra={
<Button
type="primary"
key="console"
onClick={() => {
history.replace(`/semanticModel/${selectDomainId}/dataBase`);
onCancel?.();
}}
>
</Button>
}
/>
)}
</Modal>
</>
);
};
export default ClassDataSourceTypeModal;
export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger,
}))(ClassDataSourceTypeModal);

View File

@@ -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<Props> = ({ domainManger, dispatch }) => {
const { selectDomainId } = domainManger;
const [createModalVisible, setCreateModalVisible] = useState<boolean>(false);
const [dimensionItem, setDimensionItem] = useState<any>();
const [dimensionItem, setDimensionItem] = useState<ISemantic.IDimensionItem>();
const [dataSourceList, setDataSourceList] = useState<any[]>([]);
const [pagination, setPagination] = useState({
current: 1,
@@ -45,7 +40,7 @@ const ClassDimensionTable: React.FC<Props> = ({ 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<Props> = ({ 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 (
<>
<ProTable
@@ -251,10 +216,21 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
{createModalVisible && (
<DimensionInfoModal
domainId={selectDomainId}
bindModalVisible={createModalVisible}
dimensionItem={dimensionItem}
dataSourceList={dataSourceList}
onSubmit={saveDimension}
onSubmit={() => {
setCreateModalVisible(false);
actionRef?.current?.reload();
dispatch({
type: 'domainManger/queryDimensionList',
payload: {
domainId: selectDomainId,
},
});
return;
}}
onCancel={() => {
setCreateModalVisible(false);
}}

View File

@@ -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<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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);

View File

@@ -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<Props> = ({ title, defaultCollapse = false, value, onChange }) => {
const [listDataSource, setListDataSource] = useState<List>([]);
const [currentSql, setCurrentSql] = useState<string>('');
const [activeKey, setActiveKey] = useState<string>();
const [currentRecord, setCurrentRecord] = useState<ListItem>();
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 (
<div className={styles.commonEditList}>
<Collapse
activeKey={activeKey}
defaultActiveKey={defaultCollapse ? ['editor'] : undefined}
onChange={() => {}}
ghost
>
<Panel
header={title}
key="editor"
extra={
activeKey ? (
<Button
key="saveBtn"
type="primary"
onClick={() => {
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);
}}
>
</Button>
) : (
<Button
type="primary"
key="createBtn"
onClick={() => {
setCurrentRecord(undefined);
setCurrentSql('');
setActiveKey('editor');
}}
>
</Button>
)
}
showArrow={false}
>
<div>
<SqlEditor
value={currentSql}
height={'150px'}
onChange={(sql) => {
setCurrentSql(sql);
}}
/>
</div>
</Panel>
</Collapse>
<List
itemLayout="horizontal"
dataSource={listDataSource || []}
renderItem={(item) => (
<List.Item
actions={[
<a
key="list-loadmore-edit"
onClick={() => {
setCurrentSql(item.sql);
setCurrentRecord(item);
setActiveKey('editor');
}}
>
</a>,
<a
key="list-loadmore-more"
onClick={() => {
const list = [...listDataSource].filter(({ id }) => {
return item.id !== id;
});
handleListChange(list);
setListDataSource(list);
}}
>
</a>,
]}
>
<List.Item.Meta title={item.sql} />
</List.Item>
)}
/>
</div>
);
};
export default CommonEditList;

View File

@@ -0,0 +1,10 @@
.commonEditList {
:global {
.ant-collapse-header {
padding: 0 !important;
}
.ant-collapse {
padding-bottom: 20px;
}
}
}

View File

@@ -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<any>;
onSubmit: (values?: any) => void;
};
const FormItem = Form.Item;
@@ -20,42 +23,56 @@ const { Option } = Select;
const { TextArea } = Input;
const DimensionInfoModal: React.FC<CreateFormProps> = ({
domainId,
onCancel,
bindModalVisible,
dimensionItem,
dataSourceList,
onSubmit: handleUpdate,
}) => {
const isEdit = dimensionItem?.id;
const [formVals, setFormVals] = useState<any>({
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<CreateFormProps> = ({
>
<TextArea placeholder="请输入维度描述" />
</FormItem>
<FormItem name="expr" label="表达式" rules={[{ required: true, message: '请输入表达式' }]}>
<FormItem
name="expr"
label="表达式"
tooltip="表达式中的字段必须在创建数据源的时候被标记为日期或者维度"
rules={[{ required: true, message: '请输入表达式' }]}
>
<SqlEditor height={'150px'} />
</FormItem>
</>
@@ -162,9 +184,11 @@ const DimensionInfoModal: React.FC<CreateFormProps> = ({
<Form
{...formLayout}
form={form}
initialValues={{
...formVals,
}}
initialValues={
{
// ...formVals,
}
}
>
{renderContent()}
</Form>

View File

@@ -1,139 +0,0 @@
import React, { useEffect, useState } from 'react';
import { Button, Modal, message } from 'antd';
import { addDomainExtend, editDomainExtend, getDomainExtendDetailConfig } from '../../service';
import DimensionMetricVisibleTransfer from './DimensionMetricVisibleTransfer';
import { exChangeRichEntityListToIds } from './utils';
type Props = {
domainId: number;
themeData: any;
settingType: 'dimension' | 'metric';
settingSourceList: any[];
onCancel: () => void;
visible: boolean;
onSubmit: (params?: any) => void;
};
const dimensionConfig = {
blackIdListKey: 'blackDimIdList',
visibleIdListKey: 'whiteDimIdList',
modalTitle: '问答可见维度信息',
titles: ['不可见维度', '可见维度'],
};
const metricConfig = {
blackIdListKey: 'blackMetricIdList',
visibleIdListKey: 'whiteMetricIdList',
modalTitle: '问答可见指标信息',
titles: ['不可见指标', '可见指标'],
};
const DimensionMetricVisibleModal: React.FC<Props> = ({
domainId,
visible,
themeData = {},
settingType,
settingSourceList,
onCancel,
onSubmit,
}) => {
const [sourceList, setSourceList] = useState<any[]>([]);
const [selectedKeyList, setSelectedKeyList] = useState<string[]>([]);
const settingTypeConfig = settingType === 'dimension' ? dimensionConfig : metricConfig;
useEffect(() => {
const list = settingSourceList.map((item: any) => {
const { id, name } = item;
return { id, name, type: settingType };
});
setSourceList(list);
}, [settingSourceList]);
useEffect(() => {
setSelectedKeyList(themeData.visibility?.[settingTypeConfig.visibleIdListKey] || []);
}, [themeData]);
const saveEntity = async () => {
const { id, entity } = themeData;
let saveDomainExtendQuery = addDomainExtend;
if (id) {
saveDomainExtendQuery = editDomainExtend;
}
const blackIdList = settingSourceList.reduce((list, item: any) => {
const { id: targetId } = item;
if (!selectedKeyList.includes(targetId)) {
list.push(targetId);
}
return list;
}, []);
const entityParams = exChangeRichEntityListToIds(entity);
themeData.entity = entityParams;
const params = {
...themeData,
visibility: themeData.visibility || {},
};
params.visibility[settingTypeConfig.blackIdListKey] = blackIdList;
if (!params.visibility.blackDimIdList) {
params.visibility.blackDimIdList = [];
}
if (!params.visibility.blackMetricIdList) {
params.visibility.blackMetricIdList = [];
}
const { code, msg } = await saveDomainExtendQuery({
...params,
id,
domainId,
});
if (code === 200) {
onSubmit?.();
message.success('保存成功');
return;
}
message.error(msg);
};
const handleTransferChange = (newTargetKeys: string[]) => {
setSelectedKeyList(newTargetKeys);
};
const renderFooter = () => {
return (
<>
<Button onClick={onCancel}></Button>
<Button
type="primary"
onClick={() => {
saveEntity();
}}
>
</Button>
</>
);
};
return (
<>
<Modal
width={1200}
destroyOnClose
title={settingTypeConfig.modalTitle}
maskClosable={false}
open={visible}
footer={renderFooter()}
onCancel={onCancel}
>
<DimensionMetricVisibleTransfer
titles={settingTypeConfig.titles}
sourceList={sourceList}
targetList={selectedKeyList}
onChange={(newTargetKeys) => {
handleTransferChange(newTargetKeys);
}}
/>
</Modal>
</>
);
};
export default DimensionMetricVisibleModal;

View File

@@ -1,14 +1,14 @@
import { Space, Table, Transfer, Checkbox, Tooltip, Button } from 'antd';
import { Table, Transfer, Checkbox, Button } from 'antd';
import type { ColumnsType, TableRowSelection } from 'antd/es/table/interface';
import type { TransferItem } from 'antd/es/transfer';
import type { CheckboxChangeEvent } from 'antd/es/checkbox';
import { ExclamationCircleOutlined } from '@ant-design/icons';
import difference from 'lodash/difference';
import React, { useState } from 'react';
import type { IChatConfig } from '../../data';
import DimensionValueSettingModal from './DimensionValueSettingModal';
import TransTypeTag from '../TransTypeTag';
import { TransType } from '../../enum';
import TableTitleTooltips from '../../components/TableTitleTooltips';
interface RecordType {
id: number;
@@ -72,12 +72,10 @@ const DimensionMetricVisibleTableTransfer: React.FC<Props> = ({
{
dataIndex: 'y',
title: (
<Space>
<span></span>
<Tooltip title="勾选可见后,维度值将在搜索时可以被联想出来">
<ExclamationCircleOutlined />
</Tooltip>
</Space>
<TableTitleTooltips
title="维度值可见"
tooltips="勾选可见后,维度值将在搜索时可以被联想出来"
/>
),
width: 120,
render: (_, record) => {

View File

@@ -1,138 +0,0 @@
import React, { useEffect, useState } from 'react';
import { Button, Modal, message, Space } from 'antd';
import ProCard from '@ant-design/pro-card';
import { addDomainExtend, editDomainExtend } from '../../service';
import DimensionMetricVisibleTransfer from './DimensionMetricVisibleTransfer';
import SqlEditor from '@/components/SqlEditor';
type Props = {
domainId: number;
themeData: any;
settingSourceList: any[];
onCancel: () => void;
visible: boolean;
onSubmit: (params?: any) => void;
};
const DimensionSearchVisibleModal: React.FC<Props> = ({
domainId,
themeData,
visible,
settingSourceList,
onCancel,
onSubmit,
}) => {
const [sourceList, setSourceList] = useState<any[]>([]);
const [selectedKeyList, setSelectedKeyList] = useState<string[]>([]);
const [dictRules, setDictRules] = useState<string>('');
useEffect(() => {
const knowledgeInfos = themeData?.knowledgeInfos;
if (Array.isArray(knowledgeInfos)) {
const target = knowledgeInfos[0];
if (Array.isArray(target?.ruleList)) {
setDictRules(target.ruleList[0]);
}
const selectKeys = knowledgeInfos.map((item: any) => {
return item.itemId;
});
setSelectedKeyList(selectKeys);
}
}, [themeData]);
useEffect(() => {
const list = settingSourceList.map((item: any) => {
const { id, name } = item;
return { id, name, type: 'dimension' };
});
setSourceList(list);
}, [settingSourceList]);
const saveDictBatch = async () => {
const knowledgeInfos = selectedKeyList.map((key: string) => {
return {
itemId: key,
type: 'DIMENSION',
isDictInfo: true,
ruleList: dictRules ? [dictRules] : [],
};
});
const id = themeData?.id;
let saveDomainExtendQuery = addDomainExtend;
if (id) {
saveDomainExtendQuery = editDomainExtend;
}
const { code, msg } = await saveDomainExtendQuery({
knowledgeInfos,
domainId,
id,
});
if (code === 200) {
message.success('保存可见维度值成功');
onSubmit?.();
return;
}
message.error(msg);
};
const saveDictSetting = async () => {
await saveDictBatch();
};
const handleTransferChange = (newTargetKeys: string[]) => {
setSelectedKeyList(newTargetKeys);
};
const renderFooter = () => {
return (
<>
<Button onClick={onCancel}></Button>
<Button
type="primary"
onClick={() => {
saveDictSetting();
}}
>
</Button>
</>
);
};
return (
<>
<Modal
width={1200}
destroyOnClose
title={'可见维度值设置'}
maskClosable={false}
open={visible}
footer={renderFooter()}
onCancel={onCancel}
>
<Space direction="vertical" style={{ width: '100%' }} size={20}>
<ProCard bordered title="可见设置">
<DimensionMetricVisibleTransfer
titles={['不可见维度值', '可见维度值']}
sourceList={sourceList}
targetList={selectedKeyList}
onChange={(newTargetKeys) => {
handleTransferChange(newTargetKeys);
}}
/>
</ProCard>
<ProCard bordered title="维度值过滤">
<SqlEditor
height={'150px'}
value={dictRules}
onChange={(sql: string) => {
setDictRules(sql);
}}
/>
</ProCard>
</Space>
</Modal>
</>
);
};
export default DimensionSearchVisibleModal;

View File

@@ -5,7 +5,7 @@ import { Form, Input } from 'antd';
import { formLayout } from '@/components/FormHelper/utils';
import { isString } from 'lodash';
import styles from '../style.less';
import CommonEditList from '../../components/CommonEditList/index';
import SqlEditor from '@/components/SqlEditor';
type Props = {
initialValues: any;
@@ -17,7 +17,7 @@ const FormItem = Form.Item;
const EntityCreateForm: ForwardRefRenderFunction<any, Props> = ({ initialValues }, ref) => {
const [form] = Form.useForm();
const exchangeFields = ['blackList', 'whiteList', 'ruleList'];
const exchangeFields = ['blackList', 'whiteList'];
const getFormValidateFields = async () => {
const fields = await form.validateFields();
@@ -69,8 +69,9 @@ const EntityCreateForm: ForwardRefRenderFunction<any, Props> = ({ initialValues
<Input placeholder="多个维度值用英文逗号隔开" />
</FormItem>
<FormItem name="ruleList" label="过滤规则">
<SqlEditor height={'150px'} />
<FormItem name="ruleList">
{/* <SqlEditor height={'150px'} /> */}
<CommonEditList title="过滤规则" />
</FormItem>
</Form>
</>

View File

@@ -1,195 +0,0 @@
import { useEffect, useState, forwardRef, useImperativeHandle } from 'react';
import type { ForwardRefRenderFunction } from 'react';
import FormItemTitle from '@/components/FormHelper/FormItemTitle';
import { formLayout } from '@/components/FormHelper/utils';
import { message, Form, Input, Select, Button, InputNumber } from 'antd';
import { addDomainExtend, editDomainExtend } from '../../service';
import styles from '../style.less';
type Props = {
themeData: any;
metricList: any[];
domainId: number;
onSubmit: (params?: any) => void;
};
const FormItem = Form.Item;
const Option = Select.Option;
const MetricSettingForm: ForwardRefRenderFunction<any, Props> = (
{ metricList, domainId, themeData: uniqueMetricData, onSubmit },
ref,
) => {
const [form] = Form.useForm();
const [metricListOptions, setMetricListOptions] = useState<any>([]);
const [unitState, setUnit] = useState<number | null>();
const [periodState, setPeriod] = useState<string>();
const getFormValidateFields = async () => {
return await form.validateFields();
};
useImperativeHandle(ref, () => ({
getFormValidateFields,
}));
useEffect(() => {
form.resetFields();
setUnit(null);
setPeriod('');
if (Object.keys(uniqueMetricData).length === 0) {
return;
}
const { defaultMetrics = [], id } = uniqueMetricData;
const defaultMetric = defaultMetrics[0];
const recordId = id === -1 ? undefined : id;
if (defaultMetric) {
const { period, unit } = defaultMetric;
setUnit(unit);
setPeriod(period);
form.setFieldsValue({
...defaultMetric,
id: recordId,
});
} else {
form.setFieldsValue({
id: recordId,
});
}
}, [uniqueMetricData]);
useEffect(() => {
const metricOption = metricList.map((item: any) => {
return {
label: item.name,
value: item.id,
};
});
setMetricListOptions(metricOption);
}, [metricList]);
const saveEntity = async () => {
const values = await form.validateFields();
const { id } = values;
let saveDomainExtendQuery = addDomainExtend;
if (id) {
saveDomainExtendQuery = editDomainExtend;
}
const { code, msg, data } = await saveDomainExtendQuery({
defaultMetrics: [{ ...values }],
domainId,
id,
});
if (code === 200) {
form.setFieldValue('id', data);
onSubmit?.();
message.success('保存成功');
return;
}
message.error(msg);
};
return (
<>
<Form
{...formLayout}
form={form}
layout="vertical"
className={styles.form}
initialValues={{
unit: 7,
period: 'DAY',
}}
>
<FormItem hidden={true} name="id" label="ID">
<Input placeholder="id" />
</FormItem>
<FormItem
name={'metricId'}
label={
<FormItemTitle
title={'指标'}
subTitle={'问答搜索结果选择中,如果没有指定指标,将会采用默认指标进行展示'}
/>
}
>
<Select
allowClear
showSearch
style={{ width: '100%' }}
filterOption={(inputValue: string, item: any) => {
const { label } = item;
if (label.includes(inputValue)) {
return true;
}
return false;
}}
placeholder="请选择展示指标信息"
options={metricListOptions}
/>
</FormItem>
<FormItem
label={
<FormItemTitle
title={'时间范围'}
subTitle={'问答搜索结果选择中,如果没有指定时间范围,将会采用默认时间范围'}
/>
}
>
<Input.Group compact>
<span
style={{
display: 'inline-block',
lineHeight: '32px',
marginRight: '8px',
}}
>
</span>
<InputNumber
value={unitState}
style={{ width: '120px' }}
onChange={(value) => {
setUnit(value);
form.setFieldValue('unit', value);
}}
/>
<Select
value={periodState}
style={{ width: '100px' }}
onChange={(value) => {
form.setFieldValue('period', value);
setPeriod(value);
}}
>
<Option value="DAY"></Option>
<Option value="WEEK"></Option>
<Option value="MONTH"></Option>
<Option value="YEAR"></Option>
</Select>
</Input.Group>
</FormItem>
<FormItem name="unit" hidden={true}>
<InputNumber />
</FormItem>
<FormItem name="period" hidden={true}>
<Input />
</FormItem>
<FormItem>
<Button
type="primary"
onClick={() => {
saveEntity();
}}
>
</Button>
</FormItem>
</Form>
</>
);
};
export default forwardRef(MetricSettingForm);

View File

@@ -0,0 +1,25 @@
import React from 'react';
type Props = {
title: string;
labelStyles?: CSSStyleSheet;
};
const FormLabelRequire: React.FC<Props> = ({ title, labelStyles = {} }) => {
return (
<>
<div className="ant-col ant-form-item-label">
<label
htmlFor="description"
className="ant-form-item-required"
title={title}
style={{ fontSize: '16px', ...labelStyles }}
>
{title}
</label>
</div>
</>
);
};
export default FormLabelRequire;

View File

@@ -26,7 +26,6 @@ const InfoTagList: React.FC<Props> = ({ value, createBtnString = '新增', onCha
}, [value]);
const handleTagChange = (tagList: string[]) => {
console.log(tagList, 'tagList');
onChange?.(tagList);
};

View File

@@ -1,18 +1,33 @@
import React, { useEffect, useRef, useState } from 'react';
import { Form, Button, Modal, Steps, Input, Select, Switch, InputNumber } from 'antd';
import {
Form,
Button,
Modal,
Steps,
Input,
Select,
Switch,
InputNumber,
message,
Result,
} from 'antd';
import MetricMeasuresFormTable from './MetricMeasuresFormTable';
import { SENSITIVE_LEVEL_OPTIONS } from '../constant';
import { formLayout } from '@/components/FormHelper/utils';
import FormItemTitle from '@/components/FormHelper/FormItemTitle';
import styles from './style.less';
import { getMeasureListByDomainId } from '../service';
import { creatExprMetric, updateExprMetric } from '../service';
import { ISemantic } from '../data';
import { history } from 'umi';
export type CreateFormProps = {
datasourceId?: number;
domainId: number;
createModalVisible: boolean;
metricItem: any;
onCancel?: () => void;
onSubmit: (values: any) => void;
onSubmit?: (values: any) => void;
};
const { Step } = Steps;
@@ -21,6 +36,7 @@ const { TextArea } = Input;
const { Option } = Select;
const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
datasourceId,
domainId,
onCancel,
createModalVisible,
@@ -31,17 +47,18 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
const [currentStep, setCurrentStep] = useState(0);
const formValRef = useRef({} as any);
const [form] = Form.useForm();
const updateFormVal = (val: SaveDataSetForm) => {
const updateFormVal = (val: any) => {
formValRef.current = val;
};
const [classMeasureList, setClassMeasureList] = useState<any[]>([]);
const [classMeasureList, setClassMeasureList] = useState<ISemantic.IMeasure[]>([]);
const [exprTypeParamsState, setExprTypeParamsState] = useState<any>([]);
const [exprTypeParamsState, setExprTypeParamsState] = useState<ISemantic.IMeasure[]>([]);
const [exprSql, setExprSql] = useState<string>('');
const [isPercentState, setIsPercentState] = useState<boolean>(false);
const [hasMeasuresState, setHasMeasuresState] = useState<boolean>(true);
const forward = () => setCurrentStep(currentStep + 1);
const backward = () => setCurrentStep(currentStep - 1);
@@ -50,6 +67,12 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
const { code, data } = await getMeasureListByDomainId(domainId);
if (code === 200) {
setClassMeasureList(data);
if (datasourceId) {
const hasMeasures = data.some(
(item: ISemantic.IMeasure) => item.datasourceId === datasourceId,
);
setHasMeasuresState(hasMeasures);
}
return;
}
setClassMeasureList([]);
@@ -74,7 +97,8 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
if (currentStep < 1) {
forward();
} else {
onSubmit?.(submitForm);
// onSubmit?.(submitForm);
await saveMetric(submitForm);
}
};
@@ -118,15 +142,41 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
useEffect(() => {
if (isEdit) {
initData();
} else {
// initFields([]);
}
}, [metricItem]);
const saveMetric = async (fieldsValue: any) => {
const queryParams = {
domainId,
...fieldsValue,
};
const { typeParams } = queryParams;
if (!typeParams?.expr) {
message.error('请输入度量表达式');
return;
}
if (!(Array.isArray(typeParams?.measures) && typeParams.measures.length > 0)) {
message.error('请添加一个度量');
return;
}
let saveMetricQuery = creatExprMetric;
if (queryParams.id) {
saveMetricQuery = updateExprMetric;
}
const { code, msg } = await saveMetricQuery(queryParams);
if (code === 200) {
message.success('编辑指标成功');
onSubmit?.(queryParams);
return;
}
message.error(msg);
};
const renderContent = () => {
if (currentStep === 1) {
return (
<MetricMeasuresFormTable
datasourceId={datasourceId}
typeParams={{
measures: exprTypeParamsState,
expr: exprSql,
@@ -231,6 +281,9 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
);
};
const renderFooter = () => {
if (!hasMeasuresState) {
return <Button onClick={onCancel}></Button>;
}
if (currentStep === 1) {
return (
<>
@@ -266,26 +319,47 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
footer={renderFooter()}
onCancel={onCancel}
>
<Steps style={{ marginBottom: 28 }} size="small" current={currentStep}>
<Step title="基本信息" />
<Step title="度量信息" />
</Steps>
<Form
{...formLayout}
form={form}
initialValues={{
...formValRef.current,
}}
onValuesChange={(value) => {
const { isPercent } = value;
if (isPercent !== undefined) {
setIsPercentState(isPercent);
{hasMeasuresState ? (
<>
<Steps style={{ marginBottom: 28 }} size="small" current={currentStep}>
<Step title="基本信息" />
<Step title="度量信息" />
</Steps>
<Form
{...formLayout}
form={form}
initialValues={{
...formValRef.current,
}}
onValuesChange={(value) => {
const { isPercent } = value;
if (isPercent !== undefined) {
setIsPercentState(isPercent);
}
}}
className={styles.form}
>
{renderContent()}
</Form>
</>
) : (
<Result
status="warning"
subTitle="当前数据源缺少度量,无法创建指标。请前往数据源配置中,将字段设置为度量"
extra={
<Button
type="primary"
key="console"
onClick={() => {
history.replace(`/semanticModel/${domainId}/dataSource`);
onCancel?.();
}}
>
</Button>
}
}}
className={styles.form}
>
{renderContent()}
</Form>
/>
)}
</Modal>
);
};

View File

@@ -4,17 +4,21 @@ import ProTable from '@ant-design/pro-table';
import ProCard from '@ant-design/pro-card';
import SqlEditor from '@/components/SqlEditor';
import BindMeasuresTable from './BindMeasuresTable';
import FormLabelRequire from './FormLabelRequire';
import { ISemantic } from '../data';
type Props = {
typeParams: any;
measuresList: any[];
onFieldChange: (measures: any[]) => void;
datasourceId?: number;
typeParams: ISemantic.ITypeParams;
measuresList: ISemantic.IMeasure[];
onFieldChange: (measures: ISemantic.IMeasure[]) => void;
onSqlChange: (sql: string) => void;
};
const { TextArea } = Input;
const MetricMeasuresFormTable: React.FC<Props> = ({
datasourceId,
typeParams,
measuresList,
onFieldChange,
@@ -127,7 +131,7 @@ const MetricMeasuresFormTable: React.FC<Props> = ({
<Space direction="vertical" style={{ width: '100%' }}>
<ProTable
actionRef={actionRef}
headerTitle="度量列表"
headerTitle={<FormLabelRequire title="度量列表" />}
tooltip="基于本主题域下所有数据源的度量来创建指标且该列表的度量为了加以区分均已加上数据源名称作为前缀选中度量后可基于这几个度量来写表达式若是选中的度量来自不同的数据源系统将会自动join来计算该指标"
rowKey="name"
columns={columns}
@@ -149,7 +153,7 @@ const MetricMeasuresFormTable: React.FC<Props> = ({
]}
/>
<ProCard
title={'度量表达式'}
title={<FormLabelRequire title="度量表达式" />}
tooltip="度量表达式由上面选择的度量组成如选择了度量A和B则可将表达式写成A+B"
>
<SqlEditor
@@ -165,7 +169,11 @@ const MetricMeasuresFormTable: React.FC<Props> = ({
</Space>
{measuresModalVisible && (
<BindMeasuresTable
measuresList={measuresList}
measuresList={
datasourceId && Array.isArray(measuresList)
? measuresList.filter((item) => item.datasourceId === datasourceId)
: measuresList
}
selectedMeasuresList={measuresParams?.measures || []}
onSubmit={async (values: any[]) => {
const measures = values.map(({ bizName, name, expr, datasourceId }) => {

View File

@@ -0,0 +1,23 @@
import { Space, Tooltip } from 'antd';
import React from 'react';
import { ExclamationCircleOutlined } from '@ant-design/icons';
type Props = {
title: string;
tooltips: string;
};
const TableTitleTooltips: React.FC<Props> = ({ title, tooltips }) => {
return (
<>
<Space>
<span>{title}</span>
<Tooltip title={tooltips}>
<ExclamationCircleOutlined />
</Tooltip>
</Space>
</>
);
};
export default TableTitleTooltips;

View File

@@ -297,4 +297,18 @@
background: transparent;
border-style: dashed;
}
}
}
.semanticGraphCanvas {
position: relative;
.toolbar{
position: absolute;
width: 200px;
z-index: 999;
right: 0;
top: 5px;
}
.canvasContainer {
width: 100%;
}
}

View File

@@ -1,3 +1,5 @@
import { SemanticNodeType } from './enum';
export const SENSITIVE_LEVEL_OPTIONS = [
{
label: '低',
@@ -21,3 +23,21 @@ export const SENSITIVE_LEVEL_ENUM = SENSITIVE_LEVEL_OPTIONS.reduce(
},
{},
);
export const SEMANTIC_NODE_TYPE_CONFIG = {
[SemanticNodeType.DATASOURCE]: {
label: '数据源',
value: SemanticNodeType.DATASOURCE,
color: 'cyan',
},
[SemanticNodeType.DIMENSION]: {
label: '维度',
value: SemanticNodeType.DIMENSION,
color: 'blue',
},
[SemanticNodeType.METRIC]: {
label: '指标',
value: SemanticNodeType.METRIC,
color: 'orange',
},
};

View File

@@ -1,3 +1,5 @@
import { TreeGraphData } from '@antv/g6-core';
export type ISODateString =
`${number}-${number}-${number}T${number}:${number}:${number}.${number}+${number}:${number}`;
@@ -6,6 +8,8 @@ export type UserName = string;
export type SensitiveLevel = 0 | 1 | 2 | null;
export type RefreshGraphData = (graphRootData: TreeGraphData) => void;
export declare namespace IDataSource {
interface IIdentifiersItem {
name: string;
@@ -113,13 +117,13 @@ export declare namespace ISemantic {
interface IMeasure {
name: string;
agg: string;
agg?: string;
expr: string;
constraint: string;
alias: string;
createMetric: string;
constraint?: string;
alias?: string;
createMetric?: string;
bizName: string;
isCreateMetric: number;
isCreateMetric?: number;
datasourceId: number;
}
interface ITypeParams {
@@ -142,7 +146,7 @@ export declare namespace ISemantic {
domainId: number;
domainName: string;
type: string;
typeParams: TypeParams;
typeParams: ITypeParams;
fullPath: string;
dataFormatType: string;
dataFormat: string;

View File

@@ -7,3 +7,9 @@ export enum TransType {
DIMENSION = 'dimension',
METRIC = 'metric',
}
export enum SemanticNodeType {
DATASOURCE = 'datasource',
DIMENSION = 'dimension',
METRIC = 'metric',
}