Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,8 @@ describe('AuthFileCredentialsSection quota usage mode rendering', () => {
const currentHtml = renderToStaticMarkup(createElement(AuthFileQuotaPanel, { row, quotaUsageMode: 'current' }))
const estimatedHtml = renderToStaticMarkup(createElement(AuthFileQuotaPanel, { row, quotaUsageMode: 'estimated' }))

expect(currentHtml).toContain('<button')
expect(currentHtml).toContain('usage_stats.credentials_quota_details_open')
expect(currentHtml).toContain('1.00M')
expect(currentHtml).toContain('$2.50')
expect(currentHtml).not.toContain('4.00M')
Expand Down Expand Up @@ -288,6 +290,14 @@ describe('AuthFileCredentialsSection quota usage mode rendering', () => {
expect(html.indexOf('<img')).toBeLessThan(html.indexOf('$1.67'))
expect(html).not.toContain('1.00M')
})

it('keeps quota estimate details in the click modal instead of expanding each row', () => {
expect(authFileSectionSource).toContain('QuotaDetailsModal')
expect(authFileSectionSource).toContain('credentials_quota_details_current_tokens')
expect(authFileSectionSource).toContain('credentials_quota_details_estimated_tokens')
expect(authFileSectionSource).toContain('credentials_quota_details_current_cost')
expect(authFileSectionSource).toContain('credentials_quota_details_estimated_cost')
})
})

describe('AuthFileCredentialsSection quota error display', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,12 @@ function isAuthFileDisplayMode(value: string | null | undefined): value is AuthF

export function AuthFileQuotaPanel({ row, quotaUsageMode }: { row: AuthFileCredentialRow; quotaUsageMode: QuotaUsageMode }) {
const { t } = useTranslation()
const [selectedQuota, setSelectedQuota] = useState<DisplayQuota | null>(null)
const [quotaDetailsOpen, setQuotaDetailsOpen] = useState(false)
const openQuotaDetails = (quota: DisplayQuota) => {
setSelectedQuota(quota)
setQuotaDetailsOpen(true)
}

// 限额区域按加载、错误、刷新中、无缓存、可展示数据的顺序降级。
if (row.quotaLoading) {
Expand Down Expand Up @@ -872,8 +878,9 @@ export function AuthFileQuotaPanel({ row, quotaUsageMode }: { row: AuthFileCrede
<div className={styles.credentialQuotaPanel}>
<div className={styles.credentialQuotaBars}>
{/* 每个可计算进度的 quota 都独占一个稳定块;不可进度化 quota 在 view model 中已过滤。 */}
{row.displayQuotas.map((quota) => <QuotaBar key={quota.key} quota={quota} quotaUsageMode={quotaUsageMode} />)}
{row.displayQuotas.map((quota) => <QuotaBar key={quota.key} quota={quota} quotaUsageMode={quotaUsageMode} onSelect={openQuotaDetails} />)}
</div>
<QuotaDetailsModal quota={selectedQuota} open={quotaDetailsOpen} onClose={() => setQuotaDetailsOpen(false)} />
</div>
Comment on lines 878 to 884

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Currently, selectedQuota stores a static snapshot of the quota object at the moment the user clicks the bar. If the row's quota is refreshed (either via background auto-refresh or manual update) while the modal is open, the modal will continue to display stale data.

We can resolve this by dynamically finding the matching quota from row.displayQuotas using the selected quota's key. This ensures the modal always displays live, up-to-date information.

  const activeQuota = selectedQuota ? (row.displayQuotas.find((q) => q.key === selectedQuota.key) ?? selectedQuota) : null

  return (
    <div className={styles.credentialQuotaPanel}>
      <div className={styles.credentialQuotaBars}>
        {/* 每个可计算进度的 quota 都独占一个稳定块;不可进度化 quota 在 view model 中已过滤。 */}
        {row.displayQuotas.map((quota) => <QuotaBar key={quota.key} quota={quota} quotaUsageMode={quotaUsageMode} onSelect={openQuotaDetails} />)}
      </div>
      <QuotaDetailsModal quota={activeQuota} open={quotaDetailsOpen} onClose={() => setQuotaDetailsOpen(false)} />
    </div>

)
}
Expand Down Expand Up @@ -1088,7 +1095,7 @@ export function formatQuotaBillingUsageAriaLabel(t: Translate, billingUsage: Non
})
}

function QuotaBar({ quota, quotaUsageMode }: { quota: DisplayQuota; quotaUsageMode: QuotaUsageMode }) {
function QuotaBar({ quota, quotaUsageMode, onSelect }: { quota: DisplayQuota; quotaUsageMode: QuotaUsageMode; onSelect: (quota: DisplayQuota) => void }) {
const { t } = useTranslation()
// 条宽使用剩余额度百分比,颜色跟随剩余风险状态从绿到黄到红。
const percent = quota.barPercent ?? 0
Expand All @@ -1100,7 +1107,7 @@ function QuotaBar({ quota, quotaUsageMode }: { quota: DisplayQuota; quotaUsageMo
const windowUsage = billingUsage ? undefined : quotaWindowUsageForMode(quota, quotaUsageMode)

return (
<div className={styles.credentialQuotaBarBlock}>
<button type="button" className={styles.credentialQuotaBarBlock} onClick={() => onSelect(quota)} aria-label={t('usage_stats.credentials_quota_details_open', { label: quota.label })}>
<div className={styles.credentialQuotaBarHeader}>
<span className={styles.credentialQuotaLabelGroup}>
<span>{quota.label}</span>
Expand Down Expand Up @@ -1138,7 +1145,7 @@ function QuotaBar({ quota, quotaUsageMode }: { quota: DisplayQuota; quotaUsageMo
)}
{resetLabel && <span>{resetLabel}</span>}
</div>
</div>
</button>
)
}

Expand All @@ -1155,3 +1162,55 @@ function quotaWindowUsageForMode(quota: DisplayQuota, mode: QuotaUsageMode): Dis
}
return quota.windowUsage
}

function QuotaDetailsModal({ quota, open, onClose }: { quota: DisplayQuota | null; open: boolean; onClose: () => void }) {
const { t } = useTranslation()
const currentUsage = quota?.windowUsage
const estimatedUsage = quota?.windowUsageEstimate
const billingUsage = quota?.billingUsage
const percentLabel = quota?.barPercent === null || quota?.barPercent === undefined ? '' : `${Math.round(quota.barPercent)}%`
const resetLabel = quota?.resetText ? formatQuotaResetLabel(quota.resetText) : ''

return (
<Modal open={open} title={t('usage_stats.credentials_quota_details_title', { label: quota?.label ?? '' })} onClose={onClose} width={520} className={styles.credentialQuotaDetailsModal}>
Comment on lines +1166 to +1175

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since AuthFileQuotaPanel is rendered for every single row in the credentials table, each row mounts its own QuotaDetailsModal instance. When the page first loads, selectedQuota is null for all rows, meaning we render multiple hidden modals with empty/fallback values.

We can optimize this by returning null early in QuotaDetailsModal if quota is null. This prevents rendering any modal DOM elements on initial mount, significantly reducing the DOM size and improving render performance when there are many rows.

Additionally, because selectedQuota is kept as-is when the modal closes (only open becomes false), the modal can still safely perform its close transition before being cleaned up or replaced on the next open.

function QuotaDetailsModal({ quota, open, onClose }: { quota: DisplayQuota | null; open: boolean; onClose: () => void }) {
  if (!quota) {
    return null
  }
  const { t } = useTranslation()
  const currentUsage = quota.windowUsage
  const estimatedUsage = quota.windowUsageEstimate
  const billingUsage = quota.billingUsage
  const percentLabel = quota.barPercent === null || quota.barPercent === undefined ? '' : `${Math.round(quota.barPercent)}%`
  const resetLabel = quota.resetText ? formatQuotaResetLabel(quota.resetText) : ''

  return (
    <Modal open={open} title={t('usage_stats.credentials_quota_details_title', { label: quota.label })} onClose={onClose} width={520} className={styles.credentialQuotaDetailsModal}>

<div className={styles.credentialQuotaDetailsPanel}>
{billingUsage ? (
<div className={styles.credentialQuotaDetailsGrid}>
<QuotaDetailMetric label={t('usage_stats.credentials_quota_details_billing_used')} value={billingUsage.used ?? t('usage_stats.credentials_quota_details_no_value')} />
<QuotaDetailMetric label={t('usage_stats.credentials_quota_details_billing_limit')} value={billingUsage.limit ?? t('usage_stats.credentials_quota_details_no_value')} />
<QuotaDetailMetric label={t('usage_stats.credentials_quota_details_billing_remaining')} value={billingUsage.remaining ?? t('usage_stats.credentials_quota_details_no_value')} />
</div>
) : (
<div className={styles.credentialQuotaDetailsGrid}>
<QuotaDetailMetric label={t('usage_stats.credentials_quota_details_current_tokens')} value={currentUsage?.tokens ?? t('usage_stats.credentials_quota_details_no_value')} />
<QuotaDetailMetric label={t('usage_stats.credentials_quota_details_estimated_tokens')} value={estimatedUsage?.tokens ?? t('usage_stats.credentials_quota_details_no_value')} />
<QuotaDetailMetric label={t('usage_stats.credentials_quota_details_current_cost')} value={currentUsage?.cost ?? t('usage_stats.credentials_quota_details_no_value')} />
<QuotaDetailMetric label={t('usage_stats.credentials_quota_details_estimated_cost')} value={estimatedUsage?.cost ?? t('usage_stats.credentials_quota_details_no_value')} />
</div>
)}
<div className={styles.credentialQuotaDetailsMeta}>
{percentLabel && <QuotaDetailLine label={t('usage_stats.credentials_quota_details_remaining')} value={percentLabel} />}
{resetLabel && <QuotaDetailLine label={t('usage_stats.credentials_quota_details_reset')} value={resetLabel} />}
</div>
</div>
</Modal>
)
}

function QuotaDetailMetric({ label, value }: { label: string; value: string }) {
return (
<span className={styles.credentialQuotaDetailsMetric}>
<span>{label}</span>
<strong>{value}</strong>
</span>
)
}

function QuotaDetailLine({ label, value }: { label: string; value: string }) {
return (
<div className={styles.credentialQuotaDetailsLine}>
<span>{label}</span>
<strong>{value}</strong>
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -1734,6 +1734,20 @@

.credentialQuotaBarBlock {
min-width: 150px;
appearance: none;
border: 0;
border-radius: 6px;
padding: 0;
background: transparent;
color: inherit;
font: inherit;
text-align: left;
cursor: pointer;

&:focus-visible {
outline: 2px solid color-mix(in srgb, var(--primary-color) 72%, transparent);
outline-offset: 4px;
}
}

@include mobile {
Expand Down Expand Up @@ -1879,6 +1893,76 @@
background: var(--text-tertiary);
}

.credentialQuotaDetailsModal {
max-width: calc(100vw - 28px);
}

.credentialQuotaDetailsPanel {
display: flex;
flex-direction: column;
gap: 16px;
}

.credentialQuotaDetailsGrid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}

.credentialQuotaDetailsMetric {
display: flex;
min-width: 0;
flex-direction: column;
gap: 5px;
padding: 10px 0;
border-bottom: 1px solid var(--border-color);

span {
color: var(--text-secondary);
font-size: 11px;
font-weight: 700;
}

strong {
min-width: 0;
overflow: hidden;
color: var(--text-primary);
font-size: 18px;
font-weight: 800;
font-variant-numeric: tabular-nums;
line-height: 1.2;
text-overflow: ellipsis;
white-space: nowrap;
}
}

.credentialQuotaDetailsMeta {
display: flex;
flex-direction: column;
gap: 8px;
}

.credentialQuotaDetailsLine {
display: flex;
justify-content: space-between;
gap: 12px;
color: var(--text-secondary);
font-size: 12px;

span {
flex: 0 0 auto;
}

strong {
min-width: 0;
overflow: hidden;
color: var(--text-primary);
font-weight: 800;
text-overflow: ellipsis;
white-space: nowrap;
}
}

.credentialQuotaState,
.credentialQuotaStateError,
.credentialQuotaRefreshStatus,
Expand Down Expand Up @@ -2052,6 +2136,10 @@
}

@include mobile {
.credentialQuotaDetailsGrid {
grid-template-columns: minmax(0, 1fr);
}

.credentialSectionActionButtons,
.credentialInspectionSummary,
.credentialInspectionStatsGrid,
Expand Down
36 changes: 36 additions & 0 deletions web/src/i18n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,18 @@ const resources = {
credentials_quota_usage_mode_aria: 'Quota usage display mode',
credentials_quota_usage_mode_current: 'Current',
credentials_quota_usage_mode_estimated: 'Estimated',
credentials_quota_details_open: 'Open {{label}} quota details',
credentials_quota_details_title: '{{label}} quota details',
credentials_quota_details_current_tokens: 'Current tokens',
credentials_quota_details_estimated_tokens: 'Estimated total tokens',
credentials_quota_details_current_cost: 'Current cost',
credentials_quota_details_estimated_cost: 'Estimated total cost',
credentials_quota_details_billing_used: 'Used',
credentials_quota_details_billing_limit: 'Limit',
credentials_quota_details_billing_remaining: 'Remaining',
credentials_quota_details_remaining: 'Remaining',
credentials_quota_details_reset: 'Reset',
credentials_quota_details_no_value: '-',
credentials_last_used: 'Last used',
credentials_stats_updated: 'Stats updated',
credentials_health_last_5h: 'Last 5h',
Expand Down Expand Up @@ -743,6 +755,18 @@ const resources = {
credentials_quota_usage_mode_aria: '限额用量显示模式',
credentials_quota_usage_mode_current: '当前',
credentials_quota_usage_mode_estimated: '预估',
credentials_quota_details_open: '打开 {{label}} 限额详情',
credentials_quota_details_title: '{{label}} 限额详情',
credentials_quota_details_current_tokens: '当前 Token',
credentials_quota_details_estimated_tokens: '预估总 Token',
credentials_quota_details_current_cost: '当前成本',
credentials_quota_details_estimated_cost: '预估总成本',
credentials_quota_details_billing_used: '已用',
credentials_quota_details_billing_limit: '上限',
credentials_quota_details_billing_remaining: '剩余',
credentials_quota_details_remaining: '剩余额度',
credentials_quota_details_reset: '重置时间',
credentials_quota_details_no_value: '-',
credentials_last_used: '最后使用',
credentials_stats_updated: '统计更新',
credentials_health_last_5h: '最近 5 小时',
Expand Down Expand Up @@ -1171,6 +1195,18 @@ const resources = {
credentials_quota_usage_mode_aria: '限額用量顯示模式',
credentials_quota_usage_mode_current: '目前',
credentials_quota_usage_mode_estimated: '預估',
credentials_quota_details_open: '開啟 {{label}} 限額詳情',
credentials_quota_details_title: '{{label}} 限額詳情',
credentials_quota_details_current_tokens: '目前 Token',
credentials_quota_details_estimated_tokens: '預估總 Token',
credentials_quota_details_current_cost: '目前成本',
credentials_quota_details_estimated_cost: '預估總成本',
credentials_quota_details_billing_used: '已用',
credentials_quota_details_billing_limit: '上限',
credentials_quota_details_billing_remaining: '剩餘',
credentials_quota_details_remaining: '剩餘額度',
credentials_quota_details_reset: '重置時間',
credentials_quota_details_no_value: '-',
credentials_last_used: '最後使用',
credentials_stats_updated: '統計更新',
credentials_health_last_5h: '最近 5 小時',
Expand Down