Merge remote-tracking branch 'upstream/main' into feat/payment-system-v2
# Conflicts: # frontend/src/api/admin/settings.ts # frontend/src/stores/app.ts # frontend/src/types/index.ts # frontend/src/views/admin/SettingsView.vue
This commit is contained in:
@@ -49,7 +49,15 @@
|
||||
</template>
|
||||
|
||||
<template #table>
|
||||
<DataTable :columns="columns" :data="apiKeys" :loading="loading">
|
||||
<DataTable
|
||||
:columns="columns"
|
||||
:data="apiKeys"
|
||||
:loading="loading"
|
||||
:server-side-sort="true"
|
||||
default-sort-key="created_at"
|
||||
default-sort-order="desc"
|
||||
@sort="handleSort"
|
||||
>
|
||||
<template #cell-key="{ value, row }">
|
||||
<div class="flex items-center gap-2">
|
||||
<code class="code text-xs">
|
||||
@@ -1114,6 +1122,10 @@ const pagination = ref({
|
||||
total: 0,
|
||||
pages: 0
|
||||
})
|
||||
const sortState = ref({
|
||||
sort_by: 'created_at',
|
||||
sort_order: 'desc' as 'asc' | 'desc'
|
||||
})
|
||||
|
||||
// Filter state
|
||||
const filterSearch = ref('')
|
||||
@@ -1277,10 +1289,18 @@ const loadApiKeys = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// Build filters
|
||||
const filters: { search?: string; status?: string; group_id?: number | string } = {}
|
||||
const filters: {
|
||||
search?: string
|
||||
status?: string
|
||||
group_id?: number | string
|
||||
sort_by?: string
|
||||
sort_order?: 'asc' | 'desc'
|
||||
} = {}
|
||||
if (filterSearch.value) filters.search = filterSearch.value
|
||||
if (filterStatus.value) filters.status = filterStatus.value
|
||||
if (filterGroupId.value !== '') filters.group_id = filterGroupId.value
|
||||
filters.sort_by = sortState.value.sort_by
|
||||
filters.sort_order = sortState.value.sort_order
|
||||
|
||||
const response = await keysAPI.list(pagination.value.page, pagination.value.page_size, filters, {
|
||||
signal
|
||||
@@ -1360,6 +1380,13 @@ const handlePageSizeChange = (pageSize: number) => {
|
||||
loadApiKeys()
|
||||
}
|
||||
|
||||
const handleSort = (key: string, order: 'asc' | 'desc') => {
|
||||
sortState.value.sort_by = key
|
||||
sortState.value.sort_order = order
|
||||
pagination.value.page = 1
|
||||
loadApiKeys()
|
||||
}
|
||||
|
||||
const editKey = (key: ApiKey) => {
|
||||
selectedKey.value = key
|
||||
const hasIPRestriction = (key.ip_whitelist?.length > 0) || (key.ip_blacklist?.length > 0)
|
||||
|
||||
@@ -149,7 +149,15 @@
|
||||
</template>
|
||||
|
||||
<template #table>
|
||||
<DataTable :columns="columns" :data="usageLogs" :loading="loading">
|
||||
<DataTable
|
||||
:columns="columns"
|
||||
:data="usageLogs"
|
||||
:loading="loading"
|
||||
:server-side-sort="true"
|
||||
default-sort-key="created_at"
|
||||
default-sort-order="desc"
|
||||
@sort="handleSort"
|
||||
>
|
||||
<template #cell-api_key="{ row }">
|
||||
<span class="text-sm text-gray-900 dark:text-white">{{
|
||||
row.api_key?.name || '-'
|
||||
@@ -598,6 +606,10 @@ const pagination = reactive({
|
||||
total: 0,
|
||||
pages: 0
|
||||
})
|
||||
const sortState = reactive({
|
||||
sort_by: 'created_at',
|
||||
sort_order: 'desc' as 'asc' | 'desc'
|
||||
})
|
||||
|
||||
const formatDuration = (ms: number): string => {
|
||||
if (ms < 1000) return `${ms.toFixed(0)}ms`
|
||||
@@ -660,6 +672,18 @@ const formatTokens = (value: number): string => {
|
||||
return value.toLocaleString()
|
||||
}
|
||||
|
||||
type UsageTableQueryParams = UsageQueryParams & {
|
||||
sort_by?: string
|
||||
sort_order?: 'asc' | 'desc'
|
||||
}
|
||||
|
||||
const buildUsageQueryParams = (page: number, pageSize: number): UsageTableQueryParams => ({
|
||||
page,
|
||||
page_size: pageSize,
|
||||
...filters.value,
|
||||
sort_by: sortState.sort_by,
|
||||
sort_order: sortState.sort_order
|
||||
})
|
||||
|
||||
const loadUsageLogs = async () => {
|
||||
if (abortController) {
|
||||
@@ -670,13 +694,10 @@ const loadUsageLogs = async () => {
|
||||
const { signal } = currentAbortController
|
||||
loading.value = true
|
||||
try {
|
||||
const params: UsageQueryParams = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.page_size,
|
||||
...filters.value
|
||||
}
|
||||
|
||||
const response = await usageAPI.query(params, { signal })
|
||||
const response = await usageAPI.query(
|
||||
buildUsageQueryParams(pagination.page, pagination.page_size),
|
||||
{ signal }
|
||||
)
|
||||
if (signal.aborted) {
|
||||
return
|
||||
}
|
||||
@@ -758,6 +779,13 @@ const handlePageSizeChange = (pageSize: number) => {
|
||||
loadUsageLogs()
|
||||
}
|
||||
|
||||
const handleSort = (key: string, order: 'asc' | 'desc') => {
|
||||
sortState.sort_by = key
|
||||
sortState.sort_order = order
|
||||
pagination.page = 1
|
||||
loadUsageLogs()
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape CSV value to prevent injection and handle special characters
|
||||
*/
|
||||
@@ -795,12 +823,7 @@ const exportToCSV = async () => {
|
||||
const totalRequests = Math.ceil(pagination.total / pageSize)
|
||||
|
||||
for (let page = 1; page <= totalRequests; page++) {
|
||||
const params: UsageQueryParams = {
|
||||
page: page,
|
||||
page_size: pageSize,
|
||||
...filters.value
|
||||
}
|
||||
const response = await usageAPI.query(params)
|
||||
const response = await usageAPI.query(buildUsageQueryParams(page, pageSize))
|
||||
allLogs.push(...response.items)
|
||||
}
|
||||
|
||||
|
||||
@@ -256,6 +256,17 @@ describe('user UsageView tooltip', () => {
|
||||
await setupState.exportToCSV()
|
||||
|
||||
expect(exportedBlob).not.toBeNull()
|
||||
const hasSortedExportQuery = query.mock.calls.some((call) => {
|
||||
const params = call[0] as Record<string, unknown> | undefined
|
||||
const config = call[1]
|
||||
return (
|
||||
params?.page_size === 100 &&
|
||||
params?.sort_by === 'created_at' &&
|
||||
params?.sort_order === 'desc' &&
|
||||
config === undefined
|
||||
)
|
||||
})
|
||||
expect(hasSortedExportQuery).toBe(true)
|
||||
expect(clickSpy).toHaveBeenCalled()
|
||||
expect(showSuccess).toHaveBeenCalled()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user