mirror of
https://github.com/tencentmusic/supersonic.git
synced 2025-12-10 11:07:06 +00:00
[improvement][semantic-fe] Adding the ability to edit relationships between models in the canvas. (#431)
* [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.
This commit is contained in:
@@ -224,6 +224,7 @@ ol {
|
||||
min-width: 100px;
|
||||
h3 {
|
||||
padding-bottom: 5px;
|
||||
margin: 0;
|
||||
border-bottom: 1px solid #4E86F5;
|
||||
}
|
||||
li {
|
||||
|
||||
@@ -2,8 +2,8 @@ import React, { useState } from 'react';
|
||||
import { Form, Input, Spin, Select, message } from 'antd';
|
||||
import type { FormInstance } from 'antd/lib/form';
|
||||
import { getDbNames, getTables } from '../../service';
|
||||
import SqlEditor from '@/components/SqlEditor';
|
||||
import { ISemantic } from '../../data';
|
||||
import { isString } from 'lodash';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
const { TextArea } = Input;
|
||||
@@ -109,28 +109,40 @@ const DataSourceBasicForm: React.FC<Props> = ({ isEdit, databaseConfigList, mode
|
||||
|
||||
<FormItem
|
||||
name="name"
|
||||
label="数据源中文名"
|
||||
rules={[{ required: true, message: '请输入数据源中文名' }]}
|
||||
label="模型中文名"
|
||||
rules={[{ required: true, message: '请输入模型中文名' }]}
|
||||
>
|
||||
<Input placeholder="名称不可重复" />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="bizName"
|
||||
label="数据源英文名"
|
||||
rules={[{ required: true, message: '请输入数据源英文名' }]}
|
||||
label="模型英文名"
|
||||
rules={[{ required: true, message: '请输入模型英文名' }]}
|
||||
>
|
||||
<Input placeholder="名称不可重复" disabled={isEdit} />
|
||||
</FormItem>
|
||||
<FormItem name="description" label="数据源描述">
|
||||
<TextArea placeholder="请输入数据源描述" />
|
||||
</FormItem>
|
||||
{/* <FormItem
|
||||
name="filterSql"
|
||||
label="过滤SQL"
|
||||
tooltip="主要用于词典导入场景, 对维度值进行过滤 格式: field1 = 'xxx' and field2 = 'yyy'"
|
||||
<FormItem
|
||||
name="alias"
|
||||
label="别名"
|
||||
getValueFromEvent={(value) => {
|
||||
return value.join(',');
|
||||
}}
|
||||
getValueProps={(value) => {
|
||||
return {
|
||||
value: isString(value) ? value.split(',') : [],
|
||||
};
|
||||
}}
|
||||
>
|
||||
<SqlEditor height={'150px'} />
|
||||
</FormItem> */}
|
||||
<Select
|
||||
mode="tags"
|
||||
placeholder="输入别名后回车确认,多别名输入、复制粘贴支持英文逗号自动分隔"
|
||||
tokenSeparators={[',']}
|
||||
maxTagCount={9}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem name="description" label="模型描述">
|
||||
<TextArea placeholder="请输入模型描述" />
|
||||
</FormItem>
|
||||
</Spin>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import DataSourceFieldForm from './DataSourceFieldForm';
|
||||
import { formLayout } from '@/components/FormHelper/utils';
|
||||
import { EnumDataSourceType } from '../constants';
|
||||
import styles from '../style.less';
|
||||
import { createDatasource, updateDatasource, getColumns } from '../../service';
|
||||
import { updateModel, createModel, getColumns } from '../../service';
|
||||
import type { Dispatch } from 'umi';
|
||||
import type { StateType } from '../../model';
|
||||
import { connect } from 'umi';
|
||||
@@ -29,9 +29,9 @@ export type CreateFormProps = {
|
||||
const { Step } = Steps;
|
||||
|
||||
const initFormVal = {
|
||||
name: '', // 数据源名称
|
||||
bizName: '', // 数据源英文名
|
||||
description: '', // 数据源描述
|
||||
name: '', // 模型名称
|
||||
bizName: '', // 模型英文名
|
||||
description: '', // 模型描述
|
||||
};
|
||||
|
||||
const DataSourceCreateForm: React.FC<CreateFormProps> = ({
|
||||
@@ -56,7 +56,7 @@ const DataSourceCreateForm: React.FC<CreateFormProps> = ({
|
||||
const [formDatabaseId, setFormDatabaseId] = useState<number>();
|
||||
const formValRef = useRef(initFormVal as any);
|
||||
const [form] = Form.useForm();
|
||||
const { databaseConfigList, selectModelId: modelId } = domainManger;
|
||||
const { databaseConfigList, selectModelId: modelId, selectDomainId } = domainManger;
|
||||
const updateFormVal = (val: any) => {
|
||||
formValRef.current = val;
|
||||
};
|
||||
@@ -170,18 +170,22 @@ const DataSourceCreateForm: React.FC<CreateFormProps> = ({
|
||||
const { dbName, tableName } = submitForm;
|
||||
const queryParams = {
|
||||
...submitForm,
|
||||
sqlQuery: sql,
|
||||
databaseId: databaseId || dataSourceItem?.databaseId || formDatabaseId,
|
||||
queryType: basicInfoFormMode === 'fast' ? 'table_query' : 'sql_query',
|
||||
tableQuery: dbName && tableName ? `${dbName}.${tableName}` : '',
|
||||
modelId: isEdit ? dataSourceItem.modelId : modelId,
|
||||
filterSql: sqlFilter,
|
||||
domainId: isEdit ? dataSourceItem.domainId : selectDomainId,
|
||||
modelDetail: {
|
||||
...submitForm,
|
||||
queryType: basicInfoFormMode === 'fast' ? 'table_query' : 'sql_query',
|
||||
tableQuery: dbName && tableName ? `${dbName}.${tableName}` : '',
|
||||
sqlQuery: sql,
|
||||
},
|
||||
};
|
||||
const queryDatasource = isEdit ? updateDatasource : createDatasource;
|
||||
const queryDatasource = isEdit ? updateModel : createModel;
|
||||
const { code, msg, data } = await queryDatasource(queryParams);
|
||||
setSaveLoading(false);
|
||||
if (code === 200) {
|
||||
message.success('保存数据源成功!');
|
||||
message.success('保存模型成功!');
|
||||
onSubmit?.({
|
||||
...queryParams,
|
||||
...data,
|
||||
@@ -196,7 +200,13 @@ const DataSourceCreateForm: React.FC<CreateFormProps> = ({
|
||||
const initFields = (fieldsClassifyList: any[], columns: any[]) => {
|
||||
const columnFields: any[] = columns.map((item: any) => {
|
||||
const { type, nameEn } = item;
|
||||
const oldItem = fieldsClassifyList.find((oItem) => oItem.bizName === item.nameEn) || {};
|
||||
const oldItem =
|
||||
fieldsClassifyList.find((oItem) => {
|
||||
if (oItem.type === EnumDataSourceType.MEASURES) {
|
||||
return oItem.expr === item.nameEn;
|
||||
}
|
||||
return oItem.bizName === item.nameEn;
|
||||
}) || {};
|
||||
return {
|
||||
...oldItem,
|
||||
bizName: nameEn,
|
||||
@@ -225,7 +235,7 @@ const DataSourceCreateForm: React.FC<CreateFormProps> = ({
|
||||
};
|
||||
|
||||
const initData = async () => {
|
||||
const { queryType, tableQuery } = dataSourceItem.datasourceDetail;
|
||||
const { queryType, tableQuery } = dataSourceItem?.modelDetail || {};
|
||||
let tableQueryInitValue = {};
|
||||
let columns = fieldColumns || [];
|
||||
if (queryType === 'table_query') {
|
||||
@@ -241,9 +251,9 @@ const DataSourceCreateForm: React.FC<CreateFormProps> = ({
|
||||
};
|
||||
|
||||
const formatterInitData = (columns: any[], extendParams: Record<string, any> = {}) => {
|
||||
const { id, name, bizName, description, datasourceDetail, databaseId, filterSql } =
|
||||
const { id, name, bizName, description, modelDetail, databaseId, filterSql, alias } =
|
||||
dataSourceItem as any;
|
||||
const { dimensions, identifiers, measures } = datasourceDetail;
|
||||
const { dimensions, identifiers, measures } = modelDetail || {};
|
||||
const initValue = {
|
||||
id,
|
||||
name,
|
||||
@@ -251,8 +261,8 @@ const DataSourceCreateForm: React.FC<CreateFormProps> = ({
|
||||
description,
|
||||
databaseId,
|
||||
filterSql,
|
||||
alias,
|
||||
...extendParams,
|
||||
// ...tableQueryInitValue,
|
||||
};
|
||||
const editInitFormVal = {
|
||||
...formValRef.current,
|
||||
@@ -270,7 +280,7 @@ const DataSourceCreateForm: React.FC<CreateFormProps> = ({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const { queryType } = dataSourceItem?.datasourceDetail || {};
|
||||
const { queryType } = dataSourceItem?.modelDetail || {};
|
||||
if (queryType === 'table_query') {
|
||||
if (isEdit) {
|
||||
initData();
|
||||
@@ -281,7 +291,7 @@ const DataSourceCreateForm: React.FC<CreateFormProps> = ({
|
||||
}, [dataSourceItem]);
|
||||
|
||||
useEffect(() => {
|
||||
const { queryType } = dataSourceItem?.datasourceDetail || {};
|
||||
const { queryType } = dataSourceItem?.modelDetail || {};
|
||||
if (queryType !== 'table_query') {
|
||||
if (isEdit) {
|
||||
initData();
|
||||
@@ -358,14 +368,17 @@ const DataSourceCreateForm: React.FC<CreateFormProps> = ({
|
||||
上一步
|
||||
</Button>
|
||||
<Button onClick={onCancel}>取 消</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
onDataSourceBtnClick?.();
|
||||
}}
|
||||
>
|
||||
数据源编辑
|
||||
</Button>
|
||||
{(dataSourceItem?.modelDetail?.queryType === 'sql_query' ||
|
||||
basicInfoFormMode !== 'fast') && (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
onDataSourceBtnClick?.();
|
||||
}}
|
||||
>
|
||||
数据源编辑
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
@@ -385,11 +398,12 @@ const DataSourceCreateForm: React.FC<CreateFormProps> = ({
|
||||
<Button onClick={onCancel}>取消</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
handleNext();
|
||||
onClick={async () => {
|
||||
if (!isEdit && Array.isArray(fields) && fields.length === 0) {
|
||||
await form.validateFields();
|
||||
onOpenDataSourceEdit?.();
|
||||
}
|
||||
handleNext();
|
||||
}}
|
||||
>
|
||||
下一步
|
||||
@@ -401,7 +415,6 @@ const DataSourceCreateForm: React.FC<CreateFormProps> = ({
|
||||
onClick={() => {
|
||||
handleNext(true);
|
||||
}}
|
||||
// disabled={hasEmptyNameField}
|
||||
>
|
||||
保 存
|
||||
</Button>
|
||||
@@ -414,9 +427,8 @@ const DataSourceCreateForm: React.FC<CreateFormProps> = ({
|
||||
<Modal
|
||||
forceRender
|
||||
width={1300}
|
||||
// styles={{ padding: '32px 40px 48px' }}
|
||||
destroyOnClose
|
||||
title={`${isEdit ? '编辑' : '新建'}数据源`}
|
||||
title={`${isEdit ? '编辑' : '新建'}模型`}
|
||||
maskClosable={false}
|
||||
open={createModalVisible}
|
||||
footer={renderFooter()}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import styles from '../style.less';
|
||||
|
||||
type FieldItem = {
|
||||
expr?: string;
|
||||
bizName: string;
|
||||
sqlType: string;
|
||||
name: string;
|
||||
@@ -144,7 +145,7 @@ const FieldForm: React.FC<Props> = ({ fields, sql, onFieldChange, onSqlChange })
|
||||
);
|
||||
}
|
||||
if (type === EnumDataSourceType.MEASURES) {
|
||||
const agg = fields.find((field) => field.bizName === record.bizName)?.agg;
|
||||
const agg = fields.find((field) => field.expr === record.expr)?.agg;
|
||||
return (
|
||||
<Select
|
||||
placeholder="度量算子"
|
||||
|
||||
@@ -63,7 +63,7 @@ const SqlSide: React.FC<Props> = ({ initialValues, onSubmitSuccess }) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (initialValues) {
|
||||
updateTabSql(initialValues?.datasourceDetail?.sqlQuery || '', '数据源查询');
|
||||
updateTabSql(initialValues?.modelDetail?.sqlQuery || '', '数据源查询');
|
||||
}
|
||||
}, [initialValues]);
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import moment from 'moment';
|
||||
import { message, Row, Col, Button, Space, Select, Form, Tooltip, Radio } from 'antd';
|
||||
import {
|
||||
queryStruct,
|
||||
@@ -20,7 +19,7 @@ import { DateRangeType, DateSettingType } from '@/components/MDatePicker/type';
|
||||
import StandardFormRow from '@/components/StandardFormRow';
|
||||
import MetricTable from './Table';
|
||||
import { ColumnConfig } from '../data';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import { ISemantic } from '../../data';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
@@ -50,6 +49,7 @@ const MetricTrendSection: React.FC<Props> = ({ metircData }) => {
|
||||
const [relationDimensionOptions, setRelationDimensionOptions] = useState<
|
||||
{ value: string; label: string }[]
|
||||
>([]);
|
||||
const [dimensionList, setDimensionList] = useState<ISemantic.IDimensionItem[]>([]);
|
||||
const [queryParams, setQueryParams] = useState<any>({});
|
||||
const [downloadBtnDisabledState, setDownloadBtnDisabledState] = useState<boolean>(true);
|
||||
// const [showDimensionOptions, setShowDimensionOptions] = useState<any[]>([]);
|
||||
@@ -58,8 +58,8 @@ const MetricTrendSection: React.FC<Props> = ({ metircData }) => {
|
||||
endDate: string;
|
||||
dateField: string;
|
||||
}>({
|
||||
startDate: moment().subtract('6', 'days').format('YYYY-MM-DD'),
|
||||
endDate: moment().format('YYYY-MM-DD'),
|
||||
startDate: dayjs().subtract(6, 'days').format('YYYY-MM-DD'),
|
||||
endDate: dayjs().format('YYYY-MM-DD'),
|
||||
dateField: dateFieldMap[DateRangeType.DAY],
|
||||
});
|
||||
const [rowNumber, setRowNumber] = useState<number>(5);
|
||||
@@ -69,7 +69,7 @@ const MetricTrendSection: React.FC<Props> = ({ metircData }) => {
|
||||
const [groupByDimensionFieldName, setGroupByDimensionFieldName] = useState<string>();
|
||||
|
||||
const getMetricTrendData = async (params: any = { download: false }) => {
|
||||
const { download, dimensionGroup, dimensionFilters } = params;
|
||||
const { download, dimensionGroup = [], dimensionFilters = [] } = params;
|
||||
if (download) {
|
||||
setDownloadLoding(true);
|
||||
} else {
|
||||
@@ -80,8 +80,26 @@ const MetricTrendSection: React.FC<Props> = ({ metircData }) => {
|
||||
}
|
||||
const { modelId, bizName, name } = metircData;
|
||||
indicatorFields.current = [{ name, column: bizName }];
|
||||
|
||||
const dimensionFiltersBizNameList = dimensionFilters.map((item) => {
|
||||
return item.bizName;
|
||||
});
|
||||
|
||||
const bizNameList = Array.from(new Set([...dimensionFiltersBizNameList, ...dimensionGroup]));
|
||||
|
||||
const modelIds = dimensionList.reduce(
|
||||
(idList: number[], item: ISemantic.IDimensionItem) => {
|
||||
if (bizNameList.includes(item.bizName)) {
|
||||
idList.push(item.modelId);
|
||||
}
|
||||
return idList;
|
||||
},
|
||||
[modelId],
|
||||
);
|
||||
|
||||
const res = await queryStruct({
|
||||
modelId,
|
||||
// modelId,
|
||||
modelIds: Array.from(new Set(modelIds)),
|
||||
bizName,
|
||||
groups: dimensionGroup,
|
||||
dimensionFilters,
|
||||
@@ -123,9 +141,15 @@ const MetricTrendSection: React.FC<Props> = ({ metircData }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const queryDimensionList = async (modelId: number) => {
|
||||
const { code, data, msg } = await getDimensionList({ modelId });
|
||||
const queryDimensionList = async (ids: number[]) => {
|
||||
const { code, data, msg } = await getDimensionList({ ids });
|
||||
if (code === 200 && Array.isArray(data?.list)) {
|
||||
setDimensionList(data.list);
|
||||
setRelationDimensionOptions(
|
||||
data.list.map((item: ISemantic.IMetricItem) => {
|
||||
return { label: item.name, value: item.bizName };
|
||||
}),
|
||||
);
|
||||
return data.list;
|
||||
}
|
||||
message.error(msg);
|
||||
@@ -135,26 +159,18 @@ const MetricTrendSection: React.FC<Props> = ({ metircData }) => {
|
||||
const queryDrillDownDimension = async (metricId: number) => {
|
||||
const { code, data, msg } = await getDrillDownDimension(metricId);
|
||||
if (code === 200 && Array.isArray(data)) {
|
||||
const ids = data.map((item) => item.dimensionId);
|
||||
queryDimensionList(ids);
|
||||
return data;
|
||||
}
|
||||
message.error(msg);
|
||||
if (code !== 200) {
|
||||
message.error(msg);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const initDimensionData = async (metricItem: ISemantic.IMetricItem) => {
|
||||
const dimensionList = await queryDimensionList(metricItem.modelId);
|
||||
const drillDownDimension = await queryDrillDownDimension(metricItem.id);
|
||||
const drillDownDimensionIds = drillDownDimension.map(
|
||||
(item: ISemantic.IDrillDownDimensionItem) => item.dimensionId,
|
||||
);
|
||||
const drillDownDimensionList = dimensionList.filter((metricItem: ISemantic.IMetricItem) => {
|
||||
return drillDownDimensionIds.includes(metricItem.id);
|
||||
});
|
||||
setRelationDimensionOptions(
|
||||
drillDownDimensionList.map((item: ISemantic.IMetricItem) => {
|
||||
return { label: item.name, value: item.bizName };
|
||||
}),
|
||||
);
|
||||
await queryDrillDownDimension(metricItem.id);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -163,7 +179,7 @@ const MetricTrendSection: React.FC<Props> = ({ metircData }) => {
|
||||
initDimensionData(metircData);
|
||||
setDrillDownDimensions(metircData?.relateDimension?.drillDownDimensions || []);
|
||||
}
|
||||
}, [metircData, periodDate]);
|
||||
}, [metircData?.id, periodDate]);
|
||||
|
||||
return (
|
||||
<div style={{ backgroundColor: '#fff', marginTop: 20 }}>
|
||||
|
||||
@@ -34,8 +34,8 @@ const DataSourceRelationFormDrawer: React.FC<DataSourceRelationFormDrawerProps>
|
||||
|
||||
useEffect(() => {
|
||||
const { sourceData, targetData } = nodeDataSource;
|
||||
const dataSourceFromIdentifiers = sourceData?.datasourceDetail?.identifiers || [];
|
||||
const dataSourceToIdentifiers = targetData?.datasourceDetail?.identifiers || [];
|
||||
const dataSourceFromIdentifiers = sourceData?.modelDetail?.identifiers || [];
|
||||
const dataSourceToIdentifiers = targetData?.modelDetail?.identifiers || [];
|
||||
const dataSourceToIdentifiersNames = dataSourceToIdentifiers.map((item) => {
|
||||
return item.bizName;
|
||||
});
|
||||
|
||||
@@ -61,7 +61,7 @@ const XflowJsonSchemaFormDrawerForm: React.FC<CreateFormProps> = (props) => {
|
||||
if (!payload) {
|
||||
setCreateDataSourceModalOpen(true);
|
||||
} else {
|
||||
if (payload?.datasourceDetail?.queryType === 'table_query') {
|
||||
if (payload?.modelDetail?.queryType === 'table_query') {
|
||||
setDataSourceModalVisible(true);
|
||||
} else {
|
||||
setCreateModalVisible(true);
|
||||
|
||||
@@ -9,7 +9,7 @@ import { cloneDeep } from 'lodash';
|
||||
import type { IDataSource } from '../data';
|
||||
import { SemanticNodeType } from '../enum';
|
||||
import {
|
||||
getDatasourceList,
|
||||
getModelList,
|
||||
deleteDatasource,
|
||||
getDimensionList,
|
||||
createOrUpdateViewInfo,
|
||||
@@ -84,8 +84,8 @@ export namespace GraphApi {
|
||||
|
||||
export const loadDataSourceData = async (args: NsGraph.IGraphMeta) => {
|
||||
const { domainManger, graphConfig } = args.meta;
|
||||
const { selectModelId } = domainManger;
|
||||
const { code, data = [] } = await getDatasourceList({ modelId: selectModelId });
|
||||
const { selectDomainId } = domainManger;
|
||||
const { code, data = [] } = await getModelList(selectDomainId);
|
||||
const dataSourceMap = data.reduce(
|
||||
(itemMap: Record<string, IDataSource.IDataSourceItem>, item: IDataSource.IDataSourceItem) => {
|
||||
const { id, name } = item;
|
||||
|
||||
@@ -0,0 +1,802 @@
|
||||
import G6 from '@antv/g6';
|
||||
|
||||
const colors = {
|
||||
B: '#5B8FF9',
|
||||
R: '#F46649',
|
||||
Y: '#EEBC20',
|
||||
G: '#5BD8A6',
|
||||
DI: '#A7A7A7',
|
||||
};
|
||||
|
||||
// 自定义节点、边
|
||||
export const flowRectNodeRegister = () => {
|
||||
/**
|
||||
* 自定义节点
|
||||
*/
|
||||
G6.registerNode(
|
||||
'flow-rect',
|
||||
{
|
||||
shapeType: 'flow-rect',
|
||||
draw(cfg, group) {
|
||||
const {
|
||||
name = '',
|
||||
variableName,
|
||||
variableValue,
|
||||
variableUp,
|
||||
label,
|
||||
collapsed,
|
||||
currency,
|
||||
status,
|
||||
rate,
|
||||
} = cfg;
|
||||
|
||||
const grey = '#CED4D9';
|
||||
const rectConfig = {
|
||||
width: 202,
|
||||
height: 60,
|
||||
lineWidth: 1,
|
||||
fontSize: 12,
|
||||
fill: '#fff',
|
||||
radius: 4,
|
||||
stroke: grey,
|
||||
opacity: 1,
|
||||
};
|
||||
|
||||
const nodeOrigin = {
|
||||
x: -rectConfig.width / 2,
|
||||
y: -rectConfig.height / 2,
|
||||
};
|
||||
|
||||
const textConfig = {
|
||||
textAlign: 'left',
|
||||
textBaseline: 'bottom',
|
||||
};
|
||||
|
||||
const rect = group.addShape('rect', {
|
||||
attrs: {
|
||||
x: nodeOrigin.x,
|
||||
y: nodeOrigin.y,
|
||||
...rectConfig,
|
||||
},
|
||||
});
|
||||
|
||||
const rectBBox = rect.getBBox();
|
||||
|
||||
// label title
|
||||
group.addShape('text', {
|
||||
attrs: {
|
||||
...textConfig,
|
||||
x: 12 + nodeOrigin.x,
|
||||
y: 20 + nodeOrigin.y,
|
||||
text: name.length > 28 ? name.substr(0, 28) + '...' : name,
|
||||
fontSize: 12,
|
||||
opacity: 0.85,
|
||||
fill: '#000',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'name-shape',
|
||||
});
|
||||
|
||||
// price
|
||||
const price = group.addShape('text', {
|
||||
attrs: {
|
||||
...textConfig,
|
||||
x: 12 + nodeOrigin.x,
|
||||
y: rectBBox.maxY - 12,
|
||||
text: label,
|
||||
fontSize: 16,
|
||||
fill: '#000',
|
||||
opacity: 0.85,
|
||||
},
|
||||
});
|
||||
|
||||
// label currency
|
||||
group.addShape('text', {
|
||||
attrs: {
|
||||
...textConfig,
|
||||
x: price.getBBox().maxX + 5,
|
||||
y: rectBBox.maxY - 12,
|
||||
text: currency,
|
||||
fontSize: 12,
|
||||
fill: '#000',
|
||||
opacity: 0.75,
|
||||
},
|
||||
});
|
||||
|
||||
// percentage
|
||||
const percentText = group.addShape('text', {
|
||||
attrs: {
|
||||
...textConfig,
|
||||
x: rectBBox.maxX - 8,
|
||||
y: rectBBox.maxY - 12,
|
||||
text: `${((variableValue || 0) * 100).toFixed(2)}%`,
|
||||
fontSize: 12,
|
||||
textAlign: 'right',
|
||||
fill: colors[status],
|
||||
},
|
||||
});
|
||||
|
||||
// percentage triangle
|
||||
const symbol = variableUp ? 'triangle' : 'triangle-down';
|
||||
const triangle = group.addShape('marker', {
|
||||
attrs: {
|
||||
...textConfig,
|
||||
x: percentText.getBBox().minX - 10,
|
||||
y: rectBBox.maxY - 12 - 6,
|
||||
symbol,
|
||||
r: 6,
|
||||
fill: colors[status],
|
||||
},
|
||||
});
|
||||
|
||||
// variable name
|
||||
group.addShape('text', {
|
||||
attrs: {
|
||||
...textConfig,
|
||||
x: triangle.getBBox().minX - 4,
|
||||
y: rectBBox.maxY - 12,
|
||||
text: variableName,
|
||||
fontSize: 12,
|
||||
textAlign: 'right',
|
||||
fill: '#000',
|
||||
opacity: 0.45,
|
||||
},
|
||||
});
|
||||
|
||||
// bottom line background
|
||||
const bottomBackRect = group.addShape('rect', {
|
||||
attrs: {
|
||||
x: nodeOrigin.x,
|
||||
y: rectBBox.maxY - 4,
|
||||
width: rectConfig.width,
|
||||
height: 4,
|
||||
radius: [0, 0, rectConfig.radius, rectConfig.radius],
|
||||
fill: '#E0DFE3',
|
||||
},
|
||||
});
|
||||
|
||||
// bottom percent
|
||||
const bottomRect = group.addShape('rect', {
|
||||
attrs: {
|
||||
x: nodeOrigin.x,
|
||||
y: rectBBox.maxY - 4,
|
||||
width: rate * rectBBox.width,
|
||||
height: 4,
|
||||
radius: [0, 0, 0, rectConfig.radius],
|
||||
fill: colors[status],
|
||||
},
|
||||
});
|
||||
|
||||
// collapse rect
|
||||
if (cfg.children && cfg.children.length) {
|
||||
group.addShape('rect', {
|
||||
attrs: {
|
||||
x: rectConfig.width / 2 - 8,
|
||||
y: -8,
|
||||
width: 16,
|
||||
height: 16,
|
||||
stroke: 'rgba(0, 0, 0, 0.25)',
|
||||
cursor: 'pointer',
|
||||
fill: '#fff',
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'collapse-back',
|
||||
modelId: cfg.id,
|
||||
});
|
||||
|
||||
// collpase text
|
||||
group.addShape('text', {
|
||||
attrs: {
|
||||
x: rectConfig.width / 2,
|
||||
y: -1,
|
||||
textAlign: 'center',
|
||||
textBaseline: 'middle',
|
||||
text: collapsed ? '+' : '-',
|
||||
fontSize: 16,
|
||||
cursor: 'pointer',
|
||||
fill: 'rgba(0, 0, 0, 0.25)',
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'collapse-text',
|
||||
modelId: cfg.id,
|
||||
});
|
||||
}
|
||||
|
||||
this.drawLinkPoints(cfg, group);
|
||||
return rect;
|
||||
},
|
||||
update(cfg, item) {
|
||||
const { level, status, name } = cfg;
|
||||
const group = item.getContainer();
|
||||
let mask = group.find((ele) => ele.get('name') === 'mask-shape');
|
||||
let maskLabel = group.find((ele) => ele.get('name') === 'mask-label-shape');
|
||||
if (level === 0) {
|
||||
group.get('children').forEach((child) => {
|
||||
if (child.get('name')?.includes('collapse')) return;
|
||||
child.hide();
|
||||
});
|
||||
if (!mask) {
|
||||
mask = group.addShape('rect', {
|
||||
attrs: {
|
||||
x: -101,
|
||||
y: -30,
|
||||
width: 202,
|
||||
height: 60,
|
||||
opacity: 0,
|
||||
fill: colors[status],
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'mask-shape',
|
||||
});
|
||||
maskLabel = group.addShape('text', {
|
||||
attrs: {
|
||||
fill: '#fff',
|
||||
fontSize: 20,
|
||||
x: 0,
|
||||
y: 10,
|
||||
text: name.length > 28 ? name.substr(0, 16) + '...' : name,
|
||||
textAlign: 'center',
|
||||
opacity: 0,
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'mask-label-shape',
|
||||
});
|
||||
const collapseRect = group.find((ele) => ele.get('name') === 'collapse-back');
|
||||
const collapseText = group.find((ele) => ele.get('name') === 'collapse-text');
|
||||
collapseRect?.toFront();
|
||||
collapseText?.toFront();
|
||||
} else {
|
||||
mask.show();
|
||||
maskLabel.show();
|
||||
}
|
||||
mask.animate({ opacity: 1 }, 200);
|
||||
maskLabel.animate({ opacity: 1 }, 200);
|
||||
return mask;
|
||||
} else {
|
||||
group.get('children').forEach((child) => {
|
||||
if (child.get('name')?.includes('collapse')) return;
|
||||
child.show();
|
||||
});
|
||||
mask?.animate(
|
||||
{ opacity: 0 },
|
||||
{
|
||||
duration: 200,
|
||||
callback: () => mask.hide(),
|
||||
},
|
||||
);
|
||||
maskLabel?.animate(
|
||||
{ opacity: 0 },
|
||||
{
|
||||
duration: 200,
|
||||
callback: () => maskLabel.hide(),
|
||||
},
|
||||
);
|
||||
}
|
||||
this.updateLinkPoints(cfg, group);
|
||||
},
|
||||
setState(name, value, item) {
|
||||
if (name === 'collapse') {
|
||||
const group = item.getContainer();
|
||||
const collapseText = group.find((e) => e.get('name') === 'collapse-text');
|
||||
if (collapseText) {
|
||||
if (!value) {
|
||||
collapseText.attr({
|
||||
text: '-',
|
||||
});
|
||||
} else {
|
||||
collapseText.attr({
|
||||
text: '+',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
getAnchorPoints() {
|
||||
return [
|
||||
[0, 0.5],
|
||||
[1, 0.5],
|
||||
];
|
||||
},
|
||||
},
|
||||
'rect',
|
||||
);
|
||||
|
||||
G6.registerEdge(
|
||||
'flow-cubic',
|
||||
{
|
||||
getControlPoints(cfg) {
|
||||
let controlPoints = cfg.controlPoints; // 指定controlPoints
|
||||
if (!controlPoints || !controlPoints.length) {
|
||||
const { startPoint, endPoint, sourceNode, targetNode } = cfg;
|
||||
const {
|
||||
x: startX,
|
||||
y: startY,
|
||||
coefficientX,
|
||||
coefficientY,
|
||||
} = sourceNode ? sourceNode.getModel() : startPoint;
|
||||
const { x: endX, y: endY } = targetNode ? targetNode.getModel() : endPoint;
|
||||
let curveStart = (endX - startX) * coefficientX;
|
||||
let curveEnd = (endY - startY) * coefficientY;
|
||||
curveStart = curveStart > 40 ? 40 : curveStart;
|
||||
curveEnd = curveEnd < -30 ? curveEnd : -30;
|
||||
controlPoints = [
|
||||
{ x: startPoint.x + curveStart, y: startPoint.y },
|
||||
{ x: endPoint.x + curveEnd, y: endPoint.y },
|
||||
];
|
||||
}
|
||||
return controlPoints;
|
||||
},
|
||||
getPath(points) {
|
||||
const path = [];
|
||||
path.push(['M', points[0].x, points[0].y]);
|
||||
path.push([
|
||||
'C',
|
||||
points[1].x,
|
||||
points[1].y,
|
||||
points[2].x,
|
||||
points[2].y,
|
||||
points[3].x,
|
||||
points[3].y,
|
||||
]);
|
||||
return path;
|
||||
},
|
||||
},
|
||||
'single-line',
|
||||
);
|
||||
};
|
||||
|
||||
const COLLAPSE_ICON = function COLLAPSE_ICON(x, y, r) {
|
||||
return [
|
||||
['M', x - r, y],
|
||||
['a', r, r, 0, 1, 0, r * 2, 0],
|
||||
['a', r, r, 0, 1, 0, -r * 2, 0],
|
||||
['M', x - r + 4, y],
|
||||
['L', x - r + 2 * r - 4, y],
|
||||
];
|
||||
};
|
||||
const EXPAND_ICON = function EXPAND_ICON(x, y, r) {
|
||||
return [
|
||||
['M', x - r, y],
|
||||
['a', r, r, 0, 1, 0, r * 2, 0],
|
||||
['a', r, r, 0, 1, 0, -r * 2, 0],
|
||||
['M', x - r + 4, y],
|
||||
['L', x - r + 2 * r - 4, y],
|
||||
['M', x - r + r, y - r + 4],
|
||||
['L', x, y + r - 4],
|
||||
];
|
||||
};
|
||||
|
||||
export const cardNodeRegister = (graph) => {
|
||||
const ERROR_COLOR = '#F5222D';
|
||||
const getNodeConfig = (node) => {
|
||||
if (node.nodeError) {
|
||||
return {
|
||||
basicColor: ERROR_COLOR,
|
||||
fontColor: '#FFF',
|
||||
borderColor: ERROR_COLOR,
|
||||
bgColor: '#E66A6C',
|
||||
};
|
||||
}
|
||||
let config = {
|
||||
basicColor: '#5B8FF9',
|
||||
fontColor: '#5B8FF9',
|
||||
borderColor: '#5B8FF9',
|
||||
bgColor: '#C6E5FF',
|
||||
};
|
||||
switch (node.type) {
|
||||
case 'root': {
|
||||
config = {
|
||||
basicColor: '#E3E6E8',
|
||||
fontColor: 'rgba(0,0,0,0.85)',
|
||||
borderColor: '#E3E6E8',
|
||||
bgColor: '#5b8ff9',
|
||||
};
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return config;
|
||||
};
|
||||
|
||||
const nodeBasicMethod = {
|
||||
createNodeBox: (group, config, w, h, isRoot) => {
|
||||
/* 最外面的大矩形 */
|
||||
const container = group.addShape('rect', {
|
||||
attrs: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: w,
|
||||
height: h,
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'big-rect-shape',
|
||||
});
|
||||
if (!isRoot) {
|
||||
/* 左边的小圆点 */
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
x: 3,
|
||||
y: h / 2,
|
||||
r: 6,
|
||||
fill: config.basicColor,
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'left-dot-shape',
|
||||
});
|
||||
}
|
||||
/* 矩形 */
|
||||
group.addShape('rect', {
|
||||
attrs: {
|
||||
x: 3,
|
||||
y: 0,
|
||||
width: w - 19,
|
||||
height: h,
|
||||
fill: config.bgColor,
|
||||
stroke: config.borderColor,
|
||||
radius: 2,
|
||||
cursor: 'pointer',
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'rect-shape',
|
||||
});
|
||||
|
||||
/* 左边的粗线 */
|
||||
group.addShape('rect', {
|
||||
attrs: {
|
||||
x: 3,
|
||||
y: 0,
|
||||
width: 3,
|
||||
height: h,
|
||||
fill: config.basicColor,
|
||||
radius: 1.5,
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'left-border-shape',
|
||||
});
|
||||
return container;
|
||||
},
|
||||
/* 生成树上的 marker */
|
||||
createNodeMarker: (group, collapsed, x, y) => {
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
x,
|
||||
y,
|
||||
r: 13,
|
||||
fill: 'rgba(47, 84, 235, 0.05)',
|
||||
opacity: 0,
|
||||
zIndex: -2,
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'collapse-icon-bg',
|
||||
});
|
||||
group.addShape('marker', {
|
||||
attrs: {
|
||||
x,
|
||||
y,
|
||||
r: 7,
|
||||
symbol: collapsed ? EXPAND_ICON : COLLAPSE_ICON,
|
||||
stroke: 'rgba(0,0,0,0.25)',
|
||||
fill: 'rgba(0,0,0,0)',
|
||||
lineWidth: 1,
|
||||
cursor: 'pointer',
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'collapse-icon',
|
||||
});
|
||||
},
|
||||
afterDraw: (cfg, group) => {
|
||||
/* 操作 marker 的背景色显示隐藏 */
|
||||
const icon = group.find((element) => element.get('name') === 'collapse-icon');
|
||||
if (icon) {
|
||||
const bg = group.find((element) => element.get('name') === 'collapse-icon-bg');
|
||||
icon.on('mouseenter', () => {
|
||||
bg.attr('opacity', 1);
|
||||
graph.get('canvas').draw();
|
||||
});
|
||||
icon.on('mouseleave', () => {
|
||||
bg.attr('opacity', 0);
|
||||
graph.get('canvas').draw();
|
||||
});
|
||||
}
|
||||
/* ip 显示 */
|
||||
const ipBox = group.find((element) => element.get('name') === 'ip-box');
|
||||
if (ipBox) {
|
||||
/* ip 复制的几个元素 */
|
||||
const ipLine = group.find((element) => element.get('name') === 'ip-cp-line');
|
||||
const ipBG = group.find((element) => element.get('name') === 'ip-cp-bg');
|
||||
const ipIcon = group.find((element) => element.get('name') === 'ip-cp-icon');
|
||||
const ipCPBox = group.find((element) => element.get('name') === 'ip-cp-box');
|
||||
|
||||
const onMouseEnter = () => {
|
||||
ipLine.attr('opacity', 1);
|
||||
ipBG.attr('opacity', 1);
|
||||
ipIcon.attr('opacity', 1);
|
||||
graph.get('canvas').draw();
|
||||
};
|
||||
const onMouseLeave = () => {
|
||||
ipLine.attr('opacity', 0);
|
||||
ipBG.attr('opacity', 0);
|
||||
ipIcon.attr('opacity', 0);
|
||||
graph.get('canvas').draw();
|
||||
};
|
||||
ipBox.on('mouseenter', () => {
|
||||
onMouseEnter();
|
||||
});
|
||||
ipBox.on('mouseleave', () => {
|
||||
onMouseLeave();
|
||||
});
|
||||
ipCPBox.on('mouseenter', () => {
|
||||
onMouseEnter();
|
||||
});
|
||||
ipCPBox.on('mouseleave', () => {
|
||||
onMouseLeave();
|
||||
});
|
||||
ipCPBox.on('click', () => {});
|
||||
}
|
||||
},
|
||||
setState: (name, value, item) => {
|
||||
const hasOpacityClass = [
|
||||
'ip-cp-line',
|
||||
'ip-cp-bg',
|
||||
'ip-cp-icon',
|
||||
'ip-cp-box',
|
||||
'ip-box',
|
||||
'collapse-icon-bg',
|
||||
];
|
||||
const group = item.getContainer();
|
||||
const childrens = group.get('children');
|
||||
graph.setAutoPaint(false);
|
||||
if (name === 'emptiness') {
|
||||
if (value) {
|
||||
childrens.forEach((shape) => {
|
||||
if (hasOpacityClass.indexOf(shape.get('name')) > -1) {
|
||||
return;
|
||||
}
|
||||
shape.attr('opacity', 0.4);
|
||||
});
|
||||
} else {
|
||||
childrens.forEach((shape) => {
|
||||
if (hasOpacityClass.indexOf(shape.get('name')) > -1) {
|
||||
return;
|
||||
}
|
||||
shape.attr('opacity', 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
graph.setAutoPaint(true);
|
||||
},
|
||||
};
|
||||
|
||||
G6.registerNode('card-node', {
|
||||
draw: (cfg, group) => {
|
||||
const config = getNodeConfig(cfg);
|
||||
const isRoot = cfg.dataType === 'root';
|
||||
const nodeError = cfg.nodeError;
|
||||
/* the biggest rect */
|
||||
const container = nodeBasicMethod.createNodeBox(group, config, 243, 64, isRoot);
|
||||
|
||||
if (cfg.dataType !== 'root') {
|
||||
/* the type text */
|
||||
group.addShape('text', {
|
||||
attrs: {
|
||||
text: cfg.dataType,
|
||||
x: 3,
|
||||
y: -10,
|
||||
fontSize: 12,
|
||||
textAlign: 'left',
|
||||
textBaseline: 'middle',
|
||||
fill: 'rgba(0,0,0,0.65)',
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'type-text-shape',
|
||||
});
|
||||
}
|
||||
|
||||
if (cfg.ip) {
|
||||
/* ip start */
|
||||
/* ipBox */
|
||||
const ipRect = group.addShape('rect', {
|
||||
attrs: {
|
||||
fill: nodeError ? null : '#FFF',
|
||||
stroke: nodeError ? 'rgba(255,255,255,0.65)' : null,
|
||||
radius: 2,
|
||||
cursor: 'pointer',
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'ip-container-shape',
|
||||
});
|
||||
|
||||
/* ip */
|
||||
const ipText = group.addShape('text', {
|
||||
attrs: {
|
||||
text: cfg.ip,
|
||||
x: 0,
|
||||
y: 19,
|
||||
fontSize: 12,
|
||||
textAlign: 'left',
|
||||
textBaseline: 'middle',
|
||||
fill: nodeError ? 'rgba(255,255,255,0.85)' : 'rgba(0,0,0,0.65)',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'ip-text-shape',
|
||||
});
|
||||
|
||||
const ipBBox = ipText.getBBox();
|
||||
/* the distance from the IP to the right is 12px */
|
||||
ipText.attr({
|
||||
x: 224 - 12 - ipBBox.width,
|
||||
});
|
||||
/* ipBox */
|
||||
ipRect.attr({
|
||||
x: 224 - 12 - ipBBox.width - 4,
|
||||
y: ipBBox.minY - 5,
|
||||
width: ipBBox.width + 8,
|
||||
height: ipBBox.height + 10,
|
||||
});
|
||||
|
||||
/* a transparent shape on the IP for click listener */
|
||||
group.addShape('rect', {
|
||||
attrs: {
|
||||
stroke: '',
|
||||
cursor: 'pointer',
|
||||
x: 224 - 12 - ipBBox.width - 4,
|
||||
y: ipBBox.minY - 5,
|
||||
width: ipBBox.width + 8,
|
||||
height: ipBBox.height + 10,
|
||||
fill: '#fff',
|
||||
opacity: 0,
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'ip-box',
|
||||
});
|
||||
|
||||
/* copyIpLine */
|
||||
group.addShape('rect', {
|
||||
attrs: {
|
||||
x: 194,
|
||||
y: 7,
|
||||
width: 1,
|
||||
height: 24,
|
||||
fill: '#E3E6E8',
|
||||
opacity: 0,
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'ip-cp-line',
|
||||
});
|
||||
/* copyIpBG */
|
||||
group.addShape('rect', {
|
||||
attrs: {
|
||||
x: 195,
|
||||
y: 8,
|
||||
width: 22,
|
||||
height: 22,
|
||||
fill: '#FFF',
|
||||
cursor: 'pointer',
|
||||
opacity: 0,
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'ip-cp-bg',
|
||||
});
|
||||
/* copyIpIcon */
|
||||
group.addShape('image', {
|
||||
attrs: {
|
||||
x: 200,
|
||||
y: 13,
|
||||
height: 12,
|
||||
width: 10,
|
||||
img: 'https://os.alipayobjects.com/rmsportal/DFhnQEhHyPjSGYW.png',
|
||||
cursor: 'pointer',
|
||||
opacity: 0,
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'ip-cp-icon',
|
||||
});
|
||||
/* a transparent rect on the icon area for click listener */
|
||||
group.addShape('rect', {
|
||||
attrs: {
|
||||
x: 195,
|
||||
y: 8,
|
||||
width: 22,
|
||||
height: 22,
|
||||
fill: '#FFF',
|
||||
cursor: 'pointer',
|
||||
opacity: 0,
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'ip-cp-box',
|
||||
tooltip: 'Copy the IP',
|
||||
});
|
||||
|
||||
/* ip end */
|
||||
}
|
||||
|
||||
/* name */
|
||||
group.addShape('text', {
|
||||
attrs: {
|
||||
text: cfg.name,
|
||||
x: 19,
|
||||
y: 19,
|
||||
fontSize: 14,
|
||||
fontWeight: 700,
|
||||
textAlign: 'left',
|
||||
textBaseline: 'middle',
|
||||
fill: config.fontColor,
|
||||
cursor: 'pointer',
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'name-text-shape',
|
||||
});
|
||||
|
||||
group.addShape('image', {
|
||||
attrs: {
|
||||
x: 19,
|
||||
y: 19,
|
||||
height: 30,
|
||||
width: 30,
|
||||
img: '/icons/vector.svg',
|
||||
cursor: 'pointer',
|
||||
fill: '#fff',
|
||||
// opacity: 0,
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'type-cp-icon',
|
||||
});
|
||||
|
||||
group.addShape('rect', {
|
||||
attrs: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 90,
|
||||
height: 90,
|
||||
fill: '#FFF',
|
||||
cursor: 'pointer',
|
||||
opacity: 0,
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'ip-cp-boxxx',
|
||||
tooltip: 'Copy the IP',
|
||||
});
|
||||
|
||||
/* the description text */
|
||||
group.addShape('text', {
|
||||
attrs: {
|
||||
text: cfg.keyInfo,
|
||||
x: 19,
|
||||
y: 45,
|
||||
fontSize: 14,
|
||||
textAlign: 'left',
|
||||
textBaseline: 'middle',
|
||||
fill: config.fontColor,
|
||||
cursor: 'pointer',
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'bottom-text-shape',
|
||||
});
|
||||
|
||||
if (nodeError) {
|
||||
group.addShape('text', {
|
||||
attrs: {
|
||||
x: 191,
|
||||
y: 62,
|
||||
text: '⚠️',
|
||||
fill: '#000',
|
||||
fontSize: 18,
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'error-text-shape',
|
||||
});
|
||||
}
|
||||
|
||||
const hasChildren = cfg.children && cfg.children.length > 0;
|
||||
if (hasChildren) {
|
||||
nodeBasicMethod.createNodeMarker(group, cfg.collapsed, 236, 32);
|
||||
}
|
||||
return container;
|
||||
},
|
||||
afterDraw: nodeBasicMethod.afterDraw,
|
||||
setState: nodeBasicMethod.setState,
|
||||
});
|
||||
};
|
||||
@@ -21,10 +21,16 @@ export const getMenuConfig = (props?: InitContextMenuProps) => {
|
||||
<li title='编辑' key='edit' >编辑</li>
|
||||
<li title='删除' key='delete' >删除</li>
|
||||
`;
|
||||
// if (nodeType === SemanticNodeType.DATASOURCE) {
|
||||
// ulNode = `
|
||||
// <li title='新增维度' key='createDimension' >新增维度</li>
|
||||
// <li title='新增指标' key='createMetric' >新增指标</li>
|
||||
// <li title='编辑' key='editDatasource' >编辑</li>
|
||||
// <li title='删除' key='deleteDatasource' >删除</li>
|
||||
// `;
|
||||
// }
|
||||
if (nodeType === SemanticNodeType.DATASOURCE) {
|
||||
ulNode = `
|
||||
<li title='新增维度' key='createDimension' >新增维度</li>
|
||||
<li title='新增指标' key='createMetric' >新增指标</li>
|
||||
<li title='编辑' key='editDatasource' >编辑</li>
|
||||
<li title='删除' key='deleteDatasource' >删除</li>
|
||||
`;
|
||||
|
||||
@@ -34,9 +34,9 @@ const GraphToolBar: React.FC<Props> = ({ onClick }) => {
|
||||
onClick?.({ eventName: 'createDatabase' });
|
||||
}}
|
||||
>
|
||||
新建数据源
|
||||
新建模型
|
||||
</Button>
|
||||
<Button
|
||||
{/* <Button
|
||||
key="createDimensionBtn"
|
||||
icon={<PlusOutlined />}
|
||||
size="small"
|
||||
@@ -55,7 +55,7 @@ const GraphToolBar: React.FC<Props> = ({ onClick }) => {
|
||||
}}
|
||||
>
|
||||
新建指标
|
||||
</Button>
|
||||
</Button> */}
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Form, Button, Drawer, Space, Input, Select, message, Popconfirm } from 'antd';
|
||||
import { formLayout } from '@/components/FormHelper/utils';
|
||||
import { createOrUpdateModelRela, deleteModelRela } from '../../service';
|
||||
|
||||
export type ModelRelationFormDrawerProps = {
|
||||
domainId: number;
|
||||
nodeModel: any;
|
||||
relationData: any;
|
||||
open: boolean;
|
||||
onSave?: () => void;
|
||||
onDelete?: () => void;
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
const ModelRelationFormDrawer: React.FC<ModelRelationFormDrawerProps> = ({
|
||||
domainId,
|
||||
open,
|
||||
nodeModel,
|
||||
relationData,
|
||||
onSave,
|
||||
onDelete,
|
||||
onClose,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [saveLoading, setSaveLoading] = useState<boolean>(false);
|
||||
|
||||
const [sourcePrimaryOptions, setSourcePrimaryOptions] = useState<OptionsItem[]>([]);
|
||||
const [targetPrimaryOptions, setTargetPrimaryOptions] = useState<OptionsItem[]>([]);
|
||||
|
||||
const [deleteLoading, setDeleteLoading] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!relationData?.id) {
|
||||
form.resetFields();
|
||||
return;
|
||||
}
|
||||
const { joinConditions = [] } = relationData;
|
||||
const firstCondition = joinConditions[0] || {};
|
||||
const formData = {
|
||||
...relationData,
|
||||
...firstCondition,
|
||||
};
|
||||
form.setFieldsValue(formData);
|
||||
}, [relationData]);
|
||||
|
||||
useEffect(() => {
|
||||
const { sourceData, targetData } = nodeModel;
|
||||
const dataSourceFromIdentifiers = sourceData?.modelDetail?.identifiers || [];
|
||||
const dataSourceToIdentifiers = targetData?.modelDetail?.identifiers || [];
|
||||
|
||||
const sourceOptions = dataSourceFromIdentifiers.map((item: any) => {
|
||||
return {
|
||||
label: `${item.bizName}${item.name ? `(${item.name})` : ''}`,
|
||||
value: item.bizName,
|
||||
};
|
||||
});
|
||||
|
||||
const targetOptions = dataSourceToIdentifiers.map((item: any) => {
|
||||
return {
|
||||
label: `${item.bizName}${item.name ? `(${item.name})` : ''}`,
|
||||
value: item.bizName,
|
||||
};
|
||||
});
|
||||
|
||||
setSourcePrimaryOptions(sourceOptions);
|
||||
setTargetPrimaryOptions(targetOptions);
|
||||
}, [nodeModel]);
|
||||
|
||||
const renderContent = () => {
|
||||
return (
|
||||
<>
|
||||
<FormItem hidden={true} name="id" label="ID">
|
||||
<Input placeholder="id" />
|
||||
</FormItem>
|
||||
<FormItem label="起始数据源:">
|
||||
<span style={{ color: '#296df3', fontWeight: 500 }}>{nodeModel?.sourceData?.name}</span>
|
||||
</FormItem>
|
||||
<FormItem label="目标数据源:">
|
||||
<span style={{ color: '#296df3', fontWeight: 500 }}>{nodeModel?.targetData?.name}</span>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="leftField"
|
||||
label="起始关联字段:"
|
||||
rules={[{ required: true, message: '请选择关联主键' }]}
|
||||
>
|
||||
<Select placeholder="请选择关联主键" options={sourcePrimaryOptions} />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="rightField"
|
||||
label="目标关联字段:"
|
||||
rules={[{ required: true, message: '请选择关联主键' }]}
|
||||
>
|
||||
<Select placeholder="请选择关联主键" options={targetPrimaryOptions} />
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
name="operator"
|
||||
label="算子"
|
||||
rules={[{ required: true, message: '请选择关联算子' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择关联算子"
|
||||
options={[
|
||||
{ label: '=', value: '=' },
|
||||
{ label: '!=', value: '!=' },
|
||||
{ label: '>', value: '>' },
|
||||
{ label: '>=', value: '>=' },
|
||||
{ label: '<', value: '<' },
|
||||
{ label: '<=', value: '<=' },
|
||||
// { label: 'IN', value: 'IN' },
|
||||
// { label: 'NOT_IN', value: 'NOT_IN' },
|
||||
// { label: 'EQUALS', value: 'EQUALS' },
|
||||
// { label: 'BETWEEN', value: 'BETWEEN' },
|
||||
// { label: 'GREATER_THAN', value: 'GREATER_THAN' },
|
||||
// { label: 'GREATER_THAN_EQUALS', value: 'GREATER_THAN_EQUALS' },
|
||||
// { label: 'IS_NULL', value: 'IS_NULL' },
|
||||
// { label: 'IS_NOT_NULL', value: 'IS_NOT_NULL' },
|
||||
// { label: 'LIKE', value: 'LIKE' },
|
||||
// { label: 'MINOR_THAN', value: 'MINOR_THAN' },
|
||||
// { label: 'MINOR_THAN_EQUALS', value: 'MINOR_THAN_EQUALS' },
|
||||
// { label: 'NOT_EQUALS', value: 'NOT_EQUALS' },
|
||||
// { label: 'SQL_PART', value: 'SQL_PART' },
|
||||
// { label: 'EXISTS', value: 'EXISTS' },
|
||||
]}
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
name="joinType"
|
||||
label="join类型"
|
||||
rules={[{ required: true, message: '请选择join类型' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择join类型"
|
||||
options={[
|
||||
{ label: 'left join', value: 'left join' },
|
||||
{ label: 'inner join', value: 'inner join' },
|
||||
{ label: 'right join', value: 'right join' },
|
||||
{ label: 'outer join', value: 'outer join' },
|
||||
]}
|
||||
/>
|
||||
</FormItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const saveRelation = async () => {
|
||||
const values = await form.validateFields();
|
||||
setSaveLoading(true);
|
||||
const { code, msg } = await createOrUpdateModelRela({
|
||||
id: values.id,
|
||||
domainId,
|
||||
fromModelId: nodeModel?.sourceData?.uid,
|
||||
toModelId: nodeModel?.targetData?.uid,
|
||||
joinType: values.joinType,
|
||||
joinConditions: [
|
||||
{
|
||||
...values,
|
||||
},
|
||||
],
|
||||
});
|
||||
setSaveLoading(false);
|
||||
if (code === 200) {
|
||||
message.success('保存成功');
|
||||
onSave?.();
|
||||
return;
|
||||
}
|
||||
message.error(msg);
|
||||
};
|
||||
|
||||
const deleteRelation = async () => {
|
||||
setDeleteLoading(true);
|
||||
const { code, msg } = await deleteModelRela(relationData?.id);
|
||||
setDeleteLoading(false);
|
||||
if (code === 200) {
|
||||
message.success('删除成功');
|
||||
onDelete?.();
|
||||
return;
|
||||
}
|
||||
message.error(msg);
|
||||
};
|
||||
|
||||
const renderFooter = () => {
|
||||
return (
|
||||
<Space>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onClose?.();
|
||||
}}
|
||||
>
|
||||
取 消
|
||||
</Button>
|
||||
|
||||
<Popconfirm
|
||||
title="确定删除吗?"
|
||||
onCancel={(e) => {
|
||||
e?.stopPropagation();
|
||||
}}
|
||||
onConfirm={() => {
|
||||
if (relationData?.id) {
|
||||
deleteRelation();
|
||||
} else {
|
||||
onDelete?.();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button type="primary" danger loading={deleteLoading}>
|
||||
删 除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
loading={saveLoading}
|
||||
onClick={() => {
|
||||
saveRelation();
|
||||
}}
|
||||
>
|
||||
保 存
|
||||
</Button>
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
forceRender
|
||||
width={400}
|
||||
destroyOnClose
|
||||
getContainer={false}
|
||||
title={'数据源关联信息'}
|
||||
mask={false}
|
||||
open={open}
|
||||
footer={renderFooter()}
|
||||
onClose={() => {
|
||||
onClose?.();
|
||||
}}
|
||||
>
|
||||
<Form {...formLayout} form={form}>
|
||||
{renderContent()}
|
||||
</Form>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelRelationFormDrawer;
|
||||
@@ -7,7 +7,6 @@ import type { StateType } from '../../model';
|
||||
import moment from 'moment';
|
||||
import styles from '../style.less';
|
||||
import TransTypeTag from '../../components/TransTypeTag';
|
||||
import MetricTrendSection from '@/pages/SemanticModel/Metric/components/MetricTrendSection';
|
||||
import { SENSITIVE_LEVEL_ENUM } from '../../constant';
|
||||
|
||||
type Props = {
|
||||
@@ -106,16 +105,6 @@ const NodeInfoDrawer: React.FC<Props> = ({
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '指标趋势',
|
||||
render: () => (
|
||||
<Row key={`metricTrendSection`} style={{ marginBottom: 10, display: 'flex' }}>
|
||||
<Col span={24}>
|
||||
<MetricTrendSection nodeData={nodeData} />
|
||||
</Col>
|
||||
</Row>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '创建信息',
|
||||
children: [
|
||||
|
||||
@@ -7,15 +7,26 @@ const initTooltips = () => {
|
||||
offsetY: 10,
|
||||
fixToNode: [1, 0.5],
|
||||
// 允许出现 tooltip 的 item 类型
|
||||
itemTypes: ['node'],
|
||||
itemTypes: ['node', 'edge'],
|
||||
shouldBegin: (e) => {
|
||||
const model = e!.item!.getModel();
|
||||
const eleType = e!.item!.getType();
|
||||
if (eleType === 'node' || (eleType === 'edge' && model.sourceAnchor)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
// 自定义 tooltip 内容
|
||||
getContent: (e) => {
|
||||
const eleType = e!.item!.getType();
|
||||
const outDiv = document.createElement('div');
|
||||
outDiv.style.width = 'fit-content';
|
||||
outDiv.style.height = 'fit-content';
|
||||
const model = e!.item!.getModel();
|
||||
|
||||
const { name, bizName, createdBy, updatedAt, description } = model;
|
||||
const { name, bizName, createdBy, updatedAt, description, sourceAnchor } = model;
|
||||
if (eleType === 'edge' && sourceAnchor) {
|
||||
return '点击编辑模型关系';
|
||||
}
|
||||
const list = [
|
||||
{
|
||||
label: '名称:',
|
||||
@@ -41,7 +52,7 @@ const initTooltips = () => {
|
||||
const listHtml = list.reduce((htmlString, item) => {
|
||||
const { label, value } = item;
|
||||
if (value) {
|
||||
htmlString += `<p style="margin-bottom:0">
|
||||
htmlString += `<p style="margin-bottom:0;margin-top:0">
|
||||
<span>${label} </span>
|
||||
<span>${value}</span>
|
||||
</p>`;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { connect } from 'umi';
|
||||
import type { StateType } from '../model';
|
||||
// import { IGroup } from '@antv/g-base';
|
||||
import type { Dispatch } from 'umi';
|
||||
import {
|
||||
typeConfigs,
|
||||
@@ -11,8 +10,15 @@ import {
|
||||
flatGraphDataNode,
|
||||
} from './utils';
|
||||
import { message } from 'antd';
|
||||
import { getDomainSchemaRela } from '../service';
|
||||
import { Item, TreeGraphData, NodeConfig, IItemBaseConfig } from '@antv/g6-core';
|
||||
import {
|
||||
getDomainSchemaRela,
|
||||
getModelRelaList,
|
||||
getViewInfoList,
|
||||
createOrUpdateViewInfo,
|
||||
deleteViewInfo,
|
||||
} from '../service';
|
||||
import { jsonParse } from '@/utils/utils';
|
||||
import { Item, TreeGraphData, NodeConfig, IItemBaseConfig, EdgeConfig } from '@antv/g6-core';
|
||||
import initToolBar from './components/ToolBar';
|
||||
import initTooltips from './components/ToolTips';
|
||||
import initContextMenu from './components/ContextMenu';
|
||||
@@ -28,21 +34,14 @@ import ClassDataSourceTypeModal from '../components/ClassDataSourceTypeModal';
|
||||
import GraphToolBar from './components/GraphToolBar';
|
||||
import GraphLegend from './components/GraphLegend';
|
||||
import GraphLegendVisibleModeItem from './components/GraphLegendVisibleModeItem';
|
||||
|
||||
// import { cloneDeep } from 'lodash';
|
||||
import ModelRelationFormDrawer from './components/ModelRelationFormDrawer';
|
||||
|
||||
type Props = {
|
||||
// graphShowType?: SemanticNodeType;
|
||||
domainManger: StateType;
|
||||
dispatch: Dispatch;
|
||||
};
|
||||
|
||||
const DomainManger: React.FC<Props> = ({
|
||||
domainManger,
|
||||
// graphShowType = SemanticNodeType.DIMENSION,
|
||||
// graphShowType,
|
||||
dispatch,
|
||||
}) => {
|
||||
const DomainManger: React.FC<Props> = ({ domainManger, dispatch }) => {
|
||||
const ref = useRef(null);
|
||||
const dataSourceRef = useRef<ISemantic.IDomainSchemaRelaList>([]);
|
||||
const [graphData, setGraphData] = useState<TreeGraphData>();
|
||||
@@ -54,7 +53,7 @@ const DomainManger: React.FC<Props> = ({
|
||||
|
||||
const legendDataRef = useRef<any[]>([]);
|
||||
const graphRef = useRef<any>(null);
|
||||
// const legendDataFilterFunctions = useRef<any>({});
|
||||
|
||||
const [dimensionItem, setDimensionItem] = useState<ISemantic.IDimensionItem>();
|
||||
|
||||
const [metricItem, setMetricItem] = useState<ISemantic.IMetricItem>();
|
||||
@@ -77,6 +76,21 @@ const DomainManger: React.FC<Props> = ({
|
||||
const graphShowTypeRef = useRef<SemanticNodeType>();
|
||||
const [graphShowTypeState, setGraphShowTypeState] = useState<SemanticNodeType>();
|
||||
|
||||
const [modelRelationDrawerOpen, setModelRelationDrawerOpen] = useState<boolean>(false);
|
||||
const [nodeModel, setNodeModel] = useState<{
|
||||
sourceData: ISemantic.IModelItem;
|
||||
targetData: ISemantic.IModelItem;
|
||||
}>({ sourceData: {} as ISemantic.IModelItem, targetData: {} as ISemantic.IModelItem });
|
||||
|
||||
// const [relationData, setRelationData] = useState<any[]>([]);
|
||||
const relationDataListRef = useRef<any>([]);
|
||||
|
||||
const [relationConfig, setRelationConfig] = useState<any>({});
|
||||
|
||||
const [currentRelationDataItem, setCurrentRelationDataItem] = useState<any>({});
|
||||
|
||||
const [currentEdgeItem, setCurrentEdgeItem] = useState<any>();
|
||||
|
||||
const graphLegendDataSourceIds = useRef<string[]>();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -137,15 +151,15 @@ const DomainManger: React.FC<Props> = ({
|
||||
};
|
||||
|
||||
const queryDataSourceList = async (params: {
|
||||
modelId: number;
|
||||
domainId: number;
|
||||
graphShowType?: SemanticNodeType;
|
||||
}) => {
|
||||
const { code, data } = await getDomainSchemaRela(params.modelId);
|
||||
const { code, data } = await getDomainSchemaRela(params.domainId);
|
||||
if (code === 200) {
|
||||
if (data) {
|
||||
setDataSourceInfoList(
|
||||
data.map((item: ISemantic.IDomainSchemaRelaItem) => {
|
||||
return item.datasource;
|
||||
return item.model;
|
||||
}),
|
||||
);
|
||||
const graphRootData = changeGraphData(data);
|
||||
@@ -160,11 +174,69 @@ const DomainManger: React.FC<Props> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const getRelationConfig = async (domainId: number) => {
|
||||
const { code, data, msg } = await getViewInfoList(domainId);
|
||||
if (code === 200) {
|
||||
const target = data[0];
|
||||
if (target) {
|
||||
const { config } = target;
|
||||
const parseConfig = jsonParse(config, []);
|
||||
setRelationConfig(target);
|
||||
parseConfig.forEach((item) => {
|
||||
graphRef?.current?.addItem('edge', item);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
message.error(msg);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteRelationConfig = async (recordId: number) => {
|
||||
const { code, data, msg } = await deleteViewInfo(recordId);
|
||||
if (code === 200) {
|
||||
} else {
|
||||
message.error(msg);
|
||||
}
|
||||
};
|
||||
|
||||
const saveRelationConfig = async (domainId: number, graphData: any) => {
|
||||
const { code, msg } = await createOrUpdateViewInfo({
|
||||
id: relationConfig?.id,
|
||||
// modelId: domainManger.selectModelId,
|
||||
domainId: domainId,
|
||||
type: 'modelEdgeRelation',
|
||||
config: JSON.stringify(graphData),
|
||||
});
|
||||
if (code === 200) {
|
||||
queryModelRelaList(selectDomainId);
|
||||
} else {
|
||||
message.error(msg);
|
||||
}
|
||||
};
|
||||
|
||||
const queryModelRelaList = async (domainId: number) => {
|
||||
const { code, data, msg } = await getModelRelaList(domainId);
|
||||
if (code === 200) {
|
||||
// setRelationData(data);
|
||||
relationDataListRef.current = data;
|
||||
} else {
|
||||
message.error(msg);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteEdge = () => {
|
||||
graphRef.current.removeItem(currentEdgeItem);
|
||||
setCurrentEdgeItem(undefined);
|
||||
saveModelRelationEdges();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
graphLegendDataSourceIds.current = undefined;
|
||||
graphRef.current = null;
|
||||
queryDataSourceList({ modelId });
|
||||
}, [modelId]);
|
||||
queryDataSourceList({ domainId: selectDomainId });
|
||||
queryModelRelaList(selectDomainId);
|
||||
// deleteRelationConfig(16);
|
||||
}, [selectDomainId]);
|
||||
|
||||
// const getLegendDataFilterFunctions = () => {
|
||||
// legendDataRef.current.map((item: any) => {
|
||||
@@ -310,6 +382,13 @@ const DomainManger: React.FC<Props> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const modelRelationDataInit = (fromModelId: number, toModelId: number) => {
|
||||
const targetData = relationDataListRef.current.find((item) => {
|
||||
return item.fromModelId === fromModelId && item.toModelId === toModelId;
|
||||
});
|
||||
setCurrentRelationDataItem(targetData);
|
||||
};
|
||||
|
||||
const handleNodeTypeClick = (nodeData: any) => {
|
||||
setCurrentNodeData(nodeData);
|
||||
setInfoDrawerVisible(true);
|
||||
@@ -399,7 +478,8 @@ const DomainManger: React.FC<Props> = ({
|
||||
|
||||
if (!graph && graphData) {
|
||||
const graphNodeList = flatGraphDataNode(graphData.children);
|
||||
const graphConfigKey = graphNodeList.length > 20 ? 'dendrogram' : 'mindmap';
|
||||
// const graphConfigKey = graphNodeList.length > 20 ? 'dendrogram' : 'mindmap';
|
||||
const graphConfigKey = 'dendrogram';
|
||||
|
||||
// getLegendDataFilterFunctions();
|
||||
const toolbar = initToolBar({ onSearch: handleSeachNode, onClick: handleToolBarClick });
|
||||
@@ -407,9 +487,158 @@ const DomainManger: React.FC<Props> = ({
|
||||
const contextMenu = initContextMenu({
|
||||
onMenuClick: handleContextMenuClick,
|
||||
});
|
||||
// const legend = initLegend({
|
||||
// nodeData: legendDataRef.current,
|
||||
// filterFunctions: { ...legendDataFilterFunctions.current },
|
||||
|
||||
G6.registerNode(
|
||||
'rect-node',
|
||||
{
|
||||
width: 220,
|
||||
height: 80,
|
||||
afterDraw(cfg, group) {
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
r: 8,
|
||||
x: 80 / 2,
|
||||
y: 0,
|
||||
fill: '#fff',
|
||||
stroke: '#5F95FF',
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: `anchor-point`, // the name, for searching by group.find(ele => ele.get('name') === 'anchor-point')
|
||||
anchorPointIdx: 1, // flag the idx of the anchor-point circle
|
||||
links: 0, // cache the number of edges connected to this shape
|
||||
visible: false, // invisible by default, shows up when links > 1 or the node is in showAnchors state
|
||||
draggable: true, // allow to catch the drag events on this shape
|
||||
});
|
||||
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
r: 8,
|
||||
x: -80 / 2,
|
||||
y: 0,
|
||||
fill: '#fff',
|
||||
stroke: '#5F95FF',
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: `anchor-point`, // the name, for searching by group.find(ele => ele.get('name') === 'anchor-point')
|
||||
anchorPointIdx: 2, // flag the idx of the anchor-point circle
|
||||
links: 0, // cache the number of edges connected to this shape
|
||||
visible: false, // invisible by default, shows up when links > 1 or the node is in showAnchors state
|
||||
draggable: true, // allow to catch the drag events on this shape
|
||||
});
|
||||
},
|
||||
setState(name, value, item) {
|
||||
if (name === 'showAnchors') {
|
||||
const anchorPoints = item
|
||||
.getContainer()
|
||||
.findAll((ele) => ele.get('name') === 'anchor-point');
|
||||
anchorPoints.forEach((point) => {
|
||||
if (value || point.get('links') > 0) point.show();
|
||||
else point.hide();
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
'rect',
|
||||
);
|
||||
|
||||
let sourceAnchorIdx: any;
|
||||
let targetAnchorIdx: any;
|
||||
|
||||
// graphRef.current = new G6.TreeGraph({
|
||||
// container: 'semanticGraph',
|
||||
// width,
|
||||
// height,
|
||||
// modes: {
|
||||
// default: [
|
||||
// // {
|
||||
// // type: 'collapse-expand',
|
||||
// // onChange: function onChange(item, collapsed) {
|
||||
// // const data = item!.get('model');
|
||||
// // data.collapsed = collapsed;
|
||||
// // return true;
|
||||
// // },
|
||||
// // },
|
||||
// // 'drag-node',
|
||||
// {
|
||||
// type: 'drag-node',
|
||||
// shouldBegin: (e) => {
|
||||
// if (e.target.get('name') === 'anchor-point') return false;
|
||||
// return true;
|
||||
// },
|
||||
// },
|
||||
// 'drag-canvas',
|
||||
// {
|
||||
// type: 'create-edge',
|
||||
// trigger: 'drag', // set the trigger to be drag to make the create-edge triggered by drag
|
||||
// shouldBegin: (e) => {
|
||||
// // avoid beginning at other shapes on the node
|
||||
// if (e.target && e.target.get('name') !== 'anchor-point') return false;
|
||||
// sourceAnchorIdx = e.target.get('anchorPointIdx');
|
||||
// e.target.set('links', e.target.get('links') + 1); // cache the number of edge connected to this anchor-point circle
|
||||
// return true;
|
||||
// },
|
||||
// shouldEnd: (e) => {
|
||||
// // avoid ending at other shapes on the node
|
||||
// if (e.target && e.target.get('name') !== 'anchor-point') return false;
|
||||
// if (e.target) {
|
||||
// targetAnchorIdx = e.target.get('anchorPointIdx');
|
||||
// e.target.set('links', e.target.get('links') + 1); // cache the number of edge connected to this anchor-point circle
|
||||
// return true;
|
||||
// }
|
||||
// targetAnchorIdx = undefined;
|
||||
// return true;
|
||||
// },
|
||||
// },
|
||||
// // 'activate-relations',
|
||||
// {
|
||||
// type: 'zoom-canvas',
|
||||
// sensitivity: 0.3, // 设置缩放灵敏度,值越小,缩放越不敏感,默认值为 1
|
||||
// },
|
||||
// {
|
||||
// type: 'activate-relations',
|
||||
// trigger: 'mouseenter', // 触发方式,可以是 'mouseenter' 或 'click'
|
||||
// resetSelected: true, // 点击空白处时,是否取消高亮
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// // defaultNode: {
|
||||
// // size: 26,
|
||||
// // anchorPoints: [
|
||||
// // [0, 0.5],
|
||||
// // [1, 0.5],
|
||||
// // ],
|
||||
// // labelCfg: {
|
||||
// // position: 'right',
|
||||
// // offset: 5,
|
||||
// // style: {
|
||||
// // stroke: '#fff',
|
||||
// // lineWidth: 4,
|
||||
// // },
|
||||
// // },
|
||||
// // },
|
||||
|
||||
// // defaultEdge: {
|
||||
// // type: graphConfigMap[graphConfigKey].defaultEdge.type,
|
||||
// // },
|
||||
// defaultNode: {
|
||||
// type: 'rect-node',
|
||||
// style: {
|
||||
// fill: '#eee',
|
||||
// stroke: '#ccc',
|
||||
// },
|
||||
// },
|
||||
// defaultEdge: {
|
||||
// type: 'quadratic',
|
||||
// style: {
|
||||
// stroke: '#F6BD16',
|
||||
// lineWidth: 2,
|
||||
// },
|
||||
// },
|
||||
// layout: {
|
||||
// ...graphConfigMap[graphConfigKey].layout,
|
||||
// },
|
||||
// plugins: [tooltip, toolbar, contextMenu],
|
||||
// // plugins: [legend, tooltip, toolbar, contextMenu],
|
||||
// });
|
||||
|
||||
graphRef.current = new G6.TreeGraph({
|
||||
@@ -418,17 +647,38 @@ const DomainManger: React.FC<Props> = ({
|
||||
height,
|
||||
modes: {
|
||||
default: [
|
||||
// {
|
||||
// type: 'collapse-expand',
|
||||
// onChange: function onChange(item, collapsed) {
|
||||
// const data = item!.get('model');
|
||||
// data.collapsed = collapsed;
|
||||
// return true;
|
||||
// },
|
||||
// },
|
||||
'drag-node',
|
||||
// config the shouldBegin for drag-node to avoid node moving while dragging on the anchor-point circles
|
||||
{
|
||||
type: 'drag-node',
|
||||
shouldBegin: (e) => {
|
||||
if (e.target.get('name') === 'anchor-point') return false;
|
||||
return true;
|
||||
},
|
||||
},
|
||||
'drag-canvas',
|
||||
// 'activate-relations',
|
||||
// config the shouldBegin and shouldEnd to make sure the create-edge is began and ended at anchor-point circles
|
||||
{
|
||||
type: 'create-edge',
|
||||
trigger: 'drag', // set the trigger to be drag to make the create-edge triggered by drag
|
||||
shouldBegin: (e) => {
|
||||
// avoid beginning at other shapes on the node
|
||||
if (e.target && e.target.get('name') !== 'anchor-point') return false;
|
||||
sourceAnchorIdx = e.target.get('anchorPointIdx');
|
||||
e.target.set('links', e.target.get('links') + 1); // cache the number of edge connected to this anchor-point circle
|
||||
return true;
|
||||
},
|
||||
shouldEnd: (e) => {
|
||||
// avoid ending at other shapes on the node
|
||||
if (e.target && e.target.get('name') !== 'anchor-point') return false;
|
||||
if (e.target) {
|
||||
targetAnchorIdx = e.target.get('anchorPointIdx');
|
||||
e.target.set('links', e.target.get('links') + 1); // cache the number of edge connected to this anchor-point circle
|
||||
return true;
|
||||
}
|
||||
targetAnchorIdx = undefined;
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'zoom-canvas',
|
||||
sensitivity: 0.3, // 设置缩放灵敏度,值越小,缩放越不敏感,默认值为 1
|
||||
@@ -440,30 +690,23 @@ const DomainManger: React.FC<Props> = ({
|
||||
},
|
||||
],
|
||||
},
|
||||
layout: {
|
||||
...graphConfigMap[graphConfigKey].layout,
|
||||
},
|
||||
plugins: [tooltip, toolbar, contextMenu],
|
||||
|
||||
defaultNode: {
|
||||
size: 26,
|
||||
anchorPoints: [
|
||||
[0, 0.5],
|
||||
[1, 0.5],
|
||||
],
|
||||
labelCfg: {
|
||||
position: 'right',
|
||||
offset: 5,
|
||||
style: {
|
||||
stroke: '#fff',
|
||||
lineWidth: 4,
|
||||
},
|
||||
type: 'rect-node',
|
||||
style: {
|
||||
fill: '#eee',
|
||||
stroke: '#ccc',
|
||||
},
|
||||
},
|
||||
defaultEdge: {
|
||||
type: graphConfigMap[graphConfigKey].defaultEdge.type,
|
||||
},
|
||||
layout: {
|
||||
...graphConfigMap[graphConfigKey].layout,
|
||||
},
|
||||
plugins: [tooltip, toolbar, contextMenu],
|
||||
// plugins: [legend, tooltip, toolbar, contextMenu],
|
||||
});
|
||||
|
||||
graphRef.current.set('initGraphData', graphData);
|
||||
graphRef.current.set('initDataSource', dataSourceRef.current);
|
||||
|
||||
@@ -496,6 +739,122 @@ const DomainManger: React.FC<Props> = ({
|
||||
label: node.name,
|
||||
});
|
||||
});
|
||||
|
||||
graphRef.current.on('aftercreateedge', (e) => {
|
||||
// update the sourceAnchor and targetAnchor for the newly added edge
|
||||
const model = e?.item?.getModel?.() || {};
|
||||
const { targetAnchor, sourceAnchor } = model;
|
||||
if (!targetAnchor && !sourceAnchor) {
|
||||
graphRef.current.updateItem(e.edge, {
|
||||
sourceAnchor: sourceAnchorIdx,
|
||||
targetAnchor: targetAnchorIdx,
|
||||
label: '模型关系编辑',
|
||||
style: {
|
||||
stroke: '#296df3',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// update the curveOffset for parallel edges
|
||||
// const edges = graphRef.current.save().edges;
|
||||
|
||||
// const savedGraph = graphRef.current.save();
|
||||
// const edges = [];
|
||||
|
||||
// // savedGraph.children.forEach((root) => {
|
||||
// traverse(savedGraph, null);
|
||||
// // });
|
||||
|
||||
// function traverse(node, parent) {
|
||||
// if (Array.isArray(node.children)) {
|
||||
// node.children.forEach((child) => {
|
||||
// if (child.type === 'edge') {
|
||||
// // 假设边的节点类型为 'edge'
|
||||
// edges.push({
|
||||
// source: parent ? parent.id : null,
|
||||
// target: child.id,
|
||||
// // 其他边的属性
|
||||
// });
|
||||
// }
|
||||
// traverse(child, node);
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
// // processParallelEdgesOnAnchorPoint(edges);
|
||||
// graphRef.current.getEdges().forEach((edge, i) => {
|
||||
// graphRef.current.updateItem(edge, {
|
||||
// curveOffset: edges[i].curveOffset,
|
||||
// curvePosition: edges[i].curvePosition,
|
||||
// });
|
||||
// });
|
||||
});
|
||||
|
||||
graphRef.current.on('afteradditem', (e) => {
|
||||
const model = e!.item!.getModel();
|
||||
const { sourceAnchor, targetAnchor } = model;
|
||||
if (e.item && e.item.getType() === 'edge') {
|
||||
if (!sourceAnchor) {
|
||||
graphRef.current.updateItem(e.item, {
|
||||
sourceAnchor: sourceAnchorIdx,
|
||||
});
|
||||
}
|
||||
if (sourceAnchor && targetAnchor) {
|
||||
graphRef.current.updateItem(e.item, {
|
||||
label: '模型关系编辑',
|
||||
style: {
|
||||
stroke: '#296df3',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
graphRef.current.on('afterremoveitem', (e) => {
|
||||
if (e.item && e.item.source && e.item.target) {
|
||||
const sourceNode = graphRef.current.findById(e.item.source);
|
||||
const targetNode = graphRef.current.findById(e.item.target);
|
||||
const { sourceAnchor, targetAnchor } = e.item;
|
||||
if (sourceNode && !isNaN(sourceAnchor)) {
|
||||
const sourceAnchorShape = sourceNode
|
||||
.getContainer()
|
||||
.find(
|
||||
(ele) =>
|
||||
ele.get('name') === 'anchor-point' && ele.get('anchorPointIdx') === sourceAnchor,
|
||||
);
|
||||
sourceAnchorShape.set('links', sourceAnchorShape.get('links') - 1);
|
||||
}
|
||||
if (targetNode && !isNaN(targetAnchor)) {
|
||||
const targetAnchorShape = targetNode
|
||||
.getContainer()
|
||||
.find(
|
||||
(ele) =>
|
||||
ele.get('name') === 'anchor-point' && ele.get('anchorPointIdx') === targetAnchor,
|
||||
);
|
||||
targetAnchorShape.set('links', targetAnchorShape.get('links') - 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
graphRef.current.on('node:mouseenter', (e) => {
|
||||
graphRef.current.setItemState(e.item, 'showAnchors', true);
|
||||
});
|
||||
graphRef.current.on('node:mouseleave', (e) => {
|
||||
graphRef.current.setItemState(e.item, 'showAnchors', false);
|
||||
});
|
||||
graphRef.current.on('node:dragenter', (e) => {
|
||||
graphRef.current.setItemState(e.item, 'showAnchors', true);
|
||||
});
|
||||
graphRef.current.on('node:dragleave', (e) => {
|
||||
graphRef.current.setItemState(e.item, 'showAnchors', false);
|
||||
});
|
||||
graphRef.current.on('node:dragstart', (e) => {
|
||||
graphRef.current.setItemState(e.item, 'showAnchors', true);
|
||||
});
|
||||
graphRef.current.on('node:dragout', (e) => {
|
||||
graphRef.current.setItemState(e.item, 'showAnchors', false);
|
||||
});
|
||||
|
||||
graphRef.current.data(graphData);
|
||||
graphRef.current.render();
|
||||
|
||||
@@ -524,12 +883,34 @@ const DomainManger: React.FC<Props> = ({
|
||||
}
|
||||
});
|
||||
|
||||
graphRef.current.on('edge:click', (e: any) => {
|
||||
const model = e!.item!.getModel();
|
||||
const eleType = e!.item!.getType();
|
||||
const sourceNode = e.item.get('sourceNode');
|
||||
const targetNode = e.item.get('targetNode');
|
||||
if (eleType === 'node' || (eleType === 'edge' && model.sourceAnchor)) {
|
||||
if (sourceNode && targetNode) {
|
||||
const sourceData = sourceNode.getModel();
|
||||
const targetData = targetNode.getModel();
|
||||
setNodeModel({
|
||||
sourceData,
|
||||
targetData,
|
||||
});
|
||||
modelRelationDataInit(sourceData.uid, targetData.uid);
|
||||
}
|
||||
setCurrentEdgeItem(e.item);
|
||||
setModelRelationDrawerOpen(true);
|
||||
}
|
||||
});
|
||||
|
||||
graphRef.current.on('canvas:click', () => {
|
||||
setInfoDrawerVisible(false);
|
||||
setModelRelationDrawerOpen(false);
|
||||
});
|
||||
|
||||
const rootNode = graphRef.current.findById('root');
|
||||
graphRef.current.hideItem(rootNode);
|
||||
getRelationConfig(selectDomainId);
|
||||
if (typeof window !== 'undefined')
|
||||
window.onresize = () => {
|
||||
if (!graphRef.current || graphRef.current.get('destroyed')) return;
|
||||
@@ -541,7 +922,7 @@ const DomainManger: React.FC<Props> = ({
|
||||
|
||||
const updateGraphData = async (params?: { graphShowType?: SemanticNodeType }) => {
|
||||
const graphRootData = await queryDataSourceList({
|
||||
modelId,
|
||||
domainId: selectDomainId,
|
||||
graphShowType: params?.graphShowType,
|
||||
});
|
||||
if (graphRootData) {
|
||||
@@ -556,6 +937,15 @@ const DomainManger: React.FC<Props> = ({
|
||||
graphRef.current.fitView();
|
||||
};
|
||||
|
||||
const saveModelRelationEdges = () => {
|
||||
const edges = graphRef.current.getEdges();
|
||||
const edgesModel = edges.map((edge) => edge.getModel());
|
||||
const modelRelationEdges = edgesModel.filter(
|
||||
(edgeModel) => edgeModel.sourceAnchor && edgeModel.targetAnchor,
|
||||
);
|
||||
saveRelationConfig(selectDomainId, modelRelationEdges);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<GraphLegend
|
||||
@@ -719,6 +1109,26 @@ const DomainManger: React.FC<Props> = ({
|
||||
nodeData={currentNodeData}
|
||||
/>
|
||||
}
|
||||
<ModelRelationFormDrawer
|
||||
domainId={domainManger.selectDomainId}
|
||||
nodeModel={nodeModel}
|
||||
relationData={currentRelationDataItem}
|
||||
onClose={() => {
|
||||
setCurrentRelationDataItem({});
|
||||
setModelRelationDrawerOpen(false);
|
||||
}}
|
||||
onSave={() => {
|
||||
saveModelRelationEdges();
|
||||
setCurrentRelationDataItem({});
|
||||
setModelRelationDrawerOpen(false);
|
||||
}}
|
||||
onDelete={() => {
|
||||
handleDeleteEdge();
|
||||
setCurrentRelationDataItem({});
|
||||
setModelRelationDrawerOpen(false);
|
||||
}}
|
||||
open={modelRelationDrawerOpen}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,817 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { connect } from 'umi';
|
||||
import type { StateType } from '../model';
|
||||
// import { IGroup } from '@antv/g-base';
|
||||
import type { Dispatch } from 'umi';
|
||||
import {
|
||||
typeConfigs,
|
||||
formatterRelationData,
|
||||
loopNodeFindDataSource,
|
||||
getNodeConfigByType,
|
||||
flatGraphDataNode,
|
||||
} from './utils';
|
||||
import { message } from 'antd';
|
||||
import { getDomainSchemaRela } from '../service';
|
||||
import { Item, TreeGraphData, NodeConfig, IItemBaseConfig } from '@antv/g6-core';
|
||||
import initToolBar from './components/ToolBar';
|
||||
import initTooltips from './components/ToolTips';
|
||||
import initContextMenu from './components/ContextMenu';
|
||||
// import initLegend from './components/Legend';
|
||||
import { SemanticNodeType } from '../enum';
|
||||
import G6 from '@antv/g6';
|
||||
import { ISemantic, IDataSource } from '../data';
|
||||
import NodeInfoDrawer from './components/NodeInfoDrawer';
|
||||
import DimensionInfoModal from '../components/DimensionInfoModal';
|
||||
import MetricInfoCreateForm from '../components/MetricInfoCreateForm';
|
||||
import DeleteConfirmModal from './components/DeleteConfirmModal';
|
||||
import ClassDataSourceTypeModal from '../components/ClassDataSourceTypeModal';
|
||||
import GraphToolBar from './components/GraphToolBar';
|
||||
import GraphLegend from './components/GraphLegend';
|
||||
import GraphLegendVisibleModeItem from './components/GraphLegendVisibleModeItem';
|
||||
import { flowRectNodeRegister, cardNodeRegister } from './CustomNodeRegister';
|
||||
|
||||
// import { cloneDeep } from 'lodash';
|
||||
|
||||
type Props = {
|
||||
// graphShowType?: SemanticNodeType;
|
||||
domainManger: StateType;
|
||||
dispatch: Dispatch;
|
||||
};
|
||||
|
||||
flowRectNodeRegister();
|
||||
|
||||
const DomainManger: React.FC<Props> = ({
|
||||
domainManger,
|
||||
// graphShowType = SemanticNodeType.DIMENSION,
|
||||
// graphShowType,
|
||||
dispatch,
|
||||
}) => {
|
||||
const ref = useRef(null);
|
||||
const dataSourceRef = useRef<ISemantic.IDomainSchemaRelaList>([]);
|
||||
const [graphData, setGraphData] = useState<TreeGraphData>();
|
||||
const [createDimensionModalVisible, setCreateDimensionModalVisible] = useState<boolean>(false);
|
||||
const [createMetricModalVisible, setCreateMetricModalVisible] = useState<boolean>(false);
|
||||
const [infoDrawerVisible, setInfoDrawerVisible] = useState<boolean>(false);
|
||||
|
||||
const [currentNodeData, setCurrentNodeData] = useState<any>();
|
||||
|
||||
const legendDataRef = useRef<any[]>([]);
|
||||
const graphRef = useRef<any>(null);
|
||||
// const legendDataFilterFunctions = useRef<any>({});
|
||||
const [dimensionItem, setDimensionItem] = useState<ISemantic.IDimensionItem>();
|
||||
|
||||
const [metricItem, setMetricItem] = useState<ISemantic.IMetricItem>();
|
||||
|
||||
const [nodeDataSource, setNodeDataSource] = useState<any>();
|
||||
|
||||
const [dataSourceInfoList, setDataSourceInfoList] = useState<IDataSource.IDataSourceItem[]>([]);
|
||||
|
||||
const { dimensionList, metricList, selectModelId: modelId, selectDomainId } = domainManger;
|
||||
|
||||
const dimensionListRef = useRef<ISemantic.IDimensionItem[]>([]);
|
||||
const metricListRef = useRef<ISemantic.IMetricItem[]>([]);
|
||||
|
||||
const [confirmModalOpenState, setConfirmModalOpenState] = useState<boolean>(false);
|
||||
const [createDataSourceModalOpen, setCreateDataSourceModalOpen] = useState(false);
|
||||
|
||||
const visibleModeOpenRef = useRef<boolean>(false);
|
||||
const [visibleModeOpen, setVisibleModeOpen] = useState<boolean>(false);
|
||||
|
||||
const graphShowTypeRef = useRef<SemanticNodeType>();
|
||||
const [graphShowTypeState, setGraphShowTypeState] = useState<SemanticNodeType>();
|
||||
|
||||
const graphLegendDataSourceIds = useRef<string[]>();
|
||||
|
||||
useEffect(() => {
|
||||
dimensionListRef.current = dimensionList;
|
||||
metricListRef.current = metricList;
|
||||
}, [dimensionList, metricList]);
|
||||
|
||||
const handleSeachNode = (text: string) => {
|
||||
const filterData = dataSourceRef.current.reduce(
|
||||
(data: ISemantic.IDomainSchemaRelaList, item: ISemantic.IDomainSchemaRelaItem) => {
|
||||
const { dimensions, metrics } = item;
|
||||
const dimensionsList = dimensions.filter((dimension) => {
|
||||
return dimension.name.includes(text);
|
||||
});
|
||||
const metricsList = metrics.filter((metric) => {
|
||||
return metric.name.includes(text);
|
||||
});
|
||||
data.push({
|
||||
...item,
|
||||
dimensions: dimensionsList,
|
||||
metrics: metricsList,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
[],
|
||||
);
|
||||
const rootGraphData = changeGraphData(filterData);
|
||||
refreshGraphData(rootGraphData);
|
||||
};
|
||||
|
||||
const changeGraphData = (dataSourceList: ISemantic.IDomainSchemaRelaList): TreeGraphData => {
|
||||
const relationData = formatterRelationData({
|
||||
dataSourceList,
|
||||
type: graphShowTypeRef.current,
|
||||
limit: 20,
|
||||
showDataSourceId: graphLegendDataSourceIds.current,
|
||||
});
|
||||
|
||||
const graphRootData = {
|
||||
id: 'root',
|
||||
name: domainManger.selectDomainName,
|
||||
children: relationData,
|
||||
};
|
||||
return graphRootData;
|
||||
};
|
||||
|
||||
const initLegendData = (graphRootData: TreeGraphData) => {
|
||||
const legendList = graphRootData?.children?.map((item: any) => {
|
||||
const { id, name } = item;
|
||||
return {
|
||||
id,
|
||||
label: name,
|
||||
order: 4,
|
||||
...typeConfigs.datasource,
|
||||
};
|
||||
});
|
||||
legendDataRef.current = legendList as any;
|
||||
};
|
||||
|
||||
const queryDataSourceList = async (params: {
|
||||
domainId: number;
|
||||
graphShowType?: SemanticNodeType;
|
||||
}) => {
|
||||
const { code, data } = await getDomainSchemaRela(params.domainId);
|
||||
if (code === 200) {
|
||||
if (data) {
|
||||
setDataSourceInfoList(
|
||||
data.map((item: ISemantic.IDomainSchemaRelaItem) => {
|
||||
return item.model;
|
||||
}),
|
||||
);
|
||||
const graphRootData = changeGraphData(data);
|
||||
dataSourceRef.current = data;
|
||||
initLegendData(graphRootData);
|
||||
setGraphData(graphRootData);
|
||||
return graphRootData;
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
graphLegendDataSourceIds.current = undefined;
|
||||
graphRef.current = null;
|
||||
queryDataSourceList({ domainId: selectDomainId });
|
||||
}, [selectDomainId]);
|
||||
|
||||
// const getLegendDataFilterFunctions = () => {
|
||||
// legendDataRef.current.map((item: any) => {
|
||||
// const { id } = item;
|
||||
// legendDataFilterFunctions.current = {
|
||||
// ...legendDataFilterFunctions.current,
|
||||
// [id]: (d: any) => {
|
||||
// if (d.legendType === id) {
|
||||
// return true;
|
||||
// }
|
||||
// return false;
|
||||
// },
|
||||
// };
|
||||
// });
|
||||
// };
|
||||
|
||||
// const setAllActiveLegend = (legend: any) => {
|
||||
// const legendCanvas = legend._cfgs.legendCanvas;
|
||||
// if (!legendCanvas) {
|
||||
// return;
|
||||
// }
|
||||
// // 从图例中找出node-group节点;
|
||||
// const group = legendCanvas.find((e: any) => e.get('name') === 'node-group');
|
||||
// // 数据源的图例节点在node-group中的children中;
|
||||
// const groups = group.get('children');
|
||||
// groups.forEach((itemGroup: any) => {
|
||||
// const labelText = itemGroup.find((e: any) => e.get('name') === 'circle-node-text');
|
||||
// // legend中activateLegend事件触发在图例节点的Text上,方法中存在向上溯源的逻辑:const shapeGroup = shape.get('parent');
|
||||
// // 因此复用实例方法时,在这里不能直接将图例节点传入,需要在节点的children中找任意一个元素作为入参;
|
||||
// legend.activateLegend(labelText);
|
||||
// });
|
||||
// };
|
||||
|
||||
const handleContextMenuClickEdit = (item: IItemBaseConfig) => {
|
||||
const targetData = item.model;
|
||||
if (!targetData) {
|
||||
return;
|
||||
}
|
||||
const datasource = loopNodeFindDataSource(item);
|
||||
if (datasource) {
|
||||
setNodeDataSource({
|
||||
...datasource,
|
||||
id: datasource.uid,
|
||||
});
|
||||
}
|
||||
if (targetData.nodeType === SemanticNodeType.DATASOURCE) {
|
||||
setCreateDataSourceModalOpen(true);
|
||||
return;
|
||||
}
|
||||
if (targetData.nodeType === SemanticNodeType.DIMENSION) {
|
||||
const targetItem = dimensionListRef.current.find((item) => item.id === targetData.uid);
|
||||
if (targetItem) {
|
||||
setDimensionItem({ ...targetItem });
|
||||
setCreateDimensionModalVisible(true);
|
||||
} else {
|
||||
message.error('获取维度初始化数据失败');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (targetData.nodeType === SemanticNodeType.METRIC) {
|
||||
const targetItem = metricListRef.current.find((item) => item.id === targetData.uid);
|
||||
if (targetItem) {
|
||||
setMetricItem({ ...targetItem });
|
||||
setCreateMetricModalVisible(true);
|
||||
} else {
|
||||
message.error('获取指标初始化数据失败');
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const handleContextMenuClickCreate = (item: IItemBaseConfig, key: string) => {
|
||||
const datasource = item.model;
|
||||
if (!datasource) {
|
||||
return;
|
||||
}
|
||||
setNodeDataSource({
|
||||
...datasource,
|
||||
id: datasource.uid,
|
||||
});
|
||||
if (key === 'createDimension') {
|
||||
setCreateDimensionModalVisible(true);
|
||||
}
|
||||
if (key === 'createMetric') {
|
||||
setCreateMetricModalVisible(true);
|
||||
}
|
||||
setDimensionItem(undefined);
|
||||
setMetricItem(undefined);
|
||||
};
|
||||
|
||||
const handleContextMenuClickDelete = (item: IItemBaseConfig) => {
|
||||
const targetData = item.model;
|
||||
if (!targetData) {
|
||||
return;
|
||||
}
|
||||
if (targetData.nodeType === SemanticNodeType.DATASOURCE) {
|
||||
setCurrentNodeData({
|
||||
...targetData,
|
||||
id: targetData.uid,
|
||||
});
|
||||
setConfirmModalOpenState(true);
|
||||
return;
|
||||
}
|
||||
if (targetData.nodeType === SemanticNodeType.DIMENSION) {
|
||||
const targetItem = dimensionListRef.current.find((item) => item.id === targetData.uid);
|
||||
if (targetItem) {
|
||||
setCurrentNodeData({ ...targetData, ...targetItem });
|
||||
setConfirmModalOpenState(true);
|
||||
} else {
|
||||
message.error('获取维度初始化数据失败');
|
||||
}
|
||||
}
|
||||
if (targetData.nodeType === SemanticNodeType.METRIC) {
|
||||
const targetItem = metricListRef.current.find((item) => item.id === targetData.uid);
|
||||
if (targetItem) {
|
||||
setCurrentNodeData({ ...targetData, ...targetItem });
|
||||
setConfirmModalOpenState(true);
|
||||
} else {
|
||||
message.error('获取指标初始化数据失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleContextMenuClick = (key: string, item: Item) => {
|
||||
if (!item?._cfg) {
|
||||
return;
|
||||
}
|
||||
switch (key) {
|
||||
case 'edit':
|
||||
case 'editDatasource':
|
||||
handleContextMenuClickEdit(item._cfg);
|
||||
break;
|
||||
case 'delete':
|
||||
case 'deleteDatasource':
|
||||
handleContextMenuClickDelete(item._cfg);
|
||||
break;
|
||||
case 'createDimension':
|
||||
case 'createMetric':
|
||||
handleContextMenuClickCreate(item._cfg, key);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleNodeTypeClick = (nodeData: any) => {
|
||||
setCurrentNodeData(nodeData);
|
||||
setInfoDrawerVisible(true);
|
||||
};
|
||||
|
||||
const graphConfigMap = {
|
||||
dendrogram: {
|
||||
defaultEdge: {
|
||||
type: 'cubic-horizontal',
|
||||
},
|
||||
layout: {
|
||||
type: 'dendrogram',
|
||||
direction: 'LR',
|
||||
animate: false,
|
||||
nodeSep: 200,
|
||||
rankSep: 300,
|
||||
radial: true,
|
||||
},
|
||||
},
|
||||
mindmap: {
|
||||
defaultEdge: {
|
||||
type: 'polyline',
|
||||
},
|
||||
layout: {
|
||||
type: 'mindmap',
|
||||
animate: false,
|
||||
direction: 'H',
|
||||
getHeight: () => {
|
||||
return 50;
|
||||
},
|
||||
getWidth: () => {
|
||||
return 50;
|
||||
},
|
||||
getVGap: () => {
|
||||
return 10;
|
||||
},
|
||||
getHGap: () => {
|
||||
return 50;
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function handleToolBarClick(code: string) {
|
||||
if (code === 'visibleMode') {
|
||||
visibleModeOpenRef.current = !visibleModeOpenRef.current;
|
||||
setVisibleModeOpen(visibleModeOpenRef.current);
|
||||
return;
|
||||
}
|
||||
visibleModeOpenRef.current = false;
|
||||
setVisibleModeOpen(false);
|
||||
}
|
||||
|
||||
const lessNodeZoomRealAndMoveCenter = () => {
|
||||
const bbox = graphRef.current.get('group').getBBox();
|
||||
|
||||
// 计算图形的中心点
|
||||
const centerX = (bbox.minX + bbox.maxX) / 2;
|
||||
const centerY = (bbox.minY + bbox.maxY) / 2;
|
||||
|
||||
// 获取画布的中心点
|
||||
const canvasWidth = graphRef.current.get('width');
|
||||
const canvasHeight = graphRef.current.get('height');
|
||||
const canvasCenterX = canvasWidth / 2;
|
||||
const canvasCenterY = canvasHeight / 2;
|
||||
|
||||
// 计算画布需要移动的距离
|
||||
const dx = canvasCenterX - centerX;
|
||||
const dy = canvasCenterY - centerY;
|
||||
|
||||
// 将画布移动到中心点
|
||||
graphRef.current.translate(dx, dy);
|
||||
|
||||
// 将缩放比例设置为 1,以画布中心点为中心进行缩放
|
||||
graphRef.current.zoomTo(1, { x: canvasCenterX, y: canvasCenterY });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!Array.isArray(graphData?.children)) {
|
||||
return;
|
||||
}
|
||||
const container = document.getElementById('semanticGraph');
|
||||
const width = container!.scrollWidth;
|
||||
const height = container!.scrollHeight || 500;
|
||||
|
||||
const graph = graphRef.current;
|
||||
|
||||
if (!graph && graphData) {
|
||||
const graphNodeList = flatGraphDataNode(graphData.children);
|
||||
const graphConfigKey = graphNodeList.length > 20 ? 'dendrogram' : 'mindmap';
|
||||
|
||||
// getLegendDataFilterFunctions();
|
||||
const toolbar = initToolBar({ onSearch: handleSeachNode, onClick: handleToolBarClick });
|
||||
const tooltip = initTooltips();
|
||||
const contextMenu = initContextMenu({
|
||||
onMenuClick: handleContextMenuClick,
|
||||
});
|
||||
// const legend = initLegend({
|
||||
// nodeData: legendDataRef.current,
|
||||
// filterFunctions: { ...legendDataFilterFunctions.current },
|
||||
// });
|
||||
|
||||
graphRef.current = new G6.Graph({
|
||||
container: 'semanticGraph',
|
||||
width,
|
||||
height,
|
||||
// translate the graph to align the canvas's center, support by v3.5.1
|
||||
fitCenter: true,
|
||||
modes: {
|
||||
default: [
|
||||
// {
|
||||
// type: 'collapse-expand',
|
||||
// onChange: function onChange(item, collapsed) {
|
||||
// const data = item!.get('model');
|
||||
// data.collapsed = collapsed;
|
||||
// return true;
|
||||
// },
|
||||
// },
|
||||
'drag-node',
|
||||
'drag-canvas',
|
||||
// 'activate-relations',
|
||||
{
|
||||
type: 'zoom-canvas',
|
||||
sensitivity: 0.3, // 设置缩放灵敏度,值越小,缩放越不敏感,默认值为 1
|
||||
},
|
||||
{
|
||||
type: 'activate-relations',
|
||||
trigger: 'mouseenter', // 触发方式,可以是 'mouseenter' 或 'click'
|
||||
resetSelected: true, // 点击空白处时,是否取消高亮
|
||||
},
|
||||
],
|
||||
},
|
||||
defaultNode: {
|
||||
type: 'card-node',
|
||||
},
|
||||
});
|
||||
|
||||
// graphRef.current = new G6.TreeGraph({
|
||||
// container: 'semanticGraph',
|
||||
// width,
|
||||
// height,
|
||||
// modes: {
|
||||
// default: [
|
||||
// // {
|
||||
// // type: 'collapse-expand',
|
||||
// // onChange: function onChange(item, collapsed) {
|
||||
// // const data = item!.get('model');
|
||||
// // data.collapsed = collapsed;
|
||||
// // return true;
|
||||
// // },
|
||||
// // },
|
||||
// 'drag-node',
|
||||
// 'drag-canvas',
|
||||
// // 'activate-relations',
|
||||
// {
|
||||
// type: 'zoom-canvas',
|
||||
// sensitivity: 0.3, // 设置缩放灵敏度,值越小,缩放越不敏感,默认值为 1
|
||||
// },
|
||||
// {
|
||||
// type: 'activate-relations',
|
||||
// trigger: 'mouseenter', // 触发方式,可以是 'mouseenter' 或 'click'
|
||||
// resetSelected: true, // 点击空白处时,是否取消高亮
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// defaultNode: {
|
||||
// type: 'card-node',
|
||||
// },
|
||||
// defaultEdge: {
|
||||
// type: 'cubic-horizontal',
|
||||
// style: {
|
||||
// stroke: '#CED4D9',
|
||||
// },
|
||||
// },
|
||||
// // defaultNode: {
|
||||
// // size: 26,
|
||||
// // anchorPoints: [
|
||||
// // [0, 0.5],
|
||||
// // [1, 0.5],
|
||||
// // ],
|
||||
// // labelCfg: {
|
||||
// // position: 'right',
|
||||
// // offset: 5,
|
||||
// // style: {
|
||||
// // stroke: '#fff',
|
||||
// // lineWidth: 4,
|
||||
// // },
|
||||
// // },
|
||||
// // },
|
||||
// // defaultEdge: {
|
||||
// // type: graphConfigMap[graphConfigKey].defaultEdge.type,
|
||||
// // },
|
||||
// layout: {
|
||||
// ...graphConfigMap[graphConfigKey].layout,
|
||||
// },
|
||||
// plugins: [tooltip, toolbar, contextMenu],
|
||||
// // plugins: [legend, tooltip, toolbar, contextMenu],
|
||||
// });
|
||||
|
||||
cardNodeRegister(graphRef.current);
|
||||
|
||||
graphRef.current.set('initGraphData', graphData);
|
||||
graphRef.current.set('initDataSource', dataSourceRef.current);
|
||||
|
||||
// const legendCanvas = legend._cfgs.legendCanvas;
|
||||
|
||||
// legend模式事件方法bindEvents会有点击图例空白清空选中的逻辑,在注册click事件前,先将click事件队列清空;
|
||||
// legend._cfgs.legendCanvas._events.click = [];
|
||||
// legendCanvas.on('click', () => {
|
||||
// // @ts-ignore findLegendItemsByState为Legend的 private方法,忽略ts校验
|
||||
// const activedNodeList = legend.findLegendItemsByState('active');
|
||||
// // 获取当前所有激活节点后进行数据遍历筛选;
|
||||
// const activedNodeIds = activedNodeList.map((item: IGroup) => {
|
||||
// return item.cfg.id;
|
||||
// });
|
||||
// const graphDataClone = cloneDeep(graphData);
|
||||
// const filterGraphDataChildren = Array.isArray(graphDataClone?.children)
|
||||
// ? graphDataClone.children.reduce((children: TreeGraphData[], item: TreeGraphData) => {
|
||||
// if (activedNodeIds.includes(item.id)) {
|
||||
// children.push(item);
|
||||
// }
|
||||
// return children;
|
||||
// }, [])
|
||||
// : [];
|
||||
// graphDataClone.children = filterGraphDataChildren;
|
||||
// refreshGraphData(graphDataClone);
|
||||
// });
|
||||
|
||||
graphRef.current.node(function (node: NodeConfig) {
|
||||
return getNodeConfigByType(node, {
|
||||
label: node.name,
|
||||
});
|
||||
});
|
||||
|
||||
const data = {
|
||||
nodes: [
|
||||
{
|
||||
name: 'cardNodeApp',
|
||||
ip: '127.0.0.1',
|
||||
nodeError: true,
|
||||
dataType: 'root',
|
||||
keyInfo: 'this is a card node info',
|
||||
x: 100,
|
||||
y: 50,
|
||||
},
|
||||
{
|
||||
name: 'cardNodeApp',
|
||||
ip: '127.0.0.1',
|
||||
nodeError: false,
|
||||
dataType: 'subRoot',
|
||||
keyInfo: 'this is sub root',
|
||||
x: 100,
|
||||
y: 150,
|
||||
},
|
||||
{
|
||||
name: 'cardNodeApp',
|
||||
ip: '127.0.0.1',
|
||||
nodeError: false,
|
||||
dataType: 'subRoot',
|
||||
keyInfo: 'this is sub root',
|
||||
x: 100,
|
||||
y: 250,
|
||||
children: [
|
||||
{
|
||||
name: 'sub',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
edges: [],
|
||||
};
|
||||
|
||||
graphRef.current.data(data);
|
||||
// graphRef.current.data(graphData);
|
||||
graphRef.current.render();
|
||||
|
||||
const nodeCount = graphRef.current.getNodes().length;
|
||||
if (nodeCount < 10) {
|
||||
lessNodeZoomRealAndMoveCenter();
|
||||
} else {
|
||||
graphRef.current.fitView([80, 80]);
|
||||
}
|
||||
|
||||
graphRef.current.on('node:click', (evt: any) => {
|
||||
const item = evt.item; // 被操作的节点 item
|
||||
const itemData = item?._cfg?.model;
|
||||
if (itemData) {
|
||||
const { nodeType } = itemData;
|
||||
if (
|
||||
[
|
||||
SemanticNodeType.DIMENSION,
|
||||
SemanticNodeType.METRIC,
|
||||
SemanticNodeType.DATASOURCE,
|
||||
].includes(nodeType)
|
||||
) {
|
||||
handleNodeTypeClick(itemData);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
graphRef.current.on('canvas:click', () => {
|
||||
setInfoDrawerVisible(false);
|
||||
});
|
||||
|
||||
const rootNode = graphRef.current.findById('root');
|
||||
graphRef.current.hideItem(rootNode);
|
||||
if (typeof window !== 'undefined')
|
||||
window.onresize = () => {
|
||||
if (!graphRef.current || graphRef.current.get('destroyed')) return;
|
||||
if (!container || !container.scrollWidth || !container.scrollHeight) return;
|
||||
graphRef.current.changeSize(container.scrollWidth, container.scrollHeight);
|
||||
};
|
||||
}
|
||||
}, [graphData]);
|
||||
|
||||
const updateGraphData = async (params?: { graphShowType?: SemanticNodeType }) => {
|
||||
const graphRootData = await queryDataSourceList({
|
||||
modelId,
|
||||
graphShowType: params?.graphShowType,
|
||||
});
|
||||
if (graphRootData) {
|
||||
refreshGraphData(graphRootData);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshGraphData = (graphRootData: TreeGraphData) => {
|
||||
graphRef.current.changeData(graphRootData);
|
||||
const rootNode = graphRef.current.findById('root');
|
||||
graphRef.current.hideItem(rootNode);
|
||||
graphRef.current.fitView();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<GraphLegend
|
||||
legendOptions={legendDataRef.current}
|
||||
defaultCheckAll={true}
|
||||
onChange={(nodeIds: string[]) => {
|
||||
graphLegendDataSourceIds.current = nodeIds;
|
||||
const rootGraphData = changeGraphData(dataSourceRef.current);
|
||||
refreshGraphData(rootGraphData);
|
||||
}}
|
||||
/>
|
||||
{visibleModeOpen && (
|
||||
<GraphLegendVisibleModeItem
|
||||
value={graphShowTypeState}
|
||||
onChange={(showType) => {
|
||||
graphShowTypeRef.current = showType;
|
||||
setGraphShowTypeState(showType);
|
||||
const rootGraphData = changeGraphData(dataSourceRef.current);
|
||||
refreshGraphData(rootGraphData);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<GraphToolBar
|
||||
onClick={({ eventName }: { eventName: string }) => {
|
||||
setNodeDataSource(undefined);
|
||||
if (eventName === 'createDatabase') {
|
||||
setCreateDataSourceModalOpen(true);
|
||||
}
|
||||
if (eventName === 'createDimension') {
|
||||
setCreateDimensionModalVisible(true);
|
||||
setDimensionItem(undefined);
|
||||
}
|
||||
if (eventName === 'createMetric') {
|
||||
setCreateMetricModalVisible(true);
|
||||
setMetricItem(undefined);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
ref={ref}
|
||||
key={`${modelId}`}
|
||||
id="semanticGraph"
|
||||
style={{ width: '100%', height: 'calc(100vh - 175px)', position: 'relative' }}
|
||||
/>
|
||||
<NodeInfoDrawer
|
||||
nodeData={currentNodeData}
|
||||
placement="right"
|
||||
onClose={() => {
|
||||
setInfoDrawerVisible(false);
|
||||
}}
|
||||
open={infoDrawerVisible}
|
||||
mask={false}
|
||||
getContainer={false}
|
||||
onEditBtnClick={(nodeData: any) => {
|
||||
handleContextMenuClickEdit({ model: nodeData });
|
||||
setInfoDrawerVisible(false);
|
||||
}}
|
||||
onNodeChange={({ eventName }: { eventName: string }) => {
|
||||
updateGraphData();
|
||||
setInfoDrawerVisible(false);
|
||||
if (eventName === SemanticNodeType.METRIC) {
|
||||
dispatch({
|
||||
type: 'domainManger/queryMetricList',
|
||||
payload: {
|
||||
modelId,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (eventName === SemanticNodeType.DIMENSION) {
|
||||
dispatch({
|
||||
type: 'domainManger/queryDimensionList',
|
||||
payload: {
|
||||
modelId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{createDimensionModalVisible && (
|
||||
<DimensionInfoModal
|
||||
modelId={modelId}
|
||||
bindModalVisible={createDimensionModalVisible}
|
||||
dimensionItem={dimensionItem}
|
||||
dataSourceList={nodeDataSource ? [nodeDataSource] : dataSourceInfoList}
|
||||
onSubmit={() => {
|
||||
setCreateDimensionModalVisible(false);
|
||||
updateGraphData();
|
||||
dispatch({
|
||||
type: 'domainManger/queryDimensionList',
|
||||
payload: {
|
||||
modelId,
|
||||
},
|
||||
});
|
||||
}}
|
||||
onCancel={() => {
|
||||
setCreateDimensionModalVisible(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{createMetricModalVisible && (
|
||||
<MetricInfoCreateForm
|
||||
domainId={selectDomainId}
|
||||
modelId={modelId}
|
||||
key={metricItem?.id}
|
||||
datasourceId={nodeDataSource?.id}
|
||||
createModalVisible={createMetricModalVisible}
|
||||
metricItem={metricItem}
|
||||
onSubmit={() => {
|
||||
setCreateMetricModalVisible(false);
|
||||
updateGraphData();
|
||||
dispatch({
|
||||
type: 'domainManger/queryMetricList',
|
||||
payload: {
|
||||
modelId,
|
||||
},
|
||||
});
|
||||
}}
|
||||
onCancel={() => {
|
||||
setCreateMetricModalVisible(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{
|
||||
<ClassDataSourceTypeModal
|
||||
open={createDataSourceModalOpen}
|
||||
onCancel={() => {
|
||||
setNodeDataSource(undefined);
|
||||
setCreateDataSourceModalOpen(false);
|
||||
}}
|
||||
dataSourceItem={nodeDataSource}
|
||||
onSubmit={() => {
|
||||
updateGraphData();
|
||||
}}
|
||||
/>
|
||||
}
|
||||
{
|
||||
<DeleteConfirmModal
|
||||
open={confirmModalOpenState}
|
||||
onOkClick={() => {
|
||||
setConfirmModalOpenState(false);
|
||||
updateGraphData();
|
||||
graphShowTypeState === SemanticNodeType.DIMENSION
|
||||
? dispatch({
|
||||
type: 'domainManger/queryDimensionList',
|
||||
payload: {
|
||||
modelId,
|
||||
},
|
||||
})
|
||||
: dispatch({
|
||||
type: 'domainManger/queryMetricList',
|
||||
payload: {
|
||||
modelId,
|
||||
},
|
||||
});
|
||||
}}
|
||||
onCancelClick={() => {
|
||||
setConfirmModalOpenState(false);
|
||||
}}
|
||||
nodeData={currentNodeData}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default connect(({ domainManger }: { domainManger: StateType }) => ({
|
||||
domainManger,
|
||||
}))(DomainManger);
|
||||
@@ -0,0 +1,490 @@
|
||||
import G6 from '@antv/g6';
|
||||
|
||||
let graph;
|
||||
|
||||
const ERROR_COLOR = '#F5222D';
|
||||
const getNodeConfig = (node) => {
|
||||
if (node.nodeError) {
|
||||
return {
|
||||
basicColor: ERROR_COLOR,
|
||||
fontColor: '#FFF',
|
||||
borderColor: ERROR_COLOR,
|
||||
bgColor: '#E66A6C',
|
||||
};
|
||||
}
|
||||
let config = {
|
||||
basicColor: '#5B8FF9',
|
||||
fontColor: '#5B8FF9',
|
||||
borderColor: '#5B8FF9',
|
||||
bgColor: '#C6E5FF',
|
||||
};
|
||||
switch (node.type) {
|
||||
case 'root': {
|
||||
config = {
|
||||
basicColor: '#E3E6E8',
|
||||
fontColor: 'rgba(0,0,0,0.85)',
|
||||
borderColor: '#E3E6E8',
|
||||
bgColor: '#5b8ff9',
|
||||
};
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return config;
|
||||
};
|
||||
|
||||
const COLLAPSE_ICON = function COLLAPSE_ICON(x, y, r) {
|
||||
return [
|
||||
['M', x - r, y],
|
||||
['a', r, r, 0, 1, 0, r * 2, 0],
|
||||
['a', r, r, 0, 1, 0, -r * 2, 0],
|
||||
['M', x - r + 4, y],
|
||||
['L', x - r + 2 * r - 4, y],
|
||||
];
|
||||
};
|
||||
const EXPAND_ICON = function EXPAND_ICON(x, y, r) {
|
||||
return [
|
||||
['M', x - r, y],
|
||||
['a', r, r, 0, 1, 0, r * 2, 0],
|
||||
['a', r, r, 0, 1, 0, -r * 2, 0],
|
||||
['M', x - r + 4, y],
|
||||
['L', x - r + 2 * r - 4, y],
|
||||
['M', x - r + r, y - r + 4],
|
||||
['L', x, y + r - 4],
|
||||
];
|
||||
};
|
||||
const nodeBasicMethod = {
|
||||
createNodeBox: (group, config, w, h, isRoot) => {
|
||||
/* 最外面的大矩形 */
|
||||
const container = group.addShape('rect', {
|
||||
attrs: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: w,
|
||||
height: h,
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'big-rect-shape',
|
||||
});
|
||||
if (!isRoot) {
|
||||
/* 左边的小圆点 */
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
x: 3,
|
||||
y: h / 2,
|
||||
r: 6,
|
||||
fill: config.basicColor,
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'left-dot-shape',
|
||||
});
|
||||
}
|
||||
/* 矩形 */
|
||||
group.addShape('rect', {
|
||||
attrs: {
|
||||
x: 3,
|
||||
y: 0,
|
||||
width: w - 19,
|
||||
height: h,
|
||||
fill: config.bgColor,
|
||||
stroke: config.borderColor,
|
||||
radius: 2,
|
||||
cursor: 'pointer',
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'rect-shape',
|
||||
});
|
||||
|
||||
/* 左边的粗线 */
|
||||
group.addShape('rect', {
|
||||
attrs: {
|
||||
x: 3,
|
||||
y: 0,
|
||||
width: 3,
|
||||
height: h,
|
||||
fill: config.basicColor,
|
||||
radius: 1.5,
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'left-border-shape',
|
||||
});
|
||||
return container;
|
||||
},
|
||||
/* 生成树上的 marker */
|
||||
createNodeMarker: (group, collapsed, x, y) => {
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
x,
|
||||
y,
|
||||
r: 13,
|
||||
fill: 'rgba(47, 84, 235, 0.05)',
|
||||
opacity: 0,
|
||||
zIndex: -2,
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'collapse-icon-bg',
|
||||
});
|
||||
group.addShape('marker', {
|
||||
attrs: {
|
||||
x,
|
||||
y,
|
||||
r: 7,
|
||||
symbol: collapsed ? EXPAND_ICON : COLLAPSE_ICON,
|
||||
stroke: 'rgba(0,0,0,0.25)',
|
||||
fill: 'rgba(0,0,0,0)',
|
||||
lineWidth: 1,
|
||||
cursor: 'pointer',
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'collapse-icon',
|
||||
});
|
||||
},
|
||||
afterDraw: (cfg, group) => {
|
||||
/* 操作 marker 的背景色显示隐藏 */
|
||||
const icon = group.find((element) => element.get('name') === 'collapse-icon');
|
||||
if (icon) {
|
||||
const bg = group.find((element) => element.get('name') === 'collapse-icon-bg');
|
||||
icon.on('mouseenter', () => {
|
||||
bg.attr('opacity', 1);
|
||||
graph.get('canvas').draw();
|
||||
});
|
||||
icon.on('mouseleave', () => {
|
||||
bg.attr('opacity', 0);
|
||||
graph.get('canvas').draw();
|
||||
});
|
||||
}
|
||||
/* ip 显示 */
|
||||
const ipBox = group.find((element) => element.get('name') === 'ip-box');
|
||||
if (ipBox) {
|
||||
/* ip 复制的几个元素 */
|
||||
const ipLine = group.find((element) => element.get('name') === 'ip-cp-line');
|
||||
const ipBG = group.find((element) => element.get('name') === 'ip-cp-bg');
|
||||
const ipIcon = group.find((element) => element.get('name') === 'ip-cp-icon');
|
||||
const ipCPBox = group.find((element) => element.get('name') === 'ip-cp-box');
|
||||
|
||||
const onMouseEnter = () => {
|
||||
ipLine.attr('opacity', 1);
|
||||
ipBG.attr('opacity', 1);
|
||||
ipIcon.attr('opacity', 1);
|
||||
graph.get('canvas').draw();
|
||||
};
|
||||
const onMouseLeave = () => {
|
||||
ipLine.attr('opacity', 0);
|
||||
ipBG.attr('opacity', 0);
|
||||
ipIcon.attr('opacity', 0);
|
||||
graph.get('canvas').draw();
|
||||
};
|
||||
ipBox.on('mouseenter', () => {
|
||||
onMouseEnter();
|
||||
});
|
||||
ipBox.on('mouseleave', () => {
|
||||
onMouseLeave();
|
||||
});
|
||||
ipCPBox.on('mouseenter', () => {
|
||||
onMouseEnter();
|
||||
});
|
||||
ipCPBox.on('mouseleave', () => {
|
||||
onMouseLeave();
|
||||
});
|
||||
ipCPBox.on('click', () => {});
|
||||
}
|
||||
},
|
||||
setState: (name, value, item) => {
|
||||
const hasOpacityClass = [
|
||||
'ip-cp-line',
|
||||
'ip-cp-bg',
|
||||
'ip-cp-icon',
|
||||
'ip-cp-box',
|
||||
'ip-box',
|
||||
'collapse-icon-bg',
|
||||
];
|
||||
const group = item.getContainer();
|
||||
const childrens = group.get('children');
|
||||
graph.setAutoPaint(false);
|
||||
if (name === 'emptiness') {
|
||||
if (value) {
|
||||
childrens.forEach((shape) => {
|
||||
if (hasOpacityClass.indexOf(shape.get('name')) > -1) {
|
||||
return;
|
||||
}
|
||||
shape.attr('opacity', 0.4);
|
||||
});
|
||||
} else {
|
||||
childrens.forEach((shape) => {
|
||||
if (hasOpacityClass.indexOf(shape.get('name')) > -1) {
|
||||
return;
|
||||
}
|
||||
shape.attr('opacity', 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
graph.setAutoPaint(true);
|
||||
},
|
||||
};
|
||||
|
||||
G6.registerNode('card-node', {
|
||||
draw: (cfg, group) => {
|
||||
const config = getNodeConfig(cfg);
|
||||
const isRoot = cfg.dataType === 'root';
|
||||
const nodeError = cfg.nodeError;
|
||||
/* the biggest rect */
|
||||
const container = nodeBasicMethod.createNodeBox(group, config, 243, 64, isRoot);
|
||||
|
||||
if (cfg.dataType !== 'root') {
|
||||
/* the type text */
|
||||
group.addShape('text', {
|
||||
attrs: {
|
||||
text: cfg.dataType,
|
||||
x: 3,
|
||||
y: -10,
|
||||
fontSize: 12,
|
||||
textAlign: 'left',
|
||||
textBaseline: 'middle',
|
||||
fill: 'rgba(0,0,0,0.65)',
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'type-text-shape',
|
||||
});
|
||||
}
|
||||
|
||||
if (cfg.ip) {
|
||||
/* ip start */
|
||||
/* ipBox */
|
||||
const ipRect = group.addShape('rect', {
|
||||
attrs: {
|
||||
fill: nodeError ? null : '#FFF',
|
||||
stroke: nodeError ? 'rgba(255,255,255,0.65)' : null,
|
||||
radius: 2,
|
||||
cursor: 'pointer',
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'ip-container-shape',
|
||||
});
|
||||
|
||||
/* ip */
|
||||
const ipText = group.addShape('text', {
|
||||
attrs: {
|
||||
text: cfg.ip,
|
||||
x: 0,
|
||||
y: 19,
|
||||
fontSize: 12,
|
||||
textAlign: 'left',
|
||||
textBaseline: 'middle',
|
||||
fill: nodeError ? 'rgba(255,255,255,0.85)' : 'rgba(0,0,0,0.65)',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'ip-text-shape',
|
||||
});
|
||||
|
||||
const ipBBox = ipText.getBBox();
|
||||
/* the distance from the IP to the right is 12px */
|
||||
ipText.attr({
|
||||
x: 224 - 12 - ipBBox.width,
|
||||
});
|
||||
/* ipBox */
|
||||
ipRect.attr({
|
||||
x: 224 - 12 - ipBBox.width - 4,
|
||||
y: ipBBox.minY - 5,
|
||||
width: ipBBox.width + 8,
|
||||
height: ipBBox.height + 10,
|
||||
});
|
||||
|
||||
/* a transparent shape on the IP for click listener */
|
||||
group.addShape('rect', {
|
||||
attrs: {
|
||||
stroke: '',
|
||||
cursor: 'pointer',
|
||||
x: 224 - 12 - ipBBox.width - 4,
|
||||
y: ipBBox.minY - 5,
|
||||
width: ipBBox.width + 8,
|
||||
height: ipBBox.height + 10,
|
||||
fill: '#fff',
|
||||
opacity: 0,
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'ip-box',
|
||||
});
|
||||
|
||||
/* copyIpLine */
|
||||
group.addShape('rect', {
|
||||
attrs: {
|
||||
x: 194,
|
||||
y: 7,
|
||||
width: 1,
|
||||
height: 24,
|
||||
fill: '#E3E6E8',
|
||||
opacity: 0,
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'ip-cp-line',
|
||||
});
|
||||
/* copyIpBG */
|
||||
group.addShape('rect', {
|
||||
attrs: {
|
||||
x: 195,
|
||||
y: 8,
|
||||
width: 22,
|
||||
height: 22,
|
||||
fill: '#FFF',
|
||||
cursor: 'pointer',
|
||||
opacity: 0,
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'ip-cp-bg',
|
||||
});
|
||||
/* copyIpIcon */
|
||||
group.addShape('image', {
|
||||
attrs: {
|
||||
x: 200,
|
||||
y: 13,
|
||||
height: 12,
|
||||
width: 10,
|
||||
img: 'https://os.alipayobjects.com/rmsportal/DFhnQEhHyPjSGYW.png',
|
||||
cursor: 'pointer',
|
||||
opacity: 0,
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'ip-cp-icon',
|
||||
});
|
||||
/* a transparent rect on the icon area for click listener */
|
||||
group.addShape('rect', {
|
||||
attrs: {
|
||||
x: 195,
|
||||
y: 8,
|
||||
width: 22,
|
||||
height: 22,
|
||||
fill: '#FFF',
|
||||
cursor: 'pointer',
|
||||
opacity: 0,
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'ip-cp-box',
|
||||
tooltip: 'Copy the IP',
|
||||
});
|
||||
|
||||
/* ip end */
|
||||
}
|
||||
|
||||
/* name */
|
||||
group.addShape('text', {
|
||||
attrs: {
|
||||
text: cfg.name,
|
||||
x: 19,
|
||||
y: 19,
|
||||
fontSize: 14,
|
||||
fontWeight: 700,
|
||||
textAlign: 'left',
|
||||
textBaseline: 'middle',
|
||||
fill: config.fontColor,
|
||||
cursor: 'pointer',
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'name-text-shape',
|
||||
});
|
||||
|
||||
/* the description text */
|
||||
group.addShape('text', {
|
||||
attrs: {
|
||||
text: cfg.keyInfo,
|
||||
x: 19,
|
||||
y: 45,
|
||||
fontSize: 14,
|
||||
textAlign: 'left',
|
||||
textBaseline: 'middle',
|
||||
fill: config.fontColor,
|
||||
cursor: 'pointer',
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'bottom-text-shape',
|
||||
});
|
||||
|
||||
if (nodeError) {
|
||||
group.addShape('text', {
|
||||
attrs: {
|
||||
x: 191,
|
||||
y: 62,
|
||||
text: '⚠️',
|
||||
fill: '#000',
|
||||
fontSize: 18,
|
||||
},
|
||||
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
|
||||
name: 'error-text-shape',
|
||||
});
|
||||
}
|
||||
|
||||
const hasChildren = cfg.children && cfg.children.length > 0;
|
||||
if (hasChildren) {
|
||||
nodeBasicMethod.createNodeMarker(group, cfg.collapsed, 236, 32);
|
||||
}
|
||||
return container;
|
||||
},
|
||||
afterDraw: nodeBasicMethod.afterDraw,
|
||||
setState: nodeBasicMethod.setState,
|
||||
});
|
||||
|
||||
const container = document.getElementById('container');
|
||||
const width = container.scrollWidth;
|
||||
const height = container.scrollHeight || 500;
|
||||
graph = new G6.Graph({
|
||||
container: 'container',
|
||||
width,
|
||||
height,
|
||||
// translate the graph to align the canvas's center, support by v3.5.1
|
||||
fitCenter: true,
|
||||
modes: {
|
||||
default: ['drag-node'],
|
||||
},
|
||||
defaultNode: {
|
||||
type: 'card-node',
|
||||
},
|
||||
});
|
||||
|
||||
const data = {
|
||||
nodes: [
|
||||
{
|
||||
name: 'cardNodeApp',
|
||||
ip: '127.0.0.1',
|
||||
nodeError: true,
|
||||
dataType: 'root',
|
||||
keyInfo: 'this is a card node info',
|
||||
x: 100,
|
||||
y: 50,
|
||||
},
|
||||
{
|
||||
name: 'cardNodeApp',
|
||||
ip: '127.0.0.1',
|
||||
nodeError: false,
|
||||
dataType: 'subRoot',
|
||||
keyInfo: 'this is sub root',
|
||||
x: 100,
|
||||
y: 150,
|
||||
},
|
||||
{
|
||||
name: 'cardNodeApp',
|
||||
ip: '127.0.0.1',
|
||||
nodeError: false,
|
||||
dataType: 'subRoot',
|
||||
keyInfo: 'this is sub root',
|
||||
x: 100,
|
||||
y: 250,
|
||||
children: [
|
||||
{
|
||||
name: 'sub',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
edges: [],
|
||||
};
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
if (typeof window !== 'undefined')
|
||||
window.onresize = () => {
|
||||
if (!graph || graph.get('destroyed')) return;
|
||||
if (!container || !container.scrollWidth || !container.scrollHeight) return;
|
||||
graph.changeSize(container.scrollWidth, container.scrollHeight);
|
||||
};
|
||||
@@ -72,8 +72,8 @@ export const formatterRelationData = (params: {
|
||||
const { type, dataSourceList, limit, showDataSourceId } = params;
|
||||
const relationData = dataSourceList.reduce(
|
||||
(relationList: TreeGraphData[], item: ISemantic.IDomainSchemaRelaItem) => {
|
||||
const { datasource, dimensions, metrics } = item;
|
||||
const { id } = datasource;
|
||||
const { model, dimensions, metrics } = item;
|
||||
const { id } = model;
|
||||
const dataSourceNodeId = `${SemanticNodeType.DATASOURCE}-${id}`;
|
||||
let childrenList = [];
|
||||
if (type === SemanticNodeType.METRIC) {
|
||||
@@ -89,7 +89,7 @@ export const formatterRelationData = (params: {
|
||||
}
|
||||
if (!showDataSourceId || showDataSourceId.includes(dataSourceNodeId)) {
|
||||
relationList.push({
|
||||
...datasource,
|
||||
...model,
|
||||
legendType: dataSourceNodeId,
|
||||
id: dataSourceNodeId,
|
||||
uid: id,
|
||||
@@ -130,6 +130,24 @@ export const getNodeConfigByType = (nodeData: any, defaultConfig = {}) => {
|
||||
return {
|
||||
...defaultConfig,
|
||||
labelCfg: { position: 'bottom', ...labelCfg },
|
||||
// type: 'rect',
|
||||
size: [80, 40],
|
||||
// linkPoints: {
|
||||
// // top: true,
|
||||
// right: true,
|
||||
// // bottom: true,
|
||||
// left: true,
|
||||
// /* linkPoints' size, 8 by default */
|
||||
// // size: 5,
|
||||
// /* linkPoints' style */
|
||||
// // fill: '#ccc',
|
||||
// // stroke: '#333',
|
||||
// // lineWidth: 2,
|
||||
// },
|
||||
// style: {
|
||||
// fill: '#eee',
|
||||
// stroke: '#ccc',
|
||||
// },
|
||||
};
|
||||
}
|
||||
case SemanticNodeType.DIMENSION:
|
||||
|
||||
@@ -68,10 +68,6 @@ const BindMeasuresTable: React.FC<CreateFormProps> = ({
|
||||
dataIndex: 'agg',
|
||||
title: '算子类型',
|
||||
},
|
||||
{
|
||||
dataIndex: 'datasourceName',
|
||||
title: '所属数据源',
|
||||
},
|
||||
];
|
||||
const renderFooter = () => {
|
||||
return (
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
import type { ActionType, ProColumns } from '@ant-design/pro-table';
|
||||
import ProTable from '@ant-design/pro-table';
|
||||
import { message, Button, Space, Popconfirm } from 'antd';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import type { Dispatch } from 'umi';
|
||||
import { connect } from 'umi';
|
||||
import ClassDataSourceTypeModal from './ClassDataSourceTypeModal';
|
||||
import type { StateType } from '../model';
|
||||
import { getDatasourceList, deleteDatasource } from '../service';
|
||||
import moment from 'moment';
|
||||
|
||||
type Props = {
|
||||
dispatch: Dispatch;
|
||||
domainManger: StateType;
|
||||
};
|
||||
|
||||
const ClassDataSourceTable: React.FC<Props> = ({ dispatch, domainManger }) => {
|
||||
const { selectModelId } = domainManger;
|
||||
const [dataSourceItem, setDataSourceItem] = useState<any>();
|
||||
const [createDataSourceModalOpen, setCreateDataSourceModalOpen] = useState(false);
|
||||
|
||||
const actionRef = useRef<ActionType>();
|
||||
|
||||
const columns: ProColumns[] = [
|
||||
{
|
||||
dataIndex: 'id',
|
||||
title: 'ID',
|
||||
},
|
||||
{
|
||||
dataIndex: 'name',
|
||||
title: '数据源名称',
|
||||
},
|
||||
{
|
||||
dataIndex: 'bizName',
|
||||
title: '英文名称',
|
||||
},
|
||||
{
|
||||
dataIndex: 'createdBy',
|
||||
title: '创建人',
|
||||
},
|
||||
{
|
||||
dataIndex: 'description',
|
||||
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: 100,
|
||||
render: (_, record) => {
|
||||
return (
|
||||
<Space>
|
||||
<a
|
||||
key="datasourceEditBtn"
|
||||
onClick={() => {
|
||||
setDataSourceItem(record);
|
||||
setCreateDataSourceModalOpen(true);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</a>
|
||||
<Popconfirm
|
||||
title="确认删除?"
|
||||
okText="是"
|
||||
cancelText="否"
|
||||
onConfirm={async () => {
|
||||
const { code, msg } = await deleteDatasource(record.id);
|
||||
if (code === 200) {
|
||||
setDataSourceItem(undefined);
|
||||
actionRef.current?.reload();
|
||||
} else {
|
||||
message.error(msg);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<a
|
||||
key="datasourceDeleteBtn"
|
||||
onClick={() => {
|
||||
setDataSourceItem(record);
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</a>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const queryDataSourceList = async (params: any) => {
|
||||
dispatch({
|
||||
type: 'domainManger/setPagination',
|
||||
payload: {
|
||||
...params,
|
||||
},
|
||||
});
|
||||
const { code, data, msg } = await getDatasourceList({ ...params });
|
||||
let resData: any = {};
|
||||
if (code === 200) {
|
||||
resData = {
|
||||
data: data || [],
|
||||
success: true,
|
||||
};
|
||||
} else {
|
||||
message.error(msg);
|
||||
resData = {
|
||||
data: [],
|
||||
total: 0,
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
return resData;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProTable
|
||||
actionRef={actionRef}
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
params={{ modelId: selectModelId }}
|
||||
request={queryDataSourceList}
|
||||
pagination={false}
|
||||
search={false}
|
||||
size="small"
|
||||
options={{ reload: false, density: false, fullScreen: false }}
|
||||
toolBarRender={() => [
|
||||
<Button
|
||||
key="create"
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
setDataSourceItem(undefined);
|
||||
setCreateDataSourceModalOpen(true);
|
||||
}}
|
||||
>
|
||||
创建数据源
|
||||
</Button>,
|
||||
]}
|
||||
/>
|
||||
{createDataSourceModalOpen && (
|
||||
<ClassDataSourceTypeModal
|
||||
open={createDataSourceModalOpen}
|
||||
onCancel={() => {
|
||||
setCreateDataSourceModalOpen(false);
|
||||
}}
|
||||
dataSourceItem={dataSourceItem}
|
||||
onSubmit={() => {
|
||||
actionRef.current?.reload();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default connect(({ domainManger }: { domainManger: StateType }) => ({
|
||||
domainManger,
|
||||
}))(ClassDataSourceTable);
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, Drawer, Result, Modal, Card, Row, Col } from 'antd';
|
||||
import { Drawer, Modal, Card, Row, Col } from 'antd';
|
||||
import { ConsoleSqlOutlined, CoffeeOutlined } from '@ant-design/icons';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import type { Dispatch } from 'umi';
|
||||
@@ -47,7 +47,7 @@ const ClassDataSourceTypeModal: React.FC<Props> = ({
|
||||
setCreateDataSourceModalOpen(open);
|
||||
return;
|
||||
}
|
||||
if (dataSourceItem?.datasourceDetail?.queryType === 'table_query') {
|
||||
if (dataSourceItem?.modelDetail?.queryType === 'table_query') {
|
||||
setDataSourceModalVisible(true);
|
||||
} else {
|
||||
setCreateModalVisible(true);
|
||||
@@ -80,16 +80,16 @@ const ClassDataSourceTypeModal: React.FC<Props> = ({
|
||||
};
|
||||
|
||||
const queryTableColumnListByScript = async (dataSource: IDataSource.IDataSourceItem) => {
|
||||
if (!dataSource) {
|
||||
if (!dataSource?.modelDetail?.sqlQuery) {
|
||||
return;
|
||||
}
|
||||
const { code, data, msg } = await excuteSql({
|
||||
sql: dataSource.datasourceDetail?.sqlQuery,
|
||||
const { code, data } = await excuteSql({
|
||||
sql: dataSource.modelDetail?.sqlQuery,
|
||||
id: dataSource.databaseId,
|
||||
});
|
||||
if (code === 200) {
|
||||
fetchTaskResult(data);
|
||||
setSql(dataSource?.datasourceDetail?.sqlQuery);
|
||||
setSql(dataSource?.modelDetail?.sqlQuery);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -124,7 +124,7 @@ const ClassDataSourceTypeModal: React.FC<Props> = ({
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Meta title="快速创建" description="自动进行数据源可视化创建" />
|
||||
<Meta title="快速创建" description="自动进行模型可视化创建" />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
@@ -143,7 +143,7 @@ const ClassDataSourceTypeModal: React.FC<Props> = ({
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Meta title="SQL脚本" description="自定义SQL脚本创建数据源" />
|
||||
<Meta title="SQL脚本" description="自定义SQL脚本创建模型" />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -207,7 +207,6 @@ const ClassDataSourceTypeModal: React.FC<Props> = ({
|
||||
setSql(sql);
|
||||
setScriptColumns(columns);
|
||||
setCurrentDatabaseId(databaseId);
|
||||
onSubmit?.();
|
||||
setDataSourceEditOpen(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -8,14 +8,14 @@ import type { StateType } from '../model';
|
||||
import { StatusEnum } from '../enum';
|
||||
import { SENSITIVE_LEVEL_ENUM } from '../constant';
|
||||
import {
|
||||
getDatasourceList,
|
||||
getModelList,
|
||||
getDimensionList,
|
||||
deleteDimension,
|
||||
batchUpdateDimensionStatus,
|
||||
} from '../service';
|
||||
import DimensionInfoModal from './DimensionInfoModal';
|
||||
import DimensionValueSettingModal from './DimensionValueSettingModal';
|
||||
import { updateDimension } from '../service';
|
||||
// import { updateDimension } from '../service';
|
||||
import { ISemantic, IDataSource } from '../data';
|
||||
import moment from 'moment';
|
||||
import BatchCtrlDropDownButton from '@/components/BatchCtrlDropDownButton';
|
||||
@@ -27,7 +27,7 @@ type Props = {
|
||||
};
|
||||
|
||||
const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
||||
const { selectModelId: modelId } = domainManger;
|
||||
const { selectModelId: modelId, selectDomainId: domainId } = domainManger;
|
||||
const [createModalVisible, setCreateModalVisible] = useState<boolean>(false);
|
||||
const [dimensionItem, setDimensionItem] = useState<ISemantic.IDimensionItem>();
|
||||
const [dataSourceList, setDataSourceList] = useState<IDataSource.IDataSourceItem[]>([]);
|
||||
@@ -80,7 +80,7 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
||||
};
|
||||
|
||||
const queryDataSourceList = async () => {
|
||||
const { code, data, msg } = await getDatasourceList({ modelId });
|
||||
const { code, data, msg } = await getModelList(domainId);
|
||||
if (code === 200) {
|
||||
setDataSourceList(data);
|
||||
} else {
|
||||
@@ -92,20 +92,20 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
||||
queryDataSourceList();
|
||||
}, [modelId]);
|
||||
|
||||
const updateDimensionStatus = async (dimensionData: ISemantic.IDimensionItem) => {
|
||||
const { code, msg } = await updateDimension(dimensionData);
|
||||
if (code === 200) {
|
||||
actionRef?.current?.reload();
|
||||
dispatch({
|
||||
type: 'domainManger/queryDimensionList',
|
||||
payload: {
|
||||
modelId,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
message.error(msg);
|
||||
};
|
||||
// const updateDimensionStatus = async (dimensionData: ISemantic.IDimensionItem) => {
|
||||
// const { code, msg } = await updateDimension(dimensionData);
|
||||
// if (code === 200) {
|
||||
// actionRef?.current?.reload();
|
||||
// dispatch({
|
||||
// type: 'domainManger/queryDimensionList',
|
||||
// payload: {
|
||||
// modelId,
|
||||
// },
|
||||
// });
|
||||
// return;
|
||||
// }
|
||||
// message.error(msg);
|
||||
// };
|
||||
|
||||
const queryBatchUpdateStatus = async (ids: React.Key[], status: StatusEnum) => {
|
||||
if (Array.isArray(ids) && ids.length === 0) {
|
||||
@@ -213,11 +213,6 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: 'datasourceName',
|
||||
title: '数据源名称',
|
||||
search: false,
|
||||
},
|
||||
{
|
||||
dataIndex: 'createdBy',
|
||||
title: '创建人',
|
||||
@@ -299,6 +294,7 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
||||
title="确认删除?"
|
||||
okText="是"
|
||||
cancelText="否"
|
||||
placement="left"
|
||||
onConfirm={async () => {
|
||||
const { code, msg } = await deleteDimension(record.id);
|
||||
if (code === 200) {
|
||||
@@ -331,29 +327,29 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
||||
},
|
||||
};
|
||||
|
||||
const dropdownButtonItems = [
|
||||
{
|
||||
key: 'batchStart',
|
||||
label: '批量启用',
|
||||
},
|
||||
{
|
||||
key: 'batchStop',
|
||||
label: '批量停用',
|
||||
},
|
||||
{
|
||||
key: 'batchDelete',
|
||||
label: (
|
||||
<Popconfirm
|
||||
title="确定批量删除吗?"
|
||||
onConfirm={() => {
|
||||
queryBatchUpdateStatus(selectedRowKeys, StatusEnum.DELETED);
|
||||
}}
|
||||
>
|
||||
<a>批量删除</a>
|
||||
</Popconfirm>
|
||||
),
|
||||
},
|
||||
];
|
||||
// const dropdownButtonItems = [
|
||||
// {
|
||||
// key: 'batchStart',
|
||||
// label: '批量启用',
|
||||
// },
|
||||
// {
|
||||
// key: 'batchStop',
|
||||
// label: '批量停用',
|
||||
// },
|
||||
// {
|
||||
// key: 'batchDelete',
|
||||
// label: (
|
||||
// <Popconfirm
|
||||
// title="确定批量删除吗?"
|
||||
// onConfirm={() => {
|
||||
// queryBatchUpdateStatus(selectedRowKeys, StatusEnum.DELETED);
|
||||
// }}
|
||||
// >
|
||||
// <a>批量删除</a>
|
||||
// </Popconfirm>
|
||||
// ),
|
||||
// },
|
||||
// ];
|
||||
|
||||
const onMenuClick = (key: string) => {
|
||||
switch (key) {
|
||||
|
||||
@@ -155,20 +155,6 @@ const DimensionInfoModal: React.FC<CreateFormProps> = ({
|
||||
>
|
||||
<Input placeholder="名称不可重复" disabled={isEdit} />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
hidden={isEdit}
|
||||
name="datasourceId"
|
||||
label="所属数据源"
|
||||
rules={[{ required: true, message: '请选择所属数据源' }]}
|
||||
>
|
||||
<Select placeholder="请选择数据源" disabled={isEdit}>
|
||||
{dataSourceList.map((item) => (
|
||||
<Option key={item.id} value={item.id}>
|
||||
{item.name}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem label="别名">
|
||||
<Row>
|
||||
<Col flex="1 1 200px">
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { StateType } from '../model';
|
||||
import TransTypeTag from './TransTypeTag';
|
||||
import TableTitleTooltips from '../components/TableTitleTooltips';
|
||||
import { ISemantic } from '../data';
|
||||
import { getDimensionList } from '../service';
|
||||
import { getDimensionList, getDimensionInModelCluster } from '../service';
|
||||
import { SemanticNodeType, TransType } from '../enum';
|
||||
|
||||
interface RecordType {
|
||||
@@ -45,9 +45,9 @@ const DimensionMetricRelationTableTransfer: React.FC<Props> = ({
|
||||
}, [metricItem, relationsInitialValue]);
|
||||
|
||||
const queryDimensionList = async () => {
|
||||
const { code, data, msg } = await getDimensionList({ modelId: metricItem?.modelId || modelId });
|
||||
if (code === 200 && Array.isArray(data?.list)) {
|
||||
setDimensionList(data.list);
|
||||
const { code, data, msg } = await getDimensionInModelCluster(metricItem?.modelId || modelId);
|
||||
if (code === 200 && Array.isArray(data)) {
|
||||
setDimensionList(data);
|
||||
} else {
|
||||
message.error(msg);
|
||||
}
|
||||
|
||||
@@ -2,11 +2,9 @@ import { Tabs, Breadcrumb, Space } from 'antd';
|
||||
import React from 'react';
|
||||
import { connect, history } from 'umi';
|
||||
|
||||
import ClassDataSourceTable from './ClassDataSourceTable';
|
||||
import ClassDimensionTable from './ClassDimensionTable';
|
||||
import ClassMetricTable from './ClassMetricTable';
|
||||
import PermissionSection from './Permission/PermissionSection';
|
||||
// import EntitySettingSection from './Entity/EntitySettingSection';
|
||||
import ChatSettingSection from '../ChatSetting/ChatSettingSection';
|
||||
import OverView from './OverView';
|
||||
import styles from './style.less';
|
||||
@@ -38,7 +36,7 @@ const DomainManagerTab: React.FC<Props> = ({
|
||||
onBackDomainBtnClick,
|
||||
onMenuChange,
|
||||
}) => {
|
||||
const defaultTabKey = 'xflow';
|
||||
const defaultTabKey = 'dimenstion';
|
||||
const { selectDomainId, domainList, selectModelId, selectModelName, selectDomainName } =
|
||||
domainManger;
|
||||
|
||||
@@ -55,6 +53,15 @@ const DomainManagerTab: React.FC<Props> = ({
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: '画布',
|
||||
key: 'xflow',
|
||||
children: (
|
||||
<div style={{ width: '100%' }}>
|
||||
<SemanticGraphCanvas />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: '权限管理',
|
||||
key: 'permissonSetting',
|
||||
@@ -74,21 +81,6 @@ const DomainManagerTab: React.FC<Props> = ({
|
||||
});
|
||||
|
||||
const isModelItem = [
|
||||
{
|
||||
label: '画布',
|
||||
key: 'xflow',
|
||||
children: (
|
||||
<div style={{ width: '100%' }}>
|
||||
<SemanticGraphCanvas />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
label: '数据源',
|
||||
key: 'dataSource',
|
||||
children: <ClassDataSourceTable />,
|
||||
},
|
||||
{
|
||||
label: '维度',
|
||||
key: 'dimenstion',
|
||||
@@ -99,11 +91,6 @@ const DomainManagerTab: React.FC<Props> = ({
|
||||
key: 'metric',
|
||||
children: <ClassMetricTable />,
|
||||
},
|
||||
// {
|
||||
// label: '实体',
|
||||
// key: 'entity',
|
||||
// children: <EntitySettingSection />,
|
||||
// },
|
||||
{
|
||||
label: '权限管理',
|
||||
key: 'permissonSetting',
|
||||
|
||||
@@ -22,7 +22,7 @@ import { SENSITIVE_LEVEL_OPTIONS } from '../constant';
|
||||
import { formLayout } from '@/components/FormHelper/utils';
|
||||
import FormItemTitle from '@/components/FormHelper/FormItemTitle';
|
||||
import styles from './style.less';
|
||||
import { getMeasureListByModelId } from '../service';
|
||||
import { getMeasureListByModelId, getModelDetail } from '../service';
|
||||
import DimensionAndMetricRelationModal from './DimensionAndMetricRelationModal';
|
||||
import TableTitleTooltips from '../components/TableTitleTooltips';
|
||||
import { creatExprMetric, updateExprMetric, mockMetricAlias, getMetricTags } from '../service';
|
||||
@@ -88,16 +88,19 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
|
||||
const backward = () => setCurrentStep(currentStep - 1);
|
||||
|
||||
const queryClassMeasureList = async () => {
|
||||
const { code, data } = await getMeasureListByModelId(modelId);
|
||||
// const { code, data } = await getMeasureListByModelId(modelId);
|
||||
const { code, data } = await getModelDetail({ modelId });
|
||||
if (code === 200) {
|
||||
setClassMeasureList(data);
|
||||
if (datasourceId) {
|
||||
const hasMeasures = data.some(
|
||||
(item: ISemantic.IMeasure) => item.datasourceId === datasourceId,
|
||||
);
|
||||
setHasMeasuresState(hasMeasures);
|
||||
if (Array.isArray(data?.modelDetail?.measures)) {
|
||||
setClassMeasureList(data);
|
||||
if (datasourceId) {
|
||||
const hasMeasures = data.some(
|
||||
(item: ISemantic.IMeasure) => item.datasourceId === datasourceId,
|
||||
);
|
||||
setHasMeasuresState(hasMeasures);
|
||||
}
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
setClassMeasureList([]);
|
||||
};
|
||||
|
||||
@@ -96,7 +96,7 @@ const ModelCreateFormModal: React.FC<ModelCreateFormModalProps> = (props) => {
|
||||
return (
|
||||
<Modal
|
||||
width={640}
|
||||
styles={{ padding: '32px 40px 48px' }}
|
||||
// styles={{ padding: '32px 40px 48px' }}
|
||||
destroyOnClose
|
||||
title={'模型信息'}
|
||||
open={true}
|
||||
@@ -125,7 +125,18 @@ const ModelCreateFormModal: React.FC<ModelCreateFormModalProps> = (props) => {
|
||||
>
|
||||
<Input placeholder="请输入模型英文名称" />
|
||||
</FormItem>
|
||||
<FormItem name="alias" label="别名">
|
||||
<FormItem
|
||||
name="alias"
|
||||
label="别名"
|
||||
getValueFromEvent={(value) => {
|
||||
return value.join(',');
|
||||
}}
|
||||
getValueProps={(value) => {
|
||||
return {
|
||||
value: value.split(','),
|
||||
};
|
||||
}}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
placeholder="输入别名后回车确认,多别名输入、复制粘贴支持英文逗号自动分隔"
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { Dispatch } from 'umi';
|
||||
import { connect } from 'umi';
|
||||
import type { StateType } from '../model';
|
||||
import { deleteModel, updateModel } from '../service';
|
||||
import ClassDataSourceTypeModal from './ClassDataSourceTypeModal';
|
||||
|
||||
import ModelCreateFormModal from './ModelCreateFormModal';
|
||||
|
||||
@@ -33,6 +34,7 @@ const ModelTable: React.FC<Props> = ({
|
||||
const [modelCreateFormModalVisible, setModelCreateFormModalVisible] = useState<boolean>(false);
|
||||
const [modelItem, setModelItem] = useState<ISemantic.IModelItem>();
|
||||
const [saveLoading, setSaveLoading] = useState<boolean>(false);
|
||||
const [createDataSourceModalOpen, setCreateDataSourceModalOpen] = useState(false);
|
||||
const actionRef = useRef<ActionType>();
|
||||
|
||||
const updateModelStatus = async (modelData: ISemantic.IModelItem) => {
|
||||
@@ -141,7 +143,8 @@ const ModelTable: React.FC<Props> = ({
|
||||
key="metricEditBtn"
|
||||
onClick={() => {
|
||||
setModelItem(record);
|
||||
setModelCreateFormModalVisible(true);
|
||||
// setModelCreateFormModalVisible(true);
|
||||
setCreateDataSourceModalOpen(true);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
@@ -217,7 +220,9 @@ const ModelTable: React.FC<Props> = ({
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
setModelItem(undefined);
|
||||
setModelCreateFormModalVisible(true);
|
||||
// setModelCreateFormModalVisible(true);
|
||||
|
||||
setCreateDataSourceModalOpen(true);
|
||||
}}
|
||||
>
|
||||
创建模型
|
||||
@@ -225,6 +230,20 @@ const ModelTable: React.FC<Props> = ({
|
||||
]
|
||||
}
|
||||
/>
|
||||
{createDataSourceModalOpen && (
|
||||
<ClassDataSourceTypeModal
|
||||
open={createDataSourceModalOpen}
|
||||
dataSourceItem={modelItem}
|
||||
onSubmit={() => {
|
||||
// actionRef.current?.reload();
|
||||
// setCreateDataSourceModalOpen(false);
|
||||
onModelChange?.();
|
||||
}}
|
||||
onCancel={() => {
|
||||
setCreateDataSourceModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{modelCreateFormModalVisible && (
|
||||
<ModelCreateFormModal
|
||||
domainId={selectDomainId}
|
||||
|
||||
@@ -235,6 +235,7 @@
|
||||
|
||||
|
||||
.classTable {
|
||||
// padding: 0 20px;
|
||||
:global {
|
||||
.ant-pro-table-search-query-filter {
|
||||
margin-bottom: 0;
|
||||
@@ -253,6 +254,9 @@
|
||||
.ant-table-selection-column {
|
||||
text-align: left;
|
||||
}
|
||||
.ant-pro-card-body {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ export declare namespace IDataSource {
|
||||
sensitiveLevel: SensitiveLevel;
|
||||
domainId: number;
|
||||
databaseId: number;
|
||||
datasourceDetail: IDataSourceDetail;
|
||||
modelDetail: IDataSourceDetail;
|
||||
}
|
||||
type IDataSourceList = IDataSourceItem[];
|
||||
}
|
||||
@@ -237,7 +237,7 @@ export declare namespace ISemantic {
|
||||
domainId: number;
|
||||
dimensions: IDimensionList;
|
||||
metrics: IMetricList;
|
||||
datasource: IDataSourceItem;
|
||||
model: IDataSourceItem;
|
||||
}
|
||||
type IDomainSchemaRelaList = IDomainSchemaRelaItem[];
|
||||
|
||||
|
||||
@@ -12,10 +12,6 @@ export function getDomainList(): Promise<any> {
|
||||
return request.get(`${process.env.API_BASE_URL}domain/getDomainList`);
|
||||
}
|
||||
|
||||
export function getDatasourceList(data: any): Promise<any> {
|
||||
return request.get(`${process.env.API_BASE_URL}datasource/getDatasourceList/${data.modelId}`);
|
||||
}
|
||||
|
||||
export function getDomainDetail(data: any): Promise<any> {
|
||||
return request.get(`${process.env.API_BASE_URL}domain/getDomain/${data.modelId}`);
|
||||
}
|
||||
@@ -61,6 +57,10 @@ export function getDimensionList(data: any): Promise<any> {
|
||||
return request.post(`${process.env.API_BASE_URL}dimension/queryDimension`, queryParams);
|
||||
}
|
||||
|
||||
export function getDimensionInModelCluster(modelId: number): Promise<any> {
|
||||
return request.get(`${process.env.API_BASE_URL}dimension/getDimensionInModelCluster/${modelId}`);
|
||||
}
|
||||
|
||||
export function createDimension(data: any): Promise<any> {
|
||||
return request.post(`${process.env.API_BASE_URL}dimension/createDimension`, {
|
||||
data,
|
||||
@@ -252,6 +252,29 @@ export function createOrUpdateDatasourceRela(data: any): Promise<any> {
|
||||
});
|
||||
}
|
||||
|
||||
export function createOrUpdateModelRela(data: any): Promise<any> {
|
||||
return request(`${process.env.API_BASE_URL}modelRela`, {
|
||||
method: data?.id ? 'PUT' : 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteModelRela(id: any): Promise<any> {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
return request(`${process.env.API_BASE_URL}modelRela/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
export function getModelRelaList(domainId: number): Promise<any> {
|
||||
return request(`${process.env.API_BASE_URL}modelRela/list`, {
|
||||
method: 'GET',
|
||||
params: { domainId },
|
||||
});
|
||||
}
|
||||
|
||||
export function createOrUpdateViewInfo(data: any): Promise<any> {
|
||||
return request(`${process.env.API_BASE_URL}viewInfo/createOrUpdateViewInfo`, {
|
||||
method: 'POST',
|
||||
@@ -265,6 +288,12 @@ export function getViewInfoList(domainId: number): Promise<any> {
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteViewInfo(recordId: any): Promise<any> {
|
||||
return request(`${process.env.API_BASE_URL}viewInfo/deleteViewInfo/${recordId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteDatasourceRela(domainId: any): Promise<any> {
|
||||
return request(`${process.env.API_BASE_URL}viewInfo/deleteDatasourceRela/${domainId}`, {
|
||||
method: 'DELETE',
|
||||
@@ -418,7 +447,7 @@ export function queryDimValue(data: any): Promise<any> {
|
||||
}
|
||||
|
||||
export async function queryStruct({
|
||||
modelId,
|
||||
modelIds,
|
||||
bizName,
|
||||
dateField = 'sys_imp_date',
|
||||
startDate,
|
||||
@@ -427,7 +456,7 @@ export async function queryStruct({
|
||||
groups = [],
|
||||
dimensionFilters = [],
|
||||
}: {
|
||||
modelId: number;
|
||||
modelIds: number[];
|
||||
bizName: string;
|
||||
dateField: string;
|
||||
startDate: string;
|
||||
@@ -442,7 +471,7 @@ export async function queryStruct({
|
||||
method: 'POST',
|
||||
...(download ? { responseType: 'blob', getResponse: true } : {}),
|
||||
data: {
|
||||
modelId,
|
||||
modelIds,
|
||||
groups: [dateField, ...groups],
|
||||
dimensionFilters,
|
||||
aggregators: [
|
||||
|
||||
Reference in New Issue
Block a user