mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-05 00:47:48 +08:00
Merge pull request #509 from beilo/feat/key-ranking-usage
Add paginated API key usage leaderboard
This commit is contained in:
@@ -64,6 +64,35 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<LeaderboardTable
|
||||
title="API Key 用量排行"
|
||||
:items="apiKeyLeaderboard"
|
||||
:metric="apiKeyLeaderboardMetric"
|
||||
:loading="apiKeyLeaderboardLoading"
|
||||
:show-metric-select="false"
|
||||
@update:metric="apiKeyLeaderboardMetric = $event"
|
||||
>
|
||||
<template #actions>
|
||||
<LeaderboardControls
|
||||
:metric="apiKeyLeaderboardMetric"
|
||||
:time-range="apiKeyLeaderboardTimeRange"
|
||||
@update:metric="apiKeyLeaderboardMetric = $event"
|
||||
@update:time-range="apiKeyLeaderboardTimeRange = $event"
|
||||
/>
|
||||
</template>
|
||||
<template #pagination>
|
||||
<Pagination
|
||||
v-if="apiKeyLeaderboardTotal > 0"
|
||||
:current="apiKeyLeaderboardPage"
|
||||
:total="apiKeyLeaderboardTotal"
|
||||
:page-size="apiKeyLeaderboardPageSize"
|
||||
:page-size-options="apiKeyLeaderboardPageSizeOptions"
|
||||
@update:current="apiKeyLeaderboardPage = $event"
|
||||
@update:page-size="apiKeyLeaderboardPageSize = $event"
|
||||
/>
|
||||
</template>
|
||||
</LeaderboardTable>
|
||||
|
||||
<UsageProviderTable
|
||||
:data="providerStats"
|
||||
:is-admin="true"
|
||||
@@ -74,10 +103,11 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import Card from '@/components/ui/card.vue'
|
||||
import { Pagination } from '@/components/ui'
|
||||
import { TimeRangePicker } from '@/components/common'
|
||||
import { CostForecastChart, QuotaProgressCard } from '@/components/stats'
|
||||
import { CostForecastChart, LeaderboardControls, LeaderboardTable, QuotaProgressCard } from '@/components/stats'
|
||||
import { UsageProviderTable } from '@/features/usage/components'
|
||||
import { adminApi, type CostForecastResponse, type CostSavingsResponse, type QuotaUsageProvider } from '@/api/admin'
|
||||
import { adminApi, type CostForecastResponse, type CostSavingsResponse, type LeaderboardItem, type QuotaUsageProvider } from '@/api/admin'
|
||||
import { usageApi } from '@/api/usage'
|
||||
import { formatCurrency, formatTokens } from '@/utils/format'
|
||||
import { getDateRangeFromPeriod } from '@/features/usage/composables'
|
||||
@@ -90,16 +120,26 @@ const forecast = ref<CostForecastResponse | null>(null)
|
||||
const costSavings = ref<CostSavingsResponse | null>(null)
|
||||
const quotaProviders = ref<QuotaUsageProvider[]>([])
|
||||
const providerStats = ref<ProviderStatsItem[]>([])
|
||||
const apiKeyLeaderboard = ref<LeaderboardItem[]>([])
|
||||
const apiKeyLeaderboardMetric = ref<'requests' | 'tokens' | 'cost'>('cost')
|
||||
const apiKeyLeaderboardTimeRange = ref<DateRangeParams>(getDateRangeFromPeriod('last30days'))
|
||||
const apiKeyLeaderboardPage = ref(1)
|
||||
const apiKeyLeaderboardPageSize = ref(10)
|
||||
const apiKeyLeaderboardTotal = ref(0)
|
||||
const apiKeyLeaderboardPageSizeOptions = [10, 20, 50, 100]
|
||||
|
||||
const forecastLoading = ref(false)
|
||||
const quotaLoading = ref(false)
|
||||
const apiKeyLeaderboardLoading = ref(false)
|
||||
let forecastRequestId = 0
|
||||
let savingsRequestId = 0
|
||||
let quotaRequestId = 0
|
||||
let providerStatsRequestId = 0
|
||||
let apiKeyLeaderboardRequestId = 0
|
||||
let loadAllPromise: Promise<void> | null = null
|
||||
let hasPendingLoadAll = false
|
||||
let loadAllDebounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let apiKeyLeaderboardDebounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const forecastHistory = computed(() => forecast.value?.history || [])
|
||||
const forecastFuture = computed(() => forecast.value?.forecast || [])
|
||||
@@ -159,12 +199,55 @@ async function loadProviderStats() {
|
||||
providerStats.value = stats
|
||||
}
|
||||
|
||||
async function loadApiKeyLeaderboard() {
|
||||
const requestId = ++apiKeyLeaderboardRequestId
|
||||
apiKeyLeaderboardLoading.value = true
|
||||
try {
|
||||
const response = await adminApi.getLeaderboardApiKeys({
|
||||
...buildApiKeyLeaderboardTimeRangeParams(),
|
||||
metric: apiKeyLeaderboardMetric.value,
|
||||
order: 'desc',
|
||||
limit: apiKeyLeaderboardPageSize.value,
|
||||
offset: (apiKeyLeaderboardPage.value - 1) * apiKeyLeaderboardPageSize.value,
|
||||
include_inactive: false,
|
||||
exclude_admin: false
|
||||
})
|
||||
if (requestId !== apiKeyLeaderboardRequestId) return
|
||||
apiKeyLeaderboard.value = response.items
|
||||
apiKeyLeaderboardTotal.value = response.total
|
||||
if (response.items.length === 0 && response.total > 0 && apiKeyLeaderboardPage.value > 1) {
|
||||
apiKeyLeaderboardPage.value = 1
|
||||
scheduleApiKeyLeaderboardLoad()
|
||||
}
|
||||
} finally {
|
||||
if (requestId === apiKeyLeaderboardRequestId) {
|
||||
apiKeyLeaderboardLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildApiKeyLeaderboardTimeRangeParams() {
|
||||
return {
|
||||
start_date: apiKeyLeaderboardTimeRange.value.start_date,
|
||||
end_date: apiKeyLeaderboardTimeRange.value.end_date,
|
||||
preset: apiKeyLeaderboardTimeRange.value.preset,
|
||||
timezone: apiKeyLeaderboardTimeRange.value.timezone,
|
||||
tz_offset_minutes: apiKeyLeaderboardTimeRange.value.tz_offset_minutes
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAll() {
|
||||
if (loadAllPromise) {
|
||||
hasPendingLoadAll = true
|
||||
return loadAllPromise
|
||||
}
|
||||
loadAllPromise = Promise.all([loadForecast(), loadSavings(), loadQuotaUsage(), loadProviderStats()])
|
||||
loadAllPromise = Promise.all([
|
||||
loadForecast(),
|
||||
loadSavings(),
|
||||
loadQuotaUsage(),
|
||||
loadProviderStats(),
|
||||
loadApiKeyLeaderboard()
|
||||
])
|
||||
.then(() => undefined)
|
||||
.finally(() => {
|
||||
loadAllPromise = null
|
||||
@@ -186,7 +269,36 @@ function scheduleLoadAll() {
|
||||
}, 120)
|
||||
}
|
||||
|
||||
watch(timeRange, scheduleLoadAll, { deep: true })
|
||||
function scheduleApiKeyLeaderboardLoad() {
|
||||
if (apiKeyLeaderboardDebounceTimer) {
|
||||
clearTimeout(apiKeyLeaderboardDebounceTimer)
|
||||
}
|
||||
apiKeyLeaderboardDebounceTimer = setTimeout(() => {
|
||||
apiKeyLeaderboardDebounceTimer = null
|
||||
void loadApiKeyLeaderboard()
|
||||
}, 120)
|
||||
}
|
||||
|
||||
function resetApiKeyLeaderboardPage() {
|
||||
if (apiKeyLeaderboardPage.value === 1) {
|
||||
return
|
||||
}
|
||||
apiKeyLeaderboardPage.value = 1
|
||||
}
|
||||
|
||||
watch(timeRange, () => {
|
||||
resetApiKeyLeaderboardPage()
|
||||
scheduleLoadAll()
|
||||
}, { deep: true })
|
||||
watch(apiKeyLeaderboardMetric, () => {
|
||||
resetApiKeyLeaderboardPage()
|
||||
scheduleApiKeyLeaderboardLoad()
|
||||
})
|
||||
watch(apiKeyLeaderboardTimeRange, () => {
|
||||
resetApiKeyLeaderboardPage()
|
||||
scheduleApiKeyLeaderboardLoad()
|
||||
}, { deep: true })
|
||||
watch([apiKeyLeaderboardPage, apiKeyLeaderboardPageSize], scheduleApiKeyLeaderboardLoad)
|
||||
|
||||
onMounted(() => {
|
||||
void loadAll()
|
||||
@@ -197,11 +309,16 @@ onUnmounted(() => {
|
||||
clearTimeout(loadAllDebounceTimer)
|
||||
loadAllDebounceTimer = null
|
||||
}
|
||||
if (apiKeyLeaderboardDebounceTimer) {
|
||||
clearTimeout(apiKeyLeaderboardDebounceTimer)
|
||||
apiKeyLeaderboardDebounceTimer = null
|
||||
}
|
||||
hasPendingLoadAll = false
|
||||
loadAllPromise = null
|
||||
forecastRequestId += 1
|
||||
savingsRequestId += 1
|
||||
quotaRequestId += 1
|
||||
providerStatsRequestId += 1
|
||||
apiKeyLeaderboardRequestId += 1
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user