first commit

This commit is contained in:
jerryjzhang
2023-06-12 18:44:01 +08:00
commit dc4fc69b57
879 changed files with 573090 additions and 0 deletions

View File

@@ -0,0 +1,116 @@
import { Tabs } from 'antd';
import React, { useEffect, useState } from 'react';
import { connect, Helmet } from 'umi';
import ProjectListTree from './components/ProjectList';
import EntitySection from './components/Entity/EntitySection';
import styles from './components/style.less';
import type { StateType } from './model';
import { RightOutlined, LeftOutlined } from '@ant-design/icons';
import SplitPane from 'react-split-pane';
import Pane from 'react-split-pane/lib/Pane';
import type { Dispatch } from 'umi';
const { TabPane } = Tabs;
type Props = {
domainManger: StateType;
dispatch: Dispatch;
};
const DEFAULT_LEFT_SIZE = '300px';
const ChatSetting: React.FC<Props> = ({ domainManger, dispatch }) => {
window.RUNNING_ENV = 'chat';
const [collapsed, setCollapsed] = useState(false);
const [leftSize, setLeftSize] = useState('');
const { selectDomainId, selectDomainName } = domainManger;
useEffect(() => {
const semanticLeftCollapsed = localStorage.getItem('semanticLeftCollapsed');
const semanticLeftSize =
semanticLeftCollapsed === 'true' ? '0px' : localStorage.getItem('semanticLeftSize');
setCollapsed(semanticLeftCollapsed === 'true');
setLeftSize(semanticLeftSize || DEFAULT_LEFT_SIZE);
}, []);
useEffect(() => {
if (selectDomainId) {
dispatch({
type: 'domainManger/queryDimensionList',
payload: {
domainId: selectDomainId,
},
});
dispatch({
type: 'domainManger/queryMetricList',
payload: {
domainId: selectDomainId,
},
});
}
}, [selectDomainId]);
const onCollapse = () => {
const collapsedValue = !collapsed;
setCollapsed(collapsedValue);
localStorage.setItem('semanticLeftCollapsed', String(collapsedValue));
const semanticLeftSize = collapsedValue ? '0px' : localStorage.getItem('semanticLeftSize');
const sizeValue = parseInt(semanticLeftSize || '0');
if (!collapsedValue && sizeValue <= 10) {
setLeftSize(DEFAULT_LEFT_SIZE);
localStorage.setItem('semanticLeftSize', DEFAULT_LEFT_SIZE);
} else {
setLeftSize(semanticLeftSize || DEFAULT_LEFT_SIZE);
}
};
useEffect(() => {
const width = document.getElementById('tab');
const switchWarpper: any = document.getElementById('switch');
if (width && switchWarpper) {
switchWarpper.style.width = width.offsetWidth * 0.77 + 'px';
}
});
return (
<div className={styles.projectBody}>
<Helmet title={'问答设置-超音数'} />
<SplitPane
split="vertical"
onChange={(size) => {
localStorage.setItem('semanticLeftSize', size[0]);
setLeftSize(size[0]);
}}
>
<Pane initialSize={leftSize || DEFAULT_LEFT_SIZE}>
<div className={styles.menu}>
<ProjectListTree createDomainBtnVisible={false} queryService="chat" />
</div>
</Pane>
<div className={styles.projectManger}>
<div className={styles.collapseLeftBtn} onClick={onCollapse}>
{collapsed ? <RightOutlined /> : <LeftOutlined />}
</div>
<h2 className={styles.title}>
{selectDomainName ? `选择的主题域:${selectDomainName}` : '主题域信息'}
</h2>
{selectDomainId ? (
<>
<Tabs className={styles.tab} defaultActiveKey="chatSetting" destroyInactiveTabPane>
<TabPane className={styles.tabPane} tab="问答设置" key="chatSetting">
<EntitySection />
</TabPane>
</Tabs>
</>
) : (
<h2 className={styles.mainTip}></h2>
)}
</div>
</SplitPane>
</div>
);
};
export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger,
}))(ChatSetting);

View File

@@ -0,0 +1,38 @@
import React from 'react';
import { Form, Input, Spin } from 'antd';
import type { FormInstance } from 'antd/lib/form';
const FormItem = Form.Item;
const { TextArea } = Input;
type Props = {
isEdit?: boolean;
form: FormInstance<any>;
tableLoading?: boolean;
};
const DataSourceBasicForm: React.FC<Props> = ({ isEdit, tableLoading = false }) => {
return (
<Spin spinning={tableLoading}>
<FormItem
name="name"
label="数据源中文名"
rules={[{ required: true, message: '请输入数据源中文名' }]}
>
<Input placeholder="名称不可重复" />
</FormItem>
<FormItem
name="bizName"
label="数据源英文名"
rules={[{ required: true, message: '请输入数据源英文名' }]}
>
<Input placeholder="名称不可重复" disabled={isEdit} />
</FormItem>
<FormItem name="description" label="数据源描述">
<TextArea placeholder="请输入数据源描述" />
</FormItem>
</Spin>
);
};
export default DataSourceBasicForm;

View File

@@ -0,0 +1,291 @@
import React, { useEffect, useRef, useState } from 'react';
import { Form, Button, Modal, Steps, message } from 'antd';
import BasicInfoForm from './DataSourceBasicForm';
import FieldForm from './DataSourceFieldForm';
import { formLayout } from '@/components/FormHelper/utils';
import { EnumDataSourceType } from '../constants';
import type { DataInstanceItem, FieldItem, SaveDataSetForm } from '../data';
import styles from '../style.less';
import { createDatasource, updateDatasource } from '../../service';
export type CreateFormProps = {
createModalVisible: boolean;
sql: string;
domainId: number;
dataSourceItem: DataInstanceItem | any;
onCancel?: () => void;
onSubmit?: (dataSourceInfo: any) => void;
scriptColumns: any[];
};
const { Step } = Steps;
const initFormVal = {
name: '', // 数据源名称
bizName: '', // 数据源英文名
description: '', // 数据源描述
};
const DataSourceCreateForm: React.FC<CreateFormProps> = ({
onCancel,
createModalVisible,
domainId,
scriptColumns,
sql,
onSubmit,
dataSourceItem,
}) => {
const isEdit = !!dataSourceItem?.id;
const [fields, setFields] = useState<FieldItem[]>([]);
const [currentStep, setCurrentStep] = useState(0);
const [saveLoading, setSaveLoading] = useState(false);
const formValRef = useRef(initFormVal as any);
const [form] = Form.useForm();
const updateFormVal = (val: SaveDataSetForm) => {
formValRef.current = val;
};
const forward = () => setCurrentStep(currentStep + 1);
const backward = () => setCurrentStep(currentStep - 1);
const getFieldsClassify = (fieldsList: FieldItem[]) => {
const classify = fieldsList.reduce(
(fieldsClassify, item: FieldItem) => {
const {
type,
bizName,
timeGranularity,
agg,
isCreateDimension,
name,
isCreateMetric,
dateFormat,
} = item;
switch (type) {
case EnumDataSourceType.CATEGORICAL:
fieldsClassify.dimensions.push({
bizName,
type,
isCreateDimension,
name,
});
break;
case EnumDataSourceType.TIME:
fieldsClassify.dimensions.push({
bizName,
type,
isCreateDimension,
name,
dateFormat,
typeParams: {
isPrimary: true,
timeGranularity,
},
});
break;
case EnumDataSourceType.FOREIGN:
case EnumDataSourceType.PRIMARY:
fieldsClassify.identifiers.push({
bizName,
name,
type,
});
break;
case EnumDataSourceType.MEASURES:
fieldsClassify.measures.push({
bizName,
type,
agg,
name,
isCreateMetric,
});
break;
default:
break;
}
return fieldsClassify;
},
{
identifiers: [],
dimensions: [],
measures: [],
} as any,
);
return classify;
};
const handleNext = async () => {
const fieldsValue = await form.validateFields();
const fieldsClassify = getFieldsClassify(fields);
const submitForm = {
...formValRef.current,
...fieldsValue,
...fieldsClassify,
};
updateFormVal(submitForm);
if (currentStep < 1) {
forward();
} else {
setSaveLoading(true);
const queryParams = {
...submitForm,
sqlQuery: sql,
databaseId: dataSourceItem.databaseId,
queryType: 'sql_query',
domainId,
};
const queryDatasource = isEdit ? updateDatasource : createDatasource;
const { code, msg, data } = await queryDatasource(queryParams);
setSaveLoading(false);
if (code === 200) {
message.success('保存数据源成功!');
onSubmit?.({
...queryParams,
...data,
resData: data,
});
return;
}
message.error(msg);
}
};
const initFields = (fieldsClassifyList: any[]) => {
const columnFields: any[] = scriptColumns.map((item: any) => {
const { type, nameEn } = item;
const oldItem = fieldsClassifyList.find((oItem) => oItem.bizName === item.nameEn) || {};
return {
...oldItem,
bizName: nameEn,
// name,
sqlType: type,
};
});
setFields(columnFields || []);
};
const formatterMeasures = (measuresList: any[] = []) => {
return measuresList.map((measures: any) => {
return {
...measures,
type: EnumDataSourceType.MEASURES,
};
});
};
const formatterDimensions = (dimensionsList: any[] = []) => {
return dimensionsList.map((dimension: any) => {
const { typeParams } = dimension;
return {
...dimension,
timeGranularity: typeParams?.timeGranularity || '',
};
});
};
const initData = () => {
const { id, name, bizName, description, datasourceDetail } = dataSourceItem as any;
const initValue = {
id,
name,
bizName,
description,
};
const editInitFormVal = {
...formValRef.current,
...initValue,
};
updateFormVal(editInitFormVal);
form.setFieldsValue(initValue);
const { dimensions, identifiers, measures } = datasourceDetail;
const formatFields = [
...formatterDimensions(dimensions || []),
...(identifiers || []),
...formatterMeasures(measures || []),
];
initFields(formatFields);
};
useEffect(() => {
if (isEdit) {
initData();
} else {
initFields([]);
}
}, [dataSourceItem]);
const handleFieldChange = (fieldName: string, data: any) => {
const result = fields.map((field) => {
if (field.bizName === fieldName) {
return {
...field,
...data,
};
}
return {
...field,
};
});
setFields(result);
};
const renderContent = () => {
if (currentStep === 1) {
return <FieldForm fields={fields} onFieldChange={handleFieldChange} />;
}
return <BasicInfoForm form={form} isEdit={isEdit} />;
};
const renderFooter = () => {
if (currentStep === 1) {
return (
<>
<Button style={{ float: 'left' }} onClick={backward}>
</Button>
<Button onClick={onCancel}></Button>
<Button type="primary" loading={saveLoading} onClick={handleNext}>
</Button>
</>
);
}
return (
<>
<Button onClick={onCancel}></Button>
<Button type="primary" onClick={handleNext}>
</Button>
</>
);
};
return (
<Modal
forceRender
width={1300}
bodyStyle={{ padding: '32px 40px 48px' }}
destroyOnClose
title={`${isEdit ? '编辑' : '新建'}数据源`}
maskClosable={false}
open={createModalVisible}
footer={renderFooter()}
onCancel={onCancel}
>
<Steps style={{ marginBottom: 28 }} size="small" current={currentStep}>
<Step title="基本信息" />
<Step title="字段信息" />
</Steps>
<Form
{...formLayout}
form={form}
initialValues={{
...formValRef.current,
}}
className={styles.form}
>
{renderContent()}
</Form>
</Modal>
);
};
export default DataSourceCreateForm;

View File

@@ -0,0 +1,196 @@
import React from 'react';
import { Table, Select, Checkbox, Input } from 'antd';
import type { FieldItem } from '../data';
import { isUndefined } from 'lodash';
import { TYPE_OPTIONS, DATE_FORMATTER, AGG_OPTIONS, EnumDataSourceType } from '../constants';
type Props = {
fields: FieldItem[];
onFieldChange: (fieldName: string, data: Partial<FieldItem>) => void;
};
const { Option } = Select;
const FieldForm: React.FC<Props> = ({ fields, onFieldChange }) => {
const handleFieldChange = (record: FieldItem, fieldName: string, value: any) => {
onFieldChange(record.bizName, {
...record,
[fieldName]: value,
});
};
const columns = [
{
title: '字段名称',
dataIndex: 'bizName',
width: 100,
},
{
title: '数据类型',
dataIndex: 'sqlType',
width: 80,
},
{
title: '字段类型',
dataIndex: 'type',
width: 100,
render: (_: any, record: FieldItem) => {
const type = fields.find((field) => field.bizName === record.bizName)?.type;
return (
<Select
placeholder="字段类型"
value={type}
onChange={(value) => {
let defaultParams = {};
if (value === EnumDataSourceType.MEASURES) {
defaultParams = {
agg: AGG_OPTIONS[0].value,
};
} else if (value === EnumDataSourceType.TIME) {
defaultParams = {
dateFormat: DATE_FORMATTER[0],
timeGranularity: 'day',
};
} else {
defaultParams = {
agg: undefined,
dateFormat: undefined,
timeGranularity: undefined,
};
}
// handleFieldChange(record, 'type', value);
onFieldChange(record.bizName, {
...record,
type: value,
...defaultParams,
});
}}
style={{ width: '100%' }}
>
{TYPE_OPTIONS.map((item) => (
<Option key={item.label} value={item.value}>
{item.label}
</Option>
))}
</Select>
);
},
},
{
title: '扩展配置',
dataIndex: 'extender',
width: 100,
render: (_: any, record: FieldItem) => {
const { type } = record;
if (type === EnumDataSourceType.MEASURES) {
const agg = fields.find((field) => field.bizName === record.bizName)?.agg;
return (
<Select
placeholder="度量算子"
value={agg}
onChange={(value) => {
handleFieldChange(record, 'agg', value);
}}
defaultValue={AGG_OPTIONS[0].value}
style={{ width: '100%' }}
>
{AGG_OPTIONS.map((item) => (
<Option key={item.value} value={item.value}>
{item.label}
</Option>
))}
</Select>
);
}
if (type === EnumDataSourceType.TIME) {
const dateFormat = fields.find((field) => field.bizName === record.bizName)?.dateFormat;
return (
<Select
placeholder="时间格式"
value={dateFormat}
onChange={(value) => {
handleFieldChange(record, 'dateFormat', value);
}}
defaultValue={DATE_FORMATTER[0]}
style={{ width: '100%' }}
>
{DATE_FORMATTER.map((item) => (
<Option key={item} value={item}>
{item}
</Option>
))}
</Select>
);
}
return <></>;
},
},
{
title: '快速创建',
dataIndex: 'fastCreate',
width: 100,
render: (_: any, record: FieldItem) => {
const { type, name } = record;
if (
[
EnumDataSourceType.PRIMARY,
EnumDataSourceType.FOREIGN,
EnumDataSourceType.CATEGORICAL,
EnumDataSourceType.TIME,
EnumDataSourceType.MEASURES,
].includes(type as EnumDataSourceType)
) {
const isCreateName = [EnumDataSourceType.CATEGORICAL, EnumDataSourceType.TIME].includes(
type as EnumDataSourceType,
)
? 'isCreateDimension'
: 'isCreateMetric';
const editState = !isUndefined(record[isCreateName]) ? !!record[isCreateName] : true;
return (
<Checkbox
checked={editState}
onChange={(e) => {
const value = e.target.checked ? 1 : 0;
if (!value) {
onFieldChange(record.bizName, {
...record,
name: '',
[isCreateName]: value,
});
} else {
handleFieldChange(record, isCreateName, value);
}
}}
>
<Input
value={name}
disabled={!editState}
onChange={(e) => {
const value = e.target.value;
handleFieldChange(record, 'name', value);
}}
placeholder="请输入中文名"
/>
</Checkbox>
);
}
return <></>;
},
},
];
return (
<>
<Table<FieldItem>
dataSource={fields}
columns={columns}
className="fields-table"
rowKey="bizName"
pagination={false}
scroll={{ y: 500 }}
/>
</>
);
};
export default FieldForm;

View File

@@ -0,0 +1,530 @@
import React, { useState, useEffect, useRef } from 'react';
import { Button, Table, message, Tooltip, Space, Dropdown } from 'antd';
import SplitPane from 'react-split-pane';
import Pane from 'react-split-pane/lib/Pane';
import sqlFormatter from 'sql-formatter';
import {
FullscreenOutlined,
WarningOutlined,
EditOutlined,
PlayCircleTwoTone,
SwapOutlined,
PlayCircleOutlined,
CloudServerOutlined,
} from '@ant-design/icons';
import { isFunction } from 'lodash';
import FullScreen from '@/components/FullScreen';
import SqlEditor from '@/components/SqlEditor';
import type { TaskResultParams, TaskResultItem, DataInstanceItem, TaskResultColumn } from '../data';
import { excuteSql } from '../service';
import { getDatabaseByDomainId } from '../../service';
import DataSourceCreateForm from './DataSourceCreateForm';
import styles from '../style.less';
import 'ace-builds/src-min-noconflict/ext-searchbox';
import 'ace-builds/src-min-noconflict/theme-sqlserver';
import 'ace-builds/src-min-noconflict/theme-monokai';
import 'ace-builds/src-min-noconflict/mode-sql';
type IProps = {
oprType: 'add' | 'edit';
dataSourceItem: DataInstanceItem;
domainId: number;
onUpdateSql?: (sql: string) => void;
sql?: string;
onSubmitSuccess?: (dataSourceInfo: any) => void;
onJdbcSourceChange?: (jdbcId: number) => void;
};
type ResultTableItem = Record<string, any>;
type ResultColItem = {
key: string;
title: string;
dataIndex: string;
};
type ScreenSize = 'small' | 'middle' | 'large';
type JdbcSourceItems = {
label: string;
key: number;
};
const SqlDetail: React.FC<IProps> = ({
dataSourceItem,
onSubmitSuccess,
domainId,
sql = '',
onUpdateSql,
onJdbcSourceChange,
}) => {
const [resultTable, setResultTable] = useState<ResultTableItem[]>([]);
const [resultTableLoading, setResultTableLoading] = useState(false);
const [resultCols, setResultCols] = useState<ResultColItem[]>([]);
const [pagination, setPagination] = useState({
current: 1,
pageSize: 20,
total: 0,
});
const [jdbcSourceItems, setJdbcSourceItems] = useState<JdbcSourceItems[]>([]);
const [dataSourceModalVisible, setDataSourceModalVisible] = useState(false);
const [tableScroll, setTableScroll] = useState({
scrollToFirstRowOnChange: true,
x: '100%',
y: 200,
});
// const [dataSourceResult, setDataSourceResult] = useState<any>({});
const [runState, setRunState] = useState<boolean | undefined>();
const [taskLog, setTaskLog] = useState('');
const [isSqlExcLocked, setIsSqlExcLocked] = useState(false);
const [screenSize, setScreenSize] = useState<ScreenSize>('middle');
const [isSqlIdeFullScreen, setIsSqlIdeFullScreen] = useState<boolean>(false);
const [isSqlResFullScreen, setIsSqlResFullScreen] = useState<boolean>(false);
// const [sqlParams, setSqlParams] = useState<SqlParamsItem[]>([]);
const resultInnerWrap = useRef<HTMLDivElement>();
const [editorSize, setEditorSize] = useState<number>(0);
const DEFAULT_FULLSCREEN_TOP = 0;
const [partialSql, setPartialSql] = useState('');
const [isPartial, setIsPartial] = useState(false);
const [isRight, setIsRight] = useState(false);
const [scriptColumns, setScriptColumns] = useState<any[]>([]);
// const [jdbcSourceName, setJdbcSourceName] = useState<string>(() => {
// const sourceId = dataSourceItem.databaseId;
// if (sourceId) {
// const target: any = jdbcSourceItems.filter((item: any) => {
// return item.key === Number(sourceId);
// })[0];
// if (target) {
// return target.label;
// }
// }
// return 'ClickHouse';
// });
const queryDatabaseConfig = async () => {
const { code, data } = await getDatabaseByDomainId(domainId);
if (code === 200) {
setJdbcSourceItems([
{
label: data?.name,
key: data?.id,
},
]);
onJdbcSourceChange?.(data?.id && Number(data?.id));
return;
}
message.error('数据库配置获取错误');
};
function creatCalcItem(key: string, data: string) {
const line = document.createElement('div'); // 需要每条数据一行,这样避免数据换行的时候获得的宽度不准确
const child = document.createElement('span');
child.classList.add(`resultCalcItem_${key}`);
child.innerText = data;
line.appendChild(child);
return line;
}
// 计算每列的宽度,通过容器插入文档中动态得到该列数据(包括表头)的最长宽度,设为列宽度,保证每列的数据都能一行展示完
function getKeyWidthMap(list: TaskResultItem[]): TaskResultItem {
const widthMap = {};
const container = document.createElement('div');
container.id = 'resultCalcWrap';
container.style.position = 'fixed';
container.style.left = '-99999px';
container.style.top = '-99999px';
container.style.width = '19999px';
container.style.fontSize = '12px';
list.forEach((item, index) => {
if (index === 0) {
Object.keys(item).forEach((key, keyIndex) => {
// 因为key可能存在一些特殊字符导致querySelectorAll获取的时候报错所以用keyIndex(而不用key)拼接className
container.appendChild(creatCalcItem(`${keyIndex}`, key));
container.appendChild(creatCalcItem(`${keyIndex}`, `${item[key]}`));
});
} else {
Object.keys(item).forEach((key, keyIndex) => {
container.appendChild(creatCalcItem(`${keyIndex}`, `${item[key]}`));
});
}
});
document.body.appendChild(container);
Object.keys(list[0]).forEach((key, keyIndex) => {
// 因为key可能存在一些特殊字符导致querySelectorAll获取的时候报错所以用keyIndex(而不用key)拼接className
const widthArr = Array.from(container.querySelectorAll(`.resultCalcItem_${keyIndex}`)).map(
(node: any) => node.offsetWidth,
);
widthMap[key] = Math.max(...widthArr);
});
document.body.removeChild(container);
return widthMap;
}
const updateResultCols = (list: TaskResultItem[], columns: TaskResultColumn[]) => {
if (list.length) {
const widthMap = getKeyWidthMap(list);
const cols = columns.map(({ nameEn }) => {
return {
key: nameEn,
title: nameEn,
dataIndex: nameEn,
width: `${(widthMap[nameEn] as number) + 22}px`, // 字宽度 + 20px(比左右padding宽几像素作为一个buffer值)
};
});
setResultCols(cols);
}
};
const fetchTaskResult = (params: TaskResultParams) => {
setResultTable(
params.resultList.map((item, index) => {
return {
...item,
index,
};
}),
);
setPagination({
current: 1,
pageSize: 20,
total: params.resultList.length,
});
setScriptColumns(params.columns);
updateResultCols(params.resultList, params.columns);
};
const changePaging = (paging: Pagination) => {
setPagination({
...pagination,
...paging,
});
};
const onSqlChange = (sqlString: string) => {
if (onUpdateSql && isFunction(onUpdateSql)) {
onUpdateSql(sqlString);
}
};
const formatSQL = () => {
const sqlvalue = sqlFormatter.format(sql);
if (onUpdateSql && isFunction(onUpdateSql)) {
onUpdateSql(sqlvalue);
}
// eslint-disable-next-line no-param-reassign
sql = sqlvalue;
};
const separateSql = async (value: string) => {
setResultTableLoading(true);
const { code, data, msg } = await excuteSql({
sql: value,
domainId,
});
setResultTableLoading(false);
if (code === 200) {
// setDataSourceResult(data);
fetchTaskResult(data);
setRunState(true);
} else {
setRunState(false);
setTaskLog(msg);
}
};
const onSelect = (value: string) => {
if (value) {
setIsPartial(true);
setPartialSql(value);
} else {
setIsPartial(false);
}
};
const excuteScript = () => {
if (!sql) {
return message.error('SQL查询语句不可以为空');
}
if (isSqlExcLocked) {
return message.warning('请间隔5s再重新执行');
}
const waitTime = 5000;
setIsSqlExcLocked(true); // 加锁5s后再解锁
setTimeout(() => {
setIsSqlExcLocked(false);
}, waitTime);
return isPartial ? separateSql(partialSql) : separateSql(sql);
};
const showDataSetModal = () => {
setDataSourceModalVisible(true);
};
const startCreatDataSource = async () => {
showDataSetModal();
};
const updateNormalResScroll = () => {
const node = resultInnerWrap?.current;
if (node) {
setTableScroll({
scrollToFirstRowOnChange: true,
x: '100%',
y: node.clientHeight - 120,
});
}
};
const updateFullScreenResScroll = () => {
const windowHeight = window.innerHeight;
const paginationHeight = 96;
setTableScroll({
scrollToFirstRowOnChange: true,
x: '100%',
y: windowHeight - DEFAULT_FULLSCREEN_TOP - paginationHeight - 30, // 30为退出全屏按钮的高度
});
};
const handleFullScreenSqlIde = () => {
setIsSqlIdeFullScreen(true);
};
const handleNormalScreenSqlIde = () => {
setIsSqlIdeFullScreen(false);
};
const handleFullScreenSqlResult = () => {
setIsSqlResFullScreen(true);
};
const handleNormalScreenSqlResult = () => {
setIsSqlResFullScreen(false);
};
const handleThemeChange = () => {
setIsRight(!isRight);
};
const renderResult = () => {
if (runState === false) {
return (
<>
{
<div className={styles.taskFailed}>
<WarningOutlined className={styles.resultFailIcon} />
</div>
}
<div
className={styles.sqlResultLog}
dangerouslySetInnerHTML={{
__html: taskLog.replace(/\r\n/g, '<br/>').replace(/\t/g, '&nbsp;&nbsp;&nbsp;&nbsp;'),
}}
/>
</>
);
}
if (runState) {
return (
<>
<div className={styles.detail} />
<Table<TaskResultItem>
loading={resultTableLoading}
dataSource={resultTable}
columns={resultCols}
onChange={changePaging}
pagination={pagination}
scroll={tableScroll}
className={styles.resultTable}
rowClassName="resultTableRow"
rowKey="index"
/>
</>
);
}
return <div className={styles.sqlResultContent}></div>;
};
// 更新任务结果列表的高度,使其撑满容器
useEffect(() => {
if (isSqlResFullScreen) {
updateFullScreenResScroll();
} else {
updateNormalResScroll();
}
}, [resultTable, isSqlResFullScreen]);
useEffect(() => {
queryDatabaseConfig();
const windowHeight = window.innerHeight;
let size: ScreenSize = 'small';
if (windowHeight > 1100) {
size = 'large';
} else if (windowHeight > 850) {
size = 'middle';
}
setScreenSize(size);
}, []);
const exploreEditorSize = localStorage.getItem('exploreEditorSize');
return (
<>
<div className={styles.sqlOprBar}>
<div className={styles.sqlOprBarLeftBox}>
<Tooltip title="数据类型">
<Dropdown
menu={{
items: jdbcSourceItems,
onClick: (e) => {
const value = e.key;
const target: any = jdbcSourceItems.filter((item: any) => {
return item.key === Number(value);
})[0];
if (target) {
// setJdbcSourceName(target.label);
onJdbcSourceChange?.(Number(value));
}
},
}}
placement="bottom"
>
<Button style={{ marginRight: '15px', minWidth: '140px' }}>
<Space>
<CloudServerOutlined className={styles.sqlOprIcon} style={{ marginRight: 0 }} />
<span>{jdbcSourceItems[0]?.label}</span>
</Space>
</Button>
</Dropdown>
</Tooltip>
<Tooltip title="全屏">
<FullscreenOutlined className={styles.sqlOprIcon} onClick={handleFullScreenSqlIde} />
</Tooltip>
<Tooltip title="格式化SQL语句">
<EditOutlined className={styles.sqlOprIcon} onClick={formatSQL} />
</Tooltip>
<Tooltip title="改变主题">
<SwapOutlined className={styles.sqlOprIcon} onClick={handleThemeChange} />
</Tooltip>
<Tooltip title="执行脚本">
<Button
style={{
lineHeight: '24px',
top: '3px',
position: 'relative',
}}
type="primary"
shape="round"
icon={
isPartial ? '' : isSqlExcLocked ? <PlayCircleOutlined /> : <PlayCircleTwoTone />
}
size={'small'}
className={
isSqlExcLocked ? `${styles.disableIcon} ${styles.sqlOprIcon}` : styles.sqlOprBtn
}
onClick={excuteScript}
>
{isPartial ? '部分运行' : '运行'}
</Button>
</Tooltip>
</div>
</div>
<SplitPane
split="horizontal"
onChange={(size) => {
setEditorSize(size);
localStorage.setItem('exploreEditorSize', size[0]);
}}
>
<Pane initialSize={exploreEditorSize || '500px'}>
<div className={styles.sqlMain}>
<div className={styles.sqlEditorWrapper}>
<FullScreen
isFullScreen={isSqlIdeFullScreen}
top={`${DEFAULT_FULLSCREEN_TOP}px`}
triggerBackToNormal={handleNormalScreenSqlIde}
>
<SqlEditor
value={sql}
// height={sqlEditorHeight}
// theme="monokai"
isRightTheme={isRight}
sizeChanged={editorSize}
onSqlChange={onSqlChange}
onSelect={onSelect}
/>
</FullScreen>
</div>
</div>
</Pane>
<div className={`${styles.sqlBottmWrap} ${screenSize}`}>
<div className={styles.sqlResultWrap}>
<div className={styles.sqlToolBar}>
{
<Button
className={styles.sqlToolBtn}
type="primary"
onClick={startCreatDataSource}
disabled={!runState}
>
</Button>
}
<Button
className={styles.sqlToolBtn}
type="primary"
onClick={handleFullScreenSqlResult}
disabled={!runState}
>
</Button>
</div>
<div
className={styles.sqlResultPane}
ref={resultInnerWrap as React.MutableRefObject<HTMLDivElement | null>}
>
<FullScreen
isFullScreen={isSqlResFullScreen}
top={`${DEFAULT_FULLSCREEN_TOP}px`}
triggerBackToNormal={handleNormalScreenSqlResult}
>
{renderResult()}
</FullScreen>
</div>
</div>
</div>
</SplitPane>
{dataSourceModalVisible && (
<DataSourceCreateForm
sql={sql}
domainId={domainId}
dataSourceItem={dataSourceItem}
scriptColumns={scriptColumns}
onCancel={() => {
setDataSourceModalVisible(false);
}}
onSubmit={(dataSourceInfo: any) => {
setDataSourceModalVisible(false);
onSubmitSuccess?.(dataSourceInfo);
}}
createModalVisible={dataSourceModalVisible}
/>
)}
</>
);
};
export default SqlDetail;

View File

@@ -0,0 +1,126 @@
import React, { useState, useRef, useEffect } from 'react';
import { Tabs } from 'antd';
import SqlDetail from './SqlDetail';
import type { SqlItem } from '../data';
import styles from '../style.less';
type Panes = {
title: string;
key: string;
type: 'add' | 'edit';
scriptId?: number;
sql?: string;
sqlInfo?: SqlItem;
isSave?: boolean; // 暂存提示保存
};
type TableRef = {
current?: {
fetchSqlList: () => void;
upDateActiveItem: (key: any) => void;
};
};
type Props = {
initialValues: any;
domainId: number;
onSubmitSuccess?: (dataSourceInfo: any) => void;
};
const { TabPane } = Tabs;
const LIST_KEY = 'list';
const SqlSide: React.FC<Props> = ({ initialValues, domainId, onSubmitSuccess }) => {
const defaultPanes: Panes[] = [
{
key: '数据源查询',
title: initialValues?.name || '数据源查询',
type: 'add',
isSave: true,
},
];
const [activeKey, setActiveKey] = useState('数据源查询');
const [panes, setPanes] = useState<Panes[]>(defaultPanes);
const tableRef: TableRef = useRef();
const panesRef = useRef<Panes[]>(defaultPanes);
const [dataSourceItem, setDataSourceItem] = useState<any>(initialValues || {});
const updatePane = (list: Panes[]) => {
setPanes(list);
panesRef.current = list;
};
// 更新脚本内容
const updateTabSql = (sql: string, targetKey: string) => {
const newPanes = panesRef.current.slice();
const index = newPanes.findIndex((item) => item.key === targetKey);
const targetItem = newPanes[index];
newPanes.splice(index, 1, {
...targetItem,
sql,
isSave: false,
});
updatePane(newPanes);
};
useEffect(() => {
if (initialValues) {
updateTabSql(initialValues?.datasourceDetail?.sqlQuery || '', '数据源查询');
}
}, [initialValues]);
const onChange = (key: string) => {
setActiveKey(key);
tableRef?.current?.upDateActiveItem(key);
if (key === LIST_KEY) {
tableRef?.current?.fetchSqlList();
}
};
return (
<>
<div className={styles.outside}>
<Tabs
type="editable-card"
hideAdd={true}
activeKey={activeKey}
onChange={onChange}
className={styles.middleArea}
>
{panes.map((pane) => {
return (
<TabPane
tab={<div className={styles.paneName}>{pane.title}</div>}
closable={false}
key={pane.key}
>
<SqlDetail
onSubmitSuccess={onSubmitSuccess}
dataSourceItem={dataSourceItem}
oprType={pane.type}
domainId={domainId}
onUpdateSql={(sql: string) => {
updateTabSql(sql, pane.key);
}}
onJdbcSourceChange={(databaseId) => {
setDataSourceItem({
...dataSourceItem,
databaseId,
});
}}
sql={pane.sql}
/>
</TabPane>
);
})}
</Tabs>
</div>
{/* </SplitPane> */}
</>
);
};
export default SqlSide;

View File

@@ -0,0 +1,67 @@
export const EDITOR_HEIGHT_MAP = new Map([
['small', '250px'],
['middle', '300px'],
['large', '400px'],
]);
export enum EnumDataSourceType {
CATEGORICAL = 'categorical',
TIME = 'time',
MEASURES = 'measures',
PRIMARY = 'primary',
FOREIGN = 'foreign',
}
export const TYPE_OPTIONS = [
{
label: '维度',
value: EnumDataSourceType.CATEGORICAL,
},
{
label: '日期',
value: EnumDataSourceType.TIME,
},
{
label: '度量',
value: EnumDataSourceType.MEASURES,
},
{
label: '主键',
value: EnumDataSourceType.PRIMARY,
},
{
label: '外键',
value: EnumDataSourceType.FOREIGN,
},
];
export const AGG_OPTIONS = [
{
label: 'sum',
value: 'sum',
},
{
label: 'max',
value: 'max',
},
{
label: 'min',
value: 'min',
},
{
label: 'avg',
value: 'avg',
},
{
label: 'count',
value: 'count',
},
{
label: 'count_distinct',
value: 'count_distinct',
},
];
export const DATE_OPTIONS = ['day', 'week', 'month'];
export const DATE_FORMATTER = ['YYYY-MM-DD', 'YYYYMMDD', 'YYYY-MM', 'YYYYMM'];

View File

@@ -0,0 +1,68 @@
import React, { useState, useEffect } from 'react';
import SplitPane from 'react-split-pane';
import SqlSide from './components/SqlSide';
import Pane from 'react-split-pane/lib/Pane';
import styles from './style.less';
import { RightOutlined, LeftOutlined } from '@ant-design/icons';
type Props = {
initialValues: any;
domainId: number;
onSubmitSuccess?: (dataSourceInfo: any) => void;
};
const DEFAULT_RIGHT_SIZE = '300px';
const DataExploreView: React.FC<Props> = ({ initialValues, domainId, onSubmitSuccess }) => {
const [collapsed, setCollapsed] = useState(false);
useEffect(() => {
const exploreRightCollapsed = localStorage.getItem('exploreRightCollapsed');
setCollapsed(exploreRightCollapsed === 'true');
}, []);
const onCollapse = () => {
const collapsedValue = !collapsed;
setCollapsed(collapsedValue);
localStorage.setItem('exploreRightCollapsed', String(collapsedValue));
const exploreRightSize = collapsedValue ? '0px' : localStorage.getItem('exploreRightSize');
const sizeValue = parseInt(exploreRightSize || '0');
if (!collapsedValue && sizeValue <= 10) {
localStorage.setItem('exploreRightSize', DEFAULT_RIGHT_SIZE);
}
};
return (
<div
className={`${styles.pageContainer} ${
window.location.hash.includes('external') ? styles.externalPageContainer : ''
}`}
>
<div className={styles.main}>
<SplitPane
split="vertical"
onChange={(size) => {
localStorage.setItem('exploreRightSize', size[1]);
}}
>
<div className={styles.rightListSide}>
{false && (
<div className={styles.collapseRightBtn} onClick={onCollapse}>
{collapsed ? <LeftOutlined /> : <RightOutlined />}
</div>
)}
<SqlSide
initialValues={initialValues}
domainId={domainId}
onSubmitSuccess={onSubmitSuccess}
/>
</div>
<Pane initialSize={0} />
</SplitPane>
</div>
</div>
);
};
export default DataExploreView;

View File

@@ -0,0 +1,12 @@
import request from 'umi-request';
type ExcuteSqlParams = {
sql: string;
domainId: number;
};
// 执行脚本
export async function excuteSql(params: ExcuteSqlParams) {
const data = { ...params };
return request.post(`${process.env.API_BASE_URL}database/executeSql`, { data });
}

View File

@@ -0,0 +1,759 @@
@borderColor: #eee;
@activeColor: #a0c5e8;
@hoverColor: #dee4e9;
.pageContainer {
position: absolute;
top: 55px;
right: 0;
bottom: 0;
left: 0;
// margin: -24px;
background: #fff;
&.externalPageContainer {
margin: 0 !important;
}
}
.searchBar {
:global {
.ant-form-item-label {
width: 70px;
}
}
}
.main {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
:global {
.ant-tabs {
height: 100% !important;
.ant-tabs-content {
height: 100% !important;
.ant-tabs-tabpane {
height: 100%;
}
}
}
}
}
.rightSide {
position: relative;
z-index: 1;
min-width: 250px;
height: 100%;
margin-left: 4px;
padding: 10px;
overflow: hidden;
:global {
.ant-form-item {
margin-bottom: 6px;
.ant-form-item-label {
width: 70px;
}
.ant-form-item-control {
min-width: 100px;
}
}
}
}
.rightListSide {
position: relative;
z-index: 2;
flex: 1;
height: 100%;
// padding: 10px 10px 0;
background-color: #fff;
// 去掉标签间距
:global {
.ant-tabs-card.ant-tabs-top > .ant-tabs-nav .ant-tabs-tab + .ant-tabs-tab,
.ant-tabs-card.ant-tabs-bottom > .ant-tabs-nav .ant-tabs-tab + .ant-tabs-tab,
.ant-tabs-card.ant-tabs-top > div > .ant-tabs-nav .ant-tabs-tab + .ant-tabs-tab,
.ant-tabs-card.ant-tabs-bottom > div > .ant-tabs-nav .ant-tabs-tab + .ant-tabs-tab {
margin-left: 0;
}
.ant-tabs > .ant-tabs-nav .ant-tabs-nav-add,
.ant-tabs > div > .ant-tabs-nav .ant-tabs-nav-add {
margin-left: 0;
}
}
}
.leftListSide {
position: relative;
z-index: 2;
flex: 1;
height: 100%;
// padding: 10px 10px 0;
background-color: #fff;
}
.tableTotal {
margin: 0 2px;
color: #296df3;
font-weight: bold;
}
.tableDetaildrawer {
:global {
.ant-drawer-header {
padding: 10px 45px 10px 10px;
}
.ant-drawer-close {
padding: 10px;
}
.ant-drawer-body {
padding: 0 10px 10px;
}
.ant-tabs-top > .ant-tabs-nav {
margin-bottom: 8px;
}
}
}
.tableDetailTable {
:global {
.ant-table-cell,
.resultTableRow > td {
padding: 8px;
font-size: 12px;
}
}
}
.sqlEditor {
min-width: 0;
height: 100%;
border: solid 1px @borderColor;
:global {
.ace_editor {
font-family: 'Menlo', 'Monaco', 'Ubuntu Mono', 'Consolas', 'source-code-pro' !important;
}
}
}
.sqlOprBar {
margin-top: -10px;
padding: 5px;
display: flex;
.sqlOprBarLeftBox {
flex: 1 1 200px;
}
.sqlOprBarRightBox {
flex: 0 1 210px;
}
:global {
.ant-btn-round.ant-btn-sm {
font-size: 12px;
}
.ant-btn-primary {
color: #fff;
background: #02a7f0;
border-color: #02a7f0;
}
.ant-segmented-item-selected {
color: #fff;
background: #02a7f0;
border-color: #02a7f0;
}
}
}
.sqlOprIcon {
margin-right: 30px;
color: #02a7f0;
font-size: 22px;
&:hover {
cursor: pointer;
opacity: 0.8;
}
&:active {
opacity: 0.7;
}
}
.sqlOprBtn {
margin-right: 30px;
vertical-align: super !important;
&:hover {
cursor: pointer;
opacity: 0.8;
}
&:active {
opacity: 0.7;
}
}
.sqlOprSwitch {
// vertical-align: super !important;
float: right;
margin-right: 10px !important;
}
:global {
.is-sql-full-select {
background-color: #02a7f0;
}
.cjjWdp:hover {
z-index: 10;
}
}
.sqlMain {
display: flex;
flex-direction: row;
height: 100%;
.sqlEditorWrapper {
flex: 1;
height: 100%;
overflow: hidden;
}
.sqlParams {
width: 20%;
height: 100% !important;
overflow: auto;
}
.hideSqlParams {
width: 0;
height: 100% !important;
overflow: auto;
}
}
.sqlParamsBody {
.header {
display: flex;
padding: 10px;
font-weight: bold;
.title {
flex: 1;
}
.icon {
display: flex;
align-items: center;
margin-right: 10px !important;
cursor: pointer;
}
}
.paramsList {
.paramsItem {
display: flex;
padding: 10px;
:global {
.ant-list-item-action {
margin-left: 5px;
}
}
.name {
flex: 1;
width: 80%;
overflow: hidden;
font-size: 12px;
text-overflow: ellipsis;
&:hover {
cursor: pointer;
}
}
.icon {
// display: none;
margin-left: 10px;
}
}
}
// .paramsItem:hover {
// .icon {
// display: inline-block;
// margin-left: 8px;
// cursor: pointer;
// }
// }
}
.disableIcon {
vertical-align: super !important;
// color: rgba(0, 10, 36, 0.25);
background: #7d7f80 !important;
border-color: #7d7f80 !important;
:global {
.anticon .anticon-play-circle {
color: #fff;
}
}
&:hover {
cursor: not-allowed;
opacity: 1;
}
}
.sqlTaskListWrap {
position: relative;
width: 262px;
border-top: 0 !important;
border-radius: 0;
:global {
.ant-card-head {
min-height: 20px;
}
.ant-card-head-title {
padding: 8px 0;
}
}
}
.sqlTaskList {
position: absolute !important;
top: 42px;
right: 0;
bottom: 0;
left: 0;
overflow: auto;
}
.sqlBottmWrap {
// position: absolute;
// top: 484px;
// right: 0;
// bottom: 0;
// left: 0;
display: flex;
height: 100%;
// padding: 0 10px;
&:global(.small) {
top: 334px;
}
&:global(.middle) {
top: 384px;
}
}
.sqlResultWrap {
position: relative;
display: flex;
flex: 1;
flex-direction: column;
overflow: auto;
border: solid 1px @borderColor;
border-top: 0;
border-left: 0;
}
.sqlToolBar {
display: flex;
flex-direction: row-reverse;
align-items: center;
height: 41px;
padding: 5px 0;
text-align: right;
}
.sqlResultPane {
flex: 1;
border-top: solid 1px @borderColor;
}
.sqlToolBtn {
margin-right: 15px;
}
.runScriptBtn {
margin-right: 15px;
background-color: #e87954;
border-color: #e87954;
&:hover{
border-color: #f89878;
background: #f89878;
}
&:focus {
border-color: #f89878;
background: #f89878;
}
}
.taskFailed {
padding: 20px 20px 0 20px;
}
.sqlResultContent {
position: absolute;
top: 50%;
width: 100%;
color: rgba(0, 0, 0, 0.25);
font-size: 16px;
text-align: center;
}
.sqlResultLog {
padding: 20px;
word-wrap: break-word;
}
.tableList {
position: absolute !important;
top: 160px;
right: 0;
bottom: 26px;
left: 0;
overflow-x: hidden;
overflow-y: auto;
border-bottom: solid 1px @borderColor;
}
.tablePage {
position: absolute !important;
bottom: 0;
left: 0;
z-index: 1;
width: 100%;
min-width: 250px;
overflow: hidden;
}
.tableListItem {
width: 88%;
overflow: hidden;
font-size: 12px;
text-overflow: ellipsis;
&:hover {
cursor: pointer;
}
}
.tableItem {
&:global(.ant-list-item) {
padding: 6px 0 6px 6px;
}
:global(.ant-list-item-action) {
margin-left: 12px !important;
}
&:hover {
background: @hoverColor;
border-bottom: 1px solid #f0f0f0;
}
&:global(.active) {
background: @activeColor;
}
}
.taskIcon {
margin-right: 10px;
color: #1890ff;
font-size: 14px;
}
.taskSuccessIcon {
.taskIcon();
color: #67c23a;
}
.taskFailIcon {
.taskIcon();
color: #f56c6c;
}
.resultFailIcon {
margin-right: 8px;
color: #f56c6c;
}
.taskItem {
padding: 10px 8px !important;
font-size: 12px;
cursor: pointer;
&:global(.ant-list-item) {
justify-content: flex-start;
}
&:hover {
background: @hoverColor;
}
}
.activeTask {
background: @activeColor;
}
.resultTable {
width: 100%;
:global {
.ant-table-body {
width: 100%;
// max-height: none !important;
overflow: auto !important;
}
.ant-table-cell,
.resultTableRow > td {
padding: 8px;
font-size: 12px;
}
}
}
.taskLogWrap {
word-wrap: break-word;
}
.siteTagPlus {
background: #fff;
border-style: dashed;
}
.editTag {
margin-bottom: 5px;
user-select: none;
}
.tagInput {
width: 78px;
margin-right: 8px;
vertical-align: top;
}
.outside {
position: relative;
height: 100%;
}
.collapseRightBtn {
position: absolute;
top: calc(50% + 50px);
right: 0;
z-index: 100;
display: flex;
align-items: center;
height: 70px;
color: #fff;
font-size: 12px;
background-color: rgba(40, 46, 54, 0.2);
border-radius: 24px 0 0 24px;
cursor: pointer;
transition: all 0.3s ease;
}
.collapseLeftBtn {
position: absolute;
top: calc(50% + 45px);
left: 0;
z-index: 100;
display: flex;
align-items: center;
height: 70px;
color: #fff;
font-size: 12px;
background-color: rgba(40, 46, 54, 0.2);
border-radius: 0 24px 24px 0;
cursor: pointer;
transition: all 0.3s ease;
}
.detail {
.titleCollapse {
float: right;
padding-right: 18px;
color: #1890ff;
line-height: 35px;
text-align: right;
cursor: pointer;
}
.tableTitle {
display: inline-block;
width: 85%;
margin-left: 15px;
overflow: hidden;
line-height: 35px;
white-space: nowrap;
text-overflow: ellipsis;
cursor: pointer;
}
:global {
.ant-divider-horizontal {
margin: 0;
}
}
}
.search {
margin-left: 10px;
}
.middleArea {
:global {
.ant-tabs-nav .ant-tabs-tab {
border: none;
// background: #d9d9d96e;
border-right: 1px solid #f0f0f0;
border-radius: 0 !important;
}
.ant-tabs-nav-add {
border-radius: 0 !important;
}
.ant-tabs-tab {
.ant-tabs-tab-remove {
.closeTab {
opacity: 0;
}
.dot {
opacity: 1;
}
}
}
.ant-tabs-tab:hover {
.ant-tabs-tab-remove {
.closeTab {
opacity: 1 !important;
}
.dot {
opacity: 0;
}
}
}
}
}
.menu {
position: relative;
z-index: 1;
height: 100%;
padding: 5px;
overflow: hidden;
overflow-x: hidden;
overflow-y: auto;
:global {
.ant-form {
margin: -2px;
}
}
}
.menuList {
position: absolute !important;
top: 95px;
right: 0;
bottom: 26px;
left: 0;
overflow-x: hidden;
overflow-y: auto;
border-bottom: solid 1px @borderColor;
.menuItem {
&:global(.ant-list-item) {
padding: 6px 0 6px 14px;
}
:global(.ant-list-item-action) {
margin-left: 12px !important;
}
&:hover {
background: @hoverColor;
border-bottom: 1px solid #f0f0f0;
.icon {
display: block;
}
}
&:global(.active) {
background: @activeColor;
}
.menuListItem {
width: 90%;
overflow: hidden;
font-size: 12px;
white-space: nowrap;
text-overflow: ellipsis;
&:hover {
cursor: pointer;
}
}
.icon {
display: none;
margin-right: 15px !important;
cursor: pointer;
}
.menuIcon {
display: flex;
}
}
}
.scriptFile {
width: 100%;
margin: 10px;
overflow: hidden;
font-size: 14px;
white-space: nowrap;
text-overflow: ellipsis;
.icon {
margin-right: 10px;
}
}
.sqlScriptName {
width: 93% !important;
margin: 14px 0 0 14px !important;
}
.fileIcon {
width: 20px !important;
height: 20px !important;
padding-top: 2px !important;
padding-right: 5px !important;
vertical-align: middle;
}
.itemName {
vertical-align: middle;
}
.paneName {
width: 100px;
overflow: hidden;
font-size: 12px !important;
white-space: nowrap;
text-overflow: ellipsis;
}
.titleIcon {
width: 16px !important;
height: 16px !important;
margin: 0 3px 4px;
}

View File

@@ -0,0 +1,143 @@
import { Tabs } from 'antd';
import React, { useEffect, useState } from 'react';
import { connect, Helmet } from 'umi';
import ProjectListTree from './components/ProjectList';
import ClassDataSourceTable from './components/ClassDataSourceTable';
import ClassDimensionTable from './components/ClassDimensionTable';
import ClassMetricTable from './components/ClassMetricTable';
import PermissionSection from './components/Permission/PermissionSection';
import DatabaseSection from './components/Database/DatabaseSection';
import styles from './components/style.less';
import type { StateType } from './model';
import { RightOutlined, LeftOutlined } from '@ant-design/icons';
import SemanticFlow from './SemanticFlows';
// import SemanticGraph from './SemanticGraph';
import SplitPane from 'react-split-pane';
import Pane from 'react-split-pane/lib/Pane';
import type { Dispatch } from 'umi';
const { TabPane } = Tabs;
type Props = {
domainManger: StateType;
dispatch: Dispatch;
};
const DEFAULT_LEFT_SIZE = '300px';
const DomainManger: React.FC<Props> = ({ domainManger, dispatch }) => {
const [collapsed, setCollapsed] = useState(false);
const [leftSize, setLeftSize] = useState('');
const { selectDomainId, selectDomainName } = domainManger;
useEffect(() => {
const semanticLeftCollapsed = localStorage.getItem('semanticLeftCollapsed');
const semanticLeftSize =
semanticLeftCollapsed === 'true' ? '0px' : localStorage.getItem('semanticLeftSize');
setCollapsed(semanticLeftCollapsed === 'true');
setLeftSize(semanticLeftSize || DEFAULT_LEFT_SIZE);
}, []);
useEffect(() => {
if (selectDomainId) {
dispatch({
type: 'domainManger/queryDimensionList',
payload: {
domainId: selectDomainId,
},
});
dispatch({
type: 'domainManger/queryMetricList',
payload: {
domainId: selectDomainId,
},
});
}
}, [selectDomainId]);
const onCollapse = () => {
const collapsedValue = !collapsed;
setCollapsed(collapsedValue);
localStorage.setItem('semanticLeftCollapsed', String(collapsedValue));
const semanticLeftSize = collapsedValue ? '0px' : localStorage.getItem('semanticLeftSize');
const sizeValue = parseInt(semanticLeftSize || '0');
if (!collapsedValue && sizeValue <= 10) {
setLeftSize(DEFAULT_LEFT_SIZE);
localStorage.setItem('semanticLeftSize', DEFAULT_LEFT_SIZE);
} else {
setLeftSize(semanticLeftSize || DEFAULT_LEFT_SIZE);
}
};
useEffect(() => {
const width = document.getElementById('tab');
const switchWarpper: any = document.getElementById('switch');
if (width && switchWarpper) {
switchWarpper.style.width = width.offsetWidth * 0.77 + 'px';
}
});
return (
<div className={styles.projectBody}>
<Helmet title={'语义建模-超音数'} />
<SplitPane
split="vertical"
onChange={(size) => {
localStorage.setItem('semanticLeftSize', size[0]);
setLeftSize(size[0]);
}}
>
<Pane initialSize={leftSize || DEFAULT_LEFT_SIZE}>
<div className={styles.menu}>
<ProjectListTree />
</div>
</Pane>
<div className={styles.projectManger}>
<div className={styles.collapseLeftBtn} onClick={onCollapse}>
{collapsed ? <RightOutlined /> : <LeftOutlined />}
</div>
<h2 className={styles.title}>
{selectDomainName ? `选择的主题域:${selectDomainName}` : '主题域信息'}
</h2>
{selectDomainId ? (
<>
<Tabs className={styles.tab} defaultActiveKey="xflow" destroyInactiveTabPane>
{/* <TabPane className={styles.tabPane} tab="关系可视化" key="graph">
<div style={{ width: '100%', height: 'calc(100vh - 200px)' }}>
<SemanticGraph domainId={selectDomainId} />
</div>
</TabPane> */}
<TabPane className={styles.tabPane} tab="可视化建模" key="xflow">
<div style={{ width: '100%', height: 'calc(100vh - 200px)' }}>
<SemanticFlow />
</div>
</TabPane>
<TabPane className={styles.tabPane} tab="数据库" key="dataBase">
<DatabaseSection />
</TabPane>
<TabPane className={styles.tabPane} tab="数据源" key="dataSource">
<ClassDataSourceTable />
</TabPane>
<TabPane className={styles.tabPane} tab="维度" key="dimenstion">
<ClassDimensionTable key={selectDomainId} />
</TabPane>
<TabPane className={styles.tabPane} tab="指标" key="metric">
<ClassMetricTable />
</TabPane>
<TabPane className={styles.tabPane} tab="权限管理" key="permissonSetting">
<PermissionSection />
</TabPane>
</Tabs>
</>
) : (
<h2 className={styles.mainTip}></h2>
)}
</div>
</SplitPane>
</div>
);
};
export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger,
}))(DomainManger);

View File

@@ -0,0 +1,209 @@
import type { HookHub, ICmdHooks as IHooks, NsGraph } from '@antv/xflow';
import { Deferred, ManaSyringe } from '@antv/xflow';
import { Modal, ConfigProvider } from 'antd';
import type { IArgsBase, ICommandHandler } from '@antv/xflow';
import { ICommandContextProvider } from '@antv/xflow';
import { DATASOURCE_NODE_RENDER_ID } from '../constant';
import { CustomCommands } from './constants';
import 'antd/es/modal/style/index.css';
export namespace NsConfirmModalCmd {
/** Command: 用于注册named factory */
// eslint-disable-next-line
export const command = CustomCommands.SHOW_CONFIRM_MODAL;
/** hook name */
// eslint-disable-next-line
export const hookKey = 'confirmModal';
/** hook 参数类型 */
export interface IArgs extends IArgsBase {
nodeConfig: NsGraph.INodeConfig;
confirmModalCallBack: IConfirmModalService;
}
export interface IConfirmModalService {
(): Promise<any>;
}
/** hook handler 返回类型 */
export type IResult = any;
/** hooks 类型 */
export interface ICmdHooks extends IHooks {
confirmModal: HookHub<IArgs, IResult>;
}
}
const deleteDataSourceConfirmNode = (name: string) => {
return (
<>
<span style={{ color: '#296DF3', fontWeight: 'bold' }}>{name}</span>
</>
);
};
// prettier-ignore
type ICommand = ICommandHandler<NsConfirmModalCmd.IArgs, NsConfirmModalCmd.IResult, NsConfirmModalCmd.ICmdHooks>;
@ManaSyringe.injectable()
/** 部署画布数据 */
export class ConfirmModalCommand implements ICommand {
/** api */
@ManaSyringe.inject(ICommandContextProvider) contextProvider!: ICommand['contextProvider'];
/** 执行Cmd */
execute = async () => {
const ctx = this.contextProvider();
const { args } = ctx.getArgs();
const hooks = ctx.getHooks();
await hooks.confirmModal.call(args, async (confirmArgs: NsConfirmModalCmd.IArgs) => {
const { nodeConfig, confirmModalCallBack } = confirmArgs;
const { renderKey, label } = nodeConfig;
if (!nodeConfig.modalProps?.modalContent) {
let modalContent = <></>;
if (renderKey === DATASOURCE_NODE_RENDER_ID) {
modalContent = deleteDataSourceConfirmNode(label!);
}
nodeConfig.modalProps = {
...(nodeConfig.modalProps || {}),
modalContent,
};
}
const getAppContext: IGetAppCtx = () => {
return {
confirmModalCallBack,
};
};
const x6Graph = await ctx.getX6Graph();
const cell = x6Graph.getCellById(nodeConfig.id);
if (!cell || !cell.isNode()) {
throw new Error(`${nodeConfig.id} is not a valid node`);
}
/** 通过modal 获取 new name */
await showModal(nodeConfig, getAppContext);
return;
});
// ctx.setResult(result);
return this;
};
/** undo cmd */
undo = async () => {
if (this.isUndoable()) {
const ctx = this.contextProvider();
ctx.undo();
}
return this;
};
/** redo cmd */
redo = async () => {
if (!this.isUndoable()) {
await this.execute();
}
return this;
};
isUndoable(): boolean {
const ctx = this.contextProvider();
return ctx.isUndoable();
}
}
export interface IGetAppCtx {
(): {
confirmModalCallBack: NsConfirmModalCmd.IConfirmModalService;
};
}
export type IModalInstance = ReturnType<typeof Modal.confirm>;
function showModal(node: NsGraph.INodeConfig, getAppContext: IGetAppCtx) {
/** showModal 返回一个Promise */
const defer = new Deferred<string | void>();
const modalTitle = node.modalProps?.title;
const modalContent = node.modalProps?.modalContent;
/** modal确认保存逻辑 */
class ModalCache {
static modal: IModalInstance;
}
/** modal确认保存逻辑 */
const onOk = async () => {
const { modal } = ModalCache;
const appContext = getAppContext();
const { confirmModalCallBack } = appContext;
try {
modal.update({ okButtonProps: { loading: true } });
/** 执行 confirm回调*/
if (confirmModalCallBack) {
await confirmModalCallBack();
}
/** 更新成功后关闭modal */
onHide();
} catch (error) {
console.error(error);
/** 如果resolve空字符串则不更新 */
modal.update({ okButtonProps: { loading: false } });
}
};
/** modal销毁逻辑 */
const onHide = () => {
modal.destroy();
ModalCache.modal = null as any;
container.destroy();
};
/** modal内容 */
const ModalContent = () => {
return (
<div>
<ConfigProvider>{modalContent}</ConfigProvider>
</div>
);
};
/** 创建modal dom容器 */
const container = createContainer();
/** 创建modal */
const modal = Modal.confirm({
title: modalTitle,
content: <ModalContent />,
getContainer: () => {
return container.element;
},
okButtonProps: {
onClick: (e) => {
e.stopPropagation();
onOk();
},
},
onCancel: () => {
onHide();
},
afterClose: () => {
onHide();
},
});
/** 缓存modal实例 */
ModalCache.modal = modal;
/** showModal 返回一个Promise用于await */
return defer.promise;
}
const createContainer = () => {
const div = document.createElement('div');
div.classList.add('xflow-modal-container');
window.document.body.append(div);
return {
element: div,
destroy: () => {
window.document.body.removeChild(div);
},
};
};

View File

@@ -0,0 +1,89 @@
import type {
NsGraphCmd,
ICmdHooks as IHooks,
NsGraph,
IArgsBase,
ICommandHandler,
HookHub,
} from '@antv/xflow';
import { XFlowGraphCommands, ManaSyringe } from '@antv/xflow';
import { ICommandContextProvider } from '@antv/xflow';
import { CustomCommands } from './constants';
// prettier-ignore
type ICommand = ICommandHandler<NsDeployDagCmd.IArgs, NsDeployDagCmd.IResult, NsDeployDagCmd.ICmdHooks>;
export namespace NsDeployDagCmd {
/** Command: 用于注册named factory */
// eslint-disable-next-line
export const command = CustomCommands.DEPLOY_SERVICE;
/** hook name */
// eslint-disable-next-line
export const hookKey = 'deployDag';
/** hook 参数类型 */
export interface IArgs extends IArgsBase {
deployDagService: IDeployDagService;
}
export interface IDeployDagService {
(meta: NsGraph.IGraphMeta, data: NsGraph.IGraphData): Promise<{ success: boolean }>;
}
/** hook handler 返回类型 */
export interface IResult {
success: boolean;
}
/** hooks 类型 */
export interface ICmdHooks extends IHooks {
deployDag: HookHub<IArgs, IResult>;
}
}
@ManaSyringe.injectable()
/** 部署画布数据 */
export class DeployDagCommand implements ICommand {
/** api */
@ManaSyringe.inject(ICommandContextProvider) contextProvider!: ICommand['contextProvider'];
/** 执行Cmd */
execute = async () => {
const ctx = this.contextProvider();
const { args } = ctx.getArgs();
const hooks = ctx.getHooks();
const result = await hooks.deployDag.call(args, async (handlerArgs) => {
const { commandService, deployDagService } = handlerArgs;
/** 执行Command */
await commandService!.executeCommand<NsGraphCmd.SaveGraphData.IArgs>(
XFlowGraphCommands.SAVE_GRAPH_DATA.id,
{
saveGraphDataService: async (meta, graph) => {
await deployDagService(meta, graph);
},
},
);
return { success: true };
});
ctx.setResult(result);
return this;
};
/** undo cmd */
undo = async () => {
if (this.isUndoable()) {
const ctx = this.contextProvider();
ctx.undo();
}
return this;
};
/** redo cmd */
redo = async () => {
if (!this.isUndoable()) {
await this.execute();
}
return this;
};
isUndoable(): boolean {
const ctx = this.contextProvider();
return ctx.isUndoable();
}
}

View File

@@ -0,0 +1,255 @@
import type { HookHub, ICmdHooks as IHooks, NsGraph, IModelService } from '@antv/xflow';
import { Deferred, ManaSyringe } from '@antv/xflow';
import type { FormInstance } from 'antd';
import { Modal, Form, Input, ConfigProvider } from 'antd';
import type { IArgsBase, ICommandHandler, IGraphCommandService } from '@antv/xflow';
import { ICommandContextProvider } from '@antv/xflow';
import { CustomCommands } from './constants';
import 'antd/es/modal/style/index.css';
// prettier-ignore
type ICommand = ICommandHandler<NsRenameNodeCmd.IArgs, NsRenameNodeCmd.IResult, NsRenameNodeCmd.ICmdHooks>;
export namespace NsRenameNodeCmd {
/** Command: 用于注册named factory */
// eslint-disable-next-line
export const command = CustomCommands.SHOW_RENAME_MODAL;
/** hook name */
// eslint-disable-next-line
export const hookKey = 'renameNode';
/** hook 参数类型 */
export interface IArgs extends IArgsBase {
nodeConfig: NsGraph.INodeConfig;
updateNodeNameService: IUpdateNodeNameService;
}
export interface IUpdateNodeNameService {
(newName: string, nodeConfig: NsGraph.INodeConfig, meta: NsGraph.IGraphMeta): Promise<{
err: string | null;
nodeName: string;
}>;
}
/** hook handler 返回类型 */
export interface IResult {
err: string | null;
preNodeName?: string;
currentNodeName?: string;
}
/** hooks 类型 */
export interface ICmdHooks extends IHooks {
renameNode: HookHub<IArgs, IResult>;
}
}
@ManaSyringe.injectable()
/** 部署画布数据 */
// prettier-ignore
export class RenameNodeCommand implements ICommand {
/** api */
@ManaSyringe.inject(ICommandContextProvider) contextProvider!: ICommand['contextProvider'];
/** 执行Cmd */
execute = async () => {
const ctx = this.contextProvider();
// const app = useXFlowApp();
const { args } = ctx.getArgs();
const hooks = ctx.getHooks();
const result = await hooks.renameNode.call(args, async (args) => {
const { nodeConfig, graphMeta, commandService, modelService, updateNodeNameService } = args;
const preNodeName = nodeConfig.label;
const getAppContext: IGetAppCtx = () => {
return {
graphMeta,
commandService,
modelService,
updateNodeNameService,
};
};
const x6Graph = await ctx.getX6Graph();
const cell = x6Graph.getCellById(nodeConfig.id);
const nodes = x6Graph.getNodes();
const edges = x6Graph.getEdges();
nodes.forEach((node) => {
if (node !== cell) {
x6Graph.removeCell(node);
}
});
edges.forEach((edge) => {
x6Graph.removeEdge(edge);
});
if (!cell || !cell.isNode()) {
throw new Error(`${nodeConfig.id} is not a valid node`);
}
/** 通过modal 获取 new name */
const newName = await showModal(nodeConfig, getAppContext);
/** 更新 node name */
if (newName) {
const cellData = cell.getData<NsGraph.INodeConfig>();
cell.setData({ ...cellData, label: newName } as NsGraph.INodeConfig);
return { err: null, preNodeName, currentNodeName: newName };
}
return { err: null, preNodeName, currentNodeName: '' };
});
ctx.setResult(result);
return this;
};
/** undo cmd */
undo = async () => {
if (this.isUndoable()) {
const ctx = this.contextProvider();
ctx.undo();
}
return this;
};
/** redo cmd */
redo = async () => {
if (!this.isUndoable()) {
await this.execute();
}
return this;
};
isUndoable(): boolean {
const ctx = this.contextProvider();
return ctx.isUndoable();
}
}
export interface IGetAppCtx {
(): {
graphMeta: NsGraph.IGraphMeta;
commandService: IGraphCommandService;
modelService: IModelService;
updateNodeNameService: NsRenameNodeCmd.IUpdateNodeNameService;
};
}
export type IModalInstance = ReturnType<typeof Modal.confirm>;
export interface IFormProps {
newNodeName: string;
}
const layout = {
labelCol: { span: 5 },
wrapperCol: { span: 19 },
};
function showModal(node: NsGraph.INodeConfig, getAppContext: IGetAppCtx) {
/** showModal 返回一个Promise */
const defer = new Deferred<string | void>();
/** modal确认保存逻辑 */
class ModalCache {
static modal: IModalInstance;
static form: FormInstance<IFormProps>;
}
/** modal确认保存逻辑 */
const onOk = async () => {
const { form, modal } = ModalCache;
const appContext = getAppContext();
const { updateNodeNameService, graphMeta } = appContext;
try {
modal.update({ okButtonProps: { loading: true } });
await form.validateFields();
const values = await form.getFieldsValue();
const newName: string = values.newNodeName;
/** 执行 backend service */
if (updateNodeNameService) {
const { err, nodeName } = await updateNodeNameService(newName, node, graphMeta);
if (err) {
throw new Error(err);
}
defer.resolve(nodeName);
}
/** 更新成功后关闭modal */
onHide();
} catch (error) {
console.error(error);
/** 如果resolve空字符串则不更新 */
modal.update({ okButtonProps: { loading: false } });
}
};
/** modal销毁逻辑 */
const onHide = () => {
modal.destroy();
ModalCache.form = null as any;
ModalCache.modal = null as any;
container.destroy();
};
/** modal内容 */
const ModalContent = () => {
const [form] = Form.useForm<IFormProps>();
/** 缓存form实例 */
ModalCache.form = form;
return (
<div>
<ConfigProvider>
<Form form={form} {...layout} initialValues={{ newNodeName: node.label }}>
<Form.Item
name="newNodeName"
label="节点名"
rules={[
{ required: true, message: '请输入新节点名' },
{ min: 3, message: '节点名不能少于3个字符' },
]}
>
<Input />
</Form.Item>
</Form>
</ConfigProvider>
</div>
);
};
/** 创建modal dom容器 */
const container = createContainer();
/** 创建modal */
const modal = Modal.confirm({
title: '重命名',
content: <ModalContent />,
getContainer: () => {
return container.element;
},
okButtonProps: {
onClick: (e) => {
e.stopPropagation();
onOk();
},
},
onCancel: () => {
onHide();
},
afterClose: () => {
onHide();
},
});
/** 缓存modal实例 */
ModalCache.modal = modal;
/** showModal 返回一个Promise用于await */
return defer.promise;
}
const createContainer = () => {
const div = document.createElement('div');
div.classList.add('xflow-modal-container');
window.document.body.append(div);
return {
element: div,
destroy: () => {
window.document.body.removeChild(div);
},
};
};

View File

@@ -0,0 +1,88 @@
import type {
ICmdHooks as IHooks,
NsGraph,
IArgsBase,
ICommandHandler,
HookHub,
} from '@antv/xflow';
import { ManaSyringe } from '@antv/xflow';
import { ICommandContextProvider } from '@antv/xflow';
import { CustomCommands } from './constants';
import { getDatasourceRelaList } from '../../service';
// prettier-ignore
type ICommand = ICommandHandler<NsDataSourceRelationCmd.IArgs, NsDataSourceRelationCmd.IResult, NsDataSourceRelationCmd.ICmdHooks>;
export namespace NsDataSourceRelationCmd {
/** Command: 用于注册named factory */
// eslint-disable-next-line
export const command = CustomCommands.DATASOURCE_RELATION;
/** hook name */
// eslint-disable-next-line
export const hookKey = 'dataSourceRelation';
/** hook 参数类型 */
export interface IArgs extends IArgsBase {
dataSourceRelationService: IDataSourceRelationService;
}
export interface IDataSourceRelationService {
(meta: NsGraph.IGraphMeta, data: NsGraph.IGraphData): Promise<{ success: boolean }>;
}
/** hook handler 返回类型 */
export type IResult = any[] | undefined;
/** hooks 类型 */
export interface ICmdHooks extends IHooks {
dataSourceRelation: HookHub<IArgs, IResult>;
}
}
@ManaSyringe.injectable()
/** 部署画布数据 */
export class DataSourceRelationCommand implements ICommand {
/** api */
@ManaSyringe.inject(ICommandContextProvider) contextProvider!: ICommand['contextProvider'];
/** 执行Cmd */
execute = async () => {
const ctx = this.contextProvider();
const { args } = ctx.getArgs();
const hooks = ctx.getHooks();
const graphMeta = await ctx.getGraphMeta();
const domainId = graphMeta?.meta?.domainManger?.selectDomainId;
if (!domainId) {
return this;
}
const result = await hooks.dataSourceRelation.call(args, async () => {
const { code, data } = await getDatasourceRelaList(domainId);
if (code === 200) {
return data;
}
return [];
});
ctx.setResult(result);
ctx.setGlobal('dataSourceRelationList', result);
return this;
};
/** undo cmd */
undo = async () => {
if (this.isUndoable()) {
const ctx = this.contextProvider();
ctx.undo();
}
return this;
};
/** redo cmd */
redo = async () => {
if (!this.isUndoable()) {
await this.execute();
}
return this;
};
isUndoable(): boolean {
const ctx = this.contextProvider();
return ctx.isUndoable();
}
}

View File

@@ -0,0 +1,43 @@
import type { IGraphCommand } from '@antv/xflow';
/** 节点命令 */
export namespace CustomCommands {
const category = '节点操作';
/** 异步请求demo */
export const TEST_ASYNC_CMD: IGraphCommand = {
id: 'xflow:async-cmd',
label: '异步请求',
category,
};
/** 重命名节点弹窗 */
export const SHOW_RENAME_MODAL: IGraphCommand = {
id: 'xflow:rename-node-modal',
label: '打开重命名弹窗',
category,
};
/** 二次确认弹窗 */
export const SHOW_CONFIRM_MODAL: IGraphCommand = {
id: 'xflow:confirm-modal',
label: '打开二次确认弹窗',
category,
};
/** 部署服务 */
export const DEPLOY_SERVICE: IGraphCommand = {
id: 'xflow:deploy-service',
label: '部署服务',
category,
};
export const DATASOURCE_RELATION: IGraphCommand = {
id: 'xflow:datasource-relation',
label: '获取数据源关系数据',
category,
};
/** 查看维度 */
export const VIEW_DIMENSION: IGraphCommand = {
id: 'xflow:view-dimension',
label: '查看维度',
category,
};
}

View File

@@ -0,0 +1,28 @@
import { DeployDagCommand, NsDeployDagCmd } from './CmdDeploy';
import { RenameNodeCommand, NsRenameNodeCmd } from './CmdRenameNodeModal';
import { ConfirmModalCommand, NsConfirmModalCmd } from './CmdConfirmModal';
import {
DataSourceRelationCommand,
NsDataSourceRelationCmd,
} from './CmdUpdateDataSourceRelationList';
import type { ICommandContributionConfig } from '@antv/xflow';
/** 注册成为可以执行的命令 */
export const COMMAND_CONTRIBUTIONS: ICommandContributionConfig[] = [
{
...NsDeployDagCmd,
CommandHandler: DeployDagCommand,
},
{
...NsRenameNodeCmd,
CommandHandler: RenameNodeCommand,
},
{
...NsConfirmModalCmd,
CommandHandler: ConfirmModalCommand,
},
{
...NsDataSourceRelationCmd,
CommandHandler: DataSourceRelationCommand,
},
];

View File

@@ -0,0 +1,266 @@
import type { NsGraphCmd } from '@antv/xflow';
import { createCmdConfig, DisposableCollection, XFlowGraphCommands } from '@antv/xflow';
import type { IApplication } from '@antv/xflow';
import type { IGraphPipelineCommand, IGraphCommandService, NsGraph } from '@antv/xflow';
import { GraphApi } from './service';
import { addDataSourceInfoAsDimensionParents } from './utils';
import { COMMAND_CONTRIBUTIONS } from './CmdExtensions';
import { CustomCommands } from './CmdExtensions/constants';
export const useCmdConfig = createCmdConfig((config) => {
// 注册全局Command扩展
config.setCommandContributions(() => COMMAND_CONTRIBUTIONS);
// 设置hook
config.setRegisterHookFn((hooks) => {
const list = [
hooks.graphMeta.registerHook({
name: 'get graph meta from backend',
handler: async (args) => {
args.graphMetaService = GraphApi.queryGraphMeta;
},
}),
hooks.saveGraphData.registerHook({
name: 'save graph data',
handler: async (args) => {
if (!args.saveGraphDataService) {
args.saveGraphDataService = GraphApi.saveGraphData;
}
},
}),
hooks.addNode.registerHook({
name: 'get node config from backend api',
handler: async (args) => {
args.createNodeService = GraphApi.addNode;
},
}),
hooks.delNode.registerHook({
name: 'get edge config from backend api',
handler: async (args) => {
args.deleteNodeService = GraphApi.delNode;
},
}),
hooks.addEdge.registerHook({
name: '获取起始和结束节点的业务数据,并写入在边上',
handler: async (handlerArgs, handler: any) => {
const { commandService } = handlerArgs;
const main = async (args: any) => {
const res = await handler(args);
if (res && res.edgeCell) {
const sourceNode = res.edgeCell.getSourceNode();
const targetNode = res.edgeCell.getTargetNode();
const sourceNodeData = sourceNode?.getData() || {};
const targetNodeData = targetNode?.getData() || {};
res.edgeCell.setData({
sourceNodeData,
targetNodeData,
source: sourceNodeData.id,
target: targetNodeData.id,
});
// 对边进行tooltips设置
res.edgeCell.addTools([
{
name: 'tooltip',
args: {
tooltip: '左键点击进行关系编辑,右键点击进行删除操作',
},
},
]);
if (commandService) {
const initGraphCmdsState: any = commandService.getGlobal('initGraphCmdsSuccess');
if (initGraphCmdsState) {
// 保存图数据
commandService!.executeCommand<NsGraphCmd.SaveGraphData.IArgs>(
XFlowGraphCommands.SAVE_GRAPH_DATA.id,
{
saveGraphDataService: (meta, graphData) =>
GraphApi.saveGraphData!(meta, graphData),
},
);
}
}
return res;
}
};
return main;
},
}),
hooks.delEdge.registerHook({
name: '边删除,并向后台请求删除数据源间关联关系',
handler: async (args) => {
args.deleteEdgeService = GraphApi.delEdge;
},
}),
];
const toDispose = new DisposableCollection();
toDispose.pushAll(list);
return toDispose;
});
});
/** 查询图的节点和边的数据 */
export const initGraphCmds = async (app: IApplication) => {
const { commandService } = app;
await app.executeCommandPipeline([
/** 1. 从服务端获取数据 */
{
commandId: XFlowGraphCommands.LOAD_DATA.id,
getCommandOption: async () => {
commandService.setGlobal('initGraphCmdsSuccess', false);
return {
args: {
loadDataService: GraphApi.loadDataSourceData,
},
};
},
} as IGraphPipelineCommand<NsGraphCmd.GraphLoadData.IArgs>,
/** 2. 执行布局算法 */
{
commandId: XFlowGraphCommands.GRAPH_LAYOUT.id,
getCommandOption: async (ctx) => {
const { graphData } = ctx.getResult();
return {
args: {
layoutType: 'dagre',
layoutOptions: {
type: 'dagre',
/** 布局方向 */
rankdir: 'LR',
/** 节点间距 */
nodesep: 30,
/** 层间距 */
ranksep: 80,
begin: [0, 0],
},
graphData,
},
};
},
} as IGraphPipelineCommand<NsGraphCmd.GraphLayout.IArgs>,
/** 3. 画布内容渲染 */
{
commandId: XFlowGraphCommands.GRAPH_RENDER.id,
getCommandOption: async (ctx) => {
const { graphData } = ctx.getResult();
const { edges, nodes } = graphData;
const filterClassNodeEdges = edges.filter((item: NsGraph.IEdgeConfig) => {
return !item.source.includes('classNodeId-');
});
const filterClassNodeNodes = nodes.filter((item: NsGraph.INodeConfig) => {
return !item.id.includes('classNodeId-');
});
return {
args: {
graphData: {
edges: filterClassNodeEdges,
nodes: filterClassNodeNodes,
},
},
};
},
} as IGraphPipelineCommand<NsGraphCmd.GraphRender.IArgs>,
/** 4. 缩放画布 */
{
commandId: XFlowGraphCommands.GRAPH_ZOOM.id,
getCommandOption: async () => {
commandService.setGlobal('initGraphCmdsSuccess', true);
return {
args: { factor: 'fit', zoomOptions: { maxScale: 0.9 } },
};
},
} as IGraphPipelineCommand<NsGraphCmd.GraphZoom.IArgs>,
// commandService.executeCommand(CustomCommands.DATASOURCE_RELATION.id, {});
{
commandId: CustomCommands.DATASOURCE_RELATION.id,
getCommandOption: async () => {
return {
args: {},
};
},
},
]);
// const nodes = await app.getAllNodes();
// const classNodes = nodes.filter((item) => {
// return item.id.includes('classNodeId');
// });
// if (classNodes?.[0]) {
// const targetClassId = classNodes[0].id;
// await app.commandService.executeCommand<NsNodeCmd.DelNode.IArgs>(
// XFlowNodeCommands.DEL_NODE.id,
// {
// nodeConfig: { id: targetClassId, type: 'class' },
// },
// );
// }
};
/** 查询当前数据源下的维度节点和边的数据 */
export const initDimensionGraphCmds = async (args: {
commandService: IGraphCommandService;
target: NsGraph.INodeConfig;
}) => {
const { commandService, target } = args;
await commandService.executeCommandPipeline([
{
commandId: XFlowGraphCommands.LOAD_DATA.id,
getCommandOption: async () => {
return {
args: {
loadDataService: GraphApi.loadDimensionData,
},
};
},
} as IGraphPipelineCommand<NsGraphCmd.GraphLoadData.IArgs>,
/** 2. 执行布局算法 */
{
commandId: XFlowGraphCommands.GRAPH_LAYOUT.id,
getCommandOption: async (ctx) => {
const { graphData } = ctx.getResult();
const targetData = {
...target.data,
};
delete targetData.x;
delete targetData.y;
const addGraphData = addDataSourceInfoAsDimensionParents(graphData, targetData);
ctx.setResult(addGraphData);
return {
args: {
layoutType: 'dagre',
layoutOptions: {
type: 'dagre',
/** 布局方向 */
rankdir: 'LR',
/** 节点间距 */
nodesep: 30,
/** 层间距 */
ranksep: 80,
begin: [0, 0],
},
graphData: addGraphData,
},
};
},
} as IGraphPipelineCommand<NsGraphCmd.GraphLayout.IArgs>,
/** 3. 画布内容渲染 */
{
commandId: XFlowGraphCommands.GRAPH_RENDER.id,
getCommandOption: async (ctx) => {
const { graphData } = ctx.getResult();
return {
args: {
graphData,
},
};
},
} as IGraphPipelineCommand<NsGraphCmd.GraphRender.IArgs>,
/** 4. 缩放画布 */
{
commandId: XFlowGraphCommands.GRAPH_ZOOM.id,
getCommandOption: async () => {
return {
args: { factor: 'fit', zoomOptions: { maxScale: 0.9 } },
};
},
} as IGraphPipelineCommand<NsGraphCmd.GraphZoom.IArgs>,
]);
};

View File

@@ -0,0 +1,51 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { uuidv4 } from '@antv/xflow';
import { XFlowNodeCommands } from '@antv/xflow';
import { DATASOURCE_NODE_RENDER_ID } from './constant';
import type { NsNodeCmd } from '@antv/xflow';
import type { NsNodeCollapsePanel } from '@antv/xflow';
import { Card } from 'antd';
export const onNodeDrop: NsNodeCollapsePanel.IOnNodeDrop = async (node, commands, modelService) => {
const args: NsNodeCmd.AddNode.IArgs = {
nodeConfig: { ...node, id: uuidv4() },
};
commands.executeCommand(XFlowNodeCommands.ADD_NODE.id, args);
};
const NodeDescription = (props: { name: string }) => {
return (
<Card size="small" style={{ width: '200px' }} bordered={false}>
</Card>
);
};
export const nodeDataService: NsNodeCollapsePanel.INodeDataService = async (meta, modelService) => {
return [
{
id: '数据源',
header: '数据源',
children: [
{
id: '2',
label: '新增数据源',
parentId: '1',
renderKey: DATASOURCE_NODE_RENDER_ID,
// renderComponent: (props) => (
// <div className="react-dnd-node react-custom-node-1"> {props.data.label} </div>
// ),
popoverContent: <NodeDescription name="数据源" />,
},
],
},
];
};
export const searchService: NsNodeCollapsePanel.ISearchService = async (
nodes: NsNodeCollapsePanel.IPanelNode[] = [],
keyword: string,
) => {
const list = nodes.filter((node) => node.label.includes(keyword));
return list;
};

View File

@@ -0,0 +1,79 @@
import type { IProps } from './index';
import { NsGraph } from '@antv/xflow';
import type { Graph } from '@antv/x6';
import { createHookConfig, DisposableCollection } from '@antv/xflow';
import { DATASOURCE_NODE_RENDER_ID, GROUP_NODE_RENDER_ID } from './constant';
import { AlgoNode } from './ReactNodes/algoNode';
import { GroupNode } from './ReactNodes/group';
export const useGraphHookConfig = createHookConfig<IProps>((config) => {
// 获取 Props
// const props = proxy.getValue();
config.setRegisterHook((hooks) => {
const disposableList = [
// 注册增加 react Node Render
hooks.reactNodeRender.registerHook({
name: 'add react node',
handler: async (renderMap) => {
renderMap.set(DATASOURCE_NODE_RENDER_ID, AlgoNode);
renderMap.set(GROUP_NODE_RENDER_ID, GroupNode);
},
}),
// 注册修改graphOptions配置的钩子
hooks.graphOptions.registerHook({
name: 'custom-x6-options',
after: 'dag-extension-x6-options',
handler: async (options) => {
const graphOptions: Graph.Options = {
connecting: {
allowLoop: false,
// 是否触发交互事件
validateMagnet() {
// return magnet.getAttribute('port-group') !== NsGraph.AnchorGroup.TOP
return true;
},
// 显示可用的链接桩
validateConnection(args: any) {
const { sourceView, targetView, sourceMagnet, targetMagnet } = args;
// 不允许连接到自己
if (sourceView === targetView) {
return false;
}
// 没有起点的返回false
if (!sourceMagnet) {
return false;
}
if (!targetMagnet) {
return false;
}
// 只能从上游节点的输出链接桩创建连接
if (sourceMagnet?.getAttribute('port-group') === NsGraph.AnchorGroup.LEFT) {
return false;
}
// 只能连接到下游节点的输入桩
if (targetMagnet?.getAttribute('port-group') === NsGraph.AnchorGroup.RIGHT) {
return false;
}
const node = targetView!.cell as any;
// 判断目标链接桩是否可连接
const portId = targetMagnet.getAttribute('port')!;
const port = node.getPort(portId);
return !!port;
},
},
};
options.connecting = { ...options.connecting, ...graphOptions.connecting };
},
}),
// hooks.afterGraphInit.registerHook({
// name: '注册toolTips工具',
// handler: async (args) => {},
// }),
];
const toDispose = new DisposableCollection();
toDispose.pushAll(disposableList);
return toDispose;
});
});

View File

@@ -0,0 +1,176 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import type { NsNodeCmd, NsEdgeCmd, IMenuOptions, NsGraph, NsGraphCmd } from '@antv/xflow';
import type { NsRenameNodeCmd } from './CmdExtensions/CmdRenameNodeModal';
import { createCtxMenuConfig, MenuItemType } from '@antv/xflow';
import { IconStore, XFlowNodeCommands, XFlowEdgeCommands, XFlowGraphCommands } from '@antv/xflow';
import { initDimensionGraphCmds } from './ConfigCmd';
import type { NsConfirmModalCmd } from './CmdExtensions/CmdConfirmModal';
import { NS_DATA_SOURCE_RELATION_MODAL_OPEN_STATE } from './ConfigModelService';
import { DeleteOutlined, EditOutlined, StopOutlined } from '@ant-design/icons';
import { CustomCommands } from './CmdExtensions/constants';
import { GraphApi } from './service';
/** menuitem 配置 */
export namespace NsMenuItemConfig {
/** 注册菜单依赖的icon */
IconStore.set('DeleteOutlined', DeleteOutlined);
IconStore.set('EditOutlined', EditOutlined);
IconStore.set('StopOutlined', StopOutlined);
export const DELETE_EDGE: IMenuOptions = {
id: XFlowEdgeCommands.DEL_EDGE.id,
label: '删除边',
iconName: 'DeleteOutlined',
onClick: async (args) => {
const { target, commandService, modelService } = args;
await commandService.executeCommand<NsEdgeCmd.DelEdge.IArgs>(XFlowEdgeCommands.DEL_EDGE.id, {
edgeConfig: target.data as NsGraph.IEdgeConfig,
});
// 保存数据源关联关系
await commandService.executeCommand(CustomCommands.DATASOURCE_RELATION.id, {});
// 保存图数据
commandService.executeCommand<NsGraphCmd.SaveGraphData.IArgs>(
XFlowGraphCommands.SAVE_GRAPH_DATA.id,
{ saveGraphDataService: (meta, graphData) => GraphApi.saveGraphData!(meta, graphData) },
);
// 关闭设置关联关系弹窗
const modalModel = await modelService!.awaitModel(
NS_DATA_SOURCE_RELATION_MODAL_OPEN_STATE.ID,
);
modalModel.setValue({ open: false });
},
};
export const DELETE_NODE: IMenuOptions = {
id: XFlowNodeCommands.DEL_NODE.id,
label: '删除节点',
iconName: 'DeleteOutlined',
onClick: async ({ target, commandService }) => {
commandService.executeCommand<NsNodeCmd.DelNode.IArgs>(XFlowNodeCommands.DEL_NODE.id, {
nodeConfig: { id: target?.data?.id || '', targetData: target.data },
});
},
};
export const EMPTY_MENU: IMenuOptions = {
id: 'EMPTY_MENU_ITEM',
label: '暂无可用',
isEnabled: false,
iconName: 'DeleteOutlined',
};
export const RENAME_NODE: IMenuOptions = {
id: CustomCommands.SHOW_RENAME_MODAL.id,
label: '重命名',
isVisible: true,
iconName: 'EditOutlined',
onClick: async ({ target, commandService }) => {
const nodeConfig = target.data as NsGraph.INodeConfig;
commandService.executeCommand<NsRenameNodeCmd.IArgs>(CustomCommands.SHOW_RENAME_MODAL.id, {
nodeConfig,
updateNodeNameService: GraphApi.renameNode,
});
},
};
export const DELETE_DATASOURCE_NODE: IMenuOptions = {
id: CustomCommands.SHOW_RENAME_MODAL.id,
label: '删除数据源',
isVisible: true,
iconName: 'EditOutlined',
onClick: async ({ target, commandService }) => {
const nodeConfig = {
...target.data,
modalProps: {
title: '确认删除?',
},
} as NsGraph.INodeConfig;
await commandService.executeCommand<NsConfirmModalCmd.IArgs>(
CustomCommands.SHOW_CONFIRM_MODAL.id,
{
nodeConfig,
confirmModalCallBack: async () => {
await commandService.executeCommand<NsNodeCmd.DelNode.IArgs>(
XFlowNodeCommands.DEL_NODE.id,
{
nodeConfig: {
id: target?.data?.id || '',
type: 'dataSource',
targetData: target.data,
},
},
);
commandService.executeCommand<NsGraphCmd.SaveGraphData.IArgs>(
XFlowGraphCommands.SAVE_GRAPH_DATA.id,
{
saveGraphDataService: (meta, graphData) => GraphApi.saveGraphData!(meta, graphData),
},
);
},
},
);
},
};
export const VIEW_DIMENSION: IMenuOptions = {
id: CustomCommands.VIEW_DIMENSION.id,
label: '查看维度',
isVisible: true,
iconName: 'EditOutlined',
onClick: async (args) => {
const { target, commandService, modelService } = args as any;
initDimensionGraphCmds({ commandService, target });
},
};
export const SEPARATOR: IMenuOptions = {
id: 'separator',
type: MenuItemType.Separator,
};
}
export const useMenuConfig = createCtxMenuConfig((config) => {
config.setMenuModelService(async (target, model, modelService, toDispose) => {
const { type, cell } = target as any;
switch (type) {
/** 节点菜单 */
case 'node':
model.setValue({
id: 'root',
type: MenuItemType.Root,
submenu: [
// NsMenuItemConfig.VIEW_DIMENSION,
// NsMenuItemConfig.SEPARATOR,
// NsMenuItemConfig.DELETE_NODE,
NsMenuItemConfig.DELETE_DATASOURCE_NODE,
// NsMenuItemConfig.RENAME_NODE,
],
});
break;
/** 边菜单 */
case 'edge':
model.setValue({
id: 'root',
type: MenuItemType.Root,
submenu: [NsMenuItemConfig.DELETE_EDGE],
});
break;
/** 画布菜单 */
case 'blank':
model.setValue({
id: 'root',
type: MenuItemType.Root,
submenu: [NsMenuItemConfig.EMPTY_MENU],
});
break;
/** 默认菜单 */
default:
model.setValue({
id: 'root',
type: MenuItemType.Root,
submenu: [NsMenuItemConfig.EMPTY_MENU],
});
break;
}
});
});

View File

@@ -0,0 +1,33 @@
import type { Disposable, IModelService } from '@antv/xflow';
import { createModelServiceConfig, DisposableCollection } from '@antv/xflow';
export namespace NS_DATA_SOURCE_RELATION_MODAL_OPEN_STATE {
export const ID = 'NS_DATA_SOURCE_RELATION_MODAL_OPEN_STATE';
// export const id = 'NS_DATA_SOURCE_RELATION_MODAL_OPEN_STATE';
export interface IState {
open: boolean;
}
}
export const useModelServiceConfig = createModelServiceConfig((config) => {
config.registerModel((registry) => {
const list: Disposable[] = [
registry.registerModel({
id: NS_DATA_SOURCE_RELATION_MODAL_OPEN_STATE.ID,
// getInitialValue: () => {
// open: false;
// },
}),
];
const toDispose = new DisposableCollection();
toDispose.pushAll(list);
return toDispose;
});
});
export const useOpenState = async (contextService: IModelService) => {
const ctx = await contextService.awaitModel<NS_DATA_SOURCE_RELATION_MODAL_OPEN_STATE.IState>(
NS_DATA_SOURCE_RELATION_MODAL_OPEN_STATE.ID,
);
return ctx.getValidValue();
};

View File

@@ -0,0 +1,242 @@
import type { IToolbarItemOptions } from '@antv/xflow';
import { createToolbarConfig } from '@antv/xflow';
import type { IModelService } from '@antv/xflow';
import {
XFlowGraphCommands,
XFlowDagCommands,
NsGraphStatusCommand,
MODELS,
IconStore,
} from '@antv/xflow';
import {
UngroupOutlined,
SaveOutlined,
CloudSyncOutlined,
GroupOutlined,
GatewayOutlined,
PlaySquareOutlined,
StopOutlined,
} from '@ant-design/icons';
import { GraphApi } from './service';
import type { NsGraphCmd } from '@antv/xflow';
import { Radio } from 'antd';
export namespace NSToolbarConfig {
/** 注册icon 类型 */
IconStore.set('SaveOutlined', SaveOutlined);
IconStore.set('CloudSyncOutlined', CloudSyncOutlined);
IconStore.set('GatewayOutlined', GatewayOutlined);
IconStore.set('GroupOutlined', GroupOutlined);
IconStore.set('UngroupOutlined', UngroupOutlined);
IconStore.set('PlaySquareOutlined', PlaySquareOutlined);
IconStore.set('StopOutlined', StopOutlined);
/** toolbar依赖的状态 */
export interface IToolbarState {
isMultiSelectionActive: boolean;
isNodeSelected: boolean;
isGroupSelected: boolean;
isProcessing: boolean;
}
export const getDependencies = async (modelService: IModelService) => {
return [
await MODELS.SELECTED_CELLS.getModel(modelService),
await MODELS.GRAPH_ENABLE_MULTI_SELECT.getModel(modelService),
await NsGraphStatusCommand.MODEL.getModel(modelService),
];
};
/** toolbar依赖的状态 */
export const getToolbarState = async (modelService: IModelService) => {
// isMultiSelectionActive
const { isEnable: isMultiSelectionActive } = await MODELS.GRAPH_ENABLE_MULTI_SELECT.useValue(
modelService,
);
// isGroupSelected
const isGroupSelected = await MODELS.IS_GROUP_SELECTED.useValue(modelService);
// isNormalNodesSelected: node不能是GroupNode
const isNormalNodesSelected = await MODELS.IS_NORMAL_NODES_SELECTED.useValue(modelService);
// statusInfo
const statusInfo = await NsGraphStatusCommand.MODEL.useValue(modelService);
return {
isNodeSelected: isNormalNodesSelected,
isGroupSelected,
isMultiSelectionActive,
isProcessing: statusInfo.graphStatus === NsGraphStatusCommand.StatusEnum.PROCESSING,
} as NSToolbarConfig.IToolbarState;
};
export const getToolbarItems = async () => {
const toolbarGroup1: IToolbarItemOptions[] = [];
const toolbarGroup2: IToolbarItemOptions[] = [];
const toolbarGroup3: IToolbarItemOptions[] = [];
/** 保存数据 */
toolbarGroup1.push({
id: XFlowGraphCommands.SAVE_GRAPH_DATA.id,
iconName: 'SaveOutlined',
tooltip: '保存数据',
onClick: async ({ commandService }) => {
commandService.executeCommand<NsGraphCmd.SaveGraphData.IArgs>(
XFlowGraphCommands.SAVE_GRAPH_DATA.id,
{ saveGraphDataService: (meta, graphData) => GraphApi.saveGraphData!(meta, graphData) },
);
},
});
// /** 部署服务按钮 */
// toolbarGroup1.push({
// iconName: 'CloudSyncOutlined',
// tooltip: '部署服务',
// id: CustomCommands.DEPLOY_SERVICE.id,
// onClick: ({ commandService }) => {
// commandService.executeCommand<NsDeployDagCmd.IArgs>(CustomCommands.DEPLOY_SERVICE.id, {
// deployDagService: (meta, graphData) => GraphApi.deployDagService(meta, graphData),
// });
// },
// });
// /** 开启框选 */
// toolbarGroup2.push({
// id: XFlowGraphCommands.GRAPH_TOGGLE_MULTI_SELECT.id,
// tooltip: '开启框选',
// iconName: 'GatewayOutlined',
// active: state.isMultiSelectionActive,
// onClick: async ({ commandService }) => {
// commandService.executeCommand<NsGraphCmd.GraphToggleMultiSelect.IArgs>(
// XFlowGraphCommands.GRAPH_TOGGLE_MULTI_SELECT.id,
// {},
// );
// },
// });
// /** 新建群组 */
// toolbarGroup2.push({
// id: XFlowGroupCommands.ADD_GROUP.id,
// tooltip: '新建群组',
// iconName: 'GroupOutlined',
// isEnabled: state.isNodeSelected,
// onClick: async ({ commandService, modelService }) => {
// const cells = await MODELS.SELECTED_CELLS.useValue(modelService);
// const groupChildren = cells.map((cell) => cell.id);
// commandService.executeCommand<NsGroupCmd.AddGroup.IArgs>(XFlowGroupCommands.ADD_GROUP.id, {
// nodeConfig: {
// id: uuidv4(),
// renderKey: GROUP_NODE_RENDER_ID,
// groupChildren,
// groupCollapsedSize: { width: 200, height: 40 },
// label: '新建群组',
// },
// });
// },
// });
// /** 解散群组 */
// toolbarGroup2.push({
// id: XFlowGroupCommands.DEL_GROUP.id,
// tooltip: '解散群组',
// iconName: 'UngroupOutlined',
// isEnabled: state.isGroupSelected,
// onClick: async ({ commandService, modelService }) => {
// const cell = await MODELS.SELECTED_NODE.useValue(modelService);
// const nodeConfig = cell.getData();
// commandService.executeCommand<NsGroupCmd.AddGroup.IArgs>(XFlowGroupCommands.DEL_GROUP.id, {
// nodeConfig: nodeConfig,
// });
// },
// });
// toolbarGroup3.push({
// id: XFlowDagCommands.QUERY_GRAPH_STATUS.id + 'play',
// tooltip: '开始执行',
// iconName: 'PlaySquareOutlined',
// isEnabled: !state.isProcessing,
// onClick: async ({ commandService }) => {
// commandService.executeCommand<NsGraphStatusCommand.IArgs>(
// XFlowDagCommands.QUERY_GRAPH_STATUS.id,
// {
// graphStatusService: GraphApi.graphStatusService,
// loopInterval: 3000,
// },
// );
// },
// });
// toolbarGroup3.push({
// id: XFlowDagCommands.QUERY_GRAPH_STATUS.id + 'stop',
// tooltip: '停止执行',
// iconName: 'StopOutlined',
// isEnabled: state.isProcessing,
// onClick: async ({ commandService }) => {
// commandService.executeCommand<NsGraphStatusCommand.IArgs>(
// XFlowDagCommands.QUERY_GRAPH_STATUS.id,
// {
// graphStatusService: GraphApi.stopGraphStatusService,
// loopInterval: 5000,
// },
// );
// },
// render: (props) => {
// return (
// <Popconfirm
// title="确定停止执行?"
// onConfirm={() => {
// props.onClick();
// }}
// >
// {props.children}
// </Popconfirm>
// );
// },
// });
return [
{ name: 'graphData', items: toolbarGroup1 },
{ name: 'groupOperations', items: toolbarGroup2 },
{
name: 'customCmd',
items: toolbarGroup3,
},
];
};
}
export const getExtraToolbarItems = async () => {
const toolbarGroup: IToolbarItemOptions[] = [];
/** 保存数据 */
toolbarGroup.push({
id: XFlowDagCommands.QUERY_GRAPH_STATUS.id + 'switchShowType',
render: () => {
return (
<Radio.Group defaultValue="dataSource" buttonStyle="solid" size="small">
<Radio.Button value="dataSource"></Radio.Button>
<Radio.Button value="dimension"></Radio.Button>
<Radio.Button value="metric"></Radio.Button>
</Radio.Group>
);
},
// text: '添加节点',
// tooltip: '添加节点配置extraGroups',
});
return [{ name: 'extra', items: toolbarGroup }];
};
export const useToolbarConfig = createToolbarConfig((toolbarConfig) => {
/** 生产 toolbar item */
toolbarConfig.setToolbarModelService(async (toolbarModel, modelService, toDispose) => {
const updateToolbarModel = async () => {
const state = await NSToolbarConfig.getToolbarState(modelService);
const toolbarItems = await NSToolbarConfig.getToolbarItems(state);
// const extraToolbarItems = await getExtraToolbarItems();
toolbarModel.setValue((toolbar) => {
toolbar.mainGroups = toolbarItems;
// toolbar.extraGroups = extraToolbarItems;
});
};
const models = await NSToolbarConfig.getDependencies(modelService);
const subscriptions = models.map((model) => {
return model.watch(async () => {
updateToolbarModel();
});
});
toDispose.pushAll(subscriptions);
});
});

View File

@@ -0,0 +1,85 @@
import React from 'react';
import ReactDom from 'react-dom';
import { Tooltip } from 'antd';
import type { EdgeView } from '@antv/x6';
import { Graph, ToolsView } from '@antv/x6';
class TooltipTool extends ToolsView.ToolItem<EdgeView, TooltipToolOptions> {
private knob: HTMLDivElement;
render() {
if (!this.knob) {
this.knob = ToolsView.createElement('div', false) as HTMLDivElement;
this.knob.style.position = 'absolute';
this.container.appendChild(this.knob);
}
return this;
}
private toggleTooltip(visible: boolean) {
if (this.knob) {
ReactDom.unmountComponentAtNode(this.knob);
if (visible) {
ReactDom.render(
<Tooltip title={this.options.tooltip} open={visible} destroyTooltipOnHide>
<div />
</Tooltip>,
this.knob,
);
}
}
}
private onMosueEnter({ e }: { e: MouseEvent }) {
this.updatePosition(e);
this.toggleTooltip(true);
}
private onMouseLeave() {
this.updatePosition();
this.toggleTooltip(false);
}
private onMouseMove() {
this.updatePosition();
this.toggleTooltip(false);
}
delegateEvents() {
this.cellView.on('cell:mouseenter', this.onMosueEnter, this);
this.cellView.on('cell:mouseleave', this.onMouseLeave, this);
this.cellView.on('cell:mousemove', this.onMouseMove, this);
return super.delegateEvents();
}
private updatePosition(e?: MouseEvent) {
const style = this.knob.style;
if (e) {
const p = this.graph.clientToGraph(e.clientX, e.clientY);
style.display = 'block';
style.left = `${p.x}px`;
style.top = `${p.y}px`;
} else {
style.display = 'none';
style.left = '-1000px';
style.top = '-1000px';
}
}
protected onRemove() {
this.toggleTooltip(false);
this.cellView.off('cell:mouseenter', this.onMosueEnter, this);
this.cellView.off('cell:mouseleave', this.onMouseLeave, this);
this.cellView.off('cell:mousemove', this.onMouseMove, this);
}
}
TooltipTool.config({
tagName: 'div',
isSVGElement: false,
});
export interface TooltipToolOptions extends ToolsView.ToolItem.Options {
tooltip?: string;
}
Graph.registerEdgeTool('tooltip', TooltipTool, true);

View File

@@ -0,0 +1,101 @@
@light-border: 1px solid #d9d9d9;
@primaryColor: #c2c8d5;
.xflow-algo-node {
z-index: 10;
display: flex;
width: 180px;
height: 36px;
line-height: 36px;
text-align: center;
background-color: #fff;
border: 1px solid @primaryColor;
border-radius: 2px;
box-shadow: ~'-1px -1px 4px 0 rgba(223,223,223,0.50), -2px 2px 4px 0 rgba(244,244,244,0.50), 2px 3px 8px 2px rgba(151,151,151,0.05)';
transition: all ease-in-out 0.15s;
&:hover {
background-color: #fff;
border: 1px solid #3057e3;
// border: 1px solid @primaryColor;
box-shadow: 0 0 3px 3px rgba(48, 86, 227, 0.15);
cursor: move;
}
.icon {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
}
.label {
width: 108px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
word-break: break-word;
}
.status {
width: 36px;
}
&.panel-node {
border: 0;
}
}
.x6-node-selected {
.xflow-algo-node {
background-color: rgba(48, 86, 227, 0.05);
border: 1px solid #3057e3;
box-shadow: 0 0 3px 3px rgba(48, 86, 227, 0.15);
&:hover {
background-color: #fff;
box-shadow: 0 0 5px 5px rgba(48, 86, 227, 0.15);
}
}
}
.dag-solution-layout {
.xflow-canvas-root {
.xflow-algo-node {
height: 72px !important;
line-height: 72px !important;
}
}
}
.dataSourceTooltipWrapper {
max-width: 500px;
.ant-tooltip-inner {
padding:0;
}
.dataSourceTooltip {
width: 300px;
background: #fff;
border-radius: 5px;
color: #4d4d4d;
padding: 15px;
opacity: .9;
font-size: 11px;
box-shadow: 0 0 5px #d8d8d8;
p {
margin-bottom: 0;
font-size: 13px;
padding: 4px;
line-height: 18px;
overflow: hidden;
display: flex;
flex-direction: row;
.dataSourceTooltipLabel {
display: block;
width: 64px;
color: grey;
}
.dataSourceTooltipValue{
flex: 1 1;
display: block;
width: 220px;
padding: 0 4px;
word-break: break-all;
}
}
}
}

View File

@@ -0,0 +1,112 @@
import React from 'react';
import {
DatabaseOutlined,
RedoOutlined,
CloseCircleOutlined,
CheckCircleOutlined,
ExclamationCircleOutlined,
InfoCircleOutlined,
} from '@ant-design/icons';
import type { NsGraph } from '@antv/xflow';
import { NsGraphStatusCommand } from '@antv/xflow';
import { Tooltip } from 'antd';
import moment from 'moment';
import './algoNode.less';
const fontStyle = { fontSize: '16px', color: '#3057e3' };
interface IProps {
status: NsGraphStatusCommand.StatusEnum;
hide: boolean;
}
export const AlgoIcon: React.FC<IProps> = (props) => {
if (props.hide) {
return null;
}
switch (props.status) {
case NsGraphStatusCommand.StatusEnum.PROCESSING:
return <RedoOutlined spin style={{ color: '#c1cdf7', fontSize: '16px' }} />;
case NsGraphStatusCommand.StatusEnum.ERROR:
return <CloseCircleOutlined style={{ color: '#ff4d4f', fontSize: '16px' }} />;
case NsGraphStatusCommand.StatusEnum.SUCCESS:
return <CheckCircleOutlined style={{ color: '#39ca74cc', fontSize: '16px' }} />;
case NsGraphStatusCommand.StatusEnum.WARNING:
return <ExclamationCircleOutlined style={{ color: '#faad14', fontSize: '16px' }} />;
case NsGraphStatusCommand.StatusEnum.DEFAULT:
return <InfoCircleOutlined style={{ color: '#d9d9d9', fontSize: '16px' }} />;
default:
return null;
}
};
export const AlgoNode: NsGraph.INodeRender = (props) => {
const { data } = props;
const dataSourceData = data.payload;
const openState = dataSourceData ? undefined : false;
let tooltipNode = <></>;
if (dataSourceData) {
const { name, id, bizName, description, createdBy, updatedAt } = dataSourceData;
const labelList = [
{
label: '数据源ID',
value: id,
},
{
label: '名称',
value: name,
},
{
label: '英文名',
value: bizName,
},
{
label: '创建人',
value: createdBy,
},
{
label: '更新时间',
value: updatedAt ? moment(updatedAt).format('YYYY-MM-DD HH:mm:ss') : '-',
},
{
label: '描述',
value: description,
},
];
tooltipNode = (
<div className="dataSourceTooltip">
{labelList.map(({ label, value }) => {
return (
<p key={value}>
<span className="dataSourceTooltipLabel">{label}:</span>
<span className="dataSourceTooltipValue">{value || '-'}</span>
</p>
);
})}
</div>
);
}
return (
<div className={`xflow-algo-node ${props.isNodeTreePanel ? 'panel-node' : ''}`}>
<span className="icon">
<DatabaseOutlined style={fontStyle} />
</span>
<span className="label">
<Tooltip
open={openState}
title={tooltipNode}
placement="right"
color="#fff"
overlayClassName="dataSourceTooltipWrapper"
>
{props.data.label}
</Tooltip>
</span>
<span className="status">
<AlgoIcon status={props.data && props.data.status} hide={props.isNodeTreePanel} />
</span>
</div>
);
};

View File

@@ -0,0 +1,50 @@
@light-border: 1px solid #d9d9d9;
@primaryColor: #c1cdf7;
.xflow-group-node {
z-index: 9;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.65);
border: 1px solid rgba(255, 255, 255, 0.25);
border-radius: 4px;
box-shadow: ~'rgb(17 49 96 / 12%) 0px 1px 3px 0px, rgb(17 49 96 / 4%) 0px 0px 0px 1px';
cursor: grab;
&:hover {
background-color: rgba(227, 244, 255, 0.45);
border: 1px solid @primaryColor;
box-shadow: 0 0 3px 3px rgba(64, 169, 255, 0.2);
cursor: move;
}
.xflow-group-header {
display: flex;
justify-content: space-between;
padding: 0 12px;
font-size: 14px;
line-height: 38px;
.header-left {
width: 80%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.header-right {
display: inline-flex;
align-items: center;
span.anticon {
margin-left: 8px;
}
}
}
}
.x6-node-selected {
.xflow-group-node {
background-color: rgba(243, 249, 255, 0.92);
border: 1px solid @primaryColor;
box-shadow: 0 0 3px 3px rgb(64 169 255 / 20%);
&:hover {
background-color: rgba(243, 249, 255, 0.6);
}
}
}

View File

@@ -0,0 +1,37 @@
import { PlusSquareOutlined, MinusSquareOutlined } from '@ant-design/icons';
import type { NsGraph } from '@antv/xflow';
import { useXFlowApp, XFlowGroupCommands } from '@antv/xflow';
import './group.less';
export const GroupNode: NsGraph.INodeRender = (props) => {
const { cell } = props;
const app = useXFlowApp();
const isCollapsed = props.data.isCollapsed || false;
const onExpand = () => {
app.executeCommand(XFlowGroupCommands.COLLAPSE_GROUP.id, {
nodeId: cell.id,
isCollapsed: false,
collapsedSize: { width: 200, height: 40 },
});
};
const onCollapse = () => {
app.executeCommand(XFlowGroupCommands.COLLAPSE_GROUP.id, {
nodeId: cell.id,
isCollapsed: true,
collapsedSize: { width: 200, height: 40 },
gap: 3,
});
};
return (
<div className="xflow-group-node">
<div className="xflow-group-header">
<div className="header-left">{props.data.label}</div>
<div className="header-right">
{isCollapsed && <PlusSquareOutlined onClick={onExpand} />}
{!isCollapsed && <MinusSquareOutlined onClick={onCollapse} />}
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,166 @@
import React, { useEffect, useState } from 'react';
import { Form, Button, Drawer, Space, Input, Select, message } from 'antd';
import { formLayout } from '@/components/FormHelper/utils';
import { createOrUpdateDatasourceRela } from '../../service';
import { getRelationConfigInfo } from '../utils';
import { useXFlowApp } from '@antv/xflow';
import { CustomCommands } from '../CmdExtensions/constants';
export type DataSourceRelationFormDrawerProps = {
domainId: number;
nodeDataSource: any;
open: boolean;
onClose?: () => void;
};
const FormItem = Form.Item;
const { Option } = Select;
const DataSourceRelationFormDrawer: React.FC<DataSourceRelationFormDrawerProps> = ({
domainId,
open,
nodeDataSource,
onClose,
}) => {
const [form] = Form.useForm();
const [saveLoading, setSaveLoading] = useState(false);
const [dataSourceOptions, setDataSourceOptions] = useState<any[]>([]);
const app = useXFlowApp();
const getRelationListInfo = async () => {
await app.commandService.executeCommand(CustomCommands.DATASOURCE_RELATION.id, {});
};
useEffect(() => {
const { sourceData, targetData } = nodeDataSource;
const dataSourceFromIdentifiers = sourceData?.datasourceDetail?.identifiers || [];
const dataSourceToIdentifiers = targetData?.datasourceDetail?.identifiers || [];
const dataSourceToIdentifiersNames = dataSourceToIdentifiers.map((item) => {
return item.name;
});
const keyOptions = dataSourceFromIdentifiers.reduce((options: any[], item: any) => {
const { name } = item;
if (dataSourceToIdentifiersNames.includes(name)) {
options.push(item);
}
return options;
}, []);
setDataSourceOptions(
keyOptions.map((item: any) => {
const { name } = item;
return {
label: name,
value: name,
};
}),
);
}, [nodeDataSource]);
useEffect(() => {
const { sourceData, targetData } = nodeDataSource;
if (!sourceData || !targetData) {
return;
}
const relationList = app.commandService.getGlobal('dataSourceRelationList') || [];
const config = getRelationConfigInfo(sourceData.id, targetData.id, relationList);
if (config) {
form.setFieldsValue({
joinKey: config.joinKey,
});
} else {
form.setFieldsValue({
joinKey: '',
});
}
}, [nodeDataSource]);
const renderContent = () => {
return (
<>
<FormItem hidden={true} name="id" label="ID">
<Input placeholder="id" />
</FormItem>
<FormItem label="主数据源:">{nodeDataSource?.sourceData?.name}</FormItem>
<FormItem label="关联数据源:">{nodeDataSource?.targetData?.name}</FormItem>
<FormItem
name="joinKey"
label="可关联Key:"
tooltip="主从数据源中必须具有相同的主键或外键才可建立关联关系"
rules={[{ required: true, message: '请选择关联Key' }]}
>
<Select placeholder="请选择关联Key">
{dataSourceOptions.map((item) => (
<Option key={item.value} value={item.value}>
{item.label}
</Option>
))}
</Select>
</FormItem>
</>
);
};
const saveRelation = async () => {
const values = await form.validateFields();
setSaveLoading(true);
const { code, msg } = await createOrUpdateDatasourceRela({
domainId,
datasourceFrom: nodeDataSource?.sourceData?.id,
datasourceTo: nodeDataSource?.targetData?.id,
...values,
});
setSaveLoading(false);
if (code === 200) {
message.success('保存成功');
getRelationListInfo();
onClose?.();
return;
}
message.error(msg);
};
const renderFooter = () => {
return (
<Space>
<Button
onClick={() => {
onClose?.();
}}
>
</Button>
<Button
type="primary"
loading={saveLoading}
onClick={() => {
saveRelation();
}}
>
</Button>
</Space>
);
};
return (
<Drawer
forceRender
width={400}
getContainer={false}
title={'数据源关联信息'}
mask={false}
open={open}
footer={renderFooter()}
onClose={() => {
onClose?.();
}}
>
<Form {...formLayout} form={form}>
{renderContent()}
</Form>
</Drawer>
);
};
export default DataSourceRelationFormDrawer;

View File

@@ -0,0 +1,35 @@
import React from 'react';
import { WorkspacePanel } from '@antv/xflow';
import type { NsJsonSchemaForm } from '@antv/xflow';
import XflowJsonSchemaFormDrawerForm from './XflowJsonSchemaFormDrawerForm';
export type CreateFormProps = {
controlMapService?: any;
formSchemaService?: any;
formValueUpdateService?: any;
};
const XflowJsonSchemaFormDrawer: React.FC<CreateFormProps> = ({
controlMapService,
formSchemaService,
formValueUpdateService,
}) => {
const defaultFormValueUpdateService: NsJsonSchemaForm.IFormValueUpdateService = async () => {};
const defaultFormSchemaService: NsJsonSchemaForm.IFormSchemaService = async () => {
return { tabs: [] };
};
const defaultControlMapService: NsJsonSchemaForm.IControlMapService = (controlMap) => {
return controlMap;
};
return (
<WorkspacePanel position={{}}>
<XflowJsonSchemaFormDrawerForm
controlMapService={controlMapService || defaultControlMapService}
formSchemaService={formSchemaService || defaultFormSchemaService}
formValueUpdateService={formValueUpdateService || defaultFormValueUpdateService}
/>
</WorkspacePanel>
);
};
export default XflowJsonSchemaFormDrawer;

View File

@@ -0,0 +1,125 @@
import React, { useEffect, useState } from 'react';
import { Drawer } from 'antd';
import { WorkspacePanel, useXFlowApp, useModelAsync, XFlowGraphCommands } from '@antv/xflow';
import { useJsonSchemaFormModel } from '@antv/xflow-extension/es/canvas-json-schema-form/service';
import { NS_DATA_SOURCE_RELATION_MODAL_OPEN_STATE } from '../ConfigModelService';
import { connect } from 'umi';
import { DATASOURCE_NODE_RENDER_ID } from '../constant';
import DataSourceRelationFormDrawer from './DataSourceRelationFormDrawer';
import { GraphApi } from '../service';
import type { StateType } from '../../model';
import DataSource from '../../Datasource';
export type CreateFormProps = {
controlMapService: any;
formSchemaService: any;
formValueUpdateService: any;
domainManger: StateType;
};
const XflowJsonSchemaFormDrawerForm: React.FC<CreateFormProps> = (props) => {
const { domainManger } = props;
const [visible, setVisible] = useState(false);
const [createModalVisible, setCreateModalVisible] = useState<boolean>(false);
const [dataSourceItem, setDataSourceItem] = useState<any>();
const [nodeDataSource, setNodeDataSource] = useState<any>({
sourceData: {},
targetData: {},
});
const app = useXFlowApp();
// 借用JsonSchemaForm钩子函数对元素状态进行监听
const { state, commandService, modelService } = useJsonSchemaFormModel({
...props,
targetType: ['node', 'edge', 'canvas', 'group'],
position: {},
});
const [modalOpenState] = useModelAsync({
getModel: async () => {
return await modelService.awaitModel(NS_DATA_SOURCE_RELATION_MODAL_OPEN_STATE.ID);
},
initialState: false,
});
useEffect(() => {
const { open } = modalOpenState as any;
setVisible(open);
}, [modalOpenState]);
useEffect(() => {
const { targetType, targetData } = state;
if (targetType && ['node', 'edge'].includes(targetType)) {
const { renderKey, payload } = targetData as any;
if (renderKey === DATASOURCE_NODE_RENDER_ID) {
setDataSourceItem(payload);
setCreateModalVisible(true);
} else {
const { sourceNodeData, targetNodeData } = targetData as any;
setNodeDataSource({
sourceData: sourceNodeData.payload,
targetData: targetNodeData.payload,
});
setVisible(true);
}
}
}, [state]);
const resetSelectedNode = async () => {
const x6Graph = await app.graphProvider.getGraphInstance();
x6Graph.resetSelection();
};
const handleDataSourceRelationDrawerClose = () => {
resetSelectedNode();
setVisible(false);
};
return (
<WorkspacePanel position={{}}>
<DataSourceRelationFormDrawer
domainId={domainManger.selectDomainId}
nodeDataSource={nodeDataSource}
onClose={() => {
handleDataSourceRelationDrawerClose();
}}
open={visible}
/>
<Drawer
width={'100%'}
destroyOnClose
title="数据源编辑"
open={createModalVisible}
onClose={() => {
resetSelectedNode();
setCreateModalVisible(false);
setDataSourceItem(undefined);
}}
footer={null}
>
<DataSource
initialValues={dataSourceItem}
domainId={Number(domainManger?.selectDomainId)}
onSubmitSuccess={(dataSourceInfo: any) => {
setCreateModalVisible(false);
const { targetCell, targetData } = state;
targetCell?.setData({
...targetData,
label: dataSourceInfo.name,
payload: dataSourceInfo,
id: `dataSource-${dataSourceInfo.id}`,
});
setDataSourceItem(undefined);
commandService.executeCommand(XFlowGraphCommands.SAVE_GRAPH_DATA.id, {
saveGraphDataService: (meta, graphData) => GraphApi.saveGraphData!(meta, graphData),
});
}}
/>
</Drawer>
</WorkspacePanel>
);
};
export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger,
}))(XflowJsonSchemaFormDrawerForm);

View File

@@ -0,0 +1,5 @@
export const DND_RENDER_ID = 'DND_NDOE';
export const GROUP_NODE_RENDER_ID = 'GROUP_NODE_RENDER_ID';
export const DATASOURCE_NODE_RENDER_ID = 'DATASOURCE_NODE';
export const NODE_WIDTH = 180;
export const NODE_HEIGHT = 72;

View File

@@ -0,0 +1,29 @@
import type { ISODateString, GraphConfigType, UserName } from '../data';
import type { NsGraph } from '@antv/xflow';
export type GraphConfigListItem = {
id: number;
domainId: number;
config: string;
type: GraphConfigType;
createdAt: ISODateString;
createdBy: UserName;
updatedAt: ISODateString;
updatedBy: UserName;
};
export type GraphConfig = { id: number; config: NsGraph.IGraphData };
export type RelationListItem = {
id: number;
domainId: number;
datasourceFrom: number;
datasourceTo: number;
joinKey: string;
createdAt: string;
createdBy: string;
updatedAt: string;
updatedBy: string;
};
export type RelationList = RelationListItem[];

View File

@@ -0,0 +1,103 @@
@body-bg: #fafafa;
@primaryColor: #3056e3;
@light-border: 1px solid #d9d9d9;
.dag-solution {
.__dumi-default-previewer-actions {
border: 0;
}
}
.dag-solution-layout {
position: relative;
height: 610px;
border: @light-border;
.xflow-x6-canvas {
background: @body-bg;
}
.x6-edge {
&:hover {
path:nth-child(2) {
stroke: @primaryColor;
stroke-width: 2px;
}
}
&.x6-edge-selected {
path:nth-child(2) {
stroke: @primaryColor;
stroke-width: 2px;
}
}
}
.xflow-canvas-dnd-node-tree {
border-right: @light-border;
}
.xflow-workspace-toolbar-top {
background-image: ~'linear-gradient(180deg, #ffffff 0%, #fafafa 100%)';
border-bottom: @light-border;
}
.xflow-workspace-toolbar-bottom {
text-align: center;
background: #fff;
border-top: @light-border;
}
.xflow-modal-container {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1000;
}
.xflow-collapse-panel {
.xflow-collapse-panel-header {
display: flex;
align-items: center;
justify-content: space-evenly;
background: #f7f8fa;
.ant-input-affix-wrapper {
padding: 2px 11px;
}
}
.xflow-collapse-panel-body {
background: #f7f8fa;
.xflow-collapse-header {
padding: 12px 8px;
}
}
.xflow-node-dnd-panel-footer {
display: none;
}
}
.xflow-json-form .tabs .ant-tabs-nav {
box-shadow: unset;
}
// .xflow-json-schema-form {
// .ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn {
// color: #525252;
// font-weight: 300 !important;
// }
// .xflow-json-schema-form-footer {
// display: none;
// }
// .xflow-json-form .tabs.xTab .ant-tabs-nav .ant-tabs-nav-list,
// .xflow-json-form .tabs.xTab .ant-tabs-nav .ant-tabs-nav-list .ant-tabs-tab {
// background: #f7f8fa;
// }
// .xflow-json-schema-form-body {
// position: relative;
// width: 100%;
// height: 100%;
// background: #f7f8fa;
// box-shadow: 0 1px 1px 0 rgb(206 201 201 / 50%);
// }
// }
}

View File

@@ -0,0 +1,151 @@
import React, { useState, useEffect } from 'react';
/** app 核心组件 */
import { XFlow, XFlowCanvas, XFlowGraphCommands } from '@antv/xflow';
import type { IApplication, IAppLoad, NsGraph, NsGraphCmd } from '@antv/xflow';
/** 交互组件 */
import {
/** 触发Command的交互组件 */
CanvasScaleToolbar,
NodeCollapsePanel,
CanvasContextMenu,
CanvasToolbar,
/** Graph的扩展交互组件 */
CanvasSnapline,
CanvasNodePortTooltip,
DagGraphExtension,
} from '@antv/xflow';
/** app 组件配置 */
/** 配置画布 */
import { useGraphHookConfig } from './ConfigGraph';
/** 配置Command */
import { useCmdConfig, initGraphCmds } from './ConfigCmd';
/** 配置Model */
import { useModelServiceConfig } from './ConfigModelService';
/** 配置Menu */
import { useMenuConfig } from './ConfigMenu';
/** 配置Toolbar */
import { useToolbarConfig } from './ConfigToolbar';
/** 配置Dnd组件面板 */
import * as dndPanelConfig from './ConfigDndPanel';
import { connect } from 'umi';
import type { StateType } from '../model';
import './index.less';
import XflowJsonSchemaFormDrawer from './components/XflowJsonSchemaFormDrawer';
import { getViewInfoList } from '../service';
import { getGraphConfigFromList } from './utils';
import type { GraphConfig } from './data';
import '@antv/xflow/dist/index.css';
import './ReactNodes/ToolTipsNode';
export interface IProps {
domainManger: StateType;
}
export const SemanticFlow: React.FC<IProps> = (props) => {
const { domainManger } = props;
const graphHooksConfig = useGraphHookConfig(props);
const toolbarConfig = useToolbarConfig();
const menuConfig = useMenuConfig();
const cmdConfig = useCmdConfig();
const modelServiceConfig = useModelServiceConfig();
const [graphConfig, setGraphConfig] = useState<GraphConfig>();
const [meta, setMeta] = useState<NsGraph.IGraphMeta>({
flowId: 'semanticFlow',
domainManger,
});
const cache =
React.useMemo<{ app: IApplication } | null>(
() => ({
app: null as any,
}),
[],
) || ({} as any);
const queryGraphConfig = async () => {
const { code, data } = await getViewInfoList(domainManger.selectDomainId);
if (code === 200) {
const config = getGraphConfigFromList(data);
setGraphConfig(config || ({} as GraphConfig));
}
};
useEffect(() => {
queryGraphConfig();
}, [domainManger.selectDomainId]);
useEffect(() => {
setMeta({
...meta,
domainManger,
graphConfig,
});
}, [graphConfig]);
/**
* @param app 当前XFlow工作空间
*/
const onLoad: IAppLoad = async (app) => {
cache.app = app;
initGraphCmds(cache.app);
};
const updateGraph = async (app: IApplication) => {
await app.executeCommand(XFlowGraphCommands.LOAD_META.id, {
meta,
} as NsGraphCmd.GraphMeta.IArgs);
initGraphCmds(app);
};
/** 父组件meta属性更新时,执行initGraphCmds */
React.useEffect(() => {
if (cache.app) {
updateGraph(cache.app);
}
}, [cache.app, meta]);
return (
<div id="semanticFlowContainer" style={{ height: '100%' }}>
{meta.graphConfig && (
<XFlow
className="dag-user-custom-clz dag-solution-layout"
hookConfig={graphHooksConfig}
modelServiceConfig={modelServiceConfig}
commandConfig={cmdConfig}
onLoad={onLoad}
meta={meta}
>
<DagGraphExtension layout="LR" />
<NodeCollapsePanel
className="xflow-node-panel"
searchService={dndPanelConfig.searchService}
nodeDataService={dndPanelConfig.nodeDataService}
onNodeDrop={dndPanelConfig.onNodeDrop}
position={{ width: 230, top: 0, bottom: 0, left: 0 }}
footerPosition={{ height: 0 }}
bodyPosition={{ top: 40, bottom: 0, left: 0 }}
/>
<CanvasToolbar
className="xflow-workspace-toolbar-top"
layout="horizontal"
config={toolbarConfig}
position={{ top: 0, left: 230, right: 0, bottom: 0 }}
/>
<XFlowCanvas position={{ top: 40, left: 230, right: 0, bottom: 0 }}>
<CanvasScaleToolbar position={{ top: 60, left: 20 }} />
<CanvasContextMenu config={menuConfig} />
<CanvasSnapline color="#faad14" />
<CanvasNodePortTooltip />
</XFlowCanvas>
<XflowJsonSchemaFormDrawer />
</XFlow>
)}
</div>
);
};
export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger,
}))(SemanticFlow);

View File

@@ -0,0 +1,352 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { DATASOURCE_NODE_RENDER_ID, NODE_WIDTH, NODE_HEIGHT } from './constant';
import { uuidv4, NsGraph, NsGraphStatusCommand } from '@antv/xflow';
import type { NsRenameNodeCmd } from './CmdExtensions/CmdRenameNodeModal';
import type { NsNodeCmd, NsEdgeCmd, NsGraphCmd } from '@antv/xflow';
import type { NsDeployDagCmd } from './CmdExtensions/CmdDeploy';
import { getRelationConfigInfo, addClassInfoAsDataSourceParents } from './utils';
import { cloneDeep } from 'lodash';
import type { IDataSource } from '../data';
import {
getDatasourceList,
deleteDatasource,
getDimensionList,
createOrUpdateViewInfo,
getViewInfoList,
deleteDatasourceRela,
} from '../service';
import { message } from 'antd';
/** mock 后端接口调用 */
export namespace GraphApi {
export const NODE_COMMON_PROPS = {
renderKey: DATASOURCE_NODE_RENDER_ID,
width: NODE_WIDTH,
height: NODE_HEIGHT,
} as const;
/** 查图的meta元信息 */
export const queryGraphMeta: NsGraphCmd.GraphMeta.IArgs['graphMetaService'] = async (args) => {
return { ...args, flowId: args.meta.flowId };
};
export const createPorts = (nodeId: string, count = 1, layout = 'LR') => {
const ports = [] as NsGraph.INodeAnchor[];
Array(count)
.fill(1)
.forEach((item, idx) => {
const portIdx = idx + 1;
ports.push(
...[
{
id: `${nodeId}-input-${portIdx}`,
type: NsGraph.AnchorType.INPUT,
group: layout === 'TB' ? NsGraph.AnchorGroup.TOP : NsGraph.AnchorGroup.LEFT,
tooltip: `输入桩-${portIdx}`,
},
{
id: `${nodeId}-output-${portIdx}`,
type: NsGraph.AnchorType.OUTPUT,
group: layout === 'TB' ? NsGraph.AnchorGroup.BOTTOM : NsGraph.AnchorGroup.RIGHT,
tooltip: `输出桩-${portIdx}`,
},
],
);
});
return ports;
};
export const createDataSourceNode = (dataSourceItem: IDataSource.IDataSourceItem) => {
const { id, name } = dataSourceItem;
const nodeId = `dataSource-${id}`;
return {
...NODE_COMMON_PROPS,
id: nodeId,
label: `${name}-${id}`,
ports: createPorts(nodeId),
payload: dataSourceItem,
};
};
/** 删除节点的api */
export const delDataSource = async (nodeConfig: any) => {
const dataSourceId = nodeConfig.targetData?.payload?.id;
if (!dataSourceId) {
// dataSourceId 不存在时为未保存节点直接返回true删除
return true;
}
const { code, msg } = await deleteDatasource(dataSourceId);
if (code === 200) {
return true;
}
message.error(msg);
return false;
};
export const loadDataSourceData = async (args: NsGraph.IGraphMeta) => {
const { domainManger, graphConfig } = args.meta;
const { selectDomainId } = domainManger;
const { code, data = [] } = await getDatasourceList({ domainId: selectDomainId });
const dataSourceMap = data.reduce(
(itemMap: Record<string, IDataSource.IDataSourceItem>, item: IDataSource.IDataSourceItem) => {
const { id, name } = item;
itemMap[`dataSource-${id}`] = item;
itemMap[name] = item;
return itemMap;
},
{},
);
if (code === 200) {
// 如果config存在将数据源信息进行merge
if (graphConfig?.id && graphConfig?.config) {
const { config } = graphConfig;
const { nodes, edges } = config;
const nodesMap = nodes.reduce(
(itemMap: Record<string, NsGraph.INodeConfig>, item: NsGraph.INodeConfig) => {
itemMap[item.id] = item;
return itemMap;
},
{},
);
let mergeNodes = nodes;
let mergeEdges = edges;
if (Array.isArray(nodes)) {
mergeNodes = data.reduce(
(mergeNodeList: NsGraph.INodeConfig[], item: IDataSource.IDataSourceItem) => {
const { id } = item;
const targetDataSourceItem = nodesMap[`dataSource-${id}`];
if (targetDataSourceItem) {
mergeNodeList.push({
...targetDataSourceItem,
payload: item,
});
} else {
mergeNodeList.push(createDataSourceNode(item));
}
return mergeNodeList;
},
[],
);
}
if (Array.isArray(edges)) {
mergeEdges = edges.reduce(
(mergeEdgeList: NsGraph.IEdgeConfig[], item: NsGraph.IEdgeConfig) => {
const { source, target } = item;
const sourceDataSourceItem = dataSourceMap[source];
const targetDataSourceItem = dataSourceMap[target];
if (sourceDataSourceItem && targetDataSourceItem) {
const tempItem = { ...item };
tempItem.sourceNodeData.payload = sourceDataSourceItem;
tempItem.targetNodeData.payload = targetDataSourceItem;
mergeEdgeList.push(tempItem);
}
return mergeEdgeList;
},
[],
);
}
return { nodes: mergeNodes, edges: mergeEdges };
}
// 如果config不存在进行初始化
const nodes: NsGraph.INodeConfig[] = data.map((item: IDataSource.IDataSourceItem) => {
return createDataSourceNode(item);
});
return addClassInfoAsDataSourceParents({ nodes, edges: [] }, domainManger);
}
return {};
};
export const loadDimensionData = async (args: NsGraph.IGraphMeta) => {
const { domainManger } = args.meta;
const { domainId } = domainManger;
const { code, data } = await getDimensionList({ domainId });
if (code === 200) {
const { list } = data;
const nodes: NsGraph.INodeConfig[] = list.map((item: any) => {
const { id, name } = item;
const nodeId = `dimension-${id}`;
return {
...NODE_COMMON_PROPS,
id: nodeId,
label: `${name}-${id}`,
ports: createPorts(nodeId),
payload: item,
};
});
return { nodes, edges: [] };
}
return {};
};
/** 保存图数据的api */
export const saveGraphData: NsGraphCmd.SaveGraphData.IArgs['saveGraphDataService'] = async (
graphMeta: NsGraph.IGraphMeta,
graphData: NsGraph.IGraphData,
) => {
const { commandService } = graphMeta;
const initGraphCmdsState = commandService.getGlobal('initGraphCmdsSuccess');
// 如果graph处于初始化阶段则禁止配置文件保存操作
if (!initGraphCmdsState) {
return;
}
const tempGraphData = cloneDeep(graphData);
const { edges, nodes } = tempGraphData;
if (Array.isArray(nodes)) {
tempGraphData.nodes = nodes.map((item: any) => {
delete item.payload;
return item;
});
}
if (Array.isArray(edges)) {
tempGraphData.edges = edges.map((item: any) => {
delete item.sourceNodeData.payload;
delete item.targetNodeData.payload;
return item;
});
}
const { domainManger, graphConfig } = graphMeta.meta;
const { code, msg } = await createOrUpdateViewInfo({
id: graphConfig?.id,
domainId: domainManger.selectDomainId,
type: 'datasource',
config: JSON.stringify(tempGraphData),
});
if (code !== 200) {
message.error(msg);
}
return {
success: true,
data: graphData,
};
};
/** 部署图数据的api */
export const deployDagService: NsDeployDagCmd.IDeployDagService = async (
meta: NsGraph.IGraphMeta,
graphData: NsGraph.IGraphData,
) => {
return {
success: true,
data: graphData,
};
};
/** 添加节点api */
export const addNode: NsNodeCmd.AddNode.IArgs['createNodeService'] = async (
args: NsNodeCmd.AddNode.IArgs,
) => {
console.info('addNode service running, add node:', args);
const { id, ports = createPorts(id, 1), groupChildren } = args.nodeConfig;
const nodeId = id || uuidv4();
/** 这里添加连线桩 */
const node: NsNodeCmd.AddNode.IArgs['nodeConfig'] = {
...NODE_COMMON_PROPS,
...args.nodeConfig,
id: nodeId,
ports: ports,
};
/** group没有链接桩 */
if (groupChildren && groupChildren.length) {
node.ports = [];
}
return node;
};
/** 更新节点 name可能依赖接口判断是否重名返回空字符串时不更新 */
export const renameNode: NsRenameNodeCmd.IUpdateNodeNameService = async (
name,
node,
graphMeta,
) => {
return { err: null, nodeName: name };
};
/** 删除节点的api */
export const delNode: NsNodeCmd.DelNode.IArgs['deleteNodeService'] = async (args: any) => {
const { type } = args.nodeConfig;
switch (type) {
case 'dataSource':
return await delDataSource(args.nodeConfig);
case 'class':
return true;
default:
return true;
}
};
/** 添加边的api */
export const addEdge: NsEdgeCmd.AddEdge.IArgs['createEdgeService'] = async (args) => {
console.info('addEdge service running, add edge:', args);
const { edgeConfig } = args;
return {
...edgeConfig,
id: uuidv4(),
};
};
/** 删除边的api */
export const delEdge: NsEdgeCmd.DelEdge.IArgs['deleteEdgeService'] = async (args) => {
console.info('delEdge service running, del edge:', args);
const { commandService, edgeConfig } = args;
if (!edgeConfig?.sourceNodeData || !edgeConfig?.targetNodeData) {
return true;
}
const { sourceNodeData, targetNodeData } = edgeConfig as any;
const sourceDataId = sourceNodeData.payload.id;
const targetDataId = targetNodeData.payload.id;
const { getGlobal } = commandService as any;
const dataSourceRelationList = getGlobal('dataSourceRelationList');
const relationConfig = getRelationConfigInfo(
sourceDataId,
targetDataId,
dataSourceRelationList,
);
if (!relationConfig) {
// 如果配置不存在则直接删除
return true;
}
const { code, msg } = await deleteDatasourceRela(relationConfig.id);
if (code === 200) {
return true;
}
message.error(msg);
return false;
};
let runningNodeId = 0;
const statusMap = {} as NsGraphStatusCommand.IStatusInfo['statusMap'];
let graphStatus: NsGraphStatusCommand.StatusEnum = NsGraphStatusCommand.StatusEnum.DEFAULT;
export const graphStatusService: NsGraphStatusCommand.IArgs['graphStatusService'] = async () => {
if (runningNodeId < 4) {
statusMap[`node${runningNodeId}`] = { status: NsGraphStatusCommand.StatusEnum.SUCCESS };
statusMap[`node${runningNodeId + 1}`] = {
status: NsGraphStatusCommand.StatusEnum.PROCESSING,
};
runningNodeId += 1;
graphStatus = NsGraphStatusCommand.StatusEnum.PROCESSING;
} else {
runningNodeId = 0;
statusMap.node4 = { status: NsGraphStatusCommand.StatusEnum.SUCCESS };
graphStatus = NsGraphStatusCommand.StatusEnum.SUCCESS;
}
return {
graphStatus: graphStatus,
statusMap: statusMap,
};
};
export const stopGraphStatusService: NsGraphStatusCommand.IArgs['graphStatusService'] =
async () => {
Object.entries(statusMap).forEach(([, val]) => {
const { status } = val as { status: NsGraphStatusCommand.StatusEnum };
if (status === NsGraphStatusCommand.StatusEnum.PROCESSING) {
val.status = NsGraphStatusCommand.StatusEnum.ERROR;
}
});
return {
graphStatus: NsGraphStatusCommand.StatusEnum.ERROR,
statusMap: statusMap,
};
};
}

View File

@@ -0,0 +1,137 @@
import type { NsGraph } from '@antv/xflow';
import { uuidv4 } from '@antv/xflow';
import type { StateType } from '../model';
import { GraphApi } from './service';
import { NODE_WIDTH, NODE_HEIGHT } from './constant';
import moment from 'moment';
import { jsonParse } from '@/utils/utils';
import type { GraphConfigListItem, RelationListItem } from './data';
export const getEdgesNodesIds = (edges: NsGraph.IEdgeConfig[], type?: 'source' | 'target') => {
const hasEdgesNodesIds = edges.reduce((nodesList: string[], item: NsGraph.IEdgeConfig) => {
const { source, target } = item;
if (!type) {
nodesList.push(source, target);
} else if (type === 'source') {
nodesList.push(source);
} else if (type === 'target') {
nodesList.push(target);
}
return nodesList;
}, []);
const uniqueHasEdgesNodesIds = Array.from(new Set(hasEdgesNodesIds));
return uniqueHasEdgesNodesIds;
};
export const computedSingerNodesEdgesPosition = ({ nodes, edges }: NsGraph.IGraphData) => {
const hasEdgesNodesIds = getEdgesNodesIds(edges);
const defaultXPostion = 100;
const defaultYPostion = 100;
const paddingSize = 50;
let xPosistion = defaultXPostion;
const yPostition = defaultYPostion;
const positionNodes = nodes.reduce(
(nodesList: NsGraph.INodeConfig[], item: NsGraph.INodeConfig, index: number) => {
const { id, width, height = NODE_HEIGHT } = item;
if (!hasEdgesNodesIds.includes(id)) {
xPosistion = xPosistion + (width || NODE_WIDTH + paddingSize) * index;
}
nodesList.push({
...item,
x: xPosistion,
y: height > yPostition ? height + paddingSize : yPostition,
});
return nodesList;
},
[],
);
return { nodes: positionNodes, edges };
};
export const addClassInfoAsDataSourceParents = (
{ nodes = [], edges = [] }: NsGraph.IGraphData,
domainManger: StateType,
) => {
const { selectDomainId, selectDomainName } = domainManger;
const sourceId = `classNodeId-${selectDomainId}`;
const classNode = {
...GraphApi.NODE_COMMON_PROPS,
id: sourceId,
label: selectDomainName,
ports: GraphApi.createPorts(sourceId),
};
const classEdges = nodes.reduce((edgesList: NsGraph.IEdgeConfig[], item: NsGraph.INodeConfig) => {
const { id } = item;
const sourcePortId = `${sourceId}-output-1`;
const edge = {
id: uuidv4(),
source: sourceId,
target: id,
sourcePortId,
targetPortId: `${id}-input-1`,
};
edgesList.push(edge);
return edgesList;
}, []);
const graphData = {
nodes: [classNode, ...nodes],
edges: [...edges, ...classEdges],
};
return graphData;
};
export const addDataSourceInfoAsDimensionParents = (
{ nodes = [], edges = [] }: NsGraph.IGraphData,
targetDataSource: NsGraph.INodeConfig,
) => {
const { id: sourceId } = targetDataSource;
const dimensionEdges = nodes.reduce(
(edgesList: NsGraph.IEdgeConfig[], item: NsGraph.INodeConfig) => {
const { id } = item;
const sourcePortId = `${sourceId}-output-1`;
const edge = {
id: uuidv4(),
source: sourceId,
target: id,
sourcePortId,
targetPortId: `${id}-input-1`,
};
edgesList.push(edge);
return edgesList;
},
[],
);
const graphData = {
nodes: [targetDataSource, ...nodes],
edges: [...edges, ...dimensionEdges],
};
return graphData;
};
export const getGraphConfigFromList = (configList: GraphConfigListItem[]) => {
configList.sort((a, b) => moment(b.updatedAt).valueOf() - moment(a.updatedAt).valueOf());
const targetConfig = configList[0];
if (targetConfig) {
const { config, id } = targetConfig;
return {
config: jsonParse(config),
id,
};
}
return;
};
export const getRelationConfigInfo = (
fromDataSourceId: number,
toDataSourceId: number,
relationList: RelationListItem[],
) => {
const relationConfig = relationList.filter((item: RelationListItem) => {
const { datasourceFrom, datasourceTo } = item;
return fromDataSourceId === datasourceFrom && toDataSourceId === datasourceTo;
})[0];
return relationConfig;
};

View File

@@ -0,0 +1,82 @@
import G6 from '@antv/g6';
// import { modifyCSS, createDom } from '@antv/dom-util';
import { createDom } from '@antv/dom-util';
const initToolBar = () => {
// const defaultConfig = G6.ToolBar
const toolBarInstance = new G6.ToolBar();
const config = toolBarInstance._cfgs;
const defaultContentDomString = config.getContent();
// const regex = /<ul[^>]*>|<\/ul>/g;
// const innerDom = defaultContentDom.replace(regex, '');
const defaultContentDom = createDom(defaultContentDomString);
// @ts-ignore
const elements = defaultContentDom.querySelectorAll('li[code="redo"], li[code="undo"]');
elements.forEach((element) => {
element.remove();
});
const searchBtnDom = `<li code="search">
<svg
viewBox="64 64 896 896"
focusable="false"
data-icon="search"
width="24"
height="24"
fill="currentColor"
aria-hidden="true"
>
<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" />
</svg>
</li>`;
const toolbar = new G6.ToolBar({
position: { x: 10, y: 10 },
getContent: () => {
return `${searchBtnDom}${defaultContentDom}`;
},
});
// const toolbar = new G6.ToolBar({
// getContent: (graph) => {
// const searchInput = document.createElement('input');
// searchInput.id = 'search-input';
// searchInput.placeholder = '搜索节点';
// const searchBtn = document.createElement('button');
// searchBtn.id = 'search-btn';
// searchBtn.innerHTML = '搜索';
// const container = document.createElement('div');
// container.appendChild(searchInput);
// container.appendChild(searchBtn);
// return container;
// },
// handleClick: (name, graph) => {
// if (name === 'search-btn') {
// const searchText = document.getElementById('search-input').value.trim();
// if (!searchText) {
// return;
// }
// const foundNode = graph.getNodes().find((node) => {
// const model = node.getModel();
// return model.label === searchText;
// });
// if (foundNode) {
// // 如果找到了节点,将其设置为选中状态
// graph.setItemState(foundNode, 'active', true);
// // 将视图移动到找到的节点位置
// graph.focusItem(foundNode, true, {
// duration: 300,
// easing: 'easeCubic',
// });
// } else {
// alert('未找到节点');
// }
// }
// },
// });
return toolbar;
};
export default initToolBar;

View File

@@ -0,0 +1,344 @@
import React, { useEffect, useState, useRef } from 'react';
import { connect } from 'umi';
import type { StateType } from '../model';
import type { Dispatch } from 'umi';
import { typeConfigs } from './utils';
import { message } from 'antd';
import { getDatasourceList, getDomainSchemaRela } from '../service';
import initToolBar from './components/ToolBar';
import G6 from '@antv/g6';
type Props = {
domainId: number;
domainManger: StateType;
dispatch: Dispatch;
};
const DomainManger: React.FC<Props> = ({ domainManger, domainId }) => {
const ref = useRef(null);
const [graphData, setGraphData] = useState<any>({});
const legendDataRef = useRef<any[]>([]);
const graphRef = useRef<any>(null);
const legendDataFilterFunctions = useRef<any>({});
const { dimensionList } = domainManger;
const toggleNodeVisibility = (graph, node, visible) => {
if (visible) {
graph.showItem(node);
} else {
graph.hideItem(node);
}
};
const toggleChildrenVisibility = (graph, node, visible) => {
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 formatterRelationData = (dataSourceList: any[]) => {
const relationData = dataSourceList.reduce((relationList: any[], item: any) => {
const { id, name } = item;
const dataSourceId = `dataSource-${id}`;
const dimensionChildrenList = dimensionList.reduce(
(dimensionChildren: any[], dimension: any) => {
const { id: dimensionId, name: dimensionName, datasourceId } = dimension;
if (datasourceId === id) {
dimensionChildren.push({
nodeType: 'dimension',
legendType: dataSourceId,
id: `dimension-${dimensionId}`,
name: dimensionName,
style: {
lineWidth: 2,
fill: '#f0f7ff',
stroke: '#a6ccff',
},
});
}
return dimensionChildren;
},
[],
);
relationList.push({
name,
legendType: dataSourceId,
id: dataSourceId,
nodeType: 'datasource',
size: 40,
children: [...dimensionChildrenList],
style: {
lineWidth: 2,
fill: '#BDEFDB',
stroke: '#5AD8A6',
},
});
return relationList;
}, []);
return relationData;
};
const queryDataSourceList = async (params: any) => {
getDomainSchemaRela(params.domainId);
const { code, data, msg } = await getDatasourceList({ ...params });
if (code === 200) {
const relationData = formatterRelationData(data);
const legendList = relationData.map((item: any) => {
const { id, name } = item;
return {
id,
label: name,
order: 4,
...typeConfigs.datasource,
};
});
legendDataRef.current = legendList;
setGraphData({
id: 'root',
name: domainManger.selectDomainName,
children: relationData,
});
} else {
message.error(msg);
}
};
useEffect(() => {
queryDataSourceList({ domainId });
}, []);
const getLegendDataFilterFunctions = () => {
legendDataRef.current.map((item: any) => {
const { id } = item;
legendDataFilterFunctions.current = {
...legendDataFilterFunctions.current,
[id]: (d) => {
if (d.legendType === id) {
return true;
}
return false;
},
};
});
};
const setAllActiveLegend = (legend: any) => {
const legendCanvas = legend._cfgs.legendCanvas;
// 从图例中找出node-group节点;
const group = legendCanvas.find((e: any) => e.get('name') === 'node-group');
// 数据源的图例节点在node-group中的children中
const groups = group.get('children');
groups.forEach((itemGroup: any) => {
const labelText = itemGroup.find((e: any) => e.get('name') === 'circle-node-text');
// legend中activateLegend事件触发在图例节点的Text上方法中存在向上溯源的逻辑const shapeGroup = shape.get('parent');
// 因此复用实例方法时在这里不能直接将图例节点传入需要在节点的children中找任意一个元素作为入参
legend.activateLegend(labelText);
});
};
// const [visible, setVisible] = useState(false);
useEffect(() => {
if (!(Array.isArray(graphData.children) && graphData.children.length > 0)) {
return;
}
const container = document.getElementById('semanticGraph');
const width = container!.scrollWidth;
const height = container!.scrollHeight || 500;
if (!graphRef.current) {
getLegendDataFilterFunctions();
const toolbar = initToolBar();
// const toolbar = new G6.ToolBar({
// getContent: (graph) => {
// const searchIcon = document.createElement('i');
// searchIcon.className = 'g6-toolbar-search-icon';
// searchIcon.style.cssText = `
// display: inline-block;
// width: 16px;
// height: 16px;
// background-image: url(https://gw.alipayobjects.com/zos/rmsportal/wzQIcOMRTkQwFgaaDIFs.svg);
// background-size: 16px 16px;
// margin-right: 8px;
// cursor: pointer;
// `;
// searchIcon.addEventListener('click', () => {
// setVisible((prevVisible) => !prevVisible);
// });
// const ul = document.createElement('ul');
// ul.className = 'g6-component-toolbar';
// ul.appendChild(searchIcon);
// return ul;
// },
// });
const tooltip = new G6.Tooltip({
offsetX: 10,
offsetY: 10,
fixToNode: [1, 0.5],
// the types of items that allow the tooltip show up
// 允许出现 tooltip 的 item 类型
// itemTypes: ['node', 'edge'],
itemTypes: ['node'],
// custom the tooltip's content
// 自定义 tooltip 内容
getContent: (e) => {
const outDiv = document.createElement('div');
outDiv.style.width = 'fit-content';
outDiv.style.height = 'fit-content';
const model = e.item.getModel();
if (e.item.getType() === 'node') {
outDiv.innerHTML = `${model.name}`;
}
// else {
// const source = e.item.getSource();
// const target = e.item.getTarget();
// outDiv.innerHTML = `来源:${source.getModel().name}<br/>去向:${
// target.getModel().name
// }`;
// }
return outDiv;
},
});
const legend = new G6.Legend({
// container: 'legendContainer',
data: {
nodes: legendDataRef.current,
},
align: 'center',
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({
container: 'semanticGraph',
width,
height,
linkCenter: true,
modes: {
default: [
{
type: 'collapse-expand',
onChange: function onChange(item, collapsed) {
const data = item.get('model');
data.collapsed = collapsed;
return true;
},
},
'drag-node',
'drag-canvas',
// 'activate-relations',
'zoom-canvas',
{
type: 'activate-relations',
trigger: 'mouseenter', // 触发方式,可以是 'mouseenter' 或 'click'
resetSelected: true, // 点击空白处时,是否取消高亮
},
],
},
defaultNode: {
size: 26,
labelCfg: {
position: 'bottom',
style: {
stroke: '#fff',
lineWidth: 4,
},
},
},
layout: {
type: 'dendrogram',
direction: 'LR',
nodeSep: 200,
rankSep: 300,
radial: true,
},
plugins: [legend, tooltip, toolbar],
});
const legendCanvas = legend._cfgs.legendCanvas;
// legend模式事件方法bindEvents会有点击图例空白清空选中的逻辑在注册click事件前先将click事件队列清空
legend._cfgs.legendCanvas._events.click = [];
legendCanvas.on('click', (e) => {
const shape = e.target;
const shapeGroup = shape.get('parent');
const shapeGroupId = shapeGroup?.cfg?.id;
if (shapeGroupId) {
const isActive = shapeGroup.get('active');
const targetNode = graph.findById(shapeGroupId);
// const model = targetNode.getModel();
toggleNodeVisibility(graph, targetNode, isActive);
toggleChildrenVisibility(graph, targetNode, isActive);
}
});
const graph = graphRef.current;
graph.node(function (node) {
return {
label: node.name,
labelCfg: { style: { fill: '#3c3c3c' } },
};
});
graph.data(graphData);
graph.render();
graph.fitView();
setAllActiveLegend(legend);
const rootNode = graph.findById('root');
graph.hideItem(rootNode);
if (typeof window !== 'undefined')
window.onresize = () => {
if (!graph || graph.get('destroyed')) return;
if (!container || !container.scrollWidth || !container.scrollHeight) return;
graph.changeSize(container.scrollWidth, container.scrollHeight);
};
}
}, [domainId, graphData]);
return <div ref={ref} id="semanticGraph" style={{ width: '100%', height: '100%' }} />;
};
export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger,
}))(DomainManger);

View File

@@ -0,0 +1,12 @@
import request from 'umi-request';
type ExcuteSqlParams = {
sql: string;
domainId: number;
};
// 执行脚本
export async function excuteSql(params: ExcuteSqlParams) {
const data = { ...params };
return request.post(`${process.env.API_BASE_URL}database/executeSql`, { data });
}

View File

@@ -0,0 +1,759 @@
@borderColor: #eee;
@activeColor: #a0c5e8;
@hoverColor: #dee4e9;
.pageContainer {
position: absolute;
top: 55px;
right: 0;
bottom: 0;
left: 0;
// margin: -24px;
background: #fff;
&.externalPageContainer {
margin: 0 !important;
}
}
.searchBar {
:global {
.ant-form-item-label {
width: 70px;
}
}
}
.main {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
:global {
.ant-tabs {
height: 100% !important;
.ant-tabs-content {
height: 100% !important;
.ant-tabs-tabpane {
height: 100%;
}
}
}
}
}
.rightSide {
position: relative;
z-index: 1;
min-width: 250px;
height: 100%;
margin-left: 4px;
padding: 10px;
overflow: hidden;
:global {
.ant-form-item {
margin-bottom: 6px;
.ant-form-item-label {
width: 70px;
}
.ant-form-item-control {
min-width: 100px;
}
}
}
}
.rightListSide {
position: relative;
z-index: 2;
flex: 1;
height: 100%;
// padding: 10px 10px 0;
background-color: #fff;
// 去掉标签间距
:global {
.ant-tabs-card.ant-tabs-top > .ant-tabs-nav .ant-tabs-tab + .ant-tabs-tab,
.ant-tabs-card.ant-tabs-bottom > .ant-tabs-nav .ant-tabs-tab + .ant-tabs-tab,
.ant-tabs-card.ant-tabs-top > div > .ant-tabs-nav .ant-tabs-tab + .ant-tabs-tab,
.ant-tabs-card.ant-tabs-bottom > div > .ant-tabs-nav .ant-tabs-tab + .ant-tabs-tab {
margin-left: 0;
}
.ant-tabs > .ant-tabs-nav .ant-tabs-nav-add,
.ant-tabs > div > .ant-tabs-nav .ant-tabs-nav-add {
margin-left: 0;
}
}
}
.leftListSide {
position: relative;
z-index: 2;
flex: 1;
height: 100%;
// padding: 10px 10px 0;
background-color: #fff;
}
.tableTotal {
margin: 0 2px;
color: #296df3;
font-weight: bold;
}
.tableDetaildrawer {
:global {
.ant-drawer-header {
padding: 10px 45px 10px 10px;
}
.ant-drawer-close {
padding: 10px;
}
.ant-drawer-body {
padding: 0 10px 10px;
}
.ant-tabs-top > .ant-tabs-nav {
margin-bottom: 8px;
}
}
}
.tableDetailTable {
:global {
.ant-table-cell,
.resultTableRow > td {
padding: 8px;
font-size: 12px;
}
}
}
.sqlEditor {
min-width: 0;
height: 100%;
border: solid 1px @borderColor;
:global {
.ace_editor {
font-family: 'Menlo', 'Monaco', 'Ubuntu Mono', 'Consolas', 'source-code-pro' !important;
}
}
}
.sqlOprBar {
margin-top: -10px;
padding: 5px;
display: flex;
.sqlOprBarLeftBox {
flex: 1 1 200px;
}
.sqlOprBarRightBox {
flex: 0 1 210px;
}
:global {
.ant-btn-round.ant-btn-sm {
font-size: 12px;
}
.ant-btn-primary {
color: #fff;
background: #02a7f0;
border-color: #02a7f0;
}
.ant-segmented-item-selected {
color: #fff;
background: #02a7f0;
border-color: #02a7f0;
}
}
}
.sqlOprIcon {
margin-right: 30px;
color: #02a7f0;
font-size: 22px;
&:hover {
cursor: pointer;
opacity: 0.8;
}
&:active {
opacity: 0.7;
}
}
.sqlOprBtn {
margin-right: 30px;
vertical-align: super !important;
&:hover {
cursor: pointer;
opacity: 0.8;
}
&:active {
opacity: 0.7;
}
}
.sqlOprSwitch {
// vertical-align: super !important;
float: right;
margin-right: 10px !important;
}
:global {
.is-sql-full-select {
background-color: #02a7f0;
}
.cjjWdp:hover {
z-index: 10;
}
}
.sqlMain {
display: flex;
flex-direction: row;
height: 100%;
.sqlEditorWrapper {
flex: 1;
height: 100%;
overflow: hidden;
}
.sqlParams {
width: 20%;
height: 100% !important;
overflow: auto;
}
.hideSqlParams {
width: 0;
height: 100% !important;
overflow: auto;
}
}
.sqlParamsBody {
.header {
display: flex;
padding: 10px;
font-weight: bold;
.title {
flex: 1;
}
.icon {
display: flex;
align-items: center;
margin-right: 10px !important;
cursor: pointer;
}
}
.paramsList {
.paramsItem {
display: flex;
padding: 10px;
:global {
.ant-list-item-action {
margin-left: 5px;
}
}
.name {
flex: 1;
width: 80%;
overflow: hidden;
font-size: 12px;
text-overflow: ellipsis;
&:hover {
cursor: pointer;
}
}
.icon {
// display: none;
margin-left: 10px;
}
}
}
// .paramsItem:hover {
// .icon {
// display: inline-block;
// margin-left: 8px;
// cursor: pointer;
// }
// }
}
.disableIcon {
vertical-align: super !important;
// color: rgba(0, 10, 36, 0.25);
background: #7d7f80 !important;
border-color: #7d7f80 !important;
:global {
.anticon .anticon-play-circle {
color: #fff;
}
}
&:hover {
cursor: not-allowed;
opacity: 1;
}
}
.sqlTaskListWrap {
position: relative;
width: 262px;
border-top: 0 !important;
border-radius: 0;
:global {
.ant-card-head {
min-height: 20px;
}
.ant-card-head-title {
padding: 8px 0;
}
}
}
.sqlTaskList {
position: absolute !important;
top: 42px;
right: 0;
bottom: 0;
left: 0;
overflow: auto;
}
.sqlBottmWrap {
// position: absolute;
// top: 484px;
// right: 0;
// bottom: 0;
// left: 0;
display: flex;
height: 100%;
// padding: 0 10px;
&:global(.small) {
top: 334px;
}
&:global(.middle) {
top: 384px;
}
}
.sqlResultWrap {
position: relative;
display: flex;
flex: 1;
flex-direction: column;
overflow: auto;
border: solid 1px @borderColor;
border-top: 0;
border-left: 0;
}
.sqlToolBar {
display: flex;
flex-direction: row-reverse;
align-items: center;
height: 41px;
padding: 5px 0;
text-align: right;
}
.sqlResultPane {
flex: 1;
border-top: solid 1px @borderColor;
}
.sqlToolBtn {
margin-right: 15px;
}
.runScriptBtn {
margin-right: 15px;
background-color: #e87954;
border-color: #e87954;
&:hover{
border-color: #f89878;
background: #f89878;
}
&:focus {
border-color: #f89878;
background: #f89878;
}
}
.taskFailed {
padding: 20px 20px 0 20px;
}
.sqlResultContent {
position: absolute;
top: 50%;
width: 100%;
color: rgba(0, 0, 0, 0.25);
font-size: 16px;
text-align: center;
}
.sqlResultLog {
padding: 20px;
word-wrap: break-word;
}
.tableList {
position: absolute !important;
top: 160px;
right: 0;
bottom: 26px;
left: 0;
overflow-x: hidden;
overflow-y: auto;
border-bottom: solid 1px @borderColor;
}
.tablePage {
position: absolute !important;
bottom: 0;
left: 0;
z-index: 1;
width: 100%;
min-width: 250px;
overflow: hidden;
}
.tableListItem {
width: 88%;
overflow: hidden;
font-size: 12px;
text-overflow: ellipsis;
&:hover {
cursor: pointer;
}
}
.tableItem {
&:global(.ant-list-item) {
padding: 6px 0 6px 6px;
}
:global(.ant-list-item-action) {
margin-left: 12px !important;
}
&:hover {
background: @hoverColor;
border-bottom: 1px solid #f0f0f0;
}
&:global(.active) {
background: @activeColor;
}
}
.taskIcon {
margin-right: 10px;
color: #1890ff;
font-size: 14px;
}
.taskSuccessIcon {
.taskIcon();
color: #67c23a;
}
.taskFailIcon {
.taskIcon();
color: #f56c6c;
}
.resultFailIcon {
margin-right: 8px;
color: #f56c6c;
}
.taskItem {
padding: 10px 8px !important;
font-size: 12px;
cursor: pointer;
&:global(.ant-list-item) {
justify-content: flex-start;
}
&:hover {
background: @hoverColor;
}
}
.activeTask {
background: @activeColor;
}
.resultTable {
width: 100%;
:global {
.ant-table-body {
width: 100%;
// max-height: none !important;
overflow: auto !important;
}
.ant-table-cell,
.resultTableRow > td {
padding: 8px;
font-size: 12px;
}
}
}
.taskLogWrap {
word-wrap: break-word;
}
.siteTagPlus {
background: #fff;
border-style: dashed;
}
.editTag {
margin-bottom: 5px;
user-select: none;
}
.tagInput {
width: 78px;
margin-right: 8px;
vertical-align: top;
}
.outside {
position: relative;
height: 100%;
}
.collapseRightBtn {
position: absolute;
top: calc(50% + 50px);
right: 0;
z-index: 100;
display: flex;
align-items: center;
height: 70px;
color: #fff;
font-size: 12px;
background-color: rgba(40, 46, 54, 0.2);
border-radius: 24px 0 0 24px;
cursor: pointer;
transition: all 0.3s ease;
}
.collapseLeftBtn {
position: absolute;
top: calc(50% + 45px);
left: 0;
z-index: 100;
display: flex;
align-items: center;
height: 70px;
color: #fff;
font-size: 12px;
background-color: rgba(40, 46, 54, 0.2);
border-radius: 0 24px 24px 0;
cursor: pointer;
transition: all 0.3s ease;
}
.detail {
.titleCollapse {
float: right;
padding-right: 18px;
color: #1890ff;
line-height: 35px;
text-align: right;
cursor: pointer;
}
.tableTitle {
display: inline-block;
width: 85%;
margin-left: 15px;
overflow: hidden;
line-height: 35px;
white-space: nowrap;
text-overflow: ellipsis;
cursor: pointer;
}
:global {
.ant-divider-horizontal {
margin: 0;
}
}
}
.search {
margin-left: 10px;
}
.middleArea {
:global {
.ant-tabs-nav .ant-tabs-tab {
border: none;
// background: #d9d9d96e;
border-right: 1px solid #f0f0f0;
border-radius: 0 !important;
}
.ant-tabs-nav-add {
border-radius: 0 !important;
}
.ant-tabs-tab {
.ant-tabs-tab-remove {
.closeTab {
opacity: 0;
}
.dot {
opacity: 1;
}
}
}
.ant-tabs-tab:hover {
.ant-tabs-tab-remove {
.closeTab {
opacity: 1 !important;
}
.dot {
opacity: 0;
}
}
}
}
}
.menu {
position: relative;
z-index: 1;
height: 100%;
padding: 5px;
overflow: hidden;
overflow-x: hidden;
overflow-y: auto;
:global {
.ant-form {
margin: -2px;
}
}
}
.menuList {
position: absolute !important;
top: 95px;
right: 0;
bottom: 26px;
left: 0;
overflow-x: hidden;
overflow-y: auto;
border-bottom: solid 1px @borderColor;
.menuItem {
&:global(.ant-list-item) {
padding: 6px 0 6px 14px;
}
:global(.ant-list-item-action) {
margin-left: 12px !important;
}
&:hover {
background: @hoverColor;
border-bottom: 1px solid #f0f0f0;
.icon {
display: block;
}
}
&:global(.active) {
background: @activeColor;
}
.menuListItem {
width: 90%;
overflow: hidden;
font-size: 12px;
white-space: nowrap;
text-overflow: ellipsis;
&:hover {
cursor: pointer;
}
}
.icon {
display: none;
margin-right: 15px !important;
cursor: pointer;
}
.menuIcon {
display: flex;
}
}
}
.scriptFile {
width: 100%;
margin: 10px;
overflow: hidden;
font-size: 14px;
white-space: nowrap;
text-overflow: ellipsis;
.icon {
margin-right: 10px;
}
}
.sqlScriptName {
width: 93% !important;
margin: 14px 0 0 14px !important;
}
.fileIcon {
width: 20px !important;
height: 20px !important;
padding-top: 2px !important;
padding-right: 5px !important;
vertical-align: middle;
}
.itemName {
vertical-align: middle;
}
.paneName {
width: 100px;
overflow: hidden;
font-size: 12px !important;
white-space: nowrap;
text-overflow: ellipsis;
}
.titleIcon {
width: 16px !important;
height: 16px !important;
margin: 0 3px 4px;
}

View File

@@ -0,0 +1,80 @@
export const typeConfigs = {
datasource: {
type: 'circle',
size: 5,
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: [
{
id: 'type1',
label: 'node-type1',
order: 4,
...typeConfigs.datasource,
},
{
id: 'type2',
label: 'node-type2',
order: 0,
...typeConfigs.dimension,
},
{
id: 'type3',
label: 'node-type3',
order: 2,
...typeConfigs.metric,
},
],
// edges: [
// {
// id: 'eType1',
// label: 'edge-type1',
// order: 2,
// ...typeConfigs.eType1,
// },
// {
// id: 'eType2',
// label: 'edge-type2',
// ...typeConfigs.eType2,
// },
// {
// id: 'eType3',
// label: 'edge-type3',
// ...typeConfigs.eType3,
// },
// ],
};

View File

@@ -0,0 +1,123 @@
import React, { useEffect, useRef, useState } from 'react';
import { Button, Modal } from 'antd';
import type { IDataSource } from '../data';
import ProTable from '@ant-design/pro-table';
import type { ActionType, ProColumns } from '@ant-design/pro-table';
import { connect } from 'umi';
import type { Dispatch } from 'umi';
import type { StateType } from '../model';
export type CreateFormProps = {
measuresList: any[];
selectedMeasuresList: any[];
onCancel: () => void;
onSubmit: (selectMeasuresList: any[]) => void;
createModalVisible: boolean;
projectManger: StateType;
dispatch: Dispatch;
};
const BindMeasuresTable: React.FC<CreateFormProps> = ({
measuresList,
selectedMeasuresList = [],
onSubmit,
onCancel,
createModalVisible,
projectManger,
}) => {
const { searchParams = {} } = projectManger || {};
const actionRef = useRef<ActionType>();
const [selectedMeasuresKeys, setSelectedMeasuresKeys] = useState<string[]>(() => {
return selectedMeasuresList.map((item: any) => {
return item.bizName;
});
});
const [selectMeasuresList, setSelectMeasuresList] = useState<IDataSource.IMeasuresItem[]>([]);
const handleSubmit = async () => {
onSubmit?.(selectMeasuresList);
};
const findMeasureItemByName = (bizName: string) => {
return measuresList.find((item) => {
return item.bizName === bizName;
});
};
useEffect(() => {
const selectedMeasures: IDataSource.IMeasuresItem[] = selectedMeasuresKeys.map((bizName) => {
const item = findMeasureItemByName(bizName);
return item;
});
setSelectMeasuresList([...selectedMeasures]);
}, [selectedMeasuresKeys]);
useEffect(() => {}, []);
const columns: ProColumns[] = [
{
dataIndex: 'bizName',
title: '度量名称',
},
{
dataIndex: 'alias',
title: '别名',
},
{
dataIndex: 'agg',
title: '算子类型',
},
{
dataIndex: 'datasourceName',
title: '所属数据源',
},
];
const renderFooter = () => {
return (
<>
<Button onClick={onCancel}></Button>
<Button type="primary" onClick={handleSubmit}>
</Button>
</>
);
};
const rowSelection = {
selectedRowKeys: selectedMeasuresKeys,
onChange: (_selectedRowKeys: any[]) => {
setSelectedMeasuresKeys([..._selectedRowKeys]);
},
};
return (
<Modal
width={800}
destroyOnClose
title="度量添加"
open={createModalVisible}
footer={renderFooter()}
onCancel={() => {
onCancel();
}}
>
<ProTable
actionRef={actionRef}
rowKey="bizName"
rowSelection={rowSelection}
columns={columns}
params={{ ...searchParams }}
pagination={false}
dataSource={measuresList}
size="small"
search={false}
options={false}
/>
</Modal>
);
};
export default connect(({ projectManger }: { projectManger: StateType }) => ({
projectManger,
}))(BindMeasuresTable);

View File

@@ -0,0 +1,178 @@
import type { ActionType, ProColumns } from '@ant-design/pro-table';
import ProTable from '@ant-design/pro-table';
import { message, Button, Drawer, Space, Popconfirm } from 'antd';
import React, { useRef, useState } from 'react';
import type { Dispatch } from 'umi';
import { connect } from 'umi';
import type { StateType } from '../model';
import { getDatasourceList, deleteDatasource } from '../service';
import DataSource from '../Datasource';
import moment from 'moment';
type Props = {
dispatch: Dispatch;
domainManger: StateType;
};
const ClassDataSourceTable: React.FC<Props> = ({ dispatch, domainManger }) => {
const { selectDomainId } = domainManger;
const [createModalVisible, setCreateModalVisible] = useState<boolean>(false);
const [dataSourceItem, setDataSourceItem] = useState<any>();
const actionRef = useRef<ActionType>();
const columns: ProColumns[] = [
{
dataIndex: 'id',
title: 'ID',
},
{
dataIndex: 'name',
title: '数据源名称',
},
{
dataIndex: 'bizName',
title: '英文名称',
},
{
dataIndex: 'createdBy',
title: '创建人',
},
{
dataIndex: 'description',
title: '描述',
search: false,
},
{
dataIndex: 'updatedAt',
title: '更新时间',
search: false,
render: (value: any) => {
return value && value !== '-' ? moment(value).format('YYYY-MM-DD HH:mm:ss') : '-';
},
},
{
title: '操作',
dataIndex: 'x',
valueType: 'option',
render: (_, record) => {
return (
<Space>
<a
key="classEditBtn"
onClick={() => {
setDataSourceItem(record);
setCreateModalVisible(true);
}}
>
</a>
<Popconfirm
title="确认删除?"
okText="是"
cancelText="否"
onConfirm={async () => {
const { code } = await deleteDatasource(record.id);
if (code === 200) {
setDataSourceItem(undefined);
actionRef.current?.reload();
} else {
message.error('删除失败');
}
}}
>
<a
key="classEditBtn"
onClick={() => {
setDataSourceItem(record);
}}
>
</a>
</Popconfirm>
</Space>
);
},
},
];
const queryDataSourceList = async (params: any) => {
dispatch({
type: 'domainManger/setPagination',
payload: {
...params,
},
});
const { code, data, msg } = await getDatasourceList({ ...params });
let resData: any = {};
if (code === 200) {
resData = {
data: data || [],
success: true,
};
} else {
message.error(msg);
resData = {
data: [],
total: 0,
success: false,
};
}
return resData;
};
return (
<>
<ProTable
actionRef={actionRef}
headerTitle="数据源列表"
rowKey="id"
columns={columns}
params={{ domainId: selectDomainId }}
request={queryDataSourceList}
pagination={false}
search={false}
size="small"
options={{ reload: false, density: false, fullScreen: false }}
toolBarRender={() => [
<Button
key="create"
type="primary"
onClick={() => {
setDataSourceItem(undefined);
setCreateModalVisible(true);
}}
>
</Button>,
]}
/>
{createModalVisible && (
<Drawer
width={'100%'}
destroyOnClose
title="数据源编辑"
open={true}
onClose={() => {
setCreateModalVisible(false);
setDataSourceItem(undefined);
}}
footer={null}
>
<DataSource
initialValues={dataSourceItem}
domainId={Number(selectDomainId)}
onSubmitSuccess={() => {
setCreateModalVisible(false);
setDataSourceItem(undefined);
actionRef.current?.reload();
}}
/>
</Drawer>
)}
</>
);
};
export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger,
}))(ClassDataSourceTable);

View File

@@ -0,0 +1,263 @@
import type { ActionType, ProColumns } from '@ant-design/pro-table';
import ProTable from '@ant-design/pro-table';
import { message, Button, Space, Popconfirm } from 'antd';
import React, { useRef, useState, useEffect } from 'react';
import type { Dispatch } from 'umi';
import { connect } from 'umi';
import type { StateType } from '../model';
import { SENSITIVE_LEVEL_ENUM } from '../constant';
import {
getDatasourceList,
getDimensionList,
createDimension,
updateDimension,
deleteDimension,
} from '../service';
import DimensionInfoModal from './DimensionInfoModal';
import moment from 'moment';
import styles from './style.less';
type Props = {
dispatch: Dispatch;
domainManger: StateType;
};
const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
const { selectDomainId } = domainManger;
const [createModalVisible, setCreateModalVisible] = useState<boolean>(false);
const [dimensionItem, setDimensionItem] = useState<any>();
const [dataSourceList, setDataSourceList] = useState<any[]>([]);
const [pagination, setPagination] = useState({
current: 1,
pageSize: 20,
total: 0,
});
const actionRef = useRef<ActionType>();
const queryDimensionList = async (params: any) => {
const { code, data, msg } = await getDimensionList({
...params,
...pagination,
domainId: selectDomainId,
});
const { list, pageSize, current, total } = data;
let resData: any = {};
if (code === 200) {
setPagination({
pageSize,
current,
total,
});
resData = {
data: list || [],
success: true,
};
} else {
message.error(msg);
resData = {
data: [],
total: 0,
success: false,
};
}
return resData;
};
const queryDataSourceList = async () => {
const { code, data, msg } = await getDatasourceList({ domainId: selectDomainId });
if (code === 200) {
setDataSourceList(data);
} else {
message.error(msg);
}
};
useEffect(() => {
queryDataSourceList();
}, [selectDomainId]);
const columns: ProColumns[] = [
{
dataIndex: 'id',
title: 'ID',
order: 100,
},
{
dataIndex: 'name',
title: '维度名称',
},
{
dataIndex: 'bizName',
title: '字段名称',
order: 9,
},
{
dataIndex: 'sensitiveLevel',
title: '敏感度',
valueEnum: SENSITIVE_LEVEL_ENUM,
},
{
dataIndex: 'datasourceName',
title: '数据源名称',
search: false,
},
{
dataIndex: 'createdBy',
title: '创建人',
search: false,
},
{
dataIndex: 'description',
title: '描述',
search: false,
},
{
dataIndex: 'updatedAt',
title: '更新时间',
search: false,
render: (value: any) => {
return value && value !== '-' ? moment(value).format('YYYY-MM-DD HH:mm:ss') : '-';
},
},
{
title: '操作',
dataIndex: 'x',
valueType: 'option',
render: (_, record) => {
return (
<Space>
<a
key="classEditBtn"
onClick={() => {
setDimensionItem(record);
setCreateModalVisible(true);
}}
>
</a>
<Popconfirm
title="确认删除?"
okText="是"
cancelText="否"
onConfirm={async () => {
const { code } = await deleteDimension(record.id);
if (code === 200) {
setDimensionItem(undefined);
actionRef.current?.reload();
} else {
message.error('删除失败');
}
}}
>
<a
key="classEditBtn"
onClick={() => {
setDimensionItem(record);
}}
>
</a>
</Popconfirm>
</Space>
);
},
},
];
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 (
<>
<ProTable
className={`${styles.classTable} ${styles.classTableSelectColumnAlignLeft}`}
actionRef={actionRef}
headerTitle="维度列表"
rowKey="id"
columns={columns}
request={queryDimensionList}
pagination={pagination}
search={{
span: 4,
defaultCollapsed: false,
collapseRender: () => {
return <></>;
},
}}
onChange={(data: any) => {
const { current, pageSize, total } = data;
setPagination({
current,
pageSize,
total,
});
}}
tableAlertRender={() => {
return false;
}}
size="small"
options={{ reload: false, density: false, fullScreen: false }}
toolBarRender={() => [
<Button
key="create"
type="primary"
onClick={() => {
setDimensionItem(undefined);
setCreateModalVisible(true);
}}
>
</Button>,
]}
/>
{createModalVisible && (
<DimensionInfoModal
bindModalVisible={createModalVisible}
dimensionItem={dimensionItem}
dataSourceList={dataSourceList}
onSubmit={saveDimension}
onCancel={() => {
setCreateModalVisible(false);
}}
/>
)}
</>
);
};
export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger,
}))(ClassDimensionTable);

View File

@@ -0,0 +1,238 @@
import type { ActionType, ProColumns } from '@ant-design/pro-table';
import ProTable from '@ant-design/pro-table';
import { message, Button, Space, Popconfirm } from 'antd';
import React, { useRef, useState } from 'react';
import type { Dispatch } from 'umi';
import { connect } from 'umi';
import type { StateType } from '../model';
import { SENSITIVE_LEVEL_ENUM } from '../constant';
import { creatExprMetric, updateExprMetric, queryMetric, deleteMetric } from '../service';
import MetricInfoCreateForm from './MetricInfoCreateForm';
import moment from 'moment';
import styles from './style.less';
type Props = {
dispatch: Dispatch;
domainManger: StateType;
};
const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
const { selectDomainId } = domainManger;
const [createModalVisible, setCreateModalVisible] = useState<boolean>(false);
const [metricItem, setMetricItem] = useState<any>();
const [pagination, setPagination] = useState({
current: 1,
pageSize: 20,
total: 0,
});
const actionRef = useRef<ActionType>();
const queryMetricList = async (params: any) => {
const { code, data, msg } = await queryMetric({
...params,
...pagination,
domainId: selectDomainId,
});
const { list, pageSize, current, total } = data;
let resData: any = {};
if (code === 200) {
setPagination({
pageSize,
current,
total,
});
resData = {
data: list || [],
success: true,
};
} else {
message.error(msg);
resData = {
data: [],
total: 0,
success: false,
};
}
return resData;
};
const columns: ProColumns[] = [
{
dataIndex: 'id',
title: 'ID',
},
{
dataIndex: 'name',
title: '指标名称',
},
{
dataIndex: 'bizName',
title: '字段名称',
},
{
dataIndex: 'sensitiveLevel',
title: '敏感度',
valueEnum: SENSITIVE_LEVEL_ENUM,
},
{
dataIndex: 'createdBy',
title: '创建人',
search: false,
},
{
dataIndex: 'description',
title: '描述',
search: false,
},
{
dataIndex: 'updatedAt',
title: '更新时间',
search: false,
render: (value: any) => {
return value && value !== '-' ? moment(value).format('YYYY-MM-DD HH:mm:ss') : '-';
},
},
{
title: '操作',
dataIndex: 'x',
valueType: 'option',
render: (_, record) => {
return (
<Space>
<a
key="classEditBtn"
onClick={() => {
setMetricItem(record);
setCreateModalVisible(true);
}}
>
</a>
<Popconfirm
title="确认删除?"
okText="是"
cancelText="否"
onConfirm={async () => {
const { code } = await deleteMetric(record.id);
if (code === 200) {
setMetricItem(undefined);
actionRef.current?.reload();
} else {
message.error('删除失败');
}
}}
>
<a
key="classEditBtn"
onClick={() => {
setMetricItem(record);
}}
>
</a>
</Popconfirm>
</Space>
);
},
},
];
const saveMetric = async (fieldsValue: any, reloadState: boolean = true) => {
const queryParams = {
domainId: selectDomainId,
...fieldsValue,
};
if (queryParams.typeParams && !queryParams.typeParams.expr) {
message.error('度量表达式不能为空');
return;
}
let saveMetricQuery = creatExprMetric;
if (queryParams.id) {
saveMetricQuery = updateExprMetric;
}
const { code, msg } = await saveMetricQuery(queryParams);
if (code === 200) {
message.success('编辑指标成功');
setCreateModalVisible(false);
if (reloadState) {
actionRef?.current?.reload();
}
dispatch({
type: 'domainManger/queryMetricList',
payload: {
domainId: selectDomainId,
},
});
return;
}
message.error(msg);
};
return (
<>
<ProTable
className={`${styles.classTable} ${styles.classTableSelectColumnAlignLeft}`}
actionRef={actionRef}
headerTitle="指标列表"
rowKey="id"
search={{
span: 4,
defaultCollapsed: false,
collapseRender: () => {
return <></>;
},
}}
columns={columns}
params={{ domainId: selectDomainId }}
request={queryMetricList}
pagination={pagination}
tableAlertRender={() => {
return false;
}}
onChange={(data: any) => {
const { current, pageSize, total } = data;
setPagination({
current,
pageSize,
total,
});
}}
size="small"
options={{ reload: false, density: false, fullScreen: false }}
toolBarRender={() => [
<Button
key="create"
type="primary"
onClick={() => {
setMetricItem(undefined);
setCreateModalVisible(true);
}}
>
</Button>,
]}
/>
{createModalVisible && (
<MetricInfoCreateForm
domainId={Number(selectDomainId)}
createModalVisible={createModalVisible}
metricItem={metricItem}
onSubmit={(values) => {
saveMetric(values);
}}
onCancel={() => {
setCreateModalVisible(false);
}}
/>
)}
</>
);
};
export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger,
}))(ClassMetricTable);

View File

@@ -0,0 +1,159 @@
import type { ActionType } from '@ant-design/pro-table';
import type { Ref, ReactNode } from 'react';
import { Space, message } from 'antd';
import React, { useRef, forwardRef, useImperativeHandle, useState, useEffect } from 'react';
import { EditableProTable } from '@ant-design/pro-table';
type Props = {
title?: string;
tableDataSource: any[];
columnList: any[];
rowKey: string;
editableProTableProps?: any;
onDataSourceChange?: (dataSource: any) => void;
extenderCtrlColumn?: (text, record, _, action) => ReactNode[];
editableActionRender?: (row, config, defaultDom, actionRef) => ReactNode[];
ref?: any;
};
export type CommonEditTableRef = {
getCommonEditTableDataSource: () => void;
editTableActionRef: ActionType;
};
const CommonEditTable: React.FC<Props> = forwardRef(
(
{
title,
columnList,
rowKey,
tableDataSource,
editableProTableProps = {},
onDataSourceChange,
extenderCtrlColumn,
editableActionRender,
}: Props,
ref: Ref<any>,
) => {
const [dataSource, setDataSource] = useState<any[]>(tableDataSource);
const actionRef = useRef<ActionType>();
useImperativeHandle(ref, () => ({
getCommonEditTableDataSource: () => {
return [...dataSource];
},
editTableActionRef: actionRef,
}));
useEffect(() => {
setDataSource(
tableDataSource.map((item: any) => {
return {
...item,
editRowId: item[rowKey] || (Math.random() * 1000000).toFixed(0),
};
}),
);
}, [tableDataSource]);
const handleDataSourceChange = (data: any) => {
setTimeout(() => {
onDataSourceChange?.(data);
}, 0);
};
const columns = [
...columnList,
{
title: '操作',
dataIndex: 'x',
valueType: 'option',
render: (text, record, _, action) => {
return (
<Space>
<a
key="editable"
onClick={() => {
action?.startEditable?.(record.editRowId);
}}
>
</a>
<a
key="deleteBtn"
onClick={() => {
const data = [...dataSource].filter((item) => item[rowKey] !== record[rowKey]);
setDataSource(data);
handleDataSourceChange(data);
}}
>
</a>
{extenderCtrlColumn?.(text, record, _, action)}
</Space>
);
},
},
{
dataIndex: 'editRowId',
hideInTable: true,
},
];
const defaultActionRender = (row, config, defaultDom) => {
return editableActionRender?.(row, config, defaultDom, actionRef);
};
const actionRender = editableActionRender ? defaultActionRender : undefined;
return (
<>
<EditableProTable
key={title}
actionRef={actionRef}
headerTitle={title}
rowKey={'editRowId'}
columns={columns}
value={dataSource}
tableAlertRender={() => {
return false;
}}
onChange={(data) => {
let tableData = data;
if (rowKey) {
// 如果rowKey存在将rowId复写为rowKey值
tableData = data.map((item: any) => {
return {
...item,
editRowId: item[rowKey],
};
});
}
setDataSource(tableData);
handleDataSourceChange(data);
}}
editable={{
onSave: (_, row) => {
const rowKeyValue = row[rowKey];
const isSame = dataSource.filter((item: any, index: number) => {
return index !== row.index && item[rowKey] === rowKeyValue;
});
if (isSame[0]) {
message.error('存在重复值');
return Promise.reject();
}
return true;
},
actionRender: actionRender,
}}
pagination={false}
size="small"
recordCreatorProps={{
record: () => ({ editRowId: (Math.random() * 1000000).toFixed(0) }),
}}
{...editableProTableProps}
/>
</>
);
},
);
export default CommonEditTable;

View File

@@ -0,0 +1,160 @@
import { useEffect, forwardRef, useImperativeHandle, useState } from 'react';
import type { ForwardRefRenderFunction } from 'react';
import { message, Form, Input, Select, Button, Space } from 'antd';
import { saveDatabase, getDatabaseByDomainId, testDatabaseConnect } from '../../service';
import { formLayout } from '@/components/FormHelper/utils';
import styles from '../style.less';
type Props = {
domainId: number;
onSubmit: (params?: any) => void;
};
const FormItem = Form.Item;
const TextArea = Input.TextArea;
const DatabaseCreateForm: ForwardRefRenderFunction<any, Props> = ({ domainId }, ref) => {
const [form] = Form.useForm();
const [selectedDbType, setSelectedDbType] = useState<string>('h2');
const queryDatabaseConfig = async () => {
const { code, data } = await getDatabaseByDomainId(domainId);
if (code === 200) {
form.setFieldsValue({ ...data });
setSelectedDbType(data?.type);
return;
}
message.error('数据库配置获取错误');
};
useEffect(() => {
form.resetFields();
queryDatabaseConfig();
}, [domainId]);
const getFormValidateFields = async () => {
return await form.validateFields();
};
useImperativeHandle(ref, () => ({
getFormValidateFields,
}));
const saveDatabaseConfig = async () => {
const values = await form.validateFields();
const { code, msg } = await saveDatabase({
...values,
domainId,
});
if (code === 200) {
message.success('保存成功');
return;
}
message.error(msg);
};
const testDatabaseConnection = async () => {
const values = await form.validateFields();
const { code, data } = await testDatabaseConnect({
...values,
domainId,
});
if (code === 200 && data) {
message.success('连接测试通过');
return;
}
message.error('连接测试失败');
};
return (
<>
<Form
{...formLayout}
form={form}
layout="vertical"
className={styles.form}
onValuesChange={(value) => {
const { type } = value;
if (type) {
setSelectedDbType(type);
}
}}
>
<FormItem name="name" label="名称" rules={[{ required: true, message: '请输入名称' }]}>
<Input placeholder="请输入数据库名称" />
</FormItem>
<FormItem
name="type"
label="数据库类型"
rules={[{ required: true, message: '请选择数据库类型' }]}
>
<Select
style={{ width: '100%' }}
placeholder="请选择数据库类型"
options={[
{ value: 'h2', label: 'h2' },
{ value: 'mysql', label: 'mysql' },
{ value: 'clickhouse', label: 'clickhouse' },
]}
/>
</FormItem>
{selectedDbType === 'h2' ? (
<FormItem name="url" label="链接" rules={[{ required: true, message: '请输入链接' }]}>
<Input placeholder="请输入链接" />
</FormItem>
) : (
<>
<FormItem name="host" label="host" rules={[{ required: true, message: '请输入IP' }]}>
<Input placeholder="请输入IP" />
</FormItem>
<FormItem
name="port"
label="port"
rules={[{ required: true, message: '请输入端口号' }]}
>
<Input placeholder="请输入端口号" />
</FormItem>
</>
)}
<FormItem
name="username"
label="用户名"
rules={[{ required: true, message: '请输入用户名' }]}
>
<Input placeholder="请输入用户名" />
</FormItem>
<FormItem name="password" label="密码">
<Input.Password placeholder="请输入密码" />
</FormItem>
<FormItem name="database" label="数据库名称">
<Input placeholder="请输入数据库名称" />
</FormItem>
<FormItem name="description" label="描述">
<TextArea placeholder="请输入数据库描述" style={{ height: 100 }} />
</FormItem>
<FormItem>
<Space>
<Button
type="primary"
onClick={() => {
testDatabaseConnection();
}}
>
</Button>
<Button
type="primary"
onClick={() => {
saveDatabaseConfig();
}}
>
</Button>
</Space>
</FormItem>
</Form>
</>
);
};
export default forwardRef(DatabaseCreateForm);

View File

@@ -0,0 +1,35 @@
import { Space } from 'antd';
import React, { useRef } from 'react';
import type { Dispatch } from 'umi';
import { connect } from 'umi';
import type { StateType } from '../../model';
import ProCard from '@ant-design/pro-card';
import DatabaseCreateForm from './DatabaseCreateForm';
type Props = {
dispatch: Dispatch;
domainManger: StateType;
};
const DatabaseSection: React.FC<Props> = ({ domainManger }) => {
const { selectDomainId } = domainManger;
const entityCreateRef = useRef<any>({});
return (
<div style={{ width: 800, margin: '0 auto' }}>
<Space direction="vertical" style={{ width: '100%' }} size={20}>
<ProCard title="数据库设置" bordered>
<DatabaseCreateForm
ref={entityCreateRef}
domainId={Number(selectDomainId)}
onSubmit={() => {}}
/>
</ProCard>
</Space>
</div>
);
};
export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger,
}))(DatabaseSection);

View File

@@ -0,0 +1,165 @@
import React, { useEffect, useState } from 'react';
import { Button, Form, Input, Modal, Select } from 'antd';
import { SENSITIVE_LEVEL_OPTIONS } from '../constant';
import { formLayout } from '@/components/FormHelper/utils';
import SqlEditor from '@/components/SqlEditor';
import { message } from 'antd';
export type CreateFormProps = {
dimensionItem: any;
onCancel: () => void;
bindModalVisible: boolean;
dataSourceList: any[];
onSubmit: (values: any) => Promise<any>;
};
const FormItem = Form.Item;
const { Option } = Select;
const { TextArea } = Input;
const DimensionInfoModal: React.FC<CreateFormProps> = ({
onCancel,
bindModalVisible,
dimensionItem,
dataSourceList,
onSubmit: handleUpdate,
}) => {
const isEdit = dimensionItem?.id;
const [formVals, setFormVals] = useState<any>({
roleCode: '',
users: [],
effectiveTime: 1,
});
const [form] = Form.useForm();
const { setFieldsValue } = form;
const handleSubmit = async () => {
const fieldsValue = await form.validateFields();
setFormVals({ ...fieldsValue });
try {
await handleUpdate(fieldsValue);
} catch (error) {
message.error('保存失败,接口调用出错');
}
};
const setFormVal = () => {
setFieldsValue(dimensionItem);
};
useEffect(() => {
if (dimensionItem) {
setFormVal();
}
}, [dimensionItem]);
const renderFooter = () => {
return (
<>
<Button onClick={onCancel}></Button>
<Button type="primary" onClick={handleSubmit}>
</Button>
</>
);
};
const renderContent = () => {
return (
<>
<FormItem hidden={true} name="id" label="ID">
<Input placeholder="id" />
</FormItem>
<FormItem
name="name"
label="维度中文名"
rules={[{ required: true, message: '请输入维度中文名' }]}
>
<Input placeholder="名称不可重复" />
</FormItem>
<FormItem
name="bizName"
label="维度英文名"
rules={[{ required: true, message: '请输入维度英文名' }]}
>
<Input placeholder="名称不可重复" disabled={isEdit} />
</FormItem>
<FormItem
name="datasourceId"
label="所属数据源"
rules={[{ required: true, message: '请选择所属数据源' }]}
>
<Select placeholder="请选择数据源" disabled={isEdit}>
{dataSourceList.map((item) => (
<Option key={item.id} value={item.id}>
{item.name}
</Option>
))}
</Select>
</FormItem>
<FormItem
name="semanticType"
label="类型"
rules={[{ required: true, message: '请选择维度类型' }]}
>
<Select placeholder="请选择维度类型">
{['CATEGORY', 'ID', 'DATE'].map((item) => (
<Option key={item} value={item}>
{item}
</Option>
))}
</Select>
</FormItem>
<FormItem
name="sensitiveLevel"
label="敏感度"
rules={[{ required: true, message: '请选择敏感度' }]}
>
<Select placeholder="请选择敏感度">
{SENSITIVE_LEVEL_OPTIONS.map((item) => (
<Option key={item.value} value={item.value}>
{item.label}
</Option>
))}
</Select>
</FormItem>
<FormItem
name="description"
label="维度描述"
rules={[{ required: true, message: '请输入维度描述' }]}
>
<TextArea placeholder="请输入维度描述" />
</FormItem>
<FormItem name="expr" label="表达式" rules={[{ required: true, message: '请输入表达式' }]}>
<SqlEditor height={'150px'} />
</FormItem>
</>
);
};
return (
<Modal
width={800}
destroyOnClose
title="维度信息"
style={{ top: 48 }}
maskClosable={false}
open={bindModalVisible}
footer={renderFooter()}
onCancel={onCancel}
>
<Form
{...formLayout}
form={form}
initialValues={{
...formVals,
}}
>
{renderContent()}
</Form>
</Modal>
);
};
export default DimensionInfoModal;

View File

@@ -0,0 +1,135 @@
import { useState, forwardRef } from 'react';
import type { ForwardRefRenderFunction } from 'react';
import { Form, Button } from 'antd';
import FormItemTitle from '@/components/FormHelper/FormItemTitle';
import { formLayout } from '@/components/FormHelper/utils';
import DimensionMetricVisibleModal from './DimensionMetricVisibleModal';
import DimensionSearchVisibleModal from './DimensionSearchVisibleModal';
type Props = {
themeData: any;
metricList: any[];
dimensionList: any[];
domainId: number;
onSubmit: (params?: any) => void;
};
const FormItem = Form.Item;
const DimensionMetricVisibleForm: ForwardRefRenderFunction<any, Props> = ({
domainId,
metricList,
dimensionList,
themeData,
onSubmit,
}) => {
const [dimensionModalVisible, setDimensionModalVisible] = useState(false);
const [dimensionSearchModalVisible, setDimensionSearchModalVisible] = useState(false);
const [metricModalVisible, setMetricModalVisible] = useState<boolean>(false);
return (
<>
<Form {...formLayout}>
<FormItem
label={
<FormItemTitle title={'可见维度'} subTitle={'设置可见后,维度将允许在问答中被使用'} />
}
>
<Button
type="primary"
onClick={() => {
setDimensionModalVisible(true);
}}
>
</Button>
</FormItem>
<FormItem
label={
<FormItemTitle title={'可见指标'} subTitle={'设置可见后,指标将允许在问答中被使用'} />
}
>
<Button
type="primary"
onClick={() => {
setMetricModalVisible(true);
}}
>
</Button>
</FormItem>
<FormItem
label={
<FormItemTitle
title={'可见维度值'}
subTitle={'设置可见后,在可见维度设置的基础上,维度值将在搜索时可以被联想出来'}
/>
}
>
<Button
type="primary"
onClick={() => {
setDimensionSearchModalVisible(true);
}}
>
</Button>
</FormItem>
</Form>
{dimensionModalVisible && (
<DimensionMetricVisibleModal
domainId={domainId}
themeData={themeData}
settingSourceList={dimensionList}
settingType="dimension"
visible={dimensionModalVisible}
onCancel={() => {
setDimensionModalVisible(false);
}}
onSubmit={() => {
onSubmit?.();
setDimensionModalVisible(false);
}}
/>
)}
{dimensionSearchModalVisible && (
<DimensionSearchVisibleModal
domainId={domainId}
settingSourceList={dimensionList.filter((item) => {
const blackDimensionList = themeData.visibility?.blackDimIdList;
if (Array.isArray(blackDimensionList)) {
return !blackDimensionList.includes(item.id);
}
return false;
})}
themeData={themeData}
visible={dimensionSearchModalVisible}
onCancel={() => {
setDimensionSearchModalVisible(false);
}}
onSubmit={() => {
onSubmit?.({ from: 'dimensionSearchVisible' });
setDimensionSearchModalVisible(false);
}}
/>
)}
{metricModalVisible && (
<DimensionMetricVisibleModal
domainId={domainId}
themeData={themeData}
settingSourceList={metricList}
settingType="metric"
visible={metricModalVisible}
onCancel={() => {
setMetricModalVisible(false);
}}
onSubmit={() => {
onSubmit?.();
setMetricModalVisible(false);
}}
/>
)}
</>
);
};
export default forwardRef(DimensionMetricVisibleForm);

View File

@@ -0,0 +1,151 @@
import React, { useEffect, useState } from 'react';
import { Button, Modal, message } from 'antd';
import { addDomainExtend, editDomainExtend, getDomainExtendDetailConfig } from '../../service';
import DimensionMetricVisibleTransfer from './DimensionMetricVisibleTransfer';
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 [visibilityData, setVisibilityData] = 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]);
const queryThemeListData: any = async () => {
const { code, data } = await getDomainExtendDetailConfig({
domainId,
});
if (code === 200) {
setVisibilityData(data.visibility);
return;
}
message.error('获取可见信息失败');
};
useEffect(() => {
queryThemeListData();
}, []);
useEffect(() => {
setSelectedKeyList(visibilityData?.[settingTypeConfig.visibleIdListKey] || []);
}, [visibilityData]);
const saveEntity = async () => {
const { id } = 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 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;

View File

@@ -0,0 +1,88 @@
import { Transfer, Tag } from 'antd';
import React, { useEffect, useState } from 'react';
interface RecordType {
key: string;
name: string;
type: 'dimension' | 'metric';
}
type Props = {
sourceList: any[];
targetList: string[];
titles?: string[];
onChange?: (params?: any) => void;
transferProps?: Record<string, any>;
};
const DimensionMetricVisibleTransfer: React.FC<Props> = ({
sourceList = [],
targetList = [],
titles,
transferProps = {},
onChange,
}) => {
const [transferData, setTransferData] = useState<RecordType[]>([]);
const [targetKeys, setTargetKeys] = useState<string[]>(targetList);
useEffect(() => {
setTransferData(
sourceList.map(({ id, name, type }: any) => {
return {
key: id,
name,
type,
};
}),
);
}, [sourceList]);
useEffect(() => {
setTargetKeys(targetList);
}, [targetList]);
const handleChange = (newTargetKeys: string[]) => {
setTargetKeys(newTargetKeys);
onChange?.(newTargetKeys);
};
return (
<div style={{ display: 'flex', justifyContent: 'center' }}>
<Transfer
dataSource={transferData}
showSearch
titles={titles || ['不可见维度', '可见维度']}
listStyle={{
width: 430,
height: 500,
}}
filterOption={(inputValue: string, item: any) => {
const { name } = item;
if (name.includes(inputValue)) {
return true;
}
return false;
}}
targetKeys={targetKeys}
onChange={handleChange}
render={(item) => (
<div style={{ display: 'flex' }}>
<span style={{ flex: '1' }}>{item.name}</span>
<span style={{ flex: '0 1 40px' }}>
{item.type === 'dimension' ? (
<Tag color="blue">{'维度'}</Tag>
) : item.type === 'metric' ? (
<Tag color="orange">{'指标'}</Tag>
) : (
<></>
)}
</span>
</div>
)}
{...transferProps}
/>
</div>
);
};
export default DimensionMetricVisibleTransfer;

View File

@@ -0,0 +1,138 @@
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 dictionaryInfos = themeData?.dictionaryInfos;
if (Array.isArray(dictionaryInfos)) {
const target = dictionaryInfos[0];
if (Array.isArray(target?.ruleList)) {
setDictRules(target.ruleList[0]);
}
const selectKeys = dictionaryInfos.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 dictionaryInfos = 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({
dictionaryInfos,
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;

View File

@@ -0,0 +1,168 @@
import { useEffect, useState, forwardRef, useImperativeHandle } from 'react';
import type { ForwardRefRenderFunction } from 'react';
import { message, Form, Input, Select, Button } from 'antd';
import { addDomainExtend, editDomainExtend } from '../../service';
import { formLayout } from '@/components/FormHelper/utils';
import styles from '../style.less';
type Props = {
entityData: any;
metricList: any[];
dimensionList: any[];
domainId: number;
onSubmit: (params?: any) => void;
};
const FormItem = Form.Item;
const TextArea = Input.TextArea;
const EntityCreateForm: ForwardRefRenderFunction<any, Props> = (
{ entityData, metricList, dimensionList, domainId, onSubmit },
ref,
) => {
const [form] = Form.useForm();
const [metricListOptions, setMetricListOptions] = useState<any>([]);
const [dimensionListOptions, setDimensionListOptions] = useState<any>([]);
const getFormValidateFields = async () => {
return await form.validateFields();
};
useEffect(() => {
form.resetFields();
if (Object.keys(entityData).length === 0) {
return;
}
const { detailData = {}, names = [] } = entityData;
if (!detailData.dimensionIds) {
entityData = {
...entityData,
detailData: {
...detailData,
dimensionIds: [],
},
};
}
if (!detailData.metricIds) {
entityData = {
...entityData,
detailData: {
...detailData,
metricIds: [],
},
};
}
form.setFieldsValue({ ...entityData, name: names.join(',') });
}, [entityData]);
useImperativeHandle(ref, () => ({
getFormValidateFields,
}));
useEffect(() => {
const metricOption = metricList.map((item: any) => {
return {
label: item.name,
value: item.id,
};
});
setMetricListOptions(metricOption);
}, [metricList]);
useEffect(() => {
const dimensionEnum = dimensionList.map((item: any) => {
return {
label: item.name,
value: item.id,
};
});
setDimensionListOptions(dimensionEnum);
}, [dimensionList]);
const saveEntity = async () => {
const values = await form.validateFields();
const { id, name } = values;
let saveDomainExtendQuery = addDomainExtend;
if (id) {
saveDomainExtendQuery = editDomainExtend;
}
const { code, msg, data } = await saveDomainExtendQuery({
entity: {
...values,
names: name.split(','),
},
domainId,
});
if (code === 200) {
form.setFieldValue('id', data);
onSubmit?.();
message.success('保存成功');
return;
}
message.error(msg);
};
return (
<>
<Form {...formLayout} form={form} layout="vertical" className={styles.form}>
<FormItem hidden={true} name="id" label="ID">
<Input placeholder="id" />
</FormItem>
<FormItem
name="name"
label="实体名称"
rules={[{ required: true, message: '请输入实体名称' }]}
>
<TextArea
placeholder="请输入实体名称,多个实体名称以英文逗号分隔"
style={{ height: 100 }}
/>
</FormItem>
<FormItem
name="entityIds"
label="唯一标识"
rules={[{ required: true, message: '请选择实体标识' }]}
>
<Select
mode="multiple"
allowClear
style={{ width: '100%' }}
placeholder="请选择主体标识"
options={dimensionListOptions}
/>
</FormItem>
<FormItem name={['detailData', 'dimensionIds']} label="维度信息">
<Select
mode="multiple"
allowClear
style={{ width: '100%' }}
placeholder="请选择展示维度信息"
options={dimensionListOptions}
/>
</FormItem>
<FormItem name={['detailData', 'metricIds']} label="指标信息">
<Select
mode="multiple"
allowClear
style={{ width: '100%' }}
placeholder="请选择展示指标信息"
options={metricListOptions}
/>
</FormItem>
<FormItem>
<Button
type="primary"
onClick={() => {
saveEntity();
}}
>
</Button>
</FormItem>
</Form>
</>
);
};
export default forwardRef(EntityCreateForm);

View File

@@ -0,0 +1,102 @@
import { message, Space } from 'antd';
import React, { useState, useEffect, useRef } from 'react';
import type { Dispatch } from 'umi';
import { connect } from 'umi';
import type { StateType } from '../../model';
import { getDomainExtendConfig } from '../../service';
import ProCard from '@ant-design/pro-card';
import EntityCreateForm from './EntityCreateForm';
import MetricSettingForm from './MetricSettingForm';
import DimensionMetricVisibleForm from './DimensionMetricVisibleForm';
type Props = {
dispatch: Dispatch;
domainManger: StateType;
};
const EntitySection: React.FC<Props> = ({ domainManger, dispatch }) => {
const { selectDomainId, dimensionList, metricList } = domainManger;
const [entityData, setEntityData] = useState<any>({});
const [themeData, setThemeData] = useState<any>({});
const entityCreateRef = useRef<any>({});
const queryThemeListData: any = async () => {
const { code, data } = await getDomainExtendConfig({
domainId: selectDomainId,
});
if (code === 200) {
const target = data?.[0] || {};
if (target) {
setThemeData(target);
setEntityData({
id: target.id,
...target.entity,
});
}
return;
}
message.error('获取主题域解析词失败');
};
const initPage = async () => {
queryThemeListData();
};
useEffect(() => {
initPage();
}, [selectDomainId]);
return (
<div style={{ width: 800, margin: '0 auto' }}>
<Space direction="vertical" style={{ width: '100%' }} size={20}>
<ProCard bordered title="问答可见">
<DimensionMetricVisibleForm
themeData={themeData}
domainId={Number(selectDomainId)}
metricList={metricList}
dimensionList={dimensionList}
onSubmit={(params: any = {}) => {
if (params.from === 'dimensionSearchVisible') {
dispatch({
type: 'domainManger/queryDimensionList',
payload: {
domainId: selectDomainId,
},
});
}
queryThemeListData();
}}
/>
</ProCard>
<ProCard bordered title="默认指标">
<MetricSettingForm
domainId={Number(selectDomainId)}
themeData={themeData}
metricList={metricList}
onSubmit={() => {
queryThemeListData();
}}
/>
</ProCard>
<ProCard title="实体" bordered>
<EntityCreateForm
ref={entityCreateRef}
domainId={Number(selectDomainId)}
entityData={entityData}
metricList={metricList}
dimensionList={dimensionList}
onSubmit={() => {
queryThemeListData();
}}
/>
</ProCard>
</Space>
</div>
);
};
export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger,
}))(EntitySection);

View File

@@ -0,0 +1,187 @@
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 },
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);
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%' }}
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);

View File

@@ -0,0 +1,288 @@
import React, { useEffect, useRef, useState } from 'react';
import { Form, Button, Modal, Steps, Input, Select, Switch, InputNumber } from 'antd';
import MetricMeasuresFormTable from './MetricMeasuresFormTable';
import { SENSITIVE_LEVEL_OPTIONS } from '../constant';
import { formLayout } from '@/components/FormHelper/utils';
import FormItemTitle from '@/components/FormHelper/FormItemTitle';
import styles from './style.less';
import { getMeasureListByDomainId } from '../service';
export type CreateFormProps = {
domainId: number;
createModalVisible: boolean;
metricItem: any;
onCancel?: () => void;
onSubmit: (values: any) => void;
};
const { Step } = Steps;
const FormItem = Form.Item;
const { TextArea } = Input;
const { Option } = Select;
const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
domainId,
onCancel,
createModalVisible,
metricItem,
onSubmit,
}) => {
const isEdit = !!metricItem?.id;
const [currentStep, setCurrentStep] = useState(0);
const formValRef = useRef({} as any);
const [form] = Form.useForm();
const updateFormVal = (val: SaveDataSetForm) => {
formValRef.current = val;
};
const [classMeasureList, setClassMeasureList] = useState<any[]>([]);
const [exprTypeParamsState, setExprTypeParamsState] = useState<any>([]);
const [exprSql, setExprSql] = useState<string>('');
const [isPercentState, setIsPercentState] = useState<boolean>(false);
const forward = () => setCurrentStep(currentStep + 1);
const backward = () => setCurrentStep(currentStep - 1);
const queryClassMeasureList = async () => {
const { code, data } = await getMeasureListByDomainId(domainId);
if (code === 200) {
setClassMeasureList(data);
return;
}
setClassMeasureList([]);
};
useEffect(() => {
queryClassMeasureList();
}, []);
const handleNext = async () => {
const fieldsValue = await form.validateFields();
const submitForm = {
...formValRef.current,
...fieldsValue,
typeParams: {
expr: exprSql,
measures: exprTypeParamsState,
},
dataFormatType: isPercentState ? 'percent' : '',
};
updateFormVal(submitForm);
if (currentStep < 1) {
forward();
} else {
onSubmit?.(submitForm);
}
};
const initData = () => {
const {
id,
name,
bizName,
description,
sensitiveLevel,
typeParams: typeParams,
dataFormat,
dataFormatType,
} = metricItem as any;
const isPercent = dataFormatType === 'percent';
const initValue = {
id,
name,
bizName,
sensitiveLevel,
description,
isPercent,
dataFormat: dataFormat || {
decimalPlaces: 2,
needMultiply100: false,
},
};
const editInitFormVal = {
...formValRef.current,
...initValue,
};
updateFormVal(editInitFormVal);
form.setFieldsValue(initValue);
setExprTypeParamsState(typeParams.measures);
setExprSql(typeParams.expr);
setIsPercentState(isPercent);
};
useEffect(() => {
if (isEdit) {
initData();
} else {
// initFields([]);
}
}, [metricItem]);
const renderContent = () => {
if (currentStep === 1) {
return (
<MetricMeasuresFormTable
typeParams={{
measures: exprTypeParamsState,
expr: exprSql,
}}
measuresList={classMeasureList}
onFieldChange={(typeParams: any) => {
setExprTypeParamsState([...typeParams]);
}}
onSqlChange={(sql: string) => {
setExprSql(sql);
}}
/>
);
}
return (
<>
<FormItem hidden={true} name="id" label="ID">
<Input placeholder="id" />
</FormItem>
<FormItem
name="name"
label="指标中文名"
rules={[{ required: true, message: '请输入指标中文名' }]}
>
<Input placeholder="名称不可重复" />
</FormItem>
<FormItem
name="bizName"
label="指标英文名"
rules={[{ required: true, message: '请输入指标英文名' }]}
>
<Input placeholder="名称不可重复" disabled={isEdit} />
</FormItem>
<FormItem
name="sensitiveLevel"
label="敏感度"
rules={[{ required: true, message: '请选择敏感度' }]}
>
<Select placeholder="请选择敏感度">
{SENSITIVE_LEVEL_OPTIONS.map((item) => (
<Option key={item.value} value={item.value}>
{item.label}
</Option>
))}
</Select>
</FormItem>
<FormItem
name="description"
label="指标描述"
rules={[{ required: true, message: '请输入指标描述' }]}
>
<TextArea placeholder="请输入指标描述" />
</FormItem>
<FormItem
label={
<FormItemTitle
title={'是否展示为百分比'}
subTitle={'开启后指标数据展示时会根据配置进行格式化如0.02 -> 2%'}
/>
}
name="isPercent"
valuePropName="checked"
>
<Switch />
</FormItem>
{isPercentState && (
<>
<FormItem
label={
<FormItemTitle
title={'小数位数'}
subTitle={'对小数位数进行设置如保留两位0.021252 -> 2.12%'}
/>
}
name={['dataFormat', 'decimalPlaces']}
>
<InputNumber placeholder="请输入需要保留小数位数" style={{ width: '300px' }} />
</FormItem>
<FormItem
label={
<FormItemTitle
title={'原始值是否乘以100'}
subTitle={'如 原始值0.001 ->展示值0.1% '}
/>
// <FormItemTitle
// title={'仅添加百分号'}
// subTitle={'开启后,会对原始数值直接加%如0.02 -> 0.02%'}
// />
}
name={['dataFormat', 'needMultiply100']}
valuePropName="checked"
>
<Switch />
</FormItem>
</>
)}
</>
);
};
const renderFooter = () => {
if (currentStep === 1) {
return (
<>
<Button style={{ float: 'left' }} onClick={backward}>
</Button>
<Button onClick={onCancel}></Button>
<Button type="primary" onClick={handleNext}>
</Button>
</>
);
}
return (
<>
<Button onClick={onCancel}></Button>
<Button type="primary" onClick={handleNext}>
</Button>
</>
);
};
return (
<Modal
forceRender
width={1300}
style={{ top: 48 }}
bodyStyle={{ padding: '32px 40px 48px' }}
destroyOnClose
title={`${isEdit ? '编辑' : '新建'}指标`}
maskClosable={false}
open={createModalVisible}
footer={renderFooter()}
onCancel={onCancel}
>
<Steps style={{ marginBottom: 28 }} size="small" current={currentStep}>
<Step title="基本信息" />
<Step title="度量信息" />
</Steps>
<Form
{...formLayout}
form={form}
initialValues={{
...formValRef.current,
}}
onValuesChange={(value) => {
const { isPercent } = value;
if (isPercent !== undefined) {
setIsPercentState(isPercent);
}
}}
className={styles.form}
>
{renderContent()}
</Form>
</Modal>
);
};
export default MetricInfoCreateForm;

View File

@@ -0,0 +1,192 @@
import React, { useState, useRef, useEffect } from 'react';
import { Button, Input, Space } from 'antd';
import ProTable from '@ant-design/pro-table';
import ProCard from '@ant-design/pro-card';
import SqlEditor from '@/components/SqlEditor';
import BindMeasuresTable from './BindMeasuresTable';
type Props = {
typeParams: any;
measuresList: any[];
onFieldChange: (measures: any[]) => void;
onSqlChange: (sql: string) => void;
};
const { TextArea } = Input;
const MetricMeasuresFormTable: React.FC<Props> = ({
typeParams,
measuresList,
onFieldChange,
onSqlChange,
}) => {
const actionRef = useRef<ActionType>();
const [measuresModalVisible, setMeasuresModalVisible] = useState<boolean>(false);
const [measuresParams, setMeasuresParams] = useState(
typeParams || {
expr: '',
measures: [],
},
);
useEffect(() => {
setMeasuresParams({ ...typeParams });
}, [typeParams]);
const [exprString, setExprString] = useState(typeParams?.expr || '');
const columns = [
{
dataIndex: 'bizName',
title: '度量名称',
},
// {
// dataIndex: 'alias',
// title: '别名',
// render: (_: any, record: any) => {
// const { alias, name } = record;
// const { measures } = measuresParams;
// return (
// <Input
// placeholder="请输入别名"
// value={alias}
// onChange={(event) => {
// const { value } = event.target;
// const list = measures.map((item: any) => {
// if (item.name === name) {
// return {
// ...item,
// alias: value,
// };
// }
// return item;
// });
// onFieldChange?.(list);
// }}
// />
// );
// },
// },
{
dataIndex: 'constraint',
title: '限定条件',
tooltip:
'所用于过滤的维度需要存在于"维度"列表不需要加where关键字。比如维度A="值1" and 维度B="值2"',
render: (_: any, record: any) => {
const { constraint, name } = record;
const { measures } = measuresParams;
return (
<TextArea
placeholder="请输入限定条件"
value={constraint}
onChange={(event) => {
const { value } = event.target;
const list = measures.map((item: any) => {
if (item.name === name) {
return {
...item,
constraint: value,
};
}
return item;
});
onFieldChange?.(list);
}}
/>
);
},
},
{
title: '操作',
dataIndex: 'x',
valueType: 'option',
render: (_: any, record: any) => {
const { name } = record;
return (
<Space>
<a
key="deleteBtn"
onClick={() => {
const { measures } = measuresParams;
const list = measures.filter((item: any) => {
return item.name !== name;
});
onFieldChange?.(list);
}}
>
</a>
</Space>
);
},
},
];
return (
<>
<Space direction="vertical" style={{ width: '100%' }}>
<ProTable
actionRef={actionRef}
headerTitle="度量列表"
tooltip="一般用于在“指标”列表已有指标的基础上加工新指标比如指标NEW1=指标A/100指标NEW2=指标B/指标C。若需用到多个已有指标可以点击右上角“增加度量”"
rowKey="name"
columns={columns}
dataSource={measuresParams?.measures || []}
pagination={false}
search={false}
size="small"
options={false}
toolBarRender={() => [
<Button
key="create"
type="primary"
onClick={() => {
setMeasuresModalVisible(true);
}}
>
</Button>,
]}
/>
<ProCard
title={'度量表达式'}
tooltip="若为指标NEW1则填写指标A/100。若为指标NEW2则填写指标B/指标C"
>
<SqlEditor
value={exprString}
onChange={(sql: string) => {
const expr = sql;
setExprString(expr);
onSqlChange?.(expr);
}}
height={'150px'}
/>
</ProCard>
</Space>
{measuresModalVisible && (
<BindMeasuresTable
measuresList={measuresList}
selectedMeasuresList={measuresParams?.measures || []}
onSubmit={async (values: any[]) => {
const measures = values.map(({ bizName, name, expr, datasourceId }) => {
return {
bizName,
name,
expr,
datasourceId,
};
});
onFieldChange?.(measures);
setMeasuresModalVisible(false);
}}
onCancel={() => {
setMeasuresModalVisible(false);
}}
createModalVisible={measuresModalVisible}
/>
)}
</>
);
};
export default MetricMeasuresFormTable;

View File

@@ -0,0 +1,143 @@
import React, { useState, useEffect } from 'react';
import { Form, Input, Switch, message } from 'antd';
import SelectPartenr from '@/components/SelectPartner';
import SelectTMEPerson from '@/components/SelectTMEPerson';
import { connect } from 'umi';
import type { Dispatch } from 'umi';
import type { StateType } from '../../model';
import FormItemTitle from '@/components/FormHelper/FormItemTitle';
import { updateDomain, getDomainDetail } from '../../service';
import styles from '../style.less';
type Props = {
dispatch: Dispatch;
domainManger: StateType;
onSubmit?: (data?: any) => void;
onValuesChange?: (value, values) => void;
};
const FormItem = Form.Item;
const PermissionAdminForm: React.FC<Props> = ({ domainManger, onValuesChange }) => {
const [form] = Form.useForm();
const [isOpenState, setIsOpenState] = useState<boolean>(true);
const [classDetail, setClassDetail] = useState<any>({});
const { selectDomainId } = domainManger;
const { APP_TARGET } = process.env;
const queryClassDetail = async (domainId: number) => {
const { code, msg, data } = await getDomainDetail({ domainId });
if (code === 200) {
setClassDetail(data);
const fieldsValue = {
...data,
};
fieldsValue.admins = fieldsValue.admins || [];
fieldsValue.viewers = fieldsValue.viewers || [];
fieldsValue.viewOrgs = fieldsValue.viewOrgs || [];
fieldsValue.isOpen = !!fieldsValue.isOpen;
setIsOpenState(fieldsValue.isOpen);
form.setFieldsValue(fieldsValue);
return;
}
message.error(msg);
};
useEffect(() => {
queryClassDetail(selectDomainId);
}, [selectDomainId]);
const saveAuth = async () => {
const values = await form.validateFields();
const { admins, isOpen, viewOrgs = [], viewers = [] } = values;
const queryClassData = {
...classDetail,
admins,
viewOrgs,
viewers,
isOpen: isOpen ? 1 : 0,
};
const { code, msg } = await updateDomain(queryClassData);
if (code === 200) {
// message.success('保存成功');
return;
}
message.error(msg);
};
return (
<>
<Form
form={form}
layout="vertical"
onValuesChange={(value, values) => {
const { isOpen } = value;
if (isOpen !== undefined) {
setIsOpenState(isOpen);
}
saveAuth();
onValuesChange?.(value, values);
}}
className={styles.form}
>
<FormItem hidden={true} name="groupId" label="ID">
<Input placeholder="groupId" />
</FormItem>
<FormItem
name="admins"
label={
<FormItemTitle title={'管理员'} subTitle={'管理员将拥有主题域下所有编辑及访问权限'} />
}
>
<SelectTMEPerson placeholder="请邀请团队成员" />
</FormItem>
<Form.Item
label={
<FormItemTitle
title={'设为公开'}
subTitle={
'公开后,所有用户将可使用主题域下低/中敏感度资源,高敏感度资源需通过资源列表进行授权'
}
/>
}
name="isOpen"
valuePropName="checked"
>
<Switch />
</Form.Item>
{!isOpenState && (
<>
{APP_TARGET === 'inner' && (
<FormItem name="viewOrgs" label="按组织">
<SelectPartenr
type="selectedDepartment"
treeSelectProps={{
placeholder: '请选择需要授权的部门',
}}
/>
</FormItem>
)}
<FormItem name="viewers" label="按个人">
<SelectTMEPerson placeholder="请选择需要授权的个人" />
</FormItem>
</>
)}
{/* <FormItem>
<Button
type="primary"
onClick={() => {
saveAuth();
}}
>
保 存
</Button>
</FormItem> */}
</Form>
</>
);
};
export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger,
}))(PermissionAdminForm);

View File

@@ -0,0 +1,202 @@
import React, { useEffect, useState, useRef } from 'react';
import { Button, message, Form, Space, Drawer, Input } from 'antd';
import { ProCard } from '@ant-design/pro-components';
import { connect } from 'umi';
import { createGroupAuth, updateGroupAuth } from '../../service';
import PermissionCreateForm from './PermissionCreateForm';
import type { StateType } from '../../model';
import SqlEditor from '@/components/SqlEditor';
import DimensionMetricVisibleTransfer from '../Entity/DimensionMetricVisibleTransfer';
import styles from '../style.less';
type Props = {
domainManger: StateType;
permissonData: any;
domainId: number;
onCancel: () => void;
visible: boolean;
onSubmit: (params?: any) => void;
};
const FormItem = Form.Item;
const TextArea = Input.TextArea;
const PermissionCreateDrawer: React.FC<Props> = ({
domainManger,
visible,
permissonData,
domainId,
onCancel,
onSubmit,
}) => {
const { dimensionList, metricList } = domainManger;
const [form] = Form.useForm();
const basicInfoFormRef = useRef<any>(null);
const [sourceDimensionList, setSourceDimensionList] = useState<any[]>([]);
const [sourceMetricList, setSourceMetricList] = useState<any[]>([]);
const [selectedDimensionKeyList, setSelectedDimensionKeyList] = useState<string[]>([]);
const [selectedMetricKeyList, setSelectedMetricKeyList] = useState<string[]>([]);
useEffect(() => {
const list = dimensionList.reduce((highList: any[], item: any) => {
const { name, bizName, sensitiveLevel } = item;
if (sensitiveLevel === 2) {
highList.push({ id: bizName, name, type: 'dimension' });
}
return highList;
}, []);
setSourceDimensionList(list);
}, [dimensionList]);
useEffect(() => {
const list = metricList.reduce((highList: any[], item: any) => {
const { name, bizName, sensitiveLevel } = item;
if (sensitiveLevel === 2) {
highList.push({ id: bizName, name, type: 'metric' });
}
return highList;
}, []);
setSourceMetricList(list);
}, [metricList]);
const saveAuth = async () => {
const basicInfoFormValues = await basicInfoFormRef.current.formRef.validateFields();
const values = await form.validateFields();
const { dimensionFilters, dimensionFilterDescription } = values;
const { authRules = [] } = permissonData;
let target = authRules?.[0];
if (!target) {
target = { dimensions: dimensionList };
} else {
target.dimensions = dimensionList;
}
permissonData.authRules = [target];
let saveAuthQuery = createGroupAuth;
if (basicInfoFormValues.groupId) {
saveAuthQuery = updateGroupAuth;
}
const { code, msg } = await saveAuthQuery({
...basicInfoFormValues,
dimensionFilters: [dimensionFilters],
dimensionFilterDescription,
authRules: [
{
dimensions: selectedDimensionKeyList,
metrics: selectedMetricKeyList,
},
],
domainId,
});
if (code === 200) {
onSubmit?.();
message.success('保存成功');
return;
}
message.error(msg);
};
useEffect(() => {
form.resetFields();
const { dimensionFilters, dimensionFilterDescription } = permissonData;
form.setFieldsValue({
dimensionFilterDescription,
dimensionFilters: Array.isArray(dimensionFilters) ? dimensionFilters[0] || '' : '',
});
setSelectedDimensionKeyList(permissonData?.authRules?.[0]?.dimensions || []);
setSelectedMetricKeyList(permissonData?.authRules?.[0]?.metrics || []);
}, [permissonData]);
const renderFooter = () => {
return (
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Space>
<Button onClick={onCancel}></Button>
<Button
type="primary"
onClick={() => {
saveAuth();
}}
>
</Button>
</Space>
</div>
);
};
return (
<>
<Drawer
width={'100%'}
className={styles.permissionDrawer}
destroyOnClose
title={'权限组信息'}
maskClosable={false}
open={visible}
footer={renderFooter()}
onClose={onCancel}
>
<div style={{ overflow: 'auto', margin: '0 auto', width: '1000px' }}>
<Space direction="vertical" style={{ width: '100%' }} size={20}>
<ProCard title="基本信息" bordered>
<PermissionCreateForm
ref={basicInfoFormRef}
permissonData={permissonData}
domainId={domainId}
/>
</ProCard>
<ProCard title="列权限" bordered>
<DimensionMetricVisibleTransfer
titles={['未授权维度/指标', '已授权维度/指标']}
sourceList={[...sourceDimensionList, ...sourceMetricList]}
targetList={[...selectedDimensionKeyList, ...selectedMetricKeyList]}
onChange={(bizNameList: string[]) => {
const dimensionKeyChangeList = dimensionList.reduce(
(dimensionChangeList: string[], item: any) => {
if (bizNameList.includes(item.bizName)) {
dimensionChangeList.push(item.bizName);
}
return dimensionChangeList;
},
[],
);
const metricKeyChangeList = metricList.reduce(
(metricChangeList: string[], item: any) => {
if (bizNameList.includes(item.bizName)) {
metricChangeList.push(item.bizName);
}
return metricChangeList;
},
[],
);
setSelectedDimensionKeyList(dimensionKeyChangeList);
setSelectedMetricKeyList(metricKeyChangeList);
}}
/>
</ProCard>
<ProCard bordered title="行权限">
<div>
<Form form={form} layout="vertical">
<FormItem name="dimensionFilters" label="表达式">
<SqlEditor height={'150px'} />
</FormItem>
<FormItem name="dimensionFilterDescription" label="描述">
<TextArea placeholder="行权限描述" />
</FormItem>
</Form>
</div>
</ProCard>
</Space>
</div>
</Drawer>
</>
);
};
export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger,
}))(PermissionCreateDrawer);

View File

@@ -0,0 +1,74 @@
import { useEffect, useImperativeHandle, forwardRef } from 'react';
import { Form, Input } from 'antd';
import type { ForwardRefRenderFunction } from 'react';
import SelectPartenr from '@/components/SelectPartner';
import SelectTMEPerson from '@/components/SelectTMEPerson';
import { formLayout } from '@/components/FormHelper/utils';
import styles from '../style.less';
type Props = {
domainId: number;
permissonData: any;
onSubmit?: (data?: any) => void;
onValuesChange?: (value, values) => void;
};
const FormItem = Form.Item;
const PermissionCreateForm: ForwardRefRenderFunction<any, Props> = (
{ permissonData, onValuesChange },
ref,
) => {
const { APP_TARGET } = process.env;
const [form] = Form.useForm();
useImperativeHandle(ref, () => ({
formRef: form,
}));
useEffect(() => {
const fieldsValue = {
...permissonData,
};
fieldsValue.authorizedDepartmentIds = permissonData.authorizedDepartmentIds || [];
fieldsValue.authorizedUsers = permissonData.authorizedUsers || [];
form.setFieldsValue(fieldsValue);
}, [permissonData]);
return (
<>
<Form
{...formLayout}
key={permissonData.groupId}
form={form}
layout="vertical"
onValuesChange={(value, values) => {
onValuesChange?.(value, values);
}}
className={styles.form}
>
<FormItem hidden={true} name="groupId" label="ID">
<Input placeholder="groupId" />
</FormItem>
<FormItem name="name" label="名称" rules={[{ required: true, message: '请输入名称' }]}>
<Input placeholder="请输入名称" />
</FormItem>
{APP_TARGET === 'inner' && (
<FormItem name="authorizedDepartmentIds" label="按组织">
<SelectPartenr
type="selectedDepartment"
treeSelectProps={{
placeholder: '请选择需要授权的部门',
}}
/>
</FormItem>
)}
<FormItem name="authorizedUsers" label="按个人">
<SelectTMEPerson placeholder="请选择需要授权的个人" />
</FormItem>
</Form>
</>
);
};
export default forwardRef(PermissionCreateForm);

View File

@@ -0,0 +1,32 @@
import { Space } from 'antd';
import React from 'react';
import type { Dispatch } from 'umi';
import { connect } from 'umi';
import type { StateType } from '../../model';
import { ProCard } from '@ant-design/pro-card';
import PermissionTable from './PermissionTable';
import PermissionAdminForm from './PermissionAdminForm';
type Props = {
dispatch: Dispatch;
domainManger: StateType;
};
const PermissionSection: React.FC<Props> = () => {
return (
<>
<div>
<Space direction="vertical" style={{ width: '100%' }} size={20}>
<ProCard title="邀请成员" bordered>
<PermissionAdminForm />
</ProCard>
<PermissionTable />
</Space>
</div>
</>
);
};
export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger,
}))(PermissionSection);

View File

@@ -0,0 +1,297 @@
import type { ActionType, ProColumns } from '@ant-design/pro-table';
import ProTable from '@ant-design/pro-table';
import { message, Button, Space, Popconfirm, Tooltip } from 'antd';
import React, { useRef, useState, useEffect } from 'react';
import type { Dispatch } from 'umi';
import { connect } from 'umi';
import type { StateType } from '../../model';
import { getGroupAuthInfo, removeGroupAuth } from '../../service';
import { getDepartmentTree } from '@/components/SelectPartner/service';
import { getAllUser } from '@/components/SelectTMEPerson/service';
import PermissionCreateDrawer from './PermissionCreateDrawer';
import { findDepartmentTree } from '@/pages/SemanticModel/utils';
type Props = {
dispatch: Dispatch;
domainManger: StateType;
};
const PermissionTable: React.FC<Props> = ({ domainManger }) => {
const { APP_TARGET } = process.env;
const isInner = APP_TARGET === 'inner';
const { dimensionList, metricList, selectDomainId } = domainManger;
const [createModalVisible, setCreateModalVisible] = useState<boolean>(false);
const [permissonData, setPermissonData] = useState<any>({});
const [intentionList, setIntentionList] = useState<any[]>([]);
const [departmentTreeData, setDepartmentTreeData] = useState<any[]>([]);
const [tmePerson, setTmePerson] = useState<any[]>([]);
const [pagination, setPagination] = useState({
current: 1,
pageSize: 20,
total: 0,
});
const actionRef = useRef<ActionType>();
const queryListData = async () => {
const { code, data } = await getGroupAuthInfo(selectDomainId);
if (code === 200) {
setIntentionList(data);
return;
}
message.error('获取主题域解析词失败');
};
useEffect(() => {
if (selectDomainId) {
queryListData();
}
}, [selectDomainId]);
const queryDepartmentData = async () => {
const { code, data } = await getDepartmentTree();
if (code === 200) {
setDepartmentTreeData(data);
}
};
const queryTmePersonData = async () => {
const { code, data } = await getAllUser();
if (code === 200) {
setTmePerson(data);
}
};
useEffect(() => {
if (isInner) {
queryDepartmentData();
}
queryTmePersonData();
}, []);
const columns: ProColumns[] = [
{
dataIndex: 'groupId',
title: 'ID',
width: 50,
},
{
dataIndex: 'name',
title: '名称',
width: 150,
},
{
dataIndex: 'departmentPermission',
title: '授权组织',
ellipsis: {
showTitle: false,
},
hideInTable: !isInner,
width: 200,
render: (_, record: any) => {
const { authorizedDepartmentIds = [] } = record;
const departmentNameList = authorizedDepartmentIds.reduce(
(departmentNames: string[], id: string) => {
const department = findDepartmentTree(departmentTreeData, id);
if (department) {
departmentNames.push(department.name);
}
return departmentNames;
},
[],
);
const words = departmentNameList.join(',');
return (
<Tooltip placement="topLeft" title={words}>
{words}
</Tooltip>
);
},
},
{
dataIndex: 'personPermission',
title: '授权个人',
ellipsis: {
showTitle: false,
},
// width: 200,
render: (_, record: any) => {
const { authorizedUsers = [] } = record;
const personNameList = tmePerson.reduce((enNames: string[], item: any) => {
const hasPerson = authorizedUsers.includes(item.enName);
if (hasPerson) {
enNames.push(item.displayName);
}
return enNames;
}, []);
const words = personNameList.join(',');
return (
<Tooltip placement="topLeft" title={words}>
{words}
</Tooltip>
);
},
},
{
dataIndex: 'columnPermission',
title: '列权限',
// width: 400,
ellipsis: {
showTitle: false,
},
render: (_, record: any) => {
const { authRules } = record;
const target = authRules?.[0];
if (target) {
const { dimensions, metrics } = target;
let dimensionNameList: string[] = [];
let metricsNameList: string[] = [];
if (Array.isArray(dimensions)) {
dimensionNameList = dimensionList.reduce((enNameList: string[], item: any) => {
const { bizName, name } = item;
if (dimensions.includes(bizName)) {
enNameList.push(name);
}
return enNameList;
}, []);
}
if (Array.isArray(metrics)) {
metricsNameList = metricList.reduce((enNameList: string[], item: any) => {
const { bizName, name } = item;
if (metrics.includes(bizName)) {
enNameList.push(name);
}
return enNameList;
}, []);
}
const words = [...dimensionNameList, ...metricsNameList].join(',');
return (
<Tooltip placement="topLeft" title={words}>
{words}
</Tooltip>
);
}
return <> - </>;
},
},
{
title: '操作',
dataIndex: 'x',
valueType: 'option',
render: (_, record) => {
return (
<Space>
<a
key="classEditBtn"
onClick={() => {
setPermissonData(record);
setCreateModalVisible(true);
}}
>
</a>
{/* <a
key="dimensionEditBtn"
onClick={() => {
setPermissonData(record);
setDimensionModalVisible(true);
}}
>
维度授权
</a>
<a
key="metricEditBtn"
onClick={() => {
setPermissonData(record);
setMetricModalVisible(true);
}}
>
指标授权
</a> */}
<Popconfirm
title="确认删除?"
okText="是"
cancelText="否"
onConfirm={async () => {
const { code } = await removeGroupAuth({
domainId: record.domainId,
groupId: record.groupId,
});
if (code === 200) {
setPermissonData({});
queryListData();
} else {
message.error('删除失败');
}
}}
>
<a
key="classEditBtn"
onClick={() => {
setPermissonData(record);
}}
>
</a>
</Popconfirm>
</Space>
);
},
},
];
return (
<>
<ProTable
actionRef={actionRef}
headerTitle="资源列表"
rowKey="groupId"
columns={columns}
search={false}
dataSource={intentionList}
pagination={pagination}
onChange={(data: any) => {
const { current, pageSize, total } = data;
setPagination({
current,
pageSize,
total,
});
}}
size="small"
options={false}
toolBarRender={() => [
<Button
key="create"
type="primary"
onClick={() => {
setPermissonData({});
setCreateModalVisible(true);
}}
>
</Button>,
]}
/>
{createModalVisible && (
<PermissionCreateDrawer
domainId={Number(selectDomainId)}
visible={createModalVisible}
permissonData={permissonData}
onSubmit={() => {
queryListData();
setCreateModalVisible(false);
}}
onCancel={() => {
setCreateModalVisible(false);
}}
/>
)}
</>
);
};
export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger,
}))(PermissionTable);

View File

@@ -0,0 +1,124 @@
import React, { useState } from 'react';
import { Form, Button, Modal, Input, Switch } from 'antd';
import styles from './style.less';
import { useMounted } from '@/hooks/useMounted';
import { message } from 'antd';
import { formLayout } from '@/components/FormHelper/utils';
import { EnumTransModelType } from '@/enum';
const FormItem = Form.Item;
export type ProjectInfoFormProps = {
basicInfo: any;
onCancel: () => void;
onSubmit: (values: any) => Promise<any>;
};
const ProjectInfoForm: React.FC<ProjectInfoFormProps> = (props) => {
const { basicInfo, onSubmit: handleUpdate, onCancel } = props;
const { type, modelType } = basicInfo;
const isMounted = useMounted();
const [formVals, setFormVals] = useState<any>(basicInfo);
const [saveLoading, setSaveLoading] = useState(false);
const [form] = Form.useForm();
const handleConfirm = async () => {
const fieldsValue = await form.validateFields();
// const columnsValue = { ...fieldsValue, isUnique: fieldsValue.isUnique === true ? 1 : 0 };
const columnsValue = { ...fieldsValue, isUnique: 1 };
setFormVals({ ...formVals, ...columnsValue });
setSaveLoading(true);
try {
await handleUpdate({ ...formVals, ...columnsValue });
if (isMounted()) {
setSaveLoading(false);
}
} catch (error) {
message.error('接口调用出错');
setSaveLoading(false);
}
};
const footer = (
<>
<Button onClick={onCancel}></Button>
<Button type="primary" loading={saveLoading} onClick={handleConfirm}>
</Button>
</>
);
const titleRender = () => {
let str = EnumTransModelType[modelType];
if (type === 'top') {
str += '顶级';
} else if (modelType === 'add') {
str += '子';
}
str += '主题域';
return str;
};
return (
<Modal
width={640}
bodyStyle={{ padding: '32px 40px 48px' }}
destroyOnClose
title={titleRender()}
open={true}
footer={footer}
onCancel={onCancel}
>
<Form
{...formLayout}
form={form}
initialValues={{
...formVals,
}}
className={styles.form}
>
{type !== 'top' && modelType === 'add' && (
<FormItem name="parentName" label="父主题域名称">
<Input disabled placeholder="父主题域名称" />
</FormItem>
)}
<FormItem
name="name"
label="主题域名称"
rules={[{ required: true, message: '请输入主题域名称!' }]}
>
<Input placeholder="主题域名称不可重复" />
</FormItem>
<FormItem
name="bizName"
label="主题域英文名称"
rules={[{ required: true, message: '请输入主题域英文名称!' }]}
>
<Input placeholder="请输入主题域英文名称" />
</FormItem>
<FormItem name="description" label="主题域描述">
<Input.TextArea placeholder="主题域描述" />
</FormItem>
<FormItem name="isUnique" label="是否唯一" hidden={true}>
<Switch
size="small"
checked={true}
// onChange={(checked) => {
// setFormVals({ ...formVals, isUnique: checked });
// }}
/>
{/* <Switch
size="small"
checked={formVals.isUnique ? true : false}
onChange={(checked) => {
setFormVals({ ...formVals, isUnique: checked });
}}
/> */}
</FormItem>
</Form>
</Modal>
);
};
export default ProjectInfoForm;

View File

@@ -0,0 +1,250 @@
import { DownOutlined, PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
import { Input, message, Tree, Popconfirm, Space, Tooltip } from 'antd';
import type { DataNode } from 'antd/lib/tree';
import React, { useEffect, useState } from 'react';
import type { FC, Key } from 'react';
import { connect } from 'umi';
import type { Dispatch } from 'umi';
import type { StateType } from '../model';
import { getDomainList, createDomain, updateDomain, deleteDomain } from '../service';
import { treeParentKeyLists } from '../utils';
import ProjectInfoFormProps from './ProjectInfoForm';
import { constructorClassTreeFromList, addPathInTreeData } from '../utils';
import { PlusCircleOutlined } from '@ant-design/icons';
import styles from './style.less';
const { Search } = Input;
type ProjectListProps = {
selectDomainId: string;
selectDomainName: string;
createDomainBtnVisible?: boolean;
dispatch: Dispatch;
};
const projectTreeFlat = (projectTree: DataNode[], filterValue: string): DataNode[] => {
let newProjectTree: DataNode[] = [];
projectTree.map((item) => {
const { children, ...rest } = item;
if (String(item.title).includes(filterValue)) {
newProjectTree.push({ ...rest });
}
if (children && children.length > 0) {
newProjectTree = newProjectTree.concat(projectTreeFlat(children, filterValue));
}
return item;
});
return newProjectTree;
};
const ProjectListTree: FC<ProjectListProps> = ({
selectDomainId,
createDomainBtnVisible = true,
dispatch,
}) => {
const [projectTree, setProjectTree] = useState<DataNode[]>([]);
const [projectInfoModalVisible, setProjectInfoModalVisible] = useState<boolean>(false);
const [projectInfoParams, setProjectInfoParams] = useState<any>({});
const [filterValue, setFliterValue] = useState<string>('');
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
const [classList, setClassList] = useState<any[]>([]);
const onSearch = (value: any) => {
setFliterValue(value);
};
const initProjectTree = async () => {
const { code, data, msg } = await getDomainList();
if (code === 200) {
const treeData = addPathInTreeData(constructorClassTreeFromList(data));
setProjectTree(treeData);
setClassList(data);
setExpandedKeys(treeParentKeyLists(treeData));
const firstRootNode = data.filter((item: any) => {
return item.parentId === 0;
})[0];
if (firstRootNode) {
const { id, name } = firstRootNode;
dispatch({
type: 'domainManger/setSelectDomain',
selectDomainId: id,
selectDomainName: name,
domainData: firstRootNode,
});
}
} else {
message.error(msg);
}
};
useEffect(() => {
initProjectTree();
}, []);
const handleSelect = (selectedKeys: string, projectName: string) => {
if (selectedKeys === selectDomainId) {
return;
}
const targetNodeData = classList.filter((item: any) => {
return item.id === selectedKeys;
})[0];
dispatch({
type: 'domainManger/setSelectDomain',
selectDomainId: selectedKeys,
selectDomainName: projectName,
domainData: targetNodeData,
});
};
const editProject = async (values: any) => {
const params = { ...values };
const res = await updateDomain(params);
if (res.code === 200) {
message.success('编辑分类成功');
setProjectInfoModalVisible(false);
initProjectTree();
} else {
message.error(res.msg);
}
};
const projectSubmit = async (values: any) => {
if (values.modelType === 'add') {
await createDomain(values);
} else if (values.modelType === 'edit') {
await editProject(values);
}
initProjectTree();
setProjectInfoModalVisible(false);
};
// 删除项目
const confirmDelete = async (projectId: string) => {
const res = await deleteDomain(projectId);
if (res.code === 200) {
message.success('编辑项目成功');
setProjectInfoModalVisible(false);
initProjectTree();
} else {
message.error(res.msg);
}
};
const titleRender = (node: any) => {
const { id, name, path } = node as any;
return (
<div className={styles.projectItem}>
<span
className={styles.title}
onClick={() => {
handleSelect(id, name);
}}
>
{name}
</span>
{createDomainBtnVisible && (
<span className={styles.operation}>
{Array.isArray(path) && path.length < 3 && (
<PlusOutlined
className={styles.icon}
onClick={() => {
setProjectInfoParams({
modelType: 'add',
type: 'normal',
parentId: id,
parentName: name,
});
setProjectInfoModalVisible(true);
}}
/>
)}
<EditOutlined
className={styles.icon}
onClick={() => {
setProjectInfoParams({
modelType: 'edit',
type: 'normal',
...node,
});
setProjectInfoModalVisible(true);
}}
/>
<Popconfirm
key="popconfirm"
title={'确认删除吗?'}
onConfirm={() => {
confirmDelete(id);
}}
okText="是"
cancelText="否"
>
<DeleteOutlined className={styles.icon} />
</Popconfirm>
</span>
)}
</div>
);
};
const projectRenderTree = filterValue ? projectTreeFlat(projectTree, filterValue) : projectTree;
const handleExpand = (_expandedKeys: Key[]) => {
setExpandedKeys(_expandedKeys as string[]);
};
return (
<div className={styles.projectList}>
<h2 className={styles.treeTitle}>
<span className={styles.title}></span>
<Space>
{createDomainBtnVisible && (
<Tooltip title="新增顶级域">
<PlusCircleOutlined
onClick={() => {
setProjectInfoParams({ type: 'top', modelType: 'add' });
setProjectInfoModalVisible(true);
}}
className={styles.addBtn}
/>
</Tooltip>
)}
</Space>
</h2>
<Search
allowClear
className={styles.search}
placeholder="请输入主题域名称进行查询"
onSearch={onSearch}
/>
<Tree
expandedKeys={expandedKeys}
onExpand={handleExpand}
className={styles.tree}
selectedKeys={[selectDomainId]}
blockNode={true}
switcherIcon={<DownOutlined />}
defaultExpandAll={true}
treeData={projectRenderTree}
titleRender={titleRender}
/>
{projectInfoModalVisible && (
<ProjectInfoFormProps
basicInfo={projectInfoParams}
onSubmit={projectSubmit}
onCancel={() => {
setProjectInfoModalVisible(false);
}}
/>
)}
</div>
);
};
export default connect(
({ domainManger: { selectDomainId, selectDomainName } }: { domainManger: StateType }) => ({
selectDomainId,
selectDomainName,
}),
)(ProjectListTree);

View File

@@ -0,0 +1,224 @@
.projectBody {
display: flex;
flex-direction: row;
background-color: #fff;
height: calc(100vh - 48px);
.projectList {
display: flex;
flex-direction: column;
// width: 400px;
overflow: hidden;
// min-height: calc(100vh - 48px);
border-right: 1px solid #d9d9d9;
.treeTitle {
margin-bottom: 0;
padding: 20px;
line-height: 34px;
text-align: right;
background: #fff;
border-bottom: 1px solid #d9d9d9;
.title {
float: left;
}
.addBtn {
cursor: pointer;
&:hover {
color: #296DF3;
}
}
}
.search {
width: calc(100% - 20px);
margin: 10px;
}
.tree {
flex: 1;
padding: 10px;
.projectItem {
display: flex;
width: 100%;
cursor: auto;
.title {
flex: 1;
cursor: pointer;
}
.operation {
.icon {
margin-left: 6px;
cursor: pointer;
}
}
}
}
}
.projectManger {
width: 100%;
min-height: calc(100vh - 48px);
background: #f8f9fb;
position: relative;
.collapseLeftBtn {
position: absolute;
top: calc(50% + 45px);
left: 0;
z-index: 100;
display: flex;
align-items: center;
height: 70px;
color: #fff;
font-size: 12px;
background-color: rgba(40, 46, 54, 0.2);
border-radius: 0 24px 24px 0;
cursor: pointer;
transition: all 0.3s ease;
}
.title {
margin-bottom: 0;
padding: 20px;
font-size: 20px;
line-height: 34px;
border-bottom: 1px solid #d9d9d9;
}
.tab {
padding: 0 20px;
line-height: 28px;
}
.mainTip {
padding: 20px;
}
:global {
.ant-card-body {
padding: 0 !important;
}
.ant-tabs-content-holder {
overflow: scroll;
height: calc(100vh - 192px);
}
}
.resource {
display: flex;
.tree {
flex: 1;
.headOperation {
.btn {
margin-right: 18px;
}
}
.resourceSearch {
margin: 10px 0;
}
}
.view {
width: 480px;
padding-left: 20px;
}
.selectTypesBtn {
margin-right: 8px;
}
}
}
}
.user {
display: grid;
}
.search{
width: 50%;
margin-bottom: 20px;
}
.paramsName{
margin-right: 10px;
}
.deleteBtn{
margin-left: 5px;
}
.authBtn{
cursor: pointer;
}
.selectedResource{
font-size: 12px;
color: darkgrey;
margin-top: 4px;
}
.switch{
// min-width: 900px;
// max-width: 1200px;
// width: 100%;
display: flex;
justify-content: flex-start;
.switchUser {
width: 200px;
margin-left: 33px;
:global {
.ant-select {
width: 100% !important;
}
}
}
}
.dimensionIntentionForm {
margin-bottom: -24px !important;
margin-top: 10px;
margin-left: 26px;
}
.classTable {
:global {
.ant-pro-table-search-query-filter {
padding-left: 0 !important;
// padding-bottom: 24px !important;
.ant-pro-form-query-filter {
.ant-form-item {
// margin-bottom: 0;
}
}
}
.ant-table-tbody > tr.ant-table-row-selected > td {
background: none;
}
}
}
.classTableSelectColumnAlignLeft {
:global {
.ant-table-selection-column {
text-align: left;
}
}
}
.permissionDrawer {
:global {
.ant-drawer-body {
background: #f8f9fb;
}
}
}

View File

@@ -0,0 +1,23 @@
export const SENSITIVE_LEVEL_OPTIONS = [
{
label: '低',
value: 0,
},
{
label: '中',
value: 1,
},
{
label: '高',
value: 2,
},
];
export const SENSITIVE_LEVEL_ENUM = SENSITIVE_LEVEL_OPTIONS.reduce(
(sensitiveEnum: any, item: any) => {
const { label, value } = item;
sensitiveEnum[value] = label;
return sensitiveEnum;
},
{},
);

View File

@@ -0,0 +1,64 @@
export type ISODateString =
`${number}-${number}-${number}T${number}:${number}:${number}.${number}+${number}:${number}`;
export type GraphConfigType = 'datasource' | 'dimension' | 'metric';
export type UserName = string;
export type SensitiveLevel = 0 | 1 | 2 | null;
export declare namespace IDataSource {
interface IIdentifiersItem {
name: string;
type: string;
}
interface IDimensionsItem {
name: string;
type: string;
expr: null;
dateFormat: 'YYYY-MM-DD';
typeParams: {
isPrimary: boolean;
timeGranularity: string;
};
isCreateDimension: number;
nameCh: string;
}
interface IMeasuresItem {
name: string;
agg: string;
expr: string;
constraint: string;
alias: string;
create_metric: string;
nameCh: string;
isCreateMetric: number;
}
interface IDataSourceDetail {
queryType: string;
sqlQuery: string;
tableQuery: string;
identifiers: IIdentifiersItem[];
dimensions: IDimensionsItem[];
measures: IMeasuresItem[];
}
interface IDataSourceItem {
createdBy: UserName;
updatedBy: UserName;
createdAt: ISODateString;
updatedAt: ISODateString;
id: number;
name: string;
bizName: string;
description: string;
status: number | null;
sensitiveLevel: SensitiveLevel;
domainId: number;
databaseId: number;
datasourceDetail: IDataSourceDetail;
}
type IDataSourceList = IDataSourceItem[];
}

View File

@@ -0,0 +1,7 @@
import React from 'react';
const classManager: React.FC = ({ children }) => {
return <div>{children}</div>;
};
export default classManager;

View File

@@ -0,0 +1,98 @@
import type { Reducer, Effect } from 'umi';
import { message } from 'antd';
import { getDimensionList, queryMetric } from './service';
export type StateType = {
current: number;
pageSize: number;
selectDomainId: any;
selectDomainName: string;
dimensionList: any[];
metricList: any[];
searchParams: Record<string, any>;
domainData: any;
};
export type ModelType = {
namespace: string;
state: StateType;
effects: {
queryDimensionList: Effect;
queryMetricList: Effect;
};
reducers: {
setSelectDomain: Reducer<StateType>;
setPagination: Reducer<StateType>;
setDimensionList: Reducer<StateType>;
setMetricList: Reducer<StateType>;
reset: Reducer<StateType>;
};
};
export const defaultState: StateType = {
current: 1,
pageSize: 20,
selectDomainId: undefined,
selectDomainName: '',
searchParams: {},
dimensionList: [],
metricList: [],
domainData: {},
};
const Model: ModelType = {
namespace: 'domainManger',
state: defaultState,
effects: {
*queryDimensionList({ payload }, { call, put }) {
const { code, data, msg } = yield call(getDimensionList, payload);
if (code === 200) {
yield put({ type: 'setDimensionList', payload: { dimensionList: data.list } });
} else {
message.error(msg);
}
},
*queryMetricList({ payload }, { call, put }) {
const { code, data, msg } = yield call(queryMetric, payload);
if (code === 200) {
yield put({ type: 'setMetricList', payload: { metricList: data.list } });
} else {
message.error(msg);
}
},
},
reducers: {
setSelectDomain(state = defaultState, action) {
return {
...state,
selectDomainId: action.selectDomainId,
selectDomainName: action.selectDomainName,
domainData: action.domainData,
};
},
setPagination(state = defaultState, action) {
return {
...state,
...action.payload,
};
},
setDimensionList(state = defaultState, action) {
return {
...state,
...action.payload,
};
},
setMetricList(state = defaultState, action) {
return {
...state,
...action.payload,
};
},
reset() {
return defaultState;
},
},
};
export default Model;

View File

@@ -0,0 +1,239 @@
import request from 'umi-request';
export function getDomainList(): Promise<any> {
if (window.RUNNING_ENV === 'chat') {
return request.get(`${process.env.CHAT_API_BASE_URL}conf/domainList`);
}
return request.get(`${process.env.API_BASE_URL}domain/getDomainList`);
}
export function getDatasourceList(data: any): Promise<any> {
return request.get(`${process.env.API_BASE_URL}datasource/getDatasourceList/${data.domainId}`);
}
export function getDomainDetail(data: any): Promise<any> {
return request.get(`${process.env.API_BASE_URL}domain/getDomain/${data.domainId}`);
}
export function createDomain(data: any): Promise<any> {
return request.post(`${process.env.API_BASE_URL}domain/createDomain`, {
data,
});
}
export function updateDomain(data: any): Promise<any> {
return request.post(`${process.env.API_BASE_URL}domain/updateDomain`, {
data,
});
}
export function createDatasource(data: any): Promise<any> {
return request.post(`${process.env.API_BASE_URL}datasource/createDatasource`, {
data,
});
}
export function updateDatasource(data: any): Promise<any> {
return request.post(`${process.env.API_BASE_URL}datasource/updateDatasource`, {
data,
});
}
export function getDimensionList(data: any): Promise<any> {
const queryParams = {
data: { current: 1, pageSize: 999999, ...data },
};
if (window.RUNNING_ENV === 'chat') {
return request.post(`${process.env.CHAT_API_BASE_URL}conf/dimension/page`, queryParams);
}
return request.post(`${process.env.API_BASE_URL}dimension/queryDimension`, queryParams);
}
export function createDimension(data: any): Promise<any> {
return request.post(`${process.env.API_BASE_URL}dimension/createDimension`, {
data,
});
}
export function updateDimension(data: any): Promise<any> {
return request.post(`${process.env.API_BASE_URL}dimension/updateDimension`, {
data,
});
}
export function queryMetric(data: any): Promise<any> {
const queryParams = {
data: { current: 1, pageSize: 999999, ...data },
};
if (window.RUNNING_ENV === 'chat') {
return request.post(`${process.env.CHAT_API_BASE_URL}conf/metric/page`, queryParams);
}
return request.post(`${process.env.API_BASE_URL}metric/queryMetric`, queryParams);
}
export function creatExprMetric(data: any): Promise<any> {
return request.post(`${process.env.API_BASE_URL}metric/creatExprMetric`, {
data,
});
}
export function updateExprMetric(data: any): Promise<any> {
return request.post(`${process.env.API_BASE_URL}metric/updateExprMetric`, {
data,
});
}
export function getMeasureListByDomainId(domainId: number): Promise<any> {
return request.get(`${process.env.API_BASE_URL}datasource/getMeasureListOfDomain/${domainId}`);
}
export function deleteDatasource(id: any): Promise<any> {
return request(`${process.env.API_BASE_URL}datasource/deleteDatasource/${id}`, {
method: 'DELETE',
});
}
export function deleteDimension(id: any): Promise<any> {
return request(`${process.env.API_BASE_URL}dimension/deleteDimension/${id}`, {
method: 'DELETE',
});
}
export function deleteMetric(id: any): Promise<any> {
return request(`${process.env.API_BASE_URL}metric/deleteMetric/${id}`, {
method: 'DELETE',
});
}
export function deleteDomain(id: any): Promise<any> {
return request(`${process.env.API_BASE_URL}domain/deleteDomain/${id}`, {
method: 'DELETE',
});
}
export function getGroupAuthInfo(id: string): Promise<any> {
return request(`${process.env.AUTH_API_BASE_URL}queryGroup`, {
method: 'GET',
params: {
domainId: id,
},
});
}
export function createGroupAuth(data: any): Promise<any> {
return request(`${process.env.AUTH_API_BASE_URL}createGroup`, {
method: 'POST',
data,
});
}
export function updateGroupAuth(data: any): Promise<any> {
return request(`${process.env.AUTH_API_BASE_URL}updateGroup`, {
method: 'POST',
data,
});
}
export function removeGroupAuth(data: any): Promise<any> {
return request(`${process.env.AUTH_API_BASE_URL}removeGroup`, {
method: 'POST',
data,
});
}
export function addDomainExtend(data: any): Promise<any> {
return request(`${process.env.CHAT_API_BASE_URL}conf`, {
method: 'POST',
data,
});
}
export function editDomainExtend(data: any): Promise<any> {
return request(`${process.env.CHAT_API_BASE_URL}conf`, {
method: 'PUT',
data,
});
}
export function getDomainExtendConfig(data: any): Promise<any> {
return request(`${process.env.CHAT_API_BASE_URL}conf/search`, {
method: 'POST',
data,
});
}
export function getDomainExtendDetailConfig(data: any): Promise<any> {
return request(`${process.env.CHAT_API_BASE_URL}conf/richDesc/${data.domainId}`, {
method: 'GET',
});
}
export function getDatasourceRelaList(id?: number): Promise<any> {
return request(`${process.env.API_BASE_URL}datasource/getDatasourceRelaList/${id}`, {
method: 'GET',
});
}
export function createOrUpdateDatasourceRela(data: any): Promise<any> {
return request(`${process.env.API_BASE_URL}viewInfo/createOrUpdateDatasourceRela`, {
method: 'POST',
data,
});
}
export function createOrUpdateViewInfo(data: any): Promise<any> {
return request(`${process.env.API_BASE_URL}viewInfo/createOrUpdateViewInfo`, {
method: 'POST',
data,
});
}
export function getViewInfoList(domainId: number): Promise<any> {
return request(`${process.env.API_BASE_URL}viewInfo/getViewInfoList/${domainId}`, {
method: 'GET',
});
}
export function deleteDatasourceRela(domainId: any): Promise<any> {
return request(`${process.env.API_BASE_URL}viewInfo/deleteDatasourceRela/${domainId}`, {
method: 'DELETE',
});
}
export function getDatabaseByDomainId(domainId: number): Promise<any> {
return request(`${process.env.API_BASE_URL}database/getDatabaseByDomainId/${domainId}`, {
method: 'GET',
});
}
export function getDomainSchemaRela(domainId: number): Promise<any> {
return request(`${process.env.API_BASE_URL}viewInfo/getDomainSchemaRela/${domainId}`, {
method: 'GET',
});
}
export type SaveDatabaseParams = {
domainId: number;
name: string;
type: string;
host: string;
port: string;
username: string;
password: string;
database?: string;
description?: string;
};
export function saveDatabase(data: SaveDatabaseParams): Promise<any> {
return request(`${process.env.API_BASE_URL}database/createOrUpdateDatabase`, {
method: 'POST',
data,
});
}
export function testDatabaseConnect(data: SaveDatabaseParams): Promise<any> {
return request(`${process.env.API_BASE_URL}database/testConnect`, {
method: 'POST',
data,
});
}

View File

@@ -0,0 +1,79 @@
import type { API } from '@/services/API';
import type { DataNode } from 'antd/lib/tree';
export const changeTreeData = (treeData: API.ProjectList, auth?: boolean): DataNode[] => {
return treeData.map((item: any) => {
const newItem: DataNode = {
...item,
key: item.id,
disabled: auth,
children: item.children ? changeTreeData(item.children, auth) : [],
};
return newItem;
});
};
export const addPathInTreeData = (treeData: API.ProjectList, loopPath: any[] = []): any => {
return treeData.map((item: any) => {
const { children, parentId = [] } = item;
const path = loopPath.slice();
path.push(parentId);
if (children) {
return {
...item,
path,
children: addPathInTreeData(children, path),
};
}
return {
...item,
path,
};
});
};
export const constructorClassTreeFromList = (list: any[], parentId: number = 0) => {
const tree = list.reduce((nodeList, nodeItem) => {
if (nodeItem.parentId == parentId) {
const children = constructorClassTreeFromList(list, nodeItem.id);
if (children.length) {
nodeItem.children = children;
}
nodeItem.key = nodeItem.id;
nodeItem.title = nodeItem.name;
nodeList.push(nodeItem);
}
return nodeList;
}, []);
return tree;
};
export const treeParentKeyLists = (treeData: API.ProjectList): string[] => {
let keys: string[] = [];
treeData.forEach((item: any) => {
if (item.children && item.children.length > 0) {
keys.push(item.id);
keys = keys.concat(treeParentKeyLists(item.children));
}
});
return keys;
};
// bfs 查询树结构
export const findDepartmentTree: any = (treeData: any[], projectId: string) => {
if (treeData.length === 0) {
return [];
}
let newStepList: any[] = [];
const departmentData = treeData.find((item) => {
if (item.subDepartments) {
newStepList = newStepList.concat(item.subDepartments);
}
return item.key === projectId;
});
if (departmentData) {
return departmentData;
}
return findDepartmentTree(newStepList, projectId);
};