mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-09 10:55:27 +08:00
Show UI drift marker for env draft vs Vercel config
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
const ENV_DRAFT_KEY = 'ds2api_env_config_draft_v1'
|
||||
|
||||
export function useAdminConfig({ token, showMessage, t }) {
|
||||
const [config, setConfig] = useState({ keys: [], accounts: [] })
|
||||
|
||||
@@ -11,6 +13,21 @@ export function useAdminConfig({ token, showMessage, t }) {
|
||||
})
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
if (data?.env_backed) {
|
||||
const rawDraft = localStorage.getItem(ENV_DRAFT_KEY)
|
||||
if (rawDraft) {
|
||||
try {
|
||||
const draft = JSON.parse(rawDraft)
|
||||
setConfig({ ...draft, env_backed: true })
|
||||
return
|
||||
} catch (_e) {
|
||||
localStorage.removeItem(ENV_DRAFT_KEY)
|
||||
}
|
||||
}
|
||||
localStorage.setItem(ENV_DRAFT_KEY, JSON.stringify(data))
|
||||
} else {
|
||||
localStorage.removeItem(ENV_DRAFT_KEY)
|
||||
}
|
||||
setConfig(data)
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -21,6 +38,17 @@ export function useAdminConfig({ token, showMessage, t }) {
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
const rawDraft = localStorage.getItem(ENV_DRAFT_KEY)
|
||||
if (rawDraft) {
|
||||
try {
|
||||
const draft = JSON.parse(rawDraft)
|
||||
if (draft?.env_backed) {
|
||||
setConfig(draft)
|
||||
}
|
||||
} catch (_e) {
|
||||
localStorage.removeItem(ENV_DRAFT_KEY)
|
||||
}
|
||||
}
|
||||
fetchConfig()
|
||||
}
|
||||
}, [fetchConfig, token])
|
||||
|
||||
@@ -101,6 +101,7 @@ export default function AccountManagerContainer({ config, onRefresh, onMessage,
|
||||
onPageSizeChange={changePageSize}
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={handleSearchChange}
|
||||
envBacked={Boolean(config?.env_backed)}
|
||||
/>
|
||||
|
||||
<AddKeyModal
|
||||
|
||||
@@ -26,6 +26,7 @@ export default function AccountsTable({
|
||||
onPageSizeChange,
|
||||
searchQuery,
|
||||
onSearchChange,
|
||||
envBacked = false,
|
||||
}) {
|
||||
const [copiedId, setCopiedId] = useState(null)
|
||||
|
||||
@@ -101,14 +102,16 @@ export default function AccountsTable({
|
||||
) : accounts.length > 0 ? (
|
||||
accounts.map((acc, i) => {
|
||||
const id = resolveAccountIdentifier(acc)
|
||||
const runtimeUnknown = envBacked && !acc.test_status
|
||||
const isActive = acc.test_status === 'ok' || acc.has_token
|
||||
return (
|
||||
<div key={i} className="p-4 flex flex-col md:flex-row md:items-center justify-between gap-4 hover:bg-muted/50 transition-colors">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div className={clsx(
|
||||
"w-2 h-2 rounded-full shrink-0",
|
||||
acc.test_status === 'failed' ? "bg-red-500 shadow-[0_0_8px_rgba(239,68,68,0.5)]" :
|
||||
(acc.test_status === 'ok' || acc.has_token) ? "bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.5)]" :
|
||||
"bg-amber-500"
|
||||
isActive ? "bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.5)]" :
|
||||
runtimeUnknown ? "bg-blue-500 shadow-[0_0_8px_rgba(59,130,246,0.5)]" : "bg-amber-500"
|
||||
)} />
|
||||
<div className="min-w-0">
|
||||
<div
|
||||
@@ -122,7 +125,7 @@ export default function AccountsTable({
|
||||
}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground mt-0.5">
|
||||
<span>{acc.test_status === 'failed' ? t('accountManager.testStatusFailed') : (acc.test_status === 'ok' || acc.has_token) ? t('accountManager.sessionActive') : t('accountManager.reauthRequired')}</span>
|
||||
<span>{acc.test_status === 'failed' ? t('accountManager.testStatusFailed') : isActive ? t('accountManager.sessionActive') : runtimeUnknown ? t('accountManager.runtimeStatusUnknown') : t('accountManager.reauthRequired')}</span>
|
||||
{acc.token_preview && (
|
||||
<span className="font-mono bg-muted px-1.5 py-0.5 rounded text-[10px]">
|
||||
{acc.token_preview}
|
||||
|
||||
@@ -4,7 +4,7 @@ import VercelSyncForm from './VercelSyncForm'
|
||||
import VercelSyncStatus from './VercelSyncStatus'
|
||||
import VercelGuide from './VercelGuide'
|
||||
|
||||
export default function VercelSyncContainer({ onMessage, authFetch, isVercel = false }) {
|
||||
export default function VercelSyncContainer({ onMessage, authFetch, isVercel = false, config = null }) {
|
||||
const { t } = useI18n()
|
||||
const apiFetch = authFetch || fetch
|
||||
|
||||
@@ -28,6 +28,7 @@ export default function VercelSyncContainer({ onMessage, authFetch, isVercel = f
|
||||
onMessage,
|
||||
t,
|
||||
isVercel,
|
||||
config,
|
||||
})
|
||||
|
||||
return (
|
||||
|
||||
@@ -69,6 +69,11 @@ export default function VercelSyncForm({
|
||||
{t('vercel.lastSyncTime', { time: new Date(syncStatus.last_sync_time * 1000).toLocaleString() })}
|
||||
</p>
|
||||
)}
|
||||
{syncStatus?.draft_differs && (
|
||||
<p className="text-xs text-amber-500 mt-2">
|
||||
{t('vercel.draftDiffers')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
|
||||
@@ -8,7 +8,7 @@ function pollDelayMs(attempt) {
|
||||
return 60000
|
||||
}
|
||||
|
||||
export function useVercelSyncState({ apiFetch, onMessage, t, isVercel = false }) {
|
||||
export function useVercelSyncState({ apiFetch, onMessage, t, isVercel = false, config = null }) {
|
||||
const [vercelToken, setVercelToken] = useState('')
|
||||
const [projectId, setProjectId] = useState('')
|
||||
const [teamId, setTeamId] = useState('')
|
||||
@@ -22,7 +22,14 @@ export function useVercelSyncState({ apiFetch, onMessage, t, isVercel = false })
|
||||
|
||||
const fetchSyncStatus = useCallback(async ({ manual = false } = {}) => {
|
||||
try {
|
||||
const res = await apiFetch('/admin/vercel/status')
|
||||
const hasConfig = Boolean(config && (Array.isArray(config?.keys) || Array.isArray(config?.accounts)))
|
||||
const res = await apiFetch('/admin/vercel/status', hasConfig ? {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
config_override: { keys: config?.keys || [], accounts: config?.accounts || [] },
|
||||
}),
|
||||
} : undefined)
|
||||
if (!res.ok) {
|
||||
throw new Error(`status ${res.status}`)
|
||||
}
|
||||
@@ -50,7 +57,7 @@ export function useVercelSyncState({ apiFetch, onMessage, t, isVercel = false })
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to fetch sync status:', e)
|
||||
}
|
||||
}, [apiFetch, isVercel, onMessage, t])
|
||||
}, [apiFetch, config, isVercel, onMessage, t])
|
||||
|
||||
useEffect(() => {
|
||||
const loadPreconfig = async () => {
|
||||
@@ -117,6 +124,7 @@ export function useVercelSyncState({ apiFetch, onMessage, t, isVercel = false })
|
||||
vercel_token: tokenToUse,
|
||||
project_id: projectId,
|
||||
team_id: teamId || undefined,
|
||||
config_override: config ? { keys: config.keys || [], accounts: config.accounts || [] } : undefined,
|
||||
}),
|
||||
})
|
||||
const data = await res.json()
|
||||
@@ -133,7 +141,7 @@ export function useVercelSyncState({ apiFetch, onMessage, t, isVercel = false })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [apiFetch, fetchSyncStatus, onMessage, preconfig?.has_token, projectId, t, teamId, vercelToken])
|
||||
}, [apiFetch, config, fetchSyncStatus, onMessage, preconfig?.has_token, projectId, t, teamId, vercelToken])
|
||||
|
||||
return {
|
||||
vercelToken,
|
||||
|
||||
@@ -79,7 +79,7 @@ export default function DashboardShell({ token, onLogout, config, fetchConfig, s
|
||||
case 'import':
|
||||
return <BatchImport onRefresh={fetchConfig} onMessage={showMessage} authFetch={authFetch} />
|
||||
case 'vercel':
|
||||
return <VercelSync onMessage={showMessage} authFetch={authFetch} isVercel={isVercel} />
|
||||
return <VercelSync onMessage={showMessage} authFetch={authFetch} isVercel={isVercel} config={config} />
|
||||
case 'settings':
|
||||
return <Settings onRefresh={fetchConfig} onMessage={showMessage} authFetch={authFetch} onForceLogout={onForceLogout} isVercel={isVercel} />
|
||||
default:
|
||||
|
||||
@@ -115,6 +115,7 @@
|
||||
"testingAllAccounts": "Refreshing tokens for all accounts...",
|
||||
"sessionActive": "Session active",
|
||||
"reauthRequired": "Re-auth required",
|
||||
"runtimeStatusUnknown": "Will be determined after sync",
|
||||
"testStatusFailed": "Last test failed",
|
||||
"noAccounts": "No accounts found.",
|
||||
"modalAddKeyTitle": "Add API key",
|
||||
@@ -294,6 +295,7 @@
|
||||
"statusNotSynced": "Not synced",
|
||||
"statusNeverSynced": "Never synced",
|
||||
"lastSyncTime": "Last sync: {time}",
|
||||
"draftDiffers": "Frontend draft differs from env config. Click Sync & redeploy.",
|
||||
"pollPaused": "Status polling paused after {count} failures.",
|
||||
"manualRefresh": "Refresh manually",
|
||||
"howItWorks": "How it works",
|
||||
|
||||
@@ -115,6 +115,7 @@
|
||||
"testingAllAccounts": "正在刷新所有账号 Token...",
|
||||
"sessionActive": "已建立会话",
|
||||
"reauthRequired": "需重新登录",
|
||||
"runtimeStatusUnknown": "状态以同步后为准",
|
||||
"testStatusFailed": "上次测试失败",
|
||||
"noAccounts": "未找到任何账号",
|
||||
"modalAddKeyTitle": "添加 API 密钥",
|
||||
@@ -294,6 +295,7 @@
|
||||
"statusNotSynced": "未同步",
|
||||
"statusNeverSynced": "从未同步",
|
||||
"lastSyncTime": "上次同步: {time}",
|
||||
"draftDiffers": "检测到前端草稿与环境变量配置不一致,请点击“同步并重新部署”。",
|
||||
"pollPaused": "状态轮询已暂停:连续失败 {count} 次。",
|
||||
"manualRefresh": "手动刷新",
|
||||
"howItWorks": "工作原理",
|
||||
|
||||
Reference in New Issue
Block a user