1. Added Claude Code sub-agents 2. Added PRD tool to MCP Server 3. Added MCP Server UI to Dev Tools 4. Improved MCP Server Database Tool 5. Updated dependencies
385 lines
13 KiB
TypeScript
385 lines
13 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
|
|
import {
|
|
BellIcon,
|
|
CheckCircleIcon,
|
|
CircleDollarSignIcon,
|
|
CreditCardIcon,
|
|
DatabaseIcon,
|
|
ListIcon,
|
|
ShieldIcon,
|
|
TagIcon,
|
|
} from 'lucide-react';
|
|
|
|
import { Badge } from '@kit/ui/badge';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@kit/ui/dialog';
|
|
import { Separator } from '@kit/ui/separator';
|
|
|
|
interface DatabaseEnum {
|
|
name: string;
|
|
values: string[];
|
|
sourceFile: string;
|
|
category: string;
|
|
description: string;
|
|
}
|
|
|
|
interface EnumBrowserProps {
|
|
searchTerm: string;
|
|
enums: DatabaseEnum[];
|
|
}
|
|
|
|
const categoryColors: Record<string, string> = {
|
|
'Security & Permissions': 'bg-red-100 text-red-800',
|
|
'Billing & Payments': 'bg-green-100 text-green-800',
|
|
Notifications: 'bg-blue-100 text-blue-800',
|
|
};
|
|
|
|
const categoryIcons: Record<string, React.ComponentType<any>> = {
|
|
'Security & Permissions': ShieldIcon,
|
|
'Billing & Payments': CreditCardIcon,
|
|
Notifications: BellIcon,
|
|
};
|
|
|
|
const valueColors: Record<string, string> = {
|
|
// Permission colors
|
|
'roles.manage': 'bg-purple-100 text-purple-800',
|
|
'billing.manage': 'bg-green-100 text-green-800',
|
|
'settings.manage': 'bg-blue-100 text-blue-800',
|
|
'members.manage': 'bg-orange-100 text-orange-800',
|
|
'invites.manage': 'bg-teal-100 text-teal-800',
|
|
|
|
// Payment provider colors
|
|
stripe: 'bg-purple-100 text-purple-800',
|
|
'lemon-squeezy': 'bg-yellow-100 text-yellow-800',
|
|
paddle: 'bg-blue-100 text-blue-800',
|
|
|
|
// Notification channel colors
|
|
in_app: 'bg-blue-100 text-blue-800',
|
|
email: 'bg-green-100 text-green-800',
|
|
|
|
// Notification type colors
|
|
info: 'bg-blue-100 text-blue-800',
|
|
warning: 'bg-yellow-100 text-yellow-800',
|
|
error: 'bg-red-100 text-red-800',
|
|
|
|
// Payment status colors
|
|
pending: 'bg-yellow-100 text-yellow-800',
|
|
succeeded: 'bg-green-100 text-green-800',
|
|
failed: 'bg-red-100 text-red-800',
|
|
|
|
// Subscription item type colors
|
|
flat: 'bg-gray-100 text-gray-800',
|
|
per_seat: 'bg-orange-100 text-orange-800',
|
|
metered: 'bg-purple-100 text-purple-800',
|
|
|
|
// Subscription status colors
|
|
active: 'bg-green-100 text-green-800',
|
|
trialing: 'bg-blue-100 text-blue-800',
|
|
past_due: 'bg-yellow-100 text-yellow-800',
|
|
canceled: 'bg-red-100 text-red-800',
|
|
unpaid: 'bg-red-100 text-red-800',
|
|
incomplete: 'bg-gray-100 text-gray-800',
|
|
incomplete_expired: 'bg-gray-100 text-gray-800',
|
|
paused: 'bg-gray-100 text-gray-800',
|
|
};
|
|
|
|
export function EnumBrowser({ searchTerm, enums }: EnumBrowserProps) {
|
|
const [selectedEnum, setSelectedEnum] = useState<string | null>(null);
|
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
|
|
// Filter enums based on search term
|
|
const filteredEnums = enums.filter(
|
|
(enumItem) =>
|
|
enumItem.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
enumItem.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
enumItem.category.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
enumItem.values.some((value) =>
|
|
value.toLowerCase().includes(searchTerm.toLowerCase()),
|
|
),
|
|
);
|
|
|
|
// Group enums by category
|
|
const enumsByCategory = filteredEnums.reduce(
|
|
(acc, enumItem) => {
|
|
if (!acc[enumItem.category]) {
|
|
acc[enumItem.category] = [];
|
|
}
|
|
acc[enumItem.category].push(enumItem);
|
|
return acc;
|
|
},
|
|
{} as Record<string, DatabaseEnum[]>,
|
|
);
|
|
|
|
const handleEnumClick = (enumName: string) => {
|
|
setSelectedEnum(enumName);
|
|
setIsDialogOpen(true);
|
|
};
|
|
|
|
const closeDialog = () => {
|
|
setIsDialogOpen(false);
|
|
setSelectedEnum(null);
|
|
};
|
|
|
|
const getValueBadgeColor = (value: string): string => {
|
|
return valueColors[value] || 'bg-gray-100 text-gray-800';
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Summary */}
|
|
<div className="grid gap-4 md:grid-cols-3">
|
|
<Card>
|
|
<CardContent className="flex items-center justify-between p-6">
|
|
<div>
|
|
<p className="text-muted-foreground text-sm font-medium">
|
|
Total Enums
|
|
</p>
|
|
<p className="text-2xl font-bold">{filteredEnums.length}</p>
|
|
</div>
|
|
<ListIcon className="text-muted-foreground h-8 w-8" />
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardContent className="flex items-center justify-between p-6">
|
|
<div>
|
|
<p className="text-muted-foreground text-sm font-medium">
|
|
Categories
|
|
</p>
|
|
<p className="text-2xl font-bold">
|
|
{Object.keys(enumsByCategory).length}
|
|
</p>
|
|
</div>
|
|
<TagIcon className="text-muted-foreground h-8 w-8" />
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardContent className="flex items-center justify-between p-6">
|
|
<div>
|
|
<p className="text-muted-foreground text-sm font-medium">
|
|
Total Values
|
|
</p>
|
|
<p className="text-2xl font-bold">
|
|
{filteredEnums.reduce(
|
|
(acc, enumItem) => acc + enumItem.values.length,
|
|
0,
|
|
)}
|
|
</p>
|
|
</div>
|
|
<DatabaseIcon className="text-muted-foreground h-8 w-8" />
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Enums by Category */}
|
|
{Object.entries(enumsByCategory).map(([category, categoryEnums]) => {
|
|
const IconComponent = categoryIcons[category] || ListIcon;
|
|
|
|
return (
|
|
<div key={category} className="space-y-4">
|
|
<div className="flex items-center gap-2">
|
|
<Badge
|
|
className={
|
|
categoryColors[category] || 'bg-gray-100 text-gray-800'
|
|
}
|
|
>
|
|
<IconComponent className="mr-1 h-3 w-3" />
|
|
{category}
|
|
</Badge>
|
|
<span className="text-muted-foreground text-sm">
|
|
{categoryEnums.length} enum
|
|
{categoryEnums.length !== 1 ? 's' : ''}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="grid gap-4 md:grid-cols-1 lg:grid-cols-2">
|
|
{categoryEnums.map((enumItem) => (
|
|
<Card
|
|
key={enumItem.name}
|
|
className="cursor-pointer transition-all hover:shadow-md"
|
|
onClick={() => handleEnumClick(enumItem.name)}
|
|
>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="flex items-start justify-between gap-2 text-sm">
|
|
<span className="flex items-center gap-2">
|
|
<ListIcon className="text-muted-foreground h-4 w-4" />
|
|
<span className="font-mono">{enumItem.name}</span>
|
|
</span>
|
|
<Badge variant="outline" className="text-xs">
|
|
{enumItem.values.length} value
|
|
{enumItem.values.length !== 1 ? 's' : ''}
|
|
</Badge>
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3">
|
|
<p className="text-muted-foreground line-clamp-2 text-sm">
|
|
{enumItem.description}
|
|
</p>
|
|
|
|
<div className="flex flex-wrap gap-1">
|
|
{enumItem.values.slice(0, 4).map((value) => (
|
|
<Badge
|
|
key={value}
|
|
variant="outline"
|
|
className={`text-xs ${getValueBadgeColor(value)}`}
|
|
>
|
|
{value}
|
|
</Badge>
|
|
))}
|
|
{enumItem.values.length > 4 && (
|
|
<Badge variant="secondary" className="text-xs">
|
|
+{enumItem.values.length - 4} more
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-muted-foreground font-mono text-xs">
|
|
{enumItem.sourceFile}
|
|
</span>
|
|
<Badge variant="secondary" className="text-xs">
|
|
Click for details
|
|
</Badge>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
|
|
{/* Enum Details Dialog */}
|
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
|
<DialogContent className="max-h-[80vh] max-w-4xl overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-2">
|
|
<ListIcon className="h-5 w-5" />
|
|
Enum Details: {selectedEnum}
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="space-y-4">
|
|
{(() => {
|
|
const enumItem = enums.find((e) => e.name === selectedEnum);
|
|
if (!enumItem) return null;
|
|
|
|
return (
|
|
<>
|
|
<div>
|
|
<h4 className="mb-2 font-medium">Description</h4>
|
|
<p className="text-muted-foreground text-sm">
|
|
{enumItem.description}
|
|
</p>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<div>
|
|
<h4 className="mb-3 font-medium">
|
|
Values ({enumItem.values.length})
|
|
</h4>
|
|
<div className="grid gap-2 md:grid-cols-2 lg:grid-cols-3">
|
|
{enumItem.values.map((value, index) => (
|
|
<div
|
|
key={value}
|
|
className="flex items-center gap-2 rounded border p-2"
|
|
>
|
|
<span className="text-muted-foreground font-mono text-xs">
|
|
{index + 1}.
|
|
</span>
|
|
<Badge
|
|
variant="outline"
|
|
className={`text-sm ${getValueBadgeColor(value)}`}
|
|
>
|
|
{value}
|
|
</Badge>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
<div>
|
|
<h4 className="mb-2 font-medium">Category</h4>
|
|
<Badge
|
|
className={
|
|
categoryColors[enumItem.category] ||
|
|
'bg-gray-100 text-gray-800'
|
|
}
|
|
>
|
|
<TagIcon className="mr-1 h-3 w-3" />
|
|
{enumItem.category}
|
|
</Badge>
|
|
</div>
|
|
<div>
|
|
<h4 className="mb-2 font-medium">Source File</h4>
|
|
<span className="text-muted-foreground font-mono text-sm">
|
|
{enumItem.sourceFile}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<div>
|
|
<h4 className="mb-2 font-medium">Usage Examples</h4>
|
|
<div className="space-y-2">
|
|
<div className="bg-muted rounded p-3">
|
|
<code className="text-muted-foreground text-sm">
|
|
CREATE TABLE example_table (
|
|
</code>
|
|
<br />
|
|
<code className="text-muted-foreground ml-4 text-sm">
|
|
id uuid PRIMARY KEY,
|
|
</code>
|
|
<br />
|
|
<code className="text-muted-foreground ml-4 text-sm">
|
|
status {enumItem.name} NOT NULL
|
|
</code>
|
|
<br />
|
|
<code className="text-muted-foreground text-sm">
|
|
);
|
|
</code>
|
|
</div>
|
|
<div className="bg-muted rounded p-3">
|
|
<code className="text-muted-foreground text-sm">
|
|
SELECT * FROM table_name WHERE status = '
|
|
{enumItem.values[0]}';
|
|
</code>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
})()}
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{filteredEnums.length === 0 && (
|
|
<Card>
|
|
<CardContent className="flex h-32 items-center justify-center">
|
|
<div className="text-center">
|
|
<ListIcon className="text-muted-foreground mx-auto h-8 w-8" />
|
|
<p className="text-muted-foreground mt-2">
|
|
{searchTerm ? 'No enums match your search' : 'No enums found'}
|
|
</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|