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,
|
2026-01-26 13:12:17 +00:00
|
|
|
FileText,
|
2026-01-13 18:47:57 +00:00
|
|
|
} from 'lucide-react';
|
2026-01-26 13:12:17 +00:00
|
|
|
import { useToast } from '../../components/feedback/ToastProvider';
|
2026-01-07 09:31:02 +00:00
|
|
|
import { developerService } from '../../services/developerService';
|
|
|
|
|
import { logger } from '@/utils/logger';
|
2026-01-26 13:12:17 +00:00
|
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
|
|
|
import { SwaggerUIDoc } from './SwaggerUI';
|
2026-01-07 09:31:02 +00:00
|
|
|
|
|
|
|
|
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);
|
2026-01-26 13:12:17 +00:00
|
|
|
// Refresh list
|
|
|
|
|
const updated = await developerService.listKeys();
|
|
|
|
|
setKeys(updated);
|
2026-01-13 18:47:57 +00:00
|
|
|
addToast('API Key revoked', 'info');
|
|
|
|
|
}
|
2026-01-07 09:31:02 +00:00
|
|
|
};
|
|
|
|
|
|
2026-01-26 13:12:17 +00:00
|
|
|
const handleDelete = async (id: string) => {
|
|
|
|
|
if (confirm('Are you sure you want to delete this key permanently?')) {
|
|
|
|
|
await developerService.deleteKey(id);
|
|
|
|
|
setKeys(keys.filter((k) => k.id !== id));
|
|
|
|
|
addToast('API Key deleted', 'info');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
if (loading)
|
|
|
|
|
return (
|
aesthetic-improvements: align spacing to 8px grid (Action 11.2.1.3)
- Created automated script (scripts/align-8px-grid.py) to align all spacing to 8px grid
- Replaced non-8px-aligned spacing: gap-3/p-3/m-3 (12px) → gap-4/p-4/m-4 (16px), gap-5/p-5/m-5 (20px) → gap-6/p-6/m-6 (24px), gap-10/p-10/m-10 (40px) → gap-12/p-12/m-12 (48px), gap-20/p-20/m-20 (80px) → gap-24/p-24/m-24 (96px)
- Preserved: 4px values (gap-1, p-1, m-1) as they may be intentional fine-tuning, responsive breakpoints (sm:, md:, lg:), test files, documentation
- Modified files across all components to ensure consistent 8px grid alignment
- Action 11.2.1.3: Align all elements to 8px grid - COMPLETE
2026-01-16 10:50:46 +00:00
|
|
|
<div className="flex justify-center py-24">
|
aesthetic-improvements: reduce decorative cyan across multiple component categories (80/20 rule, batch 11)
- Social: FeedView, ConnectionsView, GroupsView, ExploreView, GroupDetailView loading spinners and decorative text, CreatePostModal decorative select text and hashtag links, PostCard decorative tag links and waveform bars and view comments link, CreateGroupModal decorative icon (9 instances)
- Settings: DataExportModal decorative icon, LoginHistory decorative IP text, AppearanceSettingsView decorative icon and selected theme checkmark, BackupsView decorative icon, CloudIntegrationView decorative icon, AccessibilitySettingsView decorative icon, SecuritySettings decorative icon, PasskeyModal decorative icon and loading spinner (8 instances)
- Studio: ProjectsManager loading spinner and progress percentage text, CloudFileBrowser decorative music icons, AIToolsView decorative music icon, ConnectivityView decorative icon, CreateProjectModal decorative icon, CloudSettingsView decorative icon (6 instances)
- Admin: AdminDashboardView loading spinner and decorative chart bars and icon, AdminSettingsView decorative icon, AdminModerationView loading spinner, AdminUsersView loading spinner (5 instances)
- Inventory: InventoryView loading spinner, EquipmentCard decorative price icon, EquipmentDetailView loading spinner and decorative icons and price text, AddEquipmentView decorative icon (5 instances)
- Seller: CreateProductView decorative icon, SellerDashboardView loading spinner and decorative icon and sales text (3 instances)
- Live: LiveStreamDetailView decorative streamer name text (1 instance)
- Developer: DeveloperDashboardView loading spinner, WebhooksView decorative icon (2 instances)
- Upload: BulkUploadModal decorative icon, FilePreviewCard decorative audio file icon, MetadataForm decorative button text, CoverArtUploadModal decorative icon, LyricsEditorModal decorative icon (5 instances)
- Notifications: NotificationItem decorative follow icon and mark as read button, NotificationBell decorative mark all read link (3 instances)
- Total: ~46 files, ~46 instances replaced
- Preserved: Active/selected states (CloudFileBrowser selected files checkmarks, CreatePostModal post type active state, GroupCard/GroupDetailView public/private badges - semantic indicators, DataExportModal checkbox accents - focus/interaction, AppearanceSettingsView selected theme - active state, PasskeyModal checkbox accent - focus/interaction, LyricsEditorModal checkbox accent - focus/interaction, FileUploadZone drag active state - active state, EquipmentDetailView support link - functional link, FlashSaleModal link - functional link, EquipmentDetailView image indicator dots - active state), primary actions, design system variants
- Action 11.3.1.3 in progress (eleventh batch: social, settings, studio, admin, inventory, seller, live, developer, upload, notifications components)
2026-01-16 10:26:33 +00:00
|
|
|
<Loader2 className="w-10 h-10 text-kodo-steel animate-spin" />
|
2026-01-13 18:47:57 +00:00
|
|
|
</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>
|
aesthetic-improvements: align spacing to 8px grid (Action 11.2.1.3)
- Created automated script (scripts/align-8px-grid.py) to align all spacing to 8px grid
- Replaced non-8px-aligned spacing: gap-3/p-3/m-3 (12px) → gap-4/p-4/m-4 (16px), gap-5/p-5/m-5 (20px) → gap-6/p-6/m-6 (24px), gap-10/p-10/m-10 (40px) → gap-12/p-12/m-12 (48px), gap-20/p-20/m-20 (80px) → gap-24/p-24/m-24 (96px)
- Preserved: 4px values (gap-1, p-1, m-1) as they may be intentional fine-tuning, responsive breakpoints (sm:, md:, lg:), test files, documentation
- Modified files across all components to ensure consistent 8px grid alignment
- Action 11.2.1.3: Align all elements to 8px grid - COMPLETE
2026-01-16 10:50:46 +00:00
|
|
|
<div className="flex gap-4">
|
2026-01-13 18:47:57 +00:00
|
|
|
<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-26 13:12:17 +00:00
|
|
|
<Tabs defaultValue="overview" className="w-full">
|
|
|
|
|
<TabsList className="grid w-full grid-cols-2 mb-8">
|
|
|
|
|
<TabsTrigger value="overview" className="flex items-center gap-2">
|
|
|
|
|
<Activity className="w-4 h-4" />
|
|
|
|
|
Overview & Keys
|
|
|
|
|
</TabsTrigger>
|
|
|
|
|
<TabsTrigger value="docs" className="flex items-center gap-2">
|
|
|
|
|
<FileText className="w-4 h-4" />
|
|
|
|
|
API Documentation
|
|
|
|
|
</TabsTrigger>
|
|
|
|
|
</TabsList>
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2026-01-26 13:12:17 +00:00
|
|
|
<TabsContent value="overview">
|
|
|
|
|
{/* Stats */}
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
|
|
|
|
<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>
|
|
|
|
|
<tr className="text-xs text-kodo-content-dim uppercase border-b border-kodo-steel/50">
|
|
|
|
|
<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"
|
2026-01-13 18:47:57 +00:00
|
|
|
>
|
2026-01-26 13:12:17 +00:00
|
|
|
<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>
|
|
|
|
|
<td className="py-4 text-kodo-content-dim">{key.created}</td>
|
|
|
|
|
<td className="py-4 text-kodo-text-main">{key.lastUsed}</td>
|
|
|
|
|
<td className="py-4 text-right pr-4 flex justify-end gap-2">
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
className="h-8 w-8 text-kodo-content-dim hover:text-white"
|
|
|
|
|
onClick={() => addToast('Full key hidden for security')}
|
|
|
|
|
title="View Key"
|
|
|
|
|
>
|
|
|
|
|
<Eye className="w-4 h-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
{key.status === 'active' ? (
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
className="h-8 w-8 text-kodo-red hover:bg-kodo-red/10"
|
|
|
|
|
onClick={() => handleRevoke(key.id)}
|
|
|
|
|
title="Revoke Key"
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="w-4 h-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
) : (
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
className="h-8 w-8 text-kodo-content-dim hover:text-white hover:bg-white/10"
|
|
|
|
|
onClick={() => handleDelete(key.id)}
|
|
|
|
|
title="Delete Permanently"
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="w-4 h-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
))}
|
|
|
|
|
{keys.length === 0 && (
|
|
|
|
|
<tr>
|
|
|
|
|
<td colSpan={5} className="py-8 text-center text-kodo-content-dim">
|
|
|
|
|
No active API keys. Create one to get started.
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
)}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
</TabsContent>
|
|
|
|
|
|
|
|
|
|
<TabsContent value="docs">
|
|
|
|
|
<Card variant="default" className="p-0 overflow-hidden bg-black/80">
|
|
|
|
|
{/* Use iframe mode for better compatibility if JSON fetching has issues CORS/Auth */}
|
|
|
|
|
<SwaggerUIDoc useIframe={false} />
|
|
|
|
|
</Card>
|
|
|
|
|
</TabsContent>
|
|
|
|
|
</Tabs>
|
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>
|
|
|
|
|
);
|
|
|
|
|
};
|