[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:
tristanliu
2023-11-27 21:26:55 +08:00
committed by GitHub
parent 52eca178d3
commit 27ebda3439
32 changed files with 3134 additions and 437 deletions

View File

@@ -224,6 +224,7 @@ ol {
min-width: 100px;
h3 {
padding-bottom: 5px;
margin: 0;
border-bottom: 1px solid #4E86F5;
}
li {

View File

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

View File

@@ -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()}

View File

@@ -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="度量算子"

View File

@@ -63,7 +63,7 @@ const SqlSide: React.FC<Props> = ({ initialValues, onSubmitSuccess }) => {
useEffect(() => {
if (initialValues) {
updateTabSql(initialValues?.datasourceDetail?.sqlQuery || '', '数据源查询');
updateTabSql(initialValues?.modelDetail?.sqlQuery || '', '数据源查询');
}
}, [initialValues]);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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: [

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -68,10 +68,6 @@ const BindMeasuresTable: React.FC<CreateFormProps> = ({
dataIndex: 'agg',
title: '算子类型',
},
{
dataIndex: 'datasourceName',
title: '所属数据源',
},
];
const renderFooter = () => {
return (

View File

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

View File

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

View File

@@ -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) {

View File

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

View File

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

View File

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

View File

@@ -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([]);
};

View File

@@ -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="输入别名后回车确认,多别名输入、复制粘贴支持英文逗号自动分隔"

View File

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

View File

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

View File

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

View File

@@ -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: [