mirror of https://github.com/kortix-ai/suna.git
ui: improve credentials management ui/ux
This commit is contained in:
parent
317888e33b
commit
4ce617364f
|
@ -42,65 +42,107 @@ const PublishingChoiceDialog = ({ agent, isOpen, onClose, onRegularPublish, onSe
|
|||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Choose Publishing Method</DialogTitle>
|
||||
<DialogTitle>Publish to Marketplace</DialogTitle>
|
||||
<DialogDescription>
|
||||
How would you like to publish "{agent.name}" to the marketplace?
|
||||
Publish "{agent.name}" to the marketplace for others to discover and use.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{hasMCPCredentials && (
|
||||
<div className="p-4 bg-amber-50 dark:bg-amber-950/20 border border-amber-200 dark:border-amber-800 rounded-lg">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertTriangle className="h-4 w-4 text-amber-600 dark:text-amber-400 mt-0.5 shrink-0" />
|
||||
<div className="text-sm text-amber-800 dark:text-amber-200">
|
||||
<p className="font-medium">This agent contains MCP credentials</p>
|
||||
<p className="mt-1">We recommend using secure publishing to protect your API keys.</p>
|
||||
{hasMCPCredentials ? (
|
||||
<>
|
||||
<div className="p-4 bg-green-50 dark:bg-green-950/20 border border-green-200 dark:border-green-800 rounded-lg">
|
||||
<div className="flex items-start gap-2">
|
||||
<Shield className="h-4 w-4 text-green-600 dark:text-green-400 mt-0.5 shrink-0" />
|
||||
<div className="text-sm text-green-800 dark:text-green-200">
|
||||
<p className="font-medium">Secure Publishing Recommended</p>
|
||||
<p className="mt-1">This agent contains MCP credentials. We'll create a secure template that protects your API keys.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Button
|
||||
onClick={() => onSecurePublish(agent.agent_id)}
|
||||
disabled={isCreatingTemplate}
|
||||
className="w-full justify-start gap-3 h-auto p-4"
|
||||
>
|
||||
<Shield className="h-5 w-5" />
|
||||
<div className="text-left">
|
||||
<div className="font-medium">Publish as Secure Template</div>
|
||||
<div className="text-xs text-primary-foreground/80 mt-1">
|
||||
Users will use their own encrypted credentials
|
||||
</div>
|
||||
</div>
|
||||
{isCreatingTemplate && (
|
||||
<div className="ml-auto h-4 w-4 animate-spin rounded-full border-2 border-primary-foreground border-t-transparent" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<details className="group">
|
||||
<summary className="text-sm text-muted-foreground cursor-pointer hover:text-foreground transition-colors">
|
||||
Advanced: Legacy publishing options
|
||||
</summary>
|
||||
<div className="mt-2 pt-2 border-t">
|
||||
<Button
|
||||
onClick={() => onRegularPublish(agent.agent_id)}
|
||||
disabled={isPublishing}
|
||||
variant="outline"
|
||||
className="w-full justify-start gap-3 h-auto p-3 border-amber-200 bg-amber-50 hover:bg-amber-100 text-amber-800"
|
||||
>
|
||||
<AlertTriangle className="h-4 w-4 text-amber-600" />
|
||||
<div className="text-left">
|
||||
<div className="font-medium text-sm">Legacy Publishing</div>
|
||||
<div className="text-xs text-amber-600 mt-1">
|
||||
⚠️ Will expose your API keys publicly
|
||||
</div>
|
||||
</div>
|
||||
{isPublishing && (
|
||||
<div className="ml-auto h-4 w-4 animate-spin rounded-full border-2 border-amber-600 border-t-transparent" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<Button
|
||||
onClick={() => onSecurePublish(agent.agent_id)}
|
||||
disabled={isCreatingTemplate}
|
||||
className="w-full justify-start gap-3 h-auto p-4"
|
||||
>
|
||||
<Shield className="h-5 w-5" />
|
||||
<div className="text-left">
|
||||
<div className="font-medium">Publish as Secure Template</div>
|
||||
<div className="text-xs text-primary-foreground/80 mt-1">
|
||||
Modern, secure publishing method
|
||||
</div>
|
||||
</div>
|
||||
{isCreatingTemplate && (
|
||||
<div className="ml-auto h-4 w-4 animate-spin rounded-full border-2 border-primary-foreground border-t-transparent" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => onRegularPublish(agent.agent_id)}
|
||||
disabled={isPublishing}
|
||||
variant="outline"
|
||||
className="w-full justify-start gap-3 h-auto p-4"
|
||||
>
|
||||
<Globe className="h-5 w-5" />
|
||||
<div className="text-left">
|
||||
<div className="font-medium">Legacy Publishing</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
Traditional marketplace publishing
|
||||
</div>
|
||||
</div>
|
||||
{isPublishing && (
|
||||
<div className="ml-auto h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
<Button
|
||||
onClick={() => onSecurePublish(agent.agent_id)}
|
||||
disabled={isCreatingTemplate}
|
||||
className="w-full justify-start gap-3 h-auto p-4 bg-green-50 hover:bg-green-100 border border-green-200 text-green-800"
|
||||
variant="outline"
|
||||
>
|
||||
<Shield className="h-5 w-5 text-green-600" />
|
||||
<div className="text-left">
|
||||
<div className="font-medium">Secure Template (Recommended)</div>
|
||||
<div className="text-xs text-green-600 mt-1">
|
||||
Creates a secure template without exposing credentials
|
||||
</div>
|
||||
</div>
|
||||
{isCreatingTemplate && (
|
||||
<div className="ml-auto h-4 w-4 animate-spin rounded-full border-2 border-green-600 border-t-transparent" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => onRegularPublish(agent.agent_id)}
|
||||
disabled={isPublishing}
|
||||
variant="outline"
|
||||
className="w-full justify-start gap-3 h-auto p-4"
|
||||
>
|
||||
<Globe className="h-5 w-5" />
|
||||
<div className="text-left">
|
||||
<div className="font-medium">Regular Publishing</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
{hasMCPCredentials
|
||||
? "⚠️ Will expose API keys publicly"
|
||||
: "Standard marketplace publishing"
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{isPublishing && (
|
||||
<div className="ml-auto h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
@ -268,7 +310,7 @@ export const AgentsGrid = ({
|
|||
const handleRegularPublish = async (agentId: string) => {
|
||||
try {
|
||||
await publishAgentMutation.mutateAsync({ agentId, tags: [] });
|
||||
toast.success('Agent published to marketplace successfully!');
|
||||
toast.success('Agent published to marketplace! Note: Any embedded credentials will be visible to users.');
|
||||
setShowPublishingChoice(false);
|
||||
setPublishingAgent(null);
|
||||
} catch (error: any) {
|
||||
|
@ -283,7 +325,7 @@ export const AgentsGrid = ({
|
|||
make_public: true,
|
||||
tags: []
|
||||
});
|
||||
toast.success('Secure template created successfully!');
|
||||
toast.success('Agent published as secure template! Users will use their own encrypted credentials.');
|
||||
setShowPublishingChoice(false);
|
||||
setPublishingAgent(null);
|
||||
} catch (error: any) {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,538 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { Search, Download, Star, Calendar, User, Tags, TrendingUp, Shield, AlertTriangle, CheckCircle, Loader2, Store } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { toast } from 'sonner';
|
||||
import { getAgentAvatar } from '../../agents/_utils/get-agent-style';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Pagination } from '../../agents/_components/pagination';
|
||||
import {
|
||||
useMarketplaceTemplates,
|
||||
useInstallTemplate,
|
||||
useUserCredentials,
|
||||
type AgentTemplate,
|
||||
type InstallationResponse
|
||||
} from '@/hooks/react-query/secure-mcp/use-secure-mcp';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface InstallDialogProps {
|
||||
template: AgentTemplate | null;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onInstall: (templateId: string, instanceName?: string) => void;
|
||||
isInstalling: boolean;
|
||||
}
|
||||
|
||||
const InstallDialog: React.FC<InstallDialogProps> = ({
|
||||
template,
|
||||
open,
|
||||
onOpenChange,
|
||||
onInstall,
|
||||
isInstalling
|
||||
}) => {
|
||||
const [instanceName, setInstanceName] = useState('');
|
||||
|
||||
React.useEffect(() => {
|
||||
if (template) {
|
||||
setInstanceName(`${template.name} (My Copy)`);
|
||||
}
|
||||
}, [template]);
|
||||
|
||||
if (!template) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Install Agent Template</DialogTitle>
|
||||
<DialogDescription>
|
||||
Install "{template.name}" to your agent library
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Agent Name</label>
|
||||
<Input
|
||||
value={instanceName}
|
||||
onChange={(e) => setInstanceName(e.target.value)}
|
||||
placeholder="Enter a name for your agent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{template.mcp_requirements.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Required MCP Services</label>
|
||||
<div className="space-y-2">
|
||||
{template.mcp_requirements.map((req) => (
|
||||
<div key={req.qualified_name} className="flex items-center justify-between p-2 border rounded">
|
||||
<div>
|
||||
<p className="text-sm font-medium">{req.display_name}</p>
|
||||
<p className="text-xs text-muted-foreground">{req.qualified_name}</p>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{req.enabled_tools?.length || 0} tools
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Alert>
|
||||
<Shield className="h-4 w-4" />
|
||||
<AlertDescription className="text-xs">
|
||||
This agent requires MCP credentials. If you haven't set them up yet, you'll be prompted to configure them.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => onInstall(template.template_id, instanceName)}
|
||||
disabled={!instanceName.trim() || isInstalling}
|
||||
>
|
||||
{isInstalling ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||||
Installing...
|
||||
</>
|
||||
) : (
|
||||
'Install Agent'
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
interface MissingCredentialsDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
missingCredentials: Array<{
|
||||
qualified_name: string;
|
||||
display_name: string;
|
||||
required_config: string[];
|
||||
}>;
|
||||
templateName: string;
|
||||
}
|
||||
|
||||
const MissingCredentialsDialog: React.FC<MissingCredentialsDialogProps> = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
missingCredentials,
|
||||
templateName
|
||||
}) => {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Missing Credentials</DialogTitle>
|
||||
<DialogDescription>
|
||||
"{templateName}" requires MCP credentials that you haven't set up yet.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<Alert>
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
You need to configure credentials for the following MCP services before installing this agent.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="space-y-3">
|
||||
{missingCredentials.map((cred) => (
|
||||
<Card key={cred.qualified_name}>
|
||||
<CardContent className="p-4">
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium">{cred.display_name}</h4>
|
||||
<p className="text-sm text-muted-foreground">{cred.qualified_name}</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{cred.required_config.map((config) => (
|
||||
<Badge key={config} variant="outline" className="text-xs">
|
||||
{config}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button asChild>
|
||||
<Link href="/settings/credentials">
|
||||
Set Up Credentials
|
||||
</Link>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default function SecureMarketplacePage() {
|
||||
const [page, setPage] = useState(1);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||
const [installingTemplateId, setInstallingTemplateId] = useState<string | null>(null);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<AgentTemplate | null>(null);
|
||||
const [showInstallDialog, setShowInstallDialog] = useState(false);
|
||||
const [showMissingCredsDialog, setShowMissingCredsDialog] = useState(false);
|
||||
const [missingCredentials, setMissingCredentials] = useState<any[]>([]);
|
||||
|
||||
const queryParams = useMemo(() => ({
|
||||
limit: 20,
|
||||
offset: (page - 1) * 20,
|
||||
search: searchQuery || undefined,
|
||||
tags: selectedTags.length > 0 ? selectedTags.join(',') : undefined,
|
||||
}), [page, searchQuery, selectedTags]);
|
||||
|
||||
const { data: templates, isLoading, error } = useMarketplaceTemplates(queryParams);
|
||||
console.log(templates);
|
||||
const { data: userCredentials } = useUserCredentials();
|
||||
const installTemplateMutation = useInstallTemplate();
|
||||
|
||||
React.useEffect(() => {
|
||||
setPage(1);
|
||||
}, [searchQuery, selectedTags]);
|
||||
|
||||
const handleInstallClick = (template: AgentTemplate) => {
|
||||
setSelectedTemplate(template);
|
||||
setShowInstallDialog(true);
|
||||
};
|
||||
|
||||
const handleInstall = async (templateId: string, instanceName?: string) => {
|
||||
setInstallingTemplateId(templateId);
|
||||
try {
|
||||
const result = await installTemplateMutation.mutateAsync({
|
||||
template_id: templateId,
|
||||
instance_name: instanceName
|
||||
});
|
||||
|
||||
if (result.status === 'installed') {
|
||||
toast.success('Agent installed successfully!');
|
||||
setShowInstallDialog(false);
|
||||
} else if (result.status === 'credentials_required') {
|
||||
setMissingCredentials(result.missing_credentials || []);
|
||||
setShowInstallDialog(false);
|
||||
setShowMissingCredsDialog(true);
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || 'Failed to install agent');
|
||||
} finally {
|
||||
setInstallingTemplateId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTagFilter = (tag: string) => {
|
||||
setSelectedTags(prev =>
|
||||
prev.includes(tag)
|
||||
? prev.filter(t => t !== tag)
|
||||
: [...prev, tag]
|
||||
);
|
||||
};
|
||||
|
||||
const getTemplateStyling = (template: AgentTemplate) => {
|
||||
if (template.avatar && template.avatar_color) {
|
||||
return {
|
||||
avatar: template.avatar,
|
||||
color: template.avatar_color,
|
||||
};
|
||||
}
|
||||
return getAgentAvatar(template.template_id);
|
||||
};
|
||||
|
||||
const allTags = React.useMemo(() => {
|
||||
const tags = new Set<string>();
|
||||
templates?.forEach(template => {
|
||||
template.tags?.forEach(tag => tags.add(tag));
|
||||
});
|
||||
return Array.from(tags);
|
||||
}, [templates]);
|
||||
|
||||
const getUserCredentialNames = () => {
|
||||
return new Set(userCredentials?.map(cred => cred.mcp_qualified_name) || []);
|
||||
};
|
||||
|
||||
const getTemplateCredentialStatus = (template: AgentTemplate) => {
|
||||
const userCredNames = getUserCredentialNames();
|
||||
const requiredCreds = template.mcp_requirements.map(req => req.qualified_name);
|
||||
const missingCreds = requiredCreds.filter(cred => !userCredNames.has(cred));
|
||||
|
||||
return {
|
||||
hasAllCredentials: missingCreds.length === 0,
|
||||
missingCount: missingCreds.length,
|
||||
totalRequired: requiredCreds.length
|
||||
};
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="container mx-auto max-w-7xl px-4 py-8">
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>
|
||||
Failed to load marketplace templates. Please try again later.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-7xl px-4 py-8">
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-2xl font-semibold tracking-tight text-foreground">
|
||||
Secure Agent Marketplace
|
||||
</h1>
|
||||
<Badge variant="outline" className="bg-green-50 text-green-700 border-green-200">
|
||||
<Shield className="h-3 w-3 mr-1" />
|
||||
Secure
|
||||
</Badge>
|
||||
<div className="ml-auto">
|
||||
<Button asChild variant="outline" className="gap-2">
|
||||
<Link href="/marketplace">
|
||||
<Store className="h-4 w-4" />
|
||||
Regular Marketplace
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-md text-muted-foreground max-w-2xl">
|
||||
Discover and install secure AI agent templates. Your credentials stay private and encrypted.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Alert>
|
||||
<Shield className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
These agent templates use your own encrypted credentials. No API keys are shared or exposed.
|
||||
<Link href="/settings/credentials" className="ml-2 underline">
|
||||
Manage your credentials →
|
||||
</Link>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search agent templates..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{allTags.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-muted-foreground">Filter by tags:</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{allTags.map(tag => (
|
||||
<Badge
|
||||
key={tag}
|
||||
variant={selectedTags.includes(tag) ? "default" : "outline"}
|
||||
className="cursor-pointer hover:bg-primary/80"
|
||||
onClick={() => handleTagFilter(tag)}
|
||||
>
|
||||
<Tags className="h-3 w-3 mr-1" />
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{isLoading ? (
|
||||
"Loading templates..."
|
||||
) : (
|
||||
`${templates?.length || 0} template${templates?.length !== 1 ? 's' : ''} found`
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<div key={i} className="bg-neutral-100 dark:bg-sidebar border border-border rounded-2xl overflow-hidden">
|
||||
<Skeleton className="h-50" />
|
||||
<div className="p-4 space-y-3">
|
||||
<Skeleton className="h-5 rounded" />
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 rounded" />
|
||||
<Skeleton className="h-4 rounded w-3/4" />
|
||||
</div>
|
||||
<Skeleton className="h-8" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : templates?.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-muted-foreground">
|
||||
{searchQuery || selectedTags.length > 0
|
||||
? "No templates found matching your criteria. Try adjusting your search or filters."
|
||||
: "No agent templates are currently available in the marketplace."}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{templates?.map((template) => {
|
||||
const { avatar, color } = getTemplateStyling(template);
|
||||
const credentialStatus = getTemplateCredentialStatus(template);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={template.template_id}
|
||||
className="bg-neutral-100 dark:bg-sidebar border border-border rounded-2xl overflow-hidden hover:bg-muted/50 transition-all duration-200 cursor-pointer group flex flex-col h-full"
|
||||
>
|
||||
<div className={`h-50 flex items-center justify-center relative`} style={{ backgroundColor: color }}>
|
||||
<div className="text-4xl">
|
||||
{avatar}
|
||||
</div>
|
||||
<div className="absolute top-3 right-3 flex gap-2">
|
||||
<div className="flex items-center gap-1 bg-white/20 backdrop-blur-sm px-2 py-1 rounded-full">
|
||||
<Download className="h-3 w-3 text-white" />
|
||||
<span className="text-white text-xs font-medium">{template.download_count || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute bottom-3 left-3">
|
||||
<Badge variant="secondary" className="bg-white/20 backdrop-blur-sm text-white border-white/20">
|
||||
<Shield className="h-3 w-3 mr-1" />
|
||||
Secure
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 flex flex-col flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="text-foreground font-medium text-lg line-clamp-1 flex-1">
|
||||
{template.name}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm mb-3 line-clamp-2">
|
||||
{template.description || 'No description available'}
|
||||
</p>
|
||||
|
||||
{template.mcp_requirements.length > 0 && (
|
||||
<div className="mb-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
MCP Services ({template.mcp_requirements.length})
|
||||
</span>
|
||||
{credentialStatus.hasAllCredentials ? (
|
||||
<CheckCircle className="h-3 w-3 text-green-500" />
|
||||
) : (
|
||||
<AlertTriangle className="h-3 w-3 text-amber-500" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{template.mcp_requirements.slice(0, 2).map(req => (
|
||||
<Badge key={req.qualified_name} variant="outline" className="text-xs">
|
||||
{req.display_name}
|
||||
</Badge>
|
||||
))}
|
||||
{template.mcp_requirements.length > 2 && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
+{template.mcp_requirements.length - 2}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{template.tags && template.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mb-3">
|
||||
{template.tags.slice(0, 2).map(tag => (
|
||||
<Badge key={tag} variant="outline" className="text-xs">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
{template.tags.length > 2 && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
+{template.tags.length - 2}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-1 mb-4">
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<User className="h-3 w-3" />
|
||||
<span>By {template.creator_name || 'Anonymous'}</span>
|
||||
</div>
|
||||
{template.marketplace_published_at && (
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Calendar className="h-3 w-3" />
|
||||
<span>{new Date(template.marketplace_published_at).toLocaleDateString()}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleInstallClick(template);
|
||||
}}
|
||||
disabled={installingTemplateId === template.template_id}
|
||||
className="w-full transition-opacity mt-auto"
|
||||
size="sm"
|
||||
>
|
||||
{installingTemplateId === template.template_id ? (
|
||||
<>
|
||||
<Loader2 className="h-3 w-3 animate-spin mr-2" />
|
||||
Installing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="h-3 w-3 mr-2" />
|
||||
Install Template
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<InstallDialog
|
||||
template={selectedTemplate}
|
||||
open={showInstallDialog}
|
||||
onOpenChange={setShowInstallDialog}
|
||||
onInstall={handleInstall}
|
||||
isInstalling={installingTemplateId === selectedTemplate?.template_id}
|
||||
/>
|
||||
|
||||
<MissingCredentialsDialog
|
||||
open={showMissingCredsDialog}
|
||||
onOpenChange={setShowMissingCredsDialog}
|
||||
missingCredentials={missingCredentials}
|
||||
templateName={selectedTemplate?.name || ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -36,7 +36,7 @@ const MCPServerCard: React.FC<MCPServerCardProps> = ({ server, onClick }) => {
|
|||
<img
|
||||
src={iconUrl}
|
||||
alt={`${displayName} logo`}
|
||||
className="w-10 h-10 rounded-md object-cover flex-shrink-0"
|
||||
className="w-8 h-8 rounded-md object-cover flex-shrink-0"
|
||||
onError={(e) => {
|
||||
e.currentTarget.style.display = 'none';
|
||||
}}
|
||||
|
@ -83,14 +83,14 @@ const CategorySidebar: React.FC<CategorySidebarProps> = ({
|
|||
categorizedServers
|
||||
}) => {
|
||||
return (
|
||||
<div className="w-48 border-r pr-4">
|
||||
<div className="w-48 pr-4">
|
||||
<h3 className="font-medium text-sm mb-3">Categories</h3>
|
||||
<div className="space-y-1">
|
||||
<button
|
||||
onClick={() => onCategorySelect(null)}
|
||||
className={`w-full text-left px-2 py-1.5 text-sm rounded-md transition-colors ${
|
||||
selectedCategory === null
|
||||
? 'bg-primary text-primary-foreground'
|
||||
? 'bg-muted-foreground/10 text-foreground'
|
||||
: 'hover:bg-muted'
|
||||
}`}
|
||||
>
|
||||
|
@ -102,14 +102,11 @@ const CategorySidebar: React.FC<CategorySidebarProps> = ({
|
|||
onClick={() => onCategorySelect(category)}
|
||||
className={`w-full text-left px-2 py-1.5 text-sm rounded-md transition-colors flex items-center justify-between ${
|
||||
selectedCategory === category
|
||||
? 'bg-primary text-primary-foreground'
|
||||
? 'bg-muted-foreground/10 text-foreground'
|
||||
: 'hover:bg-muted'
|
||||
}`}
|
||||
>
|
||||
<span className="truncate">{category}</span>
|
||||
<Badge variant="outline" className="text-xs ml-2">
|
||||
{categorizedServers[category]?.length || 0}
|
||||
</Badge>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -16,10 +16,6 @@ import {
|
|||
} from '@/hooks/react-query/secure-mcp/use-secure-mcp';
|
||||
import { EnhancedAddCredentialDialog } from './_components/enhanced-add-credential-dialog';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
interface CredentialCardProps {
|
||||
credential: MCPCredential;
|
||||
onTest: (qualifiedName: string) => void;
|
||||
|
@ -39,67 +35,45 @@ const CredentialCard: React.FC<CredentialCardProps> = ({
|
|||
const isDeleting = isDeletingId === credential.mcp_qualified_name;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<Card className="border-border/50 hover:border-border transition-colors">
|
||||
<CardContent className="p-4 py-0">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-2 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Key className="h-4 w-4 text-primary" />
|
||||
<h3 className="font-medium">{credential.display_name}</h3>
|
||||
<div className="p-2 rounded-md bg-primary/10">
|
||||
<Key className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
<h3 className="text-md font-medium text-foreground">{credential.display_name}</h3>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-xs text-muted-foreground font-mono">
|
||||
{credential.mcp_qualified_name}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{credential.config_keys.map((key) => (
|
||||
<Badge key={key} variant="outline" className="text-xs">
|
||||
<Badge key={key} variant="outline">
|
||||
{key}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
{credential.last_used_at && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Last used: {new Date(credential.last_used_at).toLocaleDateString()}
|
||||
<p className="text-xs text-muted-foreground/70">
|
||||
Last used {new Date(credential.last_used_at).toLocaleDateString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<div className="flex gap-1.5 ml-4">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => onTest(credential.mcp_qualified_name)}
|
||||
disabled={isTesting || isDeleting}
|
||||
>
|
||||
{isTesting ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin mr-1" />
|
||||
Testing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TestTube className="h-4 w-4 mr-1" />
|
||||
Test
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
variant="ghost"
|
||||
onClick={() => onDelete(credential.mcp_qualified_name)}
|
||||
disabled={isTesting || isDeleting}
|
||||
className="text-destructive hover:text-destructive"
|
||||
className="h-8 px-2.5 text-muted-foreground hover:text-destructive hover:bg-destructive/10"
|
||||
>
|
||||
{isDeleting ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin mr-1" />
|
||||
Deleting...
|
||||
</>
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<Trash2 className="h-4 w-4 mr-1" />
|
||||
Delete
|
||||
</>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -149,10 +123,10 @@ export default function CredentialsPage() {
|
|||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="container mx-auto max-w-4xl px-4 py-8">
|
||||
<Alert variant="destructive">
|
||||
<div className="container mx-auto max-w-4xl px-4 py-6">
|
||||
<Alert variant="destructive" className="border-destructive/20 bg-destructive/5">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
<AlertDescription className="text-sm">
|
||||
Failed to load credentials. Please try again later.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
@ -161,44 +135,48 @@ export default function CredentialsPage() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-4xl px-4 py-8">
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-4">
|
||||
<div className="container mx-auto max-w-4xl px-4 py-6">
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-lg font-semibold text-foreground">
|
||||
MCP Credentials
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Manage your encrypted API credentials for MCP servers
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={() => setShowAddDialog(true)}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
<Button
|
||||
onClick={() => setShowAddDialog(true)}
|
||||
size="sm"
|
||||
className="h-8 px-3 text-sm"
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
Add Credential
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Alert>
|
||||
<Shield className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
All credentials are encrypted and stored securely. You only need to set up each MCP service once,
|
||||
and your credentials will be automatically used when installing agents that require them.
|
||||
<Alert className="border-primary/20 bg-primary/5">
|
||||
<Shield className="h-4 w-4 text-primary" />
|
||||
<AlertDescription className="text-sm text-muted-foreground">
|
||||
All credentials are encrypted and stored securely. Set up each MCP service once,
|
||||
and credentials will be automatically used when installing agents.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="grid gap-4">
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<Card key={i}>
|
||||
<CardContent className="p-6">
|
||||
<div className="animate-pulse space-y-3">
|
||||
<div className="h-4 bg-muted rounded w-1/3"></div>
|
||||
<div className="h-3 bg-muted rounded w-1/2"></div>
|
||||
<div className="flex gap-2">
|
||||
<div className="h-6 bg-muted rounded w-16"></div>
|
||||
<div className="h-6 bg-muted rounded w-20"></div>
|
||||
<Card key={i} className="border-border/50">
|
||||
<CardContent className="p-4">
|
||||
<div className="animate-pulse space-y-2">
|
||||
<div className="h-3 bg-muted/60 rounded w-1/4"></div>
|
||||
<div className="h-2.5 bg-muted/40 rounded w-1/3"></div>
|
||||
<div className="flex gap-1.5">
|
||||
<div className="h-5 bg-muted/40 rounded w-12"></div>
|
||||
<div className="h-5 bg-muted/40 rounded w-16"></div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
@ -206,21 +184,27 @@ export default function CredentialsPage() {
|
|||
))}
|
||||
</div>
|
||||
) : credentials?.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="p-12 text-center">
|
||||
<Key className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium mb-2">No credentials configured</h3>
|
||||
<p className="text-muted-foreground mb-6">
|
||||
Add your first MCP credential to start using secure agents from the marketplace
|
||||
<Card className="border-dashed border-border/50">
|
||||
<CardContent className="p-8 text-center">
|
||||
<div className="p-3 rounded-full bg-muted/50 w-fit mx-auto mb-3">
|
||||
<Key className="h-6 w-6 text-muted-foreground" />
|
||||
</div>
|
||||
<h3 className="text-sm font-medium mb-1 text-foreground">No credentials configured</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
Add your first MCP credential to start using secure agents
|
||||
</p>
|
||||
<Button onClick={() => setShowAddDialog(true)}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
<Button
|
||||
onClick={() => setShowAddDialog(true)}
|
||||
size="sm"
|
||||
className="h-8 px-3 text-sm"
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5 mr-1.5" />
|
||||
Add Your First Credential
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid gap-4">
|
||||
<div className="space-y-3">
|
||||
{credentials?.map((credential) => (
|
||||
<CredentialCard
|
||||
key={credential.credential_id}
|
||||
|
|
|
@ -156,34 +156,19 @@ export function SidebarLeft({
|
|||
</Link>
|
||||
)}
|
||||
{marketplaceEnabled && (
|
||||
<>
|
||||
<Link href="/marketplace">
|
||||
<SidebarMenuButton className={cn({
|
||||
'bg-primary/10 font-medium': pathname === '/marketplace',
|
||||
})}>
|
||||
<Store className="h-4 w-4 mr-2" />
|
||||
<span className="flex items-center justify-between w-full">
|
||||
Marketplace
|
||||
<Badge variant="new">
|
||||
New
|
||||
</Badge>
|
||||
</span>
|
||||
</SidebarMenuButton>
|
||||
</Link>
|
||||
<Link href="/marketplace/secure">
|
||||
<SidebarMenuButton className={cn({
|
||||
'bg-primary/10 font-medium': pathname === '/marketplace/secure',
|
||||
})}>
|
||||
<Shield className="h-4 w-4 mr-2" />
|
||||
<span className="flex items-center justify-between w-full">
|
||||
Secure Templates
|
||||
<Badge variant="outline" className="bg-green-50 text-green-700 border-green-200 text-xs">
|
||||
Secure
|
||||
</Badge>
|
||||
</span>
|
||||
</SidebarMenuButton>
|
||||
</Link>
|
||||
</>
|
||||
<Link href="/marketplace">
|
||||
<SidebarMenuButton className={cn({
|
||||
'bg-primary/10 font-medium': pathname.startsWith('/marketplace'),
|
||||
})}>
|
||||
<Store className="h-4 w-4 mr-2" />
|
||||
<span className="flex items-center justify-between w-full">
|
||||
Marketplace
|
||||
<Badge variant="new">
|
||||
New
|
||||
</Badge>
|
||||
</span>
|
||||
</SidebarMenuButton>
|
||||
</Link>
|
||||
)}
|
||||
</SidebarGroup>
|
||||
)}
|
||||
|
|
Loading…
Reference in New Issue