mirror of
https://github.com/tencentmusic/supersonic.git
synced 2025-12-12 04:27:39 +00:00
[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:
@@ -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",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
.create-entity-container {}
|
||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.create-relation-container {}
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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('删除连线成功!')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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]: '其他表',
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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
|
||||||
@@ -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',
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 <></>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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);
|
||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>(
|
||||||
() => ({
|
() => ({
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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: '权限管理',
|
||||||
|
|||||||
@@ -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: '所属模型',
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user