Merge pull request #7054 from ouchihao/codex/bug-usage-export-filter-snapshot

fix(usage): keep export filters consistent across pages
This commit is contained in:
Wesley Liddick
2026-09-12 19:29:57 +08:00
committed by GitHub
2 changed files with 67 additions and 2 deletions
+3 -2
View File
@@ -641,9 +641,10 @@ const exportToCSV = async () => {
try {
const allLogs: UsageLog[] = []
const pageSize = 100
const exportParams = buildUsageListParams(1, pageSize)
const totalPages = Math.ceil(pagination.total / pageSize)
for (let page = 1; page <= totalPages; page++) {
const response = await usageAPI.query(buildUsageListParams(page, pageSize))
const response = await usageAPI.query({ ...exportParams, page })
allLogs.push(...response.items)
}
if (allLogs.length === 0) {
@@ -696,7 +697,7 @@ const exportToCSV = async () => {
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `usage_${startDate.value}_to_${endDate.value}.csv`
link.download = `usage_${exportParams.start_date}_to_${exportParams.end_date}.csv`
link.click()
window.URL.revokeObjectURL(url)
appStore.showSuccess(t('usage.exportSuccess'))
@@ -3,6 +3,8 @@ import { flushPromises, mount } from '@vue/test-utils'
import UsageView from '../UsageView.vue'
import Select, { type SelectOption } from '@/components/common/Select.vue'
import DateRangePicker from '@/components/common/DateRangePicker.vue'
import UsageTable from '@/components/admin/usage/UsageTable.vue'
const {
query,
@@ -413,6 +415,68 @@ describe('user UsageView', () => {
clickSpy.mockRestore()
})
it('keeps the initial filters, sort, and filename while exporting multiple pages', async () => {
const pageResponse = { items: [usageLog], total: 101, pages: 2 }
query.mockResolvedValue(pageResponse)
const wrapper = mountUsageView()
await flushPromises()
const datePicker = wrapper.findComponent(DateRangePicker)
datePicker.vm.$emit('change', { startDate: '2026-03-01', endDate: '2026-03-08', preset: null })
await flushPromises()
let resolveFirstPage!: (value: typeof pageResponse) => void
const firstPage = new Promise<typeof pageResponse>((resolve) => { resolveFirstPage = resolve })
query.mockClear()
query.mockImplementation((params, options) =>
!options && params.page === 1 ? firstPage : Promise.resolve(pageResponse)
)
const originalCreateObjectURL = window.URL.createObjectURL
const originalRevokeObjectURL = window.URL.revokeObjectURL
window.URL.createObjectURL = vi.fn(() => 'blob:usage-export')
window.URL.revokeObjectURL = vi.fn()
let filename = ''
const clickSpy = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(function () {
filename = this.download
})
try {
await wrapper.findAll('button').find((button) => button.text() === 'Export CSV')!.trigger('click')
const initialParams = { ...query.mock.calls[0][0] }
expect(initialParams).toMatchObject({
page: 1, page_size: 100, start_date: '2026-03-01', end_date: '2026-03-08',
sort_by: 'created_at', sort_order: 'desc',
})
const keySelect = wrapper.findAllComponents(Select).find((select) =>
select.props('options').some((option: SelectOption) => option.label === 'All API Keys')
)!
keySelect.vm.$emit('update:modelValue', 1)
keySelect.vm.$emit('change', 1)
datePicker.vm.$emit('change', { startDate: '2026-04-01', endDate: '2026-04-08', preset: null })
wrapper.findComponent(UsageTable).vm.$emit('sort', 'actual_cost', 'asc')
await flushPromises()
expect(query).toHaveBeenCalledWith(expect.objectContaining({
api_key_id: 1, start_date: '2026-04-01', end_date: '2026-04-08',
sort_by: 'actual_cost', sort_order: 'asc',
}), expect.anything())
resolveFirstPage(pageResponse)
await flushPromises()
const exportCalls = query.mock.calls.filter((call) => call.length === 1)
expect.soft(exportCalls).toEqual([[initialParams], [{ ...initialParams, page: 2 }]])
expect.soft(filename).toBe('usage_2026-03-01_to_2026-03-08.csv')
expect(showSuccess).toHaveBeenCalledWith('Export success')
expect(showError).not.toHaveBeenCalled()
} finally {
window.URL.createObjectURL = originalCreateObjectURL
window.URL.revokeObjectURL = originalRevokeObjectURL
clickSpy.mockRestore()
wrapper.unmount()
}
})
it('exports historical image rows with image billing mode derived from image_count', async () => {
query.mockResolvedValue({
items: [