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