From 2c62a77cf47f79c1d72a6bdc63b125da5d7d0dea Mon Sep 17 00:00:00 2001 From: Joel Date: Mon, 10 Nov 2025 18:39:12 +0800 Subject: [PATCH] Chore: change query log time range (#28052) Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../[appId]/overview/chart-view.tsx | 45 +++------- .../time-range-picker/date-picker.tsx | 80 +++++++++++++++++ .../overview/time-range-picker/index.tsx | 86 +++++++++++++++++++ .../time-range-picker/range-selector.tsx | 81 +++++++++++++++++ web/app/components/app/overview/app-chart.tsx | 15 +++- .../date-and-time-picker/calendar/index.tsx | 4 +- .../date-and-time-picker/calendar/item.tsx | 4 +- .../date-picker/index.tsx | 27 ++++-- .../base/date-and-time-picker/types.ts | 4 + .../assets/vender/other/hourglass-shape.svg | 3 + .../src/vender/other/HourglassShape.json | 27 ++++++ .../icons/src/vender/other/HourglassShape.tsx | 20 +++++ .../base/icons/src/vender/other/index.ts | 1 + web/app/components/base/select/index.tsx | 4 +- web/i18n/en-US/app-log.ts | 2 + web/i18n/ja-JP/app-log.ts | 2 + web/i18n/zh-Hans/app-log.ts | 2 + web/utils/format.ts | 51 +++++++++++ 18 files changed, 410 insertions(+), 48 deletions(-) create mode 100644 web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/date-picker.tsx create mode 100644 web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/index.tsx create mode 100644 web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/range-selector.tsx create mode 100644 web/app/components/base/icons/assets/vender/other/hourglass-shape.svg create mode 100644 web/app/components/base/icons/src/vender/other/HourglassShape.json create mode 100644 web/app/components/base/icons/src/vender/other/HourglassShape.tsx diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chart-view.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chart-view.tsx index 847de19165..d5430b03ad 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chart-view.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chart-view.tsx @@ -5,15 +5,19 @@ import quarterOfYear from 'dayjs/plugin/quarterOfYear' import { useTranslation } from 'react-i18next' import type { PeriodParams } from '@/app/components/app/overview/app-chart' import { AvgResponseTime, AvgSessionInteractions, AvgUserInteractions, ConversationsChart, CostChart, EndUsersChart, MessagesChart, TokenPerSecond, UserSatisfactionRate, WorkflowCostChart, WorkflowDailyTerminalsChart, WorkflowMessagesChart } from '@/app/components/app/overview/app-chart' -import type { Item } from '@/app/components/base/select' -import { SimpleSelect } from '@/app/components/base/select' -import { TIME_PERIOD_MAPPING } from '@/app/components/app/log/filter' import { useStore as useAppStore } from '@/app/components/app/store' +import TimeRangePicker from './time-range-picker' dayjs.extend(quarterOfYear) const today = dayjs() +const TIME_PERIOD_MAPPING = [ + { value: 0, name: 'today' }, + { value: 7, name: 'last7days' }, + { value: 30, name: 'last30days' }, +] + const queryDateFormat = 'YYYY-MM-DD HH:mm' export type IChartViewProps = { @@ -26,21 +30,7 @@ export default function ChartView({ appId, headerRight }: IChartViewProps) { const appDetail = useAppStore(state => state.appDetail) const isChatApp = appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow' const isWorkflow = appDetail?.mode === 'workflow' - const [period, setPeriod] = useState({ name: t('appLog.filter.period.last7days'), query: { start: today.subtract(7, 'day').startOf('day').format(queryDateFormat), end: today.endOf('day').format(queryDateFormat) } }) - - const onSelect = (item: Item) => { - if (item.value === -1) { - setPeriod({ name: item.name, query: undefined }) - } - else if (item.value === 0) { - const startOfToday = today.startOf('day').format(queryDateFormat) - const endOfToday = today.endOf('day').format(queryDateFormat) - setPeriod({ name: item.name, query: { start: startOfToday, end: endOfToday } }) - } - else { - setPeriod({ name: item.name, query: { start: today.subtract(item.value as number, 'day').startOf('day').format(queryDateFormat), end: today.endOf('day').format(queryDateFormat) } }) - } - } + const [period, setPeriod] = useState({ name: t('appLog.filter.period.today'), query: { start: today.startOf('day').format(queryDateFormat), end: today.endOf('day').format(queryDateFormat) } }) if (!appDetail) return null @@ -50,20 +40,11 @@ export default function ChartView({ appId, headerRight }: IChartViewProps) {
{t('common.appMenus.overview')}
-
- ({ value: k, name: t(`appLog.filter.period.${v.name}`) }))} - className='mt-0 !w-40' - notClearable={true} - onSelect={(item) => { - const id = item.value - const value = TIME_PERIOD_MAPPING[id]?.value ?? '-1' - const name = item.name || t('appLog.filter.period.allTime') - onSelect({ value, name }) - }} - defaultValue={'2'} - /> -
+ {headerRight}
diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/date-picker.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/date-picker.tsx new file mode 100644 index 0000000000..2bfdece433 --- /dev/null +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/date-picker.tsx @@ -0,0 +1,80 @@ +'use client' +import { RiCalendarLine } from '@remixicon/react' +import type { Dayjs } from 'dayjs' +import type { FC } from 'react' +import React, { useCallback } from 'react' +import cn from '@/utils/classnames' +import { formatToLocalTime } from '@/utils/format' +import { useI18N } from '@/context/i18n' +import Picker from '@/app/components/base/date-and-time-picker/date-picker' +import type { TriggerProps } from '@/app/components/base/date-and-time-picker/types' +import { noop } from 'lodash-es' +import dayjs from 'dayjs' + +type Props = { + start: Dayjs + end: Dayjs + onStartChange: (date?: Dayjs) => void + onEndChange: (date?: Dayjs) => void +} + +const today = dayjs() +const DatePicker: FC = ({ + start, + end, + onStartChange, + onEndChange, +}) => { + const { locale } = useI18N() + + const renderDate = useCallback(({ value, handleClickTrigger, isOpen }: TriggerProps) => { + return ( +
+ {value ? formatToLocalTime(value, locale, 'MMM D') : ''} +
+ ) + }, [locale]) + + const availableStartDate = end.subtract(30, 'day') + const startDateDisabled = useCallback((date: Dayjs) => { + if (date.isAfter(today, 'date')) + return true + return !((date.isAfter(availableStartDate, 'date') || date.isSame(availableStartDate, 'date')) && (date.isBefore(end, 'date') || date.isSame(end, 'date'))) + }, [availableStartDate, end]) + + const availableEndDate = start.add(30, 'day') + const endDateDisabled = useCallback((date: Dayjs) => { + if (date.isAfter(today, 'date')) + return true + return !((date.isAfter(start, 'date') || date.isSame(start, 'date')) && (date.isBefore(availableEndDate, 'date') || date.isSame(availableEndDate, 'date'))) + }, [availableEndDate, start]) + + return ( +
+
+ +
+ + - + +
+ + ) +} +export default React.memo(DatePicker) diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/index.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/index.tsx new file mode 100644 index 0000000000..4738bdeebf --- /dev/null +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/index.tsx @@ -0,0 +1,86 @@ +'use client' +import type { PeriodParams, PeriodParamsWithTimeRange } from '@/app/components/app/overview/app-chart' +import type { FC } from 'react' +import React, { useCallback, useState } from 'react' +import type { Dayjs } from 'dayjs' +import { HourglassShape } from '@/app/components/base/icons/src/vender/other' +import RangeSelector from './range-selector' +import DatePicker from './date-picker' +import dayjs from 'dayjs' +import { useI18N } from '@/context/i18n' +import { formatToLocalTime } from '@/utils/format' + +const today = dayjs() + +type Props = { + ranges: { value: number; name: string }[] + onSelect: (payload: PeriodParams) => void + queryDateFormat: string +} + +const TimeRangePicker: FC = ({ + ranges, + onSelect, + queryDateFormat, +}) => { + const { locale } = useI18N() + + const [isCustomRange, setIsCustomRange] = useState(false) + const [start, setStart] = useState(today) + const [end, setEnd] = useState(today) + + const handleRangeChange = useCallback((payload: PeriodParamsWithTimeRange) => { + setIsCustomRange(false) + setStart(payload.query!.start) + setEnd(payload.query!.end) + onSelect({ + name: payload.name, + query: { + start: payload.query!.start.format(queryDateFormat), + end: payload.query!.end.format(queryDateFormat), + }, + }) + }, [onSelect, queryDateFormat]) + + const handleDateChange = useCallback((type: 'start' | 'end') => { + return (date?: Dayjs) => { + if (!date) return + if (type === 'start' && date.isSame(start)) return + if (type === 'end' && date.isSame(end)) return + if (type === 'start') + setStart(date) + else + setEnd(date) + + const currStart = type === 'start' ? date : start + const currEnd = type === 'end' ? date : end + onSelect({ + name: `${formatToLocalTime(currStart, locale, 'MMM D')} - ${formatToLocalTime(currEnd, locale, 'MMM D')}`, + query: { + start: currStart.format(queryDateFormat), + end: currEnd.format(queryDateFormat), + }, + }) + + setIsCustomRange(true) + } + }, [start, end, onSelect, locale, queryDateFormat]) + + return ( +
+ + + +
+ ) +} +export default React.memo(TimeRangePicker) diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/range-selector.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/range-selector.tsx new file mode 100644 index 0000000000..f99ea52492 --- /dev/null +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/range-selector.tsx @@ -0,0 +1,81 @@ +'use client' +import type { PeriodParamsWithTimeRange, TimeRange } from '@/app/components/app/overview/app-chart' +import type { FC } from 'react' +import React, { useCallback } from 'react' +import { SimpleSelect } from '@/app/components/base/select' +import type { Item } from '@/app/components/base/select' +import dayjs from 'dayjs' +import { RiArrowDownSLine, RiCheckLine } from '@remixicon/react' +import cn from '@/utils/classnames' +import { useTranslation } from 'react-i18next' + +const today = dayjs() + +type Props = { + isCustomRange: boolean + ranges: { value: number; name: string }[] + onSelect: (payload: PeriodParamsWithTimeRange) => void +} + +const RangeSelector: FC = ({ + isCustomRange, + ranges, + onSelect, +}) => { + const { t } = useTranslation() + + const handleSelectRange = useCallback((item: Item) => { + const { name, value } = item + let period: TimeRange | null = null + if (value === 0) { + const startOfToday = today.startOf('day') + const endOfToday = today.endOf('day') + period = { start: startOfToday, end: endOfToday } + } + else { + period = { start: today.subtract(item.value as number, 'day').startOf('day'), end: today.endOf('day') } + } + onSelect({ query: period!, name }) + }, [onSelect]) + + const renderTrigger = useCallback((item: Item | null, isOpen: boolean) => { + return ( +
+
{isCustomRange ? t('appLog.filter.period.custom') : item?.name}
+ +
+ ) + }, [isCustomRange]) + + const renderOption = useCallback(({ item, selected }: { item: Item; selected: boolean }) => { + return ( + <> + {selected && ( + + + )} + {item.name} + + ) + }, []) + return ( + ({ ...v, name: t(`appLog.filter.period.${v.name}`) }))} + className='mt-0 !w-40' + notClearable={true} + onSelect={handleSelectRange} + defaultValue={0} + wrapperClassName='h-8' + optionWrapClassName='w-[200px] translate-x-[-24px]' + renderTrigger={renderTrigger} + optionClassName='flex items-center py-0 pl-7 pr-2 h-8' + renderOption={renderOption} + /> + ) +} +export default React.memo(RangeSelector) diff --git a/web/app/components/app/overview/app-chart.tsx b/web/app/components/app/overview/app-chart.tsx index c550f0b23f..8f28e16402 100644 --- a/web/app/components/app/overview/app-chart.tsx +++ b/web/app/components/app/overview/app-chart.tsx @@ -4,6 +4,7 @@ import React from 'react' import ReactECharts from 'echarts-for-react' import type { EChartsOption } from 'echarts' import useSWR from 'swr' +import type { Dayjs } from 'dayjs' import dayjs from 'dayjs' import { get } from 'lodash-es' import Decimal from 'decimal.js' @@ -78,6 +79,16 @@ export type PeriodParams = { } } +export type TimeRange = { + start: Dayjs + end: Dayjs +} + +export type PeriodParamsWithTimeRange = { + name: string + query?: TimeRange +} + export type IBizChartProps = { period: PeriodParams id: string @@ -215,9 +226,7 @@ const Chart: React.FC = ({ formatter(params) { return `
${params.name}
${valueFormatter((params.data as any)[yField])} - ${!CHART_TYPE_CONFIG[chartType].showTokens - ? '' - : ` + ${!CHART_TYPE_CONFIG[chartType].showTokens ? '' : ` ( ~$${get(params.data, 'total_price', 0)} ) diff --git a/web/app/components/base/date-and-time-picker/calendar/index.tsx b/web/app/components/base/date-and-time-picker/calendar/index.tsx index 00612fcb37..03dcb0eda3 100644 --- a/web/app/components/base/date-and-time-picker/calendar/index.tsx +++ b/web/app/components/base/date-and-time-picker/calendar/index.tsx @@ -8,9 +8,10 @@ const Calendar: FC = ({ selectedDate, onDateClick, wrapperClassName, + getIsDateDisabled, }) => { return
- +
{ days.map(day => = ({ day={day} selectedDate={selectedDate} onClick={onDateClick} + isDisabled={getIsDateDisabled ? getIsDateDisabled(day.date) : false} />) }
diff --git a/web/app/components/base/date-and-time-picker/calendar/item.tsx b/web/app/components/base/date-and-time-picker/calendar/item.tsx index 1da8b9b3b5..7132d7bdfb 100644 --- a/web/app/components/base/date-and-time-picker/calendar/item.tsx +++ b/web/app/components/base/date-and-time-picker/calendar/item.tsx @@ -7,6 +7,7 @@ const Item: FC = ({ day, selectedDate, onClick, + isDisabled, }) => { const { date, isCurrentMonth } = day const isSelected = selectedDate?.isSame(date, 'date') @@ -14,11 +15,12 @@ const Item: FC = ({ return (