fix(auth-files): sync quota priority display and credential ordering
Read OAuth priorities in quota cards and sort credentials by descending priority followed by filename order across OAuth and quota pages.
This commit is contained in:
1 parent
46a9d3d449
commit
a08f38df7f
5 files changed
+54
-4
No files matched your search
@@ -69,6 +69,7 @@ import {
|
||||
parseAuthFilePriority,
|
||||
resetAuthFileCooldown,
|
||||
setOAuthCredentialFileDisabled,
|
||||
sortAuthFilesByPriority,
|
||||
} from '../services/authFiles';
|
||||
import { authFileHealth, normalizeAuthFileCooldowns } from '../services/authFileHealth';
|
||||
import { codexMetadataFor } from '../services/quotaMetadata';
|
||||
@@ -175,7 +176,7 @@ export function AuthFileManagementPage() {
|
||||
try {
|
||||
const payload = await managementApi.get('/credentials');
|
||||
if (!mountedRef.current || requestId !== fileRequestRef.current) return;
|
||||
const nextFiles = dedupeAuthFiles(responseList(payload, 'files'));
|
||||
const nextFiles = sortAuthFilesByPriority(dedupeAuthFiles(responseList(payload, 'files')));
|
||||
setFileSnapshot({ files: nextFiles, receivedAtMs: Date.now(), observedAt: readString(payload, 'observed_at') });
|
||||
const validQuotaKeys = new Set(nextFiles.map(quotaKey));
|
||||
pruneQuotaCache(validQuotaKeys);
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
updateQuotaCache,
|
||||
useQuotaCache,
|
||||
} from '../services/quotaCache';
|
||||
import { dedupeAuthFiles } from '../services/authFiles';
|
||||
import { dedupeAuthFiles, parseAuthFilePriority, sortAuthFilesByPriority } from '../services/authFiles';
|
||||
import { useI18n } from '../i18n';
|
||||
import './QuotaPage.css';
|
||||
|
||||
@@ -65,7 +65,7 @@ export function QuotaPage() {
|
||||
setError('');
|
||||
try {
|
||||
const payload = await managementApi.get('/credentials');
|
||||
const allFiles = dedupeAuthFiles(responseList(payload, 'files'));
|
||||
const allFiles = sortAuthFilesByPriority(dedupeAuthFiles(responseList(payload, 'files')));
|
||||
const nextFiles = allFiles.filter((file) => !readBoolean(file, 'disabled') && providerForFile(file));
|
||||
setFiles(nextFiles);
|
||||
const validQuotaKeys = new Set(allFiles.map(quotaKey));
|
||||
@@ -188,6 +188,7 @@ export function QuotaCard({ file, quota, onRefresh, onReset }: { file: AuthFile;
|
||||
const provider = providerForFile(file);
|
||||
const name = fileName(file);
|
||||
const disabled = readBoolean(file, 'disabled');
|
||||
const priority = parseAuthFilePriority(file.priority) ?? 0;
|
||||
return (
|
||||
<article className="panel real-quota-card">
|
||||
<div className="real-quota-card-header">
|
||||
@@ -196,7 +197,7 @@ export function QuotaCard({ file, quota, onRefresh, onReset }: { file: AuthFile;
|
||||
<div><strong title={name}>{name}</strong><span>{provider ? providerMeta[provider].label : t('quota.unknownProvider')}</span></div>
|
||||
</div>
|
||||
<div className="quota-plan"><strong>{quota.plan || '—'}</strong><span>{quota.subscriptionActiveUntil ? t('quota.expiresAt', { time: formatQuotaTimestamp(quota.subscriptionActiveUntil, locale) }) : ''}</span></div>
|
||||
<div className="quota-status"><span className={quota.status === 'success' ? 'quota-status-badge ready' : quota.status === 'error' ? 'quota-status-badge error' : 'quota-status-badge'}>{quota.status === 'success' ? t('common.enabled') : quota.status === 'loading' ? t('quota.querying') : quota.status === 'error' ? t('common.unavailable') : t('quota.notFetched')}</span><small>{t('authFiles.settings.priority')} 0</small></div>
|
||||
<div className="quota-status"><span className={quota.status === 'success' ? 'quota-status-badge ready' : quota.status === 'error' ? 'quota-status-badge error' : 'quota-status-badge'}>{quota.status === 'success' ? t('common.enabled') : quota.status === 'loading' ? t('quota.querying') : quota.status === 'error' ? t('common.unavailable') : t('quota.notFetched')}</span><small>{t('authFiles.settings.priority')} {priority}</small></div>
|
||||
<div className="quota-card-actions">
|
||||
<button type="button" className="icon-button quiet" onClick={onRefresh} disabled={disabled || quota.status === 'loading'} title={disabled ? t('quota.fileDisabled') : t('quota.refresh')} aria-label={disabled ? t('quota.fileDisabled') : t('quota.refresh')}><RefreshCw size={16} className={quota.status === 'loading' ? 'spin' : ''} aria-hidden="true" /></button>
|
||||
</div>
|
||||
|
||||
@@ -37,6 +37,19 @@ export const resetAuthFileCooldown = async (
|
||||
export const authFileName = (file: AuthFileRecord) =>
|
||||
readString(file, 'name') || translate(getCurrentLocale(), 'authFiles.unnamed');
|
||||
|
||||
/**
|
||||
* Present credentials in the same order used by routing: higher explicit
|
||||
* priority first, then a deterministic case-insensitive filename order.
|
||||
* Return a new array so callers do not mutate the API response or snapshots.
|
||||
*/
|
||||
export const sortAuthFilesByPriority = <File extends AuthFileRecord>(files: File[]): File[] =>
|
||||
[...files].sort((left, right) => {
|
||||
const priorityDelta = (parseAuthFilePriority(right.priority) ?? 0)
|
||||
- (parseAuthFilePriority(left.priority) ?? 0);
|
||||
if (priorityDelta !== 0) return priorityDelta;
|
||||
return authFileName(left).localeCompare(authFileName(right), undefined, { sensitivity: 'base' });
|
||||
});
|
||||
|
||||
export const isRuntimeOnlyAuthFile = (file: AuthFileRecord) =>
|
||||
readBoolean(file, 'runtime_only', 'runtimeOnly');
|
||||
|
||||
|
||||
@@ -25,6 +25,28 @@ it('shows every reset expiry in chronological order, including duplicates, on bo
|
||||
}
|
||||
});
|
||||
|
||||
it('shows the OAuth credential priority on the quota card', () => {
|
||||
const html = renderToStaticMarkup(<I18nProvider><QuotaCard
|
||||
file={{ name: 'priority.json', provider: 'codex', priority: 7 }}
|
||||
quota={{ status: 'success', rows: [] }} onRefresh={() => {}}
|
||||
/></I18nProvider>);
|
||||
expect(html).toContain('Priority 7');
|
||||
expect(html).not.toContain('Priority 0');
|
||||
});
|
||||
|
||||
it('normalizes a string priority and keeps zero as the default', () => {
|
||||
const stringPriority = renderToStaticMarkup(<I18nProvider><QuotaCard
|
||||
file={{ name: 'priority.json', provider: 'codex', priority: '3' }}
|
||||
quota={{ status: 'idle', rows: [] }} onRefresh={() => {}}
|
||||
/></I18nProvider>);
|
||||
const missingPriority = renderToStaticMarkup(<I18nProvider><QuotaCard
|
||||
file={{ name: 'default.json', provider: 'codex' }}
|
||||
quota={{ status: 'idle', rows: [] }} onRefresh={() => {}}
|
||||
/></I18nProvider>);
|
||||
expect(stringPriority).toContain('Priority 3');
|
||||
expect(missingPriority).toContain('Priority 0');
|
||||
});
|
||||
|
||||
it('renders real quota values with bounded bars and keeps unknown distinct from zero', () => {
|
||||
const html = render({ status: 'success', rows: [
|
||||
{ label: 'empty', remainingPercent: 0 }, { label: 'full', remainingPercent: 100 },
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
oauthModelProvidersFromAuthFiles,
|
||||
parseAuthFilePriority,
|
||||
setOAuthCredentialFileDisabled,
|
||||
sortAuthFilesByPriority,
|
||||
snapshotAuthFiles,
|
||||
} from '../src/services/authFiles';
|
||||
|
||||
@@ -181,6 +182,18 @@ describe('authentication file priority', () => {
|
||||
expect(normalizeAuthFilePriorityInput('1.5')).toBeNull();
|
||||
});
|
||||
|
||||
it('sorts credentials by descending priority, then filename', () => {
|
||||
expect(sortAuthFilesByPriority([
|
||||
{ name: 'zeta.json', priority: 2 },
|
||||
{ name: 'Beta.json', priority: 5 },
|
||||
{ name: 'alpha.json', priority: 5 },
|
||||
{ name: 'default.json' },
|
||||
{ name: 'negative.json', priority: -1 },
|
||||
]).map((file) => file.name)).toEqual([
|
||||
'alpha.json', 'Beta.json', 'zeta.json', 'default.json', 'negative.json',
|
||||
]);
|
||||
});
|
||||
|
||||
it('finds only credentials created or updated by the completed OAuth provider', () => {
|
||||
const before = snapshotAuthFiles([
|
||||
{ name: 'codex-old.json', provider: 'codex', modtime: 1, priority: 8 },
|
||||
|
||||
Reference in new issue
Block a user