From 451faa0e68fe51d5960d184e5dd234d9926a3848 Mon Sep 17 00:00:00 2001 From: Stephen Zhou Date: Thu, 2 Jul 2026 15:30:25 +0800 Subject: [PATCH] fix(web): align monitoring overview charts (#38292) --- .../__tests__/app-chart-utils.spec.ts | 20 ++++- .../app/overview/__tests__/app-chart.spec.tsx | 21 +++-- .../app/overview/app-chart-utils.ts | 14 +++- web/app/components/app/overview/app-chart.tsx | 79 ++++++++++++------- 4 files changed, 96 insertions(+), 38 deletions(-) diff --git a/web/app/components/app/overview/__tests__/app-chart-utils.spec.ts b/web/app/components/app/overview/__tests__/app-chart-utils.spec.ts index 5064af5f36d..4d51e201d21 100644 --- a/web/app/components/app/overview/__tests__/app-chart-utils.spec.ts +++ b/web/app/components/app/overview/__tests__/app-chart-utils.spec.ts @@ -1,5 +1,5 @@ /* eslint-disable ts/no-explicit-any */ -import { buildChartOptions, getChartValueField, getDefaultChartData, getSummaryValue, getTokenSummary } from '../app-chart-utils' +import { buildChartOptions, getChartValueField, getDefaultChartData, getSummaryValue, getTokenSummary, hasNonZeroChartData } from '../app-chart-utils' describe('app-chart-utils', () => { describe('getDefaultChartData', () => { @@ -16,6 +16,20 @@ describe('app-chart-utils', () => { }) }) + describe('hasNonZeroChartData', () => { + it('should detect whether rows contain a non-zero chart value', () => { + expect(hasNonZeroChartData([ + { date: 'Jan 1, 2024', count: 0 }, + { date: 'Jan 2, 2024', count: '0' }, + ], 'count')).toBe(false) + + expect(hasNonZeroChartData([ + { date: 'Jan 1, 2024', count: 0 }, + { date: 'Jan 2, 2024', count: 1 }, + ], 'count')).toBe(true) + }) + }) + describe('getSummaryValue', () => { it('should average values when the chart is configured as an average', () => { const summaryValue = getSummaryValue({ @@ -91,11 +105,15 @@ describe('app-chart-utils', () => { }) const dataset = options.dataset as { dimensions: string[], source: Array> } + const grid = options.grid as { top: number, left: number, right: number } const yAxis = options.yAxis as { max: number } const series = options.series as Array<{ lineStyle: { color: string } }> expect(dataset.dimensions).toEqual(['date', 'count']) expect(dataset.source).toHaveLength(2) + expect(grid.top).toBe(16) + expect(grid.left).toBe(0) + expect(grid.right).toBe(0) expect(yAxis.max).toBe(100) expect(series[0]!.lineStyle.color).toBe('rgba(6, 148, 162, 1)') }) diff --git a/web/app/components/app/overview/__tests__/app-chart.spec.tsx b/web/app/components/app/overview/__tests__/app-chart.spec.tsx index d6aa2414055..8c8bd15f054 100644 --- a/web/app/components/app/overview/__tests__/app-chart.spec.tsx +++ b/web/app/components/app/overview/__tests__/app-chart.spec.tsx @@ -10,8 +10,8 @@ vi.mock('react-i18next', () => ({ })) vi.mock('echarts-for-react', () => ({ - default: ({ option }: { option: unknown }) => { - reactEChartsMock(option) + default: (props: { option: unknown, opts?: unknown }) => { + reactEChartsMock(props) return
}, })) @@ -59,7 +59,10 @@ describe('app-chart', () => { ) expect(screen.getByText('Cost title'))!.toBeInTheDocument() + expect(screen.getByText('Cost title'))!.toHaveClass('system-sm-semibold-uppercase') expect(screen.getByText('300'))!.toBeInTheDocument() + expect(screen.getByText('300'))!.toHaveClass('title-3xl-semi-bold') + expect(screen.queryByText('Last 7 days'))!.not.toBeInTheDocument() expect(screen.getByText(/\$3\.7500/))!.toBeInTheDocument() expect(screen.getByTestId('echarts'))!.toBeInTheDocument() }) @@ -86,16 +89,22 @@ describe('app-chart', () => { ) expect(screen.getByText('analysis.totalMessages.title'))!.toBeInTheDocument() + expect(screen.getByText('0'))!.toBeInTheDocument() expect(screen.getByTestId('echarts'))!.toBeInTheDocument() - const options = reactEChartsMock.mock.calls[0]![0] as { - dataset: { source: Array> } - yAxis: { max: number } + const chartProps = reactEChartsMock.mock.calls[0]![0] as { + option: { + dataset: { source: Array> } + yAxis: { max: number } + } + opts: { renderer: string } } + const options = chartProps.option + expect(chartProps.opts).toEqual({ renderer: 'svg' }) expect(options.yAxis.max).toBe(500) expect(options.dataset.source).toHaveLength(3) - expect(options.dataset.source[0]).toEqual({ date: 'Jan 1, 2024', count: 0 }) + expect(options.dataset.source[0]).toEqual({ date: 'Jan 1, 2024', message_count: 0 }) }) }) }) diff --git a/web/app/components/app/overview/app-chart-utils.ts b/web/app/components/app/overview/app-chart-utils.ts index 6777bacce77..f69065eeaec 100644 --- a/web/app/components/app/overview/app-chart-utils.ts +++ b/web/app/components/app/overview/app-chart-utils.ts @@ -69,6 +69,7 @@ export const CHART_TYPE_CONFIG: Record = { }, workflowCosts: { colorType: 'blue', + showTokens: true, }, } @@ -86,7 +87,7 @@ const getMarkLineSeedData = (statisticsLength: number) => { const getTooltipContent = (chartType: ChartType, yField: string, params: TooltipParams) => { const row = params.data ?? { date: params.name } const value = valueFormatter(row[yField] ?? 0) - if (!CHART_TYPE_CONFIG[chartType].showTokens) + if (!CHART_TYPE_CONFIG[chartType].showTokens || row.total_price === undefined) return `
${params.name}
${value}
` return `
${params.name}
@@ -106,6 +107,13 @@ export const getChartValueField = (statistics: ChartRow[], valueKey?: string) => return Object.keys(statistics[0] ?? {}).find(name => name.includes('count')) ?? 'count' } +export const hasNonZeroChartData = (statistics: ChartRow[], yField: string) => { + return statistics.some((row) => { + const value = Number.parseFloat(String(row[yField] ?? 0)) + return Number.isFinite(value) && value !== 0 + }) +} + export const getSummaryValue = ({ chartType, statistics, @@ -123,7 +131,7 @@ export const getSummaryValue = ({ const divisor = values.length || 1 const sumData = isAvg ? (sumValues(values) / divisor) : sumValues(values) - if (chartType === 'costs') { + if (chartType === 'costs' || chartType === 'workflowCosts') { const formattedCost = sumData < 1000 ? sumData : `${formatNumber(Math.round(sumData / 1000))}k` @@ -163,7 +171,7 @@ export const buildChartOptions = ({ dimensions: ['date', yField], source: statistics, }, - grid: { top: 8, right: 36, bottom: 10, left: 25, containLabel: true }, + grid: { top: 16, right: 0, bottom: 10, left: 0, containLabel: true }, tooltip: { trigger: 'item', position: 'top', diff --git a/web/app/components/app/overview/app-chart.tsx b/web/app/components/app/overview/app-chart.tsx index f95f77ef58f..d90076bc0ca 100644 --- a/web/app/components/app/overview/app-chart.tsx +++ b/web/app/components/app/overview/app-chart.tsx @@ -6,7 +6,7 @@ import type { ChartRow } from './app-chart-utils' import ReactECharts from 'echarts-for-react' import * as React from 'react' import { useTranslation } from 'react-i18next' -import Basic from '@/app/components/app-sidebar/basic' +import { Infotip } from '@/app/components/base/infotip' import Loading from '@/app/components/base/loading' import { useAppAverageResponseTime, @@ -30,6 +30,7 @@ import { getDefaultChartData, getSummaryValue, getTokenSummary, + hasNonZeroChartData, } from './app-chart-utils' export type PeriodParams = { @@ -66,8 +67,10 @@ type IChartProps = { chartData: { data: ChartRow[] } } +const ECHARTS_RENDER_OPTIONS = { renderer: 'svg' as const } + const Chart: React.FC = ({ - basicInfo: { title, explanation, timePeriod }, + basicInfo: { title, explanation }, chartType = 'conversations', chartData, valueKey, @@ -93,37 +96,45 @@ const Chart: React.FC = ({ unit, }) const tokenSummary = getTokenSummary(statistics) + const showTokenSummary = CHART_TYPE_CONFIG[chartType].showTokens && hasNonZeroChartData(statistics, 'total_price') + const isZeroSummary = summaryValue === '0' || summaryValue === '0 ms' return ( -
+
- +
+
+ {title} +
+ {explanation && ( + + {explanation} + + )} +
-
- - {t('analysis.tokenUsage.consumed', { ns: 'appOverview' })} - {' '} - Tokens - - ( - - ~ - {tokenSummary} - - ) - - - )} - textStyle={{ main: `text-3xl! font-normal! ${summaryValue === '0' || summaryValue === '0 ms' ? 'text-text-quaternary!' : ''}` }} - /> +
+
+ {summaryValue} +
+ {showTokenSummary && ( +
+ {t('analysis.tokenUsage.consumed', { ns: 'appOverview' })} + {' '} + Tokens + {' '} + ( + + ~ + {tokenSummary} + + ) +
+ )} +
+
+
-
) } @@ -204,6 +215,8 @@ export const MessagesChart = createBizChartComponent({ titleKey: 'analysis.totalMessages.title', explanationKey: 'analysis.totalMessages.explanation', useChartData: useAppDailyMessages, + valueKey: 'message_count', + emptyValueKey: 'message_count', yMaxWhenEmpty: 500, }) @@ -212,6 +225,8 @@ export const ConversationsChart = createBizChartComponent({ titleKey: 'analysis.totalConversations.title', explanationKey: 'analysis.totalConversations.explanation', useChartData: useAppDailyConversations, + valueKey: 'conversation_count', + emptyValueKey: 'conversation_count', yMaxWhenEmpty: 500, }) @@ -220,6 +235,8 @@ export const EndUsersChart = createBizChartComponent({ titleKey: 'analysis.activeUsers.title', explanationKey: 'analysis.activeUsers.explanation', useChartData: useAppDailyEndUsers, + valueKey: 'terminal_count', + emptyValueKey: 'terminal_count', yMaxWhenEmpty: 500, }) @@ -276,6 +293,8 @@ export const CostChart = createBizChartComponent({ titleKey: 'analysis.tokenUsage.title', explanationKey: 'analysis.tokenUsage.explanation', useChartData: useAppTokenCosts, + valueKey: 'token_count', + emptyValueKey: 'token_count', yMaxWhenEmpty: 100, }) @@ -294,6 +313,8 @@ export const WorkflowDailyTerminalsChart = createBizChartComponent({ titleKey: 'analysis.activeUsers.title', explanationKey: 'analysis.activeUsers.explanation', useChartData: useWorkflowDailyTerminals, + valueKey: 'terminal_count', + emptyValueKey: 'terminal_count', yMaxWhenEmpty: 500, }) @@ -302,6 +323,8 @@ export const WorkflowCostChart = createBizChartComponent({ titleKey: 'analysis.tokenUsage.title', explanationKey: 'analysis.tokenUsage.explanation', useChartData: useWorkflowTokenCosts, + valueKey: 'token_count', + emptyValueKey: 'token_count', yMaxWhenEmpty: 100, })