mirror of
https://github.com/tencentmusic/supersonic.git
synced 2025-12-10 19:51:00 +00:00
[improvement][semantic-fe] Added an editing component to set filtering rules for Q&A. Now, the SQL editor will be accompanied by a list for display and control, to resolve ambiguity when using comma-separated values.
[improvement][semantic-fe] Improved validation logic and prompt copywriting for data source/dimension/metric editing and creation. [improvement][semantic-fe] Improved user experience for visual modeling. Now, when using the legend to control the display/hide of data sources and their associated metric dimensions, the canvas will be re-layout based on the activated data source in the legend. Co-authored-by: tristanliu <tristanliu@tencent.com>
This commit is contained in:
@@ -91,6 +91,7 @@
|
|||||||
"react-ace": "^9.4.1",
|
"react-ace": "^9.4.1",
|
||||||
"react-dev-inspector": "^1.8.4",
|
"react-dev-inspector": "^1.8.4",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
|
"react-fast-marquee": "^1.6.0",
|
||||||
"react-helmet-async": "^1.0.4",
|
"react-helmet-async": "^1.0.4",
|
||||||
"react-spinners": "^0.10.6",
|
"react-spinners": "^0.10.6",
|
||||||
"react-split-pane": "^2.0.3",
|
"react-split-pane": "^2.0.3",
|
||||||
|
|||||||
@@ -216,20 +216,30 @@ ol {
|
|||||||
right: 240px !important;
|
right: 240px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.g6ContextMenuContainer {
|
.g6ContextMenuContainer {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #545454;
|
color: #545454;
|
||||||
}
|
min-width: 100px;
|
||||||
.g6ContextMenuContainer li {
|
h3 {
|
||||||
|
padding-bottom: 5px;
|
||||||
|
border-bottom: 1px solid #4E86F5;
|
||||||
|
}
|
||||||
|
li {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
list-style-type:none;
|
list-style-type:none;
|
||||||
list-style: none;
|
// list-style: none;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
&:hover {
|
||||||
.g6ContextMenuContainer ul {
|
color: #4E86F5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ul {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
margin: 0;
|
||||||
.g6ContextMenuContainer li:hover {
|
}
|
||||||
color: #aaa;
|
.ant-tag {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,7 @@ export default {
|
|||||||
'menu.exception.not-permission': '403',
|
'menu.exception.not-permission': '403',
|
||||||
'menu.exception.not-find': '404',
|
'menu.exception.not-find': '404',
|
||||||
'menu.exception.server-error': '500',
|
'menu.exception.server-error': '500',
|
||||||
'menu.semanticModel': '语义建模',
|
'menu.semanticModel': '模型管理',
|
||||||
'menu.chatSetting': '问答设置',
|
'menu.chatSetting': '问答设置',
|
||||||
'menu.login': '登录',
|
'menu.login': '登录',
|
||||||
'menu.chat': '问答对话',
|
'menu.chat': '问答对话',
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { createDatasource, updateDatasource, getColumns } from '../../service';
|
|||||||
import type { Dispatch } from 'umi';
|
import type { Dispatch } from 'umi';
|
||||||
import type { StateType } from '../../model';
|
import type { StateType } from '../../model';
|
||||||
import { connect } from 'umi';
|
import { connect } from 'umi';
|
||||||
|
import { isUndefined } from 'lodash';
|
||||||
|
|
||||||
export type CreateFormProps = {
|
export type CreateFormProps = {
|
||||||
domainManger: StateType;
|
domainManger: StateType;
|
||||||
@@ -47,6 +48,7 @@ const DataSourceCreateForm: React.FC<CreateFormProps> = ({
|
|||||||
const [fields, setFields] = useState<any[]>([]);
|
const [fields, setFields] = useState<any[]>([]);
|
||||||
const [currentStep, setCurrentStep] = useState(0);
|
const [currentStep, setCurrentStep] = useState(0);
|
||||||
const [saveLoading, setSaveLoading] = useState(false);
|
const [saveLoading, setSaveLoading] = useState(false);
|
||||||
|
const [hasEmptyNameField, setHasEmptyNameField] = useState<boolean>(false);
|
||||||
const formValRef = useRef(initFormVal as any);
|
const formValRef = useRef(initFormVal as any);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const { dataBaseConfig } = domainManger;
|
const { dataBaseConfig } = domainManger;
|
||||||
@@ -54,6 +56,17 @@ const DataSourceCreateForm: React.FC<CreateFormProps> = ({
|
|||||||
formValRef.current = val;
|
formValRef.current = val;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const hasEmpty = fields.some((item) => {
|
||||||
|
const { name, isCreateDimension, isCreateMetric } = item;
|
||||||
|
if ((isCreateMetric || isCreateDimension) && !name) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
setHasEmptyNameField(hasEmpty);
|
||||||
|
}, [fields]);
|
||||||
|
|
||||||
const [fieldColumns, setFieldColumns] = useState(scriptColumns || []);
|
const [fieldColumns, setFieldColumns] = useState(scriptColumns || []);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (scriptColumns) {
|
if (scriptColumns) {
|
||||||
@@ -310,7 +323,12 @@ const DataSourceCreateForm: React.FC<CreateFormProps> = ({
|
|||||||
上一步
|
上一步
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={onCancel}>取消</Button>
|
<Button onClick={onCancel}>取消</Button>
|
||||||
<Button type="primary" loading={saveLoading} onClick={handleNext}>
|
<Button
|
||||||
|
type="primary"
|
||||||
|
loading={saveLoading}
|
||||||
|
onClick={handleNext}
|
||||||
|
disabled={hasEmptyNameField}
|
||||||
|
>
|
||||||
完成
|
完成
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,8 +1,21 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Table, Select, Checkbox, Input } from 'antd';
|
import { Table, Select, Checkbox, Input, Alert, Space, Tooltip } from 'antd';
|
||||||
import type { FieldItem } from '../data';
|
import TableTitleTooltips from '../../components/TableTitleTooltips';
|
||||||
import { isUndefined } from 'lodash';
|
import { isUndefined } from 'lodash';
|
||||||
|
import { ExclamationCircleOutlined } from '@ant-design/icons';
|
||||||
|
import Marquee from 'react-fast-marquee';
|
||||||
import { TYPE_OPTIONS, DATE_FORMATTER, AGG_OPTIONS, EnumDataSourceType } from '../constants';
|
import { TYPE_OPTIONS, DATE_FORMATTER, AGG_OPTIONS, EnumDataSourceType } from '../constants';
|
||||||
|
import styles from '../style.less';
|
||||||
|
|
||||||
|
type FieldItem = {
|
||||||
|
bizName: string;
|
||||||
|
sqlType: string;
|
||||||
|
name: string;
|
||||||
|
type: EnumDataSourceType;
|
||||||
|
agg?: string;
|
||||||
|
checked?: number;
|
||||||
|
dateFormat?: string;
|
||||||
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
fields: FieldItem[];
|
fields: FieldItem[];
|
||||||
@@ -11,6 +24,16 @@ type Props = {
|
|||||||
|
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
|
|
||||||
|
const getCreateFieldName = (type: EnumDataSourceType) => {
|
||||||
|
const isCreateName = [EnumDataSourceType.CATEGORICAL, EnumDataSourceType.TIME].includes(
|
||||||
|
type as EnumDataSourceType,
|
||||||
|
)
|
||||||
|
? 'isCreateDimension'
|
||||||
|
: 'isCreateMetric';
|
||||||
|
return isCreateName;
|
||||||
|
// const editState = !isUndefined(record[isCreateName]) ? !!record[isCreateName] : true;
|
||||||
|
};
|
||||||
|
|
||||||
const FieldForm: React.FC<Props> = ({ fields, onFieldChange }) => {
|
const FieldForm: React.FC<Props> = ({ fields, onFieldChange }) => {
|
||||||
const handleFieldChange = (record: FieldItem, fieldName: string, value: any) => {
|
const handleFieldChange = (record: FieldItem, fieldName: string, value: any) => {
|
||||||
onFieldChange(record.bizName, {
|
onFieldChange(record.bizName, {
|
||||||
@@ -58,10 +81,14 @@ const FieldForm: React.FC<Props> = ({ fields, onFieldChange }) => {
|
|||||||
timeGranularity: undefined,
|
timeGranularity: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
const isCreateName = getCreateFieldName(value);
|
||||||
|
const editState = !isUndefined(record[isCreateName]) ? !!record[isCreateName] : true;
|
||||||
// handleFieldChange(record, 'type', value);
|
// handleFieldChange(record, 'type', value);
|
||||||
onFieldChange(record.bizName, {
|
onFieldChange(record.bizName, {
|
||||||
...record,
|
...record,
|
||||||
type: value,
|
type: value,
|
||||||
|
name: '',
|
||||||
|
[isCreateName]: editState,
|
||||||
...defaultParams,
|
...defaultParams,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -105,6 +132,7 @@ const FieldForm: React.FC<Props> = ({ fields, onFieldChange }) => {
|
|||||||
if (type === EnumDataSourceType.TIME) {
|
if (type === EnumDataSourceType.TIME) {
|
||||||
const dateFormat = fields.find((field) => field.bizName === record.bizName)?.dateFormat;
|
const dateFormat = fields.find((field) => field.bizName === record.bizName)?.dateFormat;
|
||||||
return (
|
return (
|
||||||
|
<Space>
|
||||||
<Select
|
<Select
|
||||||
placeholder="时间格式"
|
placeholder="时间格式"
|
||||||
value={dateFormat}
|
value={dateFormat}
|
||||||
@@ -112,7 +140,7 @@ const FieldForm: React.FC<Props> = ({ fields, onFieldChange }) => {
|
|||||||
handleFieldChange(record, 'dateFormat', value);
|
handleFieldChange(record, 'dateFormat', value);
|
||||||
}}
|
}}
|
||||||
defaultValue={DATE_FORMATTER[0]}
|
defaultValue={DATE_FORMATTER[0]}
|
||||||
style={{ width: '100%' }}
|
style={{ minWidth: 180 }}
|
||||||
>
|
>
|
||||||
{DATE_FORMATTER.map((item) => (
|
{DATE_FORMATTER.map((item) => (
|
||||||
<Option key={item} value={item}>
|
<Option key={item} value={item}>
|
||||||
@@ -120,13 +148,22 @@ const FieldForm: React.FC<Props> = ({ fields, onFieldChange }) => {
|
|||||||
</Option>
|
</Option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
|
<Tooltip title="请选择数据库中时间字段对应格式">
|
||||||
|
<ExclamationCircleOutlined />
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <></>;
|
return <></>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '快速创建',
|
title: (
|
||||||
|
<TableTitleTooltips
|
||||||
|
title="快速创建"
|
||||||
|
tooltips="若勾选快速创建并填写名称,将会把该维度/指标直接创建到维度/指标列表"
|
||||||
|
/>
|
||||||
|
),
|
||||||
dataIndex: 'fastCreate',
|
dataIndex: 'fastCreate',
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (_: any, record: FieldItem) => {
|
render: (_: any, record: FieldItem) => {
|
||||||
@@ -140,11 +177,7 @@ const FieldForm: React.FC<Props> = ({ fields, onFieldChange }) => {
|
|||||||
EnumDataSourceType.MEASURES,
|
EnumDataSourceType.MEASURES,
|
||||||
].includes(type as EnumDataSourceType)
|
].includes(type as EnumDataSourceType)
|
||||||
) {
|
) {
|
||||||
const isCreateName = [EnumDataSourceType.CATEGORICAL, EnumDataSourceType.TIME].includes(
|
const isCreateName = getCreateFieldName(type);
|
||||||
type as EnumDataSourceType,
|
|
||||||
)
|
|
||||||
? 'isCreateDimension'
|
|
||||||
: 'isCreateMetric';
|
|
||||||
const editState = !isUndefined(record[isCreateName]) ? !!record[isCreateName] : true;
|
const editState = !isUndefined(record[isCreateName]) ? !!record[isCreateName] : true;
|
||||||
return (
|
return (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@@ -155,14 +188,21 @@ const FieldForm: React.FC<Props> = ({ fields, onFieldChange }) => {
|
|||||||
onFieldChange(record.bizName, {
|
onFieldChange(record.bizName, {
|
||||||
...record,
|
...record,
|
||||||
name: '',
|
name: '',
|
||||||
|
checked: value,
|
||||||
[isCreateName]: value,
|
[isCreateName]: value,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
handleFieldChange(record, isCreateName, value);
|
// handleFieldChange(record, isCreateName, value);
|
||||||
|
onFieldChange(record.bizName, {
|
||||||
|
...record,
|
||||||
|
checked: value,
|
||||||
|
[isCreateName]: value,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
|
className={!name && styles.dataSourceFieldsName}
|
||||||
value={name}
|
value={name}
|
||||||
disabled={!editState}
|
disabled={!editState}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -186,10 +226,18 @@ const FieldForm: React.FC<Props> = ({ fields, onFieldChange }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<Alert
|
||||||
|
style={{ marginBottom: '10px' }}
|
||||||
|
banner
|
||||||
|
message={
|
||||||
|
<Marquee pauseOnHover gradient={false}>
|
||||||
|
为了保障同一个主题域下维度/指标列表唯一,消除歧义,若本主题域下的多个数据源存在相同的字段名并且都勾选了快速创建,系统默认这些相同字段的指标维度是同一个,同时列表中将只显示最后一次创建的指标/维度。
|
||||||
|
</Marquee>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Table<FieldItem>
|
<Table<FieldItem>
|
||||||
dataSource={fields}
|
dataSource={fields}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
className="fields-table"
|
|
||||||
rowKey="bizName"
|
rowKey="bizName"
|
||||||
pagination={false}
|
pagination={false}
|
||||||
scroll={{ y: 500 }}
|
scroll={{ y: 500 }}
|
||||||
|
|||||||
@@ -757,3 +757,17 @@
|
|||||||
height: 16px !important;
|
height: 16px !important;
|
||||||
margin: 0 3px 4px;
|
margin: 0 3px 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dataSourceFieldsName {
|
||||||
|
background: #fff;
|
||||||
|
border-color: #ff4d4f;
|
||||||
|
&:hover {
|
||||||
|
border-color: #ff4d4f;
|
||||||
|
}
|
||||||
|
&:focus {
|
||||||
|
border-color: #ff7875;
|
||||||
|
box-shadow: 0 0 0 2px #ff4d4f33;
|
||||||
|
border-right-width: 1px;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,10 +11,9 @@ import OverView from './components/OverView';
|
|||||||
import styles from './components/style.less';
|
import styles from './components/style.less';
|
||||||
import type { StateType } from './model';
|
import type { StateType } from './model';
|
||||||
import { DownOutlined } from '@ant-design/icons';
|
import { DownOutlined } from '@ant-design/icons';
|
||||||
import SemanticFlow from './SemanticFlows';
|
|
||||||
import { ISemantic } from './data';
|
import { ISemantic } from './data';
|
||||||
import { findLeafNodesFromDomainList } from './utils';
|
import { findLeafNodesFromDomainList } from './utils';
|
||||||
import SemanticGraph from './SemanticGraph';
|
import SemanticGraphCanvas from './SemanticGraphCanvas';
|
||||||
import { getDomainList } from './service';
|
import { getDomainList } from './service';
|
||||||
import type { Dispatch } from 'umi';
|
import type { Dispatch } from 'umi';
|
||||||
|
|
||||||
@@ -35,6 +34,10 @@ const DomainManger: React.FC<Props> = ({ domainManger, dispatch }) => {
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [activeKey, setActiveKey] = useState<string>(menuKey);
|
const [activeKey, setActiveKey] = useState<string>(menuKey);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setActiveKey(menuKey);
|
||||||
|
}, [menuKey]);
|
||||||
|
|
||||||
const initSelectedDomain = (domainList: ISemantic.IDomainItem[]) => {
|
const initSelectedDomain = (domainList: ISemantic.IDomainItem[]) => {
|
||||||
const targetNode = domainList.filter((item: any) => {
|
const targetNode = domainList.filter((item: any) => {
|
||||||
return `${item.id}` === modelId;
|
return `${item.id}` === modelId;
|
||||||
@@ -145,21 +148,13 @@ const DomainManger: React.FC<Props> = ({ domainManger, dispatch }) => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const isModelItem = [
|
const isModelItem = [
|
||||||
// {
|
|
||||||
// label: '关系可视化',
|
|
||||||
// key: 'graph',
|
|
||||||
// children: (
|
|
||||||
// <div style={{ width: '100%', height: 'calc(100vh - 200px)' }}>
|
|
||||||
// <SemanticGraph domainId={selectDomainId} />
|
|
||||||
// </div>
|
|
||||||
// ),
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
label: '可视化建模',
|
label: '可视化建模',
|
||||||
key: 'xflow',
|
key: 'xflow',
|
||||||
children: (
|
children: (
|
||||||
<div style={{ width: '100%', height: 'calc(100vh - 200px)' }}>
|
<div style={{ width: '100%', height: 'calc(100vh - 200px)' }}>
|
||||||
<SemanticFlow />
|
{/* <SemanticFlow /> */}
|
||||||
|
<SemanticGraphCanvas />
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -192,7 +187,7 @@ const DomainManger: React.FC<Props> = ({ domainManger, dispatch }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.projectBody}>
|
<div className={styles.projectBody}>
|
||||||
<Helmet title={'语义建模-超音数'} />
|
<Helmet title={'模型管理-超音数'} />
|
||||||
<div className={styles.projectManger}>
|
<div className={styles.projectManger}>
|
||||||
<h2 className={styles.title}>
|
<h2 className={styles.title}>
|
||||||
<Popover
|
<Popover
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import DataSourceRelationFormDrawer from './DataSourceRelationFormDrawer';
|
|||||||
import DataSourceCreateForm from '../../Datasource/components/DataSourceCreateForm';
|
import DataSourceCreateForm from '../../Datasource/components/DataSourceCreateForm';
|
||||||
import ClassDataSourceTypeModal from '../../components/ClassDataSourceTypeModal';
|
import ClassDataSourceTypeModal from '../../components/ClassDataSourceTypeModal';
|
||||||
import { GraphApi } from '../service';
|
import { GraphApi } from '../service';
|
||||||
|
import { SemanticNodeType } from '../../enum';
|
||||||
import type { StateType } from '../../model';
|
import type { StateType } from '../../model';
|
||||||
import DataSource from '../../Datasource';
|
import DataSource from '../../Datasource';
|
||||||
|
|
||||||
@@ -112,7 +113,7 @@ const XflowJsonSchemaFormDrawerForm: React.FC<CreateFormProps> = (props) => {
|
|||||||
...targetData,
|
...targetData,
|
||||||
label: dataSourceInfo.name,
|
label: dataSourceInfo.name,
|
||||||
payload: dataSourceInfo,
|
payload: dataSourceInfo,
|
||||||
id: `dataSource-${dataSourceInfo.id}`,
|
id: `${SemanticNodeType.DATASOURCE}-${dataSourceInfo.id}`,
|
||||||
});
|
});
|
||||||
setDataSourceItem(undefined);
|
setDataSourceItem(undefined);
|
||||||
commandService.executeCommand(XFlowGraphCommands.SAVE_GRAPH_DATA.id, {
|
commandService.executeCommand(XFlowGraphCommands.SAVE_GRAPH_DATA.id, {
|
||||||
@@ -144,7 +145,7 @@ const XflowJsonSchemaFormDrawerForm: React.FC<CreateFormProps> = (props) => {
|
|||||||
...targetData,
|
...targetData,
|
||||||
label: dataSourceInfo.name,
|
label: dataSourceInfo.name,
|
||||||
payload: dataSourceInfo,
|
payload: dataSourceInfo,
|
||||||
id: `dataSource-${dataSourceInfo.id}`,
|
id: `${SemanticNodeType.DATASOURCE}-${dataSourceInfo.id}`,
|
||||||
});
|
});
|
||||||
setDataSourceItem(undefined);
|
setDataSourceItem(undefined);
|
||||||
commandService.executeCommand(XFlowGraphCommands.SAVE_GRAPH_DATA.id, {
|
commandService.executeCommand(XFlowGraphCommands.SAVE_GRAPH_DATA.id, {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type { NsDeployDagCmd } from './CmdExtensions/CmdDeploy';
|
|||||||
import { getRelationConfigInfo, addClassInfoAsDataSourceParents } from './utils';
|
import { getRelationConfigInfo, addClassInfoAsDataSourceParents } from './utils';
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import type { IDataSource } from '../data';
|
import type { IDataSource } from '../data';
|
||||||
|
import { SemanticNodeType } from '../enum';
|
||||||
import {
|
import {
|
||||||
getDatasourceList,
|
getDatasourceList,
|
||||||
deleteDatasource,
|
deleteDatasource,
|
||||||
@@ -57,7 +58,7 @@ export namespace GraphApi {
|
|||||||
|
|
||||||
export const createDataSourceNode = (dataSourceItem: IDataSource.IDataSourceItem) => {
|
export const createDataSourceNode = (dataSourceItem: IDataSource.IDataSourceItem) => {
|
||||||
const { id, name } = dataSourceItem;
|
const { id, name } = dataSourceItem;
|
||||||
const nodeId = `dataSource-${id}`;
|
const nodeId = `${SemanticNodeType.DATASOURCE}-${id}`;
|
||||||
return {
|
return {
|
||||||
...NODE_COMMON_PROPS,
|
...NODE_COMMON_PROPS,
|
||||||
id: nodeId,
|
id: nodeId,
|
||||||
@@ -89,7 +90,7 @@ export namespace GraphApi {
|
|||||||
const dataSourceMap = data.reduce(
|
const dataSourceMap = data.reduce(
|
||||||
(itemMap: Record<string, IDataSource.IDataSourceItem>, item: IDataSource.IDataSourceItem) => {
|
(itemMap: Record<string, IDataSource.IDataSourceItem>, item: IDataSource.IDataSourceItem) => {
|
||||||
const { id, name } = item;
|
const { id, name } = item;
|
||||||
itemMap[`dataSource-${id}`] = item;
|
itemMap[`${SemanticNodeType.DATASOURCE}-${id}`] = item;
|
||||||
|
|
||||||
itemMap[name] = item;
|
itemMap[name] = item;
|
||||||
return itemMap;
|
return itemMap;
|
||||||
@@ -114,7 +115,7 @@ export namespace GraphApi {
|
|||||||
mergeNodes = data.reduce(
|
mergeNodes = data.reduce(
|
||||||
(mergeNodeList: NsGraph.INodeConfig[], item: IDataSource.IDataSourceItem) => {
|
(mergeNodeList: NsGraph.INodeConfig[], item: IDataSource.IDataSourceItem) => {
|
||||||
const { id } = item;
|
const { id } = item;
|
||||||
const targetDataSourceItem = nodesMap[`dataSource-${id}`];
|
const targetDataSourceItem = nodesMap[`${SemanticNodeType.DATASOURCE}-${id}`];
|
||||||
if (targetDataSourceItem) {
|
if (targetDataSourceItem) {
|
||||||
mergeNodeList.push({
|
mergeNodeList.push({
|
||||||
...targetDataSourceItem,
|
...targetDataSourceItem,
|
||||||
@@ -166,7 +167,7 @@ export namespace GraphApi {
|
|||||||
const { list } = data;
|
const { list } = data;
|
||||||
const nodes: NsGraph.INodeConfig[] = list.map((item: any) => {
|
const nodes: NsGraph.INodeConfig[] = list.map((item: any) => {
|
||||||
const { id, name } = item;
|
const { id, name } = item;
|
||||||
const nodeId = `dimension-${id}`;
|
const nodeId = `${SemanticNodeType.DIMENSION}-${id}`;
|
||||||
return {
|
return {
|
||||||
...NODE_COMMON_PROPS,
|
...NODE_COMMON_PROPS,
|
||||||
id: nodeId,
|
id: nodeId,
|
||||||
|
|||||||
@@ -1,52 +1,46 @@
|
|||||||
import G6 from '@antv/g6';
|
import G6 from '@antv/g6';
|
||||||
import '../style.less';
|
import '../style.less';
|
||||||
// define the CSS with the id of your menu
|
import { Item } from '@antv/g6-core';
|
||||||
|
import { presetsTagDomString } from '../../components/AntdComponentDom/Tag';
|
||||||
|
import { SemanticNodeType } from '../../enum';
|
||||||
|
import { SEMANTIC_NODE_TYPE_CONFIG } from '../../constant';
|
||||||
|
|
||||||
// insertCss(`
|
type InitContextMenuProps = {
|
||||||
// #contextMenu {
|
graphShowType: string;
|
||||||
// position: absolute;
|
onMenuClick?: (key: string, item: Item) => void;
|
||||||
// list-style-type: none;
|
};
|
||||||
// padding: 10px 8px;
|
|
||||||
// left: -150px;
|
|
||||||
// background-color: rgba(255, 255, 255, 0.9);
|
|
||||||
// border: 1px solid #e2e2e2;
|
|
||||||
// border-radius: 4px;
|
|
||||||
// font-size: 12px;
|
|
||||||
// color: #545454;
|
|
||||||
// }
|
|
||||||
// #contextMenu li {
|
|
||||||
// cursor: pointer;
|
|
||||||
// list-style-type:none;
|
|
||||||
// list-style: none;
|
|
||||||
// margin-left: 0px;
|
|
||||||
// }
|
|
||||||
// #contextMenu li:hover {
|
|
||||||
// color: #aaa;
|
|
||||||
// }
|
|
||||||
// `);
|
|
||||||
|
|
||||||
const initContextMenu = () => {
|
export const getMenuConfig = (props?: InitContextMenuProps) => {
|
||||||
const contextMenu = new G6.Menu({
|
const { graphShowType, onMenuClick } = props || {};
|
||||||
|
return {
|
||||||
getContent(evt) {
|
getContent(evt) {
|
||||||
const itemType = evt!.item!.getType();
|
|
||||||
console.log(this, evt?.item?._cfg, 333);
|
|
||||||
const nodeData = evt?.item?._cfg?.model;
|
const nodeData = evt?.item?._cfg?.model;
|
||||||
const { name } = nodeData as any;
|
const { name, nodeType } = nodeData as any;
|
||||||
if (nodeData) {
|
if (nodeData) {
|
||||||
|
const nodeTypeConfig = SEMANTIC_NODE_TYPE_CONFIG[nodeType] || {};
|
||||||
|
let ulNode = `<ul>
|
||||||
|
<li title='编辑' key='edit' >编辑</li>
|
||||||
|
<li title='删除' key='delete' >删除</li>
|
||||||
|
</ul>`;
|
||||||
|
if (nodeType === SemanticNodeType.DATASOURCE) {
|
||||||
|
if (graphShowType) {
|
||||||
|
const typeString = graphShowType === SemanticNodeType.DIMENSION ? '维度' : '指标';
|
||||||
|
ulNode = `<ul>
|
||||||
|
<li title='新增${typeString}' key='create' >新增${typeString}</li>
|
||||||
|
</ul>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
const header = `${name}`;
|
const header = `${name}`;
|
||||||
return `<div class="g6ContextMenuContainer">
|
return `<div class="g6ContextMenuContainer">
|
||||||
<h3>${header}</h3>
|
<h3>${presetsTagDomString(nodeTypeConfig.label, nodeTypeConfig.color)}${header}</h3>
|
||||||
<ul>
|
${ulNode}
|
||||||
<li title='2'>编辑</li>
|
|
||||||
<li title='1'>删除</li>
|
|
||||||
</ul>
|
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
return `<div>当前节点信息获取失败</div>`;
|
return `<div>当前节点信息获取失败</div>`;
|
||||||
},
|
},
|
||||||
handleMenuClick(target, item) {
|
handleMenuClick(target, item) {
|
||||||
console.log(contextMenu, target, item);
|
const targetKey = target.getAttribute('key') || '';
|
||||||
const graph = contextMenu._cfgs.graph;
|
onMenuClick?.(targetKey, item);
|
||||||
},
|
},
|
||||||
// offsetX and offsetY include the padding of the parent container
|
// offsetX and offsetY include the padding of the parent container
|
||||||
// 需要加上父级容器的 padding-left 16 与自身偏移量 10
|
// 需要加上父级容器的 padding-left 16 与自身偏移量 10
|
||||||
@@ -56,7 +50,12 @@ const initContextMenu = () => {
|
|||||||
// the types of items that allow the menu show up
|
// the types of items that allow the menu show up
|
||||||
// 在哪些类型的元素上响应
|
// 在哪些类型的元素上响应
|
||||||
itemTypes: ['node'],
|
itemTypes: ['node'],
|
||||||
});
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const initContextMenu = (props?: InitContextMenuProps) => {
|
||||||
|
const config = getMenuConfig(props);
|
||||||
|
const contextMenu = new G6.Menu(config);
|
||||||
|
|
||||||
return contextMenu;
|
return contextMenu;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { Button, Modal, message } from 'antd';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { SemanticNodeType } from '../../enum';
|
||||||
|
import { deleteDimension, deleteMetric } from '../../service';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
nodeData: any;
|
||||||
|
nodeType: SemanticNodeType;
|
||||||
|
onOkClick: () => void;
|
||||||
|
onCancelClick: () => void;
|
||||||
|
open: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DeleteConfirmModal: React.FC<Props> = ({
|
||||||
|
nodeData,
|
||||||
|
nodeType,
|
||||||
|
onOkClick,
|
||||||
|
onCancelClick,
|
||||||
|
open = false,
|
||||||
|
}) => {
|
||||||
|
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||||
|
const deleteNode = async () => {
|
||||||
|
setConfirmLoading(true);
|
||||||
|
const { id } = nodeData;
|
||||||
|
let deleteQuery = deleteDimension;
|
||||||
|
if (nodeType === SemanticNodeType.METRIC) {
|
||||||
|
deleteQuery = deleteMetric;
|
||||||
|
}
|
||||||
|
const { code, msg } = await deleteQuery(id);
|
||||||
|
setConfirmLoading(false);
|
||||||
|
if (code === 200) {
|
||||||
|
onOkClick();
|
||||||
|
message.success('删除成功!');
|
||||||
|
} else {
|
||||||
|
message.error(msg);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOk = () => {
|
||||||
|
deleteNode();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
title={'删除确认'}
|
||||||
|
open={open}
|
||||||
|
onOk={handleOk}
|
||||||
|
confirmLoading={confirmLoading}
|
||||||
|
onCancel={onCancelClick}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<span style={{ color: '#296DF3', fontWeight: 'bold' }}>{nodeData?.name}</span>
|
||||||
|
将被删除,是否确认?
|
||||||
|
</>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeleteConfirmModal;
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import G6 from '@antv/g6';
|
||||||
|
|
||||||
|
const initLegend = ({ nodeData, filterFunctions }) => {
|
||||||
|
const legend = new G6.Legend({
|
||||||
|
data: {
|
||||||
|
nodes: nodeData,
|
||||||
|
},
|
||||||
|
align: 'center',
|
||||||
|
layout: 'horizontal', // vertical
|
||||||
|
position: 'bottom-right',
|
||||||
|
vertiSep: 12,
|
||||||
|
horiSep: 24,
|
||||||
|
offsetY: -24,
|
||||||
|
padding: [10, 50, 10, 50],
|
||||||
|
containerStyle: {
|
||||||
|
fill: '#a6ccff',
|
||||||
|
lineWidth: 1,
|
||||||
|
},
|
||||||
|
title: '可见数据源',
|
||||||
|
titleConfig: {
|
||||||
|
position: 'center',
|
||||||
|
offsetX: 0,
|
||||||
|
offsetY: 12,
|
||||||
|
|
||||||
|
style: {
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 500,
|
||||||
|
fill: '#000',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
enable: true,
|
||||||
|
multiple: true,
|
||||||
|
trigger: 'click',
|
||||||
|
graphActiveState: 'activeByLegend',
|
||||||
|
graphInactiveState: 'inactiveByLegend',
|
||||||
|
filterFunctions,
|
||||||
|
legendStateStyles: {
|
||||||
|
active: {
|
||||||
|
lineWidth: 2,
|
||||||
|
fill: '#f0f7ff',
|
||||||
|
stroke: '#a6ccff',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return legend;
|
||||||
|
};
|
||||||
|
export default initLegend;
|
||||||
@@ -1,45 +1,90 @@
|
|||||||
import G6 from '@antv/g6';
|
import G6, { Graph } from '@antv/g6';
|
||||||
import { createDom } from '@antv/dom-util';
|
import { createDom } from '@antv/dom-util';
|
||||||
|
import { RefreshGraphData } from '../../data';
|
||||||
const searchIconSvgPath = `<path d="M909.6 854.5L649.9 594.8C690.2 542.7 712 479 712 412c0-80.2-31.3-155.4-87.9-212.1-56.6-56.7-132-87.9-212.1-87.9s-155.5 31.3-212.1 87.9C143.2 256.5 112 331.8 112 412c0 80.1 31.3 155.5 87.9 212.1C256.5 680.8 331.8 712 412 712c67 0 130.6-21.8 182.7-62l259.7 259.6a8.2 8.2 0 0011.6 0l43.6-43.5a8.2 8.2 0 000-11.6zM570.4 570.4C528 612.7 471.8 636 412 636s-116-23.3-158.4-65.6C211.3 528 188 471.8 188 412s23.3-116.1 65.6-158.4C296 211.3 352.2 188 412 188s116.1 23.2 158.4 65.6S636 352.2 636 412s-23.3 116.1-65.6 158.4z" />`;
|
const searchIconSvgPath = `<path d="M909.6 854.5L649.9 594.8C690.2 542.7 712 479 712 412c0-80.2-31.3-155.4-87.9-212.1-56.6-56.7-132-87.9-212.1-87.9s-155.5 31.3-212.1 87.9C143.2 256.5 112 331.8 112 412c0 80.1 31.3 155.5 87.9 212.1C256.5 680.8 331.8 712 412 712c67 0 130.6-21.8 182.7-62l259.7 259.6a8.2 8.2 0 0011.6 0l43.6-43.5a8.2 8.2 0 000-11.6zM570.4 570.4C528 612.7 471.8 636 412 636s-116-23.3-158.4-65.6C211.3 528 188 471.8 188 412s23.3-116.1 65.6-158.4C296 211.3 352.2 188 412 188s116.1 23.2 158.4 65.6S636 352.2 636 412s-23.3 116.1-65.6 158.4z" />`;
|
||||||
|
|
||||||
const searchNode = (graph) => {
|
// const searchNode = (graph) => {
|
||||||
|
// const toolBarSearchInput = document.getElementById('toolBarSearchInput') as HTMLInputElement;
|
||||||
|
// const searchText = toolBarSearchInput.value.trim();
|
||||||
|
// let lastFoundNode = null;
|
||||||
|
// graph.getNodes().forEach((node) => {
|
||||||
|
// const model = node.getModel();
|
||||||
|
// const isFound = searchText && model.label.includes(searchText);
|
||||||
|
// if (isFound) {
|
||||||
|
// graph.setItemState(node, 'active', true);
|
||||||
|
// lastFoundNode = node;
|
||||||
|
// } else {
|
||||||
|
// graph.setItemState(node, 'active', false);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// if (lastFoundNode) {
|
||||||
|
// // 将视图移动到找到的节点位置
|
||||||
|
// graph.focusItem(lastFoundNode, true, {
|
||||||
|
// duration: 300,
|
||||||
|
// easing: 'easeCubic',
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
interface Node {
|
||||||
|
label: string;
|
||||||
|
children?: Node[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function findNodesByLabel(query: string, nodes: Node[]): Node[] {
|
||||||
|
const result: Node[] = [];
|
||||||
|
|
||||||
|
for (const node of nodes) {
|
||||||
|
let match = false;
|
||||||
|
let children: Node[] = [];
|
||||||
|
|
||||||
|
// 如果节点的label包含查询字符串,我们将其标记为匹配
|
||||||
|
if (node.label.includes(query)) {
|
||||||
|
match = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 我们还需要在子节点中进行搜索
|
||||||
|
if (node.children) {
|
||||||
|
children = findNodesByLabel(query, node.children);
|
||||||
|
if (children.length > 0) {
|
||||||
|
match = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果节点匹配或者其子节点匹配,我们将其添加到结果中
|
||||||
|
if (match) {
|
||||||
|
result.push({ ...node, children });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchNode = (graph: Graph, refreshGraphData?: RefreshGraphData) => {
|
||||||
const toolBarSearchInput = document.getElementById('toolBarSearchInput') as HTMLInputElement;
|
const toolBarSearchInput = document.getElementById('toolBarSearchInput') as HTMLInputElement;
|
||||||
const searchText = toolBarSearchInput.value.trim();
|
const searchText = toolBarSearchInput.value.trim();
|
||||||
let lastFoundNode = null;
|
const graphData = graph.get('initGraphData');
|
||||||
graph.getNodes().forEach((node) => {
|
const filterChildrenData = findNodesByLabel(searchText, graphData.children);
|
||||||
const model = node.getModel();
|
refreshGraphData?.({
|
||||||
const isFound = searchText && model.label.includes(searchText);
|
...graphData,
|
||||||
if (isFound) {
|
children: filterChildrenData,
|
||||||
graph.setItemState(node, 'active', true);
|
|
||||||
lastFoundNode = node;
|
|
||||||
} else {
|
|
||||||
graph.setItemState(node, 'active', false);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (lastFoundNode) {
|
|
||||||
// 将视图移动到找到的节点位置
|
|
||||||
graph.focusItem(lastFoundNode, true, {
|
|
||||||
duration: 300,
|
|
||||||
easing: 'easeCubic',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const generatorSearchInputDom = (graph) => {
|
const generatorSearchInputDom = (graph: Graph, refreshGraphData: RefreshGraphData) => {
|
||||||
const domString =
|
const domString =
|
||||||
'<input placeholder="请输入指标/维度名称" class="ant-input" id="toolBarSearchInput" type="text" value="" />';
|
'<input placeholder="请输入指标/维度名称" class="ant-input" id="toolBarSearchInput" type="text" value="" />';
|
||||||
const searchInputDom = createDom(domString);
|
const searchInputDom = createDom(domString);
|
||||||
searchInputDom.addEventListener('keydown', (event) => {
|
searchInputDom.addEventListener('keydown', (event) => {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
searchNode(graph);
|
searchNode(graph, refreshGraphData);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return searchInputDom;
|
return searchInputDom;
|
||||||
};
|
};
|
||||||
|
|
||||||
const generatorSearchBtnDom = (graph) => {
|
const generatorSearchBtnDom = (graph: Graph) => {
|
||||||
const domString = `<button
|
const domString = `<button
|
||||||
id="toolBarSearchBtn"
|
id="toolBarSearchBtn"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -66,11 +111,11 @@ const generatorSearchBtnDom = (graph) => {
|
|||||||
return searchBtnDom;
|
return searchBtnDom;
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchInputDOM = (graph) => {
|
const searchInputDOM = (graph: Graph, refreshGraphData: RefreshGraphData) => {
|
||||||
const searchInputDom = generatorSearchInputDom(graph);
|
const searchInputDom = generatorSearchInputDom(graph, refreshGraphData);
|
||||||
const searchBtnDom = generatorSearchBtnDom(graph);
|
const searchBtnDom = generatorSearchBtnDom(graph);
|
||||||
const searchInput = `
|
const searchInput = `
|
||||||
<div id="searchInputContent" class="g6-component-toolbar-search-input" style="position: absolute;top: 38px;width: 190px;left: 0;display:none">
|
<div id="searchInputContent" class="g6-component-toolbar-search-input" style="position: absolute;top: 38px;width: 190px;left: 0;">
|
||||||
<span class="ant-input-group-wrapper ant-input-search" >
|
<span class="ant-input-group-wrapper ant-input-search" >
|
||||||
<span class="ant-input-wrapper ant-input-group" id="toolBarSearchWrapper">
|
<span class="ant-input-wrapper ant-input-group" id="toolBarSearchWrapper">
|
||||||
<span class="ant-input-group-addon"></span>
|
<span class="ant-input-group-addon"></span>
|
||||||
@@ -84,7 +129,7 @@ const searchInputDOM = (graph) => {
|
|||||||
return searchDom;
|
return searchDom;
|
||||||
};
|
};
|
||||||
|
|
||||||
const initToolBar = () => {
|
const initToolBar = ({ refreshGraphData }: { refreshGraphData: RefreshGraphData }) => {
|
||||||
const toolBarInstance = new G6.ToolBar();
|
const toolBarInstance = new G6.ToolBar();
|
||||||
const config = toolBarInstance._cfgs;
|
const config = toolBarInstance._cfgs;
|
||||||
const defaultContentDomString = config.getContent();
|
const defaultContentDomString = config.getContent();
|
||||||
@@ -108,12 +153,12 @@ const initToolBar = () => {
|
|||||||
</svg>
|
</svg>
|
||||||
</li>`;
|
</li>`;
|
||||||
defaultContentDom.insertAdjacentHTML('afterbegin', searchBtnDom);
|
defaultContentDom.insertAdjacentHTML('afterbegin', searchBtnDom);
|
||||||
let searchInputContentVisible = false;
|
let searchInputContentVisible = true;
|
||||||
const toolbar = new G6.ToolBar({
|
const toolbar = new G6.ToolBar({
|
||||||
position: { x: 10, y: 10 },
|
position: { x: 10, y: 10 },
|
||||||
className: 'semantic-graph-toolbar',
|
className: 'semantic-graph-toolbar',
|
||||||
getContent: (graph) => {
|
getContent: (graph) => {
|
||||||
const searchInput = searchInputDOM(graph);
|
const searchInput = searchInputDOM(graph as Graph, refreshGraphData);
|
||||||
const content = `<div class="g6-component-toolbar-content">${defaultContentDom.outerHTML}</div>`;
|
const content = `<div class="g6-component-toolbar-content">${defaultContentDom.outerHTML}</div>`;
|
||||||
const contentDom = createDom(content);
|
const contentDom = createDom(content);
|
||||||
contentDom.appendChild(searchInput);
|
contentDom.appendChild(searchInput);
|
||||||
|
|||||||
@@ -6,17 +6,14 @@ const initTooltips = () => {
|
|||||||
offsetX: 10,
|
offsetX: 10,
|
||||||
offsetY: 10,
|
offsetY: 10,
|
||||||
fixToNode: [1, 0.5],
|
fixToNode: [1, 0.5],
|
||||||
// the types of items that allow the tooltip show up
|
|
||||||
// 允许出现 tooltip 的 item 类型
|
// 允许出现 tooltip 的 item 类型
|
||||||
// itemTypes: ['node', 'edge'],
|
|
||||||
itemTypes: ['node'],
|
itemTypes: ['node'],
|
||||||
// custom the tooltip's content
|
|
||||||
// 自定义 tooltip 内容
|
// 自定义 tooltip 内容
|
||||||
getContent: (e) => {
|
getContent: (e) => {
|
||||||
const outDiv = document.createElement('div');
|
const outDiv = document.createElement('div');
|
||||||
outDiv.style.width = 'fit-content';
|
outDiv.style.width = 'fit-content';
|
||||||
outDiv.style.height = 'fit-content';
|
outDiv.style.height = 'fit-content';
|
||||||
const model = e.item.getModel();
|
const model = e!.item!.getModel();
|
||||||
|
|
||||||
const { name, bizName, createdBy, updatedAt, description } = model;
|
const { name, bizName, createdBy, updatedAt, description } = model;
|
||||||
const list = [
|
const list = [
|
||||||
@@ -54,16 +51,9 @@ const initTooltips = () => {
|
|||||||
const html = `<div>
|
const html = `<div>
|
||||||
${listHtml}
|
${listHtml}
|
||||||
</div>`;
|
</div>`;
|
||||||
if (e.item.getType() === 'node') {
|
if (e!.item!.getType() === 'node') {
|
||||||
outDiv.innerHTML = html;
|
outDiv.innerHTML = html;
|
||||||
}
|
}
|
||||||
// else {
|
|
||||||
// const source = e.item.getSource();
|
|
||||||
// const target = e.item.getTarget();
|
|
||||||
// outDiv.innerHTML = `来源:${source.getModel().name}<br/>去向:${
|
|
||||||
// target.getModel().name
|
|
||||||
// }`;
|
|
||||||
// }
|
|
||||||
return outDiv;
|
return outDiv;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,137 +1,90 @@
|
|||||||
import React, { useEffect, useState, useRef } from 'react';
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
import { connect } from 'umi';
|
import { connect } from 'umi';
|
||||||
import type { StateType } from '../model';
|
import type { StateType } from '../model';
|
||||||
|
import { IGroup } from '@antv/g-base';
|
||||||
import type { Dispatch } from 'umi';
|
import type { Dispatch } from 'umi';
|
||||||
import { typeConfigs } from './utils';
|
import {
|
||||||
import { message, Row, Col, Radio } from 'antd';
|
typeConfigs,
|
||||||
import { getDatasourceList, getDomainSchemaRela } from '../service';
|
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 initToolBar from './components/ToolBar';
|
||||||
import initTooltips from './components/ToolTips';
|
import initTooltips from './components/ToolTips';
|
||||||
import initContextMenu from './components/ContextMenu';
|
import initContextMenu from './components/ContextMenu';
|
||||||
|
import initLegend from './components/Legend';
|
||||||
|
import { SemanticNodeType } from '../enum';
|
||||||
import G6 from '@antv/g6';
|
import G6 from '@antv/g6';
|
||||||
|
import { ISemantic, IDataSource } from '../data';
|
||||||
|
|
||||||
|
import DimensionInfoModal from '../components/DimensionInfoModal';
|
||||||
|
import MetricInfoCreateForm from '../components/MetricInfoCreateForm';
|
||||||
|
import DeleteConfirmModal from './components/DeleteConfirmModal';
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
domainId: number;
|
domainId: number;
|
||||||
|
graphShowType: SemanticNodeType;
|
||||||
domainManger: StateType;
|
domainManger: StateType;
|
||||||
dispatch: Dispatch;
|
dispatch: Dispatch;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DomainManger: React.FC<Props> = ({ domainManger, domainId }) => {
|
const DomainManger: React.FC<Props> = ({
|
||||||
|
domainManger,
|
||||||
|
domainId,
|
||||||
|
graphShowType = SemanticNodeType.DIMENSION,
|
||||||
|
dispatch,
|
||||||
|
}) => {
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
const [graphData, setGraphData] = useState<any>({});
|
const [graphData, setGraphData] = useState<TreeGraphData>();
|
||||||
const [dataSourceListData, setDataSourceListData] = useState<any[]>([]);
|
const [createDimensionModalVisible, setCreateDimensionModalVisible] = useState<boolean>(false);
|
||||||
const [graphShowType, setGraphShowType] = useState<string>('dimension');
|
const [createMetricModalVisible, setCreateMetricModalVisible] = useState<boolean>(false);
|
||||||
|
|
||||||
const legendDataRef = useRef<any[]>([]);
|
const legendDataRef = useRef<any[]>([]);
|
||||||
const graphRef = useRef<any>(null);
|
const graphRef = useRef<any>(null);
|
||||||
const legendDataFilterFunctions = useRef<any>({});
|
const legendDataFilterFunctions = useRef<any>({});
|
||||||
|
const [dimensionItem, setDimensionItem] = useState<ISemantic.IDimensionItem>();
|
||||||
|
|
||||||
// const { dimensionList } = domainManger;
|
const [metricItem, setMetricItem] = useState<ISemantic.IMetricItem>();
|
||||||
|
|
||||||
const toggleNodeVisibility = (graph, node, visible) => {
|
const [nodeDataSource, setNodeDataSource] = useState<any>();
|
||||||
if (visible) {
|
|
||||||
graph.showItem(node);
|
|
||||||
} else {
|
|
||||||
graph.hideItem(node);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleChildrenVisibility = (graph, node, visible) => {
|
const { dimensionList, metricList } = domainManger;
|
||||||
const model = node.getModel();
|
|
||||||
if (model.children) {
|
|
||||||
model.children.forEach((child) => {
|
|
||||||
const childNode = graph.findById(child.id);
|
|
||||||
toggleNodeVisibility(graph, childNode, visible);
|
|
||||||
toggleChildrenVisibility(graph, childNode, visible);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDimensionChildren = (dimensions: any[], dataSourceId: string) => {
|
const dimensionListRef = useRef<ISemantic.IDimensionItem[]>([]);
|
||||||
const dimensionChildrenList = dimensions.reduce((dimensionChildren: any[], dimension: any) => {
|
const metricListRef = useRef<ISemantic.IMetricItem[]>([]);
|
||||||
const {
|
|
||||||
id: dimensionId,
|
const [confirmModalOpenState, setConfirmModalOpenState] = useState<boolean>(false);
|
||||||
name: dimensionName,
|
|
||||||
bizName,
|
// const toggleNodeVisibility = (graph: Graph, node: Item, visible: boolean) => {
|
||||||
description,
|
// if (visible) {
|
||||||
createdBy,
|
// graph.showItem(node);
|
||||||
updatedAt,
|
// } else {
|
||||||
} = dimension;
|
// graph.hideItem(node);
|
||||||
// if (datasourceId === id) {
|
|
||||||
dimensionChildren.push({
|
|
||||||
nodeType: 'dimension',
|
|
||||||
legendType: dataSourceId,
|
|
||||||
id: `dimension-${dimensionId}`,
|
|
||||||
name: dimensionName,
|
|
||||||
bizName,
|
|
||||||
description,
|
|
||||||
createdBy,
|
|
||||||
updatedAt,
|
|
||||||
style: {
|
|
||||||
lineWidth: 2,
|
|
||||||
fill: '#f0f7ff',
|
|
||||||
stroke: '#a6ccff',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
// }
|
// }
|
||||||
return dimensionChildren;
|
// };
|
||||||
}, []);
|
|
||||||
return dimensionChildrenList;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMetricChildren = (metrics: any[], dataSourceId: string) => {
|
useEffect(() => {
|
||||||
const metricsChildrenList = metrics.reduce((metricsChildren: any[], dimension: any) => {
|
dimensionListRef.current = dimensionList;
|
||||||
const { id, name, bizName, description, createdBy, updatedAt } = dimension;
|
metricListRef.current = metricList;
|
||||||
metricsChildren.push({
|
}, [dimensionList, metricList]);
|
||||||
nodeType: 'metric',
|
|
||||||
legendType: dataSourceId,
|
|
||||||
id: `dimension-${id}`,
|
|
||||||
name,
|
|
||||||
bizName,
|
|
||||||
description,
|
|
||||||
createdBy,
|
|
||||||
updatedAt,
|
|
||||||
style: {
|
|
||||||
lineWidth: 2,
|
|
||||||
fill: '#f0f7ff',
|
|
||||||
stroke: '#a6ccff',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return metricsChildren;
|
|
||||||
}, []);
|
|
||||||
return metricsChildrenList;
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatterRelationData = (dataSourceList: any[], type = graphShowType) => {
|
// const toggleChildrenVisibility = (graph: Graph, node: Item, visible: boolean) => {
|
||||||
const relationData = dataSourceList.reduce((relationList: any[], item: any) => {
|
// const model = node.getModel();
|
||||||
const { datasource, dimensions, metrics } = item;
|
// if (Array.isArray(model.children)) {
|
||||||
const { id, name } = datasource;
|
// model.children.forEach((child) => {
|
||||||
const dataSourceId = `dataSource-${id}`;
|
// const childNode = graph.findById(child.id);
|
||||||
let childrenList = [];
|
// toggleNodeVisibility(graph, childNode, visible);
|
||||||
if (type === 'metric') {
|
// toggleChildrenVisibility(graph, childNode, visible);
|
||||||
childrenList = getMetricChildren(metrics, dataSourceId);
|
// });
|
||||||
}
|
// }
|
||||||
if (type === 'dimension') {
|
// };
|
||||||
childrenList = getDimensionChildren(dimensions, dataSourceId);
|
|
||||||
}
|
|
||||||
relationList.push({
|
|
||||||
name,
|
|
||||||
legendType: dataSourceId,
|
|
||||||
id: dataSourceId,
|
|
||||||
nodeType: 'datasource',
|
|
||||||
size: 40,
|
|
||||||
children: [...childrenList],
|
|
||||||
style: {
|
|
||||||
lineWidth: 2,
|
|
||||||
fill: '#BDEFDB',
|
|
||||||
stroke: '#5AD8A6',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return relationList;
|
|
||||||
}, []);
|
|
||||||
return relationData;
|
|
||||||
};
|
|
||||||
|
|
||||||
const changeGraphData = (data: any, type?: string) => {
|
const changeGraphData = (data: IDataSource.IDataSourceItem[], type: SemanticNodeType) => {
|
||||||
const relationData = formatterRelationData(data, type);
|
const relationData = formatterRelationData(data, type);
|
||||||
const legendList = relationData.map((item: any) => {
|
const legendList = relationData.map((item: any) => {
|
||||||
const { id, name } = item;
|
const { id, name } = item;
|
||||||
@@ -148,31 +101,38 @@ const DomainManger: React.FC<Props> = ({ domainManger, domainId }) => {
|
|||||||
name: domainManger.selectDomainName,
|
name: domainManger.selectDomainName,
|
||||||
children: relationData,
|
children: relationData,
|
||||||
};
|
};
|
||||||
setGraphData(graphRootData);
|
//
|
||||||
|
return graphRootData;
|
||||||
};
|
};
|
||||||
|
|
||||||
const queryDataSourceList = async (params: any) => {
|
const queryDataSourceList = async (params: {
|
||||||
const { code, data, msg } = await getDomainSchemaRela(params.domainId);
|
domainId: number;
|
||||||
|
graphShowType?: SemanticNodeType;
|
||||||
|
}) => {
|
||||||
|
const { code, data } = await getDomainSchemaRela(params.domainId);
|
||||||
if (code === 200) {
|
if (code === 200) {
|
||||||
if (data) {
|
if (data) {
|
||||||
changeGraphData(data);
|
const graphRootData = changeGraphData(data, params.graphShowType || graphShowType);
|
||||||
setDataSourceListData(data);
|
setGraphData(graphRootData);
|
||||||
|
return graphRootData;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
} else {
|
} else {
|
||||||
message.error(msg);
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
graphRef.current = null;
|
||||||
queryDataSourceList({ domainId });
|
queryDataSourceList({ domainId });
|
||||||
}, [domainId]);
|
}, [domainId, graphShowType]);
|
||||||
|
|
||||||
const getLegendDataFilterFunctions = () => {
|
const getLegendDataFilterFunctions = () => {
|
||||||
legendDataRef.current.map((item: any) => {
|
legendDataRef.current.map((item: any) => {
|
||||||
const { id } = item;
|
const { id } = item;
|
||||||
legendDataFilterFunctions.current = {
|
legendDataFilterFunctions.current = {
|
||||||
...legendDataFilterFunctions.current,
|
...legendDataFilterFunctions.current,
|
||||||
[id]: (d) => {
|
[id]: (d: any) => {
|
||||||
if (d.legendType === id) {
|
if (d.legendType === id) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -184,6 +144,9 @@ const DomainManger: React.FC<Props> = ({ domainManger, domainId }) => {
|
|||||||
|
|
||||||
const setAllActiveLegend = (legend: any) => {
|
const setAllActiveLegend = (legend: any) => {
|
||||||
const legendCanvas = legend._cfgs.legendCanvas;
|
const legendCanvas = legend._cfgs.legendCanvas;
|
||||||
|
if (!legendCanvas) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// 从图例中找出node-group节点;
|
// 从图例中找出node-group节点;
|
||||||
const group = legendCanvas.find((e: any) => e.get('name') === 'node-group');
|
const group = legendCanvas.find((e: any) => e.get('name') === 'node-group');
|
||||||
// 数据源的图例节点在node-group中的children中;
|
// 数据源的图例节点在node-group中的children中;
|
||||||
@@ -195,72 +158,177 @@ const DomainManger: React.FC<Props> = ({ domainManger, domainId }) => {
|
|||||||
legend.activateLegend(labelText);
|
legend.activateLegend(labelText);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
// const [visible, setVisible] = useState(false);
|
|
||||||
useEffect(() => {
|
const handleContextMenuClickEdit = (item: IItemBaseConfig) => {
|
||||||
if (!(Array.isArray(graphData.children) && graphData.children.length > 0)) {
|
const targetData = item.model;
|
||||||
|
if (!targetData) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const datasource = loopNodeFindDataSource(item);
|
||||||
|
if (datasource) {
|
||||||
|
setNodeDataSource({
|
||||||
|
id: datasource.uid,
|
||||||
|
name: datasource.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (targetData.nodeType === SemanticNodeType.DIMENSION) {
|
||||||
|
const targetItem = dimensionListRef.current.find((item) => item.id === targetData.uid);
|
||||||
|
if (targetItem) {
|
||||||
|
setDimensionItem({ ...targetItem });
|
||||||
|
setCreateDimensionModalVisible(true);
|
||||||
|
} else {
|
||||||
|
message.error('获取维度初始化数据失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (targetData.nodeType === SemanticNodeType.METRIC) {
|
||||||
|
const targetItem = metricListRef.current.find((item) => item.id === targetData.uid);
|
||||||
|
if (targetItem) {
|
||||||
|
setMetricItem({ ...targetItem });
|
||||||
|
setCreateMetricModalVisible(true);
|
||||||
|
} else {
|
||||||
|
message.error('获取指标初始化数据失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleContextMenuClickCreate = (item: IItemBaseConfig) => {
|
||||||
|
const datasource = item.model;
|
||||||
|
if (!datasource) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setNodeDataSource({
|
||||||
|
id: datasource.uid,
|
||||||
|
name: datasource.name,
|
||||||
|
});
|
||||||
|
if (graphShowType === SemanticNodeType.DIMENSION) {
|
||||||
|
setCreateDimensionModalVisible(true);
|
||||||
|
}
|
||||||
|
if (graphShowType === SemanticNodeType.METRIC) {
|
||||||
|
setCreateMetricModalVisible(true);
|
||||||
|
}
|
||||||
|
setDimensionItem(undefined);
|
||||||
|
setMetricItem(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleContextMenuClickDelete = (item: IItemBaseConfig) => {
|
||||||
|
const targetData = item.model;
|
||||||
|
if (!targetData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (targetData.nodeType === SemanticNodeType.DIMENSION) {
|
||||||
|
const targetItem = dimensionListRef.current.find((item) => item.id === targetData.uid);
|
||||||
|
if (targetItem) {
|
||||||
|
setDimensionItem({ ...targetItem });
|
||||||
|
setConfirmModalOpenState(true);
|
||||||
|
} else {
|
||||||
|
message.error('获取维度初始化数据失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (targetData.nodeType === SemanticNodeType.METRIC) {
|
||||||
|
const targetItem = metricListRef.current.find((item) => item.id === targetData.uid);
|
||||||
|
if (targetItem) {
|
||||||
|
setMetricItem({ ...targetItem });
|
||||||
|
setConfirmModalOpenState(true);
|
||||||
|
} else {
|
||||||
|
message.error('获取指标初始化数据失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleContextMenuClick = (key: string, item: Item) => {
|
||||||
|
if (!item?._cfg) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (key) {
|
||||||
|
case 'edit':
|
||||||
|
handleContextMenuClickEdit(item._cfg);
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
handleContextMenuClickDelete(item._cfg);
|
||||||
|
break;
|
||||||
|
case 'create':
|
||||||
|
handleContextMenuClickCreate(item._cfg);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!Array.isArray(graphData?.children)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const container = document.getElementById('semanticGraph');
|
const container = document.getElementById('semanticGraph');
|
||||||
const width = container!.scrollWidth;
|
const width = container!.scrollWidth;
|
||||||
const height = container!.scrollHeight || 500;
|
const height = container!.scrollHeight || 500;
|
||||||
|
|
||||||
// if (!graphRef.current) {
|
const graph = graphRef.current;
|
||||||
getLegendDataFilterFunctions();
|
|
||||||
|
|
||||||
const toolbar = initToolBar();
|
if (!graph && graphData) {
|
||||||
|
const graphNodeList = flatGraphDataNode(graphData.children);
|
||||||
|
const graphConfigKey = graphNodeList.length > 20 ? 'dendrogram' : 'mindmap';
|
||||||
|
|
||||||
|
getLegendDataFilterFunctions();
|
||||||
|
const toolbar = initToolBar({ refreshGraphData });
|
||||||
const tooltip = initTooltips();
|
const tooltip = initTooltips();
|
||||||
const contextMenu = initContextMenu();
|
const contextMenu = initContextMenu({
|
||||||
const legend = new G6.Legend({
|
graphShowType,
|
||||||
// container: 'legendContainer',
|
onMenuClick: handleContextMenuClick,
|
||||||
data: {
|
});
|
||||||
nodes: legendDataRef.current,
|
const legend = initLegend({
|
||||||
},
|
nodeData: legendDataRef.current,
|
||||||
align: 'center',
|
filterFunctions: { ...legendDataFilterFunctions.current },
|
||||||
layout: 'horizontal', // vertical
|
|
||||||
position: 'bottom-right',
|
|
||||||
vertiSep: 12,
|
|
||||||
horiSep: 24,
|
|
||||||
offsetY: -24,
|
|
||||||
padding: [4, 16, 8, 16],
|
|
||||||
containerStyle: {
|
|
||||||
fill: '#ccc',
|
|
||||||
lineWidth: 1,
|
|
||||||
},
|
|
||||||
title: '可见数据源',
|
|
||||||
titleConfig: {
|
|
||||||
position: 'center',
|
|
||||||
offsetX: 0,
|
|
||||||
offsetY: 12,
|
|
||||||
style: {
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 500,
|
|
||||||
fill: '#000',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
filter: {
|
|
||||||
enable: true,
|
|
||||||
multiple: true,
|
|
||||||
trigger: 'click',
|
|
||||||
graphActiveState: 'activeByLegend',
|
|
||||||
graphInactiveState: 'inactiveByLegend',
|
|
||||||
filterFunctions: {
|
|
||||||
...legendDataFilterFunctions.current,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
graphRef.current = new G6.TreeGraph({
|
graphRef.current = new G6.TreeGraph({
|
||||||
container: 'semanticGraph',
|
container: 'semanticGraph',
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
linkCenter: true,
|
|
||||||
modes: {
|
modes: {
|
||||||
default: [
|
default: [
|
||||||
{
|
{
|
||||||
type: 'collapse-expand',
|
type: 'collapse-expand',
|
||||||
onChange: function onChange(item, collapsed) {
|
onChange: function onChange(item, collapsed) {
|
||||||
const data = item.get('model');
|
const data = item!.get('model');
|
||||||
data.collapsed = collapsed;
|
data.collapsed = collapsed;
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
@@ -278,6 +346,10 @@ const DomainManger: React.FC<Props> = ({ domainManger, domainId }) => {
|
|||||||
},
|
},
|
||||||
defaultNode: {
|
defaultNode: {
|
||||||
size: 26,
|
size: 26,
|
||||||
|
anchorPoints: [
|
||||||
|
[0, 0.5],
|
||||||
|
[1, 0.5],
|
||||||
|
],
|
||||||
labelCfg: {
|
labelCfg: {
|
||||||
position: 'right',
|
position: 'right',
|
||||||
offset: 5,
|
offset: 5,
|
||||||
@@ -288,118 +360,164 @@ const DomainManger: React.FC<Props> = ({ domainManger, domainId }) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultEdge: {
|
defaultEdge: {
|
||||||
type: 'cubic-horizontal',
|
type: graphConfigMap[graphConfigKey].defaultEdge.type,
|
||||||
// type: 'flow-line',
|
|
||||||
// type: 'polyline',
|
|
||||||
// type: 'line',
|
|
||||||
/* configure the bending radius and min distance to the end nodes */
|
|
||||||
style: {
|
|
||||||
radius: 10,
|
|
||||||
offset: 30,
|
|
||||||
endArrow: true,
|
|
||||||
/* and other styles */
|
|
||||||
// stroke: '#F6BD16',
|
|
||||||
},
|
|
||||||
// style: {
|
|
||||||
// stroke: '#A3B1BF',
|
|
||||||
// },
|
|
||||||
},
|
},
|
||||||
layout: {
|
layout: {
|
||||||
// type: 'mindmap',
|
...graphConfigMap[graphConfigKey].layout,
|
||||||
// direction: 'H',
|
|
||||||
// getId: function getId(d) {
|
|
||||||
// return d.id;
|
|
||||||
// },
|
|
||||||
// getHeight: function getHeight() {
|
|
||||||
// return 16;
|
|
||||||
// },
|
|
||||||
// getWidth: function getWidth() {
|
|
||||||
// return 16;
|
|
||||||
// },
|
|
||||||
// getVGap: function getVGap() {
|
|
||||||
// return 30;
|
|
||||||
// },
|
|
||||||
// getHGap: function getHGap() {
|
|
||||||
// return 100;
|
|
||||||
// },
|
|
||||||
type: 'dendrogram',
|
|
||||||
direction: 'LR',
|
|
||||||
nodeSep: 200,
|
|
||||||
rankSep: 300,
|
|
||||||
radial: true,
|
|
||||||
},
|
},
|
||||||
plugins: [legend, tooltip, toolbar, contextMenu],
|
plugins: [legend, tooltip, toolbar, contextMenu],
|
||||||
});
|
});
|
||||||
|
graphRef.current.set('initGraphData', graphData);
|
||||||
const legendCanvas = legend._cfgs.legendCanvas;
|
const legendCanvas = legend._cfgs.legendCanvas;
|
||||||
|
|
||||||
// legend模式事件方法bindEvents会有点击图例空白清空选中的逻辑,在注册click事件前,先将click事件队列清空;
|
// legend模式事件方法bindEvents会有点击图例空白清空选中的逻辑,在注册click事件前,先将click事件队列清空;
|
||||||
legend._cfgs.legendCanvas._events.click = [];
|
legend._cfgs.legendCanvas._events.click = [];
|
||||||
legendCanvas.on('click', (e) => {
|
// legendCanvas.on('click', (e) => {
|
||||||
const shape = e.target;
|
// const shape = e.target;
|
||||||
const shapeGroup = shape.get('parent');
|
// const shapeGroup = shape.get('parent');
|
||||||
const shapeGroupId = shapeGroup?.cfg?.id;
|
// const shapeGroupId = shapeGroup?.cfg?.id;
|
||||||
if (shapeGroupId) {
|
// if (shapeGroupId) {
|
||||||
const isActive = shapeGroup.get('active');
|
// const isActive = shapeGroup.get('active');
|
||||||
const targetNode = graph.findById(shapeGroupId);
|
// const targetNode = graphRef.current.findById(shapeGroupId);
|
||||||
// const model = targetNode.getModel();
|
// toggleNodeVisibility(graphRef.current, targetNode, isActive);
|
||||||
toggleNodeVisibility(graph, targetNode, isActive);
|
// toggleChildrenVisibility(graphRef.current, targetNode, isActive);
|
||||||
toggleChildrenVisibility(graph, targetNode, isActive);
|
// }
|
||||||
|
// });
|
||||||
|
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
const graph = graphRef.current;
|
graphRef.current.node(function (node: NodeConfig) {
|
||||||
|
return getNodeConfigByType(node, {
|
||||||
graph.node(function (node) {
|
|
||||||
return {
|
|
||||||
label: node.name,
|
label: node.name,
|
||||||
labelCfg: { style: { fill: '#3c3c3c' } },
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
// graph.data(graphData);
|
});
|
||||||
graph.changeData(graphData);
|
|
||||||
graph.render();
|
graphRef.current.data(graphData);
|
||||||
graph.fitView();
|
graphRef.current.render();
|
||||||
|
graphRef.current.fitView([80, 80]);
|
||||||
|
|
||||||
setAllActiveLegend(legend);
|
setAllActiveLegend(legend);
|
||||||
|
|
||||||
const rootNode = graph.findById('root');
|
const rootNode = graphRef.current.findById('root');
|
||||||
graph.hideItem(rootNode);
|
graphRef.current.hideItem(rootNode);
|
||||||
if (typeof window !== 'undefined')
|
if (typeof window !== 'undefined')
|
||||||
window.onresize = () => {
|
window.onresize = () => {
|
||||||
if (!graph || graph.get('destroyed')) return;
|
if (!graphRef.current || graphRef.current.get('destroyed')) return;
|
||||||
if (!container || !container.scrollWidth || !container.scrollHeight) return;
|
if (!container || !container.scrollWidth || !container.scrollHeight) return;
|
||||||
graph.changeSize(container.scrollWidth, container.scrollHeight);
|
graphRef.current.changeSize(container.scrollWidth, container.scrollHeight);
|
||||||
};
|
};
|
||||||
// }
|
}
|
||||||
}, [graphData]);
|
}, [graphData]);
|
||||||
|
|
||||||
|
const updateGraphData = async () => {
|
||||||
|
const graphRootData = await queryDataSourceList({ domainId });
|
||||||
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Row>
|
|
||||||
<Col flex="auto" />
|
|
||||||
<Col flex="100px">
|
|
||||||
<Radio.Group
|
|
||||||
buttonStyle="solid"
|
|
||||||
size="small"
|
|
||||||
value={graphShowType}
|
|
||||||
onChange={(e) => {
|
|
||||||
const { value } = e.target;
|
|
||||||
setGraphShowType(value);
|
|
||||||
changeGraphData(dataSourceListData, value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Radio.Button value="dimension">维度</Radio.Button>
|
|
||||||
<Radio.Button value="metric">指标</Radio.Button>
|
|
||||||
</Radio.Group>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
key={`${domainId}-${graphShowType}`}
|
key={`${domainId}-${graphShowType}`}
|
||||||
id="semanticGraph"
|
id="semanticGraph"
|
||||||
style={{ width: '100%', height: '100%' }}
|
style={{ width: '100%', height: '100%' }}
|
||||||
/>
|
/>
|
||||||
|
{createDimensionModalVisible && (
|
||||||
|
<DimensionInfoModal
|
||||||
|
domainId={domainId}
|
||||||
|
bindModalVisible={createDimensionModalVisible}
|
||||||
|
dimensionItem={dimensionItem}
|
||||||
|
dataSourceList={nodeDataSource ? [nodeDataSource] : []}
|
||||||
|
onSubmit={() => {
|
||||||
|
setCreateDimensionModalVisible(false);
|
||||||
|
updateGraphData();
|
||||||
|
dispatch({
|
||||||
|
type: 'domainManger/queryDimensionList',
|
||||||
|
payload: {
|
||||||
|
domainId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
setCreateDimensionModalVisible(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{createMetricModalVisible && (
|
||||||
|
<MetricInfoCreateForm
|
||||||
|
domainId={domainId}
|
||||||
|
key={metricItem?.id}
|
||||||
|
datasourceId={nodeDataSource.id}
|
||||||
|
createModalVisible={createMetricModalVisible}
|
||||||
|
metricItem={metricItem}
|
||||||
|
onSubmit={() => {
|
||||||
|
setCreateMetricModalVisible(false);
|
||||||
|
updateGraphData();
|
||||||
|
dispatch({
|
||||||
|
type: 'domainManger/queryMetricList',
|
||||||
|
payload: {
|
||||||
|
domainId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
setCreateMetricModalVisible(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{
|
||||||
|
<DeleteConfirmModal
|
||||||
|
open={confirmModalOpenState}
|
||||||
|
onOkClick={() => {
|
||||||
|
setConfirmModalOpenState(false);
|
||||||
|
updateGraphData();
|
||||||
|
graphShowType === SemanticNodeType.DIMENSION
|
||||||
|
? dispatch({
|
||||||
|
type: 'domainManger/queryDimensionList',
|
||||||
|
payload: {
|
||||||
|
domainId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: dispatch({
|
||||||
|
type: 'domainManger/queryMetricList',
|
||||||
|
payload: {
|
||||||
|
domainId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onCancelClick={() => {
|
||||||
|
setConfirmModalOpenState(false);
|
||||||
|
}}
|
||||||
|
nodeType={graphShowType}
|
||||||
|
nodeData={graphShowType === SemanticNodeType.DIMENSION ? dimensionItem : metricItem}
|
||||||
|
/>
|
||||||
|
}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,80 +1,139 @@
|
|||||||
|
import { ISemantic, IDataSource } from '../data';
|
||||||
|
import { SemanticNodeType } from '../enum';
|
||||||
|
|
||||||
export const typeConfigs = {
|
export const typeConfigs = {
|
||||||
datasource: {
|
datasource: {
|
||||||
type: 'circle',
|
type: 'circle',
|
||||||
size: 5,
|
size: 10,
|
||||||
style: {
|
|
||||||
fill: '#5B8FF9',
|
|
||||||
},
|
},
|
||||||
},
|
|
||||||
dimension: {
|
|
||||||
type: 'circle',
|
|
||||||
size: 20,
|
|
||||||
style: {
|
|
||||||
fill: '#5AD8A6',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
metric: {
|
|
||||||
type: 'rect',
|
|
||||||
size: [10, 10],
|
|
||||||
style: {
|
|
||||||
fill: '#5D7092',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// eType1: {
|
|
||||||
// type: 'line',
|
|
||||||
// style: {
|
|
||||||
// width: 20,
|
|
||||||
// stroke: '#F6BD16',
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// eType2: {
|
|
||||||
// type: 'cubic',
|
|
||||||
// },
|
|
||||||
// eType3: {
|
|
||||||
// type: 'quadratic',
|
|
||||||
// style: {
|
|
||||||
// width: 25,
|
|
||||||
// stroke: '#6F5EF9',
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
};
|
};
|
||||||
export const legendData = {
|
|
||||||
nodes: [
|
export const getDimensionChildren = (
|
||||||
{
|
dimensions: ISemantic.IDimensionItem[],
|
||||||
id: 'type1',
|
dataSourceNodeId: string,
|
||||||
label: 'node-type1',
|
) => {
|
||||||
order: 4,
|
const dimensionChildrenList = dimensions.reduce(
|
||||||
...typeConfigs.datasource,
|
(dimensionChildren: any[], dimension: ISemantic.IDimensionItem) => {
|
||||||
|
const { id } = dimension;
|
||||||
|
dimensionChildren.push({
|
||||||
|
...dimension,
|
||||||
|
nodeType: SemanticNodeType.DIMENSION,
|
||||||
|
legendType: dataSourceNodeId,
|
||||||
|
id: `${SemanticNodeType.DIMENSION}-${id}`,
|
||||||
|
uid: id,
|
||||||
|
style: {
|
||||||
|
lineWidth: 2,
|
||||||
|
fill: '#f0f7ff',
|
||||||
|
stroke: '#a6ccff',
|
||||||
},
|
},
|
||||||
{
|
});
|
||||||
id: 'type2',
|
return dimensionChildren;
|
||||||
label: 'node-type2',
|
|
||||||
order: 0,
|
|
||||||
...typeConfigs.dimension,
|
|
||||||
},
|
},
|
||||||
{
|
[],
|
||||||
id: 'type3',
|
);
|
||||||
label: 'node-type3',
|
return dimensionChildrenList;
|
||||||
order: 2,
|
};
|
||||||
...typeConfigs.metric,
|
|
||||||
},
|
export const getMetricChildren = (metrics: ISemantic.IMetricItem[], dataSourceNodeId: string) => {
|
||||||
],
|
const metricsChildrenList = metrics.reduce(
|
||||||
// edges: [
|
(metricsChildren: any[], metric: ISemantic.IMetricItem) => {
|
||||||
// {
|
const { id } = metric;
|
||||||
// id: 'eType1',
|
metricsChildren.push({
|
||||||
// label: 'edge-type1',
|
...metric,
|
||||||
// order: 2,
|
nodeType: SemanticNodeType.METRIC,
|
||||||
// ...typeConfigs.eType1,
|
legendType: dataSourceNodeId,
|
||||||
// },
|
id: `${SemanticNodeType.METRIC}-${id}`,
|
||||||
// {
|
uid: id,
|
||||||
// id: 'eType2',
|
style: {
|
||||||
// label: 'edge-type2',
|
lineWidth: 2,
|
||||||
// ...typeConfigs.eType2,
|
fill: '#f0f7ff',
|
||||||
// },
|
stroke: '#a6ccff',
|
||||||
// {
|
},
|
||||||
// id: 'eType3',
|
});
|
||||||
// label: 'edge-type3',
|
return metricsChildren;
|
||||||
// ...typeConfigs.eType3,
|
},
|
||||||
// },
|
[],
|
||||||
// ],
|
);
|
||||||
|
return metricsChildrenList;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatterRelationData = (
|
||||||
|
dataSourceList: IDataSource.IDataSourceItem[],
|
||||||
|
type: SemanticNodeType = SemanticNodeType.DIMENSION,
|
||||||
|
) => {
|
||||||
|
const relationData = dataSourceList.reduce((relationList: any[], item: any) => {
|
||||||
|
const { datasource, dimensions, metrics } = item;
|
||||||
|
const { id } = datasource;
|
||||||
|
const dataSourceNodeId = `${SemanticNodeType.DATASOURCE}-${id}`;
|
||||||
|
let childrenList = [];
|
||||||
|
if (type === SemanticNodeType.METRIC) {
|
||||||
|
childrenList = getMetricChildren(metrics, dataSourceNodeId);
|
||||||
|
}
|
||||||
|
if (type === SemanticNodeType.DIMENSION) {
|
||||||
|
childrenList = getDimensionChildren(dimensions, dataSourceNodeId);
|
||||||
|
}
|
||||||
|
relationList.push({
|
||||||
|
...datasource,
|
||||||
|
legendType: dataSourceNodeId,
|
||||||
|
id: dataSourceNodeId,
|
||||||
|
uid: id,
|
||||||
|
nodeType: SemanticNodeType.DATASOURCE,
|
||||||
|
size: 40,
|
||||||
|
children: [...childrenList],
|
||||||
|
style: {
|
||||||
|
lineWidth: 2,
|
||||||
|
fill: '#BDEFDB',
|
||||||
|
stroke: '#5AD8A6',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return relationList;
|
||||||
|
}, []);
|
||||||
|
return relationData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loopNodeFindDataSource: any = (node: any) => {
|
||||||
|
const { model, parent } = node;
|
||||||
|
if (model?.nodeType === SemanticNodeType.DATASOURCE) {
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
const parentNode = parent?._cfg;
|
||||||
|
if (parentNode) {
|
||||||
|
return loopNodeFindDataSource(parentNode);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getNodeConfigByType = (nodeData: any, defaultConfig = {}) => {
|
||||||
|
const { nodeType } = nodeData;
|
||||||
|
const labelCfg = { style: { fill: '#3c3c3c' } };
|
||||||
|
switch (nodeType) {
|
||||||
|
case SemanticNodeType.DATASOURCE: {
|
||||||
|
return {
|
||||||
|
...defaultConfig,
|
||||||
|
labelCfg: { position: 'bottom', ...labelCfg },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case SemanticNodeType.DIMENSION:
|
||||||
|
return {
|
||||||
|
...defaultConfig,
|
||||||
|
labelCfg: { position: 'right', ...labelCfg },
|
||||||
|
};
|
||||||
|
case SemanticNodeType.METRIC:
|
||||||
|
return {
|
||||||
|
...defaultConfig,
|
||||||
|
labelCfg: { position: 'right', ...labelCfg },
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return defaultConfig;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const flatGraphDataNode = (graphData: any[]) => {
|
||||||
|
return graphData.reduce((nodeList: any[], item: any) => {
|
||||||
|
const { children } = item;
|
||||||
|
if (Array.isArray(children)) {
|
||||||
|
nodeList.push(...children);
|
||||||
|
}
|
||||||
|
return nodeList;
|
||||||
|
}, []);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { Radio } from 'antd';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { connect } from 'umi';
|
||||||
|
import styles from './components/style.less';
|
||||||
|
import type { StateType } from './model';
|
||||||
|
import { SemanticNodeType } from './enum';
|
||||||
|
import SemanticFlow from './SemanticFlows';
|
||||||
|
import SemanticGraph from './SemanticGraph';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
domainManger: StateType;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SemanticGraphCanvas: React.FC<Props> = ({ domainManger }) => {
|
||||||
|
const [graphShowType, setGraphShowType] = useState<SemanticNodeType>(SemanticNodeType.DATASOURCE);
|
||||||
|
const { selectDomainId } = domainManger;
|
||||||
|
return (
|
||||||
|
<div className={styles.semanticGraphCanvas}>
|
||||||
|
<div className={styles.toolbar}>
|
||||||
|
<Radio.Group
|
||||||
|
buttonStyle="solid"
|
||||||
|
value={graphShowType}
|
||||||
|
onChange={(e) => {
|
||||||
|
const { value } = e.target;
|
||||||
|
setGraphShowType(value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Radio.Button value={SemanticNodeType.DATASOURCE}>数据源</Radio.Button>
|
||||||
|
<Radio.Button value={SemanticNodeType.DIMENSION}>维度</Radio.Button>
|
||||||
|
<Radio.Button value={SemanticNodeType.METRIC}>指标</Radio.Button>
|
||||||
|
</Radio.Group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.canvasContainer}>
|
||||||
|
{graphShowType === SemanticNodeType.DATASOURCE ? (
|
||||||
|
<div style={{ width: '100%', height: 'calc(100vh - 200px)' }}>
|
||||||
|
<SemanticFlow />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ width: '100%', height: 'calc(100vh - 220px)' }}>
|
||||||
|
<SemanticGraph domainId={selectDomainId} graphShowType={graphShowType} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(({ domainManger }: { domainManger: StateType }) => ({
|
||||||
|
domainManger,
|
||||||
|
}))(SemanticGraphCanvas);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export const presetsTagDomString = (text: string, color: string = 'blue') => {
|
||||||
|
return `<span class="ant-tag ant-tag-${color}">${text}</span>`;
|
||||||
|
};
|
||||||
@@ -176,6 +176,9 @@ const ClassDataSourceTable: React.FC<Props> = ({ dispatch, domainManger }) => {
|
|||||||
{
|
{
|
||||||
<ClassDataSourceTypeModal
|
<ClassDataSourceTypeModal
|
||||||
open={createDataSourceModalOpen}
|
open={createDataSourceModalOpen}
|
||||||
|
onCancel={() => {
|
||||||
|
setCreateDataSourceModalOpen(false);
|
||||||
|
}}
|
||||||
onTypeChange={(type) => {
|
onTypeChange={(type) => {
|
||||||
if (type === 'fast') {
|
if (type === 'fast') {
|
||||||
setDataSourceModalVisible(true);
|
setDataSourceModalVisible(true);
|
||||||
|
|||||||
@@ -1,14 +1,24 @@
|
|||||||
import { Modal, Card, Row, Col } from 'antd';
|
import { Modal, Card, Row, Col, Result, Button } from 'antd';
|
||||||
import { ConsoleSqlOutlined, CoffeeOutlined } from '@ant-design/icons';
|
import { ConsoleSqlOutlined, CoffeeOutlined } from '@ant-design/icons';
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { history, connect } from 'umi';
|
||||||
|
import type { Dispatch } from 'umi';
|
||||||
|
import type { StateType } from '../model';
|
||||||
const { Meta } = Card;
|
const { Meta } = Card;
|
||||||
type Props = {
|
type Props = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
domainManger: StateType;
|
||||||
onTypeChange: (type: 'fast' | 'normal') => void;
|
onTypeChange: (type: 'fast' | 'normal') => void;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ClassDataSourceTypeModal: React.FC<Props> = ({ open, onTypeChange, onCancel }) => {
|
const ClassDataSourceTypeModal: React.FC<Props> = ({
|
||||||
|
open,
|
||||||
|
onTypeChange,
|
||||||
|
domainManger,
|
||||||
|
onCancel,
|
||||||
|
}) => {
|
||||||
|
const { selectDomainId, dataBaseConfig } = domainManger;
|
||||||
const [createDataSourceModalOpen, setCreateDataSourceModalOpen] = useState(false);
|
const [createDataSourceModalOpen, setCreateDataSourceModalOpen] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCreateDataSourceModalOpen(open);
|
setCreateDataSourceModalOpen(open);
|
||||||
@@ -26,6 +36,7 @@ const ClassDataSourceTypeModal: React.FC<Props> = ({ open, onTypeChange, onCance
|
|||||||
centered
|
centered
|
||||||
closable={false}
|
closable={false}
|
||||||
>
|
>
|
||||||
|
{dataBaseConfig && dataBaseConfig.id ? (
|
||||||
<Row gutter={16} style={{ marginTop: '0px' }}>
|
<Row gutter={16} style={{ marginTop: '0px' }}>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Card
|
<Card
|
||||||
@@ -63,8 +74,29 @@ const ClassDataSourceTypeModal: React.FC<Props> = ({ open, onTypeChange, onCance
|
|||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
) : (
|
||||||
|
<Result
|
||||||
|
status="warning"
|
||||||
|
subTitle="创建数据源需要先完成数据库设置"
|
||||||
|
extra={
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
key="console"
|
||||||
|
onClick={() => {
|
||||||
|
history.replace(`/semanticModel/${selectDomainId}/dataBase`);
|
||||||
|
onCancel?.();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
去设置
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default ClassDataSourceTypeModal;
|
|
||||||
|
export default connect(({ domainManger }: { domainManger: StateType }) => ({
|
||||||
|
domainManger,
|
||||||
|
}))(ClassDataSourceTypeModal);
|
||||||
|
|||||||
@@ -6,14 +6,9 @@ import type { Dispatch } from 'umi';
|
|||||||
import { connect } from 'umi';
|
import { connect } from 'umi';
|
||||||
import type { StateType } from '../model';
|
import type { StateType } from '../model';
|
||||||
import { SENSITIVE_LEVEL_ENUM } from '../constant';
|
import { SENSITIVE_LEVEL_ENUM } from '../constant';
|
||||||
import {
|
import { getDatasourceList, getDimensionList, deleteDimension } from '../service';
|
||||||
getDatasourceList,
|
|
||||||
getDimensionList,
|
|
||||||
createDimension,
|
|
||||||
updateDimension,
|
|
||||||
deleteDimension,
|
|
||||||
} from '../service';
|
|
||||||
import DimensionInfoModal from './DimensionInfoModal';
|
import DimensionInfoModal from './DimensionInfoModal';
|
||||||
|
import { ISemantic } from '../data';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import styles from './style.less';
|
import styles from './style.less';
|
||||||
|
|
||||||
@@ -25,7 +20,7 @@ type Props = {
|
|||||||
const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
||||||
const { selectDomainId } = domainManger;
|
const { selectDomainId } = domainManger;
|
||||||
const [createModalVisible, setCreateModalVisible] = useState<boolean>(false);
|
const [createModalVisible, setCreateModalVisible] = useState<boolean>(false);
|
||||||
const [dimensionItem, setDimensionItem] = useState<any>();
|
const [dimensionItem, setDimensionItem] = useState<ISemantic.IDimensionItem>();
|
||||||
const [dataSourceList, setDataSourceList] = useState<any[]>([]);
|
const [dataSourceList, setDataSourceList] = useState<any[]>([]);
|
||||||
const [pagination, setPagination] = useState({
|
const [pagination, setPagination] = useState({
|
||||||
current: 1,
|
current: 1,
|
||||||
@@ -45,7 +40,7 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
|||||||
let resData: any = {};
|
let resData: any = {};
|
||||||
if (code === 200) {
|
if (code === 200) {
|
||||||
setPagination({
|
setPagination({
|
||||||
pageSize,
|
pageSize: Math.min(pageSize, 100),
|
||||||
current,
|
current,
|
||||||
total,
|
total,
|
||||||
});
|
});
|
||||||
@@ -175,36 +170,6 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const saveDimension = async (fieldsValue: any, reloadState: boolean = true) => {
|
|
||||||
const queryParams = {
|
|
||||||
domainId: selectDomainId,
|
|
||||||
type: 'categorical',
|
|
||||||
...fieldsValue,
|
|
||||||
};
|
|
||||||
let saveDimensionQuery = createDimension;
|
|
||||||
if (queryParams.id) {
|
|
||||||
saveDimensionQuery = updateDimension;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { code, msg } = await saveDimensionQuery(queryParams);
|
|
||||||
|
|
||||||
if (code === 200) {
|
|
||||||
setCreateModalVisible(false);
|
|
||||||
if (reloadState) {
|
|
||||||
message.success('编辑维度成功');
|
|
||||||
actionRef?.current?.reload();
|
|
||||||
}
|
|
||||||
dispatch({
|
|
||||||
type: 'domainManger/queryDimensionList',
|
|
||||||
payload: {
|
|
||||||
domainId: selectDomainId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
message.error(msg);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ProTable
|
<ProTable
|
||||||
@@ -251,10 +216,21 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
|||||||
|
|
||||||
{createModalVisible && (
|
{createModalVisible && (
|
||||||
<DimensionInfoModal
|
<DimensionInfoModal
|
||||||
|
domainId={selectDomainId}
|
||||||
bindModalVisible={createModalVisible}
|
bindModalVisible={createModalVisible}
|
||||||
dimensionItem={dimensionItem}
|
dimensionItem={dimensionItem}
|
||||||
dataSourceList={dataSourceList}
|
dataSourceList={dataSourceList}
|
||||||
onSubmit={saveDimension}
|
onSubmit={() => {
|
||||||
|
setCreateModalVisible(false);
|
||||||
|
actionRef?.current?.reload();
|
||||||
|
dispatch({
|
||||||
|
type: 'domainManger/queryDimensionList',
|
||||||
|
payload: {
|
||||||
|
domainId: selectDomainId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
setCreateModalVisible(false);
|
setCreateModalVisible(false);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type { Dispatch } from 'umi';
|
|||||||
import { connect } from 'umi';
|
import { connect } from 'umi';
|
||||||
import type { StateType } from '../model';
|
import type { StateType } from '../model';
|
||||||
import { SENSITIVE_LEVEL_ENUM } from '../constant';
|
import { SENSITIVE_LEVEL_ENUM } from '../constant';
|
||||||
import { creatExprMetric, updateExprMetric, queryMetric, deleteMetric } from '../service';
|
import { queryMetric, deleteMetric } from '../service';
|
||||||
|
|
||||||
import MetricInfoCreateForm from './MetricInfoCreateForm';
|
import MetricInfoCreateForm from './MetricInfoCreateForm';
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
|||||||
let resData: any = {};
|
let resData: any = {};
|
||||||
if (code === 200) {
|
if (code === 200) {
|
||||||
setPagination({
|
setPagination({
|
||||||
pageSize,
|
pageSize: Math.min(pageSize, 100),
|
||||||
current,
|
current,
|
||||||
total,
|
total,
|
||||||
});
|
});
|
||||||
@@ -166,36 +166,36 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const saveMetric = async (fieldsValue: any, reloadState: boolean = true) => {
|
// const saveMetric = async (fieldsValue: any, reloadState: boolean = true) => {
|
||||||
const queryParams = {
|
// const queryParams = {
|
||||||
domainId: selectDomainId,
|
// domainId: selectDomainId,
|
||||||
...fieldsValue,
|
// ...fieldsValue,
|
||||||
};
|
// };
|
||||||
if (queryParams.typeParams && !queryParams.typeParams.expr) {
|
// if (queryParams.typeParams && !queryParams.typeParams.expr) {
|
||||||
message.error('度量表达式不能为空');
|
// message.error('度量表达式不能为空');
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
let saveMetricQuery = creatExprMetric;
|
// let saveMetricQuery = creatExprMetric;
|
||||||
if (queryParams.id) {
|
// if (queryParams.id) {
|
||||||
saveMetricQuery = updateExprMetric;
|
// saveMetricQuery = updateExprMetric;
|
||||||
}
|
// }
|
||||||
const { code, msg } = await saveMetricQuery(queryParams);
|
// const { code, msg } = await saveMetricQuery(queryParams);
|
||||||
if (code === 200) {
|
// if (code === 200) {
|
||||||
message.success('编辑指标成功');
|
// message.success('编辑指标成功');
|
||||||
setCreateModalVisible(false);
|
// setCreateModalVisible(false);
|
||||||
if (reloadState) {
|
// if (reloadState) {
|
||||||
actionRef?.current?.reload();
|
// actionRef?.current?.reload();
|
||||||
}
|
// }
|
||||||
dispatch({
|
// dispatch({
|
||||||
type: 'domainManger/queryMetricList',
|
// type: 'domainManger/queryMetricList',
|
||||||
payload: {
|
// payload: {
|
||||||
domainId: selectDomainId,
|
// domainId: selectDomainId,
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
message.error(msg);
|
// message.error(msg);
|
||||||
};
|
// };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -246,8 +246,15 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
|||||||
domainId={Number(selectDomainId)}
|
domainId={Number(selectDomainId)}
|
||||||
createModalVisible={createModalVisible}
|
createModalVisible={createModalVisible}
|
||||||
metricItem={metricItem}
|
metricItem={metricItem}
|
||||||
onSubmit={(values) => {
|
onSubmit={() => {
|
||||||
saveMetric(values);
|
setCreateModalVisible(false);
|
||||||
|
actionRef?.current?.reload();
|
||||||
|
dispatch({
|
||||||
|
type: 'domainManger/queryMetricList',
|
||||||
|
payload: {
|
||||||
|
domainId: selectDomainId,
|
||||||
|
},
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
setCreateModalVisible(false);
|
setCreateModalVisible(false);
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { List, Collapse, Button } from 'antd';
|
||||||
|
import { uuid } from '@/utils/utils';
|
||||||
|
import SqlEditor from '@/components/SqlEditor';
|
||||||
|
|
||||||
|
import styles from './style.less';
|
||||||
|
|
||||||
|
const { Panel } = Collapse;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title?: string;
|
||||||
|
defaultCollapse?: boolean;
|
||||||
|
value?: string[];
|
||||||
|
onChange?: (list: string[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ListItem = {
|
||||||
|
id: string;
|
||||||
|
sql: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type List = ListItem[];
|
||||||
|
|
||||||
|
const CommonEditList: React.FC<Props> = ({ title, defaultCollapse = false, value, onChange }) => {
|
||||||
|
const [listDataSource, setListDataSource] = useState<List>([]);
|
||||||
|
const [currentSql, setCurrentSql] = useState<string>('');
|
||||||
|
const [activeKey, setActiveKey] = useState<string>();
|
||||||
|
const [currentRecord, setCurrentRecord] = useState<ListItem>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
const list = value.map((sql: string) => {
|
||||||
|
return {
|
||||||
|
id: uuid(),
|
||||||
|
sql,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setListDataSource(list);
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const handleListChange = (listDataSource: List) => {
|
||||||
|
const sqlList = listDataSource.map((item) => {
|
||||||
|
return item.sql;
|
||||||
|
});
|
||||||
|
onChange?.(sqlList);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.commonEditList}>
|
||||||
|
<Collapse
|
||||||
|
activeKey={activeKey}
|
||||||
|
defaultActiveKey={defaultCollapse ? ['editor'] : undefined}
|
||||||
|
onChange={() => {}}
|
||||||
|
ghost
|
||||||
|
>
|
||||||
|
<Panel
|
||||||
|
header={title}
|
||||||
|
key="editor"
|
||||||
|
extra={
|
||||||
|
activeKey ? (
|
||||||
|
<Button
|
||||||
|
key="saveBtn"
|
||||||
|
type="primary"
|
||||||
|
onClick={() => {
|
||||||
|
if (!currentRecord && !currentSql) {
|
||||||
|
setActiveKey(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentRecord) {
|
||||||
|
const list = [...listDataSource].map((item) => {
|
||||||
|
if (item.id === currentRecord.id) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
sql: currentSql,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
setListDataSource(list);
|
||||||
|
handleListChange(list);
|
||||||
|
} else {
|
||||||
|
const list = [
|
||||||
|
...listDataSource,
|
||||||
|
{
|
||||||
|
id: uuid(),
|
||||||
|
sql: currentSql,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
setListDataSource(list);
|
||||||
|
handleListChange(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveKey(undefined);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
确认
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
key="createBtn"
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentRecord(undefined);
|
||||||
|
setCurrentSql('');
|
||||||
|
setActiveKey('editor');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
新增
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
showArrow={false}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<SqlEditor
|
||||||
|
value={currentSql}
|
||||||
|
height={'150px'}
|
||||||
|
onChange={(sql) => {
|
||||||
|
setCurrentSql(sql);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
</Collapse>
|
||||||
|
<List
|
||||||
|
itemLayout="horizontal"
|
||||||
|
dataSource={listDataSource || []}
|
||||||
|
renderItem={(item) => (
|
||||||
|
<List.Item
|
||||||
|
actions={[
|
||||||
|
<a
|
||||||
|
key="list-loadmore-edit"
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentSql(item.sql);
|
||||||
|
setCurrentRecord(item);
|
||||||
|
setActiveKey('editor');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</a>,
|
||||||
|
<a
|
||||||
|
key="list-loadmore-more"
|
||||||
|
onClick={() => {
|
||||||
|
const list = [...listDataSource].filter(({ id }) => {
|
||||||
|
return item.id !== id;
|
||||||
|
});
|
||||||
|
handleListChange(list);
|
||||||
|
setListDataSource(list);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</a>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<List.Item.Meta title={item.sql} />
|
||||||
|
</List.Item>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CommonEditList;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
.commonEditList {
|
||||||
|
:global {
|
||||||
|
.ant-collapse-header {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
.ant-collapse {
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,20 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { Button, Form, Input, Modal, Select, List } from 'antd';
|
import { Button, Form, Input, Modal, Select } from 'antd';
|
||||||
import { SENSITIVE_LEVEL_OPTIONS } from '../constant';
|
import { SENSITIVE_LEVEL_OPTIONS } from '../constant';
|
||||||
import { formLayout } from '@/components/FormHelper/utils';
|
import { formLayout } from '@/components/FormHelper/utils';
|
||||||
import SqlEditor from '@/components/SqlEditor';
|
import SqlEditor from '@/components/SqlEditor';
|
||||||
import InfoTagList from './InfoTagList';
|
import InfoTagList from './InfoTagList';
|
||||||
|
import { ISemantic } from '../data';
|
||||||
|
import { createDimension, updateDimension } from '../service';
|
||||||
import { message } from 'antd';
|
import { message } from 'antd';
|
||||||
|
|
||||||
export type CreateFormProps = {
|
export type CreateFormProps = {
|
||||||
dimensionItem: any;
|
domainId: number;
|
||||||
|
dimensionItem?: ISemantic.IDimensionItem;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
bindModalVisible: boolean;
|
bindModalVisible: boolean;
|
||||||
dataSourceList: any[];
|
dataSourceList: any[];
|
||||||
onSubmit: (values: any) => Promise<any>;
|
onSubmit: (values?: any) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FormItem = Form.Item;
|
const FormItem = Form.Item;
|
||||||
@@ -20,42 +23,56 @@ const { Option } = Select;
|
|||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
|
|
||||||
const DimensionInfoModal: React.FC<CreateFormProps> = ({
|
const DimensionInfoModal: React.FC<CreateFormProps> = ({
|
||||||
|
domainId,
|
||||||
onCancel,
|
onCancel,
|
||||||
bindModalVisible,
|
bindModalVisible,
|
||||||
dimensionItem,
|
dimensionItem,
|
||||||
dataSourceList,
|
dataSourceList,
|
||||||
onSubmit: handleUpdate,
|
onSubmit: handleUpdate,
|
||||||
}) => {
|
}) => {
|
||||||
const isEdit = dimensionItem?.id;
|
const isEdit = !!dimensionItem?.id;
|
||||||
const [formVals, setFormVals] = useState<any>({
|
|
||||||
roleCode: '',
|
|
||||||
users: [],
|
|
||||||
effectiveTime: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const { setFieldsValue } = form;
|
const { setFieldsValue, resetFields } = form;
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
const fieldsValue = await form.validateFields();
|
const fieldsValue = await form.validateFields();
|
||||||
setFormVals({ ...fieldsValue });
|
await saveDimension(fieldsValue);
|
||||||
try {
|
};
|
||||||
await handleUpdate(fieldsValue);
|
|
||||||
} catch (error) {
|
const saveDimension = async (fieldsValue: any) => {
|
||||||
message.error('保存失败,接口调用出错');
|
const queryParams = {
|
||||||
|
domainId,
|
||||||
|
type: 'categorical',
|
||||||
|
...fieldsValue,
|
||||||
|
};
|
||||||
|
let saveDimensionQuery = createDimension;
|
||||||
|
if (queryParams.id) {
|
||||||
|
saveDimensionQuery = updateDimension;
|
||||||
}
|
}
|
||||||
|
const { code, msg } = await saveDimensionQuery(queryParams);
|
||||||
|
if (code === 200) {
|
||||||
|
message.success('编辑维度成功');
|
||||||
|
handleUpdate(fieldsValue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
message.error(msg);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setFormVal = () => {
|
const setFormVal = () => {
|
||||||
console.log(dimensionItem, 'dimensionItem');
|
|
||||||
setFieldsValue(dimensionItem);
|
setFieldsValue(dimensionItem);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dimensionItem) {
|
if (dimensionItem) {
|
||||||
setFormVal();
|
setFormVal();
|
||||||
|
} else {
|
||||||
|
resetFields();
|
||||||
}
|
}
|
||||||
}, [dimensionItem]);
|
if (!isEdit && Array.isArray(dataSourceList) && dataSourceList[0]?.id) {
|
||||||
|
setFieldsValue({ datasourceId: dataSourceList[0].id });
|
||||||
|
}
|
||||||
|
}, [dimensionItem, dataSourceList]);
|
||||||
|
|
||||||
const renderFooter = () => {
|
const renderFooter = () => {
|
||||||
return (
|
return (
|
||||||
@@ -141,7 +158,12 @@ const DimensionInfoModal: React.FC<CreateFormProps> = ({
|
|||||||
>
|
>
|
||||||
<TextArea placeholder="请输入维度描述" />
|
<TextArea placeholder="请输入维度描述" />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem name="expr" label="表达式" rules={[{ required: true, message: '请输入表达式' }]}>
|
<FormItem
|
||||||
|
name="expr"
|
||||||
|
label="表达式"
|
||||||
|
tooltip="表达式中的字段必须在创建数据源的时候被标记为日期或者维度"
|
||||||
|
rules={[{ required: true, message: '请输入表达式' }]}
|
||||||
|
>
|
||||||
<SqlEditor height={'150px'} />
|
<SqlEditor height={'150px'} />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</>
|
</>
|
||||||
@@ -162,9 +184,11 @@ const DimensionInfoModal: React.FC<CreateFormProps> = ({
|
|||||||
<Form
|
<Form
|
||||||
{...formLayout}
|
{...formLayout}
|
||||||
form={form}
|
form={form}
|
||||||
initialValues={{
|
initialValues={
|
||||||
...formVals,
|
{
|
||||||
}}
|
// ...formVals,
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{renderContent()}
|
{renderContent()}
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -1,139 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { Button, Modal, message } from 'antd';
|
|
||||||
import { addDomainExtend, editDomainExtend, getDomainExtendDetailConfig } from '../../service';
|
|
||||||
import DimensionMetricVisibleTransfer from './DimensionMetricVisibleTransfer';
|
|
||||||
import { exChangeRichEntityListToIds } from './utils';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
domainId: number;
|
|
||||||
themeData: any;
|
|
||||||
settingType: 'dimension' | 'metric';
|
|
||||||
settingSourceList: any[];
|
|
||||||
onCancel: () => void;
|
|
||||||
visible: boolean;
|
|
||||||
onSubmit: (params?: any) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const dimensionConfig = {
|
|
||||||
blackIdListKey: 'blackDimIdList',
|
|
||||||
visibleIdListKey: 'whiteDimIdList',
|
|
||||||
modalTitle: '问答可见维度信息',
|
|
||||||
titles: ['不可见维度', '可见维度'],
|
|
||||||
};
|
|
||||||
|
|
||||||
const metricConfig = {
|
|
||||||
blackIdListKey: 'blackMetricIdList',
|
|
||||||
visibleIdListKey: 'whiteMetricIdList',
|
|
||||||
modalTitle: '问答可见指标信息',
|
|
||||||
titles: ['不可见指标', '可见指标'],
|
|
||||||
};
|
|
||||||
|
|
||||||
const DimensionMetricVisibleModal: React.FC<Props> = ({
|
|
||||||
domainId,
|
|
||||||
visible,
|
|
||||||
themeData = {},
|
|
||||||
settingType,
|
|
||||||
settingSourceList,
|
|
||||||
onCancel,
|
|
||||||
onSubmit,
|
|
||||||
}) => {
|
|
||||||
const [sourceList, setSourceList] = useState<any[]>([]);
|
|
||||||
const [selectedKeyList, setSelectedKeyList] = useState<string[]>([]);
|
|
||||||
const settingTypeConfig = settingType === 'dimension' ? dimensionConfig : metricConfig;
|
|
||||||
useEffect(() => {
|
|
||||||
const list = settingSourceList.map((item: any) => {
|
|
||||||
const { id, name } = item;
|
|
||||||
return { id, name, type: settingType };
|
|
||||||
});
|
|
||||||
setSourceList(list);
|
|
||||||
}, [settingSourceList]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setSelectedKeyList(themeData.visibility?.[settingTypeConfig.visibleIdListKey] || []);
|
|
||||||
}, [themeData]);
|
|
||||||
|
|
||||||
const saveEntity = async () => {
|
|
||||||
const { id, entity } = themeData;
|
|
||||||
let saveDomainExtendQuery = addDomainExtend;
|
|
||||||
if (id) {
|
|
||||||
saveDomainExtendQuery = editDomainExtend;
|
|
||||||
}
|
|
||||||
const blackIdList = settingSourceList.reduce((list, item: any) => {
|
|
||||||
const { id: targetId } = item;
|
|
||||||
if (!selectedKeyList.includes(targetId)) {
|
|
||||||
list.push(targetId);
|
|
||||||
}
|
|
||||||
return list;
|
|
||||||
}, []);
|
|
||||||
const entityParams = exChangeRichEntityListToIds(entity);
|
|
||||||
themeData.entity = entityParams;
|
|
||||||
const params = {
|
|
||||||
...themeData,
|
|
||||||
visibility: themeData.visibility || {},
|
|
||||||
};
|
|
||||||
params.visibility[settingTypeConfig.blackIdListKey] = blackIdList;
|
|
||||||
|
|
||||||
if (!params.visibility.blackDimIdList) {
|
|
||||||
params.visibility.blackDimIdList = [];
|
|
||||||
}
|
|
||||||
if (!params.visibility.blackMetricIdList) {
|
|
||||||
params.visibility.blackMetricIdList = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const { code, msg } = await saveDomainExtendQuery({
|
|
||||||
...params,
|
|
||||||
id,
|
|
||||||
domainId,
|
|
||||||
});
|
|
||||||
if (code === 200) {
|
|
||||||
onSubmit?.();
|
|
||||||
message.success('保存成功');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
message.error(msg);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTransferChange = (newTargetKeys: string[]) => {
|
|
||||||
setSelectedKeyList(newTargetKeys);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderFooter = () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button onClick={onCancel}>取消</Button>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
onClick={() => {
|
|
||||||
saveEntity();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
完成
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Modal
|
|
||||||
width={1200}
|
|
||||||
destroyOnClose
|
|
||||||
title={settingTypeConfig.modalTitle}
|
|
||||||
maskClosable={false}
|
|
||||||
open={visible}
|
|
||||||
footer={renderFooter()}
|
|
||||||
onCancel={onCancel}
|
|
||||||
>
|
|
||||||
<DimensionMetricVisibleTransfer
|
|
||||||
titles={settingTypeConfig.titles}
|
|
||||||
sourceList={sourceList}
|
|
||||||
targetList={selectedKeyList}
|
|
||||||
onChange={(newTargetKeys) => {
|
|
||||||
handleTransferChange(newTargetKeys);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DimensionMetricVisibleModal;
|
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
import { Space, Table, Transfer, Checkbox, Tooltip, Button } from 'antd';
|
import { Table, Transfer, Checkbox, Button } from 'antd';
|
||||||
import type { ColumnsType, TableRowSelection } from 'antd/es/table/interface';
|
import type { ColumnsType, TableRowSelection } from 'antd/es/table/interface';
|
||||||
import type { TransferItem } from 'antd/es/transfer';
|
import type { TransferItem } from 'antd/es/transfer';
|
||||||
import type { CheckboxChangeEvent } from 'antd/es/checkbox';
|
import type { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||||
import { ExclamationCircleOutlined } from '@ant-design/icons';
|
|
||||||
import difference from 'lodash/difference';
|
import difference from 'lodash/difference';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import type { IChatConfig } from '../../data';
|
import type { IChatConfig } from '../../data';
|
||||||
import DimensionValueSettingModal from './DimensionValueSettingModal';
|
import DimensionValueSettingModal from './DimensionValueSettingModal';
|
||||||
import TransTypeTag from '../TransTypeTag';
|
import TransTypeTag from '../TransTypeTag';
|
||||||
import { TransType } from '../../enum';
|
import { TransType } from '../../enum';
|
||||||
|
import TableTitleTooltips from '../../components/TableTitleTooltips';
|
||||||
|
|
||||||
interface RecordType {
|
interface RecordType {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -72,12 +72,10 @@ const DimensionMetricVisibleTableTransfer: React.FC<Props> = ({
|
|||||||
{
|
{
|
||||||
dataIndex: 'y',
|
dataIndex: 'y',
|
||||||
title: (
|
title: (
|
||||||
<Space>
|
<TableTitleTooltips
|
||||||
<span>维度值可见</span>
|
title="维度值可见"
|
||||||
<Tooltip title="勾选可见后,维度值将在搜索时可以被联想出来">
|
tooltips="勾选可见后,维度值将在搜索时可以被联想出来"
|
||||||
<ExclamationCircleOutlined />
|
/>
|
||||||
</Tooltip>
|
|
||||||
</Space>
|
|
||||||
),
|
),
|
||||||
width: 120,
|
width: 120,
|
||||||
render: (_, record) => {
|
render: (_, record) => {
|
||||||
|
|||||||
@@ -1,138 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { Button, Modal, message, Space } from 'antd';
|
|
||||||
import ProCard from '@ant-design/pro-card';
|
|
||||||
import { addDomainExtend, editDomainExtend } from '../../service';
|
|
||||||
import DimensionMetricVisibleTransfer from './DimensionMetricVisibleTransfer';
|
|
||||||
import SqlEditor from '@/components/SqlEditor';
|
|
||||||
type Props = {
|
|
||||||
domainId: number;
|
|
||||||
themeData: any;
|
|
||||||
settingSourceList: any[];
|
|
||||||
onCancel: () => void;
|
|
||||||
visible: boolean;
|
|
||||||
onSubmit: (params?: any) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const DimensionSearchVisibleModal: React.FC<Props> = ({
|
|
||||||
domainId,
|
|
||||||
themeData,
|
|
||||||
visible,
|
|
||||||
settingSourceList,
|
|
||||||
onCancel,
|
|
||||||
onSubmit,
|
|
||||||
}) => {
|
|
||||||
const [sourceList, setSourceList] = useState<any[]>([]);
|
|
||||||
const [selectedKeyList, setSelectedKeyList] = useState<string[]>([]);
|
|
||||||
const [dictRules, setDictRules] = useState<string>('');
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const knowledgeInfos = themeData?.knowledgeInfos;
|
|
||||||
if (Array.isArray(knowledgeInfos)) {
|
|
||||||
const target = knowledgeInfos[0];
|
|
||||||
if (Array.isArray(target?.ruleList)) {
|
|
||||||
setDictRules(target.ruleList[0]);
|
|
||||||
}
|
|
||||||
const selectKeys = knowledgeInfos.map((item: any) => {
|
|
||||||
return item.itemId;
|
|
||||||
});
|
|
||||||
setSelectedKeyList(selectKeys);
|
|
||||||
}
|
|
||||||
}, [themeData]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const list = settingSourceList.map((item: any) => {
|
|
||||||
const { id, name } = item;
|
|
||||||
return { id, name, type: 'dimension' };
|
|
||||||
});
|
|
||||||
setSourceList(list);
|
|
||||||
}, [settingSourceList]);
|
|
||||||
|
|
||||||
const saveDictBatch = async () => {
|
|
||||||
const knowledgeInfos = selectedKeyList.map((key: string) => {
|
|
||||||
return {
|
|
||||||
itemId: key,
|
|
||||||
type: 'DIMENSION',
|
|
||||||
isDictInfo: true,
|
|
||||||
ruleList: dictRules ? [dictRules] : [],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
const id = themeData?.id;
|
|
||||||
let saveDomainExtendQuery = addDomainExtend;
|
|
||||||
if (id) {
|
|
||||||
saveDomainExtendQuery = editDomainExtend;
|
|
||||||
}
|
|
||||||
const { code, msg } = await saveDomainExtendQuery({
|
|
||||||
knowledgeInfos,
|
|
||||||
domainId,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (code === 200) {
|
|
||||||
message.success('保存可见维度值成功');
|
|
||||||
onSubmit?.();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
message.error(msg);
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveDictSetting = async () => {
|
|
||||||
await saveDictBatch();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTransferChange = (newTargetKeys: string[]) => {
|
|
||||||
setSelectedKeyList(newTargetKeys);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderFooter = () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button onClick={onCancel}>取消</Button>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
onClick={() => {
|
|
||||||
saveDictSetting();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
完成
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Modal
|
|
||||||
width={1200}
|
|
||||||
destroyOnClose
|
|
||||||
title={'可见维度值设置'}
|
|
||||||
maskClosable={false}
|
|
||||||
open={visible}
|
|
||||||
footer={renderFooter()}
|
|
||||||
onCancel={onCancel}
|
|
||||||
>
|
|
||||||
<Space direction="vertical" style={{ width: '100%' }} size={20}>
|
|
||||||
<ProCard bordered title="可见设置">
|
|
||||||
<DimensionMetricVisibleTransfer
|
|
||||||
titles={['不可见维度值', '可见维度值']}
|
|
||||||
sourceList={sourceList}
|
|
||||||
targetList={selectedKeyList}
|
|
||||||
onChange={(newTargetKeys) => {
|
|
||||||
handleTransferChange(newTargetKeys);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ProCard>
|
|
||||||
<ProCard bordered title="维度值过滤">
|
|
||||||
<SqlEditor
|
|
||||||
height={'150px'}
|
|
||||||
value={dictRules}
|
|
||||||
onChange={(sql: string) => {
|
|
||||||
setDictRules(sql);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ProCard>
|
|
||||||
</Space>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DimensionSearchVisibleModal;
|
|
||||||
@@ -5,7 +5,7 @@ import { Form, Input } from 'antd';
|
|||||||
import { formLayout } from '@/components/FormHelper/utils';
|
import { formLayout } from '@/components/FormHelper/utils';
|
||||||
import { isString } from 'lodash';
|
import { isString } from 'lodash';
|
||||||
import styles from '../style.less';
|
import styles from '../style.less';
|
||||||
|
import CommonEditList from '../../components/CommonEditList/index';
|
||||||
import SqlEditor from '@/components/SqlEditor';
|
import SqlEditor from '@/components/SqlEditor';
|
||||||
type Props = {
|
type Props = {
|
||||||
initialValues: any;
|
initialValues: any;
|
||||||
@@ -17,7 +17,7 @@ const FormItem = Form.Item;
|
|||||||
const EntityCreateForm: ForwardRefRenderFunction<any, Props> = ({ initialValues }, ref) => {
|
const EntityCreateForm: ForwardRefRenderFunction<any, Props> = ({ initialValues }, ref) => {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
const exchangeFields = ['blackList', 'whiteList', 'ruleList'];
|
const exchangeFields = ['blackList', 'whiteList'];
|
||||||
|
|
||||||
const getFormValidateFields = async () => {
|
const getFormValidateFields = async () => {
|
||||||
const fields = await form.validateFields();
|
const fields = await form.validateFields();
|
||||||
@@ -69,8 +69,9 @@ const EntityCreateForm: ForwardRefRenderFunction<any, Props> = ({ initialValues
|
|||||||
<Input placeholder="多个维度值用英文逗号隔开" />
|
<Input placeholder="多个维度值用英文逗号隔开" />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
<FormItem name="ruleList" label="过滤规则">
|
<FormItem name="ruleList">
|
||||||
<SqlEditor height={'150px'} />
|
{/* <SqlEditor height={'150px'} /> */}
|
||||||
|
<CommonEditList title="过滤规则" />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</Form>
|
</Form>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,195 +0,0 @@
|
|||||||
import { useEffect, useState, forwardRef, useImperativeHandle } from 'react';
|
|
||||||
import type { ForwardRefRenderFunction } from 'react';
|
|
||||||
import FormItemTitle from '@/components/FormHelper/FormItemTitle';
|
|
||||||
import { formLayout } from '@/components/FormHelper/utils';
|
|
||||||
import { message, Form, Input, Select, Button, InputNumber } from 'antd';
|
|
||||||
import { addDomainExtend, editDomainExtend } from '../../service';
|
|
||||||
|
|
||||||
import styles from '../style.less';
|
|
||||||
type Props = {
|
|
||||||
themeData: any;
|
|
||||||
metricList: any[];
|
|
||||||
domainId: number;
|
|
||||||
onSubmit: (params?: any) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const FormItem = Form.Item;
|
|
||||||
const Option = Select.Option;
|
|
||||||
|
|
||||||
const MetricSettingForm: ForwardRefRenderFunction<any, Props> = (
|
|
||||||
{ metricList, domainId, themeData: uniqueMetricData, onSubmit },
|
|
||||||
ref,
|
|
||||||
) => {
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
const [metricListOptions, setMetricListOptions] = useState<any>([]);
|
|
||||||
const [unitState, setUnit] = useState<number | null>();
|
|
||||||
const [periodState, setPeriod] = useState<string>();
|
|
||||||
const getFormValidateFields = async () => {
|
|
||||||
return await form.validateFields();
|
|
||||||
};
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
|
||||||
getFormValidateFields,
|
|
||||||
}));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
form.resetFields();
|
|
||||||
setUnit(null);
|
|
||||||
setPeriod('');
|
|
||||||
if (Object.keys(uniqueMetricData).length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { defaultMetrics = [], id } = uniqueMetricData;
|
|
||||||
const defaultMetric = defaultMetrics[0];
|
|
||||||
const recordId = id === -1 ? undefined : id;
|
|
||||||
if (defaultMetric) {
|
|
||||||
const { period, unit } = defaultMetric;
|
|
||||||
setUnit(unit);
|
|
||||||
setPeriod(period);
|
|
||||||
form.setFieldsValue({
|
|
||||||
...defaultMetric,
|
|
||||||
id: recordId,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
form.setFieldsValue({
|
|
||||||
id: recordId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [uniqueMetricData]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const metricOption = metricList.map((item: any) => {
|
|
||||||
return {
|
|
||||||
label: item.name,
|
|
||||||
value: item.id,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
setMetricListOptions(metricOption);
|
|
||||||
}, [metricList]);
|
|
||||||
|
|
||||||
const saveEntity = async () => {
|
|
||||||
const values = await form.validateFields();
|
|
||||||
const { id } = values;
|
|
||||||
let saveDomainExtendQuery = addDomainExtend;
|
|
||||||
if (id) {
|
|
||||||
saveDomainExtendQuery = editDomainExtend;
|
|
||||||
}
|
|
||||||
const { code, msg, data } = await saveDomainExtendQuery({
|
|
||||||
defaultMetrics: [{ ...values }],
|
|
||||||
domainId,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (code === 200) {
|
|
||||||
form.setFieldValue('id', data);
|
|
||||||
onSubmit?.();
|
|
||||||
message.success('保存成功');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
message.error(msg);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Form
|
|
||||||
{...formLayout}
|
|
||||||
form={form}
|
|
||||||
layout="vertical"
|
|
||||||
className={styles.form}
|
|
||||||
initialValues={{
|
|
||||||
unit: 7,
|
|
||||||
period: 'DAY',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FormItem hidden={true} name="id" label="ID">
|
|
||||||
<Input placeholder="id" />
|
|
||||||
</FormItem>
|
|
||||||
<FormItem
|
|
||||||
name={'metricId'}
|
|
||||||
label={
|
|
||||||
<FormItemTitle
|
|
||||||
title={'指标'}
|
|
||||||
subTitle={'问答搜索结果选择中,如果没有指定指标,将会采用默认指标进行展示'}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
allowClear
|
|
||||||
showSearch
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
filterOption={(inputValue: string, item: any) => {
|
|
||||||
const { label } = item;
|
|
||||||
if (label.includes(inputValue)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}}
|
|
||||||
placeholder="请选择展示指标信息"
|
|
||||||
options={metricListOptions}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem
|
|
||||||
label={
|
|
||||||
<FormItemTitle
|
|
||||||
title={'时间范围'}
|
|
||||||
subTitle={'问答搜索结果选择中,如果没有指定时间范围,将会采用默认时间范围'}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Input.Group compact>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
display: 'inline-block',
|
|
||||||
lineHeight: '32px',
|
|
||||||
marginRight: '8px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
最近
|
|
||||||
</span>
|
|
||||||
<InputNumber
|
|
||||||
value={unitState}
|
|
||||||
style={{ width: '120px' }}
|
|
||||||
onChange={(value) => {
|
|
||||||
setUnit(value);
|
|
||||||
form.setFieldValue('unit', value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
value={periodState}
|
|
||||||
style={{ width: '100px' }}
|
|
||||||
onChange={(value) => {
|
|
||||||
form.setFieldValue('period', value);
|
|
||||||
setPeriod(value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Option value="DAY">天</Option>
|
|
||||||
<Option value="WEEK">周</Option>
|
|
||||||
<Option value="MONTH">月</Option>
|
|
||||||
<Option value="YEAR">年</Option>
|
|
||||||
</Select>
|
|
||||||
</Input.Group>
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<FormItem name="unit" hidden={true}>
|
|
||||||
<InputNumber />
|
|
||||||
</FormItem>
|
|
||||||
<FormItem name="period" hidden={true}>
|
|
||||||
<Input />
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<FormItem>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
onClick={() => {
|
|
||||||
saveEntity();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
保 存
|
|
||||||
</Button>
|
|
||||||
</FormItem>
|
|
||||||
</Form>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default forwardRef(MetricSettingForm);
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: string;
|
||||||
|
labelStyles?: CSSStyleSheet;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FormLabelRequire: React.FC<Props> = ({ title, labelStyles = {} }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="ant-col ant-form-item-label">
|
||||||
|
<label
|
||||||
|
htmlFor="description"
|
||||||
|
className="ant-form-item-required"
|
||||||
|
title={title}
|
||||||
|
style={{ fontSize: '16px', ...labelStyles }}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FormLabelRequire;
|
||||||
@@ -26,7 +26,6 @@ const InfoTagList: React.FC<Props> = ({ value, createBtnString = '新增', onCha
|
|||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
const handleTagChange = (tagList: string[]) => {
|
const handleTagChange = (tagList: string[]) => {
|
||||||
console.log(tagList, 'tagList');
|
|
||||||
onChange?.(tagList);
|
onChange?.(tagList);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,33 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { Form, Button, Modal, Steps, Input, Select, Switch, InputNumber } from 'antd';
|
import {
|
||||||
|
Form,
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
Steps,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
Switch,
|
||||||
|
InputNumber,
|
||||||
|
message,
|
||||||
|
Result,
|
||||||
|
} from 'antd';
|
||||||
import MetricMeasuresFormTable from './MetricMeasuresFormTable';
|
import MetricMeasuresFormTable from './MetricMeasuresFormTable';
|
||||||
import { SENSITIVE_LEVEL_OPTIONS } from '../constant';
|
import { SENSITIVE_LEVEL_OPTIONS } from '../constant';
|
||||||
import { formLayout } from '@/components/FormHelper/utils';
|
import { formLayout } from '@/components/FormHelper/utils';
|
||||||
import FormItemTitle from '@/components/FormHelper/FormItemTitle';
|
import FormItemTitle from '@/components/FormHelper/FormItemTitle';
|
||||||
import styles from './style.less';
|
import styles from './style.less';
|
||||||
import { getMeasureListByDomainId } from '../service';
|
import { getMeasureListByDomainId } from '../service';
|
||||||
|
import { creatExprMetric, updateExprMetric } from '../service';
|
||||||
|
import { ISemantic } from '../data';
|
||||||
|
import { history } from 'umi';
|
||||||
|
|
||||||
export type CreateFormProps = {
|
export type CreateFormProps = {
|
||||||
|
datasourceId?: number;
|
||||||
domainId: number;
|
domainId: number;
|
||||||
createModalVisible: boolean;
|
createModalVisible: boolean;
|
||||||
metricItem: any;
|
metricItem: any;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
onSubmit: (values: any) => void;
|
onSubmit?: (values: any) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const { Step } = Steps;
|
const { Step } = Steps;
|
||||||
@@ -21,6 +36,7 @@ const { TextArea } = Input;
|
|||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
|
|
||||||
const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
|
const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
|
||||||
|
datasourceId,
|
||||||
domainId,
|
domainId,
|
||||||
onCancel,
|
onCancel,
|
||||||
createModalVisible,
|
createModalVisible,
|
||||||
@@ -31,17 +47,18 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
|
|||||||
const [currentStep, setCurrentStep] = useState(0);
|
const [currentStep, setCurrentStep] = useState(0);
|
||||||
const formValRef = useRef({} as any);
|
const formValRef = useRef({} as any);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const updateFormVal = (val: SaveDataSetForm) => {
|
const updateFormVal = (val: any) => {
|
||||||
formValRef.current = val;
|
formValRef.current = val;
|
||||||
};
|
};
|
||||||
|
|
||||||
const [classMeasureList, setClassMeasureList] = useState<any[]>([]);
|
const [classMeasureList, setClassMeasureList] = useState<ISemantic.IMeasure[]>([]);
|
||||||
|
|
||||||
const [exprTypeParamsState, setExprTypeParamsState] = useState<any>([]);
|
const [exprTypeParamsState, setExprTypeParamsState] = useState<ISemantic.IMeasure[]>([]);
|
||||||
|
|
||||||
const [exprSql, setExprSql] = useState<string>('');
|
const [exprSql, setExprSql] = useState<string>('');
|
||||||
|
|
||||||
const [isPercentState, setIsPercentState] = useState<boolean>(false);
|
const [isPercentState, setIsPercentState] = useState<boolean>(false);
|
||||||
|
const [hasMeasuresState, setHasMeasuresState] = useState<boolean>(true);
|
||||||
|
|
||||||
const forward = () => setCurrentStep(currentStep + 1);
|
const forward = () => setCurrentStep(currentStep + 1);
|
||||||
const backward = () => setCurrentStep(currentStep - 1);
|
const backward = () => setCurrentStep(currentStep - 1);
|
||||||
@@ -50,6 +67,12 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
|
|||||||
const { code, data } = await getMeasureListByDomainId(domainId);
|
const { code, data } = await getMeasureListByDomainId(domainId);
|
||||||
if (code === 200) {
|
if (code === 200) {
|
||||||
setClassMeasureList(data);
|
setClassMeasureList(data);
|
||||||
|
if (datasourceId) {
|
||||||
|
const hasMeasures = data.some(
|
||||||
|
(item: ISemantic.IMeasure) => item.datasourceId === datasourceId,
|
||||||
|
);
|
||||||
|
setHasMeasuresState(hasMeasures);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setClassMeasureList([]);
|
setClassMeasureList([]);
|
||||||
@@ -74,7 +97,8 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
|
|||||||
if (currentStep < 1) {
|
if (currentStep < 1) {
|
||||||
forward();
|
forward();
|
||||||
} else {
|
} else {
|
||||||
onSubmit?.(submitForm);
|
// onSubmit?.(submitForm);
|
||||||
|
await saveMetric(submitForm);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -118,15 +142,41 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
initData();
|
initData();
|
||||||
} else {
|
|
||||||
// initFields([]);
|
|
||||||
}
|
}
|
||||||
}, [metricItem]);
|
}, [metricItem]);
|
||||||
|
|
||||||
|
const saveMetric = async (fieldsValue: any) => {
|
||||||
|
const queryParams = {
|
||||||
|
domainId,
|
||||||
|
...fieldsValue,
|
||||||
|
};
|
||||||
|
const { typeParams } = queryParams;
|
||||||
|
if (!typeParams?.expr) {
|
||||||
|
message.error('请输入度量表达式');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!(Array.isArray(typeParams?.measures) && typeParams.measures.length > 0)) {
|
||||||
|
message.error('请添加一个度量');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let saveMetricQuery = creatExprMetric;
|
||||||
|
if (queryParams.id) {
|
||||||
|
saveMetricQuery = updateExprMetric;
|
||||||
|
}
|
||||||
|
const { code, msg } = await saveMetricQuery(queryParams);
|
||||||
|
if (code === 200) {
|
||||||
|
message.success('编辑指标成功');
|
||||||
|
onSubmit?.(queryParams);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
message.error(msg);
|
||||||
|
};
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
if (currentStep === 1) {
|
if (currentStep === 1) {
|
||||||
return (
|
return (
|
||||||
<MetricMeasuresFormTable
|
<MetricMeasuresFormTable
|
||||||
|
datasourceId={datasourceId}
|
||||||
typeParams={{
|
typeParams={{
|
||||||
measures: exprTypeParamsState,
|
measures: exprTypeParamsState,
|
||||||
expr: exprSql,
|
expr: exprSql,
|
||||||
@@ -231,6 +281,9 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
const renderFooter = () => {
|
const renderFooter = () => {
|
||||||
|
if (!hasMeasuresState) {
|
||||||
|
return <Button onClick={onCancel}>取消</Button>;
|
||||||
|
}
|
||||||
if (currentStep === 1) {
|
if (currentStep === 1) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -266,6 +319,8 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
|
|||||||
footer={renderFooter()}
|
footer={renderFooter()}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
>
|
>
|
||||||
|
{hasMeasuresState ? (
|
||||||
|
<>
|
||||||
<Steps style={{ marginBottom: 28 }} size="small" current={currentStep}>
|
<Steps style={{ marginBottom: 28 }} size="small" current={currentStep}>
|
||||||
<Step title="基本信息" />
|
<Step title="基本信息" />
|
||||||
<Step title="度量信息" />
|
<Step title="度量信息" />
|
||||||
@@ -286,6 +341,25 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
|
|||||||
>
|
>
|
||||||
{renderContent()}
|
{renderContent()}
|
||||||
</Form>
|
</Form>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Result
|
||||||
|
status="warning"
|
||||||
|
subTitle="当前数据源缺少度量,无法创建指标。请前往数据源配置中,将字段设置为度量"
|
||||||
|
extra={
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
key="console"
|
||||||
|
onClick={() => {
|
||||||
|
history.replace(`/semanticModel/${domainId}/dataSource`);
|
||||||
|
onCancel?.();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
去创建
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,17 +4,21 @@ import ProTable from '@ant-design/pro-table';
|
|||||||
import ProCard from '@ant-design/pro-card';
|
import ProCard from '@ant-design/pro-card';
|
||||||
import SqlEditor from '@/components/SqlEditor';
|
import SqlEditor from '@/components/SqlEditor';
|
||||||
import BindMeasuresTable from './BindMeasuresTable';
|
import BindMeasuresTable from './BindMeasuresTable';
|
||||||
|
import FormLabelRequire from './FormLabelRequire';
|
||||||
|
import { ISemantic } from '../data';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
typeParams: any;
|
datasourceId?: number;
|
||||||
measuresList: any[];
|
typeParams: ISemantic.ITypeParams;
|
||||||
onFieldChange: (measures: any[]) => void;
|
measuresList: ISemantic.IMeasure[];
|
||||||
|
onFieldChange: (measures: ISemantic.IMeasure[]) => void;
|
||||||
onSqlChange: (sql: string) => void;
|
onSqlChange: (sql: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
|
|
||||||
const MetricMeasuresFormTable: React.FC<Props> = ({
|
const MetricMeasuresFormTable: React.FC<Props> = ({
|
||||||
|
datasourceId,
|
||||||
typeParams,
|
typeParams,
|
||||||
measuresList,
|
measuresList,
|
||||||
onFieldChange,
|
onFieldChange,
|
||||||
@@ -127,7 +131,7 @@ const MetricMeasuresFormTable: React.FC<Props> = ({
|
|||||||
<Space direction="vertical" style={{ width: '100%' }}>
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
<ProTable
|
<ProTable
|
||||||
actionRef={actionRef}
|
actionRef={actionRef}
|
||||||
headerTitle="度量列表"
|
headerTitle={<FormLabelRequire title="度量列表" />}
|
||||||
tooltip="基于本主题域下所有数据源的度量来创建指标,且该列表的度量为了加以区分,均已加上数据源名称作为前缀,选中度量后,可基于这几个度量来写表达式,若是选中的度量来自不同的数据源,系统将会自动join来计算该指标"
|
tooltip="基于本主题域下所有数据源的度量来创建指标,且该列表的度量为了加以区分,均已加上数据源名称作为前缀,选中度量后,可基于这几个度量来写表达式,若是选中的度量来自不同的数据源,系统将会自动join来计算该指标"
|
||||||
rowKey="name"
|
rowKey="name"
|
||||||
columns={columns}
|
columns={columns}
|
||||||
@@ -149,7 +153,7 @@ const MetricMeasuresFormTable: React.FC<Props> = ({
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<ProCard
|
<ProCard
|
||||||
title={'度量表达式'}
|
title={<FormLabelRequire title="度量表达式" />}
|
||||||
tooltip="度量表达式由上面选择的度量组成,如选择了度量A和B,则可将表达式写成A+B"
|
tooltip="度量表达式由上面选择的度量组成,如选择了度量A和B,则可将表达式写成A+B"
|
||||||
>
|
>
|
||||||
<SqlEditor
|
<SqlEditor
|
||||||
@@ -165,7 +169,11 @@ const MetricMeasuresFormTable: React.FC<Props> = ({
|
|||||||
</Space>
|
</Space>
|
||||||
{measuresModalVisible && (
|
{measuresModalVisible && (
|
||||||
<BindMeasuresTable
|
<BindMeasuresTable
|
||||||
measuresList={measuresList}
|
measuresList={
|
||||||
|
datasourceId && Array.isArray(measuresList)
|
||||||
|
? measuresList.filter((item) => item.datasourceId === datasourceId)
|
||||||
|
: measuresList
|
||||||
|
}
|
||||||
selectedMeasuresList={measuresParams?.measures || []}
|
selectedMeasuresList={measuresParams?.measures || []}
|
||||||
onSubmit={async (values: any[]) => {
|
onSubmit={async (values: any[]) => {
|
||||||
const measures = values.map(({ bizName, name, expr, datasourceId }) => {
|
const measures = values.map(({ bizName, name, expr, datasourceId }) => {
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { Space, Tooltip } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
import { ExclamationCircleOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: string;
|
||||||
|
tooltips: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TableTitleTooltips: React.FC<Props> = ({ title, tooltips }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Space>
|
||||||
|
<span>{title}</span>
|
||||||
|
<Tooltip title={tooltips}>
|
||||||
|
<ExclamationCircleOutlined />
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TableTitleTooltips;
|
||||||
@@ -298,3 +298,17 @@
|
|||||||
border-style: dashed;
|
border-style: dashed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.semanticGraphCanvas {
|
||||||
|
position: relative;
|
||||||
|
.toolbar{
|
||||||
|
position: absolute;
|
||||||
|
width: 200px;
|
||||||
|
z-index: 999;
|
||||||
|
right: 0;
|
||||||
|
top: 5px;
|
||||||
|
}
|
||||||
|
.canvasContainer {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { SemanticNodeType } from './enum';
|
||||||
|
|
||||||
export const SENSITIVE_LEVEL_OPTIONS = [
|
export const SENSITIVE_LEVEL_OPTIONS = [
|
||||||
{
|
{
|
||||||
label: '低',
|
label: '低',
|
||||||
@@ -21,3 +23,21 @@ export const SENSITIVE_LEVEL_ENUM = SENSITIVE_LEVEL_OPTIONS.reduce(
|
|||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const SEMANTIC_NODE_TYPE_CONFIG = {
|
||||||
|
[SemanticNodeType.DATASOURCE]: {
|
||||||
|
label: '数据源',
|
||||||
|
value: SemanticNodeType.DATASOURCE,
|
||||||
|
color: 'cyan',
|
||||||
|
},
|
||||||
|
[SemanticNodeType.DIMENSION]: {
|
||||||
|
label: '维度',
|
||||||
|
value: SemanticNodeType.DIMENSION,
|
||||||
|
color: 'blue',
|
||||||
|
},
|
||||||
|
[SemanticNodeType.METRIC]: {
|
||||||
|
label: '指标',
|
||||||
|
value: SemanticNodeType.METRIC,
|
||||||
|
color: 'orange',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { TreeGraphData } from '@antv/g6-core';
|
||||||
|
|
||||||
export type ISODateString =
|
export type ISODateString =
|
||||||
`${number}-${number}-${number}T${number}:${number}:${number}.${number}+${number}:${number}`;
|
`${number}-${number}-${number}T${number}:${number}:${number}.${number}+${number}:${number}`;
|
||||||
|
|
||||||
@@ -6,6 +8,8 @@ export type UserName = string;
|
|||||||
|
|
||||||
export type SensitiveLevel = 0 | 1 | 2 | null;
|
export type SensitiveLevel = 0 | 1 | 2 | null;
|
||||||
|
|
||||||
|
export type RefreshGraphData = (graphRootData: TreeGraphData) => void;
|
||||||
|
|
||||||
export declare namespace IDataSource {
|
export declare namespace IDataSource {
|
||||||
interface IIdentifiersItem {
|
interface IIdentifiersItem {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -113,13 +117,13 @@ export declare namespace ISemantic {
|
|||||||
|
|
||||||
interface IMeasure {
|
interface IMeasure {
|
||||||
name: string;
|
name: string;
|
||||||
agg: string;
|
agg?: string;
|
||||||
expr: string;
|
expr: string;
|
||||||
constraint: string;
|
constraint?: string;
|
||||||
alias: string;
|
alias?: string;
|
||||||
createMetric: string;
|
createMetric?: string;
|
||||||
bizName: string;
|
bizName: string;
|
||||||
isCreateMetric: number;
|
isCreateMetric?: number;
|
||||||
datasourceId: number;
|
datasourceId: number;
|
||||||
}
|
}
|
||||||
interface ITypeParams {
|
interface ITypeParams {
|
||||||
@@ -142,7 +146,7 @@ export declare namespace ISemantic {
|
|||||||
domainId: number;
|
domainId: number;
|
||||||
domainName: string;
|
domainName: string;
|
||||||
type: string;
|
type: string;
|
||||||
typeParams: TypeParams;
|
typeParams: ITypeParams;
|
||||||
fullPath: string;
|
fullPath: string;
|
||||||
dataFormatType: string;
|
dataFormatType: string;
|
||||||
dataFormat: string;
|
dataFormat: string;
|
||||||
|
|||||||
@@ -7,3 +7,9 @@ export enum TransType {
|
|||||||
DIMENSION = 'dimension',
|
DIMENSION = 'dimension',
|
||||||
METRIC = 'metric',
|
METRIC = 'metric',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum SemanticNodeType {
|
||||||
|
DATASOURCE = 'datasource',
|
||||||
|
DIMENSION = 'dimension',
|
||||||
|
METRIC = 'metric',
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user