mirror of
https://github.com/tencentmusic/supersonic.git
synced 2025-12-18 08:17:18 +00:00
[improvement][semantic-fe] enhance the analysis of metric trends (#234)
* [improvement][semantic-fe] Add model alias setting & Add view permission restrictions to the model permission management tab. [improvement][semantic-fe] Add permission control to the action buttons for the main domain; apply high sensitivity filtering to the authorization of metrics/dimensions. [improvement][semantic-fe] Optimize the editing mode in the dimension/metric/datasource components to use the modelId stored in the database for data, instead of relying on the data from the state manager. * [improvement][semantic-fe] Add time granularity setting in the data source configuration. * [improvement][semantic-fe] Dictionary import for dimension values supported in Q&A visibility * [improvement][semantic-fe] Modification of data source creation prompt wording" * [improvement][semantic-fe] metric market experience optimization * [improvement][semantic-fe] enhance the analysis of metric trends
This commit is contained in:
@@ -379,8 +379,6 @@ const SqlDetail: React.FC<IProps> = ({
|
||||
setScreenSize(size);
|
||||
}, []);
|
||||
|
||||
const exploreEditorSize = localStorage.getItem('exploreEditorSize');
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.sqlOprBar}>
|
||||
@@ -445,10 +443,9 @@ const SqlDetail: React.FC<IProps> = ({
|
||||
split="horizontal"
|
||||
onChange={(size) => {
|
||||
setEditorSize(size);
|
||||
localStorage.setItem('exploreEditorSize', size[0]);
|
||||
}}
|
||||
>
|
||||
<Pane initialSize={exploreEditorSize || '500px'}>
|
||||
<Pane initialSize={'500px'}>
|
||||
<div className={styles.sqlMain}>
|
||||
<div className={styles.sqlEditorWrapper}>
|
||||
<SqlEditor
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import { CheckCard } from '@ant-design/pro-components';
|
||||
import React, { useState } from 'react';
|
||||
import { Dropdown, Popconfirm, Typography } from 'antd';
|
||||
import { EllipsisOutlined } from '@ant-design/icons';
|
||||
import { ISemantic } from '../../data';
|
||||
import { connect } from 'umi';
|
||||
import icon from '../../../../assets/icon/sourceState.svg';
|
||||
import type { Dispatch } from 'umi';
|
||||
import type { StateType } from '../../model';
|
||||
import { SemanticNodeType } from '../../enum';
|
||||
import styles from '../style.less';
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
type Props = {
|
||||
disabledEdit?: boolean;
|
||||
metricList: ISemantic.IMetricItem[];
|
||||
onMetricChange?: (metricItem: ISemantic.IMetricItem) => void;
|
||||
onEditBtnClick?: (metricItem: ISemantic.IMetricItem) => void;
|
||||
onDeleteBtnClick?: (metricItem: ISemantic.IMetricItem) => void;
|
||||
domainManger: StateType;
|
||||
dispatch: Dispatch;
|
||||
};
|
||||
|
||||
const MetricCardList: React.FC<Props> = ({
|
||||
metricList,
|
||||
disabledEdit = false,
|
||||
onMetricChange,
|
||||
onEditBtnClick,
|
||||
onDeleteBtnClick,
|
||||
domainManger,
|
||||
}) => {
|
||||
const [currentNodeData, setCurrentNodeData] = useState<any>({});
|
||||
|
||||
const descNode = (metricItem: ISemantic.IMetricItem) => {
|
||||
const { modelName, createdBy } = metricItem;
|
||||
return (
|
||||
<>
|
||||
<div className={styles.overviewExtraContainer}>
|
||||
<div className={styles.extraWrapper}>
|
||||
<div className={styles.extraStatistic}>
|
||||
<div className={styles.extraTitle}>所属模型:</div>
|
||||
<div className={styles.extraValue}>
|
||||
<Paragraph style={{ maxWidth: 70, margin: 0 }} ellipsis={{ tooltip: modelName }}>
|
||||
<span>{modelName}</span>
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.extraWrapper}>
|
||||
<div className={styles.extraStatistic}>
|
||||
<div className={styles.extraTitle}>创建人:</div>
|
||||
<div className={styles.extraValue}>
|
||||
<Paragraph style={{ maxWidth: 70, margin: 0 }} ellipsis={{ tooltip: createdBy }}>
|
||||
<span>{createdBy}</span>
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const extraNode = (metricItem: ISemantic.IMetricItem) => {
|
||||
return (
|
||||
<Dropdown
|
||||
placement="top"
|
||||
menu={{
|
||||
onClick: ({ key, domEvent }) => {
|
||||
domEvent.stopPropagation();
|
||||
if (key === 'edit') {
|
||||
onEditBtnClick?.(metricItem);
|
||||
}
|
||||
},
|
||||
items: [
|
||||
{
|
||||
label: '编辑',
|
||||
key: 'edit',
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<Popconfirm
|
||||
title="确认删除?"
|
||||
okText="是"
|
||||
cancelText="否"
|
||||
onConfirm={() => {
|
||||
onDeleteBtnClick?.(metricItem);
|
||||
}}
|
||||
>
|
||||
<a key="modelDeleteBtn">删除</a>
|
||||
</Popconfirm>
|
||||
),
|
||||
key: 'delete',
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
<EllipsisOutlined
|
||||
style={{ fontSize: 22, color: 'rgba(0,0,0,0.5)' }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '0px 20px 20px' }}>
|
||||
<CheckCard.Group value={currentNodeData.id} defaultValue={undefined}>
|
||||
{metricList &&
|
||||
metricList.map((metricItem: ISemantic.IMetricItem) => {
|
||||
return (
|
||||
<CheckCard
|
||||
style={{ width: 350 }}
|
||||
avatar={icon}
|
||||
title={`${metricItem.name}`}
|
||||
key={metricItem.id}
|
||||
value={metricItem.id}
|
||||
description={descNode(metricItem)}
|
||||
extra={!disabledEdit && extraNode(metricItem)}
|
||||
onClick={() => {
|
||||
setCurrentNodeData({ ...metricItem, nodeType: SemanticNodeType.METRIC });
|
||||
onMetricChange?.(metricItem);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</CheckCard.Group>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(({ domainManger }: { domainManger: StateType }) => ({
|
||||
domainManger,
|
||||
}))(MetricCardList);
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Form, Input, Space, Row, Col } from 'antd';
|
||||
import { Form, Input, Space, Row, Col, Switch } from 'antd';
|
||||
import StandardFormRow from '@/components/StandardFormRow';
|
||||
import TagSelect from '@/components/TagSelect';
|
||||
import React, { useEffect } from 'react';
|
||||
@@ -10,20 +10,21 @@ import styles from '../style.less';
|
||||
const FormItem = Form.Item;
|
||||
|
||||
type Props = {
|
||||
filterValues?: any;
|
||||
initFilterValues?: any;
|
||||
onFiltersChange: (_: any, values: any) => void;
|
||||
};
|
||||
|
||||
const MetricFilter: React.FC<Props> = ({ filterValues = {}, onFiltersChange }) => {
|
||||
const MetricFilter: React.FC<Props> = ({ initFilterValues = {}, onFiltersChange }) => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue({
|
||||
...filterValues,
|
||||
...initFilterValues,
|
||||
});
|
||||
}, [form, filterValues]);
|
||||
}, [form]);
|
||||
|
||||
const handleValuesChange = (value: any, values: any) => {
|
||||
localStorage.setItem('metricMarketShowType', !!values.showType ? '1' : '0');
|
||||
onFiltersChange(value, values);
|
||||
};
|
||||
|
||||
@@ -32,17 +33,6 @@ const MetricFilter: React.FC<Props> = ({ filterValues = {}, onFiltersChange }) =
|
||||
};
|
||||
|
||||
const filterList = [
|
||||
// {
|
||||
// title: '指标类型',
|
||||
// key: 'type',
|
||||
// options: [
|
||||
// {
|
||||
// value: 'ATOMIC',
|
||||
// label: '原子指标',
|
||||
// },
|
||||
// { value: 'DERIVED', label: '衍生指标' },
|
||||
// ],
|
||||
// },
|
||||
{
|
||||
title: '敏感度',
|
||||
key: 'sensitiveLevel',
|
||||
@@ -94,6 +84,11 @@ const MetricFilter: React.FC<Props> = ({ filterValues = {}, onFiltersChange }) =
|
||||
</div>
|
||||
</StandardFormRow>
|
||||
<Space size={80}>
|
||||
<StandardFormRow key="showType" title="切换为卡片" block>
|
||||
<FormItem name="showType" valuePropName="checked">
|
||||
<Switch size="small" />
|
||||
</FormItem>
|
||||
</StandardFormRow>
|
||||
<StandardFormRow key="domainIds" title="所属主题域" block>
|
||||
<FormItem name="domainIds">
|
||||
<DomainTreeSelect />
|
||||
@@ -103,17 +98,15 @@ const MetricFilter: React.FC<Props> = ({ filterValues = {}, onFiltersChange }) =
|
||||
const { title, key, options } = item;
|
||||
return (
|
||||
<StandardFormRow key={key} title={title} block>
|
||||
<div style={{ marginLeft: -30 }}>
|
||||
<FormItem name={key}>
|
||||
<TagSelect reverseCheckAll single>
|
||||
{options.map((item: any) => (
|
||||
<TagSelect.Option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</TagSelect.Option>
|
||||
))}
|
||||
</TagSelect>
|
||||
</FormItem>
|
||||
</div>
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
import { CHART_SECONDARY_COLOR } from '@/common/constants';
|
||||
import {
|
||||
formatByDecimalPlaces,
|
||||
formatByPercentageData,
|
||||
getFormattedValueData,
|
||||
} from '@/utils/utils';
|
||||
import { Skeleton, Button, Tooltip } from 'antd';
|
||||
import { DownloadOutlined } from '@ant-design/icons';
|
||||
import type { ECharts } from 'echarts';
|
||||
import * as echarts from 'echarts';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import styles from '../style.less';
|
||||
import moment from 'moment';
|
||||
|
||||
type Props = {
|
||||
title?: string;
|
||||
tip?: string;
|
||||
data: any[];
|
||||
fields: any[];
|
||||
// columnFieldName: string;
|
||||
// valueFieldName: string;
|
||||
loading: boolean;
|
||||
isPer?: boolean;
|
||||
isPercent?: boolean;
|
||||
dateFieldName?: string;
|
||||
dateFormat?: string;
|
||||
height?: number;
|
||||
renderType?: string;
|
||||
decimalPlaces?: number;
|
||||
onDownload?: () => void;
|
||||
};
|
||||
|
||||
const TrendChart: React.FC<Props> = ({
|
||||
title,
|
||||
tip,
|
||||
data,
|
||||
fields,
|
||||
loading,
|
||||
isPer,
|
||||
isPercent,
|
||||
dateFieldName,
|
||||
// columnFieldName,
|
||||
// valueFieldName,
|
||||
dateFormat,
|
||||
height,
|
||||
renderType,
|
||||
decimalPlaces,
|
||||
onDownload,
|
||||
}) => {
|
||||
const chartRef = useRef<any>();
|
||||
const [instance, setInstance] = useState<ECharts>();
|
||||
const renderChart = useCallback(() => {
|
||||
let instanceObj: ECharts;
|
||||
if (!instance) {
|
||||
instanceObj = echarts.init(chartRef.current);
|
||||
setInstance(instanceObj);
|
||||
} else {
|
||||
instanceObj = instance;
|
||||
if (renderType === 'clear') {
|
||||
instanceObj.clear();
|
||||
}
|
||||
}
|
||||
const xData = Array.from(
|
||||
new Set(
|
||||
data
|
||||
.map((item) =>
|
||||
moment(`${(dateFieldName && item[dateFieldName]) || item.sys_imp_date}`).format(
|
||||
dateFormat ?? 'YYYY-MM-DD',
|
||||
),
|
||||
)
|
||||
.sort((a, b) => {
|
||||
return moment(a).valueOf() - moment(b).valueOf();
|
||||
}),
|
||||
),
|
||||
);
|
||||
const seriesData = fields.map((field) => {
|
||||
const fieldData = {
|
||||
type: 'line',
|
||||
name: field.name,
|
||||
symbol: 'circle',
|
||||
showSymbol: data.length === 1,
|
||||
smooth: true,
|
||||
data: data.reduce((itemData, item) => {
|
||||
const target = item[field.column];
|
||||
if (target) {
|
||||
itemData.push(target);
|
||||
}
|
||||
return itemData;
|
||||
}, []),
|
||||
};
|
||||
return fieldData;
|
||||
});
|
||||
|
||||
instanceObj.setOption({
|
||||
legend: {
|
||||
left: 0,
|
||||
top: 0,
|
||||
icon: 'rect',
|
||||
itemWidth: 15,
|
||||
itemHeight: 5,
|
||||
selected: fields.reduce((result, item) => {
|
||||
if (item.selected === false) {
|
||||
result[item.name] = false;
|
||||
}
|
||||
return result;
|
||||
}, {}),
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
axisTick: {
|
||||
alignWithLabel: true,
|
||||
lineStyle: {
|
||||
color: CHART_SECONDARY_COLOR,
|
||||
},
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: CHART_SECONDARY_COLOR,
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
showMaxLabel: true,
|
||||
color: '#999',
|
||||
},
|
||||
data: xData,
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
opacity: 0.3,
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
formatter: function (value: any) {
|
||||
return value === 0
|
||||
? 0
|
||||
: isPer
|
||||
? `${formatByDecimalPlaces(value, decimalPlaces ?? 0)}%`
|
||||
: isPercent
|
||||
? formatByPercentageData(value, decimalPlaces ?? 0)
|
||||
: getFormattedValueData(value);
|
||||
},
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: function (params: any[]) {
|
||||
const param = params[0];
|
||||
const valueLabels = params
|
||||
.map(
|
||||
(item: any) =>
|
||||
`<div style="margin-top: 3px;">${
|
||||
item.marker
|
||||
} <span style="display: inline-block; width: 70px; margin-right: 5px;">${
|
||||
item.seriesName
|
||||
}</span><span style="display: inline-block; width: 90px; text-align: right; font-weight: 500;">${
|
||||
item.value === ''
|
||||
? '-'
|
||||
: isPer
|
||||
? `${formatByDecimalPlaces(item.value, decimalPlaces ?? 2)}%`
|
||||
: isPercent
|
||||
? formatByPercentageData(item.value, decimalPlaces ?? 2)
|
||||
: getFormattedValueData(item.value)
|
||||
}</span></div>`,
|
||||
)
|
||||
.join('');
|
||||
return `${param.name}<br />${valueLabels}`;
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
left: '1%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
top: height && height < 300 ? 45 : 60,
|
||||
containLabel: true,
|
||||
},
|
||||
series: seriesData,
|
||||
});
|
||||
instanceObj.resize();
|
||||
}, [data, fields, instance, isPer, isPercent, dateFieldName, decimalPlaces, renderType]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading) {
|
||||
renderChart();
|
||||
}
|
||||
}, [renderChart, loading, data]);
|
||||
|
||||
return (
|
||||
<div className={styles.trendChart}>
|
||||
{title && (
|
||||
<div className={styles.top}>
|
||||
<div className={styles.title}>{title}</div>
|
||||
{onDownload && (
|
||||
<Tooltip title="下载">
|
||||
<Button shape="circle" className={styles.downloadBtn} onClick={onDownload}>
|
||||
<DownloadOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Skeleton
|
||||
className={styles.chart}
|
||||
style={{ height, display: loading ? 'table' : 'none' }}
|
||||
paragraph={{ rows: height && height > 300 ? 9 : 6 }}
|
||||
/>
|
||||
<div
|
||||
className={styles.chart}
|
||||
style={{ height, display: !loading ? 'block' : 'none' }}
|
||||
ref={chartRef}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrendChart;
|
||||
@@ -0,0 +1,134 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { SemanticNodeType } from '../../enum';
|
||||
import moment from 'moment';
|
||||
import { message } from 'antd';
|
||||
import { queryStruct } from '@/pages/SemanticModel/service';
|
||||
import TrendChart from '@/pages/SemanticModel/Metric/components/MetricTrend';
|
||||
import MDatePicker from '@/components/MDatePicker';
|
||||
import { DateRangeType, DateSettingType } from '@/components/MDatePicker/type';
|
||||
import { ISemantic } from '../../data';
|
||||
|
||||
type Props = {
|
||||
nodeData: any;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
const MetricTrendSection: React.FC<Props> = ({ nodeData }) => {
|
||||
const dateFieldMap = {
|
||||
[DateRangeType.DAY]: 'sys_imp_date',
|
||||
[DateRangeType.WEEK]: 'sys_imp_week',
|
||||
[DateRangeType.MONTH]: 'sys_imp_month',
|
||||
};
|
||||
const indicatorFields = useRef<{ name: string; column: string }[]>([]);
|
||||
const [metricTrendData, setMetricTrendData] = useState<ISemantic.IMetricTrendItem[]>([]);
|
||||
const [metricTrendLoading, setMetricTrendLoading] = useState<boolean>(false);
|
||||
const [metricColumnConfig, setMetricColumnConfig] = useState<ISemantic.IMetricTrendColumn>();
|
||||
const [authMessage, setAuthMessage] = useState<string>('');
|
||||
const [periodDate, setPeriodDate] = useState<{
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
dateField: string;
|
||||
}>({
|
||||
startDate: moment().subtract('7', 'days').format('YYYY-MM-DD'),
|
||||
endDate: moment().format('YYYY-MM-DD'),
|
||||
dateField: dateFieldMap[DateRangeType.DAY],
|
||||
});
|
||||
|
||||
const getMetricTrendData = async () => {
|
||||
setMetricTrendLoading(true);
|
||||
const { modelId, bizName, name } = nodeData;
|
||||
indicatorFields.current = [{ name, column: bizName }];
|
||||
const { code, data, msg } = await queryStruct({
|
||||
modelId,
|
||||
bizName,
|
||||
dateField: periodDate.dateField,
|
||||
startDate: periodDate.startDate,
|
||||
endDate: periodDate.endDate,
|
||||
});
|
||||
setMetricTrendLoading(false);
|
||||
if (code === 200) {
|
||||
const { resultList, columns, queryAuthorization } = data;
|
||||
setMetricTrendData(resultList);
|
||||
const message = queryAuthorization?.message;
|
||||
if (message) {
|
||||
setAuthMessage(message);
|
||||
}
|
||||
const targetConfig = columns.find((item: ISemantic.IMetricTrendColumn) => {
|
||||
return item.nameEn === bizName;
|
||||
});
|
||||
if (targetConfig) {
|
||||
setMetricColumnConfig(targetConfig);
|
||||
}
|
||||
} else {
|
||||
message.error(msg);
|
||||
setMetricTrendData([]);
|
||||
setMetricColumnConfig(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (nodeData.id && nodeData?.nodeType === SemanticNodeType.METRIC) {
|
||||
getMetricTrendData();
|
||||
}
|
||||
}, [nodeData, periodDate]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ marginBottom: 5 }}>
|
||||
<MDatePicker
|
||||
initialValues={{
|
||||
dateSettingType: 'DYNAMIC',
|
||||
dynamicParams: {
|
||||
number: 7,
|
||||
periodType: 'DAYS',
|
||||
includesCurrentPeriod: true,
|
||||
shortCutId: 'last7Days',
|
||||
dateRangeType: 'DAY',
|
||||
dynamicAdvancedConfigType: 'last',
|
||||
dateRangeStringDesc: '最近7天',
|
||||
dateSettingType: DateSettingType.DYNAMIC,
|
||||
},
|
||||
staticParams: {},
|
||||
}}
|
||||
onDateRangeChange={(value, config) => {
|
||||
const [startDate, endDate] = value;
|
||||
const { dateSettingType, dynamicParams, staticParams } = config;
|
||||
let dateField = dateFieldMap[DateRangeType.DAY];
|
||||
if (DateSettingType.DYNAMIC === dateSettingType) {
|
||||
dateField = dateFieldMap[dynamicParams.dateRangeType];
|
||||
}
|
||||
if (DateSettingType.STATIC === dateSettingType) {
|
||||
dateField = dateFieldMap[staticParams.dateRangeType];
|
||||
}
|
||||
setPeriodDate({ startDate, endDate, dateField });
|
||||
}}
|
||||
disabledAdvanceSetting={true}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ color: '#d46b08', marginBottom: 15 }}>指标存在如下权限问题: {authMessage}</div>
|
||||
<TrendChart
|
||||
data={metricTrendData}
|
||||
isPer={
|
||||
metricColumnConfig?.dataFormatType === 'percent' &&
|
||||
metricColumnConfig?.dataFormat?.needMultiply100 === false
|
||||
? true
|
||||
: false
|
||||
}
|
||||
isPercent={
|
||||
metricColumnConfig?.dataFormatType === 'percent' &&
|
||||
metricColumnConfig?.dataFormat?.needMultiply100 === true
|
||||
? true
|
||||
: false
|
||||
}
|
||||
fields={indicatorFields.current}
|
||||
loading={metricTrendLoading}
|
||||
dateFieldName={periodDate.dateField}
|
||||
height={350}
|
||||
renderType="clear"
|
||||
decimalPlaces={metricColumnConfig?.dataFormat?.decimalPlaces || 2}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MetricTrendSection;
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ActionType, ProColumns } from '@ant-design/pro-table';
|
||||
import ProTable from '@ant-design/pro-table';
|
||||
import { message, Space, Popconfirm, Tag } from 'antd';
|
||||
import { message, Space, Popconfirm, Tag, Spin } from 'antd';
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import type { Dispatch } from 'umi';
|
||||
import { connect, history } from 'umi';
|
||||
@@ -9,9 +9,12 @@ import { SENSITIVE_LEVEL_ENUM } from '../constant';
|
||||
import { queryMetric, deleteMetric } from '../service';
|
||||
import MetricFilter from './components/MetricFilter';
|
||||
import MetricInfoCreateForm from '../components/MetricInfoCreateForm';
|
||||
import MetricCardList from './components/MetricCardList';
|
||||
import NodeInfoDrawer from '../SemanticGraph/components/NodeInfoDrawer';
|
||||
import { SemanticNodeType } from '../enum';
|
||||
import moment from 'moment';
|
||||
import styles from './style.less';
|
||||
import { IDataSource, ISemantic } from '../data';
|
||||
import { ISemantic } from '../data';
|
||||
|
||||
type Props = {
|
||||
dispatch: Dispatch;
|
||||
@@ -30,19 +33,23 @@ type QueryMetricListParams = {
|
||||
const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
||||
const { selectDomainId, selectModelId: modelId } = domainManger;
|
||||
const [createModalVisible, setCreateModalVisible] = useState<boolean>(false);
|
||||
const [pagination, setPagination] = useState({
|
||||
const defaultPagination = {
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
});
|
||||
};
|
||||
const [pagination, setPagination] = useState(defaultPagination);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [dataSource, setDataSource] = useState<IDataSource.IDataSourceItem[]>([]);
|
||||
const [dataSource, setDataSource] = useState<ISemantic.IMetricItem[]>([]);
|
||||
const [metricItem, setMetricItem] = useState<ISemantic.IMetricItem>();
|
||||
const [filterParams, setFilterParams] = useState<Record<string, any>>({});
|
||||
const [filterParams, setFilterParams] = useState<Record<string, any>>({
|
||||
showType: localStorage.getItem('metricMarketShowType') === '1' ? true : false,
|
||||
});
|
||||
const [infoDrawerVisible, setInfoDrawerVisible] = useState<boolean>(false);
|
||||
const actionRef = useRef<ActionType>();
|
||||
|
||||
useEffect(() => {
|
||||
queryMetricList();
|
||||
queryMetricList(filterParams);
|
||||
}, []);
|
||||
|
||||
const queryMetricList = async (params: QueryMetricListParams = {}, disabledLoading = false) => {
|
||||
@@ -52,10 +59,9 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
||||
const { code, data, msg } = await queryMetric({
|
||||
...pagination,
|
||||
...params,
|
||||
pageSize: params.showType ? 100 : defaultPagination.pageSize,
|
||||
});
|
||||
if (!disabledLoading) {
|
||||
setLoading(false);
|
||||
}
|
||||
setLoading(false);
|
||||
const { list, pageSize, current, total } = data || {};
|
||||
let resData: any = {};
|
||||
if (code === 200) {
|
||||
@@ -81,6 +87,21 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
||||
return resData;
|
||||
};
|
||||
|
||||
const deleteMetricQuery = async (id: number) => {
|
||||
const { code, msg } = await deleteMetric(id);
|
||||
if (code === 200) {
|
||||
setMetricItem(undefined);
|
||||
queryMetricList(filterParams);
|
||||
} else {
|
||||
message.error(msg);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMetricEdit = (metricItem: ISemantic.IMetricItem) => {
|
||||
setMetricItem(metricItem);
|
||||
setCreateModalVisible(true);
|
||||
};
|
||||
|
||||
const columns: ProColumns[] = [
|
||||
{
|
||||
dataIndex: 'id',
|
||||
@@ -90,18 +111,16 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
||||
dataIndex: 'name',
|
||||
title: '指标名称',
|
||||
render: (_, record: any) => {
|
||||
if (record.hasAdminRes) {
|
||||
return (
|
||||
<a
|
||||
onClick={() => {
|
||||
history.replace(`/model/${record.domainId}/${record.modelId}/metric`);
|
||||
}}
|
||||
>
|
||||
{record.name}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return <> {record.name}</>;
|
||||
return (
|
||||
<a
|
||||
onClick={() => {
|
||||
setMetricItem(record);
|
||||
setInfoDrawerVisible(true);
|
||||
}}
|
||||
>
|
||||
{record.name}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
},
|
||||
// {
|
||||
@@ -116,6 +135,20 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
||||
{
|
||||
dataIndex: 'modelName',
|
||||
title: '所属模型',
|
||||
render: (_, record: any) => {
|
||||
if (record.hasAdminRes) {
|
||||
return (
|
||||
<a
|
||||
onClick={() => {
|
||||
history.replace(`/model/${record.domainId}/${record.modelId}/metric`);
|
||||
}}
|
||||
>
|
||||
{record.modelName}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return <> {record.modelName}</>;
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: 'sensitiveLevel',
|
||||
@@ -179,27 +212,13 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
||||
<a
|
||||
key="metricEditBtn"
|
||||
onClick={() => {
|
||||
setMetricItem(record);
|
||||
setCreateModalVisible(true);
|
||||
handleMetricEdit(record);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</a>
|
||||
|
||||
<Popconfirm
|
||||
title="确认删除?"
|
||||
okText="是"
|
||||
cancelText="否"
|
||||
onConfirm={async () => {
|
||||
const { code, msg } = await deleteMetric(record.id);
|
||||
if (code === 200) {
|
||||
setMetricItem(undefined);
|
||||
queryMetricList();
|
||||
} else {
|
||||
message.error(msg);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Popconfirm title="确认删除?" okText="是" cancelText="否" onConfirm={() => {}}>
|
||||
<a
|
||||
key="metricDeleteBtn"
|
||||
onClick={() => {
|
||||
@@ -238,36 +257,64 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
||||
<>
|
||||
<div className={styles.metricFilterWrapper}>
|
||||
<MetricFilter
|
||||
initFilterValues={filterParams}
|
||||
onFiltersChange={(_, values) => {
|
||||
if (_.showType !== undefined) {
|
||||
setLoading(true);
|
||||
setDataSource([]);
|
||||
}
|
||||
handleFilterChange(values);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<ProTable
|
||||
className={`${styles.metricTable}`}
|
||||
actionRef={actionRef}
|
||||
rowKey="id"
|
||||
search={false}
|
||||
dataSource={dataSource}
|
||||
columns={columns}
|
||||
pagination={pagination}
|
||||
tableAlertRender={() => {
|
||||
return false;
|
||||
}}
|
||||
loading={loading}
|
||||
onChange={(data: any) => {
|
||||
const { current, pageSize, total } = data;
|
||||
const pagin = {
|
||||
current,
|
||||
pageSize,
|
||||
total,
|
||||
};
|
||||
setPagination(pagin);
|
||||
queryMetricList({ ...pagin, ...filterParams });
|
||||
}}
|
||||
size="small"
|
||||
options={{ reload: false, density: false, fullScreen: false }}
|
||||
/>
|
||||
<>
|
||||
{filterParams.showType ? (
|
||||
<Spin spinning={loading} style={{ minHeight: 500 }}>
|
||||
<MetricCardList
|
||||
metricList={dataSource}
|
||||
disabledEdit={true}
|
||||
onMetricChange={(metricItem) => {
|
||||
setInfoDrawerVisible(true);
|
||||
setMetricItem(metricItem);
|
||||
}}
|
||||
onDeleteBtnClick={(metricItem) => {
|
||||
deleteMetricQuery(metricItem.id);
|
||||
}}
|
||||
onEditBtnClick={(metricItem) => {
|
||||
setMetricItem(metricItem);
|
||||
setCreateModalVisible(true);
|
||||
}}
|
||||
/>
|
||||
</Spin>
|
||||
) : (
|
||||
<ProTable
|
||||
className={`${styles.metricTable}`}
|
||||
actionRef={actionRef}
|
||||
rowKey="id"
|
||||
search={false}
|
||||
dataSource={dataSource}
|
||||
columns={columns}
|
||||
pagination={pagination}
|
||||
tableAlertRender={() => {
|
||||
return false;
|
||||
}}
|
||||
loading={loading}
|
||||
onChange={(data: any) => {
|
||||
const { current, pageSize, total } = data;
|
||||
const pagin = {
|
||||
current,
|
||||
pageSize,
|
||||
total,
|
||||
};
|
||||
setPagination(pagin);
|
||||
queryMetricList({ ...pagin, ...filterParams });
|
||||
}}
|
||||
size="small"
|
||||
options={{ reload: false, density: false, fullScreen: false }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
{createModalVisible && (
|
||||
<MetricInfoCreateForm
|
||||
domainId={Number(selectDomainId)}
|
||||
@@ -276,7 +323,7 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
||||
metricItem={metricItem}
|
||||
onSubmit={() => {
|
||||
setCreateModalVisible(false);
|
||||
queryMetricList();
|
||||
queryMetricList(filterParams);
|
||||
dispatch({
|
||||
type: 'domainManger/queryMetricList',
|
||||
payload: {
|
||||
@@ -289,6 +336,26 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{infoDrawerVisible && (
|
||||
<NodeInfoDrawer
|
||||
nodeData={{ ...metricItem, nodeType: SemanticNodeType.METRIC }}
|
||||
placement="right"
|
||||
onClose={() => {
|
||||
setInfoDrawerVisible(false);
|
||||
}}
|
||||
width="100%"
|
||||
open={infoDrawerVisible}
|
||||
mask={true}
|
||||
getContainer={false}
|
||||
onEditBtnClick={(nodeData: any) => {
|
||||
handleMetricEdit(nodeData);
|
||||
}}
|
||||
maskClosable={true}
|
||||
onNodeChange={({ eventName }: { eventName: string }) => {
|
||||
setInfoDrawerVisible(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -80,4 +80,38 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.overviewExtraContainer {
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
.extraWrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
.extraStatistic {
|
||||
display: inline-flex;
|
||||
color: rgba(42, 46, 54, 0.65);
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.5714285714285714;
|
||||
list-style: none;
|
||||
.extraTitle {
|
||||
font-size: 12px;
|
||||
min-width: 50px;
|
||||
margin-inline-end: 6px;
|
||||
margin-block-end: 0;
|
||||
margin-bottom: 4px;
|
||||
color: rgba(42, 46, 54, 0.45);
|
||||
}
|
||||
.extraValue {
|
||||
font-size: 12px;
|
||||
color: rgba(42, 46, 54, 0.65);
|
||||
display: inline-block;
|
||||
direction: ltr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,4 @@
|
||||
import {
|
||||
Button,
|
||||
Drawer,
|
||||
message,
|
||||
Row,
|
||||
Col,
|
||||
Divider,
|
||||
Tag,
|
||||
Space,
|
||||
Typography,
|
||||
Popconfirm,
|
||||
} from 'antd';
|
||||
import { Button, Drawer, message, Row, Col, Divider, Tag, Space, Popconfirm } from 'antd';
|
||||
import React, { useState, useEffect, ReactNode } from 'react';
|
||||
import { SemanticNodeType } from '../../enum';
|
||||
import { deleteDimension, deleteMetric, deleteDatasource } from '../../service';
|
||||
@@ -18,11 +7,9 @@ import type { StateType } from '../../model';
|
||||
import moment from 'moment';
|
||||
import styles from '../style.less';
|
||||
import TransTypeTag from '../../components/TransTypeTag';
|
||||
import { MetricTypeWording } from '../../enum';
|
||||
import MetricTrendSection from '@/pages/SemanticModel/Metric/components/MetricTrendSection';
|
||||
import { SENSITIVE_LEVEL_ENUM } from '../../constant';
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
|
||||
type Props = {
|
||||
nodeData: any;
|
||||
domainManger: StateType;
|
||||
@@ -46,7 +33,8 @@ type InfoListItemChildrenItem = {
|
||||
type InfoListItem = {
|
||||
title: string;
|
||||
hideItem?: boolean;
|
||||
children: InfoListItemChildrenItem[];
|
||||
render?: () => ReactNode;
|
||||
children?: InfoListItemChildrenItem[];
|
||||
};
|
||||
|
||||
const DescriptionItem = ({ title, content }: DescriptionItemProps) => (
|
||||
@@ -66,7 +54,8 @@ const NodeInfoDrawer: React.FC<Props> = ({
|
||||
...restProps
|
||||
}) => {
|
||||
const [infoList, setInfoList] = useState<InfoListItem[]>([]);
|
||||
const { selectDomainName } = domainManger;
|
||||
const { selectModelName } = domainManger;
|
||||
|
||||
useEffect(() => {
|
||||
if (!nodeData) {
|
||||
return;
|
||||
@@ -79,9 +68,9 @@ const NodeInfoDrawer: React.FC<Props> = ({
|
||||
createdAt,
|
||||
updatedAt,
|
||||
description,
|
||||
domainName,
|
||||
// domainName,
|
||||
sensitiveLevel,
|
||||
type,
|
||||
modelName,
|
||||
nodeType,
|
||||
} = nodeData;
|
||||
|
||||
@@ -99,9 +88,9 @@ const NodeInfoDrawer: React.FC<Props> = ({
|
||||
value: alias || '-',
|
||||
},
|
||||
{
|
||||
label: '所属主题域',
|
||||
value: domainName,
|
||||
content: <Tag>{domainName || selectDomainName}</Tag>,
|
||||
label: '所属模型',
|
||||
value: modelName,
|
||||
content: <Tag>{modelName || selectModelName}</Tag>,
|
||||
},
|
||||
|
||||
{
|
||||
@@ -113,26 +102,24 @@ const NodeInfoDrawer: React.FC<Props> = ({
|
||||
{
|
||||
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: '指标趋势',
|
||||
render: () => (
|
||||
<div key="指标趋势" style={{ display: 'block' }}>
|
||||
<Row key={`metricTrendSection`} style={{ marginBottom: 10, display: 'flex' }}>
|
||||
<Col span={24}>
|
||||
<MetricTrendSection nodeData={nodeData} />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '创建信息',
|
||||
children: [
|
||||
@@ -161,9 +148,9 @@ const NodeInfoDrawer: React.FC<Props> = ({
|
||||
value: bizName,
|
||||
},
|
||||
{
|
||||
label: '所属主题域',
|
||||
value: domainName,
|
||||
content: <Tag>{domainName || selectDomainName}</Tag>,
|
||||
label: '所属模型',
|
||||
value: modelName,
|
||||
content: <Tag>{modelName || selectModelName}</Tag>,
|
||||
},
|
||||
{
|
||||
label: '描述',
|
||||
@@ -259,31 +246,36 @@ const NodeInfoDrawer: React.FC<Props> = ({
|
||||
>
|
||||
<div key={nodeData?.id} className={styles.nodeInfoDrawerContent}>
|
||||
{infoList.map((item) => {
|
||||
const { children, title } = item;
|
||||
const { children, title, render } = 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>
|
||||
);
|
||||
})}
|
||||
{render?.() ||
|
||||
(Array.isArray(children) &&
|
||||
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>
|
||||
{extraNode}
|
||||
{nodeData?.hasAdminRes && extraNode}
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Modal, Button } from 'antd';
|
||||
import DimensionMetricRelationTableTransfer from './DimensionMetricRelationTableTransfer';
|
||||
import { ISemantic } from '../data';
|
||||
|
||||
import FormItemTitle from '@/components/FormHelper/FormItemTitle';
|
||||
|
||||
type Props = {
|
||||
onCancel: () => void;
|
||||
open: boolean;
|
||||
relationsInitialValue?: ISemantic.IDrillDownDimensionItem[];
|
||||
onSubmit: (relations: ISemantic.IDrillDownDimensionItem[]) => void;
|
||||
};
|
||||
|
||||
const DimensionAndMetricRelationModal: React.FC<Props> = ({
|
||||
open,
|
||||
relationsInitialValue,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
}) => {
|
||||
const [relationList, setRelationList] = useState<ISemantic.IDrillDownDimensionItem[]>([]);
|
||||
|
||||
const renderFooter = () => {
|
||||
return (
|
||||
<>
|
||||
<Button onClick={onCancel}>取消</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
onSubmit(relationList);
|
||||
}}
|
||||
>
|
||||
完成
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
width={1200}
|
||||
destroyOnClose
|
||||
title={
|
||||
<FormItemTitle
|
||||
title={'维度关联'}
|
||||
subTitle={'注意:完成指标信息更新后,维度关联配置信息才会被保存'}
|
||||
/>
|
||||
}
|
||||
maskClosable={false}
|
||||
open={open}
|
||||
footer={renderFooter()}
|
||||
onCancel={onCancel}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<DimensionMetricRelationTableTransfer
|
||||
relationsInitialValue={relationsInitialValue}
|
||||
onChange={(relations: ISemantic.IDrillDownDimensionItem[]) => {
|
||||
setRelationList(relations);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DimensionAndMetricRelationModal;
|
||||
@@ -0,0 +1,233 @@
|
||||
import { Table, Transfer, Checkbox } from 'antd';
|
||||
import type { ColumnsType, TableRowSelection } from 'antd/es/table/interface';
|
||||
import type { TransferItem } from 'antd/es/transfer';
|
||||
import type { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||
import difference from 'lodash/difference';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { connect } from 'umi';
|
||||
import type { StateType } from '../model';
|
||||
import TransTypeTag from './TransTypeTag';
|
||||
import TableTitleTooltips from '../components/TableTitleTooltips';
|
||||
import { ISemantic } from '../data';
|
||||
import { SemanticNodeType, TransType } from '../enum';
|
||||
|
||||
interface RecordType {
|
||||
id: number;
|
||||
key: string;
|
||||
name: string;
|
||||
transType: TransType.DIMENSION | TransType.METRIC;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
domainManger: StateType;
|
||||
relationsInitialValue?: ISemantic.IDrillDownDimensionItem[];
|
||||
onChange: (relations: ISemantic.IDrillDownDimensionItem[]) => void;
|
||||
};
|
||||
|
||||
const DimensionMetricRelationTableTransfer: React.FC<Props> = ({
|
||||
domainManger,
|
||||
relationsInitialValue,
|
||||
onChange,
|
||||
}) => {
|
||||
const { dimensionList } = domainManger;
|
||||
|
||||
const [targetKeys, setTargetKeys] = useState<string[]>([]);
|
||||
|
||||
const [checkedMap, setCheckedMap] = useState<Record<string, ISemantic.IDrillDownDimensionItem>>(
|
||||
{},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!Array.isArray(relationsInitialValue)) {
|
||||
return;
|
||||
}
|
||||
const ids = relationsInitialValue.map((item) => `${item.dimensionId}`);
|
||||
const relationMap = relationsInitialValue.reduce((relationCheckedMap, item: any) => {
|
||||
const { dimensionId, necessary } = item;
|
||||
relationCheckedMap[dimensionId] = {
|
||||
dimensionId: Number(dimensionId),
|
||||
necessary: necessary,
|
||||
};
|
||||
return relationCheckedMap;
|
||||
}, {});
|
||||
setCheckedMap(relationMap);
|
||||
setTargetKeys(ids);
|
||||
}, [relationsInitialValue]);
|
||||
|
||||
const updateRelationCheckedMap = (
|
||||
record: RecordType,
|
||||
updateData: ISemantic.IDrillDownDimensionItem,
|
||||
) => {
|
||||
const { id } = record;
|
||||
const relationCheckedMap = {
|
||||
...checkedMap,
|
||||
};
|
||||
const target = relationCheckedMap[id];
|
||||
if (target) {
|
||||
relationCheckedMap[id] = {
|
||||
...target,
|
||||
...updateData,
|
||||
};
|
||||
} else {
|
||||
relationCheckedMap[id] = {
|
||||
...updateData,
|
||||
};
|
||||
}
|
||||
setCheckedMap(relationCheckedMap);
|
||||
handleRealtionChange(targetKeys, relationCheckedMap);
|
||||
};
|
||||
|
||||
const handleRealtionChange = (
|
||||
targetKeys: string[],
|
||||
relationCheckedMap: Record<string, ISemantic.IDrillDownDimensionItem>,
|
||||
) => {
|
||||
const relations = targetKeys.reduce(
|
||||
(relationList: ISemantic.IDrillDownDimensionItem[], dimensionId: string) => {
|
||||
const target = relationCheckedMap[dimensionId];
|
||||
if (target) {
|
||||
relationList.push(target);
|
||||
} else {
|
||||
relationList.push({
|
||||
dimensionId: Number(dimensionId),
|
||||
necessary: false,
|
||||
});
|
||||
}
|
||||
return relationList;
|
||||
},
|
||||
[],
|
||||
);
|
||||
onChange?.(relations);
|
||||
};
|
||||
|
||||
const rightColumns: ColumnsType<RecordType> = [
|
||||
{
|
||||
dataIndex: 'name',
|
||||
title: '名称',
|
||||
},
|
||||
{
|
||||
dataIndex: 'transType',
|
||||
width: 80,
|
||||
title: '类型',
|
||||
render: (transType: SemanticNodeType) => {
|
||||
return <TransTypeTag type={transType} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: 'y',
|
||||
title: (
|
||||
<TableTitleTooltips
|
||||
title="是否绑定"
|
||||
tooltips="若勾选绑定,则在查询该指标数据时必须结合该维度进行查询"
|
||||
/>
|
||||
),
|
||||
width: 120,
|
||||
render: (_: any, record: RecordType) => {
|
||||
const { transType, id } = record;
|
||||
return transType === TransType.DIMENSION ? (
|
||||
<Checkbox
|
||||
checked={checkedMap[id]?.necessary}
|
||||
onChange={(e: CheckboxChangeEvent) => {
|
||||
updateRelationCheckedMap(record, { dimensionId: id, necessary: e.target.checked });
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const leftColumns: ColumnsType<RecordType> = [
|
||||
{
|
||||
dataIndex: 'name',
|
||||
title: '名称',
|
||||
},
|
||||
{
|
||||
dataIndex: 'transType',
|
||||
title: '类型',
|
||||
render: (transType) => {
|
||||
return <TransTypeTag type={transType} />;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Transfer
|
||||
showSearch
|
||||
titles={['未关联维度', '已关联维度']}
|
||||
dataSource={dimensionList.map((item) => {
|
||||
const transType = TransType.DIMENSION;
|
||||
const { id } = item;
|
||||
return {
|
||||
...item,
|
||||
transType,
|
||||
key: `${id}`,
|
||||
};
|
||||
})}
|
||||
listStyle={{
|
||||
width: 500,
|
||||
height: 600,
|
||||
}}
|
||||
filterOption={(inputValue: string, item: any) => {
|
||||
const { name } = item;
|
||||
if (name.includes(inputValue)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}}
|
||||
targetKeys={targetKeys}
|
||||
onChange={(newTargetKeys: string[]) => {
|
||||
setTargetKeys(newTargetKeys);
|
||||
handleRealtionChange(newTargetKeys, checkedMap);
|
||||
}}
|
||||
>
|
||||
{({
|
||||
direction,
|
||||
filteredItems,
|
||||
onItemSelectAll,
|
||||
onItemSelect,
|
||||
selectedKeys: listSelectedKeys,
|
||||
}) => {
|
||||
const columns = direction === 'left' ? leftColumns : rightColumns;
|
||||
const rowSelection: TableRowSelection<TransferItem> = {
|
||||
onSelectAll(selected, selectedRows) {
|
||||
const treeSelectedKeys = selectedRows.map(({ key }) => key);
|
||||
const diffKeys = selected
|
||||
? difference(treeSelectedKeys, listSelectedKeys)
|
||||
: difference(listSelectedKeys, treeSelectedKeys);
|
||||
onItemSelectAll(diffKeys as string[], selected);
|
||||
},
|
||||
onSelect({ key }, selected) {
|
||||
onItemSelect(key as string, selected);
|
||||
},
|
||||
selectedRowKeys: listSelectedKeys,
|
||||
};
|
||||
|
||||
return (
|
||||
<Table
|
||||
rowSelection={rowSelection}
|
||||
columns={columns}
|
||||
dataSource={filteredItems as any}
|
||||
size="small"
|
||||
pagination={false}
|
||||
scroll={{ y: 450 }}
|
||||
onRow={({ key }) => ({
|
||||
onClick: () => {
|
||||
onItemSelect(key as string, !listSelectedKeys.includes(key as string));
|
||||
},
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</Transfer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(({ domainManger }: { domainManger: StateType }) => ({
|
||||
domainManger,
|
||||
}))(DimensionMetricRelationTableTransfer);
|
||||
@@ -46,8 +46,8 @@ const DimensionInfoModal: React.FC<CreateFormProps> = ({
|
||||
return;
|
||||
}
|
||||
const queryParams = {
|
||||
...dimensionItem,
|
||||
domainId: selectDomainId,
|
||||
id: dimensionItem.id,
|
||||
...fieldsValue,
|
||||
};
|
||||
const { code, msg } = await updateDimension(queryParams);
|
||||
|
||||
@@ -23,6 +23,7 @@ import { formLayout } from '@/components/FormHelper/utils';
|
||||
import FormItemTitle from '@/components/FormHelper/FormItemTitle';
|
||||
import styles from './style.less';
|
||||
import { getMeasureListByModelId } from '../service';
|
||||
import DimensionAndMetricRelationModal from './DimensionAndMetricRelationModal';
|
||||
import TableTitleTooltips from '../components/TableTitleTooltips';
|
||||
import { creatExprMetric, updateExprMetric, mockMetricAlias, getMetricTags } from '../service';
|
||||
import { ISemantic } from '../data';
|
||||
@@ -77,6 +78,12 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
|
||||
|
||||
const [tagOptions, setTagOptions] = useState<{ label: string; value: string }[]>([]);
|
||||
|
||||
const [metricRelationModalOpenState, setMetricRelationModalOpenState] = useState<boolean>(false);
|
||||
|
||||
const [drillDownDimensions, setDrillDownDimensions] = useState<
|
||||
ISemantic.IDrillDownDimensionItem[]
|
||||
>(metricItem?.relateDimension?.drillDownDimensions || []);
|
||||
|
||||
const forward = () => setCurrentStep(currentStep + 1);
|
||||
const backward = () => setCurrentStep(currentStep - 1);
|
||||
|
||||
@@ -169,6 +176,10 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
|
||||
const saveMetric = async (fieldsValue: any) => {
|
||||
const queryParams = {
|
||||
modelId: isEdit ? metricItem.modelId : modelId,
|
||||
relateDimension: {
|
||||
...(metricItem?.relateDimension || {}),
|
||||
drillDownDimensions,
|
||||
},
|
||||
...fieldsValue,
|
||||
};
|
||||
const { typeParams, alias, dataFormatType } = queryParams;
|
||||
@@ -346,6 +357,23 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
|
||||
>
|
||||
<TextArea placeholder="请输入业务口径" />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label={
|
||||
<FormItemTitle
|
||||
title={'下钻维度配置'}
|
||||
subTitle={'配置下钻维度后,将可以在指标卡中进行下钻'}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
setMetricRelationModalOpenState(true);
|
||||
}}
|
||||
>
|
||||
设 置
|
||||
</Button>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label={
|
||||
<FormItemTitle
|
||||
@@ -362,22 +390,6 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
|
||||
</Radio.Group>
|
||||
</FormItem>
|
||||
|
||||
{/* <FormItem
|
||||
label={
|
||||
<FormItemTitle
|
||||
title={'是否展示为百分比'}
|
||||
subTitle={'开启后,指标数据展示时会根据配置进行格式化,如0.02 -> 2%'}
|
||||
/>
|
||||
}
|
||||
name="isPercent"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch
|
||||
onChange={(checked) => {
|
||||
form.setFieldValue(['dataFormat', 'needMultiply100'], checked);
|
||||
}}
|
||||
/>
|
||||
</FormItem> */}
|
||||
{(isPercentState || isDecimalState) && (
|
||||
<FormItem
|
||||
label={
|
||||
@@ -486,6 +498,17 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
|
||||
>
|
||||
{renderContent()}
|
||||
</Form>
|
||||
<DimensionAndMetricRelationModal
|
||||
relationsInitialValue={drillDownDimensions}
|
||||
open={metricRelationModalOpenState}
|
||||
onCancel={() => {
|
||||
setMetricRelationModalOpenState(false);
|
||||
}}
|
||||
onSubmit={(relations) => {
|
||||
setDrillDownDimensions(relations);
|
||||
setMetricRelationModalOpenState(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Result
|
||||
|
||||
@@ -167,6 +167,15 @@ export declare namespace ISemantic {
|
||||
expr: string;
|
||||
}
|
||||
|
||||
interface IDrillDownDimensionItem {
|
||||
dimensionId: number;
|
||||
necessary: boolean;
|
||||
}
|
||||
|
||||
interface IRelateDimension {
|
||||
drillDownDimensions: IDrillDownDimensionItem[];
|
||||
}
|
||||
|
||||
interface IMetricItem {
|
||||
createdBy: string;
|
||||
updatedBy: string;
|
||||
@@ -181,6 +190,7 @@ export declare namespace ISemantic {
|
||||
sensitiveLevel: number;
|
||||
domainId: number;
|
||||
domainName: string;
|
||||
modelName: string;
|
||||
modelId: number;
|
||||
type: string;
|
||||
typeParams: ITypeParams;
|
||||
@@ -189,6 +199,31 @@ export declare namespace ISemantic {
|
||||
dataFormat: string;
|
||||
alias: string;
|
||||
useCnt: number;
|
||||
relateDimension?: IRelateDimension;
|
||||
}
|
||||
|
||||
interface IMetricTrendColumn {
|
||||
name: string;
|
||||
type: string;
|
||||
nameEn: string;
|
||||
showType: string;
|
||||
authorized: boolean;
|
||||
dataFormatType: string;
|
||||
dataFormat: {
|
||||
needMultiply100: boolean;
|
||||
decimalPlaces: number;
|
||||
};
|
||||
}
|
||||
|
||||
type IMetricTrendItem = Record<string, any>;
|
||||
interface IMetricTrend {
|
||||
columns: IMetricTrendColumn;
|
||||
resultList: IMetricTrendItem[];
|
||||
pageNo?: number;
|
||||
pageSize?: number;
|
||||
totalCount?: number;
|
||||
queryAuthorization?: string;
|
||||
sql?: string;
|
||||
}
|
||||
|
||||
type IDimensionList = IDimensionItem[];
|
||||
|
||||
@@ -365,3 +365,48 @@ export function searchDictLatestTaskList(data: any): Promise<any> {
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export function queryStruct({
|
||||
modelId,
|
||||
bizName,
|
||||
dateField = 'sys_imp_date',
|
||||
startDate,
|
||||
endDate,
|
||||
}: {
|
||||
modelId: number;
|
||||
bizName: string;
|
||||
dateField: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
}): Promise<any> {
|
||||
return request(`${process.env.API_BASE_URL}query/struct`, {
|
||||
method: 'POST',
|
||||
data: {
|
||||
modelId,
|
||||
groups: [dateField],
|
||||
aggregators: [
|
||||
{
|
||||
column: bizName,
|
||||
// func: 'SUM',
|
||||
nameCh: 'null',
|
||||
args: null,
|
||||
},
|
||||
],
|
||||
orders: [],
|
||||
dimensionFilters: [],
|
||||
metricFilters: [],
|
||||
params: [],
|
||||
dateInfo: {
|
||||
dateMode: 'BETWEEN',
|
||||
startDate,
|
||||
endDate,
|
||||
dateList: [],
|
||||
unit: 7,
|
||||
period: 'DAY',
|
||||
text: 'null',
|
||||
},
|
||||
limit: 365,
|
||||
nativeQuery: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user