fix(grok): display remaining quota capacity

This commit is contained in:
Heatherm Huang
2026-07-13 10:11:33 +08:00
parent 3375b4ed2b
commit cbddb57dec
4 changed files with 121 additions and 6 deletions
@@ -386,6 +386,7 @@
:label="t('admin.accounts.usageWindow.grokRequests')"
:utilization="grokRequestQuotaBar.utilization"
:resets-at="grokRequestQuotaBar.resetsAt"
:remaining-capacity="true"
color="indigo"
/>
<UsageProgressBar
@@ -393,6 +394,7 @@
:label="t('admin.accounts.usageWindow.grokTokens')"
:utilization="grokTokenQuotaBar.utilization"
:resets-at="grokTokenQuotaBar.resetsAt"
:remaining-capacity="true"
color="emerald"
/>
<div v-if="grokRetryAfterLabel" class="text-[10px] text-amber-600 dark:text-amber-400">
@@ -1036,9 +1038,9 @@ interface GrokQuotaBarInfo {
const makeGrokQuotaBar = (quota?: { limit?: number | null; remaining?: number | null; reset_at?: string | null } | null): GrokQuotaBarInfo | null => {
if (!quota || quota.limit == null || quota.remaining == null || quota.limit <= 0) return null
const used = Math.max(0, quota.limit - quota.remaining)
const remaining = Math.min(quota.limit, Math.max(0, quota.remaining))
return {
utilization: (used / quota.limit) * 100,
utilization: (remaining / quota.limit) * 100,
resetsAt: quota.reset_at || null
}
}
@@ -69,6 +69,7 @@ const props = defineProps<{
color: 'indigo' | 'emerald' | 'purple' | 'amber'
windowStats?: WindowStats | null
showNowWhenIdle?: boolean
remainingCapacity?: boolean
}>()
const { t } = useI18n()
@@ -109,6 +110,14 @@ const labelClass = computed(() => {
// Progress bar color based on utilization
const barClass = computed(() => {
if (props.remainingCapacity) {
if (props.utilization <= 20) {
return 'bg-red-500'
} else if (props.utilization <= 50) {
return 'bg-amber-500'
}
return 'bg-green-500'
}
if (props.utilization >= 100) {
return 'bg-red-500'
} else if (props.utilization >= 80) {
@@ -120,6 +129,14 @@ const barClass = computed(() => {
// Text color based on utilization
const textClass = computed(() => {
if (props.remainingCapacity) {
if (props.utilization <= 20) {
return 'text-red-600 dark:text-red-400'
} else if (props.utilization <= 50) {
return 'text-amber-600 dark:text-amber-400'
}
return 'text-gray-600 dark:text-gray-400'
}
if (props.utilization >= 100) {
return 'text-red-600 dark:text-red-400'
} else if (props.utilization >= 80) {
@@ -131,12 +148,16 @@ const textClass = computed(() => {
// Bar width (capped at 100%)
const barWidth = computed(() => {
return `${Math.min(props.utilization, 100)}%`
return `${Math.min(Math.max(props.utilization, 0), 100)}%`
})
// Display percentage (cap at 999% for readability)
const displayPercent = computed(() => {
const percent = Math.round(props.utilization)
const percent = Math.round(
props.remainingCapacity
? Math.min(Math.max(props.utilization, 0), 100)
: props.utilization
)
return percent > 999 ? '>999%' : `${percent}%`
})
@@ -566,7 +566,7 @@ describe('AccountUsageCell', () => {
expect(badges.some(node => node.attributes('title') === 'usage.userBilled')).toBe(true)
})
it('Grok OAuth 会展示本地 user billed 用量并保留超限百分比', async () => {
it('Grok OAuth 会展示本地 user billed 用量并把耗尽配额显示为 0% 剩余', async () => {
getUsage.mockResolvedValue({
grok_local_usage: {
requests: 4,
@@ -611,13 +611,55 @@ describe('AccountUsageCell', () => {
expect(wrapper.text()).toContain('1.2K')
expect(wrapper.text()).toContain('A $0.12')
expect(wrapper.text()).toContain('U $0.34')
expect(wrapper.text()).toContain('admin.accounts.usageWindow.grokRequests|120|2026-07-09T16:00:00Z')
expect(wrapper.text()).toContain('admin.accounts.usageWindow.grokRequests|0|2026-07-09T16:00:00Z')
const badges = wrapper.findAll('span[title]')
expect(badges.some(node => node.attributes('title') === 'usage.accountBilled')).toBe(true)
expect(badges.some(node => node.attributes('title') === 'usage.userBilled')).toBe(true)
})
it('Grok OAuth 配额条按剩余容量显示 100% 满格和 25% 低量', async () => {
getUsage.mockResolvedValue({
grok_request_quota: {
limit: 100,
remaining: 100,
reset_at: '2026-07-09T16:00:00Z'
},
grok_token_quota: {
limit: 1000,
remaining: 250,
reset_at: '2026-07-09T16:00:00Z'
},
grok_quota_snapshot_state: 'observed'
})
const wrapper = mount(AccountUsageCell, {
props: {
account: makeAccount({
id: 4073,
platform: 'grok',
type: 'oauth',
extra: {}
})
},
global: {
stubs: {
UsageProgressBar: {
props: ['label', 'utilization', 'resetsAt', 'color', 'remainingCapacity'],
template: '<div class="usage-bar">{{ label }}|{{ utilization }}|{{ remainingCapacity }}</div>'
},
AccountQuotaInfo: true,
GrokQuotaProbeCell: true
}
}
})
await flushPromises()
expect(wrapper.text()).toContain('admin.accounts.usageWindow.grokRequests|100|true')
expect(wrapper.text()).toContain('admin.accounts.usageWindow.grokTokens|25|true')
})
it('Key 账号在 today stats loading 时显示骨架屏', async () => {
const wrapper = mount(AccountUsageCell, {
props: {
@@ -96,4 +96,54 @@ describe('UsageProgressBar', () => {
expect(wrapper.text()).toContain('usage.resetNow')
expect(wrapper.text()).not.toContain('usage.resetPending')
})
it('剩余容量模式在 100% 时显示满格绿色', () => {
const wrapper = mount(UsageProgressBar, {
props: {
label: 'Req',
utilization: 100,
remainingCapacity: true,
color: 'indigo'
}
})
expect(wrapper.text()).toContain('100%')
expect(wrapper.get('.h-1\\.5 > div').attributes('style')).toContain('width: 100%')
expect(wrapper.get('.h-1\\.5 > div').classes()).toContain('bg-green-500')
})
it('剩余容量模式在低量和耗尽时缩短并变红', async () => {
const wrapper = mount(UsageProgressBar, {
props: {
label: 'Req',
utilization: 15,
remainingCapacity: true,
color: 'indigo'
}
})
expect(wrapper.text()).toContain('15%')
expect(wrapper.get('.h-1\\.5 > div').attributes('style')).toContain('width: 15%')
expect(wrapper.get('.h-1\\.5 > div').classes()).toContain('bg-red-500')
await wrapper.setProps({ utilization: 0 })
expect(wrapper.text()).toContain('0%')
expect(wrapper.get('.h-1\\.5 > div').attributes('style')).toContain('width: 0%')
expect(wrapper.get('.h-1\\.5 > div').classes()).toContain('bg-red-500')
})
it('默认利用率模式仍把超限显示为满格红色', () => {
const wrapper = mount(UsageProgressBar, {
props: {
label: '5h',
utilization: 120,
color: 'indigo'
}
})
expect(wrapper.text()).toContain('120%')
expect(wrapper.get('.h-1\\.5 > div').attributes('style')).toContain('width: 100%')
expect(wrapper.get('.h-1\\.5 > div').classes()).toContain('bg-red-500')
})
})