diff --git a/webapp/packages/supersonic-fe/src/components/MDatePicker/DynamicDate.tsx b/webapp/packages/supersonic-fe/src/components/MDatePicker/DynamicDate.tsx new file mode 100644 index 000000000..901743eee --- /dev/null +++ b/webapp/packages/supersonic-fe/src/components/MDatePicker/DynamicDate.tsx @@ -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 = ({ + 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( + initAdvacedConfigPanelCollapsed(), + ); + const [advancedConfigType, setAdvancedConfigType] = useState< + DynamicAdvancedConfigType | undefined + >(initAdvancedConfigType()); + const [advancedPanelFormData, setAdvancedPanelFormData] = useState( + initAdvancedPanelFormData(), + ); + const [shortCutSettingChecked, setShortCutSettingChecked] = useState( + 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( + , + ); + 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 ( + <> +
+ + {SHORT_CUT_ITEM_LIST.map((item) => { + const { id, text, dateRangeType } = item; + if (dateRangeType === dateRangeTypeProps) { + return ( + + { + handleShortCutChange(item); + }} + > +
{text}
+
+ + ); + } + return undefined; + })} +
+
+ {!disabledAdvanceSetting && ( +
+ { + setAdvacedConfigPanelCollapsed(key); + if (key.length === 0) { + onAdvanceSettingCollapsedChange?.(false); + return; + } + onAdvanceSettingCollapsedChange?.(true); + }} + bordered={false} + ghost={true} + expandIconPosition="right" + > + + 高级设置 + + } + > +
+ +
动态时间
+ + + +
+
+ { + const configType = e.target.value; + setAdvancedConfigType(configType); + updateAdvancedPanelFormData(advancedPanelFormData[configType], configType); + handleAdvancedPanelFormChange(); + }} + value={advancedConfigType} + > + + + + 最近 + { + updateAdvancedPanelFormData( + { number: value }, + DynamicAdvancedConfigType.LAST, + ); + handleAdvancedPanelFormChange(); + }} + /> + + { + const isChecked = e.target.checked; + updateAdvancedPanelFormData( + { includesCurrentPeriod: isChecked }, + DynamicAdvancedConfigType.LAST, + ); + handleAdvancedPanelFormChange(); + }} + > + 包含 + { + datePeriodTypeWordingMap[ + advancedPanelFormData[DynamicAdvancedConfigType.LAST].periodType + ] + } + + + + + + 过去第 + + { + updateAdvancedPanelFormData( + { number: value }, + DynamicAdvancedConfigType.HISTORY, + ); + handleAdvancedPanelFormChange(); + }} + /> + + + + + + 自从 + { + 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(); + }} + /> + 至此刻 + + + + +
+
+
+ )} + + ); +}; + +export default DynamicDate; diff --git a/webapp/packages/supersonic-fe/src/components/MDatePicker/StaticDate.tsx b/webapp/packages/supersonic-fe/src/components/MDatePicker/StaticDate.tsx new file mode 100644 index 000000000..f9abe418b --- /dev/null +++ b/webapp/packages/supersonic-fe/src/components/MDatePicker/StaticDate.tsx @@ -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 = ({ + initialValues, + dateRangeTypeProps, + currentDateSettingType = DateSettingType.STATIC, + onDateRangeChange, +}: any) => { + const [latestDateMap, setLatestDateMap] = useState({ + maxPartition: moment().format('YYYY-MM-DD'), + }); + + const [staticFormData, setStaticFormData] = useState(() => { + 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([]); + const [pickerType, setPickerType] = useState(() => { + 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 ( + + { + 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} + /> + {/* )} */} + + ); +}; + +export default StaticDate; diff --git a/webapp/packages/supersonic-fe/src/components/MDatePicker/index.tsx b/webapp/packages/supersonic-fe/src/components/MDatePicker/index.tsx new file mode 100644 index 000000000..d6171a268 --- /dev/null +++ b/webapp/packages/supersonic-fe/src/components/MDatePicker/index.tsx @@ -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 = ({ + 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( + initialValues?.dynamicParams?.dateRangeType || + initialValues?.staticParams?.dateRangeType || + DateRangeType.DAY, + ); + // const [pickerType, setPickerType] = useState(() => { + // // if (staticFormData.dateRangeType) { + // // return DateRangeTypeToPickerMap[staticFormData.dateRangeType]; + // // } + // return DateRangePicker.DATE; + // }); + + // const [dateRangeValue, setDateRangeValue] = useState([]); + + 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({ + 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( + initialValues?.staticParams?.dateMode || DateMode.RANGE, + ); + + const [currentDateSettingType, setCurrentDateSettingType] = useState( + initialValues?.dateSettingType || DateSettingType.STATIC, + ); + const [currentDateRange, setCurrentDateRange] = useState(() => { + 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 = ( + <> + + 时间粒度 + {/* + + */} + + } + > +
+ + {DATE_RANGE_TYPE_ITEM_LIST.map((item: any) => { + const { value, label, toolTips } = item; + if (currentDateMode === DateMode.LIST && value !== DateRangeType.DAY) { + // 在多选模式只允许选择天粒度 + return undefined; + } + return ( + + + { + setDateRangeType(value); + onDateRangeTypeChange?.(value); + }} + > +
{label}
+
+
+ + ); + })} +
+
+
+ + + { + setCurrentDateRange(dateRangeString); + }} + onShortCutClick={() => { + setVisible(false); + }} + /> + + + + 静态时间 + {/* + + */} + + } + > + + + +
+ +
已选时间:
+
{selectedDateRangeString}
+
+ + + + + +
+ + ); + return ( + + { + setVisible(newVisible); + if (!newVisible) { + // 当界面关闭时,如果是动态模式需检测用户所确认选中数据和当前面板显示数据(切换了时间粒度,但是没有保存配置数据) + // 是否为同一时间粒度,如果不是,则需将当前时间粒度调整为动态时间组件所保存的时间粒度 + if (currentDateSettingType === DateSettingType.DYNAMIC) { + const paramsDateRangeType = dynamicParams.dateRangeType; + if (paramsDateRangeType && paramsDateRangeType !== dateRangeType) { + setDateRangeType(paramsDateRangeType); + } + } + } + }} + overlayClassName={styles.popverOverlayContent} + placement="left" + > + } + readOnly + style={{ width: 280 }} + suffix={ + + + + } + /> + + {!( + currentDateSettingType === DateSettingType.STATIC && + currentDateMode === DateMode.RANGE && + dateRangeType === DateRangeType.DAY + ) &&
当前时间: {selectedDateRangeString}
} +
+ ); +}; + +export default MDatePicker; diff --git a/webapp/packages/supersonic-fe/src/components/MDatePicker/style.less b/webapp/packages/supersonic-fe/src/components/MDatePicker/style.less new file mode 100644 index 000000000..1ded53200 --- /dev/null +++ b/webapp/packages/supersonic-fe/src/components/MDatePicker/style.less @@ -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; + } + } +} + diff --git a/webapp/packages/supersonic-fe/src/components/MDatePicker/type.ts b/webapp/packages/supersonic-fe/src/components/MDatePicker/type.ts new file mode 100644 index 000000000..4ddb867b9 --- /dev/null +++ b/webapp/packages/supersonic-fe/src/components/MDatePicker/type.ts @@ -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, +} diff --git a/webapp/packages/supersonic-fe/src/components/MDatePicker/utils.ts b/webapp/packages/supersonic-fe/src/components/MDatePicker/utils.ts new file mode 100644 index 000000000..d98d408d1 --- /dev/null +++ b/webapp/packages/supersonic-fe/src/components/MDatePicker/utils.ts @@ -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}`; +}; diff --git a/webapp/packages/supersonic-fe/src/components/StandardFormRow/index.less b/webapp/packages/supersonic-fe/src/components/StandardFormRow/index.less index 63d930c75..55b795644 100644 --- a/webapp/packages/supersonic-fe/src/components/StandardFormRow/index.less +++ b/webapp/packages/supersonic-fe/src/components/StandardFormRow/index.less @@ -3,7 +3,7 @@ .standardFormRow { display: flex; width: 100%; - margin-bottom: 12px; + margin-bottom: 24px; // padding-bottom: 16px; // border-bottom: 1px dashed @border-color-split; :global { @@ -28,14 +28,14 @@ } .label { flex: 0 0 auto; - margin-right: 24px; + margin-right: 12px; color: @heading-color; font-size: @font-size-base; text-align: left; & > span { display: inline-block; flex-shrink: 0; - width: 80px; + // width: 80px; height: 32px; // height: 20px; color: #999; diff --git a/webapp/packages/supersonic-fe/src/components/TagSelect/index.less b/webapp/packages/supersonic-fe/src/components/TagSelect/index.less index b174f042c..9cff6d372 100644 --- a/webapp/packages/supersonic-fe/src/components/TagSelect/index.less +++ b/webapp/packages/supersonic-fe/src/components/TagSelect/index.less @@ -3,7 +3,7 @@ .tagSelect { position: relative; max-height: 32px; - margin-left: -8px; + // margin-left: -8px; overflow: hidden; line-height: 32px; transition: all 0.3s; diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/Datasource/components/SqlDetail.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Datasource/components/SqlDetail.tsx index 2882ccb44..1a601d47b 100644 --- a/webapp/packages/supersonic-fe/src/pages/SemanticModel/Datasource/components/SqlDetail.tsx +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Datasource/components/SqlDetail.tsx @@ -379,8 +379,6 @@ const SqlDetail: React.FC = ({ setScreenSize(size); }, []); - const exploreEditorSize = localStorage.getItem('exploreEditorSize'); - return ( <>
@@ -445,10 +443,9 @@ const SqlDetail: React.FC = ({ split="horizontal" onChange={(size) => { setEditorSize(size); - localStorage.setItem('exploreEditorSize', size[0]); }} > - +
void; + onEditBtnClick?: (metricItem: ISemantic.IMetricItem) => void; + onDeleteBtnClick?: (metricItem: ISemantic.IMetricItem) => void; + domainManger: StateType; + dispatch: Dispatch; +}; + +const MetricCardList: React.FC = ({ + metricList, + disabledEdit = false, + onMetricChange, + onEditBtnClick, + onDeleteBtnClick, + domainManger, +}) => { + const [currentNodeData, setCurrentNodeData] = useState({}); + + const descNode = (metricItem: ISemantic.IMetricItem) => { + const { modelName, createdBy } = metricItem; + return ( + <> +
+
+
+
所属模型:
+
+ + {modelName} + +
+
+
+
+
+
创建人:
+
+ + {createdBy} + +
+
+
+
+ + ); + }; + + const extraNode = (metricItem: ISemantic.IMetricItem) => { + return ( + { + domEvent.stopPropagation(); + if (key === 'edit') { + onEditBtnClick?.(metricItem); + } + }, + items: [ + { + label: '编辑', + key: 'edit', + }, + { + label: ( + { + onDeleteBtnClick?.(metricItem); + }} + > + 删除 + + ), + key: 'delete', + }, + ], + }} + > + e.stopPropagation()} + /> + + ); + }; + + return ( +
+ + {metricList && + metricList.map((metricItem: ISemantic.IMetricItem) => { + return ( + { + setCurrentNodeData({ ...metricItem, nodeType: SemanticNodeType.METRIC }); + onMetricChange?.(metricItem); + }} + /> + ); + })} + +
+ ); +}; + +export default connect(({ domainManger }: { domainManger: StateType }) => ({ + domainManger, +}))(MetricCardList); diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/components/MetricFilter.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/components/MetricFilter.tsx index c83e22c1d..35934379b 100644 --- a/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/components/MetricFilter.tsx +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/components/MetricFilter.tsx @@ -1,4 +1,4 @@ -import { Form, Input, Space, Row, Col } from 'antd'; +import { Form, Input, Space, Row, Col, Switch } from 'antd'; import StandardFormRow from '@/components/StandardFormRow'; import TagSelect from '@/components/TagSelect'; import React, { useEffect } from 'react'; @@ -10,20 +10,21 @@ import styles from '../style.less'; const FormItem = Form.Item; type Props = { - filterValues?: any; + initFilterValues?: any; onFiltersChange: (_: any, values: any) => void; }; -const MetricFilter: React.FC = ({ filterValues = {}, onFiltersChange }) => { +const MetricFilter: React.FC = ({ initFilterValues = {}, onFiltersChange }) => { const [form] = Form.useForm(); useEffect(() => { form.setFieldsValue({ - ...filterValues, + ...initFilterValues, }); - }, [form, filterValues]); + }, [form]); const handleValuesChange = (value: any, values: any) => { + localStorage.setItem('metricMarketShowType', !!values.showType ? '1' : '0'); onFiltersChange(value, values); }; @@ -32,17 +33,6 @@ const MetricFilter: React.FC = ({ filterValues = {}, onFiltersChange }) = }; const filterList = [ - // { - // title: '指标类型', - // key: 'type', - // options: [ - // { - // value: 'ATOMIC', - // label: '原子指标', - // }, - // { value: 'DERIVED', label: '衍生指标' }, - // ], - // }, { title: '敏感度', key: 'sensitiveLevel', @@ -94,6 +84,11 @@ const MetricFilter: React.FC = ({ filterValues = {}, onFiltersChange }) =
+ + + + + @@ -103,17 +98,15 @@ const MetricFilter: React.FC = ({ filterValues = {}, onFiltersChange }) = const { title, key, options } = item; return ( -
- - - {options.map((item: any) => ( - - {item.label} - - ))} - - -
+ + + {options.map((item: any) => ( + + {item.label} + + ))} + +
); })} diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/components/MetricTrend.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/components/MetricTrend.tsx new file mode 100644 index 000000000..8a94bb01d --- /dev/null +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/components/MetricTrend.tsx @@ -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 = ({ + title, + tip, + data, + fields, + loading, + isPer, + isPercent, + dateFieldName, + // columnFieldName, + // valueFieldName, + dateFormat, + height, + renderType, + decimalPlaces, + onDownload, +}) => { + const chartRef = useRef(); + const [instance, setInstance] = useState(); + 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) => + `
${ + item.marker + } ${ + item.seriesName + }${ + item.value === '' + ? '-' + : isPer + ? `${formatByDecimalPlaces(item.value, decimalPlaces ?? 2)}%` + : isPercent + ? formatByPercentageData(item.value, decimalPlaces ?? 2) + : getFormattedValueData(item.value) + }
`, + ) + .join(''); + return `${param.name}
${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 ( +
+ {title && ( +
+
{title}
+ {onDownload && ( + + + + )} +
+ )} + 300 ? 9 : 6 }} + /> +
+
+ ); +}; + +export default TrendChart; diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/components/MetricTrendSection.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/components/MetricTrendSection.tsx new file mode 100644 index 000000000..bfc75eac2 --- /dev/null +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/components/MetricTrendSection.tsx @@ -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 = ({ 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([]); + const [metricTrendLoading, setMetricTrendLoading] = useState(false); + const [metricColumnConfig, setMetricColumnConfig] = useState(); + const [authMessage, setAuthMessage] = useState(''); + 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 ( + <> +
+ { + 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} + /> +
+
指标存在如下权限问题: {authMessage}
+ + + ); +}; + +export default MetricTrendSection; diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/index.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/index.tsx index 609dbd756..9a949054d 100644 --- a/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/index.tsx +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/index.tsx @@ -1,6 +1,6 @@ import type { ActionType, ProColumns } from '@ant-design/pro-table'; import ProTable from '@ant-design/pro-table'; -import { message, Space, Popconfirm, Tag } from 'antd'; +import { message, Space, Popconfirm, Tag, Spin } from 'antd'; import React, { useRef, useState, useEffect } from 'react'; import type { Dispatch } from 'umi'; import { connect, history } from 'umi'; @@ -9,9 +9,12 @@ import { SENSITIVE_LEVEL_ENUM } from '../constant'; import { queryMetric, deleteMetric } from '../service'; import MetricFilter from './components/MetricFilter'; import MetricInfoCreateForm from '../components/MetricInfoCreateForm'; +import MetricCardList from './components/MetricCardList'; +import NodeInfoDrawer from '../SemanticGraph/components/NodeInfoDrawer'; +import { SemanticNodeType } from '../enum'; import moment from 'moment'; import styles from './style.less'; -import { IDataSource, ISemantic } from '../data'; +import { ISemantic } from '../data'; type Props = { dispatch: Dispatch; @@ -30,19 +33,23 @@ type QueryMetricListParams = { const ClassMetricTable: React.FC = ({ domainManger, dispatch }) => { const { selectDomainId, selectModelId: modelId } = domainManger; const [createModalVisible, setCreateModalVisible] = useState(false); - const [pagination, setPagination] = useState({ + const defaultPagination = { current: 1, pageSize: 20, total: 0, - }); + }; + const [pagination, setPagination] = useState(defaultPagination); const [loading, setLoading] = useState(false); - const [dataSource, setDataSource] = useState([]); + const [dataSource, setDataSource] = useState([]); const [metricItem, setMetricItem] = useState(); - const [filterParams, setFilterParams] = useState>({}); + const [filterParams, setFilterParams] = useState>({ + showType: localStorage.getItem('metricMarketShowType') === '1' ? true : false, + }); + const [infoDrawerVisible, setInfoDrawerVisible] = useState(false); const actionRef = useRef(); useEffect(() => { - queryMetricList(); + queryMetricList(filterParams); }, []); const queryMetricList = async (params: QueryMetricListParams = {}, disabledLoading = false) => { @@ -52,10 +59,9 @@ const ClassMetricTable: React.FC = ({ domainManger, dispatch }) => { const { code, data, msg } = await queryMetric({ ...pagination, ...params, + pageSize: params.showType ? 100 : defaultPagination.pageSize, }); - if (!disabledLoading) { - setLoading(false); - } + setLoading(false); const { list, pageSize, current, total } = data || {}; let resData: any = {}; if (code === 200) { @@ -81,6 +87,21 @@ const ClassMetricTable: React.FC = ({ domainManger, dispatch }) => { return resData; }; + const deleteMetricQuery = async (id: number) => { + const { code, msg } = await deleteMetric(id); + if (code === 200) { + setMetricItem(undefined); + queryMetricList(filterParams); + } else { + message.error(msg); + } + }; + + const handleMetricEdit = (metricItem: ISemantic.IMetricItem) => { + setMetricItem(metricItem); + setCreateModalVisible(true); + }; + const columns: ProColumns[] = [ { dataIndex: 'id', @@ -90,18 +111,16 @@ const ClassMetricTable: React.FC = ({ domainManger, dispatch }) => { dataIndex: 'name', title: '指标名称', render: (_, record: any) => { - if (record.hasAdminRes) { - return ( - { - history.replace(`/model/${record.domainId}/${record.modelId}/metric`); - }} - > - {record.name} - - ); - } - return <> {record.name}; + return ( + { + setMetricItem(record); + setInfoDrawerVisible(true); + }} + > + {record.name} + + ); }, }, // { @@ -116,6 +135,20 @@ const ClassMetricTable: React.FC = ({ domainManger, dispatch }) => { { dataIndex: 'modelName', title: '所属模型', + render: (_, record: any) => { + if (record.hasAdminRes) { + return ( + { + history.replace(`/model/${record.domainId}/${record.modelId}/metric`); + }} + > + {record.modelName} + + ); + } + return <> {record.modelName}; + }, }, { dataIndex: 'sensitiveLevel', @@ -179,27 +212,13 @@ const ClassMetricTable: React.FC = ({ domainManger, dispatch }) => { { - setMetricItem(record); - setCreateModalVisible(true); + handleMetricEdit(record); }} > 编辑 - { - const { code, msg } = await deleteMetric(record.id); - if (code === 200) { - setMetricItem(undefined); - queryMetricList(); - } else { - message.error(msg); - } - }} - > + {}}> { @@ -238,36 +257,64 @@ const ClassMetricTable: React.FC = ({ domainManger, dispatch }) => { <>
{ + if (_.showType !== undefined) { + setLoading(true); + setDataSource([]); + } handleFilterChange(values); }} />
- { - return false; - }} - loading={loading} - onChange={(data: any) => { - const { current, pageSize, total } = data; - const pagin = { - current, - pageSize, - total, - }; - setPagination(pagin); - queryMetricList({ ...pagin, ...filterParams }); - }} - size="small" - options={{ reload: false, density: false, fullScreen: false }} - /> + <> + {filterParams.showType ? ( + + { + setInfoDrawerVisible(true); + setMetricItem(metricItem); + }} + onDeleteBtnClick={(metricItem) => { + deleteMetricQuery(metricItem.id); + }} + onEditBtnClick={(metricItem) => { + setMetricItem(metricItem); + setCreateModalVisible(true); + }} + /> + + ) : ( + { + 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 && ( = ({ domainManger, dispatch }) => { metricItem={metricItem} onSubmit={() => { setCreateModalVisible(false); - queryMetricList(); + queryMetricList(filterParams); dispatch({ type: 'domainManger/queryMetricList', payload: { @@ -289,6 +336,26 @@ const ClassMetricTable: React.FC = ({ domainManger, dispatch }) => { }} /> )} + {infoDrawerVisible && ( + { + setInfoDrawerVisible(false); + }} + width="100%" + open={infoDrawerVisible} + mask={true} + getContainer={false} + onEditBtnClick={(nodeData: any) => { + handleMetricEdit(nodeData); + }} + maskClosable={true} + onNodeChange={({ eventName }: { eventName: string }) => { + setInfoDrawerVisible(false); + }} + /> + )} ); }; diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/style.less b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/style.less index df11043b0..e3b51c01c 100644 --- a/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/style.less +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/style.less @@ -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; + } + } + } } \ No newline at end of file diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/components/NodeInfoDrawer.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/components/NodeInfoDrawer.tsx index 9be2cb14e..289ea5153 100644 --- a/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/components/NodeInfoDrawer.tsx +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/components/NodeInfoDrawer.tsx @@ -1,15 +1,4 @@ -import { - Button, - Drawer, - message, - Row, - Col, - Divider, - Tag, - Space, - Typography, - Popconfirm, -} from 'antd'; +import { Button, Drawer, message, Row, Col, Divider, Tag, Space, Popconfirm } from 'antd'; import React, { useState, useEffect, ReactNode } from 'react'; import { SemanticNodeType } from '../../enum'; import { deleteDimension, deleteMetric, deleteDatasource } from '../../service'; @@ -18,11 +7,9 @@ import type { StateType } from '../../model'; import moment from 'moment'; import styles from '../style.less'; import TransTypeTag from '../../components/TransTypeTag'; -import { MetricTypeWording } from '../../enum'; +import MetricTrendSection from '@/pages/SemanticModel/Metric/components/MetricTrendSection'; import { SENSITIVE_LEVEL_ENUM } from '../../constant'; -const { Paragraph } = Typography; - type Props = { nodeData: any; domainManger: StateType; @@ -46,7 +33,8 @@ type InfoListItemChildrenItem = { type InfoListItem = { title: string; hideItem?: boolean; - children: InfoListItemChildrenItem[]; + render?: () => ReactNode; + children?: InfoListItemChildrenItem[]; }; const DescriptionItem = ({ title, content }: DescriptionItemProps) => ( @@ -66,7 +54,8 @@ const NodeInfoDrawer: React.FC = ({ ...restProps }) => { const [infoList, setInfoList] = useState([]); - const { selectDomainName } = domainManger; + const { selectModelName } = domainManger; + useEffect(() => { if (!nodeData) { return; @@ -79,9 +68,9 @@ const NodeInfoDrawer: React.FC = ({ createdAt, updatedAt, description, - domainName, + // domainName, sensitiveLevel, - type, + modelName, nodeType, } = nodeData; @@ -99,9 +88,9 @@ const NodeInfoDrawer: React.FC = ({ value: alias || '-', }, { - label: '所属主题域', - value: domainName, - content: {domainName || selectDomainName}, + label: '所属模型', + value: modelName, + content: {modelName || selectModelName}, }, { @@ -113,26 +102,24 @@ const NodeInfoDrawer: React.FC = ({ { title: '应用信息', children: [ - // { - // label: '全路径', - // value: fullPath, - // content: ( - // - // {fullPath} - // - // ), - // }, { label: '敏感度', value: SENSITIVE_LEVEL_ENUM[sensitiveLevel], }, - // { - // label: '指标类型', - // value: MetricTypeWording[type], - // hideItem: nodeType !== SemanticNodeType.METRIC, - // }, ], }, + { + title: '指标趋势', + render: () => ( +
+ + + + + +
+ ), + }, { title: '创建信息', children: [ @@ -161,9 +148,9 @@ const NodeInfoDrawer: React.FC = ({ value: bizName, }, { - label: '所属主题域', - value: domainName, - content: {domainName || selectDomainName}, + label: '所属模型', + value: modelName, + content: {modelName || selectModelName}, }, { label: '描述', @@ -259,31 +246,36 @@ const NodeInfoDrawer: React.FC = ({ >
{infoList.map((item) => { - const { children, title } = item; + const { children, title, render } = item; return (

{title}

- {children.map((childrenItem) => { - return ( - - - - - - ); - })} + {render?.() || + (Array.isArray(children) && + children.map((childrenItem) => { + return ( + + + + + + ); + }))}
); })}
- {extraNode} + {nodeData?.hasAdminRes && extraNode} ); diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/components/DimensionAndMetricRelationModal.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/components/DimensionAndMetricRelationModal.tsx new file mode 100644 index 000000000..f449e8ea3 --- /dev/null +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/components/DimensionAndMetricRelationModal.tsx @@ -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 = ({ + open, + relationsInitialValue, + onCancel, + onSubmit, +}) => { + const [relationList, setRelationList] = useState([]); + + const renderFooter = () => { + return ( + <> + + + + ); + }; + + return ( + <> + + } + maskClosable={false} + open={open} + footer={renderFooter()} + onCancel={onCancel} + > +
+ { + setRelationList(relations); + }} + /> +
+
+ + ); +}; + +export default DimensionAndMetricRelationModal; diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/components/DimensionMetricRelationTableTransfer.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/components/DimensionMetricRelationTableTransfer.tsx new file mode 100644 index 000000000..4a604b3ab --- /dev/null +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/components/DimensionMetricRelationTableTransfer.tsx @@ -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 = ({ + domainManger, + relationsInitialValue, + onChange, +}) => { + const { dimensionList } = domainManger; + + const [targetKeys, setTargetKeys] = useState([]); + + const [checkedMap, setCheckedMap] = useState>( + {}, + ); + + 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, + ) => { + 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 = [ + { + dataIndex: 'name', + title: '名称', + }, + { + dataIndex: 'transType', + width: 80, + title: '类型', + render: (transType: SemanticNodeType) => { + return ; + }, + }, + { + dataIndex: 'y', + title: ( + + ), + width: 120, + render: (_: any, record: RecordType) => { + const { transType, id } = record; + return transType === TransType.DIMENSION ? ( + { + updateRelationCheckedMap(record, { dimensionId: id, necessary: e.target.checked }); + }} + onClick={(event) => { + event.stopPropagation(); + }} + /> + ) : ( + <> + ); + }, + }, + ]; + + const leftColumns: ColumnsType = [ + { + dataIndex: 'name', + title: '名称', + }, + { + dataIndex: 'transType', + title: '类型', + render: (transType) => { + return ; + }, + }, + ]; + + return ( + <> + { + 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 = { + 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 ( + ({ + onClick: () => { + onItemSelect(key as string, !listSelectedKeys.includes(key as string)); + }, + })} + /> + ); + }} + + + ); +}; + +export default connect(({ domainManger }: { domainManger: StateType }) => ({ + domainManger, +}))(DimensionMetricRelationTableTransfer); diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/components/DimensionValueSettingModal.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/components/DimensionValueSettingModal.tsx index 12406e715..18bc00f6d 100644 --- a/webapp/packages/supersonic-fe/src/pages/SemanticModel/components/DimensionValueSettingModal.tsx +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/components/DimensionValueSettingModal.tsx @@ -46,8 +46,8 @@ const DimensionInfoModal: React.FC = ({ return; } const queryParams = { + ...dimensionItem, domainId: selectDomainId, - id: dimensionItem.id, ...fieldsValue, }; const { code, msg } = await updateDimension(queryParams); diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/components/MetricInfoCreateForm.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/components/MetricInfoCreateForm.tsx index 95777067b..384792932 100644 --- a/webapp/packages/supersonic-fe/src/pages/SemanticModel/components/MetricInfoCreateForm.tsx +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/components/MetricInfoCreateForm.tsx @@ -23,6 +23,7 @@ import { formLayout } from '@/components/FormHelper/utils'; import FormItemTitle from '@/components/FormHelper/FormItemTitle'; import styles from './style.less'; import { getMeasureListByModelId } from '../service'; +import DimensionAndMetricRelationModal from './DimensionAndMetricRelationModal'; import TableTitleTooltips from '../components/TableTitleTooltips'; import { creatExprMetric, updateExprMetric, mockMetricAlias, getMetricTags } from '../service'; import { ISemantic } from '../data'; @@ -77,6 +78,12 @@ const MetricInfoCreateForm: React.FC = ({ const [tagOptions, setTagOptions] = useState<{ label: string; value: string }[]>([]); + const [metricRelationModalOpenState, setMetricRelationModalOpenState] = useState(false); + + const [drillDownDimensions, setDrillDownDimensions] = useState< + ISemantic.IDrillDownDimensionItem[] + >(metricItem?.relateDimension?.drillDownDimensions || []); + const forward = () => setCurrentStep(currentStep + 1); const backward = () => setCurrentStep(currentStep - 1); @@ -169,6 +176,10 @@ const MetricInfoCreateForm: React.FC = ({ const saveMetric = async (fieldsValue: any) => { const queryParams = { modelId: isEdit ? metricItem.modelId : modelId, + relateDimension: { + ...(metricItem?.relateDimension || {}), + drillDownDimensions, + }, ...fieldsValue, }; const { typeParams, alias, dataFormatType } = queryParams; @@ -346,6 +357,23 @@ const MetricInfoCreateForm: React.FC = ({ >