add credential profile direcly on install or setup dialog

This commit is contained in:
Soumyadas15 2025-07-03 09:29:49 +05:30
parent 58033075b4
commit 87dd9d667a
3 changed files with 571 additions and 153 deletions

View File

@ -2,14 +2,22 @@ import React, { useState } from 'react';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Loader2, Save, Sparkles, AlertTriangle } from 'lucide-react';
import { Loader2, Save, Sparkles, AlertTriangle, Plus, Key, Settings, Shield } from 'lucide-react';
import { useMCPServerDetails } from '@/hooks/react-query/mcp/use-mcp-servers';
import { CredentialProfileSelector } from '@/components/workflows/CredentialProfileSelector';
import { useCredentialProfilesForMcp, type CredentialProfile } from '@/hooks/react-query/mcp/use-credential-profiles';
import {
useCredentialProfilesForMcp,
useCreateCredentialProfile,
type CredentialProfile,
type CreateCredentialProfileRequest
} from '@/hooks/react-query/mcp/use-credential-profiles';
import { cn } from '@/lib/utils';
import { MCPConfiguration } from './types';
import { Card, CardContent } from '@/components/ui/card';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { toast } from 'sonner';
interface ConfigDialogProps {
server: any;
@ -30,12 +38,40 @@ export const ConfigDialog: React.FC<ConfigDialogProps> = ({
const [selectedProfileId, setSelectedProfileId] = useState<string | null>(
existingConfig?.selectedProfileId || null
);
const [showCreateProfileDialog, setShowCreateProfileDialog] = useState(false);
const [formData, setFormData] = useState<{
profile_name: string;
display_name: string;
config: Record<string, string>;
is_default: boolean;
}>({
profile_name: `${server.displayName || server.name} Profile`,
display_name: server.displayName || server.name,
config: {},
is_default: false
});
const { data: serverDetails, isLoading } = useMCPServerDetails(server.qualifiedName);
const { data: profiles = [], refetch: refetchProfiles } = useCredentialProfilesForMcp(server.qualifiedName);
const createProfileMutation = useCreateCredentialProfile();
const requiresConfig = serverDetails?.connections?.[0]?.configSchema?.properties &&
Object.keys(serverDetails.connections[0].configSchema.properties).length > 0;
const getConfigProperties = () => {
const schema = serverDetails?.connections?.[0]?.configSchema;
return schema?.properties || {};
};
const getRequiredFields = () => {
const schema = serverDetails?.connections?.[0]?.configSchema;
return schema?.required || [];
};
const isFieldRequired = (fieldName: string) => {
return getRequiredFields().includes(fieldName);
};
const handleSave = () => {
const mcpConfig: MCPConfiguration = {
name: server.displayName || server.name || server.qualifiedName,
@ -61,146 +97,321 @@ export const ConfigDialog: React.FC<ConfigDialogProps> = ({
setSelectedProfileId(profileId);
};
return (
<DialogContent className="max-w-5xl max-h-[85vh] overflow-hidden flex flex-col">
<DialogHeader className="flex-shrink-0">
<div className="flex items-center gap-3 mb-2">
{server.iconUrl ? (
<div className="relative">
<img
src={server.iconUrl}
alt={server.displayName || server.name}
className="w-8 h-8 rounded-lg shadow-sm"
/>
<div className="absolute inset-0 bg-gradient-to-br from-white/20 to-transparent rounded-lg pointer-events-none" />
</div>
) : (
<div className="w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center shadow-sm border border-primary/20">
<Sparkles className="h-4 w-4 text-primary" />
</div>
)}
<div>
<DialogTitle className="text-lg">Configure {server.displayName || server.name}</DialogTitle>
</div>
</div>
<DialogDescription>
Set up the connection and select which tools to enable for this MCP server.
</DialogDescription>
</DialogHeader>
const handleCreateNewProfile = () => {
setShowCreateProfileDialog(true);
};
{isLoading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
) : (
<div className="flex-1 min-h-0 grid grid-cols-2 gap-6 px-1">
<div className="flex flex-col min-h-0">
<h3 className="text-sm font-semibold flex items-center gap-2 mb-4">
<div className="w-1 h-4 bg-primary rounded-full" />
Connection Settings
</h3>
{requiresConfig ? (
<div className="space-y-4">
<CredentialProfileSelector
mcpQualifiedName={server.qualifiedName}
mcpDisplayName={server.displayName || server.name}
selectedProfileId={selectedProfileId || undefined}
onProfileSelect={handleProfileSelect}
const handleConfigChange = (key: string, value: string) => {
setFormData(prev => ({
...prev,
config: {
...prev.config,
[key]: value
}
}));
};
const handleCreateSubmit = async () => {
try {
const request: CreateCredentialProfileRequest = {
mcp_qualified_name: server.qualifiedName,
profile_name: formData.profile_name,
display_name: formData.display_name,
config: formData.config,
is_default: formData.is_default
};
const response = await createProfileMutation.mutateAsync(request);
toast.success('Credential profile created successfully!');
// Create a profile object to return
const newProfile: CredentialProfile = {
profile_id: response.profile_id || 'new-profile',
mcp_qualified_name: server.qualifiedName,
profile_name: formData.profile_name,
display_name: formData.display_name,
config_keys: Object.keys(formData.config),
is_active: true,
is_default: formData.is_default,
last_used_at: null,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
};
// Refetch profiles to get the updated list
refetchProfiles();
// Auto-select the newly created profile
setSelectedProfileId(newProfile.profile_id);
setShowCreateProfileDialog(false);
// Reset form
setFormData({
profile_name: `${server.displayName || server.name} Profile`,
display_name: server.displayName || server.name,
config: {},
is_default: false
});
} catch (error: any) {
toast.error(error.message || 'Failed to create credential profile');
}
};
const configProperties = getConfigProperties();
const hasConfigFields = Object.keys(configProperties).length > 0;
return (
<>
<DialogContent className="max-w-5xl max-h-[85vh] overflow-hidden flex flex-col">
<DialogHeader className="flex-shrink-0">
<div className="flex items-center gap-3 mb-2">
{server.iconUrl ? (
<div className="relative">
<img
src={server.iconUrl}
alt={server.displayName || server.name}
className="w-8 h-8 rounded-lg shadow-sm"
/>
{!selectedProfileId && (
<Alert>
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
Please select or create a credential profile to configure this MCP server.
</AlertDescription>
</Alert>
<div className="absolute inset-0 bg-gradient-to-br from-white/20 to-transparent rounded-lg pointer-events-none" />
</div>
) : (
<div className="w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center shadow-sm border border-primary/20">
<Sparkles className="h-4 w-4 text-primary" />
</div>
)}
<div>
<DialogTitle className="text-lg">Configure {server.displayName || server.name}</DialogTitle>
</div>
</div>
<DialogDescription>
Set up the connection and select which tools to enable for this MCP server.
</DialogDescription>
</DialogHeader>
{isLoading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
) : (
<div className="flex-1 min-h-0 grid grid-cols-2 gap-6 px-1">
<div className="flex flex-col min-h-0">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold flex items-center gap-2">
<div className="w-1 h-4 bg-primary rounded-full" />
Connection Settings
</h3>
{requiresConfig && (
<Button
variant="outline"
size="sm"
onClick={handleCreateNewProfile}
>
<Plus className="h-4 w-4" />
New Profile
</Button>
)}
</div>
) : (
<div className="flex-1 flex items-center justify-center text-muted-foreground">
<Card className="border-dashed border-2 w-full">
<CardContent className="p-6 text-center">
<p className="text-sm">No configuration required for this MCP server</p>
</CardContent>
</Card>
</div>
)}
</div>
<div className="flex flex-col min-h-0">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold flex items-center gap-2">
<div className="w-1 h-4 bg-primary rounded-full" />
Available Tools
</h3>
{serverDetails?.tools && serverDetails.tools.length > 0 && (
<span className="text-xs text-muted-foreground bg-muted/50 px-2 py-1 rounded-full">
{selectedTools.size} of {serverDetails.tools.length} selected
</span>
{requiresConfig ? (
<div className="space-y-4">
<CredentialProfileSelector
mcpQualifiedName={server.qualifiedName}
mcpDisplayName={server.displayName || server.name}
selectedProfileId={selectedProfileId || undefined}
onProfileSelect={handleProfileSelect}
/>
{!selectedProfileId && (
<Alert>
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
Please select or create a credential profile to configure this MCP server.
</AlertDescription>
</Alert>
)}
</div>
) : (
<div className="flex-1 flex items-center justify-center text-muted-foreground">
<Card className="border-dashed border-2 w-full">
<CardContent className="p-6 text-center">
<p className="text-sm">No configuration required for this MCP server</p>
</CardContent>
</Card>
</div>
)}
</div>
{serverDetails?.tools && serverDetails.tools.length > 0 ? (
<ScrollArea className="-mt-1 border bg-muted/30 rounded-lg p-4 flex-1 min-h-0">
<div>
<div className="space-y-2">
{serverDetails.tools.map((tool: any) => (
<div
key={tool.name}
className={cn(
"flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-all duration-200",
selectedTools.has(tool.name)
? "bg-primary/10 border-primary/30 shadow-sm"
: "hover:bg-muted/30 border-border/50 hover:border-border"
)}
onClick={() => handleToolToggle(tool.name)}
>
<input
type="checkbox"
checked={selectedTools.has(tool.name)}
onChange={() => {}}
className="mt-1 accent-primary"
/>
<div className="flex-1">
<div className="font-medium text-sm">{tool.name}</div>
{tool.description && (
<div className="text-xs text-muted-foreground mt-1">
{tool.description}
</div>
)}
</div>
</div>
))}
</div>
</div>
</ScrollArea>
) : (
<div className="flex-1 flex items-center justify-center text-muted-foreground">
<Card className="border-dashed border-2 w-full">
<CardContent className="p-6 text-center">
<p className="text-sm">No tools available for this MCP server</p>
</CardContent>
</Card>
</div>
)}
</div>
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={onCancel}>
Cancel
</Button>
<Button
onClick={handleSave}
disabled={isLoading || (requiresConfig && !selectedProfileId)}
className="bg-primary hover:bg-primary/90"
>
<Save className="h-4 w-4" />
Save Configuration
</Button>
</DialogFooter>
</DialogContent>
<div className="flex flex-col min-h-0">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold flex items-center gap-2">
<div className="w-1 h-4 bg-primary rounded-full" />
Available Tools
</h3>
{serverDetails?.tools && serverDetails.tools.length > 0 && (
<span className="text-xs text-muted-foreground bg-muted/50 px-2 py-1 rounded-full">
{selectedTools.size} of {serverDetails.tools.length} selected
</span>
)}
</div>
{serverDetails?.tools && serverDetails.tools.length > 0 ? (
<ScrollArea className="-mt-1 border bg-muted/30 rounded-lg p-4 flex-1 min-h-0">
<div>
<div className="space-y-2">
{serverDetails.tools.map((tool: any) => (
<div
key={tool.name}
className={cn(
"flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-all duration-200",
selectedTools.has(tool.name)
? "bg-primary/10 border-primary/30 shadow-sm"
: "hover:bg-muted/30 border-border/50 hover:border-border"
)}
onClick={() => handleToolToggle(tool.name)}
>
<input
type="checkbox"
checked={selectedTools.has(tool.name)}
onChange={() => {}}
className="mt-1 accent-primary"
/>
<div className="flex-1">
<div className="font-medium text-sm">{tool.name}</div>
{tool.description && (
<div className="text-xs text-muted-foreground mt-1">
{tool.description}
</div>
)}
</div>
</div>
))}
</div>
</div>
</ScrollArea>
) : (
<div className="flex-1 flex items-center justify-center text-muted-foreground">
<Card className="border-dashed border-2 w-full">
<CardContent className="p-6 text-center">
<p className="text-sm">No tools available for this MCP server</p>
</CardContent>
</Card>
</div>
)}
</div>
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={onCancel}>
Cancel
</Button>
<Button
onClick={handleSave}
disabled={isLoading || (requiresConfig && !selectedProfileId)}
className="bg-primary hover:bg-primary/90"
>
<Save className="h-4 w-4" />
Save Configuration
</Button>
</DialogFooter>
</DialogContent>
{/* Create Profile Dialog */}
<Dialog open={showCreateProfileDialog} onOpenChange={setShowCreateProfileDialog}>
<DialogContent className="max-w-2xl max-h-[85vh] overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Key className="h-5 w-5 text-primary" />
Create Credential Profile
</DialogTitle>
<DialogDescription>
Create a new credential profile for <strong>{server.displayName || server.name}</strong>
</DialogDescription>
</DialogHeader>
<div className="space-y-6 flex-1 overflow-y-auto">
<div className="space-y-4">
<div className="grid grid-cols-1 gap-4">
<div className="space-y-2">
<Label htmlFor="profile_name">Profile Name *</Label>
<Input
id="profile_name"
value={formData.profile_name}
onChange={(e) => setFormData(prev => ({ ...prev, profile_name: e.target.value }))}
placeholder="Enter a name for this profile"
/>
<p className="text-xs text-muted-foreground">
This helps you identify different configurations for the same MCP server
</p>
</div>
</div>
{hasConfigFields ? (
<div className="space-y-4">
<h3 className="text-sm font-semibold flex items-center gap-2">
<Settings className="h-4 w-4" />
Connection Settings
</h3>
{Object.entries(configProperties).map(([key, schema]: [string, any]) => (
<div key={key} className="space-y-2">
<Label htmlFor={key}>
{schema.title || key}
{isFieldRequired(key) && (
<span className="text-destructive ml-1">*</span>
)}
</Label>
<Input
id={key}
type={schema.format === 'password' ? 'password' : 'text'}
placeholder={schema.description || `Enter ${key}`}
value={formData.config[key] || ''}
onChange={(e) => handleConfigChange(key, e.target.value)}
/>
{schema.description && (
<p className="text-xs text-muted-foreground">{schema.description}</p>
)}
</div>
))}
</div>
) : (
<Alert>
<Key className="h-4 w-4" />
<AlertDescription>
This MCP server doesn't require any API credentials to use.
</AlertDescription>
</Alert>
)}
<Alert>
<Shield className="h-4 w-4" />
<AlertDescription>
Your credentials will be encrypted and stored securely. You can create multiple profiles for the same MCP server to handle different use cases.
</AlertDescription>
</Alert>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowCreateProfileDialog(false)}>
Cancel
</Button>
<Button
onClick={handleCreateSubmit}
disabled={!formData.profile_name.trim() || createProfileMutation.isPending}
>
{createProfileMutation.isPending ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Creating...
</>
) : (
<>
<Plus className="h-4 w-4" />
Create Profile
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
};

View File

@ -1,7 +1,7 @@
'use client';
import React, { useState, useMemo, useEffect } from 'react';
import { Search, Download, Star, Calendar, User, Tags, TrendingUp, Shield, CheckCircle, Loader2, Settings, Wrench, AlertTriangle, GitBranch, Plus, ShoppingBag } from 'lucide-react';
import { Search, Download, Star, Calendar, User, Tags, TrendingUp, Shield, CheckCircle, Loader2, Settings, Wrench, AlertTriangle, GitBranch, Plus, ShoppingBag, Key } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
@ -10,6 +10,7 @@ import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle } from '@/components/ui/sheet';
import { Card, CardContent } from '@/components/ui/card';
import { Label } from '@/components/ui/label';
import { toast } from 'sonner';
import { getAgentAvatar } from '../agents/_utils/get-agent-style';
import { Skeleton } from '@/components/ui/skeleton';
@ -19,7 +20,13 @@ import {
useMarketplaceTemplates,
useInstallTemplate
} from '@/hooks/react-query/secure-mcp/use-secure-mcp';
import { useCredentialProfilesForMcp } from '@/hooks/react-query/mcp/use-credential-profiles';
import {
useCredentialProfilesForMcp,
useCreateCredentialProfile,
type CredentialProfile,
type CreateCredentialProfileRequest
} from '@/hooks/react-query/mcp/use-credential-profiles';
import { useMCPServerDetails } from '@/hooks/react-query/mcp/use-mcp-servers';
import { createClient } from '@/lib/supabase/client';
import { useFeatureFlag } from '@/lib/feature-flags';
import { useRouter } from 'next/navigation';
@ -259,6 +266,23 @@ const InstallDialog: React.FC<InstallDialogProps> = ({
const [isCheckingRequirements, setIsCheckingRequirements] = useState(false);
const [setupSteps, setSetupSteps] = useState<SetupStep[]>([]);
const [missingProfiles, setMissingProfiles] = useState<MissingProfile[]>([]);
const [showCreateProfileDialog, setShowCreateProfileDialog] = useState(false);
const [createProfileForQualifiedName, setCreateProfileForQualifiedName] = useState<string>('');
const [createProfileForDisplayName, setCreateProfileForDisplayName] = useState<string>('');
const [formData, setFormData] = useState<{
profile_name: string;
display_name: string;
config: Record<string, string>;
is_default: boolean;
}>({
profile_name: '',
display_name: '',
config: {},
is_default: false
});
const createProfileMutation = useCreateCredentialProfile();
const { data: serverDetails } = useMCPServerDetails(createProfileForQualifiedName);
React.useEffect(() => {
if (item && open) {
@ -271,7 +295,6 @@ const InstallDialog: React.FC<InstallDialogProps> = ({
}
}, [item, open]);
// Add a function to refresh requirements when profiles are created
const refreshRequirements = React.useCallback(() => {
if (item && open) {
setIsCheckingRequirements(true);
@ -296,7 +319,6 @@ const InstallDialog: React.FC<InstallDialogProps> = ({
});
}
// Add custom server configuration steps (just ask for URL, no credentials needed)
for (const req of customServers) {
steps.push({
id: req.qualified_name,
@ -317,7 +339,7 @@ const InstallDialog: React.FC<InstallDialogProps> = ({
}
setSetupSteps(steps);
setMissingProfiles([]); // Clear missing profiles since we handle them in setup steps
setMissingProfiles([]);
setIsCheckingRequirements(false);
};
@ -338,6 +360,76 @@ const InstallDialog: React.FC<InstallDialogProps> = ({
}));
};
const handleCreateNewProfile = (qualifiedName: string, displayName: string) => {
setCreateProfileForQualifiedName(qualifiedName);
setCreateProfileForDisplayName(displayName);
setFormData({
profile_name: `${displayName} Profile`,
display_name: displayName,
config: {},
is_default: false
});
setShowCreateProfileDialog(true);
};
const getConfigProperties = () => {
const schema = serverDetails?.connections?.[0]?.configSchema;
return schema?.properties || {};
};
const getRequiredFields = () => {
const schema = serverDetails?.connections?.[0]?.configSchema;
return schema?.required || [];
};
const isFieldRequired = (fieldName: string) => {
return getRequiredFields().includes(fieldName);
};
const handleConfigChange = (key: string, value: string) => {
setFormData(prev => ({
...prev,
config: {
...prev.config,
[key]: value
}
}));
};
const handleCreateSubmit = async () => {
try {
const request: CreateCredentialProfileRequest = {
mcp_qualified_name: createProfileForQualifiedName,
profile_name: formData.profile_name,
display_name: formData.display_name,
config: formData.config,
is_default: formData.is_default
};
const response = await createProfileMutation.mutateAsync(request);
toast.success('Credential profile created successfully!');
// Auto-select the newly created profile
setProfileMappings(prev => ({
...prev,
[createProfileForQualifiedName]: response.profile_id || 'new-profile'
}));
setShowCreateProfileDialog(false);
refreshRequirements();
// Reset form
setFormData({
profile_name: '',
display_name: '',
config: {},
is_default: false
});
} catch (error: any) {
toast.error(error.message || 'Failed to create credential profile');
}
};
const isCurrentStepComplete = (): boolean => {
if (setupSteps.length === 0) return true;
if (currentStep >= setupSteps.length) return true;
@ -461,17 +553,33 @@ const InstallDialog: React.FC<InstallDialogProps> = ({
<div className="space-y-4">
{currentStepData.type === 'credential_profile' ? (
<CredentialProfileSelector
mcpQualifiedName={currentStepData.qualified_name}
mcpDisplayName={currentStepData.service_name}
selectedProfileId={profileMappings[currentStepData.qualified_name]}
onProfileSelect={(profileId, profile) => {
handleProfileSelect(currentStepData.qualified_name, profileId);
if (profile && !profileMappings[currentStepData.qualified_name]) {
refreshRequirements();
}
}}
/>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex-1">
<CredentialProfileSelector
mcpQualifiedName={currentStepData.qualified_name}
mcpDisplayName={currentStepData.service_name}
selectedProfileId={profileMappings[currentStepData.qualified_name]}
onProfileSelect={(profileId, profile) => {
handleProfileSelect(currentStepData.qualified_name, profileId);
if (profile && !profileMappings[currentStepData.qualified_name]) {
refreshRequirements();
}
}}
/>
</div>
</div>
<div className="flex justify-end">
<Button
variant="outline"
size="sm"
onClick={() => handleCreateNewProfile(currentStepData.qualified_name, currentStepData.service_name)}
>
<Plus className="h-4 w-4" />
Create New Profile
</Button>
</div>
</div>
) : (
currentStepData.required_fields?.map((field) => (
<div key={field.key} className="space-y-2">
@ -588,6 +696,105 @@ const InstallDialog: React.FC<InstallDialogProps> = ({
</div>
</DialogFooter>
</DialogContent>
{/* Create Profile Dialog */}
<Dialog open={showCreateProfileDialog} onOpenChange={setShowCreateProfileDialog}>
<DialogContent className="max-w-2xl max-h-[85vh] overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Key className="h-5 w-5 text-primary" />
Create Credential Profile
</DialogTitle>
<DialogDescription>
Create a new credential profile for <strong>{createProfileForDisplayName}</strong>
</DialogDescription>
</DialogHeader>
<div className="space-y-6 flex-1 overflow-y-auto">
<div className="space-y-4">
<div className="grid grid-cols-1 gap-4">
<div className="space-y-2">
<Label htmlFor="profile_name">Profile Name *</Label>
<Input
id="profile_name"
value={formData.profile_name}
onChange={(e) => setFormData(prev => ({ ...prev, profile_name: e.target.value }))}
placeholder="Enter a name for this profile"
/>
<p className="text-xs text-muted-foreground">
This helps you identify different configurations for the same MCP server
</p>
</div>
</div>
{Object.keys(getConfigProperties()).length > 0 ? (
<div className="space-y-4">
<h3 className="text-sm font-semibold flex items-center gap-2">
<Settings className="h-4 w-4" />
Connection Settings
</h3>
{Object.entries(getConfigProperties()).map(([key, schema]: [string, any]) => (
<div key={key} className="space-y-2">
<Label htmlFor={key}>
{schema.title || key}
{isFieldRequired(key) && (
<span className="text-destructive ml-1">*</span>
)}
</Label>
<Input
id={key}
type={schema.format === 'password' ? 'password' : 'text'}
placeholder={schema.description || `Enter ${key}`}
value={formData.config[key] || ''}
onChange={(e) => handleConfigChange(key, e.target.value)}
/>
{schema.description && (
<p className="text-xs text-muted-foreground">{schema.description}</p>
)}
</div>
))}
</div>
) : (
<Alert>
<Key className="h-4 w-4" />
<AlertDescription>
This MCP server doesn't require any API credentials to use.
</AlertDescription>
</Alert>
)}
<Alert>
<Shield className="h-4 w-4" />
<AlertDescription>
Your credentials will be encrypted and stored securely. You can create multiple profiles for the same MCP server to handle different use cases.
</AlertDescription>
</Alert>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowCreateProfileDialog(false)}>
Cancel
</Button>
<Button
onClick={handleCreateSubmit}
disabled={!formData.profile_name.trim() || createProfileMutation.isPending}
>
{createProfileMutation.isPending ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Creating...
</>
) : (
<>
<Plus className="h-4 w-4" />
Create Profile
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Dialog>
);
};

View File

@ -206,7 +206,7 @@ const DeleteConfirmationDialog: React.FC<DeleteConfirmationDialogProps> = ({
<Button variant="destructive" onClick={onConfirm} disabled={isDeleting}>
{isDeleting ? (
<>
<Loader2 className="h-4 w-4 animate-spin mr-2" />
<Loader2 className="h-4 w-4 animate-spin" />
Deleting...
</>
) : (