semantic-fe visual modeling pr (#21)

* [improvement][semantic-fe] Added an editing component to set filtering rules for Q&A. Now, the SQL editor will be accompanied by a list for display and control, to resolve ambiguity when using comma-separated values.
[improvement][semantic-fe] Improved validation logic and prompt copywriting for data source/dimension/metric editing and creation.
[improvement][semantic-fe] Improved user experience for visual modeling. Now, when using the legend to control the display/hide of data sources and their associated metric dimensions, the canvas will be re-layout based on the activated data source in the legend.

* [improvement][semantic-fe] Submitted a new version of the visual modeling tool.
[improvement][semantic-fe] Fixed an issue with the initialization of YoY and MoM metrics in Q&A settings.
[improvement][semantic-fe] Added a version field to the database settings.
[improvement][semantic-fe] 1. Added the ability to set YoY and MoM metrics in Q&A settings.2. Moved dimension value editing from the dimension editing window to the dimension list.

---------

Co-authored-by: tristanliu <tristanliu@tencent.com>
This commit is contained in:
tristanliu
2023-07-31 11:23:37 +08:00
committed by GitHub
parent e2b2d31429
commit 0ac652c5d9
50 changed files with 2375 additions and 1188 deletions

View File

@@ -0,0 +1,96 @@
@import '~antd/es/style/themes/default.less';
.standardFormRow {
display: flex;
width: 100%;
margin-bottom: 12px;
// padding-bottom: 16px;
// border-bottom: 1px dashed @border-color-split;
:global {
.ant-form-item,
.ant-legacy-form-item {
margin-right: 24px;
}
.ant-form-item-label,
.ant-legacy-form-item-label {
label {
margin-right: 0;
color: @text-color;
}
}
.ant-form-item-label,
.ant-legacy-form-item-label,
.ant-form-item-control,
.ant-legacy-form-item-control {
padding: 0;
line-height: 32px;
}
}
.label {
flex: 0 0 auto;
margin-right: 24px;
color: @heading-color;
font-size: @font-size-base;
text-align: left;
& > span {
display: inline-block;
flex-shrink: 0;
width: 80px;
height: 32px;
// height: 20px;
color: #999;
font-weight: 500;
font-size: 14px;
font-family: PingFangSC-Medium, PingFang SC;
line-height: 32px;
// line-height: 20px;
// &::after {
// content: '';
// }
}
}
.content {
flex: 1 1 0;
:global {
.ant-form-item,
.ant-legacy-form-item {
&:last-child {
display: block;
margin-right: 0;
}
}
}
}
}
.standardFormRowLast {
margin-bottom: 0;
padding-bottom: 0;
border: none;
}
.standardFormRowBlock {
:global {
.ant-form-item,
.ant-legacy-form-item,
div.ant-form-item-control-wrapper,
div.ant-legacy-form-item-control-wrapper {
display: block;
}
}
}
.standardFormRowGrid {
:global {
.ant-form-item,
.ant-legacy-form-item,
div.ant-form-item-control-wrapper,
div.ant-legacy-form-item-control-wrapper {
display: block;
}
.ant-form-item-label,
.ant-legacy-form-item-label {
float: left;
}
}
}

View File

@@ -0,0 +1,43 @@
import React from 'react';
import classNames from 'classnames';
import styles from './index.less';
type StandardFormRowProps = {
title?: string;
last?: boolean;
block?: boolean;
grid?: boolean;
style?: React.CSSProperties;
titleClassName?: string;
};
const StandardFormRow: React.FC<StandardFormRowProps> = ({
title,
children,
last,
block,
grid,
titleClassName,
...rest
}) => {
const cls = classNames(styles.standardFormRow, {
[styles.standardFormRowBlock]: block,
[styles.standardFormRowLast]: last,
[styles.standardFormRowGrid]: grid,
});
const labelCls = classNames(styles.label, titleClassName);
return (
<div className={cls} {...rest}>
{title && (
<div className={labelCls}>
<span>{title}</span>
</div>
)}
<div className={styles.content}>{children}</div>
</div>
);
};
export default StandardFormRow;

View File

@@ -0,0 +1,33 @@
@import '~antd/es/style/themes/default.less';
.tagSelect {
position: relative;
max-height: 32px;
margin-left: -8px;
overflow: hidden;
line-height: 32px;
transition: all 0.3s;
user-select: none;
:global {
.ant-tag {
margin-right: 24px;
padding: 0 8px;
font-size: @font-size-base;
}
}
&.expanded {
max-height: 200px;
transition: all 0.3s;
}
.trigger {
position: absolute;
top: 0;
right: 0;
span.anticon {
font-size: 12px;
}
}
&.hasExpandTag {
padding-right: 50px;
}
}

View File

@@ -0,0 +1,191 @@
import { DownOutlined, UpOutlined } from '@ant-design/icons';
import { useBoolean, useControllableValue } from 'ahooks';
import { Tag } from 'antd';
import classNames from 'classnames';
import { FC, useEffect } from 'react';
import React from 'react';
import styles from './index.less';
const { CheckableTag } = Tag;
export interface TagSelectOptionProps {
value: string | number | undefined;
style?: React.CSSProperties;
checked?: boolean;
onChange?: (value: string | number, state: boolean) => void;
}
const TagSelectOption: React.FC<TagSelectOptionProps> & {
isTagSelectOption: boolean;
} = ({ children, checked, onChange, value }) => (
<CheckableTag
checked={!!checked}
key={value}
onChange={(state) => onChange && onChange(value, state)}
>
{children}
</CheckableTag>
);
TagSelectOption.isTagSelectOption = true;
type TagSelectOptionElement = React.ReactElement<TagSelectOptionProps, typeof TagSelectOption>;
export interface TagSelectProps {
onChange?: (value: (string | number)[]) => void;
expandable?: boolean;
value?: (string | number)[];
defaultValue?: (string | number)[];
style?: React.CSSProperties;
hideCheckAll?: boolean;
actionsText?: {
expandText?: React.ReactNode;
collapseText?: React.ReactNode;
selectAllText?: React.ReactNode;
};
className?: string;
Option?: TagSelectOptionProps;
children?: TagSelectOptionElement | TagSelectOptionElement[];
single?: boolean;
disableUnCheck?: boolean;
empty?: boolean;
isSelectAll?: boolean;
reverseCheckAll?: boolean;
}
const TagSelect: FC<TagSelectProps> & { Option: typeof TagSelectOption } = (props) => {
const {
children,
hideCheckAll = false,
className,
style,
expandable,
actionsText = {},
single = false,
disableUnCheck = false,
empty = false,
isSelectAll = false,
reverseCheckAll = false,
} = props;
const [expand, { toggle }] = useBoolean();
const [value, setValue] = useControllableValue<(string | number)[] | undefined>(props);
useEffect(() => {
if (empty) {
setValue([]);
}
}, [empty]);
const isTagSelectOption = (node: TagSelectOptionElement) =>
node &&
node.type &&
(node.type.isTagSelectOption || node.type.displayName === 'TagSelectOption');
const getAllTags = () => {
const childrenArray = React.Children.toArray(children) as TagSelectOptionElement[];
const checkedTags = childrenArray
.filter((child) => isTagSelectOption(child))
.map((child) => child.props.value);
return checkedTags || [];
};
const onSelectAll = (checked: boolean) => {
let checkedTags: (string | number)[] = [];
if (reverseCheckAll) {
setValue(undefined);
return;
}
if (checked) {
checkedTags = getAllTags();
}
setValue(checkedTags);
};
useEffect(() => {
if (isSelectAll) {
onSelectAll(true);
}
}, []);
const handleTagChange = (tag: string | number, checked: boolean) => {
let checkedTags: (string | number)[] = [...(value || [])];
if (single && checkedTags.length > 0) {
checkedTags = [checkedTags.join('')];
}
const index = checkedTags.indexOf(tag);
if (checked && index === -1) {
if (single) {
checkedTags = [tag];
} else {
checkedTags.push(tag);
}
} else if (!checked && index > -1 && !disableUnCheck) {
checkedTags.splice(index, 1);
}
setValue(checkedTags.length === 0 ? undefined : checkedTags);
};
const checkedAll = getAllTags().length === value?.length;
const hasChecked = value === undefined ? false : value?.length > 0;
const {
expandText = '展开',
collapseText = '收起',
selectAllText = reverseCheckAll ? '不限' : '全部',
} = actionsText;
const cls = classNames(styles.tagSelect, className, {
[styles.hasExpandTag]: expandable,
[styles.expanded]: expand,
});
return (
<div className={cls} style={style}>
{hideCheckAll ? null : (
<CheckableTag
checked={reverseCheckAll ? !hasChecked : checkedAll}
key="tag-select-__all__"
onChange={onSelectAll}
>
{selectAllText}
</CheckableTag>
)}
{children &&
React.Children.map(children, (child: TagSelectOptionElement) => {
if (isTagSelectOption(child)) {
return React.cloneElement(child, {
key: `tag-select-${child.props.value}`,
value: child.props.value,
checked: value && value.indexOf(child.props.value) > -1,
onChange: handleTagChange,
});
}
return child;
})}
{expandable && (
<a
className={styles.trigger}
onClick={() => {
toggle();
}}
>
{expand ? (
<>
{collapseText} <UpOutlined />
</>
) : (
<>
{expandText}
<DownOutlined />
</>
)}
</a>
)}
</div>
);
};
TagSelect.Option = TagSelectOption;
export default TagSelect;

View File

@@ -228,7 +228,7 @@ ol {
li {
cursor: pointer;
list-style-type:none;
// list-style: none;
line-height: 25px;
margin-left: 0;
&:hover {
color: #4E86F5;
@@ -242,4 +242,15 @@ ol {
.ant-tag {
transition: none;
}
}
}
.semantic-graph-toolbar {
position: absolute;
width: 200px;
}
.g6-component-tooltip {
p {
line-height: 25px;
}
}

View File

@@ -6,6 +6,7 @@ import styles from './components/style.less';
import type { StateType } from './model';
import { DownOutlined } from '@ant-design/icons';
import EntitySection from './components/Entity/EntitySection';
import RecommendedQuestionsSection from './components/Entity/RecommendedQuestionsSection';
import { ISemantic } from './data';
import { getDomainList } from './service';
import OverView from './components/OverView';
@@ -130,15 +131,20 @@ const ChatSetting: React.FC<Props> = ({ domainManger, dispatch }) => {
const isModelItem = [
{
label: '指标场景',
label: '指标模式',
key: 'metric',
children: <EntitySection chatConfigType={ChatConfigType.AGG} />,
},
{
label: '明细场景',
label: '实体模式',
key: 'dimenstion',
children: <EntitySection chatConfigType={ChatConfigType.DETAIL} />,
},
{
label: '推荐问题',
key: 'recommendedQuestions',
children: <RecommendedQuestionsSection />,
},
];
return (

View File

@@ -0,0 +1,109 @@
import { Form, Select, Input } from 'antd';
import StandardFormRow from '@/components/StandardFormRow';
import TagSelect from '@/components/TagSelect';
import React, { useEffect } from 'react';
import { SENSITIVE_LEVEL_OPTIONS } from '../../constant';
const FormItem = Form.Item;
const { Option } = Select;
type Props = {
filterValues?: any;
onFiltersChange: (_: any, values: any) => void;
};
const MetricFilter: React.FC<Props> = ({ filterValues = {}, onFiltersChange }) => {
const [form] = Form.useForm();
useEffect(() => {
form.setFieldsValue({
...filterValues,
});
}, [form, filterValues]);
const handleValuesChange = (value: any, values: any) => {
onFiltersChange(value, values);
};
const onSearch = (value) => {
if (!value) {
return;
}
onFiltersChange(value, form.getFieldsValue());
};
const filterList = [
{
title: '指标类型',
key: 'type',
options: [
{
value: 'ATOMIC',
label: '原子指标',
},
{ value: 'DERIVED', label: '衍生指标' },
],
},
{
title: '敏感度',
key: 'sensitiveLevel',
options: SENSITIVE_LEVEL_OPTIONS,
},
];
return (
<Form
layout="inline"
form={form}
colon={false}
onValuesChange={(value, values) => {
if (value.keywords || value.keywordsType) {
return;
}
handleValuesChange(value, values);
}}
initialValues={{
keywordsType: 'name',
}}
>
<StandardFormRow key="search" block>
<Input.Group compact>
<FormItem name={'keywordsType'} noStyle>
<Select>
<Option value="name"></Option>
<Option value="bizName"></Option>
<Option value="id">ID</Option>
</Select>
</FormItem>
<FormItem name={'keywords'} noStyle>
<Input.Search
placeholder="请输入需要查询的指标信息"
allowClear
onSearch={onSearch}
style={{ width: 300 }}
enterButton
/>
</FormItem>
</Input.Group>
</StandardFormRow>
{filterList.map((item) => {
const { title, key, options } = item;
return (
<StandardFormRow key={key} title={title} block>
<FormItem name={key}>
<TagSelect reverseCheckAll single>
{options.map((item: any) => (
<TagSelect.Option key={item.value} value={item.value}>
{item.label}
</TagSelect.Option>
))}
</TagSelect>
</FormItem>
</StandardFormRow>
);
})}
</Form>
);
};
export default MetricFilter;

View File

@@ -0,0 +1,196 @@
import type { ActionType, ProColumns } from '@ant-design/pro-table';
import ProTable from '@ant-design/pro-table';
import { message, Space } 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 { queryMetric } from '../service';
import MetricFilter from './components/MetricFilter';
import moment from 'moment';
import styles from './style.less';
type Props = {
dispatch: Dispatch;
domainManger: StateType;
};
type QueryMetricListParams = {
id?: string;
name?: string;
bizName?: string;
sensitiveLevel?: string;
type?: string;
};
const ClassMetricTable: React.FC<Props> = () => {
const [pagination, setPagination] = useState({
current: 1,
pageSize: 20,
total: 0,
});
const [dataSource, setDataSource] = useState<any[]>([]);
const actionRef = useRef<ActionType>();
useEffect(() => {
queryMetricList();
}, []);
const queryMetricList = async (params: QueryMetricListParams = {}) => {
const { code, data, msg } = await queryMetric({
...params,
...pagination,
});
const { list, pageSize, current, total } = data;
let resData: any = {};
if (code === 200) {
setPagination({
pageSize: Math.min(pageSize, 100),
current,
total,
});
setDataSource(list);
resData = {
data: list || [],
success: true,
};
} else {
message.error(msg);
setDataSource([]);
resData = {
data: [],
total: 0,
success: false,
};
}
return resData;
};
const columns: ProColumns[] = [
{
dataIndex: 'id',
title: 'ID',
},
{
dataIndex: 'name',
title: '指标名称',
},
{
dataIndex: 'alias',
title: '别名',
search: false,
},
{
dataIndex: 'bizName',
title: '字段名称',
},
{
dataIndex: 'sensitiveLevel',
title: '敏感度',
valueEnum: SENSITIVE_LEVEL_ENUM,
},
{
dataIndex: 'createdBy',
title: '创建人',
search: false,
},
{
dataIndex: 'description',
title: '描述',
search: false,
},
{
dataIndex: 'type',
title: '指标类型',
// search: false,
valueEnum: {
ATOMIC: '原子指标',
DERIVED: '衍生指标',
},
},
{
dataIndex: 'updatedAt',
title: '更新时间',
search: false,
render: (value: any) => {
return value && value !== '-' ? moment(value).format('YYYY-MM-DD HH:mm:ss') : '-';
},
},
];
const handleFilterChange = async (filterParams: {
keywordsType: string;
keywords: string;
sensitiveLevel: string;
type: string;
}) => {
const params: QueryMetricListParams = {};
const { keywordsType, keywords, sensitiveLevel, type } = filterParams;
if (keywordsType && keywords) {
params[keywordsType] = keywords;
}
const sensitiveLevelValue = sensitiveLevel?.[0];
const typeValue = type?.[0];
if (sensitiveLevelValue) {
params.sensitiveLevel = sensitiveLevelValue;
}
if (type) {
params.type = typeValue;
}
await queryMetricList(params);
};
return (
<>
<div className={styles.metricFilterWrapper}>
<MetricFilter
onFiltersChange={(_, values) => {
handleFilterChange(values);
}}
/>
</div>
<ProTable
className={`${styles.metricTable}`}
actionRef={actionRef}
// headerTitle="指标列表"
rowKey="id"
search={false}
dataSource={dataSource}
columns={columns}
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);
// }}
// >
// 创建指标
// </Button>,
// ]}
/>
</>
);
};
export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger,
}))(ClassMetricTable);

View File

@@ -0,0 +1,10 @@
.metricFilterWrapper {
margin: 20px;
padding: 20px;
border-radius: 10px;
background: #fff;
}
.metricTable {
margin: 20px;
}

View File

@@ -7,6 +7,7 @@ import ClassDimensionTable from './components/ClassDimensionTable';
import ClassMetricTable from './components/ClassMetricTable';
import PermissionSection from './components/Permission/PermissionSection';
import DatabaseSection from './components/Database/DatabaseSection';
import EntitySettingSection from './components/Entity/EntitySettingSection';
import OverView from './components/OverView';
import styles from './components/style.less';
import type { StateType } from './model';
@@ -149,11 +150,10 @@ const DomainManger: React.FC<Props> = ({ domainManger, dispatch }) => {
const isModelItem = [
{
label: '可视化建模',
label: '画布',
key: 'xflow',
children: (
<div style={{ width: '100%', height: 'calc(100vh - 200px)' }}>
{/* <SemanticFlow /> */}
<div style={{ width: '100%', marginTop: -20 }}>
<SemanticGraphCanvas />
</div>
),
@@ -178,6 +178,12 @@ const DomainManger: React.FC<Props> = ({ domainManger, dispatch }) => {
key: 'metric',
children: <ClassMetricTable />,
},
{
label: '实体',
key: 'entity',
children: <EntitySettingSection />,
},
{
label: '权限管理',
key: 'permissonSetting',

View File

@@ -7,7 +7,7 @@ import { connect } from 'umi';
import { DATASOURCE_NODE_RENDER_ID } from '../constant';
import DataSourceRelationFormDrawer from './DataSourceRelationFormDrawer';
import DataSourceCreateForm from '../../Datasource/components/DataSourceCreateForm';
import ClassDataSourceTypeModal from '../../components/ClassDataSourceTypeModal';
import ClassDataSourceTypeModal from '../../components/ClassDataSourceTypeModal1';
import { GraphApi } from '../service';
import { SemanticNodeType } from '../../enum';
import type { StateType } from '../../model';

View File

@@ -6,34 +6,36 @@ import { SemanticNodeType } from '../../enum';
import { SEMANTIC_NODE_TYPE_CONFIG } from '../../constant';
type InitContextMenuProps = {
graphShowType: string;
graphShowType?: string;
onMenuClick?: (key: string, item: Item) => void;
};
export const getMenuConfig = (props?: InitContextMenuProps) => {
const { graphShowType, onMenuClick } = props || {};
const { onMenuClick } = props || {};
return {
getContent(evt) {
const nodeData = evt?.item?._cfg?.model;
const { name, nodeType } = nodeData as any;
if (nodeData) {
const nodeTypeConfig = SEMANTIC_NODE_TYPE_CONFIG[nodeType] || {};
let ulNode = `<ul>
let ulNode = `
<li title='编辑' key='edit' >编辑</li>
<li title='删除' key='delete' >删除</li>
</ul>`;
`;
if (nodeType === SemanticNodeType.DATASOURCE) {
if (graphShowType) {
const typeString = graphShowType === SemanticNodeType.DIMENSION ? '维度' : '指标';
ulNode = `<ul>
<li title='新增${typeString}' key='create' >新增${typeString}</li>
</ul>`;
}
ulNode = `
<li title='新增维度' key='createDimension' >新增维度</li>
<li title='新增指标' key='createMetric' >新增指标</li>
<li title='编辑' key='editDatasource' >编辑</li>
<li title='删除' key='deleteDatasource' >删除</li>
`;
}
const header = `${name}`;
return `<div class="g6ContextMenuContainer">
<h3>${presetsTagDomString(nodeTypeConfig.label, nodeTypeConfig.color)}${header}</h3>
<ul>
${ulNode}
</ul>
</div>`;
}
return `<div>当前节点信息获取失败</div>`;

View File

@@ -1,11 +1,10 @@
import { Button, Modal, message } from 'antd';
import { Modal, message } from 'antd';
import React, { useState } from 'react';
import { SemanticNodeType } from '../../enum';
import { deleteDimension, deleteMetric } from '../../service';
import { deleteDimension, deleteMetric, deleteDatasource } from '../../service';
type Props = {
nodeData: any;
nodeType: SemanticNodeType;
onOkClick: () => void;
onCancelClick: () => void;
open: boolean;
@@ -13,7 +12,6 @@ type Props = {
const DeleteConfirmModal: React.FC<Props> = ({
nodeData,
nodeType,
onOkClick,
onCancelClick,
open = false,
@@ -21,11 +19,21 @@ const DeleteConfirmModal: React.FC<Props> = ({
const [confirmLoading, setConfirmLoading] = useState(false);
const deleteNode = async () => {
setConfirmLoading(true);
const { id } = nodeData;
let deleteQuery = deleteDimension;
const { id, nodeType } = nodeData;
let deleteQuery;
if (nodeType === SemanticNodeType.DIMENSION) {
deleteQuery = deleteDimension;
}
if (nodeType === SemanticNodeType.METRIC) {
deleteQuery = deleteMetric;
}
if (nodeType === SemanticNodeType.DATASOURCE) {
deleteQuery = deleteDatasource;
}
if (!deleteQuery) {
message.error('当前节点类型不是维度,指标,数据源中的一种,请确认节点数据');
return;
}
const { code, msg } = await deleteQuery(id);
setConfirmLoading(false);
if (code === 200) {

View File

@@ -0,0 +1,66 @@
import { Button, Space } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import React from 'react';
import { connect } from 'umi';
import type { StateType } from '../../model';
type Props = {
domainManger: StateType;
onClick: (params?: { eventName?: string }) => void;
[key: string]: any;
};
const GraphToolBar: React.FC<Props> = ({ onClick }) => {
return (
<div
style={{
padding: 0,
backgroundColor: '#fff',
display: 'flex',
justifyContent: 'end',
position: 'absolute',
top: 20,
right: 20,
zIndex: 1,
}}
>
<Space>
<Button
key="createDatabaseBtn"
icon={<PlusOutlined />}
size="small"
onClick={() => {
onClick?.({ eventName: 'createDatabase' });
}}
>
</Button>
<Button
key="createDimensionBtn"
icon={<PlusOutlined />}
size="small"
onClick={() => {
onClick?.({ eventName: 'createDimension' });
}}
>
</Button>
<Button
key="createMetricBtn"
icon={<PlusOutlined />}
size="small"
onClick={() => {
onClick?.({ eventName: 'createMetric' });
}}
>
</Button>
</Space>
</div>
);
};
export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger,
}))(GraphToolBar);

View File

@@ -0,0 +1,292 @@
import {
Button,
Drawer,
message,
Row,
Col,
Divider,
Tag,
Space,
Typography,
Popconfirm,
} from 'antd';
import React, { useState, useEffect, ReactNode } from 'react';
import { SemanticNodeType } from '../../enum';
import { deleteDimension, deleteMetric, deleteDatasource } from '../../service';
import { connect } from 'umi';
import type { StateType } from '../../model';
import moment from 'moment';
import styles from '../style.less';
import TransTypeTag from '../../components/TransTypeTag';
import { MetricTypeWording } from '../../enum';
import { SENSITIVE_LEVEL_ENUM } from '../../constant';
const { Paragraph } = Typography;
type Props = {
nodeData: any;
domainManger: StateType;
onNodeChange: (params?: { eventName?: string }) => void;
onEditBtnClick?: (nodeData: any) => void;
[key: string]: any;
};
interface DescriptionItemProps {
title: string;
content: React.ReactNode;
}
type InfoListItemChildrenItem = {
label: string;
value: string;
content?: ReactNode;
hideItem?: boolean;
};
type InfoListItem = {
title: string;
hideItem?: boolean;
children: InfoListItemChildrenItem[];
};
const DescriptionItem = ({ title, content }: DescriptionItemProps) => (
<div style={{ marginBottom: 7, fontSize: 14 }}>
<Space>
<span style={{ width: 'max-content' }}>{title}:</span>
{content}
</Space>
</div>
);
const NodeInfoDrawer: React.FC<Props> = ({
nodeData,
domainManger,
onNodeChange,
onEditBtnClick,
...restProps
}) => {
const [infoList, setInfoList] = useState<InfoListItem[]>([]);
const { selectDomainName } = domainManger;
useEffect(() => {
if (!nodeData) {
return;
}
const {
alias,
fullPath,
bizName,
createdBy,
createdAt,
updatedAt,
description,
domainName,
sensitiveLevel,
type,
nodeType,
} = nodeData;
const list = [
{
title: '基本信息',
children: [
{
label: '字段名称',
value: bizName,
},
{
label: '别名',
value: alias || '-',
},
{
label: '所属主题域',
value: domainName,
content: <Tag>{domainName || selectDomainName}</Tag>,
},
{
label: '描述',
value: description,
},
],
},
{
title: '应用信息',
children: [
{
label: '全路径',
value: fullPath,
content: (
<Paragraph style={{ width: 275, margin: 0 }} ellipsis={{ tooltip: fullPath }}>
{fullPath}
</Paragraph>
),
},
{
label: '敏感度',
value: SENSITIVE_LEVEL_ENUM[sensitiveLevel],
},
{
label: '指标类型',
value: MetricTypeWording[type],
hideItem: nodeType !== SemanticNodeType.METRIC,
},
],
},
{
title: '创建信息',
children: [
{
label: '创建人',
value: createdBy,
},
{
label: '创建时间',
value: createdAt ? moment(createdAt).format('YYYY-MM-DD HH:mm:ss') : '',
},
{
label: '更新时间',
value: updatedAt ? moment(updatedAt).format('YYYY-MM-DD HH:mm:ss') : '',
},
],
},
];
const datasourceList = [
{
title: '基本信息',
children: [
{
label: '英文名称',
value: bizName,
},
{
label: '所属主题域',
value: domainName,
content: <Tag>{domainName || selectDomainName}</Tag>,
},
{
label: '描述',
value: description,
},
],
},
{
title: '创建信息',
children: [
{
label: '创建人',
value: createdBy,
},
{
label: '创建时间',
value: createdAt ? moment(createdAt).format('YYYY-MM-DD HH:mm:ss') : '',
},
{
label: '更新时间',
value: updatedAt ? moment(updatedAt).format('YYYY-MM-DD HH:mm:ss') : '',
},
],
},
];
setInfoList(nodeType === SemanticNodeType.DATASOURCE ? datasourceList : list);
}, [nodeData]);
const handleDeleteConfirm = async () => {
let deleteQuery;
if (nodeData?.nodeType === SemanticNodeType.METRIC) {
deleteQuery = deleteMetric;
}
if (nodeData?.nodeType === SemanticNodeType.DIMENSION) {
deleteQuery = deleteDimension;
}
if (nodeData?.nodeType === SemanticNodeType.DATASOURCE) {
deleteQuery = deleteDatasource;
}
if (!deleteQuery) {
return;
}
const { code, msg } = await deleteQuery(nodeData?.uid);
if (code === 200) {
onNodeChange?.({ eventName: nodeData?.nodeType });
} else {
message.error(msg);
}
};
return (
<>
<Drawer
title={
<Space>
{nodeData?.name}
<TransTypeTag type={nodeData?.nodeType} />
</Space>
}
placement="right"
mask={false}
getContainer={false}
footer={
<div className="ant-drawer-extra">
<Space>
<Button
type="primary"
key="editBtn"
onClick={() => {
onEditBtnClick?.(nodeData);
}}
>
</Button>
<Popconfirm
title="确认删除?"
okText="是"
cancelText="否"
onConfirm={() => {
handleDeleteConfirm();
}}
>
<Button danger key="deleteBtn">
</Button>
</Popconfirm>
</Space>
</div>
}
{...restProps}
>
<div key={nodeData?.id} className={styles.nodeInfoDrawerContent}>
{infoList.map((item) => {
const { children, title } = item;
return (
<div key={title} style={{ display: item.hideItem ? 'none' : 'block' }}>
<p className={styles.title}>{title}</p>
{children.map((childrenItem) => {
return (
<Row
key={`${childrenItem.label}-${childrenItem.value}`}
style={{ marginBottom: 10, display: childrenItem.hideItem ? 'none' : 'flex' }}
>
<Col span={24}>
<DescriptionItem
title={childrenItem.label}
content={childrenItem.content || childrenItem.value}
/>
</Col>
</Row>
);
})}
<Divider />
</div>
);
})}
</div>
</Drawer>
</>
);
};
export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger,
}))(NodeInfoDrawer);

View File

@@ -1,6 +1,6 @@
import G6, { Graph } from '@antv/g6';
import { createDom } from '@antv/dom-util';
import { RefreshGraphData } from '../../data';
import { ToolBarSearchCallBack } from '../../data';
const searchIconSvgPath = `<path d="M909.6 854.5L649.9 594.8C690.2 542.7 712 479 712 412c0-80.2-31.3-155.4-87.9-212.1-56.6-56.7-132-87.9-212.1-87.9s-155.5 31.3-212.1 87.9C143.2 256.5 112 331.8 112 412c0 80.1 31.3 155.5 87.9 212.1C256.5 680.8 331.8 712 412 712c67 0 130.6-21.8 182.7-62l259.7 259.6a8.2 8.2 0 0011.6 0l43.6-43.5a8.2 8.2 0 000-11.6zM570.4 570.4C528 612.7 471.8 636 412 636s-116-23.3-158.4-65.6C211.3 528 188 471.8 188 412s23.3-116.1 65.6-158.4C296 211.3 352.2 188 412 188s116.1 23.2 158.4 65.6S636 352.2 636 412s-23.3 116.1-65.6 158.4z" />`;
// const searchNode = (graph) => {
@@ -27,64 +27,25 @@ const searchIconSvgPath = `<path d="M909.6 854.5L649.9 594.8C690.2 542.7 712 479
// }
// };
interface Node {
label: string;
children?: Node[];
}
function findNodesByLabel(query: string, nodes: Node[]): Node[] {
const result: Node[] = [];
for (const node of nodes) {
let match = false;
let children: Node[] = [];
// 如果节点的label包含查询字符串我们将其标记为匹配
if (node.label.includes(query)) {
match = true;
}
// 我们还需要在子节点中进行搜索
if (node.children) {
children = findNodesByLabel(query, node.children);
if (children.length > 0) {
match = true;
}
}
// 如果节点匹配或者其子节点匹配,我们将其添加到结果中
if (match) {
result.push({ ...node, children });
}
}
return result;
}
const searchNode = (graph: Graph, refreshGraphData?: RefreshGraphData) => {
const searchNode = (graph: Graph, onSearch?: ToolBarSearchCallBack) => {
const toolBarSearchInput = document.getElementById('toolBarSearchInput') as HTMLInputElement;
const searchText = toolBarSearchInput.value.trim();
const graphData = graph.get('initGraphData');
const filterChildrenData = findNodesByLabel(searchText, graphData.children);
refreshGraphData?.({
...graphData,
children: filterChildrenData,
});
onSearch?.(searchText);
};
const generatorSearchInputDom = (graph: Graph, refreshGraphData: RefreshGraphData) => {
const generatorSearchInputDom = (graph: Graph, onSearch: ToolBarSearchCallBack) => {
const domString =
'<input placeholder="请输入指标/维度名称" class="ant-input" id="toolBarSearchInput" type="text" value="" />';
const searchInputDom = createDom(domString);
searchInputDom.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
searchNode(graph, refreshGraphData);
searchNode(graph, onSearch);
}
});
return searchInputDom;
};
const generatorSearchBtnDom = (graph: Graph) => {
const generatorSearchBtnDom = (graph: Graph, onSearch: ToolBarSearchCallBack) => {
const domString = `<button
id="toolBarSearchBtn"
type="button"
@@ -106,14 +67,14 @@ const generatorSearchBtnDom = (graph: Graph) => {
</button>`;
const searchBtnDom = createDom(domString);
searchBtnDom.addEventListener('click', () => {
searchNode(graph);
searchNode(graph, onSearch);
});
return searchBtnDom;
};
const searchInputDOM = (graph: Graph, refreshGraphData: RefreshGraphData) => {
const searchInputDom = generatorSearchInputDom(graph, refreshGraphData);
const searchBtnDom = generatorSearchBtnDom(graph);
const searchInputDOM = (graph: Graph, onSearch: ToolBarSearchCallBack) => {
const searchInputDom = generatorSearchInputDom(graph, onSearch);
const searchBtnDom = generatorSearchBtnDom(graph, onSearch);
const searchInput = `
<div id="searchInputContent" class="g6-component-toolbar-search-input" style="position: absolute;top: 38px;width: 190px;left: 0;">
<span class="ant-input-group-wrapper ant-input-search" >
@@ -129,7 +90,7 @@ const searchInputDOM = (graph: Graph, refreshGraphData: RefreshGraphData) => {
return searchDom;
};
const initToolBar = ({ refreshGraphData }: { refreshGraphData: RefreshGraphData }) => {
const initToolBar = ({ onSearch }: { onSearch: ToolBarSearchCallBack }) => {
const toolBarInstance = new G6.ToolBar();
const config = toolBarInstance._cfgs;
const defaultContentDomString = config.getContent();
@@ -155,10 +116,10 @@ const initToolBar = ({ refreshGraphData }: { refreshGraphData: RefreshGraphData
defaultContentDom.insertAdjacentHTML('afterbegin', searchBtnDom);
let searchInputContentVisible = true;
const toolbar = new G6.ToolBar({
position: { x: 10, y: 10 },
position: { x: 20, y: 20 },
className: 'semantic-graph-toolbar',
getContent: (graph) => {
const searchInput = searchInputDOM(graph as Graph, refreshGraphData);
const searchInput = searchInputDOM(graph as Graph, onSearch);
const content = `<div class="g6-component-toolbar-content">${defaultContentDom.outerHTML}</div>`;
const contentDom = createDom(content);
contentDom.appendChild(searchInput);

View File

@@ -20,15 +20,17 @@ import initLegend from './components/Legend';
import { SemanticNodeType } from '../enum';
import G6 from '@antv/g6';
import { ISemantic, IDataSource } from '../data';
import NodeInfoDrawer from './components/NodeInfoDrawer';
import DimensionInfoModal from '../components/DimensionInfoModal';
import MetricInfoCreateForm from '../components/MetricInfoCreateForm';
import DeleteConfirmModal from './components/DeleteConfirmModal';
import ClassDataSourceTypeModal from '../components/ClassDataSourceTypeModal';
import GraphToolBar from './components/GraphToolBar';
import { cloneDeep } from 'lodash';
type Props = {
domainId: number;
graphShowType: SemanticNodeType;
graphShowType?: SemanticNodeType;
domainManger: StateType;
dispatch: Dispatch;
};
@@ -36,13 +38,18 @@ type Props = {
const DomainManger: React.FC<Props> = ({
domainManger,
domainId,
graphShowType = SemanticNodeType.DIMENSION,
// graphShowType = SemanticNodeType.DIMENSION,
graphShowType,
dispatch,
}) => {
const ref = useRef(null);
const dataSourceRef = useRef<ISemantic.IDomainSchemaRelaList>([]);
const [graphData, setGraphData] = useState<TreeGraphData>();
const [createDimensionModalVisible, setCreateDimensionModalVisible] = useState<boolean>(false);
const [createMetricModalVisible, setCreateMetricModalVisible] = useState<boolean>(false);
const [infoDrawerVisible, setInfoDrawerVisible] = useState<boolean>(false);
const [currentNodeData, setCurrentNodeData] = useState<any>();
const legendDataRef = useRef<any[]>([]);
const graphRef = useRef<any>(null);
@@ -53,12 +60,15 @@ const DomainManger: React.FC<Props> = ({
const [nodeDataSource, setNodeDataSource] = useState<any>();
const [dataSourceInfoList, setDataSourceInfoList] = useState<IDataSource.IDataSourceItem[]>([]);
const { dimensionList, metricList } = domainManger;
const dimensionListRef = useRef<ISemantic.IDimensionItem[]>([]);
const metricListRef = useRef<ISemantic.IMetricItem[]>([]);
const [confirmModalOpenState, setConfirmModalOpenState] = useState<boolean>(false);
const [createDataSourceModalOpen, setCreateDataSourceModalOpen] = useState(false);
// const toggleNodeVisibility = (graph: Graph, node: Item, visible: boolean) => {
// if (visible) {
@@ -84,8 +94,34 @@ const DomainManger: React.FC<Props> = ({
// }
// };
const changeGraphData = (data: IDataSource.IDataSourceItem[], type: SemanticNodeType) => {
const relationData = formatterRelationData(data, type);
const handleSeachNode = (text: string) => {
const filterData = dataSourceRef.current.reduce(
(data: ISemantic.IDomainSchemaRelaList, item: ISemantic.IDomainSchemaRelaItem) => {
const { dimensions, metrics } = item;
const dimensionsList = dimensions.filter((dimension) => {
return dimension.name.includes(text);
});
const metricsList = metrics.filter((metric) => {
return metric.name.includes(text);
});
data.push({
...item,
dimensions: dimensionsList,
metrics: metricsList,
});
return data;
},
[],
);
const rootGraphData = changeGraphData(filterData);
refreshGraphData(rootGraphData);
};
const changeGraphData = (
dataSourceList: ISemantic.IDomainSchemaRelaList,
type?: SemanticNodeType,
): TreeGraphData => {
const relationData = formatterRelationData({ dataSourceList, type, limit: 20 });
const legendList = relationData.map((item: any) => {
const { id, name } = item;
return {
@@ -101,7 +137,6 @@ const DomainManger: React.FC<Props> = ({
name: domainManger.selectDomainName,
children: relationData,
};
//
return graphRootData;
};
@@ -112,8 +147,14 @@ const DomainManger: React.FC<Props> = ({
const { code, data } = await getDomainSchemaRela(params.domainId);
if (code === 200) {
if (data) {
const graphRootData = changeGraphData(data, params.graphShowType || graphShowType);
setDataSourceInfoList(
data.map((item: ISemantic.IDomainSchemaRelaItem) => {
return item.datasource;
}),
);
const graphRootData = changeGraphData(data);
setGraphData(graphRootData);
dataSourceRef.current = data;
return graphRootData;
}
return false;
@@ -164,14 +205,17 @@ const DomainManger: React.FC<Props> = ({
if (!targetData) {
return;
}
const datasource = loopNodeFindDataSource(item);
if (datasource) {
setNodeDataSource({
...datasource,
id: datasource.uid,
name: datasource.name,
});
}
if (targetData.nodeType === SemanticNodeType.DATASOURCE) {
setCreateDataSourceModalOpen(true);
return;
}
if (targetData.nodeType === SemanticNodeType.DIMENSION) {
const targetItem = dimensionListRef.current.find((item) => item.id === targetData.uid);
if (targetItem) {
@@ -180,6 +224,7 @@ const DomainManger: React.FC<Props> = ({
} else {
message.error('获取维度初始化数据失败');
}
return;
}
if (targetData.nodeType === SemanticNodeType.METRIC) {
const targetItem = metricListRef.current.find((item) => item.id === targetData.uid);
@@ -189,22 +234,23 @@ const DomainManger: React.FC<Props> = ({
} else {
message.error('获取指标初始化数据失败');
}
return;
}
};
const handleContextMenuClickCreate = (item: IItemBaseConfig) => {
const handleContextMenuClickCreate = (item: IItemBaseConfig, key: string) => {
const datasource = item.model;
if (!datasource) {
return;
}
setNodeDataSource({
...datasource,
id: datasource.uid,
name: datasource.name,
});
if (graphShowType === SemanticNodeType.DIMENSION) {
if (key === 'createDimension') {
setCreateDimensionModalVisible(true);
}
if (graphShowType === SemanticNodeType.METRIC) {
if (key === 'createMetric') {
setCreateMetricModalVisible(true);
}
setDimensionItem(undefined);
@@ -216,10 +262,19 @@ const DomainManger: React.FC<Props> = ({
if (!targetData) {
return;
}
if (targetData.nodeType === SemanticNodeType.DATASOURCE) {
setCurrentNodeData({
...targetData,
id: targetData.uid,
});
setConfirmModalOpenState(true);
return;
}
if (targetData.nodeType === SemanticNodeType.DIMENSION) {
const targetItem = dimensionListRef.current.find((item) => item.id === targetData.uid);
if (targetItem) {
setDimensionItem({ ...targetItem });
// setDimensionItem({ ...targetItem });
setCurrentNodeData(targetItem);
setConfirmModalOpenState(true);
} else {
message.error('获取维度初始化数据失败');
@@ -228,7 +283,8 @@ const DomainManger: React.FC<Props> = ({
if (targetData.nodeType === SemanticNodeType.METRIC) {
const targetItem = metricListRef.current.find((item) => item.id === targetData.uid);
if (targetItem) {
setMetricItem({ ...targetItem });
// setMetricItem({ ...targetItem });
setCurrentNodeData(targetItem);
setConfirmModalOpenState(true);
} else {
message.error('获取指标初始化数据失败');
@@ -242,19 +298,27 @@ const DomainManger: React.FC<Props> = ({
}
switch (key) {
case 'edit':
case 'editDatasource':
handleContextMenuClickEdit(item._cfg);
break;
case 'delete':
case 'deleteDatasource':
handleContextMenuClickDelete(item._cfg);
break;
case 'create':
handleContextMenuClickCreate(item._cfg);
case 'createDimension':
case 'createMetric':
handleContextMenuClickCreate(item._cfg, key);
break;
default:
break;
}
};
const handleNodeTypeClick = (nodeData: any) => {
setCurrentNodeData(nodeData);
setInfoDrawerVisible(true);
};
const graphConfigMap = {
dendrogram: {
defaultEdge: {
@@ -308,7 +372,7 @@ const DomainManger: React.FC<Props> = ({
const graphConfigKey = graphNodeList.length > 20 ? 'dendrogram' : 'mindmap';
getLegendDataFilterFunctions();
const toolbar = initToolBar({ refreshGraphData });
const toolbar = initToolBar({ onSearch: handleSeachNode });
const tooltip = initTooltips();
const contextMenu = initContextMenu({
graphShowType,
@@ -325,14 +389,14 @@ const DomainManger: React.FC<Props> = ({
height,
modes: {
default: [
{
type: 'collapse-expand',
onChange: function onChange(item, collapsed) {
const data = item!.get('model');
data.collapsed = collapsed;
return true;
},
},
// {
// type: 'collapse-expand',
// onChange: function onChange(item, collapsed) {
// const data = item!.get('model');
// data.collapsed = collapsed;
// return true;
// },
// },
'drag-node',
'drag-canvas',
// 'activate-relations',
@@ -368,6 +432,8 @@ const DomainManger: React.FC<Props> = ({
plugins: [legend, tooltip, toolbar, contextMenu],
});
graphRef.current.set('initGraphData', graphData);
graphRef.current.set('initDataSource', dataSourceRef.current);
const legendCanvas = legend._cfgs.legendCanvas;
// legend模式事件方法bindEvents会有点击图例空白清空选中的逻辑在注册click事件前先将click事件队列清空
@@ -408,13 +474,34 @@ const DomainManger: React.FC<Props> = ({
label: node.name,
});
});
graphRef.current.data(graphData);
graphRef.current.render();
graphRef.current.fitView([80, 80]);
setAllActiveLegend(legend);
graphRef.current.on('node:click', (evt: any) => {
const item = evt.item; // 被操作的节点 item
const itemData = item?._cfg?.model;
if (itemData) {
const { nodeType } = itemData;
if (
[
SemanticNodeType.DIMENSION,
SemanticNodeType.METRIC,
SemanticNodeType.DATASOURCE,
].includes(nodeType)
) {
handleNodeTypeClick(itemData);
return;
}
}
});
graphRef.current.on('canvas:click', () => {
setInfoDrawerVisible(false);
});
const rootNode = graphRef.current.findById('root');
graphRef.current.hideItem(rootNode);
if (typeof window !== 'undefined')
@@ -442,18 +529,69 @@ const DomainManger: React.FC<Props> = ({
return (
<>
<GraphToolBar
onClick={({ eventName }: { eventName: string }) => {
setNodeDataSource(undefined);
if (eventName === 'createDatabase') {
setCreateDataSourceModalOpen(true);
}
if (eventName === 'createDimension') {
setCreateDimensionModalVisible(true);
setDimensionItem(undefined);
}
if (eventName === 'createMetric') {
setCreateMetricModalVisible(true);
setMetricItem(undefined);
}
}}
/>
<div
ref={ref}
key={`${domainId}-${graphShowType}`}
id="semanticGraph"
style={{ width: '100%', height: '100%' }}
style={{ width: '100%', height: 'calc(100vh - 175px)', position: 'relative' }}
/>
<NodeInfoDrawer
nodeData={currentNodeData}
placement="right"
onClose={() => {
setInfoDrawerVisible(false);
}}
open={infoDrawerVisible}
mask={false}
getContainer={false}
onEditBtnClick={(nodeData: any) => {
handleContextMenuClickEdit({ model: nodeData });
setInfoDrawerVisible(false);
}}
onNodeChange={({ eventName }: { eventName: string }) => {
updateGraphData();
setInfoDrawerVisible(false);
if (eventName === SemanticNodeType.METRIC) {
dispatch({
type: 'domainManger/queryMetricList',
payload: {
domainId,
},
});
}
if (eventName === SemanticNodeType.DIMENSION) {
dispatch({
type: 'domainManger/queryDimensionList',
payload: {
domainId,
},
});
}
}}
/>
{createDimensionModalVisible && (
<DimensionInfoModal
domainId={domainId}
bindModalVisible={createDimensionModalVisible}
dimensionItem={dimensionItem}
dataSourceList={nodeDataSource ? [nodeDataSource] : []}
dataSourceList={nodeDataSource ? [nodeDataSource] : dataSourceInfoList}
onSubmit={() => {
setCreateDimensionModalVisible(false);
updateGraphData();
@@ -473,7 +611,7 @@ const DomainManger: React.FC<Props> = ({
<MetricInfoCreateForm
domainId={domainId}
key={metricItem?.id}
datasourceId={nodeDataSource.id}
datasourceId={nodeDataSource?.id}
createModalVisible={createMetricModalVisible}
metricItem={metricItem}
onSubmit={() => {
@@ -491,6 +629,19 @@ const DomainManger: React.FC<Props> = ({
}}
/>
)}
{
<ClassDataSourceTypeModal
open={createDataSourceModalOpen}
onCancel={() => {
setNodeDataSource(undefined);
setCreateDataSourceModalOpen(false);
}}
dataSourceItem={nodeDataSource}
onSubmit={() => {
updateGraphData();
}}
/>
}
{
<DeleteConfirmModal
open={confirmModalOpenState}
@@ -514,8 +665,7 @@ const DomainManger: React.FC<Props> = ({
onCancelClick={() => {
setConfirmModalOpenState(false);
}}
nodeType={graphShowType}
nodeData={graphShowType === SemanticNodeType.DIMENSION ? dimensionItem : metricItem}
nodeData={currentNodeData}
/>
}
</>

View File

@@ -1,760 +1,22 @@
@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;
.nodeInfoDrawerContent {
.title {
margin-bottom: 12px;
font-size: 16px;
color: #0e73ff;
font-weight: bold;
.title {
flex: 1;
}
.icon {
display: flex;
align-items: center;
margin-right: 10px !important;
cursor: pointer;
position: relative;
&::before {
display: block;
position: absolute;
content: "";
left: -10px;
top: 7px;
height: 14px;
width: 3px;
font-size: 0;
background: #0e73ff;
border-radius: 2px;
border: 1px solid #0e73ff;
}
}
.paramsList {
.paramsItem {
display: flex;
padding: 10px;
:global {
.ant-list-item-action {
margin-left: 5px;
}
}
.name {
flex: 1;
width: 80%;
overflow: hidden;
font-size: 12px;
text-overflow: ellipsis;
&:hover {
cursor: pointer;
}
}
.icon {
// display: none;
margin-left: 10px;
}
}
}
// .paramsItem:hover {
// .icon {
// display: inline-block;
// margin-left: 8px;
// cursor: pointer;
// }
// }
}
.disableIcon {
vertical-align: super !important;
// color: rgba(0, 10, 36, 0.25);
background: #7d7f80 !important;
border-color: #7d7f80 !important;
:global {
.anticon .anticon-play-circle {
color: #fff;
}
}
&:hover {
cursor: not-allowed;
opacity: 1;
}
}
.sqlTaskListWrap {
position: relative;
width: 262px;
border-top: 0 !important;
border-radius: 0;
:global {
.ant-card-head {
min-height: 20px;
}
.ant-card-head-title {
padding: 8px 0;
}
}
}
.sqlTaskList {
position: absolute !important;
top: 42px;
right: 0;
bottom: 0;
left: 0;
overflow: auto;
}
.sqlBottmWrap {
// position: absolute;
// top: 484px;
// right: 0;
// bottom: 0;
// left: 0;
display: flex;
height: 100%;
// padding: 0 10px;
&:global(.small) {
top: 334px;
}
&:global(.middle) {
top: 384px;
}
}
.sqlResultWrap {
position: relative;
display: flex;
flex: 1;
flex-direction: column;
overflow: auto;
border: solid 1px @borderColor;
border-top: 0;
border-left: 0;
}
.sqlToolBar {
display: flex;
flex-direction: row-reverse;
align-items: center;
height: 41px;
padding: 5px 0;
text-align: right;
}
.sqlResultPane {
flex: 1;
border-top: solid 1px @borderColor;
}
.sqlToolBtn {
margin-right: 15px;
}
.runScriptBtn {
margin-right: 15px;
background-color: #e87954;
border-color: #e87954;
&:hover{
border-color: #f89878;
background: #f89878;
}
&:focus {
border-color: #f89878;
background: #f89878;
}
}
.taskFailed {
padding: 20px 20px 0 20px;
}
.sqlResultContent {
position: absolute;
top: 50%;
width: 100%;
color: rgba(0, 0, 0, 0.25);
font-size: 16px;
text-align: center;
}
.sqlResultLog {
padding: 20px;
word-wrap: break-word;
}
.tableList {
position: absolute !important;
top: 160px;
right: 0;
bottom: 26px;
left: 0;
overflow-x: hidden;
overflow-y: auto;
border-bottom: solid 1px @borderColor;
}
.tablePage {
position: absolute !important;
bottom: 0;
left: 0;
z-index: 1;
width: 100%;
min-width: 250px;
overflow: hidden;
}
.tableListItem {
width: 88%;
overflow: hidden;
font-size: 12px;
text-overflow: ellipsis;
&:hover {
cursor: pointer;
}
}
.tableItem {
&:global(.ant-list-item) {
padding: 6px 0 6px 6px;
}
:global(.ant-list-item-action) {
margin-left: 12px !important;
}
&:hover {
background: @hoverColor;
border-bottom: 1px solid #f0f0f0;
}
&:global(.active) {
background: @activeColor;
}
}
.taskIcon {
margin-right: 10px;
color: #1890ff;
font-size: 14px;
}
.taskSuccessIcon {
.taskIcon();
color: #67c23a;
}
.taskFailIcon {
.taskIcon();
color: #f56c6c;
}
.resultFailIcon {
margin-right: 8px;
color: #f56c6c;
}
.taskItem {
padding: 10px 8px !important;
font-size: 12px;
cursor: pointer;
&:global(.ant-list-item) {
justify-content: flex-start;
}
&:hover {
background: @hoverColor;
}
}
.activeTask {
background: @activeColor;
}
.resultTable {
width: 100%;
:global {
.ant-table-body {
width: 100%;
// max-height: none !important;
overflow: auto !important;
}
.ant-table-cell,
.resultTableRow > td {
padding: 8px;
font-size: 12px;
}
}
}
.taskLogWrap {
word-wrap: break-word;
}
.siteTagPlus {
background: #fff;
border-style: dashed;
}
.editTag {
margin-bottom: 5px;
user-select: none;
}
.tagInput {
width: 78px;
margin-right: 8px;
vertical-align: top;
}
.outside {
position: relative;
height: 100%;
}
.collapseRightBtn {
position: absolute;
top: calc(50% + 50px);
right: 0;
z-index: 100;
display: flex;
align-items: center;
height: 70px;
color: #fff;
font-size: 12px;
background-color: rgba(40, 46, 54, 0.2);
border-radius: 24px 0 0 24px;
cursor: pointer;
transition: all 0.3s ease;
}
.collapseLeftBtn {
position: absolute;
top: calc(50% + 45px);
left: 0;
z-index: 100;
display: flex;
align-items: center;
height: 70px;
color: #fff;
font-size: 12px;
background-color: rgba(40, 46, 54, 0.2);
border-radius: 0 24px 24px 0;
cursor: pointer;
transition: all 0.3s ease;
}
.detail {
.titleCollapse {
float: right;
padding-right: 18px;
color: #1890ff;
line-height: 35px;
text-align: right;
cursor: pointer;
}
.tableTitle {
display: inline-block;
width: 85%;
margin-left: 15px;
overflow: hidden;
line-height: 35px;
white-space: nowrap;
text-overflow: ellipsis;
cursor: pointer;
}
:global {
.ant-divider-horizontal {
margin: 0;
}
}
}
.search {
margin-left: 10px;
}
.middleArea {
:global {
.ant-tabs-nav .ant-tabs-tab {
border: none;
// background: #d9d9d96e;
border-right: 1px solid #f0f0f0;
border-radius: 0 !important;
}
.ant-tabs-nav-add {
border-radius: 0 !important;
}
.ant-tabs-tab {
.ant-tabs-tab-remove {
.closeTab {
opacity: 0;
}
.dot {
opacity: 1;
}
}
}
.ant-tabs-tab:hover {
.ant-tabs-tab-remove {
.closeTab {
opacity: 1 !important;
}
.dot {
opacity: 0;
}
}
}
}
}
.menu {
position: relative;
z-index: 1;
height: 100%;
padding: 5px;
overflow: hidden;
overflow-x: hidden;
overflow-y: auto;
:global {
.ant-form {
margin: -2px;
}
}
}
.menuList {
position: absolute !important;
top: 95px;
right: 0;
bottom: 26px;
left: 0;
overflow-x: hidden;
overflow-y: auto;
border-bottom: solid 1px @borderColor;
.menuItem {
&:global(.ant-list-item) {
padding: 6px 0 6px 14px;
}
:global(.ant-list-item-action) {
margin-left: 12px !important;
}
&:hover {
background: @hoverColor;
border-bottom: 1px solid #f0f0f0;
.icon {
display: block;
}
}
&:global(.active) {
background: @activeColor;
}
.menuListItem {
width: 90%;
overflow: hidden;
font-size: 12px;
white-space: nowrap;
text-overflow: ellipsis;
&:hover {
cursor: pointer;
}
}
.icon {
display: none;
margin-right: 15px !important;
cursor: pointer;
}
.menuIcon {
display: flex;
}
}
}
.scriptFile {
width: 100%;
margin: 10px;
overflow: hidden;
font-size: 14px;
white-space: nowrap;
text-overflow: ellipsis;
.icon {
margin-right: 10px;
}
}
.sqlScriptName {
width: 93% !important;
margin: 14px 0 0 14px !important;
}
.fileIcon {
width: 20px !important;
height: 20px !important;
padding-top: 2px !important;
padding-right: 5px !important;
vertical-align: middle;
}
.itemName {
vertical-align: middle;
}
.paneName {
width: 100px;
overflow: hidden;
font-size: 12px !important;
white-space: nowrap;
text-overflow: ellipsis;
}
.titleIcon {
width: 16px !important;
height: 16px !important;
margin: 0 3px 4px;
}
}

View File

@@ -1,5 +1,6 @@
import { ISemantic, IDataSource } from '../data';
import { ISemantic } from '../data';
import { SemanticNodeType } from '../enum';
import { TreeGraphData } from '@antv/g6-core';
export const typeConfigs = {
datasource: {
@@ -11,6 +12,7 @@ export const typeConfigs = {
export const getDimensionChildren = (
dimensions: ISemantic.IDimensionItem[],
dataSourceNodeId: string,
limit: number = 999,
) => {
const dimensionChildrenList = dimensions.reduce(
(dimensionChildren: any[], dimension: ISemantic.IDimensionItem) => {
@@ -19,7 +21,7 @@ export const getDimensionChildren = (
...dimension,
nodeType: SemanticNodeType.DIMENSION,
legendType: dataSourceNodeId,
id: `${SemanticNodeType.DIMENSION}-${id}`,
id: `${dataSourceNodeId}-${SemanticNodeType.DIMENSION}-${id}`,
uid: id,
style: {
lineWidth: 2,
@@ -31,10 +33,14 @@ export const getDimensionChildren = (
},
[],
);
return dimensionChildrenList;
return dimensionChildrenList.slice(0, limit);
};
export const getMetricChildren = (metrics: ISemantic.IMetricItem[], dataSourceNodeId: string) => {
export const getMetricChildren = (
metrics: ISemantic.IMetricItem[],
dataSourceNodeId: string,
limit: number = 999,
) => {
const metricsChildrenList = metrics.reduce(
(metricsChildren: any[], metric: ISemantic.IMetricItem) => {
const { id } = metric;
@@ -42,7 +48,7 @@ export const getMetricChildren = (metrics: ISemantic.IMetricItem[], dataSourceNo
...metric,
nodeType: SemanticNodeType.METRIC,
legendType: dataSourceNodeId,
id: `${SemanticNodeType.METRIC}-${id}`,
id: `${dataSourceNodeId}-${SemanticNodeType.METRIC}-${id}`,
uid: id,
style: {
lineWidth: 2,
@@ -54,40 +60,50 @@ export const getMetricChildren = (metrics: ISemantic.IMetricItem[], dataSourceNo
},
[],
);
return metricsChildrenList;
return metricsChildrenList.slice(0, limit);
};
export const formatterRelationData = (
dataSourceList: IDataSource.IDataSourceItem[],
type: SemanticNodeType = SemanticNodeType.DIMENSION,
) => {
const relationData = dataSourceList.reduce((relationList: any[], item: any) => {
const { datasource, dimensions, metrics } = item;
const { id } = datasource;
const dataSourceNodeId = `${SemanticNodeType.DATASOURCE}-${id}`;
let childrenList = [];
if (type === SemanticNodeType.METRIC) {
childrenList = getMetricChildren(metrics, dataSourceNodeId);
}
if (type === SemanticNodeType.DIMENSION) {
childrenList = getDimensionChildren(dimensions, dataSourceNodeId);
}
relationList.push({
...datasource,
legendType: dataSourceNodeId,
id: dataSourceNodeId,
uid: id,
nodeType: SemanticNodeType.DATASOURCE,
size: 40,
children: [...childrenList],
style: {
lineWidth: 2,
fill: '#BDEFDB',
stroke: '#5AD8A6',
},
});
return relationList;
}, []);
export const formatterRelationData = (params: {
dataSourceList: ISemantic.IDomainSchemaRelaList;
limit?: number;
type?: SemanticNodeType;
}): TreeGraphData[] => {
const { type, dataSourceList, limit } = params;
const relationData = dataSourceList.reduce(
(relationList: TreeGraphData[], item: ISemantic.IDomainSchemaRelaItem) => {
const { datasource, dimensions, metrics } = item;
const { id } = datasource;
const dataSourceNodeId = `${SemanticNodeType.DATASOURCE}-${id}`;
let childrenList = [];
if (type === SemanticNodeType.METRIC) {
childrenList = getMetricChildren(metrics, dataSourceNodeId, limit);
}
if (type === SemanticNodeType.DIMENSION) {
childrenList = getDimensionChildren(dimensions, dataSourceNodeId, limit);
}
if (!type) {
const dimensionList = getDimensionChildren(dimensions, dataSourceNodeId, limit);
const metricList = getMetricChildren(metrics, dataSourceNodeId, limit);
childrenList = [...dimensionList, ...metricList];
}
relationList.push({
...datasource,
legendType: dataSourceNodeId,
id: dataSourceNodeId,
uid: id,
nodeType: SemanticNodeType.DATASOURCE,
size: 40,
children: [...childrenList],
style: {
lineWidth: 2,
fill: '#BDEFDB',
stroke: '#5AD8A6',
},
});
return relationList;
},
[],
);
return relationData;
};
@@ -121,6 +137,11 @@ export const getNodeConfigByType = (nodeData: any, defaultConfig = {}) => {
case SemanticNodeType.METRIC:
return {
...defaultConfig,
style: {
lineWidth: 2,
fill: '#fffbe6',
stroke: '#ffe58f',
},
labelCfg: { position: 'right', ...labelCfg },
};
default:
@@ -137,3 +158,37 @@ export const flatGraphDataNode = (graphData: any[]) => {
return nodeList;
}, []);
};
interface Node {
label: string;
children?: Node[];
}
export const findNodesByLabel = (query: string, nodes: Node[]): Node[] => {
const result: Node[] = [];
for (const node of nodes) {
let match = false;
let children: Node[] = [];
// 如果节点的label包含查询字符串我们将其标记为匹配
if (node.label.includes(query)) {
match = true;
}
// 我们还需要在子节点中进行搜索
if (node.children) {
children = findNodesByLabel(query, node.children);
if (children.length > 0) {
match = true;
}
}
// 如果节点匹配或者其子节点匹配,我们将其添加到结果中
if (match) {
result.push({ ...node, children });
}
}
return result;
};

View File

@@ -12,11 +12,11 @@ type Props = {
};
const SemanticGraphCanvas: React.FC<Props> = ({ domainManger }) => {
const [graphShowType, setGraphShowType] = useState<SemanticNodeType>(SemanticNodeType.DATASOURCE);
const [graphShowType, setGraphShowType] = useState<SemanticNodeType>(SemanticNodeType.DIMENSION);
const { selectDomainId } = domainManger;
return (
<div className={styles.semanticGraphCanvas}>
<div className={styles.toolbar}>
{/* <div className={styles.toolbar}>
<Radio.Group
buttonStyle="solid"
value={graphShowType}
@@ -29,7 +29,7 @@ const SemanticGraphCanvas: React.FC<Props> = ({ domainManger }) => {
<Radio.Button value={SemanticNodeType.DIMENSION}>维度</Radio.Button>
<Radio.Button value={SemanticNodeType.METRIC}>指标</Radio.Button>
</Radio.Group>
</div>
</div> */}
<div className={styles.canvasContainer}>
{graphShowType === SemanticNodeType.DATASOURCE ? (
@@ -37,8 +37,8 @@ const SemanticGraphCanvas: React.FC<Props> = ({ domainManger }) => {
<SemanticFlow />
</div>
) : (
<div style={{ width: '100%', height: 'calc(100vh - 220px)' }}>
<SemanticGraph domainId={selectDomainId} graphShowType={graphShowType} />
<div style={{ width: '100%' }}>
<SemanticGraph domainId={selectDomainId} />
</div>
)}
</div>

View File

@@ -1,31 +1,23 @@
import type { ActionType, ProColumns } from '@ant-design/pro-table';
import ProTable from '@ant-design/pro-table';
import { message, Button, Drawer, Space, Popconfirm, Modal, Card, Row, Col } from 'antd';
import { ConsoleSqlOutlined, CoffeeOutlined } from '@ant-design/icons';
import React, { useRef, useState, useEffect } from 'react';
import { message, Button, Space, Popconfirm } from 'antd';
import React, { useRef, useState } from 'react';
import type { Dispatch } from 'umi';
import { connect } from 'umi';
import DataSourceCreateForm from '../Datasource/components/DataSourceCreateForm';
import ClassDataSourceTypeModal from './ClassDataSourceTypeModal';
import type { StateType } from '../model';
import { getDatasourceList, deleteDatasource } from '../service';
import DataSource from '../Datasource';
import moment from 'moment';
const { Meta } = Card;
type Props = {
dispatch: Dispatch;
domainManger: StateType;
};
const ClassDataSourceTable: React.FC<Props> = ({ dispatch, domainManger }) => {
const { selectDomainId, dataBaseResultColsMap, dataBaseConfig } = domainManger;
const [createModalVisible, setCreateModalVisible] = useState<boolean>(false);
const { selectDomainId } = domainManger;
const [dataSourceItem, setDataSourceItem] = useState<any>();
const [createDataSourceModalOpen, setCreateDataSourceModalOpen] = useState(false);
const [dataSourceModalVisible, setDataSourceModalVisible] = useState(false);
const [fastModeSql, setFastModeSql] = useState<string>('');
const [fastModeTableName, setFastModeTableName] = useState<string>('');
const actionRef = useRef<ActionType>();
@@ -70,11 +62,7 @@ const ClassDataSourceTable: React.FC<Props> = ({ dispatch, domainManger }) => {
key="classEditBtn"
onClick={() => {
setDataSourceItem(record);
if (record.datasourceDetail.queryType === 'table_query') {
setDataSourceModalVisible(true);
return;
}
setCreateModalVisible(true);
setCreateDataSourceModalOpen(true);
}}
>
@@ -133,25 +121,10 @@ const ClassDataSourceTable: React.FC<Props> = ({ dispatch, domainManger }) => {
return resData;
};
const queryDataBaseExcuteSql = (tableName: string) => {
const sql = `select * from ${tableName}`;
setFastModeSql(sql);
setFastModeTableName(tableName);
dispatch({
type: 'domainManger/queryDataBaseExcuteSql',
payload: {
sql,
domainId: selectDomainId,
tableName,
},
});
};
return (
<>
<ProTable
actionRef={actionRef}
headerTitle="数据源列表"
rowKey="id"
columns={columns}
params={{ domainId: selectDomainId }}
@@ -179,59 +152,12 @@ const ClassDataSourceTable: React.FC<Props> = ({ dispatch, domainManger }) => {
onCancel={() => {
setCreateDataSourceModalOpen(false);
}}
onTypeChange={(type) => {
if (type === 'fast') {
setDataSourceModalVisible(true);
} else {
setCreateModalVisible(true);
}
setCreateDataSourceModalOpen(false);
dataSourceItem={dataSourceItem}
onSubmit={() => {
actionRef.current?.reload();
}}
/>
}
{dataSourceModalVisible && (
<DataSourceCreateForm
sql={fastModeSql}
basicInfoFormMode="fast"
domainId={Number(selectDomainId)}
dataSourceItem={dataSourceItem}
onCancel={() => {
setDataSourceModalVisible(false);
}}
onDataBaseTableChange={(tableName: string) => {
queryDataBaseExcuteSql(tableName);
}}
onSubmit={() => {
setDataSourceModalVisible(false);
setDataSourceItem(undefined);
actionRef.current?.reload();
}}
createModalVisible={dataSourceModalVisible}
/>
)}
{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>
)}
</>
);
};

View File

@@ -1,28 +1,69 @@
import { Modal, Card, Row, Col, Result, Button } from 'antd';
import { Button, Drawer, Result, Modal, Card, Row, Col } from 'antd';
import { ConsoleSqlOutlined, CoffeeOutlined } from '@ant-design/icons';
import React, { useState, useEffect } from 'react';
import { history, connect } from 'umi';
import type { Dispatch } from 'umi';
import { history, connect } from 'umi';
import DataSourceCreateForm from '../Datasource/components/DataSourceCreateForm';
import type { StateType } from '../model';
import DataSource from '../Datasource';
import { IDataSource } from '../data';
const { Meta } = Card;
type Props = {
open: boolean;
domainManger: StateType;
onTypeChange: (type: 'fast' | 'normal') => void;
dataSourceItem: IDataSource.IDataSourceItem;
onTypeChange?: (type: 'fast' | 'normal') => void;
onSubmit?: () => void;
onCancel?: () => void;
dispatch: Dispatch;
domainManger: StateType;
};
const ClassDataSourceTypeModal: React.FC<Props> = ({
open,
onTypeChange,
onSubmit,
dataSourceItem,
domainManger,
onCancel,
dispatch,
}) => {
const { selectDomainId, dataBaseConfig } = domainManger;
const [createModalVisible, setCreateModalVisible] = useState<boolean>(false);
const [dataSourceModalVisible, setDataSourceModalVisible] = useState(false);
const [fastModeSql, setFastModeSql] = useState<string>('');
const [createDataSourceModalOpen, setCreateDataSourceModalOpen] = useState(false);
useEffect(() => {
setCreateDataSourceModalOpen(open);
}, [open]);
if (!dataSourceItem || !open) {
setCreateDataSourceModalOpen(open);
return;
}
if (dataSourceItem?.datasourceDetail?.queryType === 'table_query') {
setDataSourceModalVisible(true);
} else {
setCreateModalVisible(true);
}
}, [dataSourceItem, open]);
const queryDataBaseExcuteSql = (tableName: string) => {
const sql = `select * from ${tableName}`;
setFastModeSql(sql);
dispatch({
type: 'domainManger/queryDataBaseExcuteSql',
payload: {
sql,
domainId: selectDomainId,
tableName,
},
});
};
const handleCancel = () => {
onCancel?.();
};
return (
<>
@@ -30,7 +71,7 @@ const ClassDataSourceTypeModal: React.FC<Props> = ({
open={createDataSourceModalOpen}
onCancel={() => {
setCreateDataSourceModalOpen(false);
onCancel?.();
handleCancel();
}}
footer={null}
centered
@@ -43,8 +84,10 @@ const ClassDataSourceTypeModal: React.FC<Props> = ({
hoverable
style={{ height: 220 }}
onClick={() => {
onTypeChange('fast');
onTypeChange?.('fast');
setCreateDataSourceModalOpen(false);
setDataSourceModalVisible(true);
}}
cover={
<CoffeeOutlined
@@ -59,8 +102,10 @@ const ClassDataSourceTypeModal: React.FC<Props> = ({
<Col span={12}>
<Card
onClick={() => {
onTypeChange('normal');
onTypeChange?.('normal');
setCreateDataSourceModalOpen(false);
setCreateModalVisible(true);
}}
hoverable
style={{ height: 220 }}
@@ -93,10 +138,52 @@ const ClassDataSourceTypeModal: React.FC<Props> = ({
/>
)}
</Modal>
{dataSourceModalVisible && (
<DataSourceCreateForm
sql={fastModeSql}
basicInfoFormMode="fast"
domainId={Number(selectDomainId)}
dataSourceItem={dataSourceItem}
onCancel={() => {
setDataSourceModalVisible(false);
handleCancel();
}}
onDataBaseTableChange={(tableName: string) => {
queryDataBaseExcuteSql(tableName);
}}
onSubmit={() => {
setDataSourceModalVisible(false);
onSubmit?.();
}}
createModalVisible={dataSourceModalVisible}
/>
)}
{createModalVisible && (
<Drawer
width={'100%'}
destroyOnClose
title="数据源编辑"
open={true}
onClose={() => {
setCreateModalVisible(false);
handleCancel();
}}
footer={null}
>
<DataSource
initialValues={dataSourceItem}
domainId={Number(selectDomainId)}
onSubmitSuccess={() => {
setCreateModalVisible(false);
onSubmit?.();
}}
/>
</Drawer>
)}
</>
);
};
export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger,
}))(ClassDataSourceTypeModal);

View File

@@ -0,0 +1,101 @@
import { Modal, Card, Row, Col, Result, Button } from 'antd';
import { ConsoleSqlOutlined, CoffeeOutlined } from '@ant-design/icons';
import React, { useState, useEffect } from 'react';
import { history, connect } from 'umi';
import type { StateType } from '../model';
const { Meta } = Card;
type Props = {
open: boolean;
domainManger: StateType;
onTypeChange: (type: 'fast' | 'normal') => void;
onCancel?: () => void;
};
const ClassDataSourceTypeModal: React.FC<Props> = ({
open,
onTypeChange,
domainManger,
onCancel,
}) => {
const { selectDomainId, dataBaseConfig } = domainManger;
const [createDataSourceModalOpen, setCreateDataSourceModalOpen] = useState(false);
useEffect(() => {
setCreateDataSourceModalOpen(open);
}, [open]);
return (
<>
<Modal
open={createDataSourceModalOpen}
onCancel={() => {
setCreateDataSourceModalOpen(false);
onCancel?.();
}}
footer={null}
centered
closable={false}
>
{dataBaseConfig && dataBaseConfig.id ? (
<Row gutter={16} style={{ marginTop: '0px' }}>
<Col span={12}>
<Card
hoverable
style={{ height: 220 }}
onClick={() => {
onTypeChange('fast');
setCreateDataSourceModalOpen(false);
}}
cover={
<CoffeeOutlined
width={240}
style={{ paddingTop: '45px', height: 120, fontSize: '48px', color: '#1890ff' }}
/>
}
>
<Meta title="快速创建" description="自动进行数据源可视化创建" />
</Card>
</Col>
<Col span={12}>
<Card
onClick={() => {
onTypeChange('normal');
setCreateDataSourceModalOpen(false);
}}
hoverable
style={{ height: 220 }}
cover={
<ConsoleSqlOutlined
style={{ paddingTop: '45px', height: 120, fontSize: '48px', color: '#1890ff' }}
/>
}
>
<Meta title="SQL脚本" description="自定义SQL脚本创建数据源" />
</Card>
</Col>
</Row>
) : (
<Result
status="warning"
subTitle="创建数据源需要先完成数据库设置"
extra={
<Button
type="primary"
key="console"
onClick={() => {
history.replace(`/semanticModel/${selectDomainId}/dataBase`);
onCancel?.();
}}
>
</Button>
}
/>
)}
</Modal>
</>
);
};
export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger,
}))(ClassDataSourceTypeModal);

View File

@@ -8,6 +8,7 @@ import type { StateType } from '../model';
import { SENSITIVE_LEVEL_ENUM } from '../constant';
import { getDatasourceList, getDimensionList, deleteDimension } from '../service';
import DimensionInfoModal from './DimensionInfoModal';
import DimensionValueSettingModal from './DimensionValueSettingModal';
import { ISemantic } from '../data';
import moment from 'moment';
import styles from './style.less';
@@ -22,6 +23,11 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
const [createModalVisible, setCreateModalVisible] = useState<boolean>(false);
const [dimensionItem, setDimensionItem] = useState<ISemantic.IDimensionItem>();
const [dataSourceList, setDataSourceList] = useState<any[]>([]);
const [dimensionValueSettingList, setDimensionValueSettingList] = useState<
ISemantic.IDimensionValueSettingItem[]
>([]);
const [dimensionValueSettingModalVisible, setDimensionValueSettingModalVisible] =
useState<boolean>(false);
const [pagination, setPagination] = useState({
current: 1,
pageSize: 20,
@@ -141,6 +147,20 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
>
</a>
<a
key="classEditBtn"
onClick={() => {
setDimensionItem(record);
setDimensionValueSettingModalVisible(true);
if (Array.isArray(record.dimValueMaps)) {
setDimensionValueSettingList(record.dimValueMaps);
} else {
setDimensionValueSettingList([]);
}
}}
>
</a>
<Popconfirm
title="确认删除?"
okText="是"
@@ -175,7 +195,7 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
<ProTable
className={`${styles.classTable} ${styles.classTableSelectColumnAlignLeft}`}
actionRef={actionRef}
headerTitle="维度列表"
// headerTitle="维度列表"
rowKey="id"
columns={columns}
request={queryDimensionList}
@@ -236,6 +256,26 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
}}
/>
)}
{dimensionValueSettingModalVisible && (
<DimensionValueSettingModal
dimensionValueSettingList={dimensionValueSettingList}
open={dimensionValueSettingModalVisible}
dimensionItem={dimensionItem}
onCancel={() => {
setDimensionValueSettingModalVisible(false);
}}
onSubmit={() => {
actionRef?.current?.reload();
dispatch({
type: 'domainManger/queryDimensionList',
payload: {
domainId: selectDomainId,
},
});
setDimensionValueSettingModalVisible(false);
}}
/>
)}
</>
);
};

View File

@@ -166,43 +166,12 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
},
];
// 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="指标列表"
// headerTitle="指标列表"
rowKey="id"
search={{
span: 4,

View File

@@ -0,0 +1,166 @@
import React, { useState, useEffect } from 'react';
import { List, Collapse, Button, Input } from 'antd';
import { uuid } from '@/utils/utils';
import styles from './style.less';
const { Panel } = Collapse;
const { TextArea } = Input;
type Props = {
title?: string;
defaultCollapse?: boolean;
value?: string[];
onChange?: (list: string[]) => void;
};
type ListItem = {
id: string;
sql: string;
};
type List = ListItem[];
const CommonEditList: React.FC<Props> = ({ title, defaultCollapse = false, value, onChange }) => {
const [listDataSource, setListDataSource] = useState<List>([]);
const [currentSql, setCurrentSql] = useState<string>('');
const [activeKey, setActiveKey] = useState<string>();
const [currentRecord, setCurrentRecord] = useState<ListItem>();
useEffect(() => {
if (Array.isArray(value)) {
const list = value.map((sql: string) => {
return {
id: uuid(),
sql,
};
});
setListDataSource(list);
}
}, [value]);
const handleListChange = (listDataSource: List) => {
const sqlList = listDataSource.map((item) => {
return item.sql;
});
onChange?.(sqlList);
};
return (
<div className={styles.commonEditList}>
<Collapse
activeKey={activeKey}
defaultActiveKey={defaultCollapse ? ['editor'] : undefined}
onChange={() => {}}
ghost
>
<Panel
header={title}
key="editor"
extra={
activeKey ? (
<Button
key="saveBtn"
type="primary"
onClick={() => {
if (!currentRecord && !currentSql) {
setActiveKey(undefined);
return;
}
if (currentRecord) {
const list = [...listDataSource].map((item) => {
if (item.id === currentRecord.id) {
return {
...item,
sql: currentSql,
};
}
return item;
});
setListDataSource(list);
handleListChange(list);
} else {
const list = [
...listDataSource,
{
id: uuid(),
sql: currentSql,
},
];
setListDataSource(list);
handleListChange(list);
}
setActiveKey(undefined);
}}
>
</Button>
) : (
<Button
type="primary"
key="createBtn"
onClick={() => {
setCurrentRecord(undefined);
setCurrentSql('');
setActiveKey('editor');
}}
>
</Button>
)
}
showArrow={false}
>
<div>
<TextArea
placeholder="请输入推荐问题"
value={currentSql}
style={{ height: 150 }}
minLength={5}
onChange={(e) => {
setCurrentSql(e.target.value);
}}
/>
</div>
</Panel>
</Collapse>
<List
itemLayout="horizontal"
dataSource={listDataSource || []}
renderItem={(item) => (
<List.Item
actions={[
<a
key="list-loadmore-edit"
onClick={() => {
setCurrentSql(item.sql);
setCurrentRecord(item);
setActiveKey('editor');
}}
>
</a>,
<a
key="list-loadmore-more"
onClick={() => {
const list = [...listDataSource].filter(({ id }) => {
return item.id !== id;
});
handleListChange(list);
setListDataSource(list);
}}
>
</a>,
]}
>
<List.Item.Meta title={item.sql} />
</List.Item>
)}
/>
</div>
);
};
export default CommonEditList;

View File

@@ -9,7 +9,7 @@ type Props = {
title?: string;
tableDataSource: any[];
columnList: any[];
rowKey: string;
rowKey?: string;
editableProTableProps?: any;
onDataSourceChange?: (dataSource: any) => void;
extenderCtrlColumn?: (text, record, _, action) => ReactNode[];
@@ -35,6 +35,7 @@ const CommonEditTable: React.FC<Props> = forwardRef(
}: Props,
ref: Ref<any>,
) => {
const defaultRowKey = rowKey || 'editRowId';
const [dataSource, setDataSource] = useState<any[]>(tableDataSource);
const actionRef = useRef<ActionType>();
@@ -50,7 +51,7 @@ const CommonEditTable: React.FC<Props> = forwardRef(
tableDataSource.map((item: any) => {
return {
...item,
editRowId: item[rowKey] || (Math.random() * 1000000).toFixed(0),
editRowId: item[defaultRowKey] || (Math.random() * 1000000).toFixed(0),
};
}),
);
@@ -82,7 +83,9 @@ const CommonEditTable: React.FC<Props> = forwardRef(
<a
key="deleteBtn"
onClick={() => {
const data = [...dataSource].filter((item) => item[rowKey] !== record[rowKey]);
const data = [...dataSource].filter(
(item) => item[defaultRowKey] !== record[defaultRowKey],
);
setDataSource(data);
handleDataSourceChange(data);
}}
@@ -111,7 +114,7 @@ const CommonEditTable: React.FC<Props> = forwardRef(
key={title}
actionRef={actionRef}
headerTitle={title}
rowKey={'editRowId'}
rowKey={defaultRowKey}
columns={columns}
value={dataSource}
tableAlertRender={() => {
@@ -133,9 +136,9 @@ const CommonEditTable: React.FC<Props> = forwardRef(
}}
editable={{
onSave: (_, row) => {
const rowKeyValue = row[rowKey];
const rowKeyValue = row[defaultRowKey];
const isSame = dataSource.filter((item: any, index: number) => {
return index !== row.index && item[rowKey] === rowKeyValue;
return index !== row.index && item[defaultRowKey] === rowKeyValue;
});
if (isSame[0]) {
message.error('存在重复值');

View File

@@ -20,15 +20,8 @@ const DatabaseCreateForm: ForwardRefRenderFunction<any, Props> = (
) => {
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('数据库配置获取错误');
// };
const [testLoading, setTestLoading] = useState<boolean>(false);
useEffect(() => {
form.resetFields();
@@ -36,11 +29,6 @@ const DatabaseCreateForm: ForwardRefRenderFunction<any, Props> = (
setSelectedDbType(dataBaseConfig?.type);
}, [dataBaseConfig]);
// useEffect(() => {
// form.resetFields();
// // queryDatabaseConfig();
// }, [domainId]);
const getFormValidateFields = async () => {
return await form.validateFields();
};
@@ -65,10 +53,12 @@ const DatabaseCreateForm: ForwardRefRenderFunction<any, Props> = (
};
const testDatabaseConnection = async () => {
const values = await form.validateFields();
setTestLoading(true);
const { code, data } = await testDatabaseConnect({
...values,
domainId,
});
setTestLoading(false);
if (code === 200 && data) {
message.success('连接测试通过');
return;
@@ -138,7 +128,9 @@ const DatabaseCreateForm: ForwardRefRenderFunction<any, Props> = (
<FormItem name="database" label="数据库名称">
<Input placeholder="请输入数据库名称" />
</FormItem>
<FormItem name="version" label="数据库版本">
<Input placeholder="请输入数据库版本" />
</FormItem>
<FormItem name="description" label="描述">
<TextArea placeholder="请输入数据库描述" style={{ height: 100 }} />
</FormItem>
@@ -146,6 +138,7 @@ const DatabaseCreateForm: ForwardRefRenderFunction<any, Props> = (
<Space>
<Button
type="primary"
loading={testLoading}
onClick={() => {
testDatabaseConnection();
}}

View File

@@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
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';
@@ -6,6 +6,7 @@ import SqlEditor from '@/components/SqlEditor';
import InfoTagList from './InfoTagList';
import { ISemantic } from '../data';
import { createDimension, updateDimension } from '../service';
// import DimensionValueSettingModal from './DimensionValueSettingModal';
import { message } from 'antd';
export type CreateFormProps = {
@@ -31,16 +32,28 @@ const DimensionInfoModal: React.FC<CreateFormProps> = ({
onSubmit: handleUpdate,
}) => {
const isEdit = !!dimensionItem?.id;
const [dimensionValueSettingList, setDimensionValueSettingList] = useState<
ISemantic.IDimensionValueSettingItem[]
>([]);
const [form] = Form.useForm();
const { setFieldsValue, resetFields } = form;
const handleSubmit = async () => {
// const [dimensionValueSettingModalVisible, setDimensionValueSettingModalVisible] =
// useState<boolean>(false);
const handleSubmit = async (
isSilenceSubmit = false,
dimValueMaps?: ISemantic.IDimensionValueSettingItem[],
) => {
const fieldsValue = await form.validateFields();
await saveDimension(fieldsValue);
await saveDimension(
{
...fieldsValue,
dimValueMaps: dimValueMaps || dimensionValueSettingList,
},
isSilenceSubmit,
);
};
const saveDimension = async (fieldsValue: any) => {
const saveDimension = async (fieldsValue: any, isSilenceSubmit = false) => {
const queryParams = {
domainId,
type: 'categorical',
@@ -52,8 +65,10 @@ const DimensionInfoModal: React.FC<CreateFormProps> = ({
}
const { code, msg } = await saveDimensionQuery(queryParams);
if (code === 200) {
message.success('编辑维度成功');
handleUpdate(fieldsValue);
if (!isSilenceSubmit) {
message.success('编辑维度成功');
handleUpdate(fieldsValue);
}
return;
}
message.error(msg);
@@ -66,6 +81,11 @@ const DimensionInfoModal: React.FC<CreateFormProps> = ({
useEffect(() => {
if (dimensionItem) {
setFormVal();
if (Array.isArray(dimensionItem.dimValueMaps)) {
setDimensionValueSettingList(dimensionItem.dimValueMaps);
} else {
setDimensionValueSettingList([]);
}
} else {
resetFields();
}
@@ -78,7 +98,12 @@ const DimensionInfoModal: React.FC<CreateFormProps> = ({
return (
<>
<Button onClick={onCancel}></Button>
<Button type="primary" onClick={handleSubmit}>
<Button
type="primary"
onClick={() => {
handleSubmit();
}}
>
</Button>
</>
@@ -93,20 +118,22 @@ const DimensionInfoModal: React.FC<CreateFormProps> = ({
</FormItem>
<FormItem
name="name"
label="维度中文名"
rules={[{ required: true, message: '请输入维度中文名' }]}
label="维度名"
rules={[{ required: true, message: '请输入维度名' }]}
>
<Input placeholder="名称不可重复" />
</FormItem>
<FormItem
hidden={isEdit}
name="bizName"
label="维度英文名"
rules={[{ required: true, message: '请输入维度英文名' }]}
label="字段名称"
rules={[{ required: true, message: '请输入字段名称' }]}
>
<Input placeholder="名称不可重复" disabled={isEdit} />
</FormItem>
<FormItem
hidden={isEdit}
name="datasourceId"
label="所属数据源"
rules={[{ required: true, message: '请选择所属数据源' }]}
@@ -158,6 +185,16 @@ const DimensionInfoModal: React.FC<CreateFormProps> = ({
>
<TextArea placeholder="请输入维度描述" />
</FormItem>
{/* <FormItem name="dimValueMaps" label="维度值设置">
<Button
type="primary"
onClick={() => {
setDimensionValueSettingModalVisible(true);
}}
>
设置
</Button>
</FormItem> */}
<FormItem
name="expr"
label="表达式"
@@ -171,28 +208,38 @@ const DimensionInfoModal: React.FC<CreateFormProps> = ({
};
return (
<Modal
width={800}
destroyOnClose
title="维度信息"
style={{ top: 48 }}
maskClosable={false}
open={bindModalVisible}
footer={renderFooter()}
onCancel={onCancel}
>
<Form
{...formLayout}
form={form}
initialValues={
{
// ...formVals,
}
}
<>
<Modal
width={800}
destroyOnClose
title="维度信息"
style={{ top: 48 }}
maskClosable={false}
open={bindModalVisible}
footer={renderFooter()}
onCancel={onCancel}
>
{renderContent()}
</Form>
</Modal>
<Form {...formLayout} form={form}>
{renderContent()}
</Form>
</Modal>
{/* {dimensionValueSettingModalVisible && (
<DimensionValueSettingModal
dimensionValueSettingList={dimensionValueSettingList}
open={dimensionValueSettingModalVisible}
onCancel={() => {
setDimensionValueSettingModalVisible(false);
}}
onSubmit={(dimValueMaps) => {
if (isEdit) {
handleSubmit(true, dimValueMaps);
}
setDimensionValueSettingList(dimValueMaps);
setDimensionValueSettingModalVisible(false);
}}
/>
)} */}
</>
);
};

View File

@@ -0,0 +1,156 @@
import React, { useEffect, useState } from 'react';
import { Button, Modal, message } from 'antd';
import { ISemantic } from '../data';
import CommonEditTable from './CommonEditTable';
import { createDimension, updateDimension } from '../service';
import { connect } from 'umi';
import type { StateType } from '../model';
export type CreateFormProps = {
dimensionValueSettingList: ISemantic.IDimensionValueSettingItem[];
onCancel: () => void;
dimensionItem?: ISemantic.IDimensionItem;
open: boolean;
onSubmit: (values?: any) => void;
domainManger: StateType;
};
type TableDataSource = { techName: string; bizName: string; alias: string };
const DimensionInfoModal: React.FC<CreateFormProps> = ({
onCancel,
open,
dimensionItem,
dimensionValueSettingList,
domainManger,
onSubmit,
}) => {
const [tableDataSource, setTableDataSource] = useState<TableDataSource[]>([]);
const { selectDomainId } = domainManger;
const [dimValueMaps, setDimValueMaps] = useState<ISemantic.IDimensionValueSettingItem[]>([]);
useEffect(() => {
const dataSource = dimensionValueSettingList.map((item) => {
const { alias } = item;
return {
...item,
alias: Array.isArray(alias) ? alias.join(',') : '',
};
});
setTableDataSource(dataSource);
setDimValueMaps(dimensionValueSettingList);
}, [dimensionValueSettingList]);
const handleSubmit = async () => {
await saveDimension({ dimValueMaps });
onSubmit?.(dimValueMaps);
};
const saveDimension = async (fieldsValue: any) => {
if (!dimensionItem?.id) {
return;
}
const queryParams = {
domainId: selectDomainId,
id: dimensionItem.id,
...fieldsValue,
};
const { code, msg } = await updateDimension(queryParams);
if (code === 200) {
return;
}
message.error(msg);
};
const renderFooter = () => {
return (
<>
<Button onClick={onCancel}></Button>
<Button
type="primary"
onClick={() => {
handleSubmit();
}}
>
</Button>
</>
);
};
const columns = [
{
title: '技术名称',
dataIndex: 'techName',
width: 200,
formItemProps: {
fieldProps: {
placeholder: '请填写技术名称',
},
rules: [
{
required: true,
whitespace: true,
message: '此项是必填项',
},
],
},
},
{
title: '业务名称',
dataIndex: 'bizName',
width: 200,
fieldProps: {
placeholder: '请填写业务名称',
},
formItemProps: {
rules: [
{
required: true,
whitespace: true,
message: '此项是必填项',
},
],
},
},
{
title: '别名',
dataIndex: 'alias',
fieldProps: {
placeholder: '多个别名用英文逗号隔开',
},
},
];
return (
<Modal
width={1000}
destroyOnClose
title="维度值设置"
style={{ top: 48 }}
maskClosable={false}
open={open}
footer={renderFooter()}
onCancel={onCancel}
>
<CommonEditTable
tableDataSource={tableDataSource}
columnList={columns}
onDataSourceChange={(tableData) => {
const dimValueMaps = tableData.map((item: TableDataSource) => {
return {
...item,
alias: item.alias ? `${item.alias}`.split(',') : [],
};
});
setDimValueMaps(dimValueMaps);
}}
/>
</Modal>
);
};
export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger,
}))(DimensionInfoModal);

View File

@@ -11,7 +11,7 @@ import {
} from './utils';
import styles from '../style.less';
import { ISemantic } from '../../data';
import { ChatConfigType, TransType } from '../../enum';
import { ChatConfigType, TransType, SemanticNodeType } from '../../enum';
import TransTypeTag from '../TransTypeTag';
type Props = {
@@ -99,7 +99,7 @@ const DefaultSettingForm: ForwardRefRenderFunction<any, Props> = (
name,
label: (
<>
<TransTypeTag type={TransType.DIMENSION} />
<TransTypeTag type={SemanticNodeType.DIMENSION} />
{name}
</>
),
@@ -115,7 +115,7 @@ const DefaultSettingForm: ForwardRefRenderFunction<any, Props> = (
name,
label: (
<>
<TransTypeTag type={TransType.METRIC} />
<TransTypeTag type={SemanticNodeType.METRIC} />
{name}
</>
),
@@ -198,30 +198,56 @@ const DefaultSettingForm: ForwardRefRenderFunction<any, Props> = (
</FormItem>
)}
{chatConfigType === ChatConfigType.AGG && (
<FormItem
name="metricIds"
label={
<FormItemTitle
title={'指标'}
subTitle={'问答搜索结果选择中,如果没有指定指标,将会采用默认指标进行展示'}
<>
{/* <FormItem
name="metricIds"
label={
<FormItemTitle
title={'指标'}
subTitle={'问答搜索结果选择中,如果没有指定指标,将会采用默认指标进行展示'}
/>
}
>
<Select
mode="multiple"
allowClear
style={{ width: '100%' }}
filterOption={(inputValue: string, item: any) => {
const { label } = item;
if (label.includes(inputValue)) {
return true;
}
return false;
}}
placeholder="请选择展示指标信息"
options={metricListOptions}
/>
}
>
<Select
mode="multiple"
allowClear
style={{ width: '100%' }}
filterOption={(inputValue: string, item: any) => {
const { label } = item;
if (label.includes(inputValue)) {
return true;
}
return false;
}}
placeholder="请选择展示指标信息"
options={metricListOptions}
/>
</FormItem>
</FormItem>
<FormItem
name="ratioMetricIds"
label={
<FormItemTitle
title={'同环比指标'}
subTitle={'问答搜索含有指定的指标,将会同时计算该指标最后一天的同环比'}
/>
}
>
<Select
mode="multiple"
allowClear
style={{ width: '100%' }}
filterOption={(inputValue: string, item: any) => {
const { label } = item;
if (label.includes(inputValue)) {
return true;
}
return false;
}}
placeholder="请选择同环比指标"
options={metricListOptions}
/>
</FormItem> */}
</>
)}
<FormItem

View File

@@ -15,7 +15,7 @@ type Props = {
settingSourceList: any[];
onCancel: () => void;
visible: boolean;
onSubmit: (params?: any) => void;
onSubmit: (params?: { isSilenceSubmit?: boolean }) => void;
};
const dimensionConfig = {
@@ -72,7 +72,8 @@ const DimensionAndMetricVisibleModal: React.FC<Props> = ({
}
}, [entityData, settingSourceList]);
const saveEntity = async () => {
const saveEntity = async (submitData: any, isSilenceSubmit = false) => {
const { selectedKeyList, knowledgeInfosMap } = submitData;
const globalKnowledgeConfigFormFields = await formRef?.current?.getFormValidateFields?.();
let globalKnowledgeConfig = entityData.globalKnowledgeConfig;
if (globalKnowledgeConfigFormFields) {
@@ -127,8 +128,10 @@ const DimensionAndMetricVisibleModal: React.FC<Props> = ({
id,
});
if (code === 200) {
onSubmit?.();
message.success('保存成功');
if (!isSilenceSubmit) {
message.success('保存成功');
}
onSubmit?.({ isSilenceSubmit });
return;
}
message.error(msg);
@@ -145,7 +148,7 @@ const DimensionAndMetricVisibleModal: React.FC<Props> = ({
<Button
type="primary"
onClick={() => {
saveEntity();
saveEntity({ selectedKeyList, knowledgeInfosMap });
}}
>
@@ -160,8 +163,9 @@ const DimensionAndMetricVisibleModal: React.FC<Props> = ({
key: 'visibleSetting',
children: (
<DimensionMetricVisibleTransfer
onKnowledgeInfosMapChange={(knowledgeInfosMap) => {
setKnowledgeInfosMap(knowledgeInfosMap);
onKnowledgeInfosMapChange={(knowledgeInfos) => {
setKnowledgeInfosMap(knowledgeInfos);
saveEntity({ selectedKeyList, knowledgeInfosMap: knowledgeInfos }, true);
}}
knowledgeInfosMap={knowledgeInfosMap}
titles={settingTypeConfig.titles}
@@ -169,6 +173,7 @@ const DimensionAndMetricVisibleModal: React.FC<Props> = ({
targetList={selectedKeyList}
onChange={(newTargetKeys) => {
handleTransferChange(newTargetKeys);
saveEntity({ selectedKeyList: newTargetKeys, knowledgeInfosMap }, true);
}}
/>
),
@@ -177,10 +182,12 @@ const DimensionAndMetricVisibleModal: React.FC<Props> = ({
label: '全局维度值过滤',
key: 'dimensionValueFilter',
children: (
<DimensionValueSettingForm
initialValues={globalKnowledgeConfigInitialValues}
ref={formRef}
/>
<div style={{ margin: '0 auto', width: '975px' }}>
<DimensionValueSettingForm
initialValues={globalKnowledgeConfigInitialValues}
ref={formRef}
/>
</div>
),
},
];

View File

@@ -74,9 +74,11 @@ const DimensionMetricVisibleForm: ForwardRefRenderFunction<any, Props> = ({
onCancel={() => {
setDimensionModalVisible(false);
}}
onSubmit={() => {
onSubmit={(params) => {
onSubmit?.();
setDimensionModalVisible(false);
if (!params?.isSilenceSubmit) {
setDimensionModalVisible(false);
}
}}
/>
)}

View File

@@ -5,8 +5,7 @@ import { Form, Input } from 'antd';
import { formLayout } from '@/components/FormHelper/utils';
import { isString } from 'lodash';
import styles from '../style.less';
import CommonEditList from '../../components/CommonEditList/index';
import SqlEditor from '@/components/SqlEditor';
import CommonEditList from '../../components/CommonEditList';
type Props = {
initialValues: any;
onSubmit?: () => void;

View File

@@ -1,14 +1,13 @@
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 type { ISemantic, IChatConfig } from '../../data';
import { updateDomain } from '../../service';
import type { ISemantic } from '../../data';
import { formLayout } from '@/components/FormHelper/utils';
import { formatRichEntityDataListToIds } from './utils';
import styles from '../style.less';
type Props = {
entityData: IChatConfig.IChatRichConfig;
entityData?: { id: number; names: string[] };
dimensionList: ISemantic.IDimensionList;
domainId: number;
onSubmit: () => void;
@@ -21,22 +20,20 @@ const EntityCreateForm: ForwardRefRenderFunction<any, Props> = (
ref,
) => {
const [form] = Form.useForm();
const formatEntityData = formatRichEntityDataListToIds(entityData);
const [dimensionListOptions, setDimensionListOptions] = useState<any>([]);
const getFormValidateFields = async () => {
return await form.validateFields();
};
useEffect(() => {
form.resetFields();
if (!entityData?.entity) {
if (!entityData) {
return;
}
form.setFieldsValue({
...formatEntityData.entity,
id: formatEntityData.id,
...entityData,
name: entityData.names.join(','),
});
}, [entityData]);
@@ -56,20 +53,14 @@ const EntityCreateForm: ForwardRefRenderFunction<any, Props> = (
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({
chatDetailConfig: {
...formatEntityData,
entity: {
...values,
names: name.split(','),
},
const { name } = values;
const { code, msg, data } = await updateDomain({
entity: {
...values,
names: name.split(','),
},
id,
id: domainId,
domainId,
});

View File

@@ -5,7 +5,7 @@ import { connect } from 'umi';
import type { StateType } from '../../model';
import { getDomainExtendDetailConfig } from '../../service';
import ProCard from '@ant-design/pro-card';
import EntityCreateForm from './EntityCreateForm';
import DefaultSettingForm from './DefaultSettingForm';
import type { IChatConfig } from '../../data';
import DimensionMetricVisibleForm from './DimensionMetricVisibleForm';
@@ -26,8 +26,6 @@ const EntitySection: React.FC<Props> = ({
const [entityData, setentityData] = useState<IChatConfig.IChatRichConfig>();
const entityCreateRef = useRef<any>({});
const queryThemeListData: any = async () => {
const { code, data } = await getDomainExtendDetailConfig({
domainId: selectDomainId,
@@ -58,25 +56,6 @@ const EntitySection: React.FC<Props> = ({
return (
<div style={{ width: 800, margin: '0 auto' }}>
<Space direction="vertical" style={{ width: '100%' }} size={20}>
{chatConfigType === 'detail' && entityData && (
<ProCard title="实体" bordered>
<EntityCreateForm
ref={entityCreateRef}
domainId={Number(selectDomainId)}
entityData={entityData}
dimensionList={dimensionList.filter((item) => {
const blackDimensionList = entityData?.visibility?.blackDimIdList;
if (Array.isArray(blackDimensionList)) {
return !blackDimensionList.includes(item.id);
}
return false;
})}
onSubmit={() => {
queryThemeListData();
}}
/>
</ProCard>
)}
<ProCard bordered title="问答可见">
<DimensionMetricVisibleForm
chatConfigKey={

View File

@@ -0,0 +1,69 @@
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 { getDomainDetail } from '../../service';
import ProCard from '@ant-design/pro-card';
import EntityCreateForm from './EntityCreateForm';
import type { IChatConfig } from '../../data';
type Props = {
dispatch: Dispatch;
domainManger: StateType;
};
const EntitySettingSection: React.FC<Props> = ({ domainManger }) => {
const { selectDomainId, dimensionList, metricList } = domainManger;
const [entityData, setEntityData] = useState<IChatConfig.IChatRichConfig>();
const entityCreateRef = useRef<any>({});
const queryDomainData: any = async () => {
const { code, data } = await getDomainDetail({
domainId: selectDomainId,
});
if (code === 200) {
const { entity } = data;
setEntityData(entity);
return;
}
message.error('获取问答设置信息失败');
};
const initPage = async () => {
queryDomainData();
};
useEffect(() => {
initPage();
}, [selectDomainId]);
return (
<div style={{ width: 800, margin: '0 auto' }}>
<Space direction="vertical" style={{ width: '100%' }} size={20}>
{
<ProCard title="实体" bordered>
<EntityCreateForm
ref={entityCreateRef}
domainId={Number(selectDomainId)}
entityData={entityData}
dimensionList={dimensionList}
onSubmit={() => {
queryDomainData();
}}
/>
</ProCard>
}
</Space>
</div>
);
};
export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger,
}))(EntitySettingSection);

View File

@@ -0,0 +1,87 @@
import { message } from 'antd';
import React, { useState, useEffect } from 'react';
import { connect } from 'umi';
import type { StateType } from '../../model';
import { getDomainExtendConfig, addDomainExtend, editDomainExtend } from '../../service';
import ProCard from '@ant-design/pro-card';
import TextAreaCommonEditList from '../../components/CommonEditList/TextArea';
type Props = {
domainManger: StateType;
};
const RecommendedQuestionsSection: React.FC<Props> = ({ domainManger }) => {
const { selectDomainId } = domainManger;
const [questionData, setQuestionData] = useState<string[]>([]);
const [currentRecordId, setCurrentRecordId] = useState<number>(0);
const queryThemeListData: any = async () => {
const { code, data } = await getDomainExtendConfig({
domainId: selectDomainId,
});
if (code === 200) {
const target = data?.[0] || {};
if (Array.isArray(target.recommendedQuestions)) {
setQuestionData(
target.recommendedQuestions.map((item: { question: string }) => {
return item.question;
}),
);
setCurrentRecordId(target.id || 0);
} else {
setQuestionData([]);
setCurrentRecordId(0);
}
return;
}
message.error('获取问答设置信息失败');
};
const saveEntity = async (list: string[]) => {
let saveDomainExtendQuery = addDomainExtend;
if (currentRecordId) {
saveDomainExtendQuery = editDomainExtend;
}
const { code, msg } = await saveDomainExtendQuery({
recommendedQuestions: list.map((question: string) => {
return { question };
}),
id: currentRecordId,
domainId: selectDomainId,
});
if (code === 200) {
return;
}
message.error(msg);
};
const initPage = async () => {
queryThemeListData();
};
useEffect(() => {
initPage();
}, [selectDomainId]);
return (
<div style={{ width: 800, margin: '0 auto' }}>
<ProCard bordered title="问题推荐列表">
<TextAreaCommonEditList
value={questionData}
onChange={(list) => {
saveEntity(list);
setQuestionData(list);
}}
/>
</ProCard>
</div>
);
};
export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger,
}))(RecommendedQuestionsSection);

View File

@@ -18,8 +18,9 @@ export const formatRichEntityDataListToIds = (
const detailData: {
dimensionIds: number[];
metricIds: number[];
} = { dimensionIds: [], metricIds: [] };
const { dimensions, metrics } = chatDefaultConfig || {};
ratioMetricIds: number[];
} = { dimensionIds: [], metricIds: [], ratioMetricIds: [] };
const { dimensions, metrics, ratioMetrics } = chatDefaultConfig || {};
if (Array.isArray(dimensions)) {
detailData.dimensionIds = dimensions.map((item: ISemantic.IDimensionItem) => {
return item.id;
@@ -30,6 +31,12 @@ export const formatRichEntityDataListToIds = (
return item.id;
});
}
if (Array.isArray(ratioMetrics)) {
detailData.ratioMetricIds = ratioMetrics.map((item: ISemantic.IMetricItem) => {
return item.id;
});
}
let entitySetting = {};
if (entity) {
const entityItem = entity.dimItem;

View File

@@ -199,15 +199,15 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
</FormItem>
<FormItem
name="name"
label="指标中文名"
rules={[{ required: true, message: '请输入指标中文名' }]}
label="指标名"
rules={[{ required: true, message: '请输入指标名' }]}
>
<Input placeholder="名称不可重复" />
</FormItem>
<FormItem
name="bizName"
label="指标英文名"
rules={[{ required: true, message: '请输入指标英文名' }]}
label="字段名称"
rules={[{ required: true, message: '请输入字段名称' }]}
>
<Input placeholder="名称不可重复" disabled={isEdit} />
</FormItem>

View File

@@ -1,19 +1,21 @@
import { Tag } from 'antd';
import React from 'react';
import { TransType } from '../enum';
import { SemanticNodeType } from '../enum';
type Props = {
type: TransType;
type: SemanticNodeType;
};
const TransTypeTag: React.FC<Props> = ({ type }) => {
return (
<>
{type === TransType.DIMENSION ? (
{type === SemanticNodeType.DIMENSION ? (
<Tag color="blue">{'维度'}</Tag>
) : type === 'metric' ? (
) : type === SemanticNodeType.METRIC ? (
<Tag color="orange">{'指标'}</Tag>
) : type === SemanticNodeType.DATASOURCE ? (
<Tag color="green">{'数据源'}</Tag>
) : (
<></>
)}

View File

@@ -8,7 +8,8 @@
.projectManger {
width: 100%;
min-height: calc(100vh - 48px);
background: #f8f9fb;
// background: #f8f9fb;
background-color: #fff;
position: relative;
@@ -37,8 +38,17 @@
}
.tab {
padding: 0 20px;
line-height: 28px;
:global {
.ant-tabs-tab-btn {
font-size: 16px !important;
}
.ant-tabs-nav-wrap {
padding: 0 20px;
}
.ant-tabs-nav {
margin-bottom: 0;
}
}
}
.mainTip {
@@ -50,8 +60,9 @@
padding: 0 !important;
}
.ant-tabs-content-holder {
overflow: scroll;
height: calc(100vh - 192px);
margin-top: 20px;
// overflow: scroll;
// height: calc(100vh - 175px);
}
}

View File

@@ -8,7 +8,9 @@ export type UserName = string;
export type SensitiveLevel = 0 | 1 | 2 | null;
export type RefreshGraphData = (graphRootData: TreeGraphData) => void;
// export type RefreshGraphData = (graphRootData: TreeGraphData) => void;
export type ToolBarSearchCallBack = (text: string) => void;
export declare namespace IDataSource {
interface IIdentifiersItem {
@@ -113,8 +115,14 @@ export declare namespace ISemantic {
semanticType: string;
alias: string;
useCnt: number;
dimValueMaps: IDimensionValueSettingItem[];
}
interface IDimensionValueSettingItem {
techName: string;
bizName: string;
alias?: string[];
}
interface IMeasure {
name: string;
agg?: string;
@@ -156,6 +164,14 @@ export declare namespace ISemantic {
type IDimensionList = IDimensionItem[];
type IMetricList = IMetricItem[];
interface IDomainSchemaRelaItem {
domainId: number;
dimensions: IDimensionList;
metrics: IMetricList;
datasource: IDataSourceItem;
}
type IDomainSchemaRelaList = IDomainSchemaRelaItem[];
}
export declare namespace IChatConfig {
@@ -218,6 +234,7 @@ export declare namespace IChatConfig {
chatDefaultConfig: {
dimensions: ISemantic.IDimensionList;
metrics: ISemantic.IMetricList;
ratioMetrics: ISemantic.IMetricList;
unit: number;
period: string;
};

View File

@@ -13,3 +13,8 @@ export enum SemanticNodeType {
DIMENSION = 'dimension',
METRIC = 'metric',
}
export enum MetricTypeWording {
ATOMIC = '原子指标',
DERIVED = '衍生指标',
}

View File

@@ -111,7 +111,7 @@ export function deleteDomain(id: any): Promise<any> {
});
}
export function getGroupAuthInfo(id: string): Promise<any> {
export function getGroupAuthInfo(id: number): Promise<any> {
return request(`${process.env.AUTH_API_BASE_URL}queryGroup`, {
method: 'GET',
params: {