mirror of
https://github.com/tencentmusic/supersonic.git
synced 2025-12-10 11:07:06 +00:00
[improvement][semantic-fe] add metric trend download functionality (#246)
* [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 * [improvement][semantic-fe] optimize the presentation of metric trend permissions * [improvement][semantic-fe] add metric trend download functionality
This commit is contained in:
@@ -100,7 +100,7 @@
|
|||||||
"sql-formatter": "^2.3.3",
|
"sql-formatter": "^2.3.3",
|
||||||
"supersonic-chat-sdk": "0.0.0",
|
"supersonic-chat-sdk": "0.0.0",
|
||||||
"umi": "3.5",
|
"umi": "3.5",
|
||||||
"umi-request": "^1.0.8"
|
"umi-request": "^1.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ant-design/pro-cli": "^2.0.2",
|
"@ant-design/pro-cli": "^2.0.2",
|
||||||
|
|||||||
@@ -75,14 +75,6 @@ const MDatePicker: React.FC<Props> = ({
|
|||||||
initialValues?.staticParams?.dateRangeType ||
|
initialValues?.staticParams?.dateRangeType ||
|
||||||
DateRangeType.DAY,
|
DateRangeType.DAY,
|
||||||
);
|
);
|
||||||
// const [pickerType, setPickerType] = useState<PickerType>(() => {
|
|
||||||
// // if (staticFormData.dateRangeType) {
|
|
||||||
// // return DateRangeTypeToPickerMap[staticFormData.dateRangeType];
|
|
||||||
// // }
|
|
||||||
// return DateRangePicker.DATE;
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const [dateRangeValue, setDateRangeValue] = useState<any>([]);
|
|
||||||
|
|
||||||
const staticDefaultConfig = {
|
const staticDefaultConfig = {
|
||||||
dateSettingType: DateSettingType.STATIC,
|
dateSettingType: DateSettingType.STATIC,
|
||||||
@@ -92,8 +84,7 @@ const MDatePicker: React.FC<Props> = ({
|
|||||||
dateMultiple: [],
|
dateMultiple: [],
|
||||||
dateRangeStringDesc: '',
|
dateRangeStringDesc: '',
|
||||||
};
|
};
|
||||||
// const { getMaxPartitionData } = useModel('useMaxPartitionData');
|
|
||||||
// const { globalViewId } = useModel('useViewsData');
|
|
||||||
const [latestDateMap, setLatestDateMap] = useState<LatestDateMap>({
|
const [latestDateMap, setLatestDateMap] = useState<LatestDateMap>({
|
||||||
maxPartition: moment().format('YYYY-MM-DD'),
|
maxPartition: moment().format('YYYY-MM-DD'),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -83,12 +83,17 @@ const MetricFilter: React.FC<Props> = ({ initFilterValues = {}, onFiltersChange
|
|||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
</StandardFormRow>
|
</StandardFormRow>
|
||||||
<Space size={80}>
|
<Space size={40}>
|
||||||
<StandardFormRow key="showType" title="切换为卡片" block>
|
<StandardFormRow key="showType" title="切换为卡片" block>
|
||||||
<FormItem name="showType" valuePropName="checked">
|
<FormItem name="showType" valuePropName="checked">
|
||||||
<Switch size="small" />
|
<Switch size="small" />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</StandardFormRow>
|
</StandardFormRow>
|
||||||
|
<StandardFormRow key="onlyShowMe" title="仅显示我的" block>
|
||||||
|
<FormItem name="onlyShowMe" valuePropName="checked">
|
||||||
|
<Switch size="small" />
|
||||||
|
</FormItem>
|
||||||
|
</StandardFormRow>
|
||||||
<StandardFormRow key="domainIds" title="所属主题域" block>
|
<StandardFormRow key="domainIds" title="所属主题域" block>
|
||||||
<FormItem name="domainIds">
|
<FormItem name="domainIds">
|
||||||
<DomainTreeSelect />
|
<DomainTreeSelect />
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { SemanticNodeType } from '../../enum';
|
import { SemanticNodeType } from '../../enum';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { message } from 'antd';
|
import { message, Row, Col, Button } from 'antd';
|
||||||
import { queryStruct } from '@/pages/SemanticModel/service';
|
import { queryStruct, downloadCosFile } from '@/pages/SemanticModel/service';
|
||||||
import TrendChart from '@/pages/SemanticModel/Metric/components/MetricTrend';
|
import TrendChart from '@/pages/SemanticModel/Metric/components/MetricTrend';
|
||||||
import MDatePicker from '@/components/MDatePicker';
|
import MDatePicker from '@/components/MDatePicker';
|
||||||
|
import { useModel } from 'umi';
|
||||||
import { DateRangeType, DateSettingType } from '@/components/MDatePicker/type';
|
import { DateRangeType, DateSettingType } from '@/components/MDatePicker/type';
|
||||||
import { ISemantic } from '../../data';
|
import { ISemantic } from '../../data';
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ const MetricTrendSection: React.FC<Props> = ({ nodeData }) => {
|
|||||||
const [metricTrendLoading, setMetricTrendLoading] = useState<boolean>(false);
|
const [metricTrendLoading, setMetricTrendLoading] = useState<boolean>(false);
|
||||||
const [metricColumnConfig, setMetricColumnConfig] = useState<ISemantic.IMetricTrendColumn>();
|
const [metricColumnConfig, setMetricColumnConfig] = useState<ISemantic.IMetricTrendColumn>();
|
||||||
const [authMessage, setAuthMessage] = useState<string>('');
|
const [authMessage, setAuthMessage] = useState<string>('');
|
||||||
|
const [downloadLoding, setDownloadLoding] = useState<boolean>(false);
|
||||||
const [periodDate, setPeriodDate] = useState<{
|
const [periodDate, setPeriodDate] = useState<{
|
||||||
startDate: string;
|
startDate: string;
|
||||||
endDate: string;
|
endDate: string;
|
||||||
@@ -34,17 +36,28 @@ const MetricTrendSection: React.FC<Props> = ({ nodeData }) => {
|
|||||||
dateField: dateFieldMap[DateRangeType.DAY],
|
dateField: dateFieldMap[DateRangeType.DAY],
|
||||||
});
|
});
|
||||||
|
|
||||||
const getMetricTrendData = async () => {
|
const getMetricTrendData = async (download = false) => {
|
||||||
setMetricTrendLoading(true);
|
if (download) {
|
||||||
|
setDownloadLoding(true);
|
||||||
|
} else {
|
||||||
|
setMetricTrendLoading(true);
|
||||||
|
}
|
||||||
|
|
||||||
const { modelId, bizName, name } = nodeData;
|
const { modelId, bizName, name } = nodeData;
|
||||||
indicatorFields.current = [{ name, column: bizName }];
|
indicatorFields.current = [{ name, column: bizName }];
|
||||||
const { code, data, msg } = await queryStruct({
|
const res = await queryStruct({
|
||||||
modelId,
|
modelId,
|
||||||
bizName,
|
bizName,
|
||||||
dateField: periodDate.dateField,
|
dateField: periodDate.dateField,
|
||||||
startDate: periodDate.startDate,
|
startDate: periodDate.startDate,
|
||||||
endDate: periodDate.endDate,
|
endDate: periodDate.endDate,
|
||||||
|
download,
|
||||||
});
|
});
|
||||||
|
if (download) {
|
||||||
|
setDownloadLoding(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { code, data, msg } = res;
|
||||||
setMetricTrendLoading(false);
|
setMetricTrendLoading(false);
|
||||||
if (code === 200) {
|
if (code === 200) {
|
||||||
const { resultList, columns, queryAuthorization } = data;
|
const { resultList, columns, queryAuthorization } = data;
|
||||||
@@ -78,42 +91,52 @@ const MetricTrendSection: React.FC<Props> = ({ nodeData }) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={{ marginBottom: 5 }}>
|
<div style={{ marginBottom: 5 }}>
|
||||||
<MDatePicker
|
<Row>
|
||||||
initialValues={{
|
<Col flex="1 1 200px">
|
||||||
dateSettingType: 'DYNAMIC',
|
<MDatePicker
|
||||||
dynamicParams: {
|
initialValues={{
|
||||||
number: 7,
|
dateSettingType: 'DYNAMIC',
|
||||||
periodType: 'DAYS',
|
dynamicParams: {
|
||||||
includesCurrentPeriod: true,
|
number: 7,
|
||||||
shortCutId: 'last7Days',
|
periodType: 'DAYS',
|
||||||
dateRangeType: 'DAY',
|
includesCurrentPeriod: true,
|
||||||
dynamicAdvancedConfigType: 'last',
|
shortCutId: 'last7Days',
|
||||||
dateRangeStringDesc: '最近7天',
|
dateRangeType: 'DAY',
|
||||||
dateSettingType: DateSettingType.DYNAMIC,
|
dynamicAdvancedConfigType: 'last',
|
||||||
},
|
dateRangeStringDesc: '最近7天',
|
||||||
staticParams: {},
|
dateSettingType: DateSettingType.DYNAMIC,
|
||||||
}}
|
},
|
||||||
onDateRangeChange={(value, config) => {
|
staticParams: {},
|
||||||
const [startDate, endDate] = value;
|
}}
|
||||||
const { dateSettingType, dynamicParams, staticParams } = config;
|
onDateRangeChange={(value, config) => {
|
||||||
let dateField = dateFieldMap[DateRangeType.DAY];
|
const [startDate, endDate] = value;
|
||||||
if (DateSettingType.DYNAMIC === dateSettingType) {
|
const { dateSettingType, dynamicParams, staticParams } = config;
|
||||||
dateField = dateFieldMap[dynamicParams.dateRangeType];
|
let dateField = dateFieldMap[DateRangeType.DAY];
|
||||||
}
|
if (DateSettingType.DYNAMIC === dateSettingType) {
|
||||||
if (DateSettingType.STATIC === dateSettingType) {
|
dateField = dateFieldMap[dynamicParams.dateRangeType];
|
||||||
dateField = dateFieldMap[staticParams.dateRangeType];
|
}
|
||||||
}
|
if (DateSettingType.STATIC === dateSettingType) {
|
||||||
setPeriodDate({ startDate, endDate, dateField });
|
dateField = dateFieldMap[staticParams.dateRangeType];
|
||||||
}}
|
}
|
||||||
disabledAdvanceSetting={true}
|
setPeriodDate({ startDate, endDate, dateField });
|
||||||
/>
|
}}
|
||||||
|
disabledAdvanceSetting={true}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col flex="0 1">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
loading={downloadLoding}
|
||||||
|
onClick={() => {
|
||||||
|
getMetricTrendData(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
下载
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
{authMessage && (
|
{authMessage && <div style={{ color: '#d46b08', marginBottom: 15 }}>{authMessage}</div>}
|
||||||
<div style={{ color: '#d46b08', marginBottom: 15 }}>
|
|
||||||
指标存在如下权限问题: {authMessage}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div style={{ color: '#d46b08', marginBottom: 15 }}>指标存在如下权限问题: {authMessage}</div>
|
|
||||||
<TrendChart
|
<TrendChart
|
||||||
data={metricTrendData}
|
data={metricTrendData}
|
||||||
isPer={
|
isPer={
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import ProTable from '@ant-design/pro-table';
|
|||||||
import { message, Space, Popconfirm, Tag, Spin } from 'antd';
|
import { message, Space, Popconfirm, Tag, Spin } from 'antd';
|
||||||
import React, { useRef, useState, useEffect } from 'react';
|
import React, { useRef, useState, useEffect } from 'react';
|
||||||
import type { Dispatch } from 'umi';
|
import type { Dispatch } from 'umi';
|
||||||
import { connect, history } from 'umi';
|
import { connect, history, useModel } from 'umi';
|
||||||
import type { StateType } from '../model';
|
import type { StateType } from '../model';
|
||||||
import { SENSITIVE_LEVEL_ENUM } from '../constant';
|
import { SENSITIVE_LEVEL_ENUM } from '../constant';
|
||||||
import { queryMetric, deleteMetric } from '../service';
|
import { queryMetric, deleteMetric } from '../service';
|
||||||
@@ -31,6 +31,10 @@ type QueryMetricListParams = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
||||||
|
const { initialState = {} } = useModel('@@initialState');
|
||||||
|
|
||||||
|
const { currentUser = {} } = initialState as any;
|
||||||
|
|
||||||
const { selectDomainId, selectModelId: modelId } = domainManger;
|
const { selectDomainId, selectModelId: modelId } = domainManger;
|
||||||
const [createModalVisible, setCreateModalVisible] = useState<boolean>(false);
|
const [createModalVisible, setCreateModalVisible] = useState<boolean>(false);
|
||||||
const defaultPagination = {
|
const defaultPagination = {
|
||||||
@@ -59,6 +63,7 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
|||||||
const { code, data, msg } = await queryMetric({
|
const { code, data, msg } = await queryMetric({
|
||||||
...pagination,
|
...pagination,
|
||||||
...params,
|
...params,
|
||||||
|
createdBy: params.onlyShowMe ? currentUser.name : null,
|
||||||
pageSize: params.showType ? 100 : defaultPagination.pageSize,
|
pageSize: params.showType ? 100 : defaultPagination.pageSize,
|
||||||
});
|
});
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -184,15 +189,6 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
|
|||||||
title: '描述',
|
title: '描述',
|
||||||
search: false,
|
search: false,
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// dataIndex: 'type',
|
|
||||||
// title: '指标类型',
|
|
||||||
// valueEnum: {
|
|
||||||
// ATOMIC: '原子指标',
|
|
||||||
// DERIVED: '衍生指标',
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
|
|
||||||
{
|
{
|
||||||
dataIndex: 'updatedAt',
|
dataIndex: 'updatedAt',
|
||||||
title: '更新时间',
|
title: '更新时间',
|
||||||
|
|||||||
@@ -62,13 +62,11 @@ const NodeInfoDrawer: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
const {
|
const {
|
||||||
alias,
|
alias,
|
||||||
fullPath,
|
|
||||||
bizName,
|
bizName,
|
||||||
createdBy,
|
createdBy,
|
||||||
createdAt,
|
createdAt,
|
||||||
updatedAt,
|
updatedAt,
|
||||||
description,
|
description,
|
||||||
// domainName,
|
|
||||||
sensitiveLevel,
|
sensitiveLevel,
|
||||||
modelName,
|
modelName,
|
||||||
nodeType,
|
nodeType,
|
||||||
@@ -111,13 +109,11 @@ const NodeInfoDrawer: React.FC<Props> = ({
|
|||||||
{
|
{
|
||||||
title: '指标趋势',
|
title: '指标趋势',
|
||||||
render: () => (
|
render: () => (
|
||||||
<div key="指标趋势" style={{ display: 'block' }}>
|
<Row key={`metricTrendSection`} style={{ marginBottom: 10, display: 'flex' }}>
|
||||||
<Row key={`metricTrendSection`} style={{ marginBottom: 10, display: 'flex' }}>
|
<Col span={24}>
|
||||||
<Col span={24}>
|
<MetricTrendSection nodeData={nodeData} />
|
||||||
<MetricTrendSection nodeData={nodeData} />
|
</Col>
|
||||||
</Col>
|
</Row>
|
||||||
</Row>
|
|
||||||
</div>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import request from 'umi-request';
|
import request from 'umi-request';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { AUTH_TOKEN_KEY } from '@/common/constants';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
const getRunningEnv = () => {
|
const getRunningEnv = () => {
|
||||||
return window.location.pathname.includes('/chatSetting/') ? 'chat' : 'semantic';
|
return window.location.pathname.includes('/chatSetting/') ? 'chat' : 'semantic';
|
||||||
@@ -366,47 +369,69 @@ export function searchDictLatestTaskList(data: any): Promise<any> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function queryStruct({
|
const downloadStruct = (blob: Blob) => {
|
||||||
|
const fieldName = `supersonic_${moment().format('YYYYMMDDhhmmss')}.xlsx`;
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = URL.createObjectURL(new Blob([blob]));
|
||||||
|
link.download = fieldName;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
URL.revokeObjectURL(link.href);
|
||||||
|
document.body.removeChild(link);
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function queryStruct({
|
||||||
modelId,
|
modelId,
|
||||||
bizName,
|
bizName,
|
||||||
dateField = 'sys_imp_date',
|
dateField = 'sys_imp_date',
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
|
download = false,
|
||||||
}: {
|
}: {
|
||||||
modelId: number;
|
modelId: number;
|
||||||
bizName: string;
|
bizName: string;
|
||||||
dateField: string;
|
dateField: string;
|
||||||
startDate: string;
|
startDate: string;
|
||||||
endDate: string;
|
endDate: string;
|
||||||
|
download?: boolean;
|
||||||
}): Promise<any> {
|
}): Promise<any> {
|
||||||
return request(`${process.env.API_BASE_URL}query/struct`, {
|
const response = await request(
|
||||||
method: 'POST',
|
`${process.env.API_BASE_URL}query/${download ? 'download/' : ''}struct`,
|
||||||
data: {
|
{
|
||||||
modelId,
|
method: 'POST',
|
||||||
groups: [dateField],
|
...(download ? { responseType: 'blob', getResponse: true } : {}),
|
||||||
aggregators: [
|
data: {
|
||||||
{
|
modelId,
|
||||||
column: bizName,
|
groups: [dateField],
|
||||||
// func: 'SUM',
|
aggregators: [
|
||||||
nameCh: 'null',
|
{
|
||||||
args: null,
|
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,
|
||||||
orders: [],
|
nativeQuery: false,
|
||||||
dimensionFilters: [],
|
|
||||||
metricFilters: [],
|
|
||||||
params: [],
|
|
||||||
dateInfo: {
|
|
||||||
dateMode: 'BETWEEN',
|
|
||||||
startDate,
|
|
||||||
endDate,
|
|
||||||
dateList: [],
|
|
||||||
unit: 7,
|
|
||||||
period: 'DAY',
|
|
||||||
text: 'null',
|
|
||||||
},
|
},
|
||||||
limit: 365,
|
|
||||||
nativeQuery: false,
|
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
|
if (download) {
|
||||||
|
downloadStruct(response.data);
|
||||||
|
} else {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,11 +39,13 @@ const responseInterceptor = async (response: Response) => {
|
|||||||
const contextpath = response.headers.get('contextpath');
|
const contextpath = response.headers.get('contextpath');
|
||||||
win.location.href = contextpath;
|
win.location.href = contextpath;
|
||||||
} else {
|
} else {
|
||||||
const data: Result<any> = await response?.clone()?.json?.();
|
try {
|
||||||
if (Number(data.code) === 403) {
|
const data: Result<any> = await response?.clone()?.json?.();
|
||||||
history.push('/login');
|
if (Number(data.code) === 403) {
|
||||||
return response;
|
history.push('/login');
|
||||||
}
|
return response;
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
|
|||||||
Reference in New Issue
Block a user