[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:
tristanliu
2023-10-16 06:10:37 -05:00
committed by GitHub
parent 37bb9ff767
commit c5536aa25d
23 changed files with 2997 additions and 173 deletions

View File

@@ -0,0 +1,547 @@
import React, { useState, useEffect } from 'react';
import type { RadioChangeEvent } from 'antd';
import { QuestionCircleOutlined } from '@ant-design/icons';
import { objToList } from '@/utils/utils';
// import { LatestDateMap } from '@/services/global/type';
import { DateRangeType, DateRangeTypeToPickerMap, DateRangePicker } from './type';
import {
SHORT_CUT_ITEM_LIST,
datePeriodTypeWordingMap,
getDynamicDateRangeStringByParams,
getDateStrings,
datePeriodTypeMap,
dateRangeTypeExchangeDatePeriodTypeMap,
} from './utils';
import {
DynamicAdvancedConfigType,
DatePeriodType,
PerDatePeriodType,
DateSettingType,
} from './type';
import {
Typography,
Space,
Collapse,
Tag,
Row,
Col,
Radio,
InputNumber,
Select,
Checkbox,
Tooltip,
DatePicker,
} from 'antd';
import moment from 'moment';
import styles from './style.less';
const { CheckableTag } = Tag;
const { Panel } = Collapse;
const { Link } = Typography;
const { Option } = Select;
type Props = {
initialValues?: any;
submitFormDataState?: boolean;
dateRangeTypeProps?: DateRangeType;
onDateRangeChange: (value: string[], config: any) => void;
onAdvanceSettingCollapsedChange?: (collapse: boolean) => void;
onShortCutClick?: (shortCutId: string) => void;
onDateRangeStringAndDescChange?: ({
dateRangeString,
dateRangeStringDesc,
}: {
dateRangeString: string[];
dateRangeStringDesc: string;
}) => void;
disabledAdvanceSetting?: boolean;
};
const DynamicDate: React.FC<Props> = ({
initialValues,
dateRangeTypeProps,
submitFormDataState,
onDateRangeChange,
onAdvanceSettingCollapsedChange,
onShortCutClick,
onDateRangeStringAndDescChange,
disabledAdvanceSetting = false,
}: any) => {
const initAdvacedConfigPanelCollapsed = () => {
return !initialValues?.dateSettingType || initialValues?.shortCutId ? [] : ['1'];
};
const [advancedPanelFormResetState, setAdvancedPanelFormResetState] =
useState(dateRangeTypeProps);
useEffect(() => {
// 当时间粒度发生变化时重置高级设置面板
resetAdvancedPanelForm(dateRangeTypeProps);
}, [dateRangeTypeProps]);
useEffect(() => {
if (initialValues?.dateSettingType !== DateSettingType.DYNAMIC) {
return;
}
initShortCutByDateRangeChange(dateRangeTypeProps);
}, [advancedPanelFormResetState]);
const resetAdvancedPanelForm = (dateRangeType: DateRangeType) => {
const lastConfigTypeFormData = advancedPanelFormData[DynamicAdvancedConfigType.LAST];
const historyConfigTypeFormData = advancedPanelFormData[DynamicAdvancedConfigType.HISTORY];
switch (dateRangeType) {
case DateRangeType.DAY:
setAdvancedPanelFormData({
...advancedPanelFormData,
[DynamicAdvancedConfigType.LAST]: {
...lastConfigTypeFormData,
periodType: DatePeriodType.DAY,
},
[DynamicAdvancedConfigType.HISTORY]: {
...historyConfigTypeFormData,
periodType: DatePeriodType.DAY,
},
});
break;
case DateRangeType.WEEK:
setAdvancedPanelFormData({
...advancedPanelFormData,
[DynamicAdvancedConfigType.LAST]: {
...lastConfigTypeFormData,
periodType: DatePeriodType.WEEK,
},
[DynamicAdvancedConfigType.HISTORY]: {
...historyConfigTypeFormData,
periodType: DatePeriodType.WEEK,
},
});
break;
case DateRangeType.MONTH:
setAdvancedPanelFormData({
...advancedPanelFormData,
[DynamicAdvancedConfigType.LAST]: {
...lastConfigTypeFormData,
periodType: DatePeriodType.MONTH,
},
[DynamicAdvancedConfigType.HISTORY]: {
...historyConfigTypeFormData,
periodType: DatePeriodType.MONTH,
},
});
break;
default:
break;
}
setAdvancedPanelFormResetState(dateRangeType);
setAdvacedConfigPanelCollapsed([]);
};
const initAdvancedConfigType = () => {
return initialValues?.dynamicAdvancedConfigType;
};
const initAdvancedPanelFormData = () => {
let defaultConfig = {
[DynamicAdvancedConfigType.LAST]: {
number: 1,
periodType: dateRangeTypeExchangeDatePeriodTypeMap[dateRangeTypeProps],
includesCurrentPeriod: false,
},
[DynamicAdvancedConfigType.HISTORY]: {
number: 1,
periodType: dateRangeTypeExchangeDatePeriodTypeMap[dateRangeTypeProps],
},
[DynamicAdvancedConfigType.FROM_DATE_PERIOD]: {
perPeriodType: PerDatePeriodType.PERDAY,
},
[DynamicAdvancedConfigType.FROM_DATE]: { date: moment() },
};
if (advancedPanelFormData) {
defaultConfig = { ...advancedPanelFormData };
}
if (initialValues?.dynamicAdvancedConfigType) {
const { dynamicAdvancedConfigType } = initialValues;
const targetConfig = defaultConfig[dynamicAdvancedConfigType];
if (!targetConfig) {
return defaultConfig;
}
const mergeConfig = Object.keys(targetConfig).reduce((result, key) => {
return {
...result,
[key]: initialValues[key],
};
}, {});
defaultConfig[dynamicAdvancedConfigType] = mergeConfig;
}
return defaultConfig;
};
const initShortCutSettingChecked = () => {
return initialValues?.shortCutId || '';
};
const [advacedConfigPanelCollapsed, setAdvacedConfigPanelCollapsed] = useState<string | string[]>(
initAdvacedConfigPanelCollapsed(),
);
const [advancedConfigType, setAdvancedConfigType] = useState<
DynamicAdvancedConfigType | undefined
>(initAdvancedConfigType());
const [advancedPanelFormData, setAdvancedPanelFormData] = useState<any>(
initAdvancedPanelFormData(),
);
const [shortCutSettingChecked, setShortCutSettingChecked] = useState<string>(
initShortCutSettingChecked(),
);
useEffect(() => {
// 外部状态触发表单数据提交
if (submitFormDataState && advancedConfigType) {
updateAdvancedPanelFormData(
advancedPanelFormData[advancedConfigType],
advancedConfigType,
true,
);
}
}, [submitFormDataState]);
const init = () => {
setAdvacedConfigPanelCollapsed(initAdvacedConfigPanelCollapsed());
setAdvancedConfigType(initAdvancedConfigType());
setAdvancedPanelFormData(initAdvancedPanelFormData());
setShortCutSettingChecked(initShortCutSettingChecked());
};
useEffect(() => {
if (initialValues?.dateSettingType === DateSettingType.DYNAMIC) {
init();
}
}, [initialValues]);
const handleDateRangeChange = (dateRange: string[], config: any) => {
onDateRangeChange(dateRange, {
...config,
dateSettingType: DateSettingType.DYNAMIC,
});
};
const updateAdvancedPanelFormData = (
formData: any,
configType: DynamicAdvancedConfigType,
emitImmediately = false,
) => {
const mergeConfigTypeData = {
...advancedPanelFormData[configType],
...formData,
// shortCutId: shortCutSettingChecked,
dateRangeType: dateRangeTypeProps,
dynamicAdvancedConfigType: configType,
};
const { dateRangeString, dateRangeStringDesc } = getDynamicDateRangeStringByParams(
mergeConfigTypeData,
configType,
{ maxPartition: moment().format('YYYY-MM-DD') },
);
mergeConfigTypeData.dateRangeStringDesc = dateRangeStringDesc;
onDateRangeStringAndDescChange?.({ dateRangeString, dateRangeStringDesc });
if (emitImmediately) {
handleDateRangeChange(dateRangeString, mergeConfigTypeData);
}
setAdvancedPanelFormData({
...advancedPanelFormData,
[configType]: mergeConfigTypeData,
});
};
// 根据当前时间粒度判断高级设置中时间区间选项哪些可用
const isDisabledDatePeriodTypeOption = (
datePeriodType: DatePeriodType,
dateRangeType: DateRangeType,
) => {
switch (datePeriodType) {
case DatePeriodType.DAY:
return ![DateRangeType.DAY].includes(dateRangeType);
case DatePeriodType.WEEK:
return ![DateRangeType.DAY, DateRangeType.WEEK].includes(dateRangeType);
case DatePeriodType.MONTH:
return false;
case DatePeriodType.YEAR:
return false;
default:
return false;
}
};
const getDatePeriodTypeOptions = (dateRangeType: DateRangeType) => {
const list = objToList(datePeriodTypeMap);
const optionList = list.reduce((result: any[], { value, label }: any) => {
const isDisabled = isDisabledDatePeriodTypeOption(value, dateRangeType);
if (isDisabled) {
return result;
}
result.push(
<Option value={value} key={value}>
{label}
</Option>,
);
return result;
}, []);
return optionList;
};
const isAdvancedConfigTypeRadioDisabled = (type: DynamicAdvancedConfigType) => {
return type !== advancedConfigType;
};
const initShortCutByDateRangeChange = (dateRangeType: DateRangeType, emitImmediately = false) => {
const shortCutList = SHORT_CUT_ITEM_LIST.filter((item) => {
return item.dateRangeType === dateRangeType;
});
const firstItem = shortCutList[0];
if (firstItem) {
handleShortCutChange(firstItem, emitImmediately);
}
};
const handleShortCutChange = (item: any, emitImmediately = true) => {
const { id, advancedConfigType, initData } = item;
// 设置选中状态
setShortCutSettingChecked(id);
// 设置快捷选项AdvancedConfigType类型
setAdvancedConfigType(advancedConfigType);
// 更新数据至表单数据并立即向上层组件传递
updateAdvancedPanelFormData(initData, advancedConfigType, emitImmediately);
if (emitImmediately) {
// 触发快捷选项点击时间,上层组件关闭配置浮窗
onShortCutClick?.(id);
}
};
const handleAdvancedPanelFormChange = () => {
// 当高级面板表单发生变化时,重置快捷选项为空
setShortCutSettingChecked('');
};
return (
<>
<div className={styles.dateShortCutSettingContent}>
<Row>
{SHORT_CUT_ITEM_LIST.map((item) => {
const { id, text, dateRangeType } = item;
if (dateRangeType === dateRangeTypeProps) {
return (
<Col key={`row-col-${id}`}>
<CheckableTag
className={styles['ant-tag-checkable']}
checked={shortCutSettingChecked === id}
key={`row-col-tag-${id}`}
onChange={() => {
handleShortCutChange(item);
}}
>
<div className={styles['tag-value-box']}>{text}</div>
</CheckableTag>
</Col>
);
}
return undefined;
})}
</Row>
</div>
{!disabledAdvanceSetting && (
<div className={styles.dateAdvancedSettingContent}>
<Collapse
// defaultActiveKey={['1']}
activeKey={advacedConfigPanelCollapsed}
onChange={(key: string | string[]) => {
setAdvacedConfigPanelCollapsed(key);
if (key.length === 0) {
onAdvanceSettingCollapsedChange?.(false);
return;
}
onAdvanceSettingCollapsedChange?.(true);
}}
bordered={false}
ghost={true}
expandIconPosition="right"
>
<Panel
header=""
// collapsible={'disabled'}
key="1"
extra={
<Space>
<Link></Link>
</Space>
}
>
<div>
<Space>
<div style={{ color: 'rgba(0, 0, 0, 0.85)' }}></div>
<Tooltip
title={`日期随着时间推移而更新。 若在1月1日设置查询日期为“今天” 则第二天的查询日期为1月2日。`}
>
<QuestionCircleOutlined />
</Tooltip>
</Space>
</div>
<Radio.Group
onChange={(e: RadioChangeEvent) => {
const configType = e.target.value;
setAdvancedConfigType(configType);
updateAdvancedPanelFormData(advancedPanelFormData[configType], configType);
handleAdvancedPanelFormChange();
}}
value={advancedConfigType}
>
<Space direction="vertical">
<Radio value={DynamicAdvancedConfigType.LAST}>
<Space size={10}>
<span className={styles.advancedSettingItemText}></span>
<InputNumber
style={{ width: 120 }}
placeholder="请输入数字"
min={1}
disabled={isAdvancedConfigTypeRadioDisabled(DynamicAdvancedConfigType.LAST)}
value={advancedPanelFormData[DynamicAdvancedConfigType.LAST].number}
onChange={(value: number | null) => {
updateAdvancedPanelFormData(
{ number: value },
DynamicAdvancedConfigType.LAST,
);
handleAdvancedPanelFormChange();
}}
/>
<Select
// defaultValue={DatePeriodType.DAY}
style={{ width: 120 }}
disabled={isAdvancedConfigTypeRadioDisabled(DynamicAdvancedConfigType.LAST)}
value={advancedPanelFormData[DynamicAdvancedConfigType.LAST].periodType}
onClick={(e) => {
// 禁止冒泡触发Radio点击后续逻辑
e.preventDefault();
e.stopPropagation();
}}
onChange={(value: string) => {
updateAdvancedPanelFormData(
{ periodType: value },
DynamicAdvancedConfigType.LAST,
);
handleAdvancedPanelFormChange();
}}
>
{getDatePeriodTypeOptions(dateRangeTypeProps)}
</Select>
<Checkbox
disabled={isAdvancedConfigTypeRadioDisabled(DynamicAdvancedConfigType.LAST)}
checked={
advancedPanelFormData[DynamicAdvancedConfigType.LAST]
.includesCurrentPeriod
}
onChange={(e) => {
const isChecked = e.target.checked;
updateAdvancedPanelFormData(
{ includesCurrentPeriod: isChecked },
DynamicAdvancedConfigType.LAST,
);
handleAdvancedPanelFormChange();
}}
>
{
datePeriodTypeWordingMap[
advancedPanelFormData[DynamicAdvancedConfigType.LAST].periodType
]
}
</Checkbox>
</Space>
</Radio>
<Radio value={DynamicAdvancedConfigType.HISTORY}>
<Space size={10}>
<span className={styles.advancedSettingItemText}></span>
<InputNumber
style={{ width: 120 }}
placeholder="请输入数字"
min={1}
disabled={isAdvancedConfigTypeRadioDisabled(
DynamicAdvancedConfigType.HISTORY,
)}
value={advancedPanelFormData[DynamicAdvancedConfigType.HISTORY].number}
// defaultValue={3}
onChange={(value: number | null) => {
updateAdvancedPanelFormData(
{ number: value },
DynamicAdvancedConfigType.HISTORY,
);
handleAdvancedPanelFormChange();
}}
/>
<Select
// defaultValue={DatePeriodType.DAY}
style={{ width: 120 }}
disabled={isAdvancedConfigTypeRadioDisabled(
DynamicAdvancedConfigType.HISTORY,
)}
value={advancedPanelFormData[DynamicAdvancedConfigType.HISTORY].periodType}
onClick={(e) => {
// 禁止冒泡触发Radio点击后续逻辑
e.preventDefault();
e.stopPropagation();
}}
onChange={(value: string) => {
updateAdvancedPanelFormData(
{ periodType: value },
DynamicAdvancedConfigType.HISTORY,
);
handleAdvancedPanelFormChange();
}}
>
{getDatePeriodTypeOptions(dateRangeTypeProps)}
</Select>
</Space>
</Radio>
<Radio value={DynamicAdvancedConfigType.FROM_DATE}>
<Space size={10}>
<span className={styles.advancedSettingItemText}></span>
<DatePicker
disabled={isAdvancedConfigTypeRadioDisabled(
DynamicAdvancedConfigType.FROM_DATE,
)}
value={moment(
advancedPanelFormData[DynamicAdvancedConfigType.FROM_DATE].date,
)}
disabledDate={(current) => {
return current && current > moment().endOf('day');
}}
picker={DateRangeTypeToPickerMap[dateRangeTypeProps]}
onChange={(date, dateString) => {
if (!date) {
return;
}
const picker = DateRangeTypeToPickerMap[dateRangeTypeProps];
if (picker === DateRangePicker.WEEK) {
date.startOf('week').format('YYYY-MM-DD');
}
if (picker === DateRangePicker.MONTH) {
date.startOf('month').format('YYYY-MM-DD');
}
updateAdvancedPanelFormData(
{ date },
DynamicAdvancedConfigType.FROM_DATE,
);
handleAdvancedPanelFormChange();
}}
/>
</Space>
</Radio>
</Space>
</Radio.Group>
</Panel>
</Collapse>
</div>
)}
</>
);
};
export default DynamicDate;

View File

@@ -0,0 +1,160 @@
import React, { useState, useEffect } from 'react';
import { Space, DatePicker } from 'antd';
import { DateMode, DateRangeType, DateRangePicker, DateRangeTypeToPickerMap } from './type';
import { getDateStrings } from './utils';
import { DateSettingType, StaticDateSelectMode } from './type';
import moment from 'moment';
const { RangePicker } = DatePicker;
type Props = {
initialValues: any;
currentDateSettingType?: DateSettingType;
selectMode?: StaticDateSelectMode;
dateRangeTypeProps?: DateRangeType;
onDateRangeChange: (value: string[], config: any) => void;
};
const StaticDate: React.FC<Props> = ({
initialValues,
dateRangeTypeProps,
currentDateSettingType = DateSettingType.STATIC,
onDateRangeChange,
}: any) => {
const [latestDateMap, setLatestDateMap] = useState<any>({
maxPartition: moment().format('YYYY-MM-DD'),
});
const [staticFormData, setStaticFormData] = useState<any>(() => {
return {
dateSettingType: DateSettingType.STATIC,
dateMode: initialValues?.dateMode || DateMode.RANGE,
dateRangeType: initialValues?.dateRangeType || dateRangeTypeProps || DateRangeType.DAY,
dateRange: initialValues?.dateRange || [],
dateMultiple: initialValues?.dateMultiple || [],
};
});
const [dateRangeValue, setDateRangeValue] = useState<any>([]);
const [pickerType, setPickerType] = useState<PickerType>(() => {
if (dateRangeTypeProps) {
return DateRangeTypeToPickerMap[dateRangeTypeProps];
}
if (staticFormData.dateRangeType) {
return DateRangeTypeToPickerMap[staticFormData.dateRangeType];
}
return DateRangePicker.DATE;
});
useEffect(() => {
initDateRangeValue();
}, []);
useEffect(() => {
if (currentDateSettingType === DateSettingType.STATIC) {
handleDateRangeTypePropsChange(dateRangeTypeProps);
}
setPickerType(DateRangeTypeToPickerMap[dateRangeTypeProps]);
}, [dateRangeTypeProps, latestDateMap]);
const handleDateRangeTypePropsChange = async (dateRangeType: DateRangeType) => {
if (!dateRangeType) {
return;
}
setStaticFormData({
...staticFormData,
dateRangeType,
});
dateRangeChange(dateRangeValue, dateRangeType, staticFormData.dateMode);
};
const initDateRangeValue = () => {
const initDateRange = initialValues?.dateRange || [];
const [startDate, endDate] = initDateRange;
const { maxPartition } = latestDateMap;
let dateRangeMoment = [moment(), moment()];
// 如果initialValues时间存在则按initialValues时间初始化
if (startDate && endDate) {
dateRangeMoment = [moment(startDate), moment(endDate)];
}
// dateRangeValue未被初始化且maxPartition存在则按maxPartition初始化
if (dateRangeValue.length === 0 && !(startDate && endDate) && maxPartition) {
dateRangeMoment = [moment(maxPartition), moment(maxPartition)];
}
// 否则按当前时间初始化
setDateRangeValue(dateRangeMoment);
};
const updateStaticFormData = (formData: any) => {
const mergeConfigTypeData = {
...staticFormData,
...formData,
dateRangeStringDesc: '',
};
const { dateRange, dateMode } = mergeConfigTypeData;
if (dateMode === DateMode.RANGE) {
const [startDate, endDate] = dateRange;
if (startDate && endDate) {
mergeConfigTypeData.dateRangeStringDesc = `${startDate}${endDate}`;
mergeConfigTypeData.dateMultiple = [];
}
}
if (dateMode === DateMode.LIST) {
mergeConfigTypeData.dateRangeStringDesc = `日期多选`;
mergeConfigTypeData.dateRange = [];
}
setStaticFormData(mergeConfigTypeData);
onDateRangeChange(mergeConfigTypeData.dateRange, mergeConfigTypeData);
};
const dateRangeChange = (
dates: any,
dateRangeType: DateRangeType,
dateMode: DateMode,
isDateRangeChange?: boolean,
) => {
if (!dates) {
updateStaticFormData({ dateRange: [] });
return;
}
const dateStrings = getDateStrings({
dates,
dateRangeType,
latestDateMap,
isDateRangeChange,
});
if (dateStrings[0] && dateStrings[1]) {
setDateRangeValue([moment(dateStrings[0]), moment(dateStrings[1])]);
updateStaticFormData({
dateMode,
dateRangeType,
dateRange: dateStrings,
});
}
};
return (
<Space>
<RangePicker
style={{ paddingBottom: 5 }}
value={dateRangeValue}
onChange={(date) => {
setDateRangeValue(date);
const dateString = getDateStrings({
dates: date,
latestDateMap,
isDateRangeChange: true,
dateRangeType: dateRangeTypeProps || staticFormData.dateRangeType,
});
updateStaticFormData({
dateRange: dateString,
dateRangeType: dateRangeTypeProps || staticFormData.dateRangeType,
});
return;
}}
allowClear={true}
picker={pickerType}
/>
{/* )} */}
</Space>
);
};
export default StaticDate;

View File

@@ -0,0 +1,441 @@
import React, { useState, useEffect } from 'react';
import { InfoCircleOutlined, CalendarOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import { Input, Tooltip, Popover, Space, Button, Select, Row, Col, Tag } from 'antd';
import styles from './style.less';
import { DateMode, DateRangeType } from './type';
import {
LATEST_TEXT,
DATE_RANGE_TYPE_ITEM_LIST,
getWeekDateRangeString,
getMonthDateRangeString,
generatorDateRangesParams,
getDynamicDateRangeStringByParams,
} from './utils';
import { DateSettingType, DateRangeParams, DynamicAdvancedConfigType } from './type';
import { LatestDateMap } from './type';
import StaticDate from './StaticDate';
import DynamicDate from './DynamicDate';
import moment from 'moment';
import { ProCard } from '@ant-design/pro-card';
type Props = {
disabledAdvanceSetting?: boolean;
initialValues?: any;
onDateRangeChange: (value: string[], from: any) => void;
onDateRangeTypeChange?: (dateRangeType: DateRangeType) => void;
};
const { CheckableTag } = Tag;
const MDatePicker: React.FC<Props> = ({
disabledAdvanceSetting,
initialValues,
onDateRangeChange,
onDateRangeTypeChange,
}: any) => {
const getDynamicDefaultConfig = (dateRangeType: DateRangeType) => {
const dynamicDefaultConfig = {
shortCutId: 'last7Days',
dateRangeType: DateRangeType.DAY,
dynamicAdvancedConfigType: DynamicAdvancedConfigType.LATEST,
dateRangeStringDesc: LATEST_TEXT,
number: 7,
dateSettingType: DateSettingType.DYNAMIC,
};
switch (dateRangeType) {
case DateRangeType.DAY:
return dynamicDefaultConfig;
case DateRangeType.WEEK:
return {
shortCutId: 'last4Weeks',
dateRangeType: DateRangeType.WEEK,
dynamicAdvancedConfigType: DynamicAdvancedConfigType.LAST,
dateRangeStringDesc: '最近4周',
dateSettingType: DateSettingType.DYNAMIC,
includesCurrentPeriod: false,
number: 4,
periodType: 'WEEK',
};
case DateRangeType.MONTH:
return {
shortCutId: 'last6Months',
dateRangeType: DateRangeType.MONTH,
dynamicAdvancedConfigType: DynamicAdvancedConfigType.LAST,
dateRangeStringDesc: '最近6月',
includesCurrentPeriod: false,
number: 6,
periodType: 'MONTH',
dateSettingType: DateSettingType.DYNAMIC,
};
default:
return dynamicDefaultConfig;
}
};
const [dateRangeType, setDateRangeType] = useState<DateRangeType>(
initialValues?.dynamicParams?.dateRangeType ||
initialValues?.staticParams?.dateRangeType ||
DateRangeType.DAY,
);
// const [pickerType, setPickerType] = useState<PickerType>(() => {
// // if (staticFormData.dateRangeType) {
// // return DateRangeTypeToPickerMap[staticFormData.dateRangeType];
// // }
// return DateRangePicker.DATE;
// });
// const [dateRangeValue, setDateRangeValue] = useState<any>([]);
const staticDefaultConfig = {
dateSettingType: DateSettingType.STATIC,
dateMode: DateMode.RANGE,
dateRangeType: DateRangeType.DAY,
dateRange: [],
dateMultiple: [],
dateRangeStringDesc: '',
};
// const { getMaxPartitionData } = useModel('useMaxPartitionData');
// const { globalViewId } = useModel('useViewsData');
const [latestDateMap, setLatestDateMap] = useState<LatestDateMap>({
maxPartition: moment().format('YYYY-MM-DD'),
});
const [dateRangesParams] = useState(() => {
return initialValues ? generatorDateRangesParams(initialValues) : {};
});
const [confirmBtnClickState, setConfirmBtnClickState] = useState(false);
const [visible, setVisible] = useState(false);
const [staticParams, setStaticParams] = useState(() => {
return initialValues?.staticParams || {};
});
const [dynamicParams, setDynamicParams] = useState(() => {
return initialValues?.dynamicParams || {};
});
const [currentDateMode, setCurrentDateMode] = useState<DateMode>(
initialValues?.staticParams?.dateMode || DateMode.RANGE,
);
const [currentDateSettingType, setCurrentDateSettingType] = useState(
initialValues?.dateSettingType || DateSettingType.STATIC,
);
const [currentDateRange, setCurrentDateRange] = useState<string[]>(() => {
return dateRangesParams.dateRange || [];
});
const [selectedDateRangeString, setSelectedDateRangeString] = useState(() => {
return getSelectedDateRangeString();
});
// const [advanceSettingCollapsed, setAdvanceSettingCollapsed] = useState(false);
function getSelectedDateRangeString() {
const [startTime, endTime] = currentDateRange;
if (currentDateSettingType === DateSettingType.DYNAMIC) {
if (startTime && endTime) {
if (dateRangeType === DateRangeType.WEEK) {
return getWeekDateRangeString(startTime, endTime);
}
if (dateRangeType === DateRangeType.MONTH) {
return getMonthDateRangeString(startTime, endTime);
}
return `${startTime}${endTime}`;
}
return '';
}
if (currentDateSettingType === DateSettingType.STATIC) {
const { dateMode, dateMultiple } = staticParams;
if (dateMode === DateMode.RANGE) {
if (startTime && endTime) {
if (dateRangeType === DateRangeType.WEEK) {
return getWeekDateRangeString(startTime, endTime);
}
if (dateRangeType === DateRangeType.MONTH) {
return getMonthDateRangeString(startTime, endTime);
}
return `${startTime}${endTime}`;
}
return '';
}
if (dateMode === DateMode.LIST) {
return dateMultiple.join(',');
}
}
}
useEffect(() => {
setSelectedDateRangeString(getSelectedDateRangeString());
}, [staticParams, dynamicParams, currentDateRange]);
const handleDateModeChange = (dateMode: DateMode) => {
if (!dateMode) {
return;
}
setCurrentDateMode(dateMode);
if (dateMode === DateMode.LIST) {
setDateRangeType(DateRangeType.DAY);
}
};
const handleDateRangeChange = (value: string[] | boolean, config: any) => {
const { dateRangeStringDesc, dateSettingType, dateMode } = config;
handleDateModeChange(dateMode);
setDateRangeStringShow(dateRangeStringDesc);
setCurrentDateSettingType(dateSettingType);
if (Array.isArray(value)) {
setCurrentDateRange(value);
}
let dateParamsConfig: DateRangeParams = {
latestDateMap,
dateSettingType,
dynamicParams: {},
staticParams: {},
};
if (dateSettingType === DateSettingType.DYNAMIC) {
dateParamsConfig = {
...dateParamsConfig,
dateSettingType,
dynamicParams: config,
staticParams: {},
};
}
if (dateSettingType === DateSettingType.STATIC) {
dateParamsConfig = {
...dateParamsConfig,
dateSettingType,
dynamicParams: {},
staticParams: config,
};
}
setDynamicParams({ ...dateParamsConfig.dynamicParams });
setStaticParams({ ...dateParamsConfig.staticParams });
onDateRangeChange(value, dateParamsConfig);
};
const getDateRangeStringShow = (dateRange: string[]) => {
if (currentDateSettingType === DateSettingType.DYNAMIC) {
const { dateRangeStringDesc } = getDynamicDateRangeStringByParams(
dynamicParams,
dynamicParams.dynamicAdvancedConfigType,
latestDateMap,
);
return dateRangeStringDesc;
}
if (currentDateSettingType === DateSettingType.STATIC) {
const { dateMode } = staticParams;
const [startTime, endTime] = currentDateRange || [];
if (dateMode === DateMode.RANGE) {
if (startTime && endTime) {
return `${startTime}${endTime}`;
}
return '';
}
if (dateMode === DateMode.LIST) {
return '日期多选';
}
}
const [startTime, endTime] = dateRange || [];
if (startTime && endTime) {
return `${startTime}${endTime}`;
}
return '';
};
const [dateRangeStringShow, setDateRangeStringShow] = useState(() => {
return getDateRangeStringShow(dateRangesParams.dateRange);
});
const initDefaultDynamicData = ({ latestDateMap }: any) => {
if (!initialValues) {
const defaultConfig = getDynamicDefaultConfig(dateRangeType);
const { maxPartition } = latestDateMap;
const dateRange: string[] = [maxPartition, maxPartition];
const config = {
...defaultConfig,
dateRange,
};
handleDateRangeChange(dateRange, config);
}
};
useEffect(() => {
initDefaultDynamicData({ latestDateMap });
}, []);
useEffect(() => {
if (visible) {
setConfirmBtnClickState(false);
}
}, [visible]);
useEffect(() => {
const { dateRange } = dateRangesParams;
setDateRangeStringShow(getDateRangeStringShow(dateRange));
}, [dateRangesParams]);
const content = (
<>
<ProCard
className={styles.dateProCard}
title={
<Space>
{/* <Tooltip
title={``}
>
<QuestionCircleOutlined />
</Tooltip> */}
</Space>
}
>
<div className={styles.dateShortCutSettingContent}>
<Row>
{DATE_RANGE_TYPE_ITEM_LIST.map((item: any) => {
const { value, label, toolTips } = item;
if (currentDateMode === DateMode.LIST && value !== DateRangeType.DAY) {
// 在多选模式只允许选择天粒度
return undefined;
}
return (
<Col key={`row-col-${value}`}>
<Tooltip title={toolTips}>
<CheckableTag
className={styles['ant-tag-checkable']}
checked={dateRangeType === value}
key={`row-col-tag-${value}`}
onChange={() => {
setDateRangeType(value);
onDateRangeTypeChange?.(value);
}}
>
<div className={styles['tag-value-box']}>{label}</div>
</CheckableTag>
</Tooltip>
</Col>
);
})}
</Row>
</div>
</ProCard>
<ProCard
className={styles.dateProCard}
title={'快捷选项'}
// title={`动态时间${
// currentDateSettingType === DateSettingType.DYNAMIC ? '(当前选中)' : ''
// }`}
// extra="2019年9月28日"
>
<DynamicDate
disabledAdvanceSetting={disabledAdvanceSetting}
initialValues={dynamicParams}
dateRangeTypeProps={dateRangeType}
submitFormDataState={confirmBtnClickState}
onDateRangeChange={handleDateRangeChange}
onDateRangeStringAndDescChange={({ dateRangeString }) => {
setCurrentDateRange(dateRangeString);
}}
onShortCutClick={() => {
setVisible(false);
}}
/>
</ProCard>
<ProCard
className={styles.dateProCard}
title={
<Space>
{/* <Tooltip
title={``}
>
<QuestionCircleOutlined />
</Tooltip> */}
</Space>
}
>
<StaticDate
currentDateSettingType={currentDateSettingType}
initialValues={staticParams}
dateRangeTypeProps={dateRangeType}
onDateRangeChange={handleDateRangeChange}
/>
</ProCard>
<div
style={{
display: 'flex',
borderTop: '1px solid #eee',
paddingTop: '10px',
}}
>
<Space style={{ fontSize: 12, marginRight: 20 }}>
<div style={{ width: 60 }}></div>
<div>{selectedDateRangeString}</div>
</Space>
<Space style={{ marginLeft: 'auto' }}>
<Button
type="primary"
onClick={() => {
if (currentDateSettingType === DateSettingType.DYNAMIC) {
setConfirmBtnClickState(true);
}
setVisible(false);
}}
>
</Button>
<Button
onClick={() => {
setVisible(false);
}}
>
</Button>
</Space>
</div>
</>
);
return (
<Space direction="vertical">
<Popover
content={content}
// destroyTooltipOnHide={true}
// title="Title"
open={visible}
trigger="click"
onOpenChange={(newVisible) => {
setVisible(newVisible);
if (!newVisible) {
// 当界面关闭时,如果是动态模式需检测用户所确认选中数据和当前面板显示数据(切换了时间粒度,但是没有保存配置数据)
// 是否为同一时间粒度,如果不是,则需将当前时间粒度调整为动态时间组件所保存的时间粒度
if (currentDateSettingType === DateSettingType.DYNAMIC) {
const paramsDateRangeType = dynamicParams.dateRangeType;
if (paramsDateRangeType && paramsDateRangeType !== dateRangeType) {
setDateRangeType(paramsDateRangeType);
}
}
}
}}
overlayClassName={styles.popverOverlayContent}
placement="left"
>
<Input
className={styles.dateTimeShowInput}
value={dateRangeStringShow}
placeholder="请选择日期时间"
prefix={<CalendarOutlined />}
readOnly
style={{ width: 280 }}
suffix={
<Tooltip title={`${selectedDateRangeString}`}>
<InfoCircleOutlined style={{}} />
</Tooltip>
}
/>
</Popover>
{!(
currentDateSettingType === DateSettingType.STATIC &&
currentDateMode === DateMode.RANGE &&
dateRangeType === DateRangeType.DAY
) && <div style={{ color: '#0e73ff' }}>: {selectedDateRangeString}</div>}
</Space>
);
};
export default MDatePicker;

View File

@@ -0,0 +1,84 @@
.popverOverlayContent {
padding-top: 0;
width: 440px;
:global {
.ant-popover-content {
.ant-popover-arrow {
display: none;
}
}
}
}
.dateProCard {
:global {
.ant-pro-card-header {
padding: 10px;
}
.ant-pro-card-body{
padding:10px;
}
}
}
.dateTimeShowInput {
:global {
.ant-input{
font-size: 14px;
}
}
}
.advancedSettingItemText {
width: 40px;
display: inline-block;
}
.dateAdvancedSettingContent {
:global {
.ant-input{
font-size: 14px;
}
}
}
.dateShortCutSettingContent {
:global {
.ant-tag-checkable:active, .ant-tag-checkable-checked{
background-color: #e6effc;
color: #0e73ff;
}
}
.ant-tag-checkable {
position: relative;
width: 120px;
height: 28px;
margin-right: 4px;
border: none;
// margin-right: 0;
}
.tag-value-box {
position: absolute;
top: 0;
left: 0;
-webkit-box-sizing: border-box;
box-sizing: border-box;
width: 100%;
height: 100%;
padding: 0 3px;
overflow: hidden;
font-size: 13px;
line-height: 28px;
white-space: nowrap;
text-align: center;
text-overflow: ellipsis;
background-color: #f7f9fc;
// background-color: #296df3;
border-radius: 3px;
cursor: pointer;
&:hover {
background-color: #e6effc;
color: #0e73ff;
}
}
}

View File

@@ -0,0 +1,67 @@
export enum DynamicAdvancedConfigType {
LATEST = 'latest',
// CUSTOM = 'custom',
LAST = 'last',
HISTORY = 'history',
FROM_DATE_PERIOD = 'fromDatePeriod',
FROM_DATE = 'fromDate',
}
export enum DatePeriodType {
DAY = 'DAYS',
WEEK = 'WEEK',
MONTH = 'MONTH',
YEAR = 'YEAR',
}
export enum DateSettingType {
STATIC = 'STATIC',
DYNAMIC = 'DYNAMIC',
}
export enum PerDatePeriodType {
PERDAY = 'PERDAY',
PERWEEK = 'PERWEEK',
PERMONTH = 'PERMONTH',
PERYEAR = 'PERYEAR',
}
export type DateRangeParams = {
dateSettingType: DateSettingType;
dynamicParams: any;
staticParams: any;
latestDateMap?: { maxPartition: string };
};
export enum StaticDateSelectMode {
DATE_RANGE = 'dateRange',
DATE_MODE = 'dateMode',
}
export enum DateRangeType {
DAY = 'DAY',
WEEK = 'WEEK',
MONTH = 'MONTH',
}
export enum DateRangePicker {
DATE = 'date',
WEEK = 'week',
MONTH = 'month',
}
export const DateRangeTypeToPickerMap = {
[DateRangeType.DAY]: DateRangePicker.DATE,
[DateRangeType.WEEK]: DateRangePicker.WEEK,
[DateRangeType.MONTH]: DateRangePicker.MONTH,
};
export type LatestDateMap = {
maxPartition: string;
};
export enum DateMode {
RANGE = 1,
LIST = 2,
ES = 4,
}

View File

@@ -0,0 +1,543 @@
import moment from 'moment';
import type { Moment } from 'moment';
import { DateMode, DateRangeType, DateRangePicker, DateRangeTypeToPickerMap } from './type';
import {
DatePeriodType,
PerDatePeriodType,
DateSettingType,
DynamicAdvancedConfigType,
DateRangeParams,
} from './type';
import { LatestDateMap } from './type';
export const dateRangeTypeExchangeDatePeriodTypeMap = {
[DateRangeType.DAY]: DatePeriodType.DAY,
[DateRangeType.WEEK]: DatePeriodType.WEEK,
[DateRangeType.MONTH]: DatePeriodType.MONTH,
};
export const datePeriodTypeMap = {
[DatePeriodType.DAY]: '天',
[DatePeriodType.WEEK]: '周',
[DatePeriodType.MONTH]: '月',
[DatePeriodType.YEAR]: '年',
};
export const datePeriodTypeWordingMap = {
[DatePeriodType.DAY]: '当天',
[DatePeriodType.WEEK]: '本周',
[DatePeriodType.MONTH]: '本月',
[DatePeriodType.YEAR]: '今年',
};
export const perDatePeriodTypeMap = {
[PerDatePeriodType.PERDAY]: '每日',
[PerDatePeriodType.PERWEEK]: '每周(周一)',
[PerDatePeriodType.PERMONTH]: '每月(01日)',
[PerDatePeriodType.PERYEAR]: '每年(01月01日)',
};
export const DATE_RANGE_TYPE_ITEM_LIST = [
{
label: '天',
value: DateRangeType.DAY,
toolTips: '展示每天数据',
},
{
label: '周',
value: DateRangeType.WEEK,
toolTips: '仅展示每周日数据',
},
{
label: '月',
value: DateRangeType.MONTH,
toolTips: '仅展示每月最后一天数据',
},
];
export const LATEST_TEXT = '最近1天';
export const SHORT_CUT_ITEM_LIST = [
{
id: 'last7Days',
text: '最近7天',
advancedConfigType: DynamicAdvancedConfigType.LAST,
dateRangeType: DateRangeType.DAY,
initData: {
shortCutId: 'last7Days',
number: 7,
periodType: DatePeriodType.DAY,
includesCurrentPeriod: false,
},
},
{
id: 'last15Days',
text: '最近15天',
advancedConfigType: DynamicAdvancedConfigType.LAST,
dateRangeType: DateRangeType.DAY,
initData: {
shortCutId: 'last15Days',
number: 15,
periodType: DatePeriodType.DAY,
includesCurrentPeriod: false,
},
},
{
id: 'last30Days',
text: '最近30天',
advancedConfigType: DynamicAdvancedConfigType.LAST,
dateRangeType: DateRangeType.DAY,
initData: {
shortCutId: 'last30Days',
number: 30,
periodType: DatePeriodType.DAY,
includesCurrentPeriod: false,
},
},
{
id: 'last4Weeks',
text: '最近4周',
advancedConfigType: DynamicAdvancedConfigType.LAST,
dateRangeType: DateRangeType.WEEK,
initData: {
shortCutId: 'last4Weeks',
number: 4,
periodType: DatePeriodType.WEEK,
includesCurrentPeriod: false,
},
},
{
id: 'last8Weeks',
text: '最近8周',
advancedConfigType: DynamicAdvancedConfigType.LAST,
dateRangeType: DateRangeType.WEEK,
initData: {
shortCutId: 'last8Weeks',
number: 8,
periodType: DatePeriodType.WEEK,
includesCurrentPeriod: false,
},
},
{
id: 'last12Weeks',
text: '最近12周',
advancedConfigType: DynamicAdvancedConfigType.LAST,
dateRangeType: DateRangeType.WEEK,
initData: {
shortCutId: 'last12Weeks',
number: 12,
periodType: DatePeriodType.WEEK,
includesCurrentPeriod: false,
},
},
{
id: 'last6Months',
text: '最近6个月',
advancedConfigType: DynamicAdvancedConfigType.LAST,
dateRangeType: DateRangeType.MONTH,
initData: {
shortCutId: 'last6Months',
number: 6,
periodType: DatePeriodType.MONTH,
includesCurrentPeriod: false,
},
},
{
id: 'last12Months',
text: '最近12个月',
advancedConfigType: DynamicAdvancedConfigType.LAST,
dateRangeType: DateRangeType.MONTH,
initData: {
shortCutId: 'last12Months',
number: 12,
periodType: DatePeriodType.MONTH,
includesCurrentPeriod: false,
},
},
];
const computedSubtractDateRange = (number: number, includesCurrentPeriod: boolean) => {
const startDateNumber = includesCurrentPeriod ? number - 1 : number;
const endDateNumber = includesCurrentPeriod ? 0 : 1;
return { startDateNumber, endDateNumber };
};
const getLastTypeDateRange = ({
number,
periodType,
latestDateMap,
includesCurrentPeriod = false, // 是否包含当前周期 今天/本周/本月/今年
}: {
number: number;
periodType: DatePeriodType;
latestDateMap: LatestDateMap;
includesCurrentPeriod?: boolean;
}) => {
const { startDateNumber, endDateNumber } = computedSubtractDateRange(
number,
includesCurrentPeriod,
);
switch (periodType) {
case DatePeriodType.DAY: {
// 如果选取了包含今天,则放弃使用标签最新更新时间;
const periodTypeMoment = includesCurrentPeriod
? undefined
: latestDateMap?.maxPartition || undefined;
let subtractData = {
startDateNumber,
endDateNumber,
};
if (periodTypeMoment) {
// 在使用标签最新更新时间时,需包含最新的时间日期,并重置计算时间区段
subtractData = computedSubtractDateRange(number, true);
}
return [
moment(periodTypeMoment).subtract(subtractData.startDateNumber, 'days').startOf('days'),
moment(periodTypeMoment).subtract(subtractData.endDateNumber, 'days').endOf('days'),
];
}
case DatePeriodType.WEEK: {
// 如果选取了包含今天,则放弃使用标签最新更新时间;
const periodTypeMoment = includesCurrentPeriod
? undefined
: latestDateMap?.maxPartition || undefined;
let subtractData = {
startDateNumber,
endDateNumber,
};
if (periodTypeMoment) {
// 在使用标签最新更新时间时,需包含最新的时间日期,并重置计算时间区段
subtractData = computedSubtractDateRange(number, true);
}
return [
moment(periodTypeMoment).subtract(subtractData.startDateNumber, 'week').startOf('week'),
moment(periodTypeMoment).subtract(subtractData.endDateNumber, 'week').endOf('week'),
];
}
case DatePeriodType.MONTH: {
// 如果选取了包含今天,则放弃使用标签最新更新时间;
const periodTypeMoment = includesCurrentPeriod
? undefined
: latestDateMap?.maxPartition || undefined;
let subtractData = {
startDateNumber,
endDateNumber,
};
if (periodTypeMoment) {
// 在使用标签最新更新时间时,需包含最新的时间日期,并重置计算时间区段
subtractData = computedSubtractDateRange(number, true);
}
return [
moment(periodTypeMoment).subtract(subtractData.startDateNumber, 'month').startOf('month'),
moment(periodTypeMoment).subtract(subtractData.endDateNumber, 'month').endOf('month'),
];
}
case DatePeriodType.YEAR:
return [
moment().subtract(startDateNumber, 'year').startOf('year'),
moment().subtract(endDateNumber, 'year').endOf('year'),
];
default:
return [];
}
};
const getHistoryTypeDateRange = ({
number,
periodType,
}: {
number: number;
periodType: DatePeriodType;
}) => {
const dateNumber = number;
switch (periodType) {
case DatePeriodType.DAY:
return [
moment().subtract(dateNumber, 'days').startOf('days'),
moment().subtract(dateNumber, 'days').endOf('days'),
];
case DatePeriodType.WEEK:
return [
moment().subtract(dateNumber, 'week').startOf('week'),
moment().subtract(dateNumber, 'week').endOf('week'),
];
case DatePeriodType.MONTH:
return [
moment().subtract(dateNumber, 'month').startOf('month'),
moment().subtract(dateNumber, 'month').endOf('month'),
];
case DatePeriodType.YEAR:
return [
moment().subtract(dateNumber, 'year').startOf('year'),
moment().subtract(dateNumber, 'year').endOf('year'),
];
default:
return [];
}
};
const getFromDatePeriodTypeDateRange = ({
perPeriodType,
}: {
perPeriodType: PerDatePeriodType;
}) => {
switch (perPeriodType) {
case PerDatePeriodType.PERDAY:
return [moment().startOf('days'), moment()];
case PerDatePeriodType.PERWEEK:
return [moment().startOf('week'), moment()];
case PerDatePeriodType.PERMONTH:
return [moment().startOf('month'), moment()];
case PerDatePeriodType.PERYEAR:
return [moment().startOf('year'), moment()];
default:
return [];
}
};
const getFromDateTypeDateRange = ({ date }: { date: string }) => {
return [moment(date), moment()];
};
export const getLastestTypeDateRange = (latestDate: string) => {
if (latestDate) {
return [moment(latestDate), moment(latestDate)];
}
console.warn('最新标签更新日期不存在');
return [moment().subtract(1, 'week'), moment().subtract(1, 'week')];
};
export const shortCutDateRangeMap = {
// latest: (latestDate?: string) => {
// if (latestDate) {
// return [moment(latestDate), moment(latestDate)];
// }
// return [moment(), moment()];
// },
// yesterday: () => {
// return [moment().subtract(1, 'days'), moment().subtract(1, 'days')];
// },
// last3Days: () => {
// return [moment().subtract(3, 'days'), moment()];
// },
// last7Days: () => {
// return [moment().subtract(7, 'days'), moment()];
// },
// last30Days: () => {
// return [moment().subtract(30, 'days'), moment()];
// },
// today: () => {
// return [moment(), moment()];
// },
// thisWeek: () => {
// return [moment().startOf('week'), moment().endOf('week')];
// },
// thisMonth: () => {
// return [moment().startOf('month'), moment().endOf('month')];
// },
// thisYear: () => {
// return [moment().startOf('year'), moment().endOf('year')];
// },
[DynamicAdvancedConfigType.LATEST]: getLastestTypeDateRange,
[DynamicAdvancedConfigType.LAST]: getLastTypeDateRange,
[DynamicAdvancedConfigType.HISTORY]: getHistoryTypeDateRange,
[DynamicAdvancedConfigType.FROM_DATE_PERIOD]: getFromDatePeriodTypeDateRange,
[DynamicAdvancedConfigType.FROM_DATE]: getFromDateTypeDateRange,
};
export const formatDateRangeString = (dateRange: Moment[]) => {
if (dateRange && Array.isArray(dateRange)) {
return dateRange.map((item) => {
return item?.format?.('YYYY-MM-DD') || '';
});
}
return dateRange;
};
// 动态时间配置转换为静态时间参数进行请求
export const parseDynamicDateParamsToStaticDateParams = (
dateParams: any,
latestDateMap: LatestDateMap,
) => {
const {
dynamicAdvancedConfigType,
number,
periodType,
includesCurrentPeriod,
perPeriodType,
date,
dateRangeType,
} = dateParams;
const staticParams = {
dateMode: DateMode.RANGE,
dateRangeType: dateRangeType || DateRangeType.DAY,
};
let dateMomentRange: Moment[] = [];
switch (dynamicAdvancedConfigType) {
case DynamicAdvancedConfigType.LATEST: {
const latestDate = latestDateMap?.maxPartition;
dateMomentRange = getLastestTypeDateRange(latestDate);
break;
}
case DynamicAdvancedConfigType.LAST: {
dateMomentRange = getLastTypeDateRange({
number,
periodType,
latestDateMap,
includesCurrentPeriod,
});
break;
}
case DynamicAdvancedConfigType.HISTORY: {
dateMomentRange = getHistoryTypeDateRange({ number, periodType });
break;
}
case DynamicAdvancedConfigType.FROM_DATE_PERIOD:
dateMomentRange = getFromDatePeriodTypeDateRange({ perPeriodType });
break;
case DynamicAdvancedConfigType.FROM_DATE:
dateMomentRange = getFromDateTypeDateRange({ date });
break;
default:
break;
}
return {
...staticParams,
dateRange: formatDateRangeString(dateMomentRange),
};
};
export const generatorDateRangesParams = (dateRangeParams: DateRangeParams) => {
const {
dateSettingType,
latestDateMap = {},
dynamicParams = {},
staticParams = {},
} = dateRangeParams;
if (dateSettingType === DateSettingType.DYNAMIC) {
return parseDynamicDateParamsToStaticDateParams(dynamicParams, latestDateMap as any);
}
return staticParams;
};
export const getDynamicDateRangeStringByParams = (
params: any,
type: DynamicAdvancedConfigType,
latestDateMap: LatestDateMap,
) => {
const { number, periodType, includesCurrentPeriod, perPeriodType, date } = params;
let dateRangeMoment: any[] = [];
let dateRangeStringDesc = '';
switch (type) {
case DynamicAdvancedConfigType.LATEST: {
const latestDate = latestDateMap.maxPartition;
dateRangeStringDesc = LATEST_TEXT;
dateRangeMoment = shortCutDateRangeMap[DynamicAdvancedConfigType.LATEST](latestDate);
break;
}
case DynamicAdvancedConfigType.LAST:
dateRangeMoment = shortCutDateRangeMap[DynamicAdvancedConfigType.LAST]({
number,
periodType,
latestDateMap,
includesCurrentPeriod,
});
dateRangeStringDesc =
`最近${number}${datePeriodTypeMap[periodType]}` +
`${includesCurrentPeriod ? `(包含${datePeriodTypeWordingMap[periodType]})` : ''}`;
break;
case DynamicAdvancedConfigType.HISTORY:
dateRangeMoment = shortCutDateRangeMap[DynamicAdvancedConfigType.HISTORY]({
number,
periodType,
});
dateRangeStringDesc = `过去第${number}${datePeriodTypeMap[periodType]}`;
break;
case DynamicAdvancedConfigType.FROM_DATE_PERIOD:
dateRangeMoment = shortCutDateRangeMap[DynamicAdvancedConfigType.FROM_DATE_PERIOD]({
perPeriodType,
});
dateRangeStringDesc = `自从${perDatePeriodTypeMap[perPeriodType]}00:00:00至此刻`;
break;
case DynamicAdvancedConfigType.FROM_DATE:
dateRangeMoment = shortCutDateRangeMap[DynamicAdvancedConfigType.FROM_DATE]({
date,
});
dateRangeStringDesc = `${date}至此刻`;
break;
default:
dateRangeMoment = [];
dateRangeStringDesc = '';
}
return {
dateRangeString: formatDateRangeString(dateRangeMoment),
dateRangeStringDesc,
};
};
export function getDateStrings({
dates,
dateRangeType,
latestDateMap,
isDateRangeChange,
}: {
dates: any;
dateRangeType: DateRangeType;
latestDateMap?: LatestDateMap;
isDateRangeChange?: boolean;
}) {
let dateRange = dates;
if (!Array.isArray(dateRange)) {
dateRange = [dateRange];
}
const picker = DateRangeTypeToPickerMap[dateRangeType];
const dateStrings = dateRange.map((date: Moment, index: number) => {
switch (picker) {
case DateRangePicker.DATE:
if (latestDateMap?.maxPartition && !isDateRangeChange) {
return latestDateMap.maxPartition;
}
return date.format('YYYY-MM-DD');
case DateRangePicker.WEEK:
if (index === 0) {
// 仅当dateRangeType进行切换时即天/周/月被转换时对时间进行当前时间周期-1操作
return date
.startOf('week')
.subtract(!isDateRangeChange ? 1 : 0, 'week')
.format('YYYY-MM-DD');
}
return date
.endOf('week')
.subtract(!isDateRangeChange ? 1 : 0, 'week')
.format('YYYY-MM-DD');
case DateRangePicker.MONTH:
if (index === 0) {
return date
.startOf('month')
.subtract(!isDateRangeChange ? 1 : 0, 'month')
.format('YYYY-MM-DD');
}
return date
.endOf('month')
.subtract(!isDateRangeChange ? 1 : 0, 'month')
.format('YYYY-MM-DD');
default:
if (latestDateMap?.maxPartition && !isDateRangeChange) {
return latestDateMap?.maxPartition;
}
return date.format('YYYY-MM-DD');
}
});
return dateStrings;
}
export const getWeekDateRangeString = (startTime: string, endTime: string) => {
const startTimeWeekNumber = moment(startTime).format('w');
const endTimeWeekNumber = moment(endTime).format('w');
return `${startTime}(${startTimeWeekNumber}周)至${endTime}(${endTimeWeekNumber}周)`;
};
export const getMonthDateRangeString = (startTime: string, endTime: string) => {
const startTimeMonth = moment(startTime).format('YYYY-MM');
const endTimeMonth = moment(endTime).format('YYYY-MM');
return `${startTimeMonth}${endTimeMonth}`;
};

View File

@@ -3,7 +3,7 @@
.standardFormRow {
display: flex;
width: 100%;
margin-bottom: 12px;
margin-bottom: 24px;
// padding-bottom: 16px;
// border-bottom: 1px dashed @border-color-split;
:global {
@@ -28,14 +28,14 @@
}
.label {
flex: 0 0 auto;
margin-right: 24px;
margin-right: 12px;
color: @heading-color;
font-size: @font-size-base;
text-align: left;
& > span {
display: inline-block;
flex-shrink: 0;
width: 80px;
// width: 80px;
height: 32px;
// height: 20px;
color: #999;

View File

@@ -3,7 +3,7 @@
.tagSelect {
position: relative;
max-height: 32px;
margin-left: -8px;
// margin-left: -8px;
overflow: hidden;
line-height: 32px;
transition: all 0.3s;

View File

@@ -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

View File

@@ -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);

View File

@@ -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>
);
})}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}}
/>
)}
</>
);
};

View File

@@ -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;
}
}
}
}

View File

@@ -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>
</>
);

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);

View File

@@ -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

View File

@@ -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[];

View File

@@ -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,
},
});
}

View File

@@ -276,6 +276,17 @@ export function formatByDecimalPlaces(value: number | string, decimalPlaces: num
return str;
}
export function formatByPercentageData(value: number | string, decimalPlaces: number) {
const formattedValue: any = Number(value) * 100;
if (!isFinite(formattedValue)) {
return value;
}
if (formattedValue < 0) {
return `-${formatByDecimalPlaces(Math.abs(formattedValue), decimalPlaces)}%`;
}
return `${formatByDecimalPlaces(formattedValue, decimalPlaces)}%`;
}
export function formatByThousandSeperator(value: number | string) {
if (isNaN(+value)) {
return value;
@@ -400,7 +411,6 @@ export function traverseRoutes(routes, env: string, result: any[] = []) {
if (route.envRedirect) {
route.redirect = route.envRedirect[env];
}
if (route.routes) {
const filteredRoutes = traverseRoutes(route.routes, env);