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-core": "^0.8.23",
|
||||
"@antv/layout": "^0.3.20",
|
||||
"@antv/x6": "1.30.1",
|
||||
"@antv/xflow": "^1.0.55",
|
||||
"@antv/xflow-extension": "1.0.55",
|
||||
"@babel/runtime": "^7.22.5",
|
||||
"@types/numeral": "^2.0.2",
|
||||
"@types/react-draft-wysiwyg": "^1.13.2",
|
||||
|
||||
@@ -5,7 +5,13 @@ import DataSourceFieldForm from './DataSourceFieldForm';
|
||||
import { formLayout } from '@/components/FormHelper/utils';
|
||||
import { EnumDataSourceType } from '../constants';
|
||||
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 { StateType } from '../../model';
|
||||
import { connect } from 'umi';
|
||||
@@ -63,7 +69,8 @@ const DataSourceCreateForm: React.FC<CreateFormProps> = ({
|
||||
const [effectTipsData, setEffectTipsData] = useState<
|
||||
(ISemantic.IDimensionItem | ISemantic.IMetricItem)[]
|
||||
>([]);
|
||||
|
||||
const [tagObjectList, setTagObjectList] = useState<ISemantic.ITagObjectItem[]>([]);
|
||||
const [tagObjectIdState, setTagObjectIdState] = useState(modelItem?.tagObjectId);
|
||||
const formValRef = useRef(initFormVal as any);
|
||||
const [form] = Form.useForm();
|
||||
const { databaseConfigList, selectModelId: modelId, selectDomainId } = domainManger;
|
||||
@@ -91,9 +98,23 @@ const DataSourceCreateForm: React.FC<CreateFormProps> = ({
|
||||
}
|
||||
}, [scriptColumns]);
|
||||
|
||||
useEffect(() => {
|
||||
queryTagObjectList();
|
||||
}, []);
|
||||
|
||||
const forward = () => 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[] = []) => {
|
||||
if (!modelItem) {
|
||||
return false;
|
||||
@@ -151,7 +172,7 @@ const DataSourceCreateForm: React.FC<CreateFormProps> = ({
|
||||
(fieldsClassify, item: any) => {
|
||||
const {
|
||||
type,
|
||||
bizName,
|
||||
// bizName,
|
||||
fieldName,
|
||||
timeGranularity,
|
||||
agg,
|
||||
@@ -159,7 +180,7 @@ const DataSourceCreateForm: React.FC<CreateFormProps> = ({
|
||||
name,
|
||||
isCreateMetric: createMetric,
|
||||
dateFormat,
|
||||
entityNames,
|
||||
// entityNames,
|
||||
isTag,
|
||||
} = item;
|
||||
const isCreateDimension = createDimension ? 1 : 0;
|
||||
@@ -194,7 +215,8 @@ const DataSourceCreateForm: React.FC<CreateFormProps> = ({
|
||||
isCreateDimension,
|
||||
name,
|
||||
type,
|
||||
entityNames,
|
||||
// entityNames,
|
||||
tagObjectId: modelItem?.tagObjectId,
|
||||
});
|
||||
break;
|
||||
case EnumDataSourceType.MEASURES:
|
||||
@@ -253,6 +275,7 @@ const DataSourceCreateForm: React.FC<CreateFormProps> = ({
|
||||
sqlQuery: sql,
|
||||
sqlVariables: sqlParams,
|
||||
},
|
||||
tagObjectId: tagObjectIdState,
|
||||
};
|
||||
setQueryParamsState(queryParams);
|
||||
const checkState = await checkAvailableItem(fieldColumns.map((item) => item.nameEn));
|
||||
@@ -370,6 +393,7 @@ const DataSourceCreateForm: React.FC<CreateFormProps> = ({
|
||||
initFields([], fieldColumns);
|
||||
}
|
||||
}
|
||||
setTagObjectIdState(modelItem?.tagObjectId);
|
||||
}, [modelItem]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -423,6 +447,11 @@ const DataSourceCreateForm: React.FC<CreateFormProps> = ({
|
||||
<div style={{ display: currentStep === 1 ? 'block' : 'none' }}>
|
||||
<DataSourceFieldForm
|
||||
fields={fields}
|
||||
tagObjectList={tagObjectList}
|
||||
tagObjectId={tagObjectIdState}
|
||||
onTagObjectChange={(tagObjectId) => {
|
||||
setTagObjectIdState(tagObjectId);
|
||||
}}
|
||||
onFieldChange={handleFieldChange}
|
||||
onSqlChange={(sql) => {
|
||||
setSqlFilter(sql);
|
||||
|
||||
@@ -4,6 +4,7 @@ import TableTitleTooltips from '../../components/TableTitleTooltips';
|
||||
import { isUndefined } from 'lodash';
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons';
|
||||
import SqlEditor from '@/components/SqlEditor';
|
||||
import { ISemantic } from '../../data';
|
||||
import {
|
||||
TYPE_OPTIONS,
|
||||
DATE_FORMATTER,
|
||||
@@ -23,7 +24,8 @@ type FieldItem = {
|
||||
checked?: number;
|
||||
dateFormat?: string;
|
||||
timeGranularity?: string;
|
||||
entityNames?: string[];
|
||||
// entityNames?: string[];
|
||||
// tagObjectId?: number;
|
||||
isTag?: number;
|
||||
};
|
||||
const { Search } = Input;
|
||||
@@ -32,8 +34,11 @@ const FormItem = Form.Item;
|
||||
type Props = {
|
||||
onSqlChange: (sql: string) => void;
|
||||
sql: string;
|
||||
tagObjectList: ISemantic.ITagObjectItem[];
|
||||
tagObjectId?: number;
|
||||
fields: FieldItem[];
|
||||
onFieldChange: (fieldName: string, data: Partial<FieldItem>) => void;
|
||||
onTagObjectChange?: (tagObjectId: number) => void;
|
||||
};
|
||||
|
||||
const { Option } = Select;
|
||||
@@ -47,7 +52,15 @@ const getCreateFieldName = (type: EnumDataSourceType) => {
|
||||
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) => {
|
||||
onFieldChange(record.bizName, {
|
||||
...record,
|
||||
@@ -123,12 +136,28 @@ const DataSourceFieldForm: React.FC<Props> = ({ fields, sql, onFieldChange, onSq
|
||||
width: 185,
|
||||
render: (_: any, record: FieldItem) => {
|
||||
const { type } = record;
|
||||
console.log(record, 3333);
|
||||
if (type === EnumDataSourceType.PRIMARY) {
|
||||
const entityNames =
|
||||
fields.find((field) => field.bizName === record.bizName)?.entityNames || [];
|
||||
return (
|
||||
<Space>
|
||||
{/* <FormItem name="tagObjectId"> */}
|
||||
<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 }}
|
||||
mode="tags"
|
||||
value={entityNames}
|
||||
@@ -141,7 +170,7 @@ const DataSourceFieldForm: React.FC<Props> = ({ fields, sql, onFieldChange, onSq
|
||||
/>
|
||||
<Tooltip title="主键可以作为一个实体,在此设置一个或多个实体名称">
|
||||
<ExclamationCircleOutlined />
|
||||
</Tooltip>
|
||||
</Tooltip> */}
|
||||
</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 type { StateType } from '../model';
|
||||
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 TagInfoCreateForm from './components/TagInfoCreateForm';
|
||||
import { SemanticNodeType, StatusEnum } from '../enum';
|
||||
import { StatusEnum } from '../enum';
|
||||
import moment from 'moment';
|
||||
import styles from './style.less';
|
||||
import { ISemantic } from '../data';
|
||||
@@ -54,12 +54,27 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
||||
|
||||
const [hasAllPermission, setHasAllPermission] = useState<boolean>(true);
|
||||
|
||||
const [tagObjectList, setTagObjectList] = useState<ISemantic.ITagObjectItem[]>([]);
|
||||
|
||||
const actionRef = useRef<ActionType>();
|
||||
|
||||
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) => {
|
||||
if (Array.isArray(ids) && ids.length === 0) {
|
||||
return;
|
||||
@@ -167,17 +182,26 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
||||
width: 300,
|
||||
render: columnsConfig.description.render,
|
||||
},
|
||||
// {
|
||||
// dataIndex: 'status',
|
||||
// title: '状态',
|
||||
// width: 180,
|
||||
// search: false,
|
||||
// render: columnsConfig.state.render,
|
||||
// },
|
||||
{
|
||||
dataIndex: 'status',
|
||||
title: '状态',
|
||||
width: 180,
|
||||
dataIndex: 'domainName',
|
||||
title: '所属主题域',
|
||||
search: false,
|
||||
},
|
||||
{
|
||||
dataIndex: 'tagObjectName',
|
||||
title: '所属对象',
|
||||
search: false,
|
||||
render: columnsConfig.state.render,
|
||||
},
|
||||
{
|
||||
dataIndex: 'createdBy',
|
||||
title: '创建人',
|
||||
// width: 150,
|
||||
search: false,
|
||||
},
|
||||
{
|
||||
@@ -234,7 +258,7 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
||||
|
||||
const handleFilterChange = async (filterParams: {
|
||||
key: string;
|
||||
sensitiveLevel: string[];
|
||||
sensitiveLevel: string;
|
||||
showFilter: string[];
|
||||
type: string;
|
||||
}) => {
|
||||
@@ -296,6 +320,7 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
||||
<>
|
||||
<div className={styles.TagFilterWrapper}>
|
||||
<TagFilter
|
||||
tagObjectList={tagObjectList}
|
||||
initFilterValues={filterParams}
|
||||
onFiltersChange={(_, values) => {
|
||||
if (_.showType !== undefined) {
|
||||
@@ -321,22 +346,22 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
||||
return false;
|
||||
}}
|
||||
sticky={{ offsetHeader: 0 }}
|
||||
rowSelection={{
|
||||
type: 'checkbox',
|
||||
...rowSelection,
|
||||
}}
|
||||
toolBarRender={() => [
|
||||
<BatchCtrlDropDownButton
|
||||
key="ctrlBtnList"
|
||||
downloadLoading={downloadLoading}
|
||||
onDeleteConfirm={() => {
|
||||
queryBatchUpdateStatus(selectedRowKeys, StatusEnum.DELETED);
|
||||
}}
|
||||
hiddenList={['batchDownload']}
|
||||
disabledList={hasAllPermission ? [] : ['batchStart', 'batchStop', 'batchDelete']}
|
||||
onMenuClick={onMenuClick}
|
||||
/>,
|
||||
]}
|
||||
// rowSelection={{
|
||||
// type: 'checkbox',
|
||||
// ...rowSelection,
|
||||
// }}
|
||||
// toolBarRender={() => [
|
||||
// <BatchCtrlDropDownButton
|
||||
// key="ctrlBtnList"
|
||||
// downloadLoading={downloadLoading}
|
||||
// onDeleteConfirm={() => {
|
||||
// queryBatchUpdateStatus(selectedRowKeys, StatusEnum.DELETED);
|
||||
// }}
|
||||
// hiddenList={['batchDownload', 'batchStart', 'batchStop']}
|
||||
// disabledList={hasAllPermission ? [] : ['batchStart', 'batchDelete']}
|
||||
// onMenuClick={onMenuClick}
|
||||
// />,
|
||||
// ]}
|
||||
loading={loading}
|
||||
onChange={(data: any) => {
|
||||
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 TagSelect from '@/components/TagSelect';
|
||||
import React, { useEffect } from 'react';
|
||||
import { SENSITIVE_LEVEL_OPTIONS } from '../../constant';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import DomainTreeSelect from '../../components/DomainTreeSelect';
|
||||
import { ISemantic } from '../../data';
|
||||
import styles from '../style.less';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
type Props = {
|
||||
tagObjectList: ISemantic.ITagObjectItem[];
|
||||
initFilterValues?: any;
|
||||
onFiltersChange: (_: any, values: any) => void;
|
||||
};
|
||||
|
||||
const TagFilter: React.FC<Props> = ({ initFilterValues = {}, onFiltersChange }) => {
|
||||
const TagFilter: React.FC<Props> = ({ tagObjectList, initFilterValues = {}, onFiltersChange }) => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -23,6 +25,14 @@ const TagFilter: React.FC<Props> = ({ initFilterValues = {}, onFiltersChange })
|
||||
});
|
||||
}, [form]);
|
||||
|
||||
useEffect(() => {
|
||||
const target = tagObjectList?.[0];
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
form.setFieldValue('tagObjectId', target.id);
|
||||
}, [tagObjectList]);
|
||||
|
||||
const handleValuesChange = (value: any, values: any) => {
|
||||
localStorage.setItem('metricMarketShowType', !!values.showType ? '1' : '0');
|
||||
onFiltersChange(value, values);
|
||||
@@ -98,17 +108,20 @@ const TagFilter: React.FC<Props> = ({ initFilterValues = {}, onFiltersChange })
|
||||
</div>
|
||||
</StandardFormRow>
|
||||
<Space size={40}>
|
||||
{/* <StandardFormRow key="showType" title="切换为卡片" block>
|
||||
<FormItem name="showType" valuePropName="checked">
|
||||
<Switch size="small" />
|
||||
<StandardFormRow key="tagObjectId" title="所属对象" block>
|
||||
<FormItem name="tagObjectId">
|
||||
<Select
|
||||
style={{ minWidth: 150 }}
|
||||
placeholder="请选择所属对象"
|
||||
options={tagObjectList.map((item: ISemantic.ITagObjectItem) => {
|
||||
return {
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
};
|
||||
})}
|
||||
/>
|
||||
</FormItem>
|
||||
</StandardFormRow> */}
|
||||
{/* <StandardFormRow key="onlyShowMe" title="仅显示我的" block>
|
||||
<FormItem name="onlyShowMe" valuePropName="checked">
|
||||
<Switch size="small" />
|
||||
</FormItem>
|
||||
</StandardFormRow> */}
|
||||
|
||||
</StandardFormRow>
|
||||
{filterList.map((item) => {
|
||||
const { title, key, options } = item;
|
||||
return (
|
||||
@@ -125,11 +138,11 @@ const TagFilter: React.FC<Props> = ({ initFilterValues = {}, onFiltersChange })
|
||||
</StandardFormRow>
|
||||
);
|
||||
})}
|
||||
<StandardFormRow key="domainIds" title="所属主题域" block>
|
||||
{/* <StandardFormRow key="domainIds" title="所属主题域" block>
|
||||
<FormItem name="domainIds">
|
||||
<DomainTreeSelect />
|
||||
</FormItem>
|
||||
</StandardFormRow>
|
||||
</StandardFormRow> */}
|
||||
</Space>
|
||||
</Form>
|
||||
);
|
||||
|
||||
@@ -21,8 +21,6 @@ import {
|
||||
import { ISemantic } from '../../data';
|
||||
import IndicatorStar from '../../components/IndicatorStar';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
type Props = {
|
||||
tagData: ISemantic.ITagItem;
|
||||
domainManger: StateType;
|
||||
@@ -39,8 +37,8 @@ const TagInfoSider: React.FC<Props> = ({ tagData, dimensionMap, metricMap }) =>
|
||||
if (!tagData) {
|
||||
return <></>;
|
||||
}
|
||||
const { tagDefineType, tagDefineParams } = tagData;
|
||||
const { dependencies } = tagDefineParams;
|
||||
const { tagDefineType, tagDefineParams = {} } = tagData;
|
||||
const { dependencies } = tagDefineParams as any;
|
||||
if (!Array.isArray(dependencies)) {
|
||||
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) => {
|
||||
setTagTrendLoading(true);
|
||||
const { data, code } = await getTagValueDistribution({
|
||||
itemId: params.id,
|
||||
id: params.id,
|
||||
dateConf: {
|
||||
unit: 5,
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { StateType } from './model';
|
||||
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
|
||||
import { ISemantic } from './data';
|
||||
import { getDomainList, getModelList } from './service';
|
||||
import ChatSettingTab from './ChatSetting/ChatSettingTab';
|
||||
// import ChatSettingTab from './ChatSetting/ChatSettingTab';
|
||||
import DomainManagerTab from './components/DomainManagerTab';
|
||||
import type { Dispatch } from 'umi';
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ICommandContextProvider } from '@antv/xflow';
|
||||
import { DATASOURCE_NODE_RENDER_ID } from '../constant';
|
||||
import { CustomCommands } from './constants';
|
||||
|
||||
import 'antd/es/modal/style/index.css';
|
||||
// import 'antd/es/modal/style/index.css';
|
||||
|
||||
export namespace NsConfirmModalCmd {
|
||||
/** Command: 用于注册named factory */
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ICommandContextProvider } from '@antv/xflow';
|
||||
|
||||
import { CustomCommands } from './constants';
|
||||
|
||||
import 'antd/es/modal/style/index.css';
|
||||
// import 'antd/es/modal/style/index.css';
|
||||
|
||||
// prettier-ignore
|
||||
type ICommand = ICommandHandler<NsRenameNodeCmd.IArgs, NsRenameNodeCmd.IResult, NsRenameNodeCmd.ICmdHooks>;
|
||||
|
||||
@@ -82,4 +82,6 @@ export interface TooltipToolOptions extends ToolsView.ToolItem.Options {
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
Graph.registerEdgeTool('tooltip', TooltipTool, true);
|
||||
export const registerEdgeTool = () => {
|
||||
Graph.registerEdgeTool('tooltip', TooltipTool, true);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ import { connect } from 'umi';
|
||||
import { DATASOURCE_NODE_RENDER_ID } from '../constant';
|
||||
import DataSourceRelationFormDrawer from './DataSourceRelationFormDrawer';
|
||||
import DataSourceCreateForm from '../../Datasource/components/DataSourceCreateForm';
|
||||
import ClassDataSourceTypeModal from '../../components/ClassDataSourceTypeModal1';
|
||||
// import ClassDataSourceTypeModal from '../../components/ClassDataSourceTypeModal1';
|
||||
import { GraphApi } from '../service';
|
||||
import { SemanticNodeType } from '../../enum';
|
||||
import type { StateType } from '../../model';
|
||||
@@ -153,7 +153,7 @@ const XflowJsonSchemaFormDrawerForm: React.FC<CreateFormProps> = (props) => {
|
||||
}}
|
||||
/>
|
||||
</Drawer>
|
||||
{
|
||||
{/* {
|
||||
<ClassDataSourceTypeModal
|
||||
open={createDataSourceModalOpen}
|
||||
onCancel={() => {
|
||||
@@ -169,7 +169,7 @@ const XflowJsonSchemaFormDrawerForm: React.FC<CreateFormProps> = (props) => {
|
||||
setCreateDataSourceModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
} */}
|
||||
</WorkspacePanel>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -36,7 +36,7 @@ import { getGraphConfigFromList } from './utils';
|
||||
import type { GraphConfig } from './data';
|
||||
import '@antv/xflow/dist/index.css';
|
||||
|
||||
import './ReactNodes/ToolTipsNode';
|
||||
import { registerEdgeTool } from './ReactNodes/ToolTipsNode';
|
||||
|
||||
export interface IProps {
|
||||
domainManger: StateType;
|
||||
@@ -57,6 +57,8 @@ export const SemanticFlow: React.FC<IProps> = (props) => {
|
||||
domainManger,
|
||||
});
|
||||
|
||||
registerEdgeTool();
|
||||
|
||||
const cache =
|
||||
React.useMemo<{ app: IApplication } | null>(
|
||||
() => ({
|
||||
|
||||
@@ -48,7 +48,7 @@ const ViewCreateFormModal: React.FC<ModelCreateFormModalProps> = ({
|
||||
form.setFieldsValue({
|
||||
...viewItem,
|
||||
});
|
||||
setQueryType(viewItem?.queryType);
|
||||
// setQueryType(viewItem?.queryType);
|
||||
}, [viewItem]);
|
||||
|
||||
const [dimensionList, setDimensionList] = useState<ISemantic.IDimensionItem[]>();
|
||||
@@ -59,7 +59,7 @@ const ViewCreateFormModal: React.FC<ModelCreateFormModalProps> = ({
|
||||
if (selectedModelItem?.id) {
|
||||
queryDimensionList(selectedModelItem.id);
|
||||
queryMetricList(selectedModelItem.id);
|
||||
queryTagList(selectedModelItem.id);
|
||||
// queryTagList(selectedModelItem.id);
|
||||
}
|
||||
}, [selectedModelItem]);
|
||||
|
||||
@@ -81,20 +81,20 @@ const ViewCreateFormModal: React.FC<ModelCreateFormModalProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const queryTagList = async (modelId: number) => {
|
||||
const { code, data, msg } = await getTagList({
|
||||
modelIds: [modelId],
|
||||
pageSize: 9999,
|
||||
});
|
||||
// const queryTagList = async (modelId: number) => {
|
||||
// const { code, data, msg } = await getTagList({
|
||||
// modelIds: [modelId],
|
||||
// pageSize: 9999,
|
||||
// });
|
||||
|
||||
const { list } = data || {};
|
||||
if (code === 200) {
|
||||
setTagList(list);
|
||||
} else {
|
||||
message.error(msg);
|
||||
setTagList([]);
|
||||
}
|
||||
};
|
||||
// const { list } = data || {};
|
||||
// if (code === 200) {
|
||||
// setTagList(list);
|
||||
// } else {
|
||||
// message.error(msg);
|
||||
// setTagList([]);
|
||||
// }
|
||||
// };
|
||||
|
||||
const handleConfirm = async () => {
|
||||
const fieldsValue = await form.validateFields();
|
||||
@@ -183,7 +183,7 @@ const ViewCreateFormModal: React.FC<ModelCreateFormModalProps> = ({
|
||||
return (
|
||||
<>
|
||||
<div style={{ display: currentStep === 1 ? 'block' : 'none' }}>
|
||||
<div style={{ marginBottom: 10, paddingLeft: 12 }}>
|
||||
{/* <div style={{ marginBottom: 10, paddingLeft: 12 }}>
|
||||
<Radio.Group
|
||||
buttonStyle="solid"
|
||||
value={queryType}
|
||||
@@ -194,7 +194,7 @@ const ViewCreateFormModal: React.FC<ModelCreateFormModalProps> = ({
|
||||
<Radio.Button value="METRIC">指标模式</Radio.Button>
|
||||
<Radio.Button value="TAG">标签模式</Radio.Button>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<ViewModelConfigTransfer
|
||||
key={queryType}
|
||||
|
||||
@@ -129,7 +129,7 @@ const ViewTable: React.FC<Props> = ({ disabledEdit = false, modelList, domainMan
|
||||
>
|
||||
编辑
|
||||
</a>
|
||||
<a
|
||||
{/* <a
|
||||
key="searchEditBtn"
|
||||
onClick={() => {
|
||||
setViewItem(record);
|
||||
@@ -137,7 +137,7 @@ const ViewTable: React.FC<Props> = ({ disabledEdit = false, modelList, domainMan
|
||||
}}
|
||||
>
|
||||
查询设置
|
||||
</a>
|
||||
</a> */}
|
||||
{record.status === StatusEnum.ONLINE ? (
|
||||
<Button
|
||||
type="link"
|
||||
|
||||
@@ -120,11 +120,15 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
const { code, msg } = await batchCreateTag({
|
||||
itemIds: ids,
|
||||
type: TAG_DEFINE_TYPE.DIMENSION,
|
||||
modelId,
|
||||
});
|
||||
const { code, msg } = await batchCreateTag(
|
||||
ids.map((id) => {
|
||||
return {
|
||||
itemId: id,
|
||||
tagDefineType: TAG_DEFINE_TYPE.DIMENSION,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
setLoading(false);
|
||||
if (code === 200) {
|
||||
queryDimensionList({ ...filterParams, ...defaultPagination });
|
||||
|
||||
@@ -73,11 +73,14 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
const { code, msg } = await batchCreateTag({
|
||||
itemIds: ids,
|
||||
type: TAG_DEFINE_TYPE.METRIC,
|
||||
modelId,
|
||||
});
|
||||
const { code, msg } = await batchCreateTag(
|
||||
ids.map((id) => {
|
||||
return {
|
||||
itemId: id,
|
||||
tagDefineType: TAG_DEFINE_TYPE.METRIC,
|
||||
};
|
||||
}),
|
||||
);
|
||||
setLoading(false);
|
||||
if (code === 200) {
|
||||
queryMetricList({ ...filterParams, ...defaultPagination });
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
updateDimension,
|
||||
mockDimensionAlias,
|
||||
batchCreateTag,
|
||||
batchUpdateTagStatus,
|
||||
batchDeleteTag,
|
||||
} from '../service';
|
||||
import FormItemTitle from '@/components/FormHelper/FormItemTitle';
|
||||
|
||||
@@ -77,11 +77,11 @@ const DimensionInfoModal: React.FC<CreateFormProps> = ({
|
||||
}
|
||||
const { code, msg, data } = await saveDimensionQuery(queryParams);
|
||||
if (code === 200) {
|
||||
if (!queryParams.id && queryParams.isTag) {
|
||||
queryBatchExportTag(data.id);
|
||||
if (queryParams.isTag) {
|
||||
queryBatchExportTag(data.id || dimensionItem?.id);
|
||||
}
|
||||
if (dimensionItem?.id && !queryParams.isTag) {
|
||||
queryBatchUpdateStatus(dimensionItem.bizName, StatusEnum.DELETED);
|
||||
queryBatchDeleteTag(dimensionItem);
|
||||
}
|
||||
if (!isSilenceSubmit) {
|
||||
message.success('编辑维度成功');
|
||||
@@ -92,11 +92,10 @@ const DimensionInfoModal: React.FC<CreateFormProps> = ({
|
||||
message.error(msg);
|
||||
};
|
||||
|
||||
const queryBatchUpdateStatus = async (bizName: string, status: StatusEnum) => {
|
||||
const { code, msg } = await batchUpdateTagStatus({
|
||||
bizNames: [bizName],
|
||||
modelId: [modelId],
|
||||
status,
|
||||
const queryBatchDeleteTag = async (dimensionItem: ISemantic.IDimensionItem) => {
|
||||
const { code, msg } = await batchDeleteTag({
|
||||
itemIds: [dimensionItem.id],
|
||||
tagDefineType: TAG_DEFINE_TYPE.DIMENSION,
|
||||
});
|
||||
if (code === 200) {
|
||||
return;
|
||||
@@ -105,11 +104,9 @@ const DimensionInfoModal: React.FC<CreateFormProps> = ({
|
||||
};
|
||||
|
||||
const queryBatchExportTag = async (id: number) => {
|
||||
const { code, msg } = await batchCreateTag({
|
||||
itemIds: [id],
|
||||
type: TAG_DEFINE_TYPE.DIMENSION,
|
||||
modelId,
|
||||
});
|
||||
const { code, msg } = await batchCreateTag([
|
||||
{ itemId: id, tagDefineType: TAG_DEFINE_TYPE.DIMENSION },
|
||||
]);
|
||||
|
||||
if (code === 200) {
|
||||
return;
|
||||
|
||||
@@ -5,13 +5,17 @@ import { connect, history } from 'umi';
|
||||
import ClassDimensionTable from './ClassDimensionTable';
|
||||
import ClassMetricTable from './ClassMetricTable';
|
||||
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 styles from './style.less';
|
||||
import type { StateType } from '../model';
|
||||
import { HomeOutlined, FundViewOutlined } from '@ant-design/icons';
|
||||
import { ISemantic } from '../data';
|
||||
import SemanticGraphCanvas from '../SemanticGraphCanvas';
|
||||
import HeadlessFlows from '../HeadlessFlows';
|
||||
import SemanticFlows from '../SemanticFlows';
|
||||
import RecommendedQuestionsSection from '../components/Entity/RecommendedQuestionsSection';
|
||||
import View from '../View';
|
||||
// import DatabaseTable from '../components/Database/DatabaseTable';
|
||||
@@ -66,12 +70,18 @@ const DomainManagerTab: React.FC<Props> = ({
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: '标签对象管理',
|
||||
key: 'tagObjectManange',
|
||||
children: <TagObjectTable />,
|
||||
},
|
||||
{
|
||||
label: '画布',
|
||||
key: 'xflow',
|
||||
children: (
|
||||
<div style={{ width: '100%' }}>
|
||||
<SemanticGraphCanvas />
|
||||
{/* <HeadlessFlows /> */}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -104,11 +114,11 @@ const DomainManagerTab: React.FC<Props> = ({
|
||||
key: 'dimenstion',
|
||||
children: <ClassDimensionTable />,
|
||||
},
|
||||
{
|
||||
label: '标签管理',
|
||||
key: 'tag',
|
||||
children: <ClassTagTable />,
|
||||
},
|
||||
// {
|
||||
// label: '标签管理',
|
||||
// key: 'tag',
|
||||
// children: <ClassTagTable />,
|
||||
// },
|
||||
|
||||
{
|
||||
label: '权限管理',
|
||||
|
||||
@@ -54,7 +54,17 @@ const DimensionMetricVisibleTableTransfer: React.FC<Props> = ({
|
||||
return <TransTypeTag type={type} />;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
dataIndex: 'isTag',
|
||||
title: '是否标签',
|
||||
// hidden: true,
|
||||
render: (isTag) => {
|
||||
if (isTag) {
|
||||
return <span style={{ color: '#0958d9' }}>是</span>;
|
||||
}
|
||||
return '否';
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: 'modelName',
|
||||
title: '所属模型',
|
||||
|
||||
@@ -32,13 +32,14 @@ const DimensionMetricVisibleTransfer: React.FC<Props> = ({
|
||||
|
||||
useEffect(() => {
|
||||
setTransferData(
|
||||
sourceList.map(({ key, id, name, bizName, transType, modelName }) => {
|
||||
sourceList.map(({ key, id, name, bizName, transType, modelName, isTag }) => {
|
||||
return {
|
||||
key,
|
||||
name,
|
||||
bizName,
|
||||
id,
|
||||
modelName,
|
||||
isTag,
|
||||
type: transType,
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
getModelDetail,
|
||||
getDrillDownDimension,
|
||||
batchCreateTag,
|
||||
batchUpdateTagStatus,
|
||||
batchDeleteTag,
|
||||
} from '../service';
|
||||
import MetricMetricFormTable from './MetricMetricFormTable';
|
||||
import MetricFieldFormTable from './MetricFieldFormTable';
|
||||
@@ -381,11 +381,12 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
|
||||
}
|
||||
const { code, msg, data } = await saveMetricQuery(queryParams);
|
||||
if (code === 200) {
|
||||
if (!queryParams.id && queryParams.isTag) {
|
||||
queryBatchExportTag(data.id);
|
||||
if (queryParams.isTag) {
|
||||
queryBatchExportTag(data.id || metricItem?.id);
|
||||
}
|
||||
|
||||
if (metricItem?.id && !queryParams.isTag) {
|
||||
queryBatchUpdateStatus(metricItem.bizName, StatusEnum.DELETED);
|
||||
queryBatchDelete(metricItem);
|
||||
}
|
||||
message.success('编辑指标成功');
|
||||
onSubmit?.(queryParams);
|
||||
@@ -394,11 +395,10 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
|
||||
message.error(msg);
|
||||
};
|
||||
|
||||
const queryBatchUpdateStatus = async (bizName: string, status: StatusEnum) => {
|
||||
const { code, msg } = await batchUpdateTagStatus({
|
||||
bizNames: [bizName],
|
||||
modelId: [modelId],
|
||||
status,
|
||||
const queryBatchDelete = async (metricItem: ISemantic.IMetricItem) => {
|
||||
const { code, msg } = await batchDeleteTag({
|
||||
itemIds: [metricItem.id],
|
||||
tagDefineType: TAG_DEFINE_TYPE.METRIC,
|
||||
});
|
||||
if (code === 200) {
|
||||
return;
|
||||
@@ -407,11 +407,9 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
|
||||
};
|
||||
|
||||
const queryBatchExportTag = async (id: number) => {
|
||||
const { code, msg } = await batchCreateTag({
|
||||
itemIds: [id],
|
||||
type: TAG_DEFINE_TYPE.METRIC,
|
||||
modelId,
|
||||
});
|
||||
const { code, msg } = await batchCreateTag([
|
||||
{ itemId: id, tagDefineType: TAG_DEFINE_TYPE.METRIC },
|
||||
]);
|
||||
|
||||
if (code === 200) {
|
||||
return;
|
||||
|
||||
@@ -145,6 +145,7 @@ export declare namespace ISemantic {
|
||||
viewOrgs?: any[];
|
||||
admins?: string[];
|
||||
adminOrgs?: any[];
|
||||
tagObjectId?: number;
|
||||
drillDownDimensions: IDrillDownDimensionItem[];
|
||||
createdBy: UserName;
|
||||
updatedBy: UserName;
|
||||
@@ -411,6 +412,22 @@ export declare namespace ISemantic {
|
||||
tagDefineParams: ITagDefineParams;
|
||||
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 {
|
||||
|
||||
@@ -613,7 +613,7 @@ export function deleteView(viewId: number): 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',
|
||||
data: { pageSize: 9999, ...data },
|
||||
});
|
||||
@@ -664,3 +664,37 @@ export function batchCreateTag(data: any): Promise<any> {
|
||||
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