ui: improve credentials management ui/ux

This commit is contained in:
Soumyadas15 2025-06-07 13:23:20 +05:30
parent 317888e33b
commit 4ce617364f
6 changed files with 1015 additions and 783 deletions

View File

@ -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

View File

@ -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>
);
}

View File

@ -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>

View File

@ -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}

View File

@ -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>
)}