mirror of
https://github.com/langgenius/dify.git
synced 2026-07-21 18:58:35 +08:00
fix(web): align monitoring overview charts (#38292)
This commit is contained in:
parent
1af535a173
commit
451faa0e68
@ -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)')
|
||||
})
|
||||
|
||||
@ -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 })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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,
|
||||
})
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user