2026-01-07 09:31:02 +00:00
|
|
|
import React, { useState, useEffect } from 'react';
|
|
|
|
|
import { Card } from '../ui/card';
|
|
|
|
|
import { Button } from '../ui/button';
|
|
|
|
|
import { StatCard } from '../dashboard/StatCard';
|
|
|
|
|
import { CreateAPIKeyModal } from './modals/CreateAPIKeyModal';
|
2026-01-13 18:47:57 +00:00
|
|
|
import {
|
|
|
|
|
Key,
|
|
|
|
|
Activity,
|
|
|
|
|
Globe,
|
|
|
|
|
Plus,
|
|
|
|
|
Trash2,
|
|
|
|
|
Eye,
|
|
|
|
|
ExternalLink,
|
|
|
|
|
Loader2,
|
|
|
|
|
} from 'lucide-react';
|
2026-01-07 09:31:02 +00:00
|
|
|
import { useToast } from '../../context/ToastContext';
|
|
|
|
|
import { developerService } from '../../services/developerService';
|
|
|
|
|
import { logger } from '@/utils/logger';
|
|
|
|
|
|
|
|
|
|
interface ApiKey {
|
2026-01-13 18:47:57 +00:00
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
prefix: string;
|
|
|
|
|
created: string;
|
|
|
|
|
lastUsed: string;
|
|
|
|
|
status: 'active' | 'revoked';
|
2026-01-07 09:31:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const DeveloperDashboardView: React.FC = () => {
|
|
|
|
|
const { addToast } = useToast();
|
|
|
|
|
const [keys, setKeys] = useState<ApiKey[]>([]);
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
const [stats, setStats] = useState<any>({});
|
|
|
|
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-01-13 18:47:57 +00:00
|
|
|
const fetchData = async () => {
|
|
|
|
|
setLoading(true);
|
2026-01-07 09:31:02 +00:00
|
|
|
try {
|
2026-01-13 18:47:57 +00:00
|
|
|
const [keysData, statsData] = await Promise.all([
|
|
|
|
|
developerService.listKeys(),
|
|
|
|
|
developerService.getStats(),
|
|
|
|
|
]);
|
|
|
|
|
setKeys(keysData);
|
|
|
|
|
setStats(statsData);
|
2026-01-07 09:31:02 +00:00
|
|
|
} catch (e) {
|
2026-01-13 18:47:57 +00:00
|
|
|
logger.error('Error loading developer dashboard data', {
|
|
|
|
|
error: e instanceof Error ? e.message : String(e),
|
|
|
|
|
stack: e instanceof Error ? e.stack : undefined,
|
|
|
|
|
});
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
2026-01-07 09:31:02 +00:00
|
|
|
}
|
2026-01-13 18:47:57 +00:00
|
|
|
};
|
|
|
|
|
fetchData();
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const handleCreateKey = async (data: { name: string; scopes: string[] }) => {
|
|
|
|
|
try {
|
|
|
|
|
const newKey = await developerService.createKey(data);
|
|
|
|
|
setKeys([newKey, ...keys]);
|
|
|
|
|
addToast('API Key created successfully', 'success');
|
|
|
|
|
} catch (e) {
|
|
|
|
|
addToast('Failed to create API key', 'error');
|
|
|
|
|
}
|
2026-01-07 09:31:02 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleRevoke = async (id: string) => {
|
2026-01-13 18:47:57 +00:00
|
|
|
if (confirm('Are you sure you want to revoke this key?')) {
|
|
|
|
|
await developerService.revokeKey(id);
|
|
|
|
|
setKeys(keys.filter((k) => k.id !== id));
|
|
|
|
|
addToast('API Key revoked', 'info');
|
|
|
|
|
}
|
2026-01-07 09:31:02 +00:00
|
|
|
};
|
|
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
if (loading)
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex justify-center py-20">
|
|
|
|
|
<Loader2 className="w-10 h-10 text-kodo-cyan animate-spin" />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2026-01-07 09:31:02 +00:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-8 animate-fadeIn pb-20">
|
2026-01-13 18:47:57 +00:00
|
|
|
{/* Header */}
|
|
|
|
|
<div className="flex flex-col md:flex-row justify-between items-end gap-4 border-b border-kodo-steel/50 pb-6">
|
|
|
|
|
<div>
|
|
|
|
|
<h1 className="text-3xl font-display font-bold text-white mb-2">
|
|
|
|
|
DEVELOPER PORTAL
|
|
|
|
|
</h1>
|
2026-01-16 00:59:31 +00:00
|
|
|
<p className="text-kodo-content-dim font-mono text-sm">
|
2026-01-13 18:47:57 +00:00
|
|
|
Build on top of the Veza Platform.
|
|
|
|
|
</p>
|
2026-01-07 09:31:02 +00:00
|
|
|
</div>
|
2026-01-13 18:47:57 +00:00
|
|
|
<div className="flex gap-3">
|
|
|
|
|
<Button
|
|
|
|
|
variant="secondary"
|
|
|
|
|
icon={<ExternalLink className="w-4 h-4" />}
|
|
|
|
|
onClick={() => window.open('https://docs.veza.io', '_blank')}
|
|
|
|
|
>
|
|
|
|
|
Documentation
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="primary"
|
|
|
|
|
icon={<Plus className="w-4 h-4" />}
|
|
|
|
|
onClick={() => setShowCreateModal(true)}
|
|
|
|
|
>
|
|
|
|
|
Create API Key
|
|
|
|
|
</Button>
|
2026-01-07 09:31:02 +00:00
|
|
|
</div>
|
2026-01-13 18:47:57 +00:00
|
|
|
</div>
|
2026-01-07 09:31:02 +00:00
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
{/* Stats */}
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
|
|
|
<StatCard
|
|
|
|
|
label="API Requests (24h)"
|
|
|
|
|
value={stats.requests_24h?.toLocaleString() || 0}
|
|
|
|
|
icon={<Activity className="w-5 h-5" />}
|
|
|
|
|
trend={5.2}
|
|
|
|
|
color="cyan"
|
|
|
|
|
/>
|
|
|
|
|
<StatCard
|
|
|
|
|
label="Avg Latency"
|
|
|
|
|
value={`${stats.avg_latency || 0}ms`}
|
|
|
|
|
icon={<Globe className="w-5 h-5" />}
|
|
|
|
|
trend={-12}
|
|
|
|
|
color="lime"
|
|
|
|
|
/>
|
|
|
|
|
<StatCard
|
|
|
|
|
label="Active Keys"
|
|
|
|
|
value={keys.length}
|
|
|
|
|
icon={<Key className="w-5 h-5" />}
|
|
|
|
|
color="gold"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* API Keys List */}
|
|
|
|
|
<Card variant="default">
|
|
|
|
|
<h3 className="font-bold text-white mb-6">Active API Keys</h3>
|
|
|
|
|
<div className="overflow-x-auto">
|
|
|
|
|
<table className="w-full text-left">
|
|
|
|
|
<thead>
|
2026-01-16 00:59:31 +00:00
|
|
|
<tr className="text-xs text-kodo-content-dim uppercase border-b border-kodo-steel/50">
|
2026-01-13 18:47:57 +00:00
|
|
|
<th className="pb-3 pl-4">Name</th>
|
|
|
|
|
<th className="pb-3">Key Prefix</th>
|
|
|
|
|
<th className="pb-3">Created</th>
|
|
|
|
|
<th className="pb-3">Last Used</th>
|
|
|
|
|
<th className="pb-3 text-right pr-4">Actions</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody className="text-sm">
|
|
|
|
|
{keys.map((key) => (
|
|
|
|
|
<tr
|
|
|
|
|
key={key.id}
|
|
|
|
|
className="border-b border-kodo-steel/20 hover:bg-white/5 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<td className="py-4 pl-4 font-bold text-white">{key.name}</td>
|
|
|
|
|
<td className="py-4 font-mono text-kodo-gold">
|
|
|
|
|
{key.prefix}
|
|
|
|
|
</td>
|
2026-01-16 00:59:31 +00:00
|
|
|
<td className="py-4 text-kodo-content-dim">{key.created}</td>
|
|
|
|
|
<td className="py-4 text-kodo-text-main">{key.lastUsed}</td>
|
2026-01-13 18:47:57 +00:00
|
|
|
<td className="py-4 text-right pr-4 flex justify-end gap-2">
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
2026-01-16 00:59:31 +00:00
|
|
|
className="h-8 w-8 text-kodo-content-dim hover:text-white"
|
2026-01-13 18:47:57 +00:00
|
|
|
onClick={() => addToast('Full key hidden for security')}
|
|
|
|
|
>
|
|
|
|
|
<Eye className="w-4 h-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
className="h-8 w-8 text-kodo-red hover:bg-kodo-red/10"
|
|
|
|
|
onClick={() => handleRevoke(key.id)}
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="w-4 h-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
))}
|
|
|
|
|
{keys.length === 0 && (
|
|
|
|
|
<tr>
|
2026-01-16 00:59:31 +00:00
|
|
|
<td colSpan={5} className="py-8 text-center text-kodo-content-dim">
|
2026-01-13 18:47:57 +00:00
|
|
|
No active API keys. Create one to get started.
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
)}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
2026-01-07 09:31:02 +00:00
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
{showCreateModal && (
|
|
|
|
|
<CreateAPIKeyModal
|
|
|
|
|
onClose={() => setShowCreateModal(false)}
|
|
|
|
|
onCreate={handleCreateKey}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2026-01-07 09:31:02 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|