fix(management): 联动对象存储连接状态
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { CheckCircle2, Cloud, Database, Loader2, LockKeyhole, RadioTower, ShieldCheck } from 'lucide-react';
|
import { CheckCircle2, Cloud, Database, Loader2, LockKeyhole, RadioTower, ShieldCheck } from 'lucide-react';
|
||||||
import type { StorageConfig } from '@shared/types';
|
import type { StorageConfig } from '@shared/types';
|
||||||
import { api, ApiError } from '@/api/client';
|
import { api, ApiError } from '@/api/client';
|
||||||
@@ -8,10 +8,8 @@ const initialForm = {
|
|||||||
path_prefix: 'delivery-desk', secret_id: '', secret_key: '',
|
path_prefix: 'delivery-desk', secret_id: '', secret_key: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function StorageSettings() {
|
export default function StorageSettings({ items, onItemsChange }: { items: StorageConfig[]; onItemsChange: (items: StorageConfig[]) => void }) {
|
||||||
const [items, setItems] = useState<StorageConfig[]>([]);
|
|
||||||
const [form, setForm] = useState(initialForm);
|
const [form, setForm] = useState(initialForm);
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [busy, setBusy] = useState<'save' | 'test' | 'activate' | null>(null);
|
const [busy, setBusy] = useState<'save' | 'test' | 'activate' | null>(null);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [notice, setNotice] = useState('');
|
const [notice, setNotice] = useState('');
|
||||||
@@ -19,12 +17,9 @@ export default function StorageSettings() {
|
|||||||
const draft = useMemo(() => items.find((item) => item.status === 'draft'), [items]);
|
const draft = useMemo(() => items.find((item) => item.status === 'draft'), [items]);
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
setLoading(true);
|
try { onItemsChange(await api.listStorageConfigs()); }
|
||||||
try { setItems(await api.listStorageConfigs()); }
|
|
||||||
catch (reason) { setError(messageOf(reason)); }
|
catch (reason) { setError(messageOf(reason)); }
|
||||||
finally { setLoading(false); }
|
|
||||||
};
|
};
|
||||||
useEffect(() => { void load(); }, []);
|
|
||||||
|
|
||||||
const save = async () => {
|
const save = async () => {
|
||||||
setBusy('save'); setError(''); setNotice('');
|
setBusy('save'); setError(''); setNotice('');
|
||||||
@@ -55,8 +50,6 @@ export default function StorageSettings() {
|
|||||||
finally { setBusy(null); }
|
finally { setBusy(null); }
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) return <div className="grid gap-3 py-8">{[1, 2].map((item) => <div key={item} className="h-28 animate-pulse rounded-2xl bg-black/5"/>)}</div>;
|
|
||||||
|
|
||||||
return <section className="py-8">
|
return <section className="py-8">
|
||||||
<div className="mb-7"><p className="font-mono text-[10px] uppercase tracking-[.28em] text-[#d15f37]">Infrastructure / Object Storage</p><h2 className="mt-2 font-display text-4xl tracking-[-.04em]">腾讯云 COS</h2><p className="mt-2 max-w-2xl text-sm leading-6 text-black/45">先保存为草稿,再执行上传、读取和删除测试。只有测试通过的配置可以成为正式存储。</p></div>
|
<div className="mb-7"><p className="font-mono text-[10px] uppercase tracking-[.28em] text-[#d15f37]">Infrastructure / Object Storage</p><h2 className="mt-2 font-display text-4xl tracking-[-.04em]">腾讯云 COS</h2><p className="mt-2 max-w-2xl text-sm leading-6 text-black/45">先保存为草稿,再执行上传、读取和删除测试。只有测试通过的配置可以成为正式存储。</p></div>
|
||||||
{error && <div className="mb-5 rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{error}</div>}
|
{error && <div className="mb-5 rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{error}</div>}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { ArrowRightLeft, ChevronDown, Copy, Fingerprint, History, KeyRound, Plus, RefreshCw, Search, ShieldCheck, Users, X } from 'lucide-react';
|
import { ArrowRightLeft, ChevronDown, Copy, Fingerprint, History, KeyRound, Plus, RefreshCw, Search, ShieldCheck, Users, X } from 'lucide-react';
|
||||||
import type { AuditLogEntry, ManagedApiKey, ManagedUser, OperationGroup, Project } from '@shared/types';
|
import type { AuditLogEntry, ManagedApiKey, ManagedUser, OperationGroup, Project, StorageConfig } from '@shared/types';
|
||||||
import { api, ApiError } from '@/api/client';
|
import { api, ApiError } from '@/api/client';
|
||||||
import { useAuthStore } from '@/store/useAuthStore';
|
import { useAuthStore } from '@/store/useAuthStore';
|
||||||
import StorageSettings from '@/components/StorageSettings';
|
import StorageSettings from '@/components/StorageSettings';
|
||||||
@@ -28,6 +28,7 @@ export default function ManagementPage() {
|
|||||||
const [keys, setKeys] = useState<ManagedApiKey[]>([]);
|
const [keys, setKeys] = useState<ManagedApiKey[]>([]);
|
||||||
const [logs, setLogs] = useState<AuditLogEntry[]>([]);
|
const [logs, setLogs] = useState<AuditLogEntry[]>([]);
|
||||||
const [projects, setProjects] = useState<Project[]>([]);
|
const [projects, setProjects] = useState<Project[]>([]);
|
||||||
|
const [storageConfigs, setStorageConfigs] = useState<StorageConfig[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
@@ -53,11 +54,12 @@ export default function ManagementPage() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
try {
|
try {
|
||||||
const [nextAccounts, nextKeys, nextLogs, nextProjects, nextGroups] = await Promise.all([
|
const [nextAccounts, nextKeys, nextLogs, nextProjects, nextGroups, nextStorageConfigs] = await Promise.all([
|
||||||
api.listManagedUsers(), api.listApiKeys(), api.listAuditLogs(), api.listProjects(),
|
api.listManagedUsers(), api.listApiKeys(), api.listAuditLogs(), api.listProjects(),
|
||||||
isPlatform ? api.listGroups() : Promise.resolve([]),
|
isPlatform ? api.listGroups() : Promise.resolve([]),
|
||||||
|
isPlatform ? api.listStorageConfigs() : Promise.resolve([]),
|
||||||
]);
|
]);
|
||||||
setAccounts(nextAccounts); setKeys(nextKeys); setLogs(nextLogs); setProjects(nextProjects); setGroups(nextGroups);
|
setAccounts(nextAccounts); setKeys(nextKeys); setLogs(nextLogs); setProjects(nextProjects); setGroups(nextGroups); setStorageConfigs(nextStorageConfigs);
|
||||||
} catch (reason) { setError(messageOf(reason)); }
|
} catch (reason) { setError(messageOf(reason)); }
|
||||||
finally { setLoading(false); }
|
finally { setLoading(false); }
|
||||||
}, [isPlatform, user]);
|
}, [isPlatform, user]);
|
||||||
@@ -67,9 +69,9 @@ export default function ManagementPage() {
|
|||||||
...(isPlatform ? [{ id: 'groups' as const, label: '运营组', count: groups.length }] : []),
|
...(isPlatform ? [{ id: 'groups' as const, label: '运营组', count: groups.length }] : []),
|
||||||
{ id: 'accounts' as const, label: '账号', count: accounts.length },
|
{ id: 'accounts' as const, label: '账号', count: accounts.length },
|
||||||
{ id: 'keys' as const, label: 'API Key', count: keys.filter((item) => item.status === 'active').length },
|
{ id: 'keys' as const, label: 'API Key', count: keys.filter((item) => item.status === 'active').length },
|
||||||
...(isPlatform ? [{ id: 'storage' as const, label: '对象存储', count: 0 }] : []),
|
...(isPlatform ? [{ id: 'storage' as const, label: '对象存储', count: storageConfigs.some((item) => item.status === 'active') ? '已连接' : '未配置' }] : []),
|
||||||
{ id: 'audit' as const, label: '审计日志', count: logs.length },
|
{ id: 'audit' as const, label: '审计日志', count: logs.length },
|
||||||
], [accounts.length, groups.length, isPlatform, keys, logs.length]);
|
], [accounts.length, groups.length, isPlatform, keys, logs.length, storageConfigs]);
|
||||||
|
|
||||||
if (user?.role === 'operator') {
|
if (user?.role === 'operator') {
|
||||||
return <main className="mx-auto max-w-3xl px-5 py-24 text-center"><ShieldCheck className="mx-auto text-black/25"/><h1 className="mt-5 font-display text-4xl">此区域仅对管理员开放</h1><p className="mt-3 text-sm text-black/45">内容账号可以继续管理项目与作品,不具备账号和密钥管理权限。</p></main>;
|
return <main className="mx-auto max-w-3xl px-5 py-24 text-center"><ShieldCheck className="mx-auto text-black/25"/><h1 className="mt-5 font-display text-4xl">此区域仅对管理员开放</h1><p className="mt-3 text-sm text-black/45">内容账号可以继续管理项目与作品,不具备账号和密钥管理权限。</p></main>;
|
||||||
@@ -111,7 +113,7 @@ export default function ManagementPage() {
|
|||||||
{tab === 'groups' && <Groups groups={groups} accounts={accounts} onCreate={() => setPanel('group')} onRename={(item)=>{setRenameGroup(item);setRenameGroupValue(item.name);setPanel('rename_group')}} onShowAccounts={(item) => { setAccountGroupFilter(String(item.id)); setTab('accounts'); }} onReplace={(item) => { setReplaceGroup(item); setReplacementUserId(''); setPreviousAdminAction('demote'); setPanel('replace_admin'); }} onToggle={(item) => { if(item.status==='active'&&!window.confirm(`停用“${item.name}”将影响 ${item.active_user_count} 个启用账号、${item.project_count} 个项目和 ${item.customer_link_count} 个客户访问链接。确认继续吗?`))return;void run(() => api.setGroupStatus(item.id, item.status === 'active' ? 'disabled' : 'active').then(() => undefined)); }}/>}
|
{tab === 'groups' && <Groups groups={groups} accounts={accounts} onCreate={() => setPanel('group')} onRename={(item)=>{setRenameGroup(item);setRenameGroupValue(item.name);setPanel('rename_group')}} onShowAccounts={(item) => { setAccountGroupFilter(String(item.id)); setTab('accounts'); }} onReplace={(item) => { setReplaceGroup(item); setReplacementUserId(''); setPreviousAdminAction('demote'); setPanel('replace_admin'); }} onToggle={(item) => { if(item.status==='active'&&!window.confirm(`停用“${item.name}”将影响 ${item.active_user_count} 个启用账号、${item.project_count} 个项目和 ${item.customer_link_count} 个客户访问链接。确认继续吗?`))return;void run(() => api.setGroupStatus(item.id, item.status === 'active' ? 'disabled' : 'active').then(() => undefined)); }}/>}
|
||||||
{tab === 'accounts' && <Accounts accounts={accounts} groups={groups} groupFilter={accountGroupFilter} onGroupFilter={setAccountGroupFilter} currentId={user?.id} currentRole={user?.role} onCreate={() => setPanel('user')} onAudit={(item) => { setAuditUserId(item.id); setTab('audit'); }} onRename={(item) => { setRenameTarget(item); setRenameValue(item.display_name); setPanel('rename'); }} onToggle={(item) => void run(() => api.setUserStatus(item.id, item.status === 'active' ? 'disabled' : 'active').then(() => undefined))} onReset={(item) => { setResetTarget(item); setResetPassword(''); setPanel('reset'); }}/>}
|
{tab === 'accounts' && <Accounts accounts={accounts} groups={groups} groupFilter={accountGroupFilter} onGroupFilter={setAccountGroupFilter} currentId={user?.id} currentRole={user?.role} onCreate={() => setPanel('user')} onAudit={(item) => { setAuditUserId(item.id); setTab('audit'); }} onRename={(item) => { setRenameTarget(item); setRenameValue(item.display_name); setPanel('rename'); }} onToggle={(item) => void run(() => api.setUserStatus(item.id, item.status === 'active' ? 'disabled' : 'active').then(() => undefined))} onReset={(item) => { setResetTarget(item); setResetPassword(''); setPanel('reset'); }}/>}
|
||||||
{tab === 'keys' && <ApiKeys items={keys} onCreate={() => { setRevealedKey(''); setCopyState('idle'); setPanel('key'); }} onRevoke={(item) => void run(() => api.revokeApiKey(item.id))}/>}
|
{tab === 'keys' && <ApiKeys items={keys} onCreate={() => { setRevealedKey(''); setCopyState('idle'); setPanel('key'); }} onRevoke={(item) => void run(() => api.revokeApiKey(item.id))}/>}
|
||||||
{tab === 'storage' && isPlatform && <StorageSettings/>}
|
{tab === 'storage' && isPlatform && <StorageSettings items={storageConfigs} onItemsChange={setStorageConfigs}/>}
|
||||||
{tab === 'audit' && <AuditLogs items={logs} filteredUser={auditUserId ? accounts.find((item)=>item.id===auditUserId) : undefined} onClear={() => setAuditUserId(null)}/>}
|
{tab === 'audit' && <AuditLogs items={logs} filteredUser={auditUserId ? accounts.find((item)=>item.id===auditUserId) : undefined} onClear={() => setAuditUserId(null)}/>}
|
||||||
</>}
|
</>}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user