mirror of
https://github.com/tencentmusic/supersonic.git
synced 2025-12-16 06:56:57 +00:00
first commit
This commit is contained in:
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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, ' '),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -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;
|
||||
@@ -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'];
|
||||
@@ -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;
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
@@ -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>,
|
||||
]);
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
29
webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticFlows/data.d.ts
vendored
Normal file
29
webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticFlows/data.d.ts
vendored
Normal 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[];
|
||||
@@ -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%);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
0
webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/data.d.ts
vendored
Normal file
0
webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/data.d.ts
vendored
Normal 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);
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
// },
|
||||
// ],
|
||||
};
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
},
|
||||
{},
|
||||
);
|
||||
64
webapp/packages/supersonic-fe/src/pages/SemanticModel/data.d.ts
vendored
Normal file
64
webapp/packages/supersonic-fe/src/pages/SemanticModel/data.d.ts
vendored
Normal 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[];
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
const classManager: React.FC = ({ children }) => {
|
||||
return <div>{children}</div>;
|
||||
};
|
||||
|
||||
export default classManager;
|
||||
@@ -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;
|
||||
239
webapp/packages/supersonic-fe/src/pages/SemanticModel/service.ts
Normal file
239
webapp/packages/supersonic-fe/src/pages/SemanticModel/service.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
Reference in New Issue
Block a user