feat(oauth): consolidate credential and quota management
- add tabbed navigation to the OAuth page - move auth files and quota lookup into OAuth - remove their standalone sidebar entries
This commit is contained in:
1 parent
365356969e
commit
85e0764ad9
7 files changed
+79
-18
No files matched your search
+2
-18
@@ -6,8 +6,6 @@ import {
|
||||
Check,
|
||||
ChevronUp,
|
||||
ExternalLink,
|
||||
FileKey,
|
||||
Gauge,
|
||||
History,
|
||||
House,
|
||||
Languages,
|
||||
@@ -23,10 +21,8 @@ import appLogo from './assets/logo.jpg';
|
||||
import { CoreRuntimeProvider, useCoreRuntime } from './coreRuntime';
|
||||
import { ConfigPanelPage } from './pages/ConfigPanel';
|
||||
import { ApiAccessPage } from './pages/ApiAccessPage';
|
||||
import { AuthFileManagementPage } from './pages/AuthFileManagementPage';
|
||||
import { KernelPage } from './pages/Kernel';
|
||||
import { OAuthLoginPage } from './pages/ManagementPages';
|
||||
import { QuotaPage } from './pages/QuotaPage';
|
||||
import { OAuthManagementPage } from './pages/ManagementPages';
|
||||
import { AgentsPage } from './pages/AgentsPage';
|
||||
import { UsageRecordsPage } from './pages/UsageRecordsPage';
|
||||
import { languageOptions, useI18n } from './i18n';
|
||||
@@ -60,7 +56,7 @@ const pages = [
|
||||
id: 'oauth',
|
||||
labelKey: 'app.nav.oauth',
|
||||
icon: LogIn,
|
||||
component: OAuthLoginPage,
|
||||
component: OAuthManagementPage,
|
||||
},
|
||||
{
|
||||
id: 'api',
|
||||
@@ -68,18 +64,6 @@ const pages = [
|
||||
icon: Network,
|
||||
component: ApiAccessPage,
|
||||
},
|
||||
{
|
||||
id: 'auth-files',
|
||||
labelKey: 'app.nav.authFiles',
|
||||
icon: FileKey,
|
||||
component: AuthFileManagementPage,
|
||||
},
|
||||
{
|
||||
id: 'quota',
|
||||
labelKey: 'app.nav.quota',
|
||||
icon: Gauge,
|
||||
component: QuotaPage,
|
||||
},
|
||||
{
|
||||
id: 'usage-records',
|
||||
labelKey: 'app.nav.usageRecords',
|
||||
|
||||
@@ -60,6 +60,7 @@ export const jaOverrides = {
|
||||
'quota.confirm.expiry': '最も早い有効期限:{time}',
|
||||
'quota.confirm.warning': '「確認」を押した場合のみ実行されます。「キャンセル」ではリセット回数を消費しません。',
|
||||
'oauth.title': 'OAuth ログイン',
|
||||
'oauth.tabs.label': 'OAuth 管理ページ',
|
||||
'oauth.loginSuccess': '{provider} へのログインに成功しました',
|
||||
'oauth.priorityApplyFailed': 'OAuth ログインは成功しましたが、新しい認証情報の優先度を 0 に設定できませんでした:{error}',
|
||||
'oauth.authFailed': '認証に失敗しました',
|
||||
|
||||
@@ -63,6 +63,7 @@ export const zhCN = {
|
||||
'quota.confirm.expiry': '最早过期:{time}',
|
||||
'quota.confirm.warning': '只有点击“确定”后才会执行;点击“取消”不会消耗重置机会。',
|
||||
'oauth.title': 'OAuth 登录',
|
||||
'oauth.tabs.label': 'OAuth 管理子页面',
|
||||
'oauth.loginSuccess': '{provider} 登录成功',
|
||||
'oauth.priorityApplyFailed': 'OAuth 登录成功,但无法将新凭证的优先级设为 0:{error}',
|
||||
'oauth.authFailed': '认证失败',
|
||||
@@ -951,6 +952,7 @@ export const en: Record<MessageKey, string> = {
|
||||
'quota.confirm.expiry': 'Earliest expiry: {time}',
|
||||
'quota.confirm.warning': 'The reset runs only after you confirm. Canceling will not consume a reset credit.',
|
||||
'oauth.title': 'OAuth Sign-In',
|
||||
'oauth.tabs.label': 'OAuth management sections',
|
||||
'oauth.loginSuccess': '{provider} sign-in succeeded',
|
||||
'oauth.priorityApplyFailed': 'OAuth sign-in succeeded, but the new credential priority could not be set to 0: {error}',
|
||||
'oauth.authFailed': 'Authentication failed',
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export const oauthSubpages = [
|
||||
{ id: 'login', labelKey: 'oauth.title' },
|
||||
{ id: 'authFiles', labelKey: 'authFiles.title' },
|
||||
{ id: 'quota', labelKey: 'quota.title' },
|
||||
] as const;
|
||||
|
||||
export type OAuthSubpage = (typeof oauthSubpages)[number]['id'];
|
||||
@@ -13,12 +13,15 @@ import codexIcon from '../assets/icons/codex.svg';
|
||||
import grokIcon from '../assets/icons/grok.svg';
|
||||
import kimiIcon from '../assets/icons/kimi-light.svg';
|
||||
import { useI18n } from '../i18n';
|
||||
import { oauthSubpages, type OAuthSubpage } from '../oauthNavigation';
|
||||
import {
|
||||
changedOAuthAuthFileNames,
|
||||
snapshotAuthFiles,
|
||||
type AuthFileSnapshot,
|
||||
} from '../services/authFiles';
|
||||
import { managementApi, responseList } from '../services/managementApi';
|
||||
import { AuthFileManagementPage } from './AuthFileManagementPage';
|
||||
import { QuotaPage } from './QuotaPage';
|
||||
|
||||
type OAuthProviderId = 'codex' | 'claude' | 'antigravity' | 'kimi' | 'xai';
|
||||
type OAuthFlowStatus = 'idle' | 'waiting' | 'success' | 'error';
|
||||
@@ -65,6 +68,51 @@ const XAI_CALLBACK_URL = 'http://127.0.0.1:56121/callback';
|
||||
const OAUTH_SUCCESS_RESET_MS = 5000;
|
||||
const OAUTH_POLL_INTERVAL_MS = 3000;
|
||||
|
||||
export function OAuthManagementPage() {
|
||||
const { t } = useI18n();
|
||||
const [activeSubpage, setActiveSubpage] = useState<OAuthSubpage>('login');
|
||||
|
||||
return (
|
||||
<section className="page oauth-management-page">
|
||||
<div
|
||||
className="agent-subpage-tabs oauth-subpage-tabs"
|
||||
role="tablist"
|
||||
aria-label={t('oauth.tabs.label')}
|
||||
>
|
||||
{oauthSubpages.map((subpage) => {
|
||||
const active = activeSubpage === subpage.id;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
id={`oauth-subpage-tab-${subpage.id}`}
|
||||
key={subpage.id}
|
||||
role="tab"
|
||||
className={active ? 'active' : ''}
|
||||
aria-selected={active}
|
||||
aria-controls={`oauth-subpage-panel-${subpage.id}`}
|
||||
tabIndex={active ? 0 : -1}
|
||||
onClick={() => setActiveSubpage(subpage.id)}
|
||||
>
|
||||
{t(subpage.labelKey)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="oauth-subpage-panel"
|
||||
id={`oauth-subpage-panel-${activeSubpage}`}
|
||||
role="tabpanel"
|
||||
aria-labelledby={`oauth-subpage-tab-${activeSubpage}`}
|
||||
>
|
||||
{activeSubpage === 'login' ? <OAuthLoginPage /> : null}
|
||||
{activeSubpage === 'authFiles' ? <AuthFileManagementPage /> : null}
|
||||
{activeSubpage === 'quota' ? <QuotaPage /> : null}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function OAuthLoginPage() {
|
||||
const { t } = useI18n();
|
||||
const [states, setStates] = useState<Partial<Record<OAuthProviderId, OAuthProviderState>>>({});
|
||||
|
||||
@@ -4498,6 +4498,13 @@ textarea:focus-visible {
|
||||
box-shadow: 0 1px 4px rgba(45, 42, 38, 0.14);
|
||||
}
|
||||
|
||||
.oauth-management-page,
|
||||
.oauth-subpage-panel {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.agent-core-config {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { appUpdateIndicatorState } from '../src/appUpdateModel';
|
||||
import { canOpenAppPage, isAlwaysAvailablePage } from '../src/navigation';
|
||||
import { oauthSubpages } from '../src/oauthNavigation';
|
||||
|
||||
describe('首页、配置与版本管理导航', () => {
|
||||
test('内核停止时首页、配置和版本管理始终可进入', () => {
|
||||
@@ -19,6 +20,17 @@ describe('首页、配置与版本管理导航', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('OAuth 子页面导航', () => {
|
||||
test('认证文件和额度查询收纳在 OAuth 页面内', () => {
|
||||
expect(oauthSubpages.map((page) => page.id)).toEqual(['login', 'authFiles', 'quota']);
|
||||
expect(oauthSubpages.map((page) => page.labelKey)).toEqual([
|
||||
'oauth.title',
|
||||
'authFiles.title',
|
||||
'quota.title',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('软件更新导航提示点', () => {
|
||||
test('有新版显示橙点,处理中的蓝点优先', () => {
|
||||
expect(appUpdateIndicatorState(true, false)).toBe('available');
|
||||
|
||||
Reference in new issue
Block a user