chore(frontend): remove or simplify ghost features (Developer Dashboard, Education/Gamification/Studio)
This commit is contained in:
parent
7962c8f1b9
commit
1b2079dcdd
2 changed files with 31 additions and 305 deletions
|
|
@ -1,322 +1,50 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '../ui/card';
|
||||
/**
|
||||
* Developer Dashboard — simplified (P2.3)
|
||||
* API Keys management has no backend yet. This page shows a "Coming soon" message
|
||||
* and links to the API documentation (Swagger). Webhooks are available at /webhooks.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Card, CardContent } from '../ui/card';
|
||||
import { Button } from '../ui/button';
|
||||
import { StatCard } from '../dashboard/StatCard';
|
||||
import { CreateAPIKeyModal } from './modals/CreateAPIKeyModal';
|
||||
import {
|
||||
Key,
|
||||
Activity,
|
||||
Globe,
|
||||
Plus,
|
||||
Trash2,
|
||||
Eye,
|
||||
ExternalLink,
|
||||
FileText,
|
||||
Copy,
|
||||
Check,
|
||||
} from 'lucide-react';
|
||||
import { useToast } from '../../components/feedback/ToastProvider';
|
||||
import { developerService } from '../../services/developerService';
|
||||
import { logger } from '@/utils/logger';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { FileText, ExternalLink } from 'lucide-react';
|
||||
import { SwaggerUIDoc } from './SwaggerUI';
|
||||
import { useCopyToClipboard } from '@/hooks/useCopyToClipboard';
|
||||
import { DeveloperDashboardViewSkeleton } from './DeveloperDashboardViewSkeleton';
|
||||
import { EmptyState } from '../ui/empty-state';
|
||||
import { ErrorDisplay } from '../ui/ErrorDisplay';
|
||||
|
||||
interface ApiKey {
|
||||
id: string;
|
||||
name: string;
|
||||
prefix: string;
|
||||
created: string;
|
||||
lastUsed: string;
|
||||
status: 'active' | 'revoked';
|
||||
}
|
||||
|
||||
export const DeveloperDashboardView: React.FC = () => {
|
||||
const { addToast } = useToast();
|
||||
const { copy } = useCopyToClipboard();
|
||||
const [copiedKeyId, setCopiedKeyId] = useState<string | null>(null);
|
||||
const [keys, setKeys] = useState<ApiKey[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [stats, setStats] = useState<any>({});
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
|
||||
const handleCopyKey = async (key: ApiKey) => {
|
||||
await copy(key.prefix);
|
||||
setCopiedKeyId(key.id);
|
||||
addToast('Key prefix copied to clipboard', 'success');
|
||||
setTimeout(() => setCopiedKeyId(null), 2000);
|
||||
};
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const [keysData, statsData] = await Promise.all([
|
||||
developerService.listKeys(),
|
||||
developerService.getStats(),
|
||||
]);
|
||||
setKeys(keysData);
|
||||
setStats(statsData);
|
||||
} catch (e) {
|
||||
const err = e instanceof Error ? e : new Error(String(e));
|
||||
setError(err);
|
||||
logger.error('Error loading developer dashboard data', {
|
||||
error: err.message,
|
||||
stack: err.stack,
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const handleCreateKey = async (data: {
|
||||
name: string;
|
||||
scopes: string[];
|
||||
}): Promise<{ key?: string; id: string; name: string; prefix: string }> => {
|
||||
try {
|
||||
const newKey = await developerService.createKey(data);
|
||||
setKeys([newKey, ...keys]);
|
||||
addToast('API Key created successfully', 'success');
|
||||
return newKey;
|
||||
} catch (e) {
|
||||
addToast('Failed to create API key', 'error');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const handleRevoke = async (id: string) => {
|
||||
if (confirm('Are you sure you want to revoke this key?')) {
|
||||
await developerService.revokeKey(id);
|
||||
// Refresh list
|
||||
const updated = await developerService.listKeys();
|
||||
setKeys(updated);
|
||||
addToast('API Key revoked', 'info');
|
||||
}
|
||||
};
|
||||
|
||||
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');
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <DeveloperDashboardViewSkeleton />;
|
||||
if (error)
|
||||
return (
|
||||
<div className="py-12">
|
||||
<ErrorDisplay error={error} variant="card" onRetry={fetchData} />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-fadeIn pb-20">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col md:flex-row justify-between items-end gap-4 border-b border-border/50 pb-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-heading font-bold text-foreground mb-2">
|
||||
DEVELOPER PORTAL
|
||||
</h1>
|
||||
<p className="text-muted-foreground font-mono text-sm">
|
||||
Build on top of the Veza Platform.
|
||||
API & Webhooks — Coming soon. Explore the API documentation below.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<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>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon={<ExternalLink className="w-4 h-4" />}
|
||||
onClick={() => window.open('/webhooks', '_self')}
|
||||
>
|
||||
Manage Webhooks
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<Card variant="default">
|
||||
<CardContent className="pt-6">
|
||||
<EmptyState
|
||||
icon={<FileText className="w-12 h-12 text-muted-foreground" />}
|
||||
title="API Keys — Coming soon"
|
||||
description="API key management will be available in a future release. In the meantime, use the interactive API documentation below to explore endpoints. Webhooks can be configured from the Webhooks page."
|
||||
variant="card"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<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">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4 border-b border-border/30">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Key className="w-4 h-4 text-warning" /> Active API Keys
|
||||
</CardTitle>
|
||||
<span className="text-xs text-muted-foreground font-mono">
|
||||
{keys.filter(k => k.status === 'active').length} active
|
||||
</span>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-4">
|
||||
{keys.length === 0 ? (
|
||||
<EmptyState
|
||||
icon={<Key className="w-12 h-12 text-muted-foreground" />}
|
||||
title="No API keys yet"
|
||||
description="Create your first API key to start building on the Veza Platform."
|
||||
action={{
|
||||
label: 'Create API Key',
|
||||
onClick: () => setShowCreateModal(true),
|
||||
variant: 'default',
|
||||
}}
|
||||
variant="card"
|
||||
/>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left">
|
||||
<thead>
|
||||
<tr className="text-xs text-muted-foreground uppercase border-b border-border/50 tracking-wider">
|
||||
<th className="pb-3 pl-4">Name</th>
|
||||
<th className="pb-3">Key Prefix</th>
|
||||
<th className="pb-3">Status</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-border/20 hover:bg-foreground/5 transition-colors duration-[var(--duration-fast)] group"
|
||||
>
|
||||
<td className="py-4 pl-4 font-bold text-foreground">{key.name}</td>
|
||||
<td className="py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="font-mono text-warning bg-warning/10 px-2 py-0.5 rounded text-xs">
|
||||
{key.prefix}
|
||||
</code>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleCopyKey(key)}
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity duration-[var(--duration-fast)] text-muted-foreground hover:text-foreground"
|
||||
title="Copy key prefix"
|
||||
>
|
||||
{copiedKeyId === key.id ? (
|
||||
<Check className="w-3.5 h-3.5 text-success" />
|
||||
) : (
|
||||
<Copy className="w-3.5 h-3.5" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-4">
|
||||
<span className={`inline-flex items-center gap-1.5 text-xs font-bold uppercase px-2 py-0.5 rounded ${
|
||||
key.status === 'active'
|
||||
? 'bg-success/15 text-success'
|
||||
: 'bg-destructive/15 text-destructive'
|
||||
}`}>
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${
|
||||
key.status === 'active' ? 'bg-success animate-pulse' : 'bg-destructive'
|
||||
}`} />
|
||||
{key.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-4 text-muted-foreground">{key.created}</td>
|
||||
<td className="py-4 text-foreground">{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-muted-foreground hover:text-foreground"
|
||||
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-destructive hover:bg-destructive/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-muted-foreground hover:text-foreground hover:bg-foreground/10"
|
||||
onClick={() => handleDelete(key.id)}
|
||||
title="Delete Permanently"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="docs">
|
||||
<Card variant="default" className="p-0 overflow-hidden bg-background/80">
|
||||
{/* Use iframe mode for better compatibility if JSON fetching has issues CORS/Auth */}
|
||||
<SwaggerUIDoc useIframe={false} />
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{showCreateModal && (
|
||||
<CreateAPIKeyModal
|
||||
onClose={() => setShowCreateModal(false)}
|
||||
onCreate={handleCreateKey}
|
||||
/>
|
||||
)}
|
||||
<Card variant="default" className="p-0 overflow-hidden bg-background/80">
|
||||
<SwaggerUIDoc useIframe={false} />
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -91,12 +91,10 @@ export function getProtectedRoutes(): RouteEntry[] {
|
|||
{ path: '/playlists/*', element: wrapProtected(<LazyPlaylistRoutes />) },
|
||||
{ path: '/search', element: wrapProtected(<LazySearch />) },
|
||||
{ path: '/notifications', element: wrapProtected(<LazyNotifications />) },
|
||||
{ path: '/analytics', element: wrapProtected(<LazyAnalytics onNavigateTrack={() => {}} />) },
|
||||
{ path: '/analytics', element: wrapProtected(<LazyAnalytics />) },
|
||||
{ path: '/webhooks', element: wrapProtected(<LazyWebhooks />) },
|
||||
{ path: '/admin', element: wrapProtected(<LazyAdminDashboard />) },
|
||||
{ path: '/social', element: wrapProtected(<LazySocial onViewProfile={() => {}} />) },
|
||||
// Queue, Developer: connected to backend or client storage
|
||||
// Ghost features (Education, Gamification, Studio) — FEATURES.EDUCATION/GAMIFICATION/STUDIO = false, no routes
|
||||
{ path: '/social', element: wrapProtected(<LazySocial />) },
|
||||
{ path: '/queue', element: wrapProtected(<LazyQueue />) },
|
||||
{ path: '/developer', element: wrapProtected(<LazyDeveloper />) },
|
||||
// Gear: connected to backend inventory API
|
||||
|
|
|
|||
Loading…
Reference in a new issue