mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-08-30 17:56:18 +08:00
feat: add recommended provider section and update Kimi login text in OAuth page
This commit is contained in:
@@ -563,6 +563,10 @@
|
||||
"upgrade_required_desc": "The current server does not support the OAuth model aliases API. Please upgrade to the latest CLI Proxy API (CPA) version."
|
||||
},
|
||||
"auth_login": {
|
||||
"recommended_provider_section": "Recommended provider",
|
||||
"recommended_provider_badge": "Recommended provider",
|
||||
"other_oauth_providers": "Other OAuth providers",
|
||||
"other_login_methods": "Other login methods",
|
||||
"codex_oauth_title": "Codex OAuth",
|
||||
"codex_oauth_button": "Start Codex Login",
|
||||
"codex_oauth_hint": "Login to Codex service through OAuth flow, automatically obtain and save authentication files.",
|
||||
@@ -597,8 +601,8 @@
|
||||
"antigravity_oauth_start_error": "Failed to start Antigravity OAuth:",
|
||||
"antigravity_oauth_polling_error": "Failed to check authentication status:",
|
||||
"kimi_oauth_title": "Kimi OAuth",
|
||||
"kimi_oauth_button": "Start Kimi Login",
|
||||
"kimi_oauth_hint": "Login to Kimi service through OAuth device flow, automatically obtain and save authentication files.",
|
||||
"kimi_oauth_button": "Log in with Kimi",
|
||||
"kimi_oauth_hint": "Quickly log in to Kimi through device authorization. Authentication files are obtained and saved automatically.",
|
||||
"kimi_oauth_url_label": "Authorization URL:",
|
||||
"kimi_open_link": "Open Link",
|
||||
"kimi_copy_link": "Copy Link",
|
||||
|
||||
@@ -550,6 +550,10 @@
|
||||
"upgrade_required_desc": "Текущая версия сервера не поддерживает API псевдонимов моделей OAuth. Обновите CLI Proxy API (CPA) до последней версии."
|
||||
},
|
||||
"auth_login": {
|
||||
"recommended_provider_section": "Рекомендуемый провайдер",
|
||||
"recommended_provider_badge": "Рекомендуемый провайдер",
|
||||
"other_oauth_providers": "Другие провайдеры OAuth",
|
||||
"other_login_methods": "Другие способы входа",
|
||||
"codex_oauth_title": "Codex OAuth",
|
||||
"codex_oauth_button": "Начать вход Codex",
|
||||
"codex_oauth_hint": "Выполните вход в сервис Codex через OAuth и автоматически получите/сохраните файлы авторизации.",
|
||||
@@ -584,8 +588,8 @@
|
||||
"antigravity_oauth_start_error": "Не удалось запустить Antigravity OAuth:",
|
||||
"antigravity_oauth_polling_error": "Не удалось проверить статус аутентификации:",
|
||||
"kimi_oauth_title": "Kimi OAuth",
|
||||
"kimi_oauth_button": "Начать вход Kimi",
|
||||
"kimi_oauth_hint": "Выполните вход в сервис Kimi через поток авторизации устройства и автоматически получите/сохраните файлы авторизации.",
|
||||
"kimi_oauth_button": "Войти через Kimi",
|
||||
"kimi_oauth_hint": "Быстро войдите в Kimi через авторизацию устройства. Файлы авторизации будут получены и сохранены автоматически.",
|
||||
"kimi_oauth_url_label": "URL авторизации:",
|
||||
"kimi_open_link": "Открыть ссылку",
|
||||
"kimi_copy_link": "Скопировать ссылку",
|
||||
|
||||
@@ -563,6 +563,10 @@
|
||||
"upgrade_required_desc": "当前服务器版本不支持 OAuth 模型别名功能,请升级到最新版本的 CPA(CLI Proxy API)后重试。"
|
||||
},
|
||||
"auth_login": {
|
||||
"recommended_provider_section": "推荐提供商",
|
||||
"recommended_provider_badge": "推荐提供商",
|
||||
"other_oauth_providers": "其他 OAuth 提供商",
|
||||
"other_login_methods": "其他登录方式",
|
||||
"codex_oauth_title": "Codex OAuth",
|
||||
"codex_oauth_button": "开始 Codex 登录",
|
||||
"codex_oauth_hint": "通过 OAuth 流程登录 Codex 服务,自动获取并保存认证文件。",
|
||||
@@ -597,8 +601,8 @@
|
||||
"antigravity_oauth_start_error": "启动 Antigravity OAuth 失败:",
|
||||
"antigravity_oauth_polling_error": "检查认证状态失败:",
|
||||
"kimi_oauth_title": "Kimi OAuth",
|
||||
"kimi_oauth_button": "开始 Kimi 登录",
|
||||
"kimi_oauth_hint": "通过设备授权流程登录 Kimi 服务,自动获取并保存认证文件。",
|
||||
"kimi_oauth_button": "使用 Kimi 登录",
|
||||
"kimi_oauth_hint": "通过设备授权快速登录 Kimi,完成后将自动获取并保存认证文件。",
|
||||
"kimi_oauth_url_label": "授权链接:",
|
||||
"kimi_open_link": "打开链接",
|
||||
"kimi_copy_link": "复制链接",
|
||||
|
||||
@@ -563,6 +563,10 @@
|
||||
"upgrade_required_desc": "目前伺服器版本不支援 OAuth 模型別名功能,請升級到最新版本的 CPA(CLI Proxy API)後重試。"
|
||||
},
|
||||
"auth_login": {
|
||||
"recommended_provider_section": "推薦提供商",
|
||||
"recommended_provider_badge": "推薦提供商",
|
||||
"other_oauth_providers": "其他 OAuth 提供商",
|
||||
"other_login_methods": "其他登入方式",
|
||||
"codex_oauth_title": "Codex OAuth",
|
||||
"codex_oauth_button": "開始 Codex 登入",
|
||||
"codex_oauth_hint": "透過 OAuth 流程登入 Codex 服務,自動取得並儲存驗證檔案。",
|
||||
@@ -597,8 +601,8 @@
|
||||
"antigravity_oauth_start_error": "啟動 Antigravity OAuth 失敗:",
|
||||
"antigravity_oauth_polling_error": "檢查驗證狀態失敗:",
|
||||
"kimi_oauth_title": "Kimi OAuth",
|
||||
"kimi_oauth_button": "開始 Kimi 登入",
|
||||
"kimi_oauth_hint": "透過裝置授權流程登入 Kimi 服務,自動取得並儲存驗證檔案。",
|
||||
"kimi_oauth_button": "使用 Kimi 登入",
|
||||
"kimi_oauth_hint": "透過裝置授權快速登入 Kimi,完成後將自動取得並儲存驗證檔案。",
|
||||
"kimi_oauth_url_label": "授權連結:",
|
||||
"kimi_open_link": "開啟連結",
|
||||
"kimi_copy_link": "複製連結",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
.cardTitleIcon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.cardTitleIconFallback {
|
||||
@@ -37,6 +38,110 @@
|
||||
gap: $spacing-xl;
|
||||
}
|
||||
|
||||
.providerSection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-md;
|
||||
}
|
||||
|
||||
.providerList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-xl;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
margin: 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.featuredCard {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-color: color-mix(in srgb, #1783ff 42%, var(--border-color));
|
||||
background:
|
||||
radial-gradient(circle at 92% -35%, rgba(23, 131, 255, 0.24), transparent 42%),
|
||||
linear-gradient(
|
||||
135deg,
|
||||
color-mix(in srgb, #1783ff 10%, var(--bg-primary)),
|
||||
var(--bg-primary) 64%
|
||||
);
|
||||
box-shadow:
|
||||
0 14px 32px -26px rgba(23, 131, 255, 0.95),
|
||||
var(--shadow);
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
inset: 0 auto 0 0;
|
||||
width: 4px;
|
||||
background: linear-gradient(180deg, #51a7ff, #1783ff);
|
||||
content: '';
|
||||
}
|
||||
|
||||
> * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.cardTitle {
|
||||
gap: $spacing-md;
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
.cardTitleIcon {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
padding: 7px;
|
||||
border: 1px solid rgba(23, 131, 255, 0.2);
|
||||
border-radius: 11px;
|
||||
background: color-mix(in srgb, #1783ff 12%, var(--bg-primary));
|
||||
}
|
||||
|
||||
:global(.btn-primary) {
|
||||
border-color: #1783ff;
|
||||
background: #1783ff;
|
||||
box-shadow: 0 8px 20px -12px rgba(23, 131, 255, 0.9);
|
||||
|
||||
&:hover {
|
||||
border-color: #0875ed;
|
||||
background: #0875ed;
|
||||
}
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
:global(.card-header > .btn) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.recommendedBadge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 24px;
|
||||
padding: 3px 9px;
|
||||
border: 1px solid rgba(23, 131, 255, 0.34);
|
||||
border-radius: $radius-full;
|
||||
background: rgba(23, 131, 255, 0.11);
|
||||
color: color-mix(in srgb, #1783ff 82%, var(--text-primary));
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.featuredHint {
|
||||
max-width: 720px;
|
||||
margin: 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: 15px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.cardContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
+237
-208
@@ -564,224 +564,253 @@ export function OAuthPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const renderOAuthProviderCard = (provider: OAuthProviderCard, featured = false) => {
|
||||
const state = states[provider.id] || {};
|
||||
const canSubmitCallback =
|
||||
(provider.kind === 'plugin' || CALLBACK_SUPPORTED.has(provider.id)) && Boolean(state.url);
|
||||
const loginButtonLabel =
|
||||
state.status === 'success'
|
||||
? t('auth_login.login_another_account')
|
||||
: getProviderText(provider, 'oauth_button');
|
||||
const statusBadgeClassName = [
|
||||
'status-badge',
|
||||
state.status === 'success' ? 'success' : '',
|
||||
state.status === 'error' ? 'error' : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={provider.id}
|
||||
className={featured ? styles.featuredCard : undefined}
|
||||
title={
|
||||
<span className={styles.cardTitle}>
|
||||
<OAuthProviderIcon provider={provider} theme={resolvedTheme} />
|
||||
<span>{getProviderTitleText(provider)}</span>
|
||||
{featured && (
|
||||
<span className={styles.recommendedBadge}>
|
||||
{t('auth_login.recommended_provider_badge')}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
extra={
|
||||
<Button onClick={() => startAuth(provider.id)} loading={state.polling}>
|
||||
{loginButtonLabel}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className={styles.cardContent}>
|
||||
<div className={featured ? styles.featuredHint : styles.cardHint}>
|
||||
{getProviderText(provider, 'oauth_hint')}
|
||||
</div>
|
||||
{state.url && (
|
||||
<div className={styles.authUrlBox}>
|
||||
<div className={styles.authUrlLabel}>
|
||||
{getProviderText(provider, 'oauth_url_label')}
|
||||
</div>
|
||||
<div className={styles.authUrlValue}>{state.url}</div>
|
||||
<div className={styles.authUrlActions}>
|
||||
<Button variant="secondary" size="sm" onClick={() => copyLink(state.url!)}>
|
||||
{getProviderText(provider, 'copy_link')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => window.open(state.url, '_blank', 'noopener,noreferrer')}
|
||||
>
|
||||
{getProviderText(provider, 'open_link')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{canSubmitCallback && (
|
||||
<div className={styles.callbackSection}>
|
||||
<Input
|
||||
label={t(
|
||||
provider.id === 'xai'
|
||||
? 'auth_login.xai_callback_label'
|
||||
: 'auth_login.oauth_callback_label'
|
||||
)}
|
||||
hint={t(
|
||||
provider.id === 'xai'
|
||||
? 'auth_login.xai_callback_hint'
|
||||
: 'auth_login.oauth_callback_hint'
|
||||
)}
|
||||
value={state.callbackUrl || ''}
|
||||
onChange={(e) =>
|
||||
updateProviderState(provider.id, {
|
||||
callbackUrl: e.target.value,
|
||||
callbackStatus: undefined,
|
||||
callbackError: undefined,
|
||||
})
|
||||
}
|
||||
placeholder={t(
|
||||
provider.id === 'xai'
|
||||
? 'auth_login.xai_callback_placeholder'
|
||||
: 'auth_login.oauth_callback_placeholder'
|
||||
)}
|
||||
/>
|
||||
<div className={styles.callbackActions}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => submitCallback(provider.id)}
|
||||
loading={state.callbackSubmitting}
|
||||
>
|
||||
{t('auth_login.oauth_callback_button')}
|
||||
</Button>
|
||||
</div>
|
||||
{state.callbackStatus === 'success' && state.status === 'waiting' && (
|
||||
<div className="status-badge success">
|
||||
{t('auth_login.oauth_callback_status_success')}
|
||||
</div>
|
||||
)}
|
||||
{state.callbackStatus === 'error' && (
|
||||
<div className="status-badge error">
|
||||
{t('auth_login.oauth_callback_status_error')} {state.callbackError || ''}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{state.status && state.status !== 'idle' && (
|
||||
<div className={statusBadgeClassName}>
|
||||
{state.status === 'success'
|
||||
? getProviderText(provider, 'oauth_status_success')
|
||||
: state.status === 'error'
|
||||
? `${getProviderText(provider, 'oauth_status_error')} ${state.error || ''}`
|
||||
: getProviderText(provider, 'oauth_status_waiting')}
|
||||
</div>
|
||||
)}
|
||||
{state.status === 'success' && (
|
||||
<div className={styles.successActions}>
|
||||
<Button variant="secondary" size="sm" onClick={() => navigate('/auth-files')}>
|
||||
{t('auth_login.view_auth_files')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const featuredProvider = providerCards.find((provider) => provider.id === 'kimi');
|
||||
const otherOAuthProviders = providerCards.filter((provider) => provider.id !== 'kimi');
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<h1 className={styles.pageTitle}>{t('nav.oauth', { defaultValue: 'OAuth' })}</h1>
|
||||
|
||||
<div className={styles.content}>
|
||||
{providerCards.map((provider) => {
|
||||
const state = states[provider.id] || {};
|
||||
const canSubmitCallback =
|
||||
(provider.kind === 'plugin' || CALLBACK_SUPPORTED.has(provider.id)) &&
|
||||
Boolean(state.url);
|
||||
const loginButtonLabel =
|
||||
state.status === 'success'
|
||||
? t('auth_login.login_another_account')
|
||||
: getProviderText(provider, 'oauth_button');
|
||||
const statusBadgeClassName = [
|
||||
'status-badge',
|
||||
state.status === 'success' ? 'success' : '',
|
||||
state.status === 'error' ? 'error' : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
return (
|
||||
<div key={provider.id}>
|
||||
<Card
|
||||
title={
|
||||
<span className={styles.cardTitle}>
|
||||
<OAuthProviderIcon provider={provider} theme={resolvedTheme} />
|
||||
{getProviderTitleText(provider)}
|
||||
</span>
|
||||
}
|
||||
extra={
|
||||
<Button onClick={() => startAuth(provider.id)} loading={state.polling}>
|
||||
{loginButtonLabel}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className={styles.cardContent}>
|
||||
<div className={styles.cardHint}>{getProviderText(provider, 'oauth_hint')}</div>
|
||||
{state.url && (
|
||||
<div className={styles.authUrlBox}>
|
||||
<div className={styles.authUrlLabel}>
|
||||
{getProviderText(provider, 'oauth_url_label')}
|
||||
</div>
|
||||
<div className={styles.authUrlValue}>{state.url}</div>
|
||||
<div className={styles.authUrlActions}>
|
||||
<Button variant="secondary" size="sm" onClick={() => copyLink(state.url!)}>
|
||||
{getProviderText(provider, 'copy_link')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => window.open(state.url, '_blank', 'noopener,noreferrer')}
|
||||
>
|
||||
{getProviderText(provider, 'open_link')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{canSubmitCallback && (
|
||||
<div className={styles.callbackSection}>
|
||||
<Input
|
||||
label={t(
|
||||
provider.id === 'xai'
|
||||
? 'auth_login.xai_callback_label'
|
||||
: 'auth_login.oauth_callback_label'
|
||||
)}
|
||||
hint={t(
|
||||
provider.id === 'xai'
|
||||
? 'auth_login.xai_callback_hint'
|
||||
: 'auth_login.oauth_callback_hint'
|
||||
)}
|
||||
value={state.callbackUrl || ''}
|
||||
onChange={(e) =>
|
||||
updateProviderState(provider.id, {
|
||||
callbackUrl: e.target.value,
|
||||
callbackStatus: undefined,
|
||||
callbackError: undefined,
|
||||
})
|
||||
}
|
||||
placeholder={t(
|
||||
provider.id === 'xai'
|
||||
? 'auth_login.xai_callback_placeholder'
|
||||
: 'auth_login.oauth_callback_placeholder'
|
||||
)}
|
||||
/>
|
||||
<div className={styles.callbackActions}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => submitCallback(provider.id)}
|
||||
loading={state.callbackSubmitting}
|
||||
>
|
||||
{t('auth_login.oauth_callback_button')}
|
||||
</Button>
|
||||
</div>
|
||||
{state.callbackStatus === 'success' && state.status === 'waiting' && (
|
||||
<div className="status-badge success">
|
||||
{t('auth_login.oauth_callback_status_success')}
|
||||
</div>
|
||||
)}
|
||||
{state.callbackStatus === 'error' && (
|
||||
<div className="status-badge error">
|
||||
{t('auth_login.oauth_callback_status_error')} {state.callbackError || ''}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{state.status && state.status !== 'idle' && (
|
||||
<div className={statusBadgeClassName}>
|
||||
{state.status === 'success'
|
||||
? getProviderText(provider, 'oauth_status_success')
|
||||
: state.status === 'error'
|
||||
? `${getProviderText(provider, 'oauth_status_error')} ${state.error || ''}`
|
||||
: getProviderText(provider, 'oauth_status_waiting')}
|
||||
</div>
|
||||
)}
|
||||
{state.status === 'success' && (
|
||||
<div className={styles.successActions}>
|
||||
<Button variant="secondary" size="sm" onClick={() => navigate('/auth-files')}>
|
||||
{t('auth_login.view_auth_files')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{featuredProvider && (
|
||||
<section className={styles.providerSection}>
|
||||
<h2 className={styles.sectionTitle}>{t('auth_login.recommended_provider_section')}</h2>
|
||||
{renderOAuthProviderCard(featuredProvider, true)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className={styles.providerSection}>
|
||||
<h2 className={styles.sectionTitle}>{t('auth_login.other_oauth_providers')}</h2>
|
||||
<div className={styles.providerList}>
|
||||
{otherOAuthProviders.map((provider) => renderOAuthProviderCard(provider))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Vertex JSON 登录 */}
|
||||
<Card
|
||||
title={
|
||||
<span className={styles.cardTitle}>
|
||||
<img src={iconVertex} alt="" className={styles.cardTitleIcon} />
|
||||
{t('vertex_import.title')}
|
||||
</span>
|
||||
}
|
||||
extra={
|
||||
<Button onClick={handleVertexImport} loading={vertexState.loading}>
|
||||
{t('vertex_import.import_button')}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className={styles.cardContent}>
|
||||
<div className={styles.cardHint}>{t('vertex_import.description')}</div>
|
||||
<Input
|
||||
label={t('vertex_import.location_label')}
|
||||
hint={t('vertex_import.location_hint')}
|
||||
value={vertexState.location}
|
||||
onChange={(e) =>
|
||||
setVertexState((prev) => ({
|
||||
...prev,
|
||||
location: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder={t('vertex_import.location_placeholder')}
|
||||
/>
|
||||
<div className={styles.formItem}>
|
||||
<label className={styles.formItemLabel}>{t('vertex_import.file_label')}</label>
|
||||
<div className={styles.filePicker}>
|
||||
<Button variant="secondary" size="sm" onClick={handleVertexFilePick}>
|
||||
{t('vertex_import.choose_file')}
|
||||
</Button>
|
||||
<div
|
||||
className={`${styles.fileName} ${
|
||||
vertexState.fileName ? '' : styles.fileNamePlaceholder
|
||||
}`.trim()}
|
||||
>
|
||||
{vertexState.fileName || t('vertex_import.file_placeholder')}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.cardHintSecondary}>{t('vertex_import.file_hint')}</div>
|
||||
<input
|
||||
ref={vertexFileInputRef}
|
||||
type="file"
|
||||
accept=".json,application/json"
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleVertexFileChange}
|
||||
<section className={styles.providerSection}>
|
||||
<h2 className={styles.sectionTitle}>{t('auth_login.other_login_methods')}</h2>
|
||||
<Card
|
||||
title={
|
||||
<span className={styles.cardTitle}>
|
||||
<img src={iconVertex} alt="" className={styles.cardTitleIcon} />
|
||||
{t('vertex_import.title')}
|
||||
</span>
|
||||
}
|
||||
extra={
|
||||
<Button onClick={handleVertexImport} loading={vertexState.loading}>
|
||||
{t('vertex_import.import_button')}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className={styles.cardContent}>
|
||||
<div className={styles.cardHint}>{t('vertex_import.description')}</div>
|
||||
<Input
|
||||
label={t('vertex_import.location_label')}
|
||||
hint={t('vertex_import.location_hint')}
|
||||
value={vertexState.location}
|
||||
onChange={(e) =>
|
||||
setVertexState((prev) => ({
|
||||
...prev,
|
||||
location: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder={t('vertex_import.location_placeholder')}
|
||||
/>
|
||||
</div>
|
||||
{vertexState.error && <div className="status-badge error">{vertexState.error}</div>}
|
||||
{vertexState.result && (
|
||||
<div className={styles.connectionBox}>
|
||||
<div className={styles.connectionLabel}>{t('vertex_import.result_title')}</div>
|
||||
<div className={styles.keyValueList}>
|
||||
{vertexState.result.projectId && (
|
||||
<div className={styles.keyValueItem}>
|
||||
<span className={styles.keyValueKey}>
|
||||
{t('vertex_import.result_project')}
|
||||
</span>
|
||||
<span className={styles.keyValueValue}>{vertexState.result.projectId}</span>
|
||||
</div>
|
||||
)}
|
||||
{vertexState.result.email && (
|
||||
<div className={styles.keyValueItem}>
|
||||
<span className={styles.keyValueKey}>{t('vertex_import.result_email')}</span>
|
||||
<span className={styles.keyValueValue}>{vertexState.result.email}</span>
|
||||
</div>
|
||||
)}
|
||||
{vertexState.result.location && (
|
||||
<div className={styles.keyValueItem}>
|
||||
<span className={styles.keyValueKey}>
|
||||
{t('vertex_import.result_location')}
|
||||
</span>
|
||||
<span className={styles.keyValueValue}>{vertexState.result.location}</span>
|
||||
</div>
|
||||
)}
|
||||
{vertexState.result.authFile && (
|
||||
<div className={styles.keyValueItem}>
|
||||
<span className={styles.keyValueKey}>{t('vertex_import.result_file')}</span>
|
||||
<span className={styles.keyValueValue}>{vertexState.result.authFile}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.formItem}>
|
||||
<label className={styles.formItemLabel}>{t('vertex_import.file_label')}</label>
|
||||
<div className={styles.filePicker}>
|
||||
<Button variant="secondary" size="sm" onClick={handleVertexFilePick}>
|
||||
{t('vertex_import.choose_file')}
|
||||
</Button>
|
||||
<div
|
||||
className={`${styles.fileName} ${
|
||||
vertexState.fileName ? '' : styles.fileNamePlaceholder
|
||||
}`.trim()}
|
||||
>
|
||||
{vertexState.fileName || t('vertex_import.file_placeholder')}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.cardHintSecondary}>{t('vertex_import.file_hint')}</div>
|
||||
<input
|
||||
ref={vertexFileInputRef}
|
||||
type="file"
|
||||
accept=".json,application/json"
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleVertexFileChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
{vertexState.error && <div className="status-badge error">{vertexState.error}</div>}
|
||||
{vertexState.result && (
|
||||
<div className={styles.connectionBox}>
|
||||
<div className={styles.connectionLabel}>{t('vertex_import.result_title')}</div>
|
||||
<div className={styles.keyValueList}>
|
||||
{vertexState.result.projectId && (
|
||||
<div className={styles.keyValueItem}>
|
||||
<span className={styles.keyValueKey}>
|
||||
{t('vertex_import.result_project')}
|
||||
</span>
|
||||
<span className={styles.keyValueValue}>{vertexState.result.projectId}</span>
|
||||
</div>
|
||||
)}
|
||||
{vertexState.result.email && (
|
||||
<div className={styles.keyValueItem}>
|
||||
<span className={styles.keyValueKey}>
|
||||
{t('vertex_import.result_email')}
|
||||
</span>
|
||||
<span className={styles.keyValueValue}>{vertexState.result.email}</span>
|
||||
</div>
|
||||
)}
|
||||
{vertexState.result.location && (
|
||||
<div className={styles.keyValueItem}>
|
||||
<span className={styles.keyValueKey}>
|
||||
{t('vertex_import.result_location')}
|
||||
</span>
|
||||
<span className={styles.keyValueValue}>{vertexState.result.location}</span>
|
||||
</div>
|
||||
)}
|
||||
{vertexState.result.authFile && (
|
||||
<div className={styles.keyValueItem}>
|
||||
<span className={styles.keyValueKey}>{t('vertex_import.result_file')}</span>
|
||||
<span className={styles.keyValueValue}>{vertexState.result.authFile}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user