mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-21 22:31:46 +00:00
chore: apply prettier on the entire project
This commit is contained in:
@@ -1,161 +1,212 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { adminApi } from '../../api/client'
|
||||
import { useToast } from '../shared/Toast'
|
||||
import { Key, Trash2, User, Loader2, Shield } from 'lucide-react'
|
||||
import { useTranslation } from '../../i18n'
|
||||
import { Key, Loader2, Shield, Trash2, User } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { adminApi } from '../../api/client';
|
||||
import { useTranslation } from '../../i18n';
|
||||
import { useToast } from '../shared/Toast';
|
||||
|
||||
interface AdminOAuthSession {
|
||||
id: number
|
||||
client_id: string
|
||||
client_name: string
|
||||
user_id: number
|
||||
username: string
|
||||
scopes: string[]
|
||||
access_token_expires_at: string
|
||||
refresh_token_expires_at: string
|
||||
created_at: string
|
||||
id: number;
|
||||
client_id: string;
|
||||
client_name: string;
|
||||
user_id: number;
|
||||
username: string;
|
||||
scopes: string[];
|
||||
access_token_expires_at: string;
|
||||
refresh_token_expires_at: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface AdminMcpToken {
|
||||
id: number
|
||||
name: string
|
||||
token_prefix: string
|
||||
created_at: string
|
||||
last_used_at: string | null
|
||||
user_id: number
|
||||
username: string
|
||||
id: number;
|
||||
name: string;
|
||||
token_prefix: string;
|
||||
created_at: string;
|
||||
last_used_at: string | null;
|
||||
user_id: number;
|
||||
username: string;
|
||||
}
|
||||
|
||||
const SCOPES_PREVIEW = 6
|
||||
const SCOPES_PREVIEW = 6;
|
||||
|
||||
export default function AdminMcpTokensPanel() {
|
||||
const [sessions, setSessions] = useState<AdminOAuthSession[]>([])
|
||||
const [sessionsLoading, setSessionsLoading] = useState(true)
|
||||
const [tokens, setTokens] = useState<AdminMcpToken[]>([])
|
||||
const [tokensLoading, setTokensLoading] = useState(true)
|
||||
const [expandedScopes, setExpandedScopes] = useState<Set<number>>(new Set())
|
||||
const [revokeConfirmId, setRevokeConfirmId] = useState<number | null>(null)
|
||||
const [deleteConfirmId, setDeleteConfirmId] = useState<number | null>(null)
|
||||
const [sessions, setSessions] = useState<AdminOAuthSession[]>([]);
|
||||
const [sessionsLoading, setSessionsLoading] = useState(true);
|
||||
const [tokens, setTokens] = useState<AdminMcpToken[]>([]);
|
||||
const [tokensLoading, setTokensLoading] = useState(true);
|
||||
const [expandedScopes, setExpandedScopes] = useState<Set<number>>(new Set());
|
||||
const [revokeConfirmId, setRevokeConfirmId] = useState<number | null>(null);
|
||||
const [deleteConfirmId, setDeleteConfirmId] = useState<number | null>(null);
|
||||
|
||||
const toggleScopes = (id: number) =>
|
||||
setExpandedScopes(prev => {
|
||||
const next = new Set(prev)
|
||||
next.has(id) ? next.delete(id) : next.add(id)
|
||||
return next
|
||||
})
|
||||
const toast = useToast()
|
||||
const { t, locale } = useTranslation()
|
||||
setExpandedScopes((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.has(id) ? next.delete(id) : next.add(id);
|
||||
return next;
|
||||
});
|
||||
const toast = useToast();
|
||||
const { t, locale } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
adminApi.oauthSessions()
|
||||
.then(d => setSessions(d.sessions || []))
|
||||
adminApi
|
||||
.oauthSessions()
|
||||
.then((d) => setSessions(d.sessions || []))
|
||||
.catch(() => toast.error(t('admin.oauthSessions.loadError')))
|
||||
.finally(() => setSessionsLoading(false))
|
||||
.finally(() => setSessionsLoading(false));
|
||||
|
||||
adminApi.mcpTokens()
|
||||
.then(d => setTokens(d.tokens || []))
|
||||
adminApi
|
||||
.mcpTokens()
|
||||
.then((d) => setTokens(d.tokens || []))
|
||||
.catch(() => toast.error(t('admin.mcpTokens.loadError')))
|
||||
.finally(() => setTokensLoading(false))
|
||||
}, [])
|
||||
.finally(() => setTokensLoading(false));
|
||||
}, []);
|
||||
|
||||
const handleRevoke = async (id: number) => {
|
||||
try {
|
||||
await adminApi.revokeOAuthSession(id)
|
||||
setSessions(prev => prev.filter(s => s.id !== id))
|
||||
setRevokeConfirmId(null)
|
||||
toast.success(t('admin.oauthSessions.revokeSuccess'))
|
||||
await adminApi.revokeOAuthSession(id);
|
||||
setSessions((prev) => prev.filter((s) => s.id !== id));
|
||||
setRevokeConfirmId(null);
|
||||
toast.success(t('admin.oauthSessions.revokeSuccess'));
|
||||
} catch {
|
||||
toast.error(t('admin.oauthSessions.revokeError'))
|
||||
toast.error(t('admin.oauthSessions.revokeError'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
await adminApi.deleteMcpToken(id)
|
||||
setTokens(prev => prev.filter(tk => tk.id !== id))
|
||||
setDeleteConfirmId(null)
|
||||
toast.success(t('admin.mcpTokens.deleteSuccess'))
|
||||
await adminApi.deleteMcpToken(id);
|
||||
setTokens((prev) => prev.filter((tk) => tk.id !== id));
|
||||
setDeleteConfirmId(null);
|
||||
toast.success(t('admin.mcpTokens.deleteSuccess'));
|
||||
} catch {
|
||||
toast.error(t('admin.mcpTokens.deleteError'))
|
||||
toast.error(t('admin.mcpTokens.deleteError'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold" style={{ color: 'var(--text-primary)' }}>{t('admin.mcpTokens.title')}</h2>
|
||||
<p className="text-sm mt-0.5" style={{ color: 'var(--text-tertiary)' }}>{t('admin.mcpTokens.subtitle')}</p>
|
||||
<h2 className="text-lg font-semibold" style={{ color: 'var(--text-primary)' }}>
|
||||
{t('admin.mcpTokens.title')}
|
||||
</h2>
|
||||
<p className="mt-0.5 text-sm" style={{ color: 'var(--text-tertiary)' }}>
|
||||
{t('admin.mcpTokens.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* OAuth Sessions */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold mb-2" style={{ color: 'var(--text-secondary)' }}>{t('admin.oauthSessions.sectionTitle')}</h3>
|
||||
<div className="rounded-xl border overflow-hidden" style={{ borderColor: 'var(--border-primary)', background: 'var(--bg-card)' }}>
|
||||
<h3 className="mb-2 text-sm font-semibold" style={{ color: 'var(--text-secondary)' }}>
|
||||
{t('admin.oauthSessions.sectionTitle')}
|
||||
</h3>
|
||||
<div
|
||||
className="overflow-hidden rounded-xl border"
|
||||
style={{ borderColor: 'var(--border-primary)', background: 'var(--bg-card)' }}
|
||||
>
|
||||
{sessionsLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-5 h-5 animate-spin" style={{ color: 'var(--text-tertiary)' }} />
|
||||
<Loader2 className="h-5 w-5 animate-spin" style={{ color: 'var(--text-tertiary)' }} />
|
||||
</div>
|
||||
) : sessions.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 gap-2">
|
||||
<Shield className="w-8 h-8" style={{ color: 'var(--text-tertiary)' }} />
|
||||
<p className="text-sm" style={{ color: 'var(--text-tertiary)' }}>{t('admin.oauthSessions.empty')}</p>
|
||||
<div className="flex flex-col items-center justify-center gap-2 py-12">
|
||||
<Shield className="h-8 w-8" style={{ color: 'var(--text-tertiary)' }} />
|
||||
<p className="text-sm" style={{ color: 'var(--text-tertiary)' }}>
|
||||
{t('admin.oauthSessions.empty')}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid grid-cols-[1fr_auto_auto_auto] gap-x-6 px-4 py-2.5 text-xs font-medium border-b"
|
||||
style={{ color: 'var(--text-tertiary)', borderColor: 'var(--border-primary)', background: 'var(--bg-secondary)' }}>
|
||||
<div
|
||||
className="grid grid-cols-[1fr_auto_auto_auto] gap-x-6 border-b px-4 py-2.5 text-xs font-medium"
|
||||
style={{
|
||||
color: 'var(--text-tertiary)',
|
||||
borderColor: 'var(--border-primary)',
|
||||
background: 'var(--bg-secondary)',
|
||||
}}
|
||||
>
|
||||
<span>{t('admin.oauthSessions.clientName')}</span>
|
||||
<span>{t('admin.oauthSessions.owner')}</span>
|
||||
<span className="text-right">{t('admin.oauthSessions.created')}</span>
|
||||
<span></span>
|
||||
</div>
|
||||
{sessions.map((session, i) => {
|
||||
const expanded = expandedScopes.has(session.id)
|
||||
const visible = expanded ? session.scopes : session.scopes.slice(0, SCOPES_PREVIEW)
|
||||
const hidden = session.scopes.length - SCOPES_PREVIEW
|
||||
const expanded = expandedScopes.has(session.id);
|
||||
const visible = expanded ? session.scopes : session.scopes.slice(0, SCOPES_PREVIEW);
|
||||
const hidden = session.scopes.length - SCOPES_PREVIEW;
|
||||
return (
|
||||
<div key={session.id}
|
||||
<div
|
||||
key={session.id}
|
||||
className="grid grid-cols-[1fr_auto_auto_auto] items-start gap-x-6 px-4 py-3"
|
||||
style={{ borderBottom: i < sessions.length - 1 ? '1px solid var(--border-primary)' : undefined }}>
|
||||
style={{ borderBottom: i < sessions.length - 1 ? '1px solid var(--border-primary)' : undefined }}
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium truncate" style={{ color: 'var(--text-primary)' }}>{session.client_name}</p>
|
||||
<div className="flex flex-wrap gap-1 mt-1.5">
|
||||
{visible.map(scope => (
|
||||
<span key={scope} className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-mono"
|
||||
style={{ background: 'var(--bg-secondary)', color: 'var(--text-tertiary)', border: '1px solid var(--border-primary)' }}>
|
||||
<p className="truncate text-sm font-medium" style={{ color: 'var(--text-primary)' }}>
|
||||
{session.client_name}
|
||||
</p>
|
||||
<div className="mt-1.5 flex flex-wrap gap-1">
|
||||
{visible.map((scope) => (
|
||||
<span
|
||||
key={scope}
|
||||
className="inline-flex items-center rounded px-1.5 py-0.5 font-mono text-xs"
|
||||
style={{
|
||||
background: 'var(--bg-secondary)',
|
||||
color: 'var(--text-tertiary)',
|
||||
border: '1px solid var(--border-primary)',
|
||||
}}
|
||||
>
|
||||
{scope}
|
||||
</span>
|
||||
))}
|
||||
{!expanded && hidden > 0 && (
|
||||
<button onClick={() => toggleScopes(session.id)}
|
||||
className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium transition-colors hover:opacity-80"
|
||||
style={{ background: 'var(--bg-secondary)', color: 'var(--text-secondary)', border: '1px solid var(--border-primary)' }}>
|
||||
<button
|
||||
onClick={() => toggleScopes(session.id)}
|
||||
className="inline-flex items-center rounded px-1.5 py-0.5 text-xs font-medium transition-colors hover:opacity-80"
|
||||
style={{
|
||||
background: 'var(--bg-secondary)',
|
||||
color: 'var(--text-secondary)',
|
||||
border: '1px solid var(--border-primary)',
|
||||
}}
|
||||
>
|
||||
+{hidden} more
|
||||
</button>
|
||||
)}
|
||||
{expanded && hidden > 0 && (
|
||||
<button onClick={() => toggleScopes(session.id)}
|
||||
className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium transition-colors hover:opacity-80"
|
||||
style={{ background: 'var(--bg-secondary)', color: 'var(--text-secondary)', border: '1px solid var(--border-primary)' }}>
|
||||
<button
|
||||
onClick={() => toggleScopes(session.id)}
|
||||
className="inline-flex items-center rounded px-1.5 py-0.5 text-xs font-medium transition-colors hover:opacity-80"
|
||||
style={{
|
||||
background: 'var(--bg-secondary)',
|
||||
color: 'var(--text-secondary)',
|
||||
border: '1px solid var(--border-primary)',
|
||||
}}
|
||||
>
|
||||
show less
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-sm pt-0.5" style={{ color: 'var(--text-secondary)' }}>
|
||||
<User className="w-3.5 h-3.5 flex-shrink-0" />
|
||||
<div
|
||||
className="flex items-center gap-1.5 pt-0.5 text-sm"
|
||||
style={{ color: 'var(--text-secondary)' }}
|
||||
>
|
||||
<User className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
<span className="whitespace-nowrap">{session.username}</span>
|
||||
</div>
|
||||
<span className="text-xs whitespace-nowrap text-right pt-0.5" style={{ color: 'var(--text-tertiary)' }}>
|
||||
<span
|
||||
className="whitespace-nowrap pt-0.5 text-right text-xs"
|
||||
style={{ color: 'var(--text-tertiary)' }}
|
||||
>
|
||||
{new Date(session.created_at).toLocaleDateString(locale)}
|
||||
</span>
|
||||
<button onClick={() => setRevokeConfirmId(session.id)}
|
||||
className="p-1.5 rounded-lg transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20"
|
||||
style={{ color: 'var(--text-tertiary)' }} title={t('common.delete')}>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
<button
|
||||
onClick={() => setRevokeConfirmId(session.id)}
|
||||
className="rounded-lg p-1.5 transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20"
|
||||
style={{ color: 'var(--text-tertiary)' }}
|
||||
title={t('common.delete')}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
@@ -164,21 +215,34 @@ export default function AdminMcpTokensPanel() {
|
||||
|
||||
{/* MCP Tokens */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold mb-2" style={{ color: 'var(--text-secondary)' }}>{t('admin.mcpTokens.sectionTitle')}</h3>
|
||||
<div className="rounded-xl border overflow-hidden" style={{ borderColor: 'var(--border-primary)', background: 'var(--bg-card)' }}>
|
||||
<h3 className="mb-2 text-sm font-semibold" style={{ color: 'var(--text-secondary)' }}>
|
||||
{t('admin.mcpTokens.sectionTitle')}
|
||||
</h3>
|
||||
<div
|
||||
className="overflow-hidden rounded-xl border"
|
||||
style={{ borderColor: 'var(--border-primary)', background: 'var(--bg-card)' }}
|
||||
>
|
||||
{tokensLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-5 h-5 animate-spin" style={{ color: 'var(--text-tertiary)' }} />
|
||||
<Loader2 className="h-5 w-5 animate-spin" style={{ color: 'var(--text-tertiary)' }} />
|
||||
</div>
|
||||
) : tokens.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 gap-2">
|
||||
<Key className="w-8 h-8" style={{ color: 'var(--text-tertiary)' }} />
|
||||
<p className="text-sm" style={{ color: 'var(--text-tertiary)' }}>{t('admin.mcpTokens.empty')}</p>
|
||||
<div className="flex flex-col items-center justify-center gap-2 py-12">
|
||||
<Key className="h-8 w-8" style={{ color: 'var(--text-tertiary)' }} />
|
||||
<p className="text-sm" style={{ color: 'var(--text-tertiary)' }}>
|
||||
{t('admin.mcpTokens.empty')}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid grid-cols-[1fr_auto_auto_auto_auto] gap-x-4 px-4 py-2.5 text-xs font-medium border-b"
|
||||
style={{ color: 'var(--text-tertiary)', borderColor: 'var(--border-primary)', background: 'var(--bg-secondary)' }}>
|
||||
<div
|
||||
className="grid grid-cols-[1fr_auto_auto_auto_auto] gap-x-4 border-b px-4 py-2.5 text-xs font-medium"
|
||||
style={{
|
||||
color: 'var(--text-tertiary)',
|
||||
borderColor: 'var(--border-primary)',
|
||||
background: 'var(--bg-secondary)',
|
||||
}}
|
||||
>
|
||||
<span>{t('admin.mcpTokens.tokenName')}</span>
|
||||
<span>{t('admin.mcpTokens.owner')}</span>
|
||||
<span className="text-right">{t('admin.mcpTokens.created')}</span>
|
||||
@@ -186,27 +250,38 @@ export default function AdminMcpTokensPanel() {
|
||||
<span></span>
|
||||
</div>
|
||||
{tokens.map((token, i) => (
|
||||
<div key={token.id}
|
||||
<div
|
||||
key={token.id}
|
||||
className="grid grid-cols-[1fr_auto_auto_auto_auto] items-center gap-x-4 px-4 py-3"
|
||||
style={{ borderBottom: i < tokens.length - 1 ? '1px solid var(--border-primary)' : undefined }}>
|
||||
style={{ borderBottom: i < tokens.length - 1 ? '1px solid var(--border-primary)' : undefined }}
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium truncate" style={{ color: 'var(--text-primary)' }}>{token.name}</p>
|
||||
<p className="text-xs font-mono mt-0.5" style={{ color: 'var(--text-tertiary)' }}>{token.token_prefix}...</p>
|
||||
<p className="truncate text-sm font-medium" style={{ color: 'var(--text-primary)' }}>
|
||||
{token.name}
|
||||
</p>
|
||||
<p className="mt-0.5 font-mono text-xs" style={{ color: 'var(--text-tertiary)' }}>
|
||||
{token.token_prefix}...
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||
<User className="w-3.5 h-3.5 flex-shrink-0" />
|
||||
<User className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
<span className="whitespace-nowrap">{token.username}</span>
|
||||
</div>
|
||||
<span className="text-xs whitespace-nowrap text-right" style={{ color: 'var(--text-tertiary)' }}>
|
||||
<span className="whitespace-nowrap text-right text-xs" style={{ color: 'var(--text-tertiary)' }}>
|
||||
{new Date(token.created_at).toLocaleDateString(locale)}
|
||||
</span>
|
||||
<span className="text-xs whitespace-nowrap text-right" style={{ color: 'var(--text-tertiary)' }}>
|
||||
{token.last_used_at ? new Date(token.last_used_at).toLocaleDateString(locale) : t('admin.mcpTokens.never')}
|
||||
<span className="whitespace-nowrap text-right text-xs" style={{ color: 'var(--text-tertiary)' }}>
|
||||
{token.last_used_at
|
||||
? new Date(token.last_used_at).toLocaleDateString(locale)
|
||||
: t('admin.mcpTokens.never')}
|
||||
</span>
|
||||
<button onClick={() => setDeleteConfirmId(token.id)}
|
||||
className="p-1.5 rounded-lg transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20"
|
||||
style={{ color: 'var(--text-tertiary)' }} title={t('common.delete')}>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
<button
|
||||
onClick={() => setDeleteConfirmId(token.id)}
|
||||
className="rounded-lg p-1.5 transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20"
|
||||
style={{ color: 'var(--text-tertiary)' }}
|
||||
title={t('common.delete')}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
@@ -217,18 +292,32 @@ export default function AdminMcpTokensPanel() {
|
||||
|
||||
{/* Revoke OAuth session modal */}
|
||||
{revokeConfirmId !== null && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4" style={{ background: 'rgba(0,0,0,0.5)' }}
|
||||
onClick={e => { if (e.target === e.currentTarget) setRevokeConfirmId(null) }}>
|
||||
<div className="rounded-xl shadow-xl w-full max-w-sm p-6 space-y-4" style={{ background: 'var(--bg-card)' }}>
|
||||
<h3 className="text-base font-semibold" style={{ color: 'var(--text-primary)' }}>{t('admin.oauthSessions.revokeTitle')}</h3>
|
||||
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>{t('admin.oauthSessions.revokeMessage')}</p>
|
||||
<div className="flex gap-2 justify-end">
|
||||
<button onClick={() => setRevokeConfirmId(null)}
|
||||
className="px-4 py-2 rounded-lg text-sm border" style={{ borderColor: 'var(--border-primary)', color: 'var(--text-secondary)' }}>
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||
style={{ background: 'rgba(0,0,0,0.5)' }}
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) setRevokeConfirmId(null);
|
||||
}}
|
||||
>
|
||||
<div className="w-full max-w-sm space-y-4 rounded-xl p-6 shadow-xl" style={{ background: 'var(--bg-card)' }}>
|
||||
<h3 className="text-base font-semibold" style={{ color: 'var(--text-primary)' }}>
|
||||
{t('admin.oauthSessions.revokeTitle')}
|
||||
</h3>
|
||||
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||
{t('admin.oauthSessions.revokeMessage')}
|
||||
</p>
|
||||
<div className="flex justify-end gap-2">
|
||||
<button
|
||||
onClick={() => setRevokeConfirmId(null)}
|
||||
className="rounded-lg border px-4 py-2 text-sm"
|
||||
style={{ borderColor: 'var(--border-primary)', color: 'var(--text-secondary)' }}
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button onClick={() => handleRevoke(revokeConfirmId)}
|
||||
className="px-4 py-2 rounded-lg text-sm font-medium text-white bg-red-600 hover:bg-red-700">
|
||||
<button
|
||||
onClick={() => handleRevoke(revokeConfirmId)}
|
||||
className="rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700"
|
||||
>
|
||||
{t('common.delete')}
|
||||
</button>
|
||||
</div>
|
||||
@@ -238,18 +327,32 @@ export default function AdminMcpTokensPanel() {
|
||||
|
||||
{/* Delete MCP token modal */}
|
||||
{deleteConfirmId !== null && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4" style={{ background: 'rgba(0,0,0,0.5)' }}
|
||||
onClick={e => { if (e.target === e.currentTarget) setDeleteConfirmId(null) }}>
|
||||
<div className="rounded-xl shadow-xl w-full max-w-sm p-6 space-y-4" style={{ background: 'var(--bg-card)' }}>
|
||||
<h3 className="text-base font-semibold" style={{ color: 'var(--text-primary)' }}>{t('admin.mcpTokens.deleteTitle')}</h3>
|
||||
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>{t('admin.mcpTokens.deleteMessage')}</p>
|
||||
<div className="flex gap-2 justify-end">
|
||||
<button onClick={() => setDeleteConfirmId(null)}
|
||||
className="px-4 py-2 rounded-lg text-sm border" style={{ borderColor: 'var(--border-primary)', color: 'var(--text-secondary)' }}>
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||
style={{ background: 'rgba(0,0,0,0.5)' }}
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) setDeleteConfirmId(null);
|
||||
}}
|
||||
>
|
||||
<div className="w-full max-w-sm space-y-4 rounded-xl p-6 shadow-xl" style={{ background: 'var(--bg-card)' }}>
|
||||
<h3 className="text-base font-semibold" style={{ color: 'var(--text-primary)' }}>
|
||||
{t('admin.mcpTokens.deleteTitle')}
|
||||
</h3>
|
||||
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||
{t('admin.mcpTokens.deleteMessage')}
|
||||
</p>
|
||||
<div className="flex justify-end gap-2">
|
||||
<button
|
||||
onClick={() => setDeleteConfirmId(null)}
|
||||
className="rounded-lg border px-4 py-2 text-sm"
|
||||
style={{ borderColor: 'var(--border-primary)', color: 'var(--text-secondary)' }}
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button onClick={() => handleDelete(deleteConfirmId)}
|
||||
className="px-4 py-2 rounded-lg text-sm font-medium text-white bg-red-600 hover:bg-red-700">
|
||||
<button
|
||||
onClick={() => handleDelete(deleteConfirmId)}
|
||||
className="rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700"
|
||||
>
|
||||
{t('common.delete')}
|
||||
</button>
|
||||
</div>
|
||||
@@ -257,5 +360,5 @@ export default function AdminMcpTokensPanel() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user