[improvement][headless-fe] Optimized the tag setting system. (#846)

* [improvement][semantic-fe] Add model alias setting & Add view permission restrictions to the model permission management tab.
[improvement][semantic-fe] Add permission control to the action buttons for the main domain; apply high sensitivity filtering to the authorization of metrics/dimensions.
[improvement][semantic-fe] Optimize the editing mode in the dimension/metric/datasource components to use the modelId stored in the database for data, instead of relying on the data from the state manager.

* [improvement][semantic-fe] Add time granularity setting in the data source configuration.

* [improvement][semantic-fe] Dictionary import for dimension values supported in Q&A visibility

* [improvement][semantic-fe] Modification of data source creation prompt wording"

* [improvement][semantic-fe] metric market experience optimization

* [improvement][semantic-fe] enhance the analysis of metric trends

* [improvement][semantic-fe] optimize the presentation of metric trend permissions

* [improvement][semantic-fe] add metric trend download functionality

* [improvement][semantic-fe] fix the dimension initialization issue in metric correlation

* [improvement][semantic-fe] Fix the issue of database changes not taking effect when creating based on an SQL data source.

* [improvement][semantic-fe] Optimizing pagination logic and some CSS styles

* [improvement][semantic-fe] Fixing the API for the indicator list by changing "current" to "pageNum"

* [improvement][semantic-fe] Fixing the default value setting for the indicator list

* [improvement][semantic-fe] Adding batch operations for indicators/dimensions/models

* [improvement][semantic-fe] Replacing the single status update API for indicators/dimensions with a batch update API

* [improvement][semantic-fe] Redesigning the indicator homepage to incorporate trend charts and table functionality for indicators

* [improvement][semantic-fe] Optimizing the logic for setting dimension values and editing data sources, and adding system settings functionality

* [improvement][semantic-fe] Upgrading antd version to 5.x, extracting the batch operation button component, optimizing the interaction for system settings, and expanding the configuration generation types for list-to-select component.

* [improvement][semantic-fe] Adding the ability to filter dimensions based on whether they are tags or not.

* [improvement][semantic-fe] Adding the ability to edit relationships between models in the canvas.

* [improvement][semantic-fe] Updating the datePicker component to use dayjs instead.

* [improvement][semantic-fe] Fixing the issue with passing the model ID for dimensions in the indicator market.

* [improvement][semantic-fe] Fixing the abnormal state of the popup when creating a model.

* [improvement][semantic-fe] Adding permission logic for bulk operations in the indicator market.

* [improvement][semantic-fe] Adding the ability to download and transpose data.

* [improvement][semantic-fe] Fixing the initialization issue with the date selection component in the indicator details page when switching time granularity.

* [improvement][semantic-fe] Fixing the logic error in the dimension value setting.

* [improvement][semantic-fe] Fixing the synchronization issue with the question and answer settings information.

* [improvement][semantic-fe] Optimizing the canvas functionality for better performance and user experience.

* [improvement][semantic-fe] Optimizing the update process for drawing model relationship edges in the canvas.

* [improvement][semantic-fe] Changing the line type for canvas connections.

* [improvement][semantic-fe] Replacing the initialization variable from "semantic" to "headless".

* [improvement][semantic-fe] Fixing the missing migration issue for default drill-down dimension configuration in model editing. Additionally, optimizing the data retrieval method for initializing fields in the model.

* [improvement][semantic-fe] Updating the logic for the fieldName.

* [improvement][semantic-fe] Adjusting the position of the metrics tab.

* [improvement][semantic-fe] Changing the 字段名称 to 英文名称.

* [improvement][semantic-fe] Fix metric measurement deletion.

* [improvement][semantic-fe] UI optimization for metric details page.

* [improvement][semantic-fe] UI optimization for metric details page.

* [improvement][semantic-fe] UI adjustment for metric details page.

* [improvement][semantic-fe] The granularity field in the time type of model editing now supports setting it as empty.

* [improvement][semantic-fe] Added field type and metric type to the metric creation options.

* [improvement][semantic-fe] The organization structure selection feature has been added to the permission management.

* [improvement][semantic-fe] Improved user experience for the metric list.

* [improvement][semantic-fe] fix update the metric list.

* [improvement][headless-fe] Added view management functionality.

* [improvement][headless-fe] The view management functionality has been added. This feature allows users to create, edit, and manage different views within the system.

* [improvement][headless-fe] Added model editing side effect detection.

* [improvement][headless-fe] Fixed the logic error in view editing.

* [improvement][headless-fe] Fixed the issue with initializing dimension associations in metric settings.

* [improvement][headless-fe] Added the ability to hide the Q&A settings entry point.

* [improvement][headless-fe] Fixed the issue with selecting search results in metric field creation.

* [improvement][headless-fe] Added search functionality to the field list in model editing.

* [improvement][headless-fe] fix the field list in model editing

* [improvement][headless-fe] Restructured the data for the dimension value settings interface.

* [improvement][headless-fe] Added dynamic variable functionality to model creation based on SQL scripts.

* [improvement][headless-fe] Added support for passing dynamic variables as parameters in the executeSql function.

* [improvement][headless-fe] Resolved the issue where users were unable to select all options for dimensions, metrics, and fields in the metric generation process.

* [improvement][headless-fe] Replaced the term "view" with "dataset"

* [improvement][headless-fe] Added the ability to export metrics and dimensions to a specific target.

* [improvement][headless-fe] Enhanced dataset creation to support the tag mode.

* [improvement][headless-fe] Added tag value setting.

* [improvement][headless-fe] Optimized the tag setting system.
This commit is contained in:
tristanliu
2024-03-21 00:53:16 +08:00
committed by GitHub
parent 754902a67e
commit 01bfb57149
45 changed files with 2189 additions and 128 deletions

View File

@@ -64,7 +64,9 @@
"@antv/g6": "^4.8.23", "@antv/g6": "^4.8.23",
"@antv/g6-core": "^0.8.23", "@antv/g6-core": "^0.8.23",
"@antv/layout": "^0.3.20", "@antv/layout": "^0.3.20",
"@antv/x6": "1.30.1",
"@antv/xflow": "^1.0.55", "@antv/xflow": "^1.0.55",
"@antv/xflow-extension": "1.0.55",
"@babel/runtime": "^7.22.5", "@babel/runtime": "^7.22.5",
"@types/numeral": "^2.0.2", "@types/numeral": "^2.0.2",
"@types/react-draft-wysiwyg": "^1.13.2", "@types/react-draft-wysiwyg": "^1.13.2",

View File

@@ -5,7 +5,13 @@ import DataSourceFieldForm from './DataSourceFieldForm';
import { formLayout } from '@/components/FormHelper/utils'; import { formLayout } from '@/components/FormHelper/utils';
import { EnumDataSourceType } from '../constants'; import { EnumDataSourceType } from '../constants';
import styles from '../style.less'; import styles from '../style.less';
import { updateModel, createModel, getColumns, getUnAvailableItem } from '../../service'; import {
updateModel,
createModel,
getColumns,
getUnAvailableItem,
getTagObjectList,
} from '../../service';
import type { Dispatch } from 'umi'; import type { Dispatch } from 'umi';
import type { StateType } from '../../model'; import type { StateType } from '../../model';
import { connect } from 'umi'; import { connect } from 'umi';
@@ -63,7 +69,8 @@ const DataSourceCreateForm: React.FC<CreateFormProps> = ({
const [effectTipsData, setEffectTipsData] = useState< const [effectTipsData, setEffectTipsData] = useState<
(ISemantic.IDimensionItem | ISemantic.IMetricItem)[] (ISemantic.IDimensionItem | ISemantic.IMetricItem)[]
>([]); >([]);
const [tagObjectList, setTagObjectList] = useState<ISemantic.ITagObjectItem[]>([]);
const [tagObjectIdState, setTagObjectIdState] = useState(modelItem?.tagObjectId);
const formValRef = useRef(initFormVal as any); const formValRef = useRef(initFormVal as any);
const [form] = Form.useForm(); const [form] = Form.useForm();
const { databaseConfigList, selectModelId: modelId, selectDomainId } = domainManger; const { databaseConfigList, selectModelId: modelId, selectDomainId } = domainManger;
@@ -91,9 +98,23 @@ const DataSourceCreateForm: React.FC<CreateFormProps> = ({
} }
}, [scriptColumns]); }, [scriptColumns]);
useEffect(() => {
queryTagObjectList();
}, []);
const forward = () => setCurrentStep(currentStep + 1); const forward = () => setCurrentStep(currentStep + 1);
const backward = () => setCurrentStep(currentStep - 1); const backward = () => setCurrentStep(currentStep - 1);
const queryTagObjectList = async () => {
const { code, msg, data } = await getTagObjectList({ domainId: selectDomainId });
if (code === 200) {
setTagObjectList(data);
return;
}
message.error(msg);
};
const checkAvailableItem = async (fields: string[] = []) => { const checkAvailableItem = async (fields: string[] = []) => {
if (!modelItem) { if (!modelItem) {
return false; return false;
@@ -151,7 +172,7 @@ const DataSourceCreateForm: React.FC<CreateFormProps> = ({
(fieldsClassify, item: any) => { (fieldsClassify, item: any) => {
const { const {
type, type,
bizName, // bizName,
fieldName, fieldName,
timeGranularity, timeGranularity,
agg, agg,
@@ -159,7 +180,7 @@ const DataSourceCreateForm: React.FC<CreateFormProps> = ({
name, name,
isCreateMetric: createMetric, isCreateMetric: createMetric,
dateFormat, dateFormat,
entityNames, // entityNames,
isTag, isTag,
} = item; } = item;
const isCreateDimension = createDimension ? 1 : 0; const isCreateDimension = createDimension ? 1 : 0;
@@ -194,7 +215,8 @@ const DataSourceCreateForm: React.FC<CreateFormProps> = ({
isCreateDimension, isCreateDimension,
name, name,
type, type,
entityNames, // entityNames,
tagObjectId: modelItem?.tagObjectId,
}); });
break; break;
case EnumDataSourceType.MEASURES: case EnumDataSourceType.MEASURES:
@@ -253,6 +275,7 @@ const DataSourceCreateForm: React.FC<CreateFormProps> = ({
sqlQuery: sql, sqlQuery: sql,
sqlVariables: sqlParams, sqlVariables: sqlParams,
}, },
tagObjectId: tagObjectIdState,
}; };
setQueryParamsState(queryParams); setQueryParamsState(queryParams);
const checkState = await checkAvailableItem(fieldColumns.map((item) => item.nameEn)); const checkState = await checkAvailableItem(fieldColumns.map((item) => item.nameEn));
@@ -370,6 +393,7 @@ const DataSourceCreateForm: React.FC<CreateFormProps> = ({
initFields([], fieldColumns); initFields([], fieldColumns);
} }
} }
setTagObjectIdState(modelItem?.tagObjectId);
}, [modelItem]); }, [modelItem]);
useEffect(() => { useEffect(() => {
@@ -423,6 +447,11 @@ const DataSourceCreateForm: React.FC<CreateFormProps> = ({
<div style={{ display: currentStep === 1 ? 'block' : 'none' }}> <div style={{ display: currentStep === 1 ? 'block' : 'none' }}>
<DataSourceFieldForm <DataSourceFieldForm
fields={fields} fields={fields}
tagObjectList={tagObjectList}
tagObjectId={tagObjectIdState}
onTagObjectChange={(tagObjectId) => {
setTagObjectIdState(tagObjectId);
}}
onFieldChange={handleFieldChange} onFieldChange={handleFieldChange}
onSqlChange={(sql) => { onSqlChange={(sql) => {
setSqlFilter(sql); setSqlFilter(sql);

View File

@@ -4,6 +4,7 @@ import TableTitleTooltips from '../../components/TableTitleTooltips';
import { isUndefined } from 'lodash'; import { isUndefined } from 'lodash';
import { ExclamationCircleOutlined } from '@ant-design/icons'; import { ExclamationCircleOutlined } from '@ant-design/icons';
import SqlEditor from '@/components/SqlEditor'; import SqlEditor from '@/components/SqlEditor';
import { ISemantic } from '../../data';
import { import {
TYPE_OPTIONS, TYPE_OPTIONS,
DATE_FORMATTER, DATE_FORMATTER,
@@ -23,7 +24,8 @@ type FieldItem = {
checked?: number; checked?: number;
dateFormat?: string; dateFormat?: string;
timeGranularity?: string; timeGranularity?: string;
entityNames?: string[]; // entityNames?: string[];
// tagObjectId?: number;
isTag?: number; isTag?: number;
}; };
const { Search } = Input; const { Search } = Input;
@@ -32,8 +34,11 @@ const FormItem = Form.Item;
type Props = { type Props = {
onSqlChange: (sql: string) => void; onSqlChange: (sql: string) => void;
sql: string; sql: string;
tagObjectList: ISemantic.ITagObjectItem[];
tagObjectId?: number;
fields: FieldItem[]; fields: FieldItem[];
onFieldChange: (fieldName: string, data: Partial<FieldItem>) => void; onFieldChange: (fieldName: string, data: Partial<FieldItem>) => void;
onTagObjectChange?: (tagObjectId: number) => void;
}; };
const { Option } = Select; const { Option } = Select;
@@ -47,7 +52,15 @@ const getCreateFieldName = (type: EnumDataSourceType) => {
return isCreateName; return isCreateName;
}; };
const DataSourceFieldForm: React.FC<Props> = ({ fields, sql, onFieldChange, onSqlChange }) => { const DataSourceFieldForm: React.FC<Props> = ({
fields,
sql,
tagObjectList,
tagObjectId,
onTagObjectChange,
onFieldChange,
onSqlChange,
}) => {
const handleFieldChange = (record: FieldItem, fieldName: string, value: any) => { const handleFieldChange = (record: FieldItem, fieldName: string, value: any) => {
onFieldChange(record.bizName, { onFieldChange(record.bizName, {
...record, ...record,
@@ -123,12 +136,28 @@ const DataSourceFieldForm: React.FC<Props> = ({ fields, sql, onFieldChange, onSq
width: 185, width: 185,
render: (_: any, record: FieldItem) => { render: (_: any, record: FieldItem) => {
const { type } = record; const { type } = record;
console.log(record, 3333);
if (type === EnumDataSourceType.PRIMARY) { if (type === EnumDataSourceType.PRIMARY) {
const entityNames =
fields.find((field) => field.bizName === record.bizName)?.entityNames || [];
return ( return (
<Space> <Space>
{/* <FormItem name="tagObjectId"> */}
<Select <Select
style={{ minWidth: 150 }}
value={tagObjectId}
placeholder="请选择所属对象"
onChange={(value) => {
// handleFieldChange(record, 'tagObjectId', value);
onTagObjectChange?.(value);
}}
options={tagObjectList.map((item: ISemantic.ITagObjectItem) => {
return {
label: item.name,
value: item.id,
};
})}
/>
{/* </FormItem> */}
{/* <Select
style={{ minWidth: 345 }} style={{ minWidth: 345 }}
mode="tags" mode="tags"
value={entityNames} value={entityNames}
@@ -141,7 +170,7 @@ const DataSourceFieldForm: React.FC<Props> = ({ fields, sql, onFieldChange, onSq
/> />
<Tooltip title="主键可以作为一个实体,在此设置一个或多个实体名称"> <Tooltip title="主键可以作为一个实体,在此设置一个或多个实体名称">
<ExclamationCircleOutlined /> <ExclamationCircleOutlined />
</Tooltip> </Tooltip> */}
</Space> </Space>
); );
} }

View File

@@ -0,0 +1 @@
.create-entity-container {}

View File

@@ -0,0 +1,148 @@
import React from 'react'
import { Modal, Form, Input, Radio, Select } from 'antd'
import _ from 'lodash'
import { EntityType, EntityTypeDisplay } from '../const'
import './index.less';
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 6 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 16 },
},
};
interface Props {
visible: boolean;
onOk: Function;
onCancel: Function;
}
/** 创建模型弹窗 */
const CreateEntityModal = (props: Props) => {
const { visible, onOk, onCancel } = props
const [confirmLoading, setConfirmLoading] = React.useState<boolean>(false)
const [currentEntityType, setCurrentEntityType] = React.useState<EntityType>(EntityType.FACT)
const [form] = Form.useForm()
const hanldeOk = () => {
form.validateFields().then(values => {
const callback = (result: any) => {
setConfirmLoading(false)
if (result) {
onCancel();
}
}
setConfirmLoading(true)
onOk({
...values,
cb: callback,
})
})
};
const onChange = (e: any) => {
/** 切换模型类型重置表单 */
form.resetFields();
setCurrentEntityType(e.target.value)
};
return (
<Modal
title="创建模型"
visible={visible}
confirmLoading={confirmLoading}
wrapClassName="create-entity-container"
okText="确定"
cancelText="取消"
onOk={hanldeOk}
onCancel={() => onCancel()}
mask={false}
centered
destroyOnClose={true}
>
<Form form={form}>
<Form.Item
{...formItemLayout}
name="entityType"
label="模型类型"
rules={[{ required: true }]}
initialValue={currentEntityType}
>
<Radio.Group onChange={onChange}>
{_.map(EntityType, (type: EntityType) => {
return (
<Radio value={type} key={type}>
{EntityTypeDisplay[type]}
</Radio>
);
})}
</Radio.Group>
</Form.Item>
<Form.Item
{...formItemLayout}
name="displayName"
label="中文名"
rules={
[
{
required: true,
validator: (rule, v, callback) => {
if (!v) {
callback('请输入中文名称');
}
const reg1 = new RegExp(`^[a-zA-Z0-9_]*$`);
if (reg1.test(v)) {
callback('必须包含中文');
}
const reg2 = new RegExp('^[\\u4e00-\\u9fa5a-zA-Z0-9_]*$');
if (reg2.test(v)) {
callback();
} else {
callback('只能包含中文、字符、数字、下划线');
}
},
},
]
}
initialValue={'用户创建的表'}
>
<Input placeholder="请输入中文名称" autoComplete="off" />
</Form.Item>
<Form.Item
{...formItemLayout}
name="name"
label="英文名"
rules={
[
{
required: true,
validator: (rule, v, callback) => {
if (!v) {
callback('请输入英文名');
} else if (v.includes(' ')) {
callback('不能包含空格');
}
const reg = new RegExp(`^[a-zA-Z0-9_]*$`);
if (reg.test(v)) {
callback();
} else {
callback('只能包含数字、字符、下划线');
}
},
},
]
}
initialValue={'customNode'}
>
<Input placeholder="请输入英文名" autoComplete="off" />
</Form.Item>
</Form>
</Modal>
);
}
export default CreateEntityModal

View File

@@ -0,0 +1 @@
.create-relation-container {}

View File

@@ -0,0 +1,89 @@
import React, { useState } from 'react'
import { Modal, Form, Input, Select } from 'antd'
import type { EntityCanvasModel } from '../interface'
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 6 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 16 },
},
}
interface Props {
visible: boolean
onOk: (value: any) => void
onCancel: () => void
sourceEntity?: EntityCanvasModel
targetEntity?: EntityCanvasModel
}
const CreateRelationModal = (props: Props) => {
const { visible, sourceEntity, targetEntity, onOk, onCancel } = props
const [confirmLoading, setConfirmLoading] = useState<boolean>(false)
const [form] = Form.useForm()
const handleOK = () => {
form.validateFields().then(values => {
setConfirmLoading(true)
const cb = () => {
setConfirmLoading(false)
}
onOk({ ...values, cb })
})
}
return (
<Modal
title="关联模型"
visible={visible}
confirmLoading={confirmLoading}
wrapClassName="create-relation-container"
okText="确定"
cancelText="取消"
onOk={handleOK}
onCancel={onCancel}
mask={false}
centered
destroyOnClose={true}
>
<Form form={form}>
<Form.Item
{...formItemLayout}
name="SOURCE_GUID"
label="SOURCE_GUID"
rules={[{ required: true }]}
initialValue={`${sourceEntity?.entityName || ''}(${sourceEntity?.entityId || ''})`}
>
<Input />
</Form.Item>
<Form.Item
{...formItemLayout}
name="TARGET_GUID"
label="TARGET_GUID"
rules={[{ required: true }]}
initialValue={`${targetEntity?.entityName || ''}(${targetEntity?.entityId || ''})`}
>
<Input />
</Form.Item>
<Form.Item
{...formItemLayout}
name="RELATION_TYPE"
label="选择关联关系"
rules={[{ required: true }]}
initialValue={'N:1'}
>
<Select placeholder="请选择关联关系">
<Select.Option value="N:1"></Select.Option>
<Select.Option value="1:N"></Select.Option>
</Select>
</Form.Item>
</Form>
</Modal>
)
}
export default CreateRelationModal

View File

@@ -0,0 +1,20 @@
.xflow-er-solution-toolbar {
display: flex;
align-items: center;
width: 100%;
height: 40px;
background-color: #ced4de;
.icon {
padding: 0 8px;
}
.disabled {
cursor: not-allowed;
color: rgba(0, 0, 0, 0.3)
}
.icon:hover {
color: #000;
}
}

View File

@@ -0,0 +1,53 @@
import React from 'react'
import {} from 'antd'
import { PlusCircleOutlined, DeleteOutlined, LinkOutlined } from '@ant-design/icons'
import { MODELS, useXFlowApp } from '@antv/xflow'
import './index.less'
interface Props {
onAddNodeClick: () => void
onDeleteNodeClick: () => void
onConnectEdgeClick: () => void
}
const GraphToolbar = (props: Props) => {
const { onAddNodeClick, onDeleteNodeClick, onConnectEdgeClick } = props
const [selectedNodes, setSelectedNodes] = React.useState([])
/** 监听画布中选中的节点 */
const watchModelService = async () => {
const appRef = useXFlowApp()
const modelService = appRef && appRef?.modelService
if (modelService) {
const model = await MODELS.SELECTED_NODES.getModel(modelService)
model.watch(async () => {
const nodes = await MODELS.SELECTED_NODES.useValue(modelService)
setSelectedNodes(nodes)
})
}
}
watchModelService()
return (
<div className="xflow-er-solution-toolbar">
<div className="icon" onClick={() => onAddNodeClick()}>
<span></span>
<PlusCircleOutlined />
</div>
<div className="icon" onClick={() => onConnectEdgeClick()}>
<span></span>
<LinkOutlined />
</div>
<div
className={`icon ${selectedNodes?.length > 0 ? '' : 'disabled'}`}
onClick={() => onDeleteNodeClick()}
>
<span></span>
<DeleteOutlined />
</div>
</div>
)
}
export default GraphToolbar

View File

@@ -0,0 +1,25 @@
import { createCmdConfig, DisposableCollection } from '@antv/xflow'
import { MockApi } from './service'
export const useCmdConfig = createCmdConfig(config => {
/** 设置hook */
config.setRegisterHookFn(hooks => {
const list = [
hooks.addNode.registerHook({
name: 'addNodeHook',
handler: async args => {
args.createNodeService = MockApi.addNode
},
}),
hooks.addEdge.registerHook({
name: 'addEdgeHook',
handler: async args => {
args.createEdgeService = MockApi.addEdge
},
}),
]
const toDispose = new DisposableCollection()
toDispose.pushAll(list)
return toDispose
})
})

View File

@@ -0,0 +1,32 @@
import { createGraphConfig } from '@antv/xflow'
export const useGraphConfig = createGraphConfig(config => {
/** 预设XFlow画布配置项 */
config.setX6Config({
grid: true,
scroller: {
enabled: true,
},
scaling: {
min: 0.2,
max: 3,
},
connecting: {
/** 连线过程中距离目标节点50px时自动吸附 */
snap: {
radius: 50,
},
connector: {
name: 'rounded',
args: {
radius: 50,
},
},
router: {
name: 'er',
},
/** 不允许连接到画布空白位置, 即没有目标节点时连线会自动消失 */
allowBlank: false,
},
})
})

View File

@@ -0,0 +1,80 @@
import type { NsNodeCmd, NsEdgeCmd, IGraphCommandService } from '@antv/xflow'
import { createKeybindingConfig, XFlowNodeCommands, XFlowEdgeCommands, MODELS } from '@antv/xflow'
import type { Node as X6Node, Edge as X6Edge } from '@antv/x6'
import { Platform } from '@antv/x6'
import { message } from 'antd'
/** 快捷键 */
enum ShortCut {
DELETE = 'Backspace', // 删除
CmdDelete = 'Cmd+Delete', // Mac按住Command多选删除
CtrlDelete = 'Ctrl+Delete', // Windows按住Ctrl多选删除
}
export const useKeybindingConfig = createKeybindingConfig(config => {
config.setKeybindingFunc(registry => {
return registry.registerKeybinding([
{
id: 'delete',
keybinding: ShortCut.DELETE,
callback: async (item, modelService, commandService, e) => {
/** 如果是input的delete事件, 则不走删除的回调 */
const target = e && (e?.target as HTMLElement)
if (target && target.tagName && target.tagName.toLowerCase() === 'input') {
return
}
const cells = await MODELS.SELECTED_CELLS.useValue(modelService)
const nodes = cells?.filter(cell => cell.isNode())
const edges = cells?.filter(cell => cell.isEdge())
if (edges?.length > 0) {
deleteEdges(commandService, edges as X6Edge[])
}
if (nodes?.length > 0) {
deleteNodes(commandService, nodes as X6Node[])
}
},
},
{
id: 'deleteAll',
keybinding: Platform.IS_MAC ? ShortCut.CmdDelete : ShortCut.CtrlDelete,
callback: async (item, modelService, commandService, e) => {
const cells = await MODELS.SELECTED_CELLS.useValue(modelService)
const nodes = cells?.filter(cell => cell.isNode())
const edges = cells?.filter(cell => cell.isEdge())
deleteEdges(commandService, edges as X6Edge[])
deleteNodes(commandService, nodes as X6Node[])
},
},
])
})
})
export const deleteNodes = async (commandService: IGraphCommandService, nodes: X6Node[]) => {
const promiseList = nodes?.map(node => {
return commandService.executeCommand(XFlowNodeCommands.DEL_NODE.id, {
nodeConfig: {
...node.getData(),
},
} as NsNodeCmd.DelNode.IArgs)
})
const res = await Promise.all(promiseList)
if (res.length > 0) {
message.success('删除节点成功!')
}
}
export const deleteEdges = async (commandServce: IGraphCommandService, edges: X6Edge[]) => {
const promiseList = edges
?.filter(edge => edge.isEdge())
?.map(edge => {
return commandServce.executeCommand(XFlowEdgeCommands.DEL_EDGE.id, {
edgeConfig: {
...edge.getData(),
},
} as NsEdgeCmd.DelEdge.IArgs)
})
const res = await Promise.all(promiseList)
if (res.length > 0) {
message.success('删除连线成功!')
}
}

View File

@@ -0,0 +1,16 @@
export enum GraphMode {
INFO = 'INFO', // 缩略模式
DETAIL = 'DETAIL', // 详情模式
}
export enum EntityType {
FACT = 'FACT',
DIM = 'DIM',
OTHER = 'OTHER',
}
export const EntityTypeDisplay = {
[EntityType.FACT]: '事实表',
[EntityType.DIM]: '维度表',
[EntityType.OTHER]: '其他表',
}

View File

@@ -0,0 +1,57 @@
// @import '@antv/xflow/dist/index.css';
.xflow-er-solution-container {
width: 100%;
height: 550px;
border: 1px solid #ebedf1;
background-color: #fff;
.cursor-tip-container {
position: fixed;
top: 0;
left: 0;
z-index: 999;
display: none;
width: 200px;
height: 40px;
color: #000;
background: #ced4de;
.draft-entity-container {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
padding-left: 8px;
}
}
.xflow-canvas-root {
margin-top: 40px;
}
/** 覆盖节点默认选中色 */
.x6-node-selected rect {
stroke: #1890ff;
stroke-width: 4px;
}
.x6-edge-selected path {
stroke: #1890ff;
stroke-width: 2px;
}
/** 默认隐藏链接桩 */
.x6-port-body {
z-index: 1;
visibility: hidden;
}
}
.er-demo {
.__dumi-default-previewer-actions {
border: 0;
}
}

View File

@@ -0,0 +1,314 @@
import React, { useState } from 'react';
import type { IAppLoad, NsGraph, IApplication } from '@antv/xflow';
import { XFlow, XFlowCanvas, KeyBindings } from '@antv/xflow';
import { XFlowAppProvider, useXFlowApp } from '@antv/xflow';
import type { NsGraphCmd, NsNodeCmd, NsEdgeCmd } from '@antv/xflow';
import { XFlowGraphCommands, XFlowNodeCommands, XFlowEdgeCommands } from '@antv/xflow';
import { CanvasMiniMap, CanvasScaleToolbar, CanvasSnapline } from '@antv/xflow';
import { MODELS } from '@antv/xflow';
import GraphToolbar from './GraphToolbar/index';
import { connect } from 'umi';
/** 配置画布 */
import { useGraphConfig } from './config-graph';
/** 配置Command */
import { useCmdConfig } from './config-cmd';
/** 配置快捷键 */
import { useKeybindingConfig } from './config-keybinding';
import { message } from 'antd';
import type { EntityCanvasModel } from './interface';
import CreateNodeModal from './CreateNodeModal';
import CreateRelationModal from './CreateRelationModal';
import Entity from './react-node/Entity';
import Relation from './react-edge/Relation';
import '@antv/xflow/dist/index.css';
import './index.less';
/** Mock所有与服务端交互的接口 */
import { MockApi } from './service';
type Props = {
domainManger: StateType;
dispatch: Dispatch;
};
/** 鼠标的引用 */
let cursorTipRef: HTMLDivElement;
/** 鼠标在画布的位置 */
let cursorLocation: any;
const DomainManger: React.FC<Props> = (demoProps: Props) => {
/** XFlow应用实例 */
const app = useXFlowApp();
/** 画布配置项 */
const graphConfig = useGraphConfig();
/** 预设XFlow画布需要渲染的React节点 / 边 */
graphConfig.setNodeRender('NODE1', (props) => {
return <Entity {...props} deleteNode={handleDeleteNode} />;
});
graphConfig.setEdgeRender('EDGE1', (props) => {
return <Relation {...props} deleteRelation={handleDeleteEdge} />;
});
/** 命令配置项 */
const cmdConfig = useCmdConfig();
/** 快捷键配置项 */
const keybindingConfig = useKeybindingConfig();
/** 是否画布处于可以新建节点状态 */
const [graphStatuts, setGraphStatus] = useState<string>('NORMAL');
/** 是否展示新建节点弹窗 */
const [isShowCreateNodeModal, setIsShowCreateNodeModal] = useState<boolean>(false);
/** 是否展示新建关联关系弹窗 */
const [isShowCreateRelationModal, setIsShowCreateRelationModal] = useState<boolean>(false);
/** 连线source数据 */
const [relationSourceData, setRelationSourceData] = useState<EntityCanvasModel>(undefined);
/** 连线target数据 */
const [relationTargetData, setRelationTargetData] = useState<EntityCanvasModel>(undefined);
/** XFlow初始化完成的回调 */
const onLoad: IAppLoad = async (app) => {
const graph = await app.getGraphInstance();
graph.zoom(-0.2);
/** Mock从服务端获取数据 */
const graphData = await MockApi.loadGraphData();
/** 渲染画布数据 */
await app.executeCommand(XFlowGraphCommands.GRAPH_RENDER.id, {
graphData,
} as NsGraphCmd.GraphRender.IArgs);
/** 设置监听事件 */
await watchEvent(app);
};
/** 监听事件 */
const watchEvent = async (appRef: IApplication) => {
if (appRef) {
const graph = await appRef.getGraphInstance();
graph.on('node:mousedown', ({ e, x, y, node, view }) => {
appRef.executeCommand(XFlowNodeCommands.FRONT_NODE.id, {
nodeId: node?.getData()?.id,
} as NsNodeCmd.FrontNode.IArgs);
});
graph.on('edge:connected', ({ edge }) => {
const relationSourceData = edge?.getSourceNode()?.data;
const relationTargetData = edge?.getTargetNode()?.data;
setRelationSourceData(relationSourceData);
setRelationTargetData(relationTargetData);
setIsShowCreateRelationModal(true);
/** 拖拽过程中会生成一条无实际业务含义的线, 需要手动删除 */
const edgeData: NsGraph.IEdgeConfig = edge?.getData();
if (!edgeData) {
appRef.executeCommand(XFlowEdgeCommands.DEL_EDGE.id, {
x6Edge: edge as any,
} as NsEdgeCmd.DelEdge.IArgs);
}
});
graph.on('node:mouseenter', ({ e, node, view }) => {
changePortsVisible(true);
});
graph.on('node:mouseleave', ({ e, node, view }) => {
changePortsVisible(false);
});
graph.on('edge:click', ({ edge }) => {
edge.toFront();
});
}
};
const changePortsVisible = (visible: boolean) => {
const ports = document.body.querySelectorAll('.x6-port-body') as NodeListOf<SVGElement>;
for (let i = 0, len = ports.length; i < len; i = i + 1) {
ports[i].style.visibility = visible ? 'visible' : 'hidden';
}
};
/** 创建画布节点 */
const handleCreateNode = async (values: any) => {
const { cb, ...rest } = values;
const graph = await app.getGraphInstance();
/** div块鼠标的位置转换为画布的位置 */
const graphLoc = graph.clientToLocal(cursorLocation.x, cursorLocation.y - 200);
const res = await app.executeCommand(XFlowNodeCommands.ADD_NODE.id, {
nodeConfig: {
id: 'customNode',
x: graphLoc.x,
y: graphLoc.y,
width: 214,
height: 252,
renderKey: 'NODE1',
entityId: values?.name,
entityName: values?.displayName,
entityType: 'FACT',
},
} as NsNodeCmd.AddNode.IArgs);
if (res) {
cb && cb();
setIsShowCreateNodeModal(false);
message.success('添加节点成功!');
}
};
/** 删除画布节点 */
const handleDeleteNode = async (nodeId: string) => {
const res = await app.executeCommand(XFlowNodeCommands.DEL_NODE.id, {
nodeConfig: { id: nodeId },
} as NsNodeCmd.DelNode.IArgs);
if (res) {
message.success('删除节点成功!');
}
};
/** 创建关联关系 */
const handleCreateEdge = async (values: any) => {
const { cb, ...rest } = values;
const res = await app.executeCommand(XFlowEdgeCommands.ADD_EDGE.id, {
edgeConfig: {
id: 'fact1-other2',
source: 'fact1',
target: 'other2',
renderKey: 'EDGE1',
edgeContentWidth: 20,
edgeContentHeight: 20,
/** 设置连线样式 */
attrs: {
line: {
stroke: '#d8d8d8',
strokeWidth: 1,
targetMarker: {
name: 'classic',
},
},
},
},
} as NsEdgeCmd.AddEdge.IArgs);
if (res) {
cb && cb();
setIsShowCreateRelationModal(false);
message.success('添加连线成功!');
}
};
/** 删除关联关系 */
const handleDeleteEdge = async (relationId: string) => {
const res = await app.executeCommand(XFlowEdgeCommands.DEL_EDGE.id, {
edgeConfig: { id: relationId },
} as NsEdgeCmd.DelEdge.IArgs);
if (res) {
message.success('删除连线成功!');
}
};
/** 设置鼠标样式 */
const configCursorTip = ({ left, top, display }) => {
cursorTipRef.style.left = left;
cursorTipRef.style.top = top;
cursorTipRef.style.display = display;
};
return (
<XFlowAppProvider>
<div
onMouseMove={(e) => {
if (graphStatuts === 'CREATE') {
configCursorTip({
left: `${e.pageX}px`,
top: `${e.pageY - 180}px`,
display: 'block',
});
}
}}
onMouseDown={(e) => {
if (graphStatuts === 'CREATE') {
cursorLocation = { x: e.pageX, y: e.pageY };
setIsShowCreateNodeModal(true);
configCursorTip({
left: '0px',
top: '0px',
display: 'none',
});
setGraphStatus('NORMAL');
}
}}
onMouseLeave={(e) => {
if (graphStatuts === 'CREATE') {
configCursorTip({
left: '0px',
top: '0px',
display: 'none',
});
}
}}
>
<XFlow className="xflow-er-solution-container" commandConfig={cmdConfig} onLoad={onLoad}>
<GraphToolbar
onAddNodeClick={() => {
message.info('鼠标移动到画布空白位置, 再次点击鼠标完成创建', 2);
setGraphStatus('CREATE');
}}
onDeleteNodeClick={async () => {
const modelService = app.modelService;
const nodes = await MODELS.SELECTED_NODES.useValue(modelService);
nodes.forEach((node) => {
handleDeleteNode(node?.id);
});
}}
onConnectEdgeClick={() => {
setIsShowCreateRelationModal(true);
}}
/>
<XFlowCanvas config={graphConfig}>
<CanvasMiniMap nodeFillColor="#ced4de" minimapOptions={{}} />
<CanvasScaleToolbar position={{ top: 12, left: 20 }} />
<CanvasSnapline />
</XFlowCanvas>
<KeyBindings config={keybindingConfig} />
{/** 占位空节点 */}
{graphStatuts === 'CREATE' && (
<div
className="cursor-tip-container"
ref={(ref) => {
ref && (cursorTipRef = ref);
}}
>
<div className="draft-entity-container">
<div></div>
</div>
</div>
)}
{/** 创建节点弹窗 */}
<CreateNodeModal
visible={isShowCreateNodeModal}
onOk={handleCreateNode}
onCancel={() => {
setIsShowCreateNodeModal(false);
}}
/>
{/** 创建关联关系弹窗 */}
<CreateRelationModal
visible={isShowCreateRelationModal}
sourceEntity={relationSourceData}
targetEntity={relationTargetData}
onOk={handleCreateEdge}
onCancel={() => {
setIsShowCreateRelationModal(false);
}}
/>
</XFlow>
</div>
</XFlowAppProvider>
);
};
export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger,
}))(DomainManger);

View File

@@ -0,0 +1,33 @@
import type { NsGraph } from '@antv/xflow'
/** 实体数据模型 */
export interface EntityModel {
/** 实体id */
entityId: string
/** 实体name */
entityName: string
/** 实体类型 */
entityType: string
/** 实体的属性字段 */
properties: EntityProperty[]
}
/** 属性字段数据模型 */
export interface EntityProperty {
/** 属性id */
propertyId: string
/** 属性名称 */
propertyName: string
/** 属性类型 */
propertyType: string
/** 是否主键 */
isPK?: boolean
/** 是否外键 */
isFK?: boolean
}
/** 画布实体渲染模型 */
export interface EntityCanvasModel extends EntityModel, NsGraph.INodeConfig {}
/** 画布连线渲染模型 */
export type RelationCanvasModel = NsGraph.IEdgeConfig

View File

@@ -0,0 +1,127 @@
import type { EntityProperty, EntityCanvasModel, RelationCanvasModel } from './interface';
export const mockProperties: EntityProperty[] = [
{
propertyId: 'propertyId1',
propertyName: '业务日期',
propertyType: 'string',
isPK: true,
},
{
propertyId: 'propertyId2',
propertyName: '交易号1',
propertyType: 'bigint',
isFK: true,
},
{
propertyId: 'propertyId3',
propertyName: '最长显示的表单名最长显示的表单名',
propertyType: 'string',
},
{
propertyId: 'propertyId4',
propertyName: '交易支付外键',
propertyType: 'string',
},
{
propertyId: 'propertyId5',
propertyName: '卖家支付日期',
propertyType: 'string',
},
{
propertyId: 'propertyId6',
propertyName: '网商银行',
propertyType: 'string',
},
{
propertyId: 'propertyId7',
propertyName: '业务日期',
propertyType: 'string',
},
{
propertyId: 'propertyId8',
propertyName: '业务日期111',
propertyType: 'string',
},
{
propertyId: 'propertyId9',
propertyName: '业务日期222',
propertyType: 'string',
},
{
propertyId: 'propertyId10',
propertyName: '业务日期333',
propertyType: 'string',
},
];
export const mockEntityData: EntityCanvasModel[] = [
{
id: 'fact1',
x: 450,
y: 150,
width: 214,
height: 252,
entityId: 'fact1',
entityName: '模型',
entityType: 'FACT',
properties: mockProperties,
},
{
id: 'fact2',
x: 0,
y: -20,
width: 214,
height: 252,
entityId: 'fact2',
entityName: '事实表2',
entityType: 'FACT',
properties: mockProperties,
},
{
id: 'dim1',
x: 0,
y: 300,
width: 214,
height: 252,
entityId: 'dim1',
entityName: '维度表1',
entityType: 'DIM',
properties: mockProperties,
},
{
id: 'other1',
x: 180,
y: 500,
width: 214,
height: 252,
entityId: 'other1',
entityName: '其他表1',
entityType: 'OTHER',
properties: mockProperties,
},
{
id: 'other2',
x: 810,
y: 0,
width: 214,
height: 252,
entityId: 'other2',
entityName: '其他表2',
entityType: 'OTHER',
properties: mockProperties,
},
];
export const mockRelationData: RelationCanvasModel[] = [
{
id: 'fact1-fact2',
source: 'fact1',
target: 'fact2',
},
{
id: 'fact1-dim1',
source: 'fact1',
target: 'dim1',
},
];

View File

@@ -0,0 +1,74 @@
.relation-count-container {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
color: #868c91;
background-color: #d8d8d8;
cursor: pointer;
&:hover {
color: #fff;
background-color: #1890ff;
}
}
.relation-operation-popover .ant-popover-inner-content {
padding: 0;
}
.relation-operation-container {
width: 220px;
max-height: 80px;
padding: 12px 16px;
overflow: hidden;
background-color: #fff;
&:hover {
overflow-y: auto;
}
&::-webkit-scrollbar {
width: 5px;
background: #2b2f33;
}
&::-webkit-scrollbar-thumb {
background: #5f656b;
border-radius: 10px;
}
.relation-operation-item {
display: flex;
align-items: center;
justify-content: space-between;
height: 28px;
color: #000;
.relation-operation-item-content {
display: flex;
flex-basis: 160px;
align-items: center;
justify-content: space-between;
height: 100%;
// &:hover {
// cursor: pointer;
// background: #d8d8d8
// }
}
.relation-property-source,
.relation-property-target {
display: inline-block;
max-width: 65px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.relation-property-source {
padding-right: 6px;
}
.relation-property-target {
padding-left: 6px;
}
}
}

View File

@@ -0,0 +1,64 @@
import React from 'react';
import type { NsGraph } from '@antv/xflow';
import type { RelationCanvasModel } from '../interface';
import { Popover, Popconfirm, Tooltip } from 'antd';
import { ScissorOutlined } from '@ant-design/icons';
import _ from 'lodash';
import './Relation.less';
interface OwnProps {
deleteRelation: Function;
}
type Props = OwnProps & NsGraph.IReactEdgeProps;
const Relation = (props: Props) => {
const relation: RelationCanvasModel = props?.data;
const renderRelationOperationItem = (relation: RelationCanvasModel) => {
const sourcePropertyName = relation?.source;
const targetPropertyName = relation?.target;
return (
<div className="relation-operation-item" key={relation.id}>
<div className="relation-operation-item-content">
<Tooltip placement="top" title={sourcePropertyName}>
<span className="relation-property-source">{sourcePropertyName}</span>
</Tooltip>
(N:1)
<Tooltip placement="top" title={targetPropertyName}>
<span className="relation-property-target">{targetPropertyName}</span>
</Tooltip>
</div>
<Popconfirm
placement="leftTop"
title="你确定要删除该关系吗"
okText="确定"
cancelText="取消"
onConfirm={() => {
props?.deleteRelation(relation.id);
}}
>
<ScissorOutlined />
</Popconfirm>
</div>
);
};
const renderPopoverContent = () => {
return (
<div className="relation-operation-container">{renderRelationOperationItem(relation)}</div>
);
};
return (
<Popover
trigger={'hover'}
content={renderPopoverContent()}
overlayClassName="relation-operation-popover"
>
<div className="relation-count-container">{1}</div>
</Popover>
);
};
export default Relation;

View File

@@ -0,0 +1,122 @@
.entity-container {
width: 100%;
height: 100%;
background-color: white;
border-radius: 2px;
&.fact {
border: 1px solid #cdddfd;
&:hover {
border: 1px solid #1890ff;
}
}
&.dim {
border: 1px solid #decfea;
&:hover {
border: 1px solid #1890ff;
}
}
&.other {
border: 1px solid #ced4de;
&:hover {
border: 1px solid #1890ff;
}
}
.content {
width: calc(100% - 2px);
height: calc(100% - 2px);
margin: 1px 1px;
&.fact {
background-color: #cdddfd;
}
&.dim {
background-color: #decfea;
}
&.other {
background-color: #ced4de;
}
.head {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: calc(100% - 12px);
height: 38px;
margin-left: 6px;
font-size: 12px;
.type {
padding-right: 8px;
}
.del-icon {
cursor: pointer;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
size: 16px;
font-size: 14px;
&:hover {
opacity: 0.6;
color: #1890ff;
}
}
}
.body {
width: calc(100% - 12px);
height: calc(100% - 36px - 6px);
margin-bottom: 6px;
margin-left: 6px;
overflow: auto;
background-color: white;
cursor: pointer;
.body-item {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 100%;
height: 28px;
color: #595959;
font-size: 12px;
border-bottom: 1px solid rgba(206, 212, 222, 0.2);
.name {
margin-left: 6px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
.pk,
.fk {
width: 12px;
margin-right: 6px;
color: #ffd666;
font-family: HelveticaNeue-CondensedBold;
}
}
.type {
margin-right: 8px;
color: #bfbfbf;
font-size: 8px;
}
}
}
}
}

View File

@@ -0,0 +1,61 @@
import React from 'react'
import type { NsGraph } from '@antv/xflow'
import type { EntityCanvasModel, EntityProperty } from '../interface'
import { BarsOutlined, DeleteOutlined } from '@ant-design/icons'
import { EntityType } from '../const'
import './Entity.less'
interface OwnProps {
deleteNode: Function
}
type Props = OwnProps & NsGraph.IReactNodeProps
const Entity = (props: Props) => {
const entity: EntityCanvasModel = props?.data
const getCls = () => {
if (entity?.entityType === EntityType.FACT) {
return 'fact'
}
if (entity?.entityType === EntityType.DIM) {
return 'dim'
}
if (entity?.entityType === EntityType.OTHER) {
return 'other'
}
return ''
}
return (
<div className={`entity-container ${getCls()}`}>
<div className={`content ${getCls()}`}>
<div className="head">
<div>
<BarsOutlined className="type" />
<span>{entity?.entityName}</span>
</div>
<div className="del-icon" onClick={() => props.deleteNode(entity?.id)}>
<DeleteOutlined />
</div>
</div>
<div className="body">
{entity?.properties?.map((property: EntityProperty) => {
return (
<div className="body-item" key={property.propertyId}>
<div className="name">
{property?.isPK && <span className="pk">PK</span>}
{property?.isFK && <span className="fk">FK</span>}
{property?.propertyName}
</div>
<div className="type">{property.propertyType}</div>
</div>
)
})}
</div>
</div>
</div>
)
}
export default Entity

View File

@@ -0,0 +1,129 @@
import type { NsGraph, NsNodeCmd, NsEdgeCmd } from '@antv/xflow'
import { mockEntityData, mockRelationData } from './mock'
/** mock后端接口调用 */
export namespace MockApi {
/** 加载画布数据 */
export const loadGraphData = async () => {
const promise: Promise<NsGraph.IGraphData> = new Promise(resolve => {
setTimeout(() => {
/** 链接桩样式配置, 将具有相同行为和外观的链接桩归为同一组 */
const portAttrs = {
circle: {
r: 7,
stroke: '#31d0c6',
strokeWidth: 2,
fill: '#fff',
/** 链接桩在连线交互时可以被连接上 */
magnet: true,
},
}
const nodes: NsGraph.INodeConfig[] = mockEntityData?.map(entity => {
const nodeConfig: NsGraph.INodeConfig = {
...entity,
renderKey: 'NODE1',
ports: {
groups: {
top: {
position: 'top',
attrs: portAttrs,
},
right: {
position: 'right',
attrs: portAttrs,
},
bottom: {
position: 'bottom',
attrs: portAttrs,
},
left: {
position: 'left',
attrs: portAttrs,
},
},
items: [
{ id: 'top_port', group: 'top' },
{ id: 'right_port', group: 'right' },
{ id: 'bottom_port', group: 'bottom' },
{ id: 'left_port', group: 'left' },
],
},
}
return nodeConfig
})
const edges: NsGraph.IEdgeConfig[] = mockRelationData?.map(relation => {
const edgeConfig: NsGraph.IEdgeConfig = {
...relation,
renderKey: 'EDGE1',
edgeContentWidth: 20,
edgeContentHeight: 20,
/** 设置连线样式 */
attrs: {
line: {
stroke: '#d8d8d8',
strokeWidth: 1,
targetMarker: {
name: 'classic',
},
},
},
}
return edgeConfig
})
const graphData = { nodes, edges }
resolve(graphData)
}, 100)
})
const res = await promise
return res
}
/** 添加节点 */
export const addNode: NsNodeCmd.AddNode.IArgs['createNodeService'] = async args => {
const { nodeConfig } = args
const promise: Promise<NsGraph.INodeConfig> = new Promise(resolve => {
setTimeout(() => {
resolve({
...nodeConfig,
})
}, 1000)
})
const res = await promise
return res
}
/** 删除节点 */
export const delNode: NsNodeCmd.DelNode.IArgs['deleteNodeService'] = async args => {
const { nodeConfig } = args
const promise: Promise<boolean> = new Promise(resolve => {
setTimeout(() => {
resolve(true)
}, 1000)
})
const res = await promise
return res
}
/** 添加边 */
export const addEdge: NsEdgeCmd.AddEdge.IArgs['createEdgeService'] = async args => {
const { edgeConfig } = args
const promise: Promise<NsGraph.IEdgeConfig> = new Promise(resolve => {
setTimeout(() => {
resolve({
...edgeConfig,
})
}, 1000)
})
const res = await promise
return res
}
/** 删除边 */
export const delEdge: NsEdgeCmd.DelEdge.IArgs['deleteEdgeService'] = async args => {
const { edgeConfig } = args
const promise: Promise<boolean> = new Promise(resolve => {
setTimeout(() => {
resolve(true)
}, 1000)
})
const res = await promise
return res
}
}

View File

@@ -6,10 +6,10 @@ import type { Dispatch } from 'umi';
import { connect, useModel } from 'umi'; import { connect, useModel } from 'umi';
import type { StateType } from '../model'; import type { StateType } from '../model';
import { SENSITIVE_LEVEL_ENUM } from '../constant'; import { SENSITIVE_LEVEL_ENUM } from '../constant';
import { getTagList, deleteTag, batchUpdateTagStatus } from '../service'; import { getTagList, deleteTag, batchUpdateTagStatus, getTagObjectList } from '../service';
import TagFilter from './components/TagFilter'; import TagFilter from './components/TagFilter';
import TagInfoCreateForm from './components/TagInfoCreateForm'; import TagInfoCreateForm from './components/TagInfoCreateForm';
import { SemanticNodeType, StatusEnum } from '../enum'; import { StatusEnum } from '../enum';
import moment from 'moment'; import moment from 'moment';
import styles from './style.less'; import styles from './style.less';
import { ISemantic } from '../data'; import { ISemantic } from '../data';
@@ -54,12 +54,27 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
const [hasAllPermission, setHasAllPermission] = useState<boolean>(true); const [hasAllPermission, setHasAllPermission] = useState<boolean>(true);
const [tagObjectList, setTagObjectList] = useState<ISemantic.ITagObjectItem[]>([]);
const actionRef = useRef<ActionType>(); const actionRef = useRef<ActionType>();
useEffect(() => { useEffect(() => {
queryTagList(filterParams); queryTagObjectList();
}, []); }, []);
const queryTagObjectList = async () => {
const { code, msg, data } = await getTagObjectList({});
if (code === 200) {
setTagObjectList(data);
const target = data[0];
if (target) {
queryTagList({ ...filterParams, tagObjectId: target.id });
}
return;
}
message.error(msg);
};
const queryBatchUpdateStatus = async (ids: React.Key[], status: StatusEnum) => { const queryBatchUpdateStatus = async (ids: React.Key[], status: StatusEnum) => {
if (Array.isArray(ids) && ids.length === 0) { if (Array.isArray(ids) && ids.length === 0) {
return; return;
@@ -167,17 +182,26 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
width: 300, width: 300,
render: columnsConfig.description.render, render: columnsConfig.description.render,
}, },
// {
// dataIndex: 'status',
// title: '状态',
// width: 180,
// search: false,
// render: columnsConfig.state.render,
// },
{ {
dataIndex: 'status', dataIndex: 'domainName',
title: '状态', title: '所属主题域',
width: 180, search: false,
},
{
dataIndex: 'tagObjectName',
title: '所属对象',
search: false, search: false,
render: columnsConfig.state.render,
}, },
{ {
dataIndex: 'createdBy', dataIndex: 'createdBy',
title: '创建人', title: '创建人',
// width: 150,
search: false, search: false,
}, },
{ {
@@ -234,7 +258,7 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
const handleFilterChange = async (filterParams: { const handleFilterChange = async (filterParams: {
key: string; key: string;
sensitiveLevel: string[]; sensitiveLevel: string;
showFilter: string[]; showFilter: string[];
type: string; type: string;
}) => { }) => {
@@ -296,6 +320,7 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
<> <>
<div className={styles.TagFilterWrapper}> <div className={styles.TagFilterWrapper}>
<TagFilter <TagFilter
tagObjectList={tagObjectList}
initFilterValues={filterParams} initFilterValues={filterParams}
onFiltersChange={(_, values) => { onFiltersChange={(_, values) => {
if (_.showType !== undefined) { if (_.showType !== undefined) {
@@ -321,22 +346,22 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
return false; return false;
}} }}
sticky={{ offsetHeader: 0 }} sticky={{ offsetHeader: 0 }}
rowSelection={{ // rowSelection={{
type: 'checkbox', // type: 'checkbox',
...rowSelection, // ...rowSelection,
}} // }}
toolBarRender={() => [ // toolBarRender={() => [
<BatchCtrlDropDownButton // <BatchCtrlDropDownButton
key="ctrlBtnList" // key="ctrlBtnList"
downloadLoading={downloadLoading} // downloadLoading={downloadLoading}
onDeleteConfirm={() => { // onDeleteConfirm={() => {
queryBatchUpdateStatus(selectedRowKeys, StatusEnum.DELETED); // queryBatchUpdateStatus(selectedRowKeys, StatusEnum.DELETED);
}} // }}
hiddenList={['batchDownload']} // hiddenList={['batchDownload', 'batchStart', 'batchStop']}
disabledList={hasAllPermission ? [] : ['batchStart', 'batchStop', 'batchDelete']} // disabledList={hasAllPermission ? [] : ['batchStart', 'batchDelete']}
onMenuClick={onMenuClick} // onMenuClick={onMenuClick}
/>, // />,
]} // ]}
loading={loading} loading={loading}
onChange={(data: any) => { onChange={(data: any) => {
const { current, pageSize, total } = data; const { current, pageSize, total } = data;

View File

@@ -1,20 +1,22 @@
import { Form, Input, Space, Row, Col, Switch } from 'antd'; import { Form, Input, Space, Row, Col, Switch, Select } from 'antd';
import StandardFormRow from '@/components/StandardFormRow'; import StandardFormRow from '@/components/StandardFormRow';
import TagSelect from '@/components/TagSelect'; import TagSelect from '@/components/TagSelect';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { SENSITIVE_LEVEL_OPTIONS } from '../../constant'; import { SENSITIVE_LEVEL_OPTIONS } from '../../constant';
import { SearchOutlined } from '@ant-design/icons'; import { SearchOutlined } from '@ant-design/icons';
import DomainTreeSelect from '../../components/DomainTreeSelect'; import DomainTreeSelect from '../../components/DomainTreeSelect';
import { ISemantic } from '../../data';
import styles from '../style.less'; import styles from '../style.less';
const FormItem = Form.Item; const FormItem = Form.Item;
type Props = { type Props = {
tagObjectList: ISemantic.ITagObjectItem[];
initFilterValues?: any; initFilterValues?: any;
onFiltersChange: (_: any, values: any) => void; onFiltersChange: (_: any, values: any) => void;
}; };
const TagFilter: React.FC<Props> = ({ initFilterValues = {}, onFiltersChange }) => { const TagFilter: React.FC<Props> = ({ tagObjectList, initFilterValues = {}, onFiltersChange }) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
useEffect(() => { useEffect(() => {
@@ -23,6 +25,14 @@ const TagFilter: React.FC<Props> = ({ initFilterValues = {}, onFiltersChange })
}); });
}, [form]); }, [form]);
useEffect(() => {
const target = tagObjectList?.[0];
if (!target) {
return;
}
form.setFieldValue('tagObjectId', target.id);
}, [tagObjectList]);
const handleValuesChange = (value: any, values: any) => { const handleValuesChange = (value: any, values: any) => {
localStorage.setItem('metricMarketShowType', !!values.showType ? '1' : '0'); localStorage.setItem('metricMarketShowType', !!values.showType ? '1' : '0');
onFiltersChange(value, values); onFiltersChange(value, values);
@@ -98,17 +108,20 @@ const TagFilter: React.FC<Props> = ({ initFilterValues = {}, onFiltersChange })
</div> </div>
</StandardFormRow> </StandardFormRow>
<Space size={40}> <Space size={40}>
{/* <StandardFormRow key="showType" title="切换为卡片" block> <StandardFormRow key="tagObjectId" title="所属对象" block>
<FormItem name="showType" valuePropName="checked"> <FormItem name="tagObjectId">
<Switch size="small" /> <Select
style={{ minWidth: 150 }}
placeholder="请选择所属对象"
options={tagObjectList.map((item: ISemantic.ITagObjectItem) => {
return {
label: item.name,
value: item.id,
};
})}
/>
</FormItem> </FormItem>
</StandardFormRow> */} </StandardFormRow>
{/* <StandardFormRow key="onlyShowMe" title="仅显示我的" block>
<FormItem name="onlyShowMe" valuePropName="checked">
<Switch size="small" />
</FormItem>
</StandardFormRow> */}
{filterList.map((item) => { {filterList.map((item) => {
const { title, key, options } = item; const { title, key, options } = item;
return ( return (
@@ -125,11 +138,11 @@ const TagFilter: React.FC<Props> = ({ initFilterValues = {}, onFiltersChange })
</StandardFormRow> </StandardFormRow>
); );
})} })}
<StandardFormRow key="domainIds" title="所属主题域" block> {/* <StandardFormRow key="domainIds" title="所属主题域" block>
<FormItem name="domainIds"> <FormItem name="domainIds">
<DomainTreeSelect /> <DomainTreeSelect />
</FormItem> </FormItem>
</StandardFormRow> </StandardFormRow> */}
</Space> </Space>
</Form> </Form>
); );

View File

@@ -21,8 +21,6 @@ import {
import { ISemantic } from '../../data'; import { ISemantic } from '../../data';
import IndicatorStar from '../../components/IndicatorStar'; import IndicatorStar from '../../components/IndicatorStar';
const { Text } = Typography;
type Props = { type Props = {
tagData: ISemantic.ITagItem; tagData: ISemantic.ITagItem;
domainManger: StateType; domainManger: StateType;
@@ -39,8 +37,8 @@ const TagInfoSider: React.FC<Props> = ({ tagData, dimensionMap, metricMap }) =>
if (!tagData) { if (!tagData) {
return <></>; return <></>;
} }
const { tagDefineType, tagDefineParams } = tagData; const { tagDefineType, tagDefineParams = {} } = tagData;
const { dependencies } = tagDefineParams; const { dependencies } = tagDefineParams as any;
if (!Array.isArray(dependencies)) { if (!Array.isArray(dependencies)) {
return <></>; return <></>;
} }

View File

@@ -0,0 +1,163 @@
import React, { useEffect, useRef } from 'react';
import { Form, Button, Modal, Steps, Input, Select, message } from 'antd';
import { formLayout } from '@/components/FormHelper/utils';
import styles from '../../components/style.less';
import { createTagObject, updateTagObject } from '../../service';
import { ISemantic } from '../../data';
export type CreateFormProps = {
datasourceId?: number;
domainId: number;
createModalVisible: boolean;
tagItem?: ISemantic.ITagItem;
onCancel?: () => void;
onSubmit?: (values: any) => void;
};
const FormItem = Form.Item;
const { TextArea } = Input;
const TagObjectCreateForm: React.FC<CreateFormProps> = ({
domainId,
onCancel,
createModalVisible,
tagItem,
onSubmit,
}) => {
const isEdit = !!tagItem?.id;
const formValRef = useRef({} as any);
const [form] = Form.useForm();
const updateFormVal = (val: any) => {
const formVal = {
...formValRef.current,
...val,
};
formValRef.current = formVal;
};
const handleNext = async () => {
const fieldsValue = await form.validateFields();
const submitForm = {
...formValRef.current,
...fieldsValue,
};
updateFormVal(submitForm);
await saveTag(submitForm);
};
const initData = () => {
if (!tagItem) {
return;
}
const initValue = {
...tagItem,
};
const editInitFormVal = {
...formValRef.current,
...initValue,
};
updateFormVal(editInitFormVal);
form.setFieldsValue(initValue);
};
useEffect(() => {
if (isEdit) {
initData();
}
}, [tagItem]);
const saveTag = async (fieldsValue: any) => {
const queryParams = {
domainId: isEdit ? tagItem.domainId : domainId,
...fieldsValue,
typeEnum: 'TAG_OBJECT',
};
let saveTagQuery = createTagObject;
if (queryParams.id) {
saveTagQuery = updateTagObject;
}
const { code, msg } = await saveTagQuery(queryParams);
if (code === 200) {
message.success('编辑标签成功');
onSubmit?.(queryParams);
return;
}
message.error(msg);
};
const renderContent = () => {
return (
<>
<FormItem hidden={true} name="id" label="ID">
<Input placeholder="id" />
</FormItem>
<FormItem
name="name"
label="标签对象名称"
rules={[{ required: true, message: '请输入标签对象名称' }]}
>
<Input placeholder="名称不可重复" />
</FormItem>
<FormItem
name="bizName"
label="英文名称"
rules={[{ required: true, message: '请输入英文名称' }]}
>
<Input placeholder="名称不可重复" disabled={isEdit} />
</FormItem>
<FormItem
name="description"
label={'描述'}
rules={[{ required: true, message: '请输入业务口径' }]}
>
<TextArea placeholder="请输入业务口径" />
</FormItem>
</>
);
};
const renderFooter = () => {
return (
<>
<Button onClick={onCancel}></Button>
<Button type="primary" onClick={handleNext}>
</Button>
</>
);
};
return (
<Modal
forceRender
width={800}
style={{ top: 48 }}
// styles={{ padding: '32px 40px 48px' }}
destroyOnClose
title={`${isEdit ? '编辑' : '新建'}标签对象`}
maskClosable={false}
open={createModalVisible}
footer={renderFooter()}
onCancel={onCancel}
>
<>
<Form
{...formLayout}
form={form}
initialValues={{
...formValRef.current,
}}
className={styles.form}
>
{renderContent()}
</Form>
</>
</Modal>
);
};
export default TagObjectCreateForm;

View File

@@ -0,0 +1,278 @@
import type { ActionType, ProColumns } from '@ant-design/pro-table';
import ProTable from '@ant-design/pro-table';
import { message, Button, Space, Popconfirm, Input, Select } from 'antd';
import React, { useRef, useState, useEffect } from 'react';
import type { Dispatch } from 'umi';
import { StatusEnum } from '../../enum';
import { connect } from 'umi';
import type { StateType } from '../../model';
import { SENSITIVE_LEVEL_ENUM, SENSITIVE_LEVEL_OPTIONS } from '../../constant';
import { getTagObjectList, deleteTagObject, batchUpdateTagStatus } from '../../service';
import TagObjectCreateForm from './TagObjectCreateForm';
// import BatchCtrlDropDownButton from '@/components/BatchCtrlDropDownButton';
import TableHeaderFilter from '../../components/TableHeaderFilter';
import moment from 'moment';
import styles from '../style.less';
import { ISemantic } from '../../data';
import { ColumnsConfig } from '../../components/TableColumnRender';
import TagValueSettingModal from './TagValueSettingModal';
type Props = {
dispatch: Dispatch;
domainManger: StateType;
};
const TagObjectTable: React.FC<Props> = ({ domainManger, dispatch }) => {
const { selectModelId: modelId, selectDomainId } = domainManger;
const [createModalVisible, setCreateModalVisible] = useState<boolean>(false);
const [tagItem, setTagItem] = useState<ISemantic.ITagItem>();
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [tableData, setTableData] = useState<ISemantic.ITagItem[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const defaultPagination = {
current: 1,
pageSize: 20,
total: 0,
};
const [pagination, setPagination] = useState(defaultPagination);
const [filterParams, setFilterParams] = useState<Record<string, any>>({});
const [tagValueSettingModalVisible, setTagValueSettingModalVisible] = useState<boolean>(false);
const actionRef = useRef<ActionType>();
const queryBatchUpdateStatus = async (ids: React.Key[], status: StatusEnum) => {
if (Array.isArray(ids) && ids.length === 0) {
return;
}
const { code, msg } = await batchUpdateTagStatus({
ids,
status,
});
if (code === 200) {
queryTagList({ ...filterParams, ...defaultPagination });
return;
}
message.error(msg);
};
useEffect(() => {
queryTagList({ ...filterParams, ...defaultPagination });
}, [filterParams]);
const queryTagList = async (params: any) => {
setLoading(true);
const { code, data, msg } = await getTagObjectList({
...params,
domainId: selectDomainId,
status: StatusEnum.ONLINE,
});
setLoading(false);
if (code === 200) {
setTableData(data);
} else {
message.error(msg);
setTableData([]);
}
};
const columnsConfig = ColumnsConfig();
const columns: ProColumns[] = [
{
dataIndex: 'id',
title: 'ID',
width: 80,
search: false,
},
{
dataIndex: 'name',
title: '标签对象',
// width: 280,
// width: '30%',
search: false,
},
{
dataIndex: 'description',
title: '描述',
search: false,
render: columnsConfig.description.render,
},
{
dataIndex: 'status',
title: '状态',
search: false,
render: columnsConfig.state.render,
},
{
dataIndex: 'createdBy',
title: '创建人',
search: false,
},
// {
// dataIndex: 'updatedAt',
// title: '更新时间',
// search: false,
// render: (value: any) => {
// return value && value !== '-' ? moment(value).format('YYYY-MM-DD HH:mm:ss') : '-';
// },
// },
{
title: '操作',
dataIndex: 'x',
valueType: 'option',
width: 150,
render: (_, record) => {
return (
<Space className={styles.ctrlBtnContainer}>
<Button
type="link"
key="metricEditBtn"
onClick={() => {
setTagItem(record);
setCreateModalVisible(true);
}}
>
</Button>
<Popconfirm
title="确认删除?"
okText="是"
cancelText="否"
onConfirm={async () => {
const { code, msg } = await deleteTagObject(record.id);
if (code === 200) {
setTagItem(undefined);
queryTagList({ ...filterParams, ...defaultPagination });
} else {
message.error(msg);
}
}}
>
<Button
type="link"
key="metricDeleteBtn"
onClick={() => {
setTagItem(record);
}}
>
</Button>
</Popconfirm>
</Space>
);
},
},
];
const rowSelection = {
onChange: (selectedRowKeys: React.Key[]) => {
setSelectedRowKeys(selectedRowKeys);
},
};
return (
<>
<ProTable
className={`${styles.classTable} ${styles.classTableSelectColumnAlignLeft} ${styles.disabledSearchTable} `}
actionRef={actionRef}
// headerTitle={
// <TableHeaderFilter
// components={[
// {
// label: '标签搜索',
// component: (
// <Input.Search
// style={{ width: 280 }}
// placeholder="请输入ID/标签名称/英文名称"
// onSearch={(value) => {
// setFilterParams((preState) => {
// return {
// ...preState,
// key: value,
// };
// });
// }}
// />
// ),
// },
// ]}
// />
// }
rowKey="id"
loading={loading}
search={false}
rowSelection={{
type: 'checkbox',
...rowSelection,
}}
columns={columns}
params={{ modelId }}
dataSource={tableData}
// pagination={pagination}
tableAlertRender={() => {
return false;
}}
onChange={(data: any) => {
const { current, pageSize, total } = data;
const currentPagin = {
current,
pageSize,
total,
};
setPagination(currentPagin);
queryTagList({ ...filterParams, ...currentPagin });
}}
sticky={{ offsetHeader: 0 }}
size="large"
options={{ reload: false, density: false, fullScreen: false }}
toolBarRender={() => [
<Button
key="create"
type="primary"
onClick={() => {
setTagItem(undefined);
setCreateModalVisible(true);
}}
>
</Button>,
]}
/>
{createModalVisible && (
<TagObjectCreateForm
domainId={selectDomainId}
createModalVisible={createModalVisible}
tagItem={tagItem}
onSubmit={() => {
setCreateModalVisible(false);
queryTagList({ ...filterParams, ...defaultPagination });
}}
onCancel={() => {
setCreateModalVisible(false);
}}
/>
)}
{tagValueSettingModalVisible && (
<TagValueSettingModal
open={tagValueSettingModalVisible}
tagItem={tagItem}
onCancel={() => {
setTagValueSettingModalVisible(false);
}}
onSubmit={() => {
setTagValueSettingModalVisible(false);
}}
/>
)}
</>
);
};
export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger,
}))(TagObjectTable);

View File

@@ -28,7 +28,7 @@ const TagTrendSection: React.FC<Props> = ({ tagData }) => {
const queryTagValueDistribution = async (params: any) => { const queryTagValueDistribution = async (params: any) => {
setTagTrendLoading(true); setTagTrendLoading(true);
const { data, code } = await getTagValueDistribution({ const { data, code } = await getTagValueDistribution({
itemId: params.id, id: params.id,
dateConf: { dateConf: {
unit: 5, unit: 5,
}, },

View File

@@ -8,7 +8,7 @@ import type { StateType } from './model';
import { LeftOutlined, RightOutlined } from '@ant-design/icons'; import { LeftOutlined, RightOutlined } from '@ant-design/icons';
import { ISemantic } from './data'; import { ISemantic } from './data';
import { getDomainList, getModelList } from './service'; import { getDomainList, getModelList } from './service';
import ChatSettingTab from './ChatSetting/ChatSettingTab'; // import ChatSettingTab from './ChatSetting/ChatSettingTab';
import DomainManagerTab from './components/DomainManagerTab'; import DomainManagerTab from './components/DomainManagerTab';
import type { Dispatch } from 'umi'; import type { Dispatch } from 'umi';

View File

@@ -6,7 +6,7 @@ import { ICommandContextProvider } from '@antv/xflow';
import { DATASOURCE_NODE_RENDER_ID } from '../constant'; import { DATASOURCE_NODE_RENDER_ID } from '../constant';
import { CustomCommands } from './constants'; import { CustomCommands } from './constants';
import 'antd/es/modal/style/index.css'; // import 'antd/es/modal/style/index.css';
export namespace NsConfirmModalCmd { export namespace NsConfirmModalCmd {
/** Command: 用于注册named factory */ /** Command: 用于注册named factory */

View File

@@ -8,7 +8,7 @@ import { ICommandContextProvider } from '@antv/xflow';
import { CustomCommands } from './constants'; import { CustomCommands } from './constants';
import 'antd/es/modal/style/index.css'; // import 'antd/es/modal/style/index.css';
// prettier-ignore // prettier-ignore
type ICommand = ICommandHandler<NsRenameNodeCmd.IArgs, NsRenameNodeCmd.IResult, NsRenameNodeCmd.ICmdHooks>; type ICommand = ICommandHandler<NsRenameNodeCmd.IArgs, NsRenameNodeCmd.IResult, NsRenameNodeCmd.ICmdHooks>;

View File

@@ -82,4 +82,6 @@ export interface TooltipToolOptions extends ToolsView.ToolItem.Options {
tooltip?: string; tooltip?: string;
} }
export const registerEdgeTool = () => {
Graph.registerEdgeTool('tooltip', TooltipTool, true); Graph.registerEdgeTool('tooltip', TooltipTool, true);
};

View File

@@ -7,7 +7,7 @@ import { connect } from 'umi';
import { DATASOURCE_NODE_RENDER_ID } from '../constant'; import { DATASOURCE_NODE_RENDER_ID } from '../constant';
import DataSourceRelationFormDrawer from './DataSourceRelationFormDrawer'; import DataSourceRelationFormDrawer from './DataSourceRelationFormDrawer';
import DataSourceCreateForm from '../../Datasource/components/DataSourceCreateForm'; import DataSourceCreateForm from '../../Datasource/components/DataSourceCreateForm';
import ClassDataSourceTypeModal from '../../components/ClassDataSourceTypeModal1'; // import ClassDataSourceTypeModal from '../../components/ClassDataSourceTypeModal1';
import { GraphApi } from '../service'; import { GraphApi } from '../service';
import { SemanticNodeType } from '../../enum'; import { SemanticNodeType } from '../../enum';
import type { StateType } from '../../model'; import type { StateType } from '../../model';
@@ -153,7 +153,7 @@ const XflowJsonSchemaFormDrawerForm: React.FC<CreateFormProps> = (props) => {
}} }}
/> />
</Drawer> </Drawer>
{ {/* {
<ClassDataSourceTypeModal <ClassDataSourceTypeModal
open={createDataSourceModalOpen} open={createDataSourceModalOpen}
onCancel={() => { onCancel={() => {
@@ -169,7 +169,7 @@ const XflowJsonSchemaFormDrawerForm: React.FC<CreateFormProps> = (props) => {
setCreateDataSourceModalOpen(false); setCreateDataSourceModalOpen(false);
}} }}
/> />
} } */}
</WorkspacePanel> </WorkspacePanel>
); );
}; };

View File

@@ -36,7 +36,7 @@ import { getGraphConfigFromList } from './utils';
import type { GraphConfig } from './data'; import type { GraphConfig } from './data';
import '@antv/xflow/dist/index.css'; import '@antv/xflow/dist/index.css';
import './ReactNodes/ToolTipsNode'; import { registerEdgeTool } from './ReactNodes/ToolTipsNode';
export interface IProps { export interface IProps {
domainManger: StateType; domainManger: StateType;
@@ -57,6 +57,8 @@ export const SemanticFlow: React.FC<IProps> = (props) => {
domainManger, domainManger,
}); });
registerEdgeTool();
const cache = const cache =
React.useMemo<{ app: IApplication } | null>( React.useMemo<{ app: IApplication } | null>(
() => ({ () => ({

View File

@@ -48,7 +48,7 @@ const ViewCreateFormModal: React.FC<ModelCreateFormModalProps> = ({
form.setFieldsValue({ form.setFieldsValue({
...viewItem, ...viewItem,
}); });
setQueryType(viewItem?.queryType); // setQueryType(viewItem?.queryType);
}, [viewItem]); }, [viewItem]);
const [dimensionList, setDimensionList] = useState<ISemantic.IDimensionItem[]>(); const [dimensionList, setDimensionList] = useState<ISemantic.IDimensionItem[]>();
@@ -59,7 +59,7 @@ const ViewCreateFormModal: React.FC<ModelCreateFormModalProps> = ({
if (selectedModelItem?.id) { if (selectedModelItem?.id) {
queryDimensionList(selectedModelItem.id); queryDimensionList(selectedModelItem.id);
queryMetricList(selectedModelItem.id); queryMetricList(selectedModelItem.id);
queryTagList(selectedModelItem.id); // queryTagList(selectedModelItem.id);
} }
}, [selectedModelItem]); }, [selectedModelItem]);
@@ -81,20 +81,20 @@ const ViewCreateFormModal: React.FC<ModelCreateFormModalProps> = ({
} }
}; };
const queryTagList = async (modelId: number) => { // const queryTagList = async (modelId: number) => {
const { code, data, msg } = await getTagList({ // const { code, data, msg } = await getTagList({
modelIds: [modelId], // modelIds: [modelId],
pageSize: 9999, // pageSize: 9999,
}); // });
const { list } = data || {}; // const { list } = data || {};
if (code === 200) { // if (code === 200) {
setTagList(list); // setTagList(list);
} else { // } else {
message.error(msg); // message.error(msg);
setTagList([]); // setTagList([]);
} // }
}; // };
const handleConfirm = async () => { const handleConfirm = async () => {
const fieldsValue = await form.validateFields(); const fieldsValue = await form.validateFields();
@@ -183,7 +183,7 @@ const ViewCreateFormModal: React.FC<ModelCreateFormModalProps> = ({
return ( return (
<> <>
<div style={{ display: currentStep === 1 ? 'block' : 'none' }}> <div style={{ display: currentStep === 1 ? 'block' : 'none' }}>
<div style={{ marginBottom: 10, paddingLeft: 12 }}> {/* <div style={{ marginBottom: 10, paddingLeft: 12 }}>
<Radio.Group <Radio.Group
buttonStyle="solid" buttonStyle="solid"
value={queryType} value={queryType}
@@ -194,7 +194,7 @@ const ViewCreateFormModal: React.FC<ModelCreateFormModalProps> = ({
<Radio.Button value="METRIC">指标模式</Radio.Button> <Radio.Button value="METRIC">指标模式</Radio.Button>
<Radio.Button value="TAG">标签模式</Radio.Button> <Radio.Button value="TAG">标签模式</Radio.Button>
</Radio.Group> </Radio.Group>
</div> </div> */}
<ViewModelConfigTransfer <ViewModelConfigTransfer
key={queryType} key={queryType}

View File

@@ -129,7 +129,7 @@ const ViewTable: React.FC<Props> = ({ disabledEdit = false, modelList, domainMan
> >
</a> </a>
<a {/* <a
key="searchEditBtn" key="searchEditBtn"
onClick={() => { onClick={() => {
setViewItem(record); setViewItem(record);
@@ -137,7 +137,7 @@ const ViewTable: React.FC<Props> = ({ disabledEdit = false, modelList, domainMan
}} }}
> >
查询设置 查询设置
</a> </a> */}
{record.status === StatusEnum.ONLINE ? ( {record.status === StatusEnum.ONLINE ? (
<Button <Button
type="link" type="link"

View File

@@ -120,11 +120,15 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
return; return;
} }
setLoading(true); setLoading(true);
const { code, msg } = await batchCreateTag({ const { code, msg } = await batchCreateTag(
itemIds: ids, ids.map((id) => {
type: TAG_DEFINE_TYPE.DIMENSION, return {
modelId, itemId: id,
}); tagDefineType: TAG_DEFINE_TYPE.DIMENSION,
};
}),
);
setLoading(false); setLoading(false);
if (code === 200) { if (code === 200) {
queryDimensionList({ ...filterParams, ...defaultPagination }); queryDimensionList({ ...filterParams, ...defaultPagination });

View File

@@ -73,11 +73,14 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
return; return;
} }
setLoading(true); setLoading(true);
const { code, msg } = await batchCreateTag({ const { code, msg } = await batchCreateTag(
itemIds: ids, ids.map((id) => {
type: TAG_DEFINE_TYPE.METRIC, return {
modelId, itemId: id,
}); tagDefineType: TAG_DEFINE_TYPE.METRIC,
};
}),
);
setLoading(false); setLoading(false);
if (code === 200) { if (code === 200) {
queryMetricList({ ...filterParams, ...defaultPagination }); queryMetricList({ ...filterParams, ...defaultPagination });

View File

@@ -12,7 +12,7 @@ import {
updateDimension, updateDimension,
mockDimensionAlias, mockDimensionAlias,
batchCreateTag, batchCreateTag,
batchUpdateTagStatus, batchDeleteTag,
} from '../service'; } from '../service';
import FormItemTitle from '@/components/FormHelper/FormItemTitle'; import FormItemTitle from '@/components/FormHelper/FormItemTitle';
@@ -77,11 +77,11 @@ const DimensionInfoModal: React.FC<CreateFormProps> = ({
} }
const { code, msg, data } = await saveDimensionQuery(queryParams); const { code, msg, data } = await saveDimensionQuery(queryParams);
if (code === 200) { if (code === 200) {
if (!queryParams.id && queryParams.isTag) { if (queryParams.isTag) {
queryBatchExportTag(data.id); queryBatchExportTag(data.id || dimensionItem?.id);
} }
if (dimensionItem?.id && !queryParams.isTag) { if (dimensionItem?.id && !queryParams.isTag) {
queryBatchUpdateStatus(dimensionItem.bizName, StatusEnum.DELETED); queryBatchDeleteTag(dimensionItem);
} }
if (!isSilenceSubmit) { if (!isSilenceSubmit) {
message.success('编辑维度成功'); message.success('编辑维度成功');
@@ -92,11 +92,10 @@ const DimensionInfoModal: React.FC<CreateFormProps> = ({
message.error(msg); message.error(msg);
}; };
const queryBatchUpdateStatus = async (bizName: string, status: StatusEnum) => { const queryBatchDeleteTag = async (dimensionItem: ISemantic.IDimensionItem) => {
const { code, msg } = await batchUpdateTagStatus({ const { code, msg } = await batchDeleteTag({
bizNames: [bizName], itemIds: [dimensionItem.id],
modelId: [modelId], tagDefineType: TAG_DEFINE_TYPE.DIMENSION,
status,
}); });
if (code === 200) { if (code === 200) {
return; return;
@@ -105,11 +104,9 @@ const DimensionInfoModal: React.FC<CreateFormProps> = ({
}; };
const queryBatchExportTag = async (id: number) => { const queryBatchExportTag = async (id: number) => {
const { code, msg } = await batchCreateTag({ const { code, msg } = await batchCreateTag([
itemIds: [id], { itemId: id, tagDefineType: TAG_DEFINE_TYPE.DIMENSION },
type: TAG_DEFINE_TYPE.DIMENSION, ]);
modelId,
});
if (code === 200) { if (code === 200) {
return; return;

View File

@@ -5,13 +5,17 @@ import { connect, history } from 'umi';
import ClassDimensionTable from './ClassDimensionTable'; import ClassDimensionTable from './ClassDimensionTable';
import ClassMetricTable from './ClassMetricTable'; import ClassMetricTable from './ClassMetricTable';
import PermissionSection from './Permission/PermissionSection'; import PermissionSection from './Permission/PermissionSection';
import ClassTagTable from '../Insights/components/ClassTagTable'; // import ClassTagTable from '../Insights/components/ClassTagTable';
import TagObjectTable from '../Insights/components/TagObjectTable';
import OverView from './OverView'; import OverView from './OverView';
import styles from './style.less'; import styles from './style.less';
import type { StateType } from '../model'; import type { StateType } from '../model';
import { HomeOutlined, FundViewOutlined } from '@ant-design/icons'; import { HomeOutlined, FundViewOutlined } from '@ant-design/icons';
import { ISemantic } from '../data'; import { ISemantic } from '../data';
import SemanticGraphCanvas from '../SemanticGraphCanvas'; import SemanticGraphCanvas from '../SemanticGraphCanvas';
import HeadlessFlows from '../HeadlessFlows';
import SemanticFlows from '../SemanticFlows';
import RecommendedQuestionsSection from '../components/Entity/RecommendedQuestionsSection'; import RecommendedQuestionsSection from '../components/Entity/RecommendedQuestionsSection';
import View from '../View'; import View from '../View';
// import DatabaseTable from '../components/Database/DatabaseTable'; // import DatabaseTable from '../components/Database/DatabaseTable';
@@ -66,12 +70,18 @@ const DomainManagerTab: React.FC<Props> = ({
/> />
), ),
}, },
{
label: '标签对象管理',
key: 'tagObjectManange',
children: <TagObjectTable />,
},
{ {
label: '画布', label: '画布',
key: 'xflow', key: 'xflow',
children: ( children: (
<div style={{ width: '100%' }}> <div style={{ width: '100%' }}>
<SemanticGraphCanvas /> <SemanticGraphCanvas />
{/* <HeadlessFlows /> */}
</div> </div>
), ),
}, },
@@ -104,11 +114,11 @@ const DomainManagerTab: React.FC<Props> = ({
key: 'dimenstion', key: 'dimenstion',
children: <ClassDimensionTable />, children: <ClassDimensionTable />,
}, },
{ // {
label: '标签管理', // label: '标签管理',
key: 'tag', // key: 'tag',
children: <ClassTagTable />, // children: <ClassTagTable />,
}, // },
{ {
label: '权限管理', label: '权限管理',

View File

@@ -54,7 +54,17 @@ const DimensionMetricVisibleTableTransfer: React.FC<Props> = ({
return <TransTypeTag type={type} />; return <TransTypeTag type={type} />;
}, },
}, },
{
dataIndex: 'isTag',
title: '是否标签',
// hidden: true,
render: (isTag) => {
if (isTag) {
return <span style={{ color: '#0958d9' }}></span>;
}
return '否';
},
},
{ {
dataIndex: 'modelName', dataIndex: 'modelName',
title: '所属模型', title: '所属模型',

View File

@@ -32,13 +32,14 @@ const DimensionMetricVisibleTransfer: React.FC<Props> = ({
useEffect(() => { useEffect(() => {
setTransferData( setTransferData(
sourceList.map(({ key, id, name, bizName, transType, modelName }) => { sourceList.map(({ key, id, name, bizName, transType, modelName, isTag }) => {
return { return {
key, key,
name, name,
bizName, bizName,
id, id,
modelName, modelName,
isTag,
type: transType, type: transType,
}; };
}), }),

View File

@@ -29,7 +29,7 @@ import {
getModelDetail, getModelDetail,
getDrillDownDimension, getDrillDownDimension,
batchCreateTag, batchCreateTag,
batchUpdateTagStatus, batchDeleteTag,
} from '../service'; } from '../service';
import MetricMetricFormTable from './MetricMetricFormTable'; import MetricMetricFormTable from './MetricMetricFormTable';
import MetricFieldFormTable from './MetricFieldFormTable'; import MetricFieldFormTable from './MetricFieldFormTable';
@@ -381,11 +381,12 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
} }
const { code, msg, data } = await saveMetricQuery(queryParams); const { code, msg, data } = await saveMetricQuery(queryParams);
if (code === 200) { if (code === 200) {
if (!queryParams.id && queryParams.isTag) { if (queryParams.isTag) {
queryBatchExportTag(data.id); queryBatchExportTag(data.id || metricItem?.id);
} }
if (metricItem?.id && !queryParams.isTag) { if (metricItem?.id && !queryParams.isTag) {
queryBatchUpdateStatus(metricItem.bizName, StatusEnum.DELETED); queryBatchDelete(metricItem);
} }
message.success('编辑指标成功'); message.success('编辑指标成功');
onSubmit?.(queryParams); onSubmit?.(queryParams);
@@ -394,11 +395,10 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
message.error(msg); message.error(msg);
}; };
const queryBatchUpdateStatus = async (bizName: string, status: StatusEnum) => { const queryBatchDelete = async (metricItem: ISemantic.IMetricItem) => {
const { code, msg } = await batchUpdateTagStatus({ const { code, msg } = await batchDeleteTag({
bizNames: [bizName], itemIds: [metricItem.id],
modelId: [modelId], tagDefineType: TAG_DEFINE_TYPE.METRIC,
status,
}); });
if (code === 200) { if (code === 200) {
return; return;
@@ -407,11 +407,9 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
}; };
const queryBatchExportTag = async (id: number) => { const queryBatchExportTag = async (id: number) => {
const { code, msg } = await batchCreateTag({ const { code, msg } = await batchCreateTag([
itemIds: [id], { itemId: id, tagDefineType: TAG_DEFINE_TYPE.METRIC },
type: TAG_DEFINE_TYPE.METRIC, ]);
modelId,
});
if (code === 200) { if (code === 200) {
return; return;

View File

@@ -145,6 +145,7 @@ export declare namespace ISemantic {
viewOrgs?: any[]; viewOrgs?: any[];
admins?: string[]; admins?: string[];
adminOrgs?: any[]; adminOrgs?: any[];
tagObjectId?: number;
drillDownDimensions: IDrillDownDimensionItem[]; drillDownDimensions: IDrillDownDimensionItem[];
createdBy: UserName; createdBy: UserName;
updatedBy: UserName; updatedBy: UserName;
@@ -411,6 +412,22 @@ export declare namespace ISemantic {
tagDefineParams: ITagDefineParams; tagDefineParams: ITagDefineParams;
expr: string; expr: string;
} }
interface ITagObjectItem {
createdBy: string;
updatedBy: string;
createdAt: string;
updatedAt: string;
id: number;
name: string;
bizName: string;
description: string;
status: number;
typeEnum: null;
sensitiveLevel: SENSITIVE_LEVEL;
domainId: number;
ext: null;
}
} }
export declare namespace IChatConfig { export declare namespace IChatConfig {

View File

@@ -613,7 +613,7 @@ export function deleteView(viewId: number): Promise<any> {
} }
export function getTagList(data: any): Promise<any> { export function getTagList(data: any): Promise<any> {
return request(`${process.env.API_BASE_URL}tag/queryTag`, { return request(`${process.env.API_BASE_URL}tag/queryTag/market`, {
method: 'POST', method: 'POST',
data: { pageSize: 9999, ...data }, data: { pageSize: 9999, ...data },
}); });
@@ -664,3 +664,37 @@ export function batchCreateTag(data: any): Promise<any> {
data, data,
}); });
} }
export function batchDeleteTag(data: any): Promise<any> {
return request(`${process.env.API_BASE_URL}tag/delete/batch`, {
method: 'POST',
data,
});
}
export function createTagObject(data: any): Promise<any> {
return request(`${process.env.API_BASE_URL}tagObject/create`, {
method: 'POST',
data,
});
}
export function updateTagObject(data: any): Promise<any> {
return request(`${process.env.API_BASE_URL}tagObject/update`, {
method: 'POST',
data,
});
}
export function deleteTagObject(id: number): Promise<any> {
return request(`${process.env.API_BASE_URL}tagObject/delete/${id}`, {
method: 'DELETE',
});
}
export function getTagObjectList(data: any): Promise<any> {
return request(`${process.env.API_BASE_URL}tagObject/query`, {
method: 'POST',
data: { pageSize: 9999, status: 1, ...data },
});
}