fix(web): align monitoring overview charts (#38292)

This commit is contained in:
Stephen Zhou 2026-07-02 15:30:25 +08:00 committed by GitHub
parent 1af535a173
commit 451faa0e68
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 96 additions and 38 deletions

View File

@ -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<Record<string, unknown>> }
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)')
})

View File

@ -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 <div data-testid="echarts" />
},
}))
@ -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<Record<string, unknown>> }
yAxis: { max: number }
const chartProps = reactEChartsMock.mock.calls[0]![0] as {
option: {
dataset: { source: Array<Record<string, unknown>> }
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 })
})
})
})

View File

@ -69,6 +69,7 @@ export const CHART_TYPE_CONFIG: Record<ChartType, ChartConfig> = {
},
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 `<div style='color:#6B7280;font-size:12px'>${params.name}</div><div style='font-size:14px;color:#1F2A37'>${value}</div>`
return `<div style='color:#6B7280;font-size:12px'>${params.name}</div>
@ -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',

View File

@ -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<IChartProps> = ({
basicInfo: { title, explanation, timePeriod },
basicInfo: { title, explanation },
chartType = 'conversations',
chartData,
valueKey,
@ -93,37 +96,45 @@ const Chart: React.FC<IChartProps> = ({
unit,
})
const tokenSummary = getTokenSummary(statistics)
const showTokenSummary = CHART_TYPE_CONFIG[chartType].showTokens && hasNonZeroChartData(statistics, 'total_price')
const isZeroSummary = summaryValue === '0' || summaryValue === '0 ms'
return (
<div className={`flex h-[316px] w-full flex-col overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg ${className ?? ''}`}>
<div className={`flex h-[316px] w-full min-w-0 flex-col overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg xl:min-w-[480px] ${className ?? ''}`}>
<div className="flex h-11 shrink-0 items-center px-6 pt-6 pb-1">
<Basic name={title} type={timePeriod} hoverTip={explanation} />
<div className="flex min-w-0 items-center">
<div className="min-w-0 truncate system-sm-semibold-uppercase text-text-secondary">
{title}
</div>
{explanation && (
<Infotip aria-label={explanation} className="ml-1" popupClassName="w-[240px]">
{explanation}
</Infotip>
)}
</div>
</div>
<div className="flex h-8 shrink-0 items-start px-6 py-1">
<Basic
isExtraInLine={CHART_TYPE_CONFIG[chartType].showTokens}
name={summaryValue}
type={!CHART_TYPE_CONFIG[chartType].showTokens
? ''
: (
<span>
{t('analysis.tokenUsage.consumed', { ns: 'appOverview' })}
{' '}
Tokens
<span className="text-sm">
<span className="ml-1 text-text-tertiary">(</span>
<span className="text-orange-400">
~
{tokenSummary}
</span>
<span className="text-text-tertiary">)</span>
</span>
</span>
)}
textStyle={{ main: `text-3xl! font-normal! ${summaryValue === '0' || summaryValue === '0 ms' ? 'text-text-quaternary!' : ''}` }}
/>
<div className="flex h-8 shrink-0 items-baseline gap-1 px-6 py-1">
<div className={`shrink-0 title-3xl-semi-bold ${isZeroSummary ? 'text-text-quaternary' : 'text-text-primary'}`}>
{summaryValue}
</div>
{showTokenSummary && (
<div className="min-w-0 truncate system-sm-medium text-text-tertiary">
{t('analysis.tokenUsage.consumed', { ns: 'appOverview' })}
{' '}
Tokens
{' '}
<span>(</span>
<span className="text-orange-400">
~
{tokenSummary}
</span>
<span>)</span>
</div>
)}
</div>
<div className="h-[240px] shrink-0 px-6 pb-4">
<ReactECharts option={options} opts={ECHARTS_RENDER_OPTIONS} style={{ height: '100%', width: '100%' }} />
</div>
<ReactECharts option={options} style={{ height: 240, width: '100%' }} />
</div>
)
}
@ -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,
})