Merge pull request #1068 from Ethan0x0000/pr/frontend-last24h

feat(frontend): set last 24h as default range in Usage and Dashboard
This commit is contained in:
Wesley Liddick
2026-03-17 09:06:52 +08:00
committed by GitHub
9 changed files with 344 additions and 39 deletions

View File

@@ -348,12 +348,20 @@ const formatLocalDate = (date: Date): string => {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
}
const getTodayLocalDate = () => formatLocalDate(new Date())
const getLast24HoursRangeDates = (): { start: string; end: string } => {
const end = new Date()
const start = new Date(end.getTime() - 24 * 60 * 60 * 1000)
return {
start: formatLocalDate(start),
end: formatLocalDate(end)
}
}
// Date range
const granularity = ref<'day' | 'hour'>('hour')
const startDate = ref(getTodayLocalDate())
const endDate = ref(getTodayLocalDate())
const defaultRange = getLast24HoursRangeDates()
const startDate = ref(defaultRange.start)
const endDate = ref(defaultRange.end)
// Granularity options for Select component
const granularityOptions = computed(() => [

View File

@@ -5,10 +5,20 @@
<!-- Charts Section -->
<div class="space-y-4">
<div class="card p-4">
<div class="flex items-center gap-4">
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('admin.dashboard.granularity') }}:</span>
<div class="w-28">
<Select v-model="granularity" :options="granularityOptions" @change="loadChartData" />
<div class="flex flex-wrap items-center gap-4">
<div class="flex items-center gap-2">
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('admin.dashboard.timeRange') }}:</span>
<DateRangePicker
v-model:start-date="startDate"
v-model:end-date="endDate"
@change="onDateRangeChange"
/>
</div>
<div class="ml-auto flex items-center gap-2">
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('admin.dashboard.granularity') }}:</span>
<div class="w-28">
<Select v-model="granularity" :options="granularityOptions" @change="loadChartData" />
</div>
</div>
</div>
</div>
@@ -41,7 +51,7 @@
<TokenUsageTrend :trend-data="trendData" :loading="chartsLoading" />
</div>
</div>
<UsageFilters v-model="filters" v-model:startDate="startDate" v-model:endDate="endDate" :exporting="exporting" @change="applyFilters" @refresh="refreshData" @reset="resetFilters" @cleanup="openCleanupDialog" @export="exportToExcel">
<UsageFilters v-model="filters" :start-date="startDate" :end-date="endDate" :exporting="exporting" @change="applyFilters" @refresh="refreshData" @reset="resetFilters" @cleanup="openCleanupDialog" @export="exportToExcel">
<template #after-reset>
<div class="relative" ref="columnDropdownRef">
<button
@@ -106,7 +116,7 @@ import { useRoute } from 'vue-router'
import { useAppStore } from '@/stores/app'; import { adminAPI } from '@/api/admin'; import { adminUsageAPI } from '@/api/admin/usage'
import { formatReasoningEffort } from '@/utils/format'
import { resolveUsageRequestType, requestTypeToLegacyStream } from '@/utils/usageRequestType'
import AppLayout from '@/components/layout/AppLayout.vue'; import Pagination from '@/components/common/Pagination.vue'; import Select from '@/components/common/Select.vue'
import AppLayout from '@/components/layout/AppLayout.vue'; import Pagination from '@/components/common/Pagination.vue'; import Select from '@/components/common/Select.vue'; import DateRangePicker from '@/components/common/DateRangePicker.vue'
import UsageStatsCards from '@/components/admin/usage/UsageStatsCards.vue'; import UsageFilters from '@/components/admin/usage/UsageFilters.vue'
import UsageTable from '@/components/admin/usage/UsageTable.vue'; import UsageExportProgress from '@/components/admin/usage/UsageExportProgress.vue'
import UsageCleanupDialog from '@/components/admin/usage/UsageCleanupDialog.vue'
@@ -158,9 +168,22 @@ const formatLD = (d: Date) => {
const day = String(d.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
const getTodayLocalDate = () => formatLD(new Date())
const getGranularityForRange = (start: string, end: string): 'day' | 'hour' => start === end ? 'hour' : 'day'
const startDate = ref(getTodayLocalDate()); const endDate = ref(getTodayLocalDate())
const getLast24HoursRangeDates = (): { start: string; end: string } => {
const end = new Date()
const start = new Date(end.getTime() - 24 * 60 * 60 * 1000)
return {
start: formatLD(start),
end: formatLD(end)
}
}
const getGranularityForRange = (start: string, end: string): 'day' | 'hour' => {
const startTime = new Date(`${start}T00:00:00`).getTime()
const endTime = new Date(`${end}T00:00:00`).getTime()
const daysDiff = Math.ceil((endTime - startTime) / (1000 * 60 * 60 * 24))
return daysDiff <= 1 ? 'hour' : 'day'
}
const defaultRange = getLast24HoursRangeDates()
const startDate = ref(defaultRange.start); const endDate = ref(defaultRange.end)
const filters = ref<AdminUsageQueryParams>({ user_id: undefined, model: undefined, group_id: undefined, request_type: undefined, billing_type: null, start_date: startDate.value, end_date: endDate.value })
const pagination = reactive({ page: 1, page_size: 20, total: 0 })
@@ -197,6 +220,18 @@ const applyRouteQueryFilters = () => {
granularity.value = getGranularityForRange(startDate.value, endDate.value)
}
const onDateRangeChange = (range: { startDate: string; endDate: string; preset: string | null }) => {
startDate.value = range.startDate
endDate.value = range.endDate
filters.value = {
...filters.value,
start_date: range.startDate,
end_date: range.endDate
}
granularity.value = getGranularityForRange(range.startDate, range.endDate)
applyFilters()
}
const loadLogs = async () => {
abortController?.abort(); const c = new AbortController(); abortController = c; loading.value = true
try {
@@ -260,7 +295,14 @@ const loadChartData = async () => {
}
const applyFilters = () => { pagination.page = 1; loadLogs(); loadStats(); loadChartData() }
const refreshData = () => { loadLogs(); loadStats(); loadChartData() }
const resetFilters = () => { startDate.value = getTodayLocalDate(); endDate.value = getTodayLocalDate(); filters.value = { start_date: startDate.value, end_date: endDate.value, request_type: undefined, billing_type: null }; granularity.value = getGranularityForRange(startDate.value, endDate.value); applyFilters() }
const resetFilters = () => {
const range = getLast24HoursRangeDates()
startDate.value = range.start
endDate.value = range.end
filters.value = { start_date: startDate.value, end_date: endDate.value, request_type: undefined, billing_type: null }
granularity.value = getGranularityForRange(startDate.value, endDate.value)
applyFilters()
}
const handlePageChange = (p: number) => { pagination.page = p; loadLogs() }
const handlePageSizeChange = (s: number) => { pagination.page_size = s; pagination.page = 1; loadLogs() }
const cancelExport = () => exportAbortController?.abort()

View File

@@ -0,0 +1,143 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { flushPromises, mount } from '@vue/test-utils'
import type { DashboardStats } from '@/types'
import DashboardView from '../DashboardView.vue'
const { getSnapshotV2, getUserUsageTrend, getUserSpendingRanking } = vi.hoisted(() => ({
getSnapshotV2: vi.fn(),
getUserUsageTrend: vi.fn(),
getUserSpendingRanking: vi.fn()
}))
vi.mock('@/api/admin', () => ({
adminAPI: {
dashboard: {
getSnapshotV2,
getUserUsageTrend,
getUserSpendingRanking
}
}
}))
vi.mock('@/stores/app', () => ({
useAppStore: () => ({
showError: vi.fn()
})
}))
vi.mock('vue-router', () => ({
useRouter: () => ({
push: vi.fn()
})
}))
vi.mock('vue-i18n', async () => {
const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n')
return {
...actual,
useI18n: () => ({
t: (key: string) => key
})
}
})
const formatLocalDate = (date: Date): string => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
const createDashboardStats = (): DashboardStats => ({
total_users: 0,
today_new_users: 0,
active_users: 0,
hourly_active_users: 0,
stats_updated_at: '',
stats_stale: false,
total_api_keys: 0,
active_api_keys: 0,
total_accounts: 0,
normal_accounts: 0,
error_accounts: 0,
ratelimit_accounts: 0,
overload_accounts: 0,
total_requests: 0,
total_input_tokens: 0,
total_output_tokens: 0,
total_cache_creation_tokens: 0,
total_cache_read_tokens: 0,
total_tokens: 0,
total_cost: 0,
total_actual_cost: 0,
today_requests: 0,
today_input_tokens: 0,
today_output_tokens: 0,
today_cache_creation_tokens: 0,
today_cache_read_tokens: 0,
today_tokens: 0,
today_cost: 0,
today_actual_cost: 0,
average_duration_ms: 0,
uptime: 0,
rpm: 0,
tpm: 0
})
describe('admin DashboardView', () => {
beforeEach(() => {
getSnapshotV2.mockReset()
getUserUsageTrend.mockReset()
getUserSpendingRanking.mockReset()
getSnapshotV2.mockResolvedValue({
stats: createDashboardStats(),
trend: [],
models: []
})
getUserUsageTrend.mockResolvedValue({
trend: [],
start_date: '',
end_date: '',
granularity: 'hour'
})
getUserSpendingRanking.mockResolvedValue({
ranking: [],
total_actual_cost: 0,
total_requests: 0,
total_tokens: 0,
start_date: '',
end_date: ''
})
})
it('uses last 24 hours as default dashboard range', async () => {
mount(DashboardView, {
global: {
stubs: {
AppLayout: { template: '<div><slot /></div>' },
LoadingSpinner: true,
Icon: true,
DateRangePicker: true,
Select: true,
ModelDistributionChart: true,
TokenUsageTrend: true,
Line: true
}
}
})
await flushPromises()
const now = new Date()
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000)
expect(getSnapshotV2).toHaveBeenCalledTimes(1)
expect(getSnapshotV2).toHaveBeenCalledWith(expect.objectContaining({
start_date: formatLocalDate(yesterday),
end_date: formatLocalDate(now),
granularity: 'hour'
}))
})
})

View File

@@ -19,11 +19,19 @@ const { list, getStats, getSnapshotV2, getById } = vi.hoisted(() => {
})
const messages: Record<string, string> = {
'admin.dashboard.timeRange': 'Time Range',
'admin.dashboard.day': 'Day',
'admin.dashboard.hour': 'Hour',
'admin.usage.failedToLoadUser': 'Failed to load user',
}
const formatLocalDate = (date: Date): string => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
vi.mock('@/api/admin', () => ({
adminAPI: {
usage: {
@@ -68,6 +76,12 @@ vi.mock('vue-i18n', async () => {
}
})
vi.mock('vue-router', () => ({
useRoute: () => ({
query: {}
})
}))
const AppLayoutStub = { template: '<div><slot /></div>' }
const UsageFiltersStub = { template: '<div><slot name="after-reset" /></div>' }
const ModelDistributionChartStub = {
@@ -138,6 +152,7 @@ describe('admin UsageView distribution metric toggles', () => {
UserBalanceHistoryModal: true,
Pagination: true,
Select: true,
DateRangePicker: true,
Icon: true,
TokenUsageTrend: true,
ModelDistributionChart: ModelDistributionChartStub,
@@ -150,6 +165,13 @@ describe('admin UsageView distribution metric toggles', () => {
await flushPromises()
expect(getSnapshotV2).toHaveBeenCalledTimes(1)
const now = new Date()
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000)
expect(getSnapshotV2).toHaveBeenCalledWith(expect.objectContaining({
start_date: formatLocalDate(yesterday),
end_date: formatLocalDate(now),
granularity: 'hour'
}))
const modelChart = wrapper.find('[data-test="model-chart"]')
const groupChart = wrapper.find('[data-test="group-chart"]')