mirror of https://github.com/kortix-ai/suna.git
feat: app profile integrations UI
This commit is contained in:
parent
3c920ba0aa
commit
c183a812e4
|
@ -5,21 +5,31 @@ import { Card, CardContent } from '@/components/ui/card';
|
|||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { DataTable, DataTableColumn } from '@/components/ui/data-table';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import {
|
||||
AlertCircle,
|
||||
Settings,
|
||||
User,
|
||||
Plus,
|
||||
Search,
|
||||
X
|
||||
X,
|
||||
Link,
|
||||
Trash2,
|
||||
Edit,
|
||||
Loader2,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
RefreshCw,
|
||||
ExternalLink
|
||||
} from 'lucide-react';
|
||||
import { usePipedreamProfiles } from '@/hooks/react-query/pipedream/use-pipedream-profiles';
|
||||
import { usePipedreamProfiles, useCreatePipedreamProfile, useUpdatePipedreamProfile, useDeletePipedreamProfile, useConnectPipedreamProfile } from '@/hooks/react-query/pipedream/use-pipedream-profiles';
|
||||
import { usePipedreamApps } from '@/hooks/react-query/pipedream/use-pipedream';
|
||||
import { CredentialProfileManager } from '@/components/agents/pipedream/credential-profile-manager';
|
||||
import { PipedreamRegistry } from '@/components/agents/pipedream/pipedream-registry';
|
||||
import { PipedreamConnector } from '@/components/agents/pipedream/pipedream-connector';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { pipedreamKeys } from '@/hooks/react-query/pipedream/keys';
|
||||
import {
|
||||
|
@ -29,26 +39,189 @@ import {
|
|||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { PipedreamProfile } from '@/components/agents/pipedream/pipedream-types';
|
||||
import { toast } from 'sonner';
|
||||
import type { PipedreamProfile, CreateProfileRequest } from '@/components/agents/pipedream/pipedream-types';
|
||||
import type { PipedreamApp } from '@/hooks/react-query/pipedream/utils';
|
||||
|
||||
interface PipedreamConnectionsSectionProps {
|
||||
onConnectNewApp?: (app: { app_slug: string; app_name: string }) => void;
|
||||
}
|
||||
|
||||
interface AppTableProps {
|
||||
appSlug: string;
|
||||
appName: string;
|
||||
profiles: PipedreamProfile[];
|
||||
appImage?: string;
|
||||
onManageProfile: (profile: PipedreamProfile) => void;
|
||||
onConnect: (app: PipedreamApp) => void;
|
||||
onProfileUpdate: (profile: PipedreamProfile, updates: any) => void;
|
||||
onProfileDelete: (profile: PipedreamProfile) => void;
|
||||
onProfileConnect: (profile: PipedreamProfile) => void;
|
||||
isUpdating?: string;
|
||||
isDeleting?: string;
|
||||
isConnecting?: string;
|
||||
allAppsData?: any;
|
||||
}
|
||||
|
||||
const AppTable: React.FC<AppTableProps> = ({ appSlug, appName, profiles, appImage, onManageProfile }) => {
|
||||
const AppTable: React.FC<AppTableProps> = ({
|
||||
appSlug,
|
||||
appName,
|
||||
profiles,
|
||||
appImage,
|
||||
onConnect,
|
||||
onProfileUpdate,
|
||||
onProfileDelete,
|
||||
onProfileConnect,
|
||||
isUpdating,
|
||||
isDeleting,
|
||||
isConnecting,
|
||||
allAppsData
|
||||
}) => {
|
||||
const [editingProfile, setEditingProfile] = useState<string | null>(null);
|
||||
const [editName, setEditName] = useState('');
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState<PipedreamProfile | null>(null);
|
||||
const [showQuickCreate, setShowQuickCreate] = useState(false);
|
||||
const [newProfileName, setNewProfileName] = useState('');
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
|
||||
const createProfile = useCreatePipedreamProfile();
|
||||
const connectProfile = useConnectPipedreamProfile();
|
||||
|
||||
const registryApp = useMemo(() => {
|
||||
return allAppsData?.apps?.find((app: PipedreamApp) =>
|
||||
app.name_slug === appSlug ||
|
||||
app.name.toLowerCase() === appName.toLowerCase()
|
||||
);
|
||||
}, [allAppsData, appSlug, appName]);
|
||||
|
||||
const mockPipedreamApp: PipedreamApp = useMemo(() => ({
|
||||
id: appSlug,
|
||||
name: appName,
|
||||
name_slug: appSlug,
|
||||
auth_type: "oauth",
|
||||
description: `Connect to ${appName}`,
|
||||
img_src: registryApp?.img_src || "",
|
||||
custom_fields_json: registryApp?.custom_fields_json || "[]",
|
||||
categories: registryApp?.categories || [],
|
||||
featured_weight: 0,
|
||||
connect: {
|
||||
allowed_domains: registryApp?.connect?.allowed_domains || null,
|
||||
base_proxy_target_url: registryApp?.connect?.base_proxy_target_url || "",
|
||||
proxy_enabled: registryApp?.connect?.proxy_enabled || false,
|
||||
},
|
||||
}), [appSlug, appName, registryApp]);
|
||||
|
||||
const handleQuickCreate = async () => {
|
||||
if (!newProfileName.trim()) {
|
||||
toast.error('Please enter a profile name');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsCreating(true);
|
||||
try {
|
||||
const request: CreateProfileRequest = {
|
||||
profile_name: newProfileName.trim(),
|
||||
app_slug: appSlug,
|
||||
app_name: appName,
|
||||
is_default: profiles.length === 0,
|
||||
};
|
||||
|
||||
const newProfile = await createProfile.mutateAsync(request);
|
||||
|
||||
// Auto-connect the new profile
|
||||
await connectProfile.mutateAsync({
|
||||
profileId: newProfile.profile_id,
|
||||
app: appSlug,
|
||||
});
|
||||
|
||||
setNewProfileName('');
|
||||
setShowQuickCreate(false);
|
||||
toast.success('Profile created and connected!');
|
||||
} catch (error) {
|
||||
console.error('Error creating profile:', error);
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (profile: PipedreamProfile) => {
|
||||
setEditingProfile(profile.profile_id);
|
||||
setEditName(profile.profile_name);
|
||||
};
|
||||
|
||||
const handleSaveEdit = async (profile: PipedreamProfile) => {
|
||||
if (!editName.trim()) return;
|
||||
|
||||
await onProfileUpdate(profile, { profile_name: editName.trim() });
|
||||
setEditingProfile(null);
|
||||
setEditName('');
|
||||
};
|
||||
|
||||
const handleCancelEdit = () => {
|
||||
setEditingProfile(null);
|
||||
setEditName('');
|
||||
};
|
||||
|
||||
const columns: DataTableColumn<PipedreamProfile>[] = [
|
||||
{
|
||||
id: 'name',
|
||||
header: 'Profile Name',
|
||||
width: 'w-1/2',
|
||||
width: 'w-1/3',
|
||||
cell: (profile) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">{profile.profile_name}</span>
|
||||
{editingProfile === profile.profile_id ? (
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<Input
|
||||
value={editName}
|
||||
onChange={(e) => setEditName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') handleSaveEdit(profile);
|
||||
if (e.key === 'Escape') handleCancelEdit();
|
||||
}}
|
||||
className="h-8 text-sm"
|
||||
autoFocus
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleSaveEdit(profile)}
|
||||
disabled={isUpdating === profile.profile_id}
|
||||
className="h-8 px-2"
|
||||
>
|
||||
{isUpdating === profile.profile_id ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
<CheckCircle2 className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={handleCancelEdit}
|
||||
className="h-8 px-2"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<span className="font-medium">{profile.profile_name}</span>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
@ -69,6 +242,11 @@ const AppTable: React.FC<AppTableProps> = ({ appSlug, appName, profiles, appImag
|
|||
Disconnected
|
||||
</div>
|
||||
)}
|
||||
{profile.is_default && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Default
|
||||
</Badge>
|
||||
)}
|
||||
{!profile.is_active && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Inactive
|
||||
|
@ -80,22 +258,81 @@ const AppTable: React.FC<AppTableProps> = ({ appSlug, appName, profiles, appImag
|
|||
{
|
||||
id: 'actions',
|
||||
header: 'Actions',
|
||||
width: 'w-1/4',
|
||||
width: 'w-1/3',
|
||||
headerClassName: 'text-right',
|
||||
className: 'text-right',
|
||||
cell: (profile) => (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onManageProfile(profile);
|
||||
}}
|
||||
className="h-8 px-3 text-xs"
|
||||
>
|
||||
<Settings className="h-4 w-4 mr-1" />
|
||||
Manage
|
||||
</Button>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
{!profile.is_connected && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => onProfileConnect(profile)}
|
||||
disabled={isConnecting === profile.profile_id}
|
||||
className="h-8 px-2 text-xs"
|
||||
>
|
||||
{isConnecting === profile.profile_id ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
<Link className="h-3 w-3" />
|
||||
)}
|
||||
Connect
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => handleEdit(profile)}>
|
||||
<Edit className="h-4 w-4" />
|
||||
Edit Name
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => onProfileUpdate(profile, { is_default: !profile.is_default })}
|
||||
>
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
{profile.is_default ? 'Remove Default' : 'Set as Default'}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => onProfileUpdate(profile, { is_active: !profile.is_active })}
|
||||
>
|
||||
{profile.is_active ? (
|
||||
<>
|
||||
<XCircle className="h-4 w-4" />
|
||||
Deactivate
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
Activate
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
{profile.is_connected && (
|
||||
<DropdownMenuItem onClick={() => onProfileConnect(profile)}>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
Reconnect
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => setShowDeleteDialog(profile)}
|
||||
className="text-destructive"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
@ -131,6 +368,61 @@ const AppTable: React.FC<AppTableProps> = ({ appSlug, appName, profiles, appImag
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{showQuickCreate ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
placeholder="Profile name"
|
||||
value={newProfileName}
|
||||
onChange={(e) => setNewProfileName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') handleQuickCreate();
|
||||
if (e.key === 'Escape') {
|
||||
setShowQuickCreate(false);
|
||||
setNewProfileName('');
|
||||
}
|
||||
}}
|
||||
className="h-8 text-sm w-32"
|
||||
autoFocus
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleQuickCreate}
|
||||
disabled={isCreating}
|
||||
className="h-8 px-2"
|
||||
>
|
||||
{isCreating ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
<CheckCircle2 className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setShowQuickCreate(false);
|
||||
setNewProfileName('');
|
||||
}}
|
||||
className="h-8 px-2"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="link"
|
||||
onClick={() => onConnect(mockPipedreamApp)}
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
New Profile
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
|
@ -139,48 +431,114 @@ const AppTable: React.FC<AppTableProps> = ({ appSlug, appName, profiles, appImag
|
|||
emptyMessage={`No ${appName} profiles found`}
|
||||
className="bg-card border rounded-lg"
|
||||
/>
|
||||
|
||||
<AlertDialog open={!!showDeleteDialog} onOpenChange={(open) => !open && setShowDeleteDialog(null)}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Profile</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete "{showDeleteDialog?.profile_name}"? This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => {
|
||||
if (showDeleteDialog) {
|
||||
onProfileDelete(showDeleteDialog);
|
||||
setShowDeleteDialog(null);
|
||||
}
|
||||
}}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface PipedreamConnectionsSectionProps {
|
||||
onConnectNewApp?: (app: { app_slug: string; app_name: string }) => void;
|
||||
}
|
||||
|
||||
export const PipedreamConnectionsSection: React.FC<PipedreamConnectionsSectionProps> = ({
|
||||
onConnectNewApp
|
||||
}) => {
|
||||
const [showProfileManager, setShowProfileManager] = useState(false);
|
||||
const [showAppBrowser, setShowAppBrowser] = useState(false);
|
||||
const [selectedAppForProfile, setSelectedAppForProfile] = useState<{ app_slug: string; app_name: string } | null>(null);
|
||||
const [selectedProfile, setSelectedProfile] = useState<PipedreamProfile | null>(null);
|
||||
const [showConnector, setShowConnector] = useState(false);
|
||||
const [selectedApp, setSelectedApp] = useState<PipedreamApp | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [isUpdating, setIsUpdating] = useState<string | null>(null);
|
||||
const [isDeleting, setIsDeleting] = useState<string | null>(null);
|
||||
const [isConnecting, setIsConnecting] = useState<string | null>(null);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const { data: profiles, isLoading, error } = usePipedreamProfiles();
|
||||
const { data: allAppsData } = usePipedreamApps(undefined, '');
|
||||
const updateProfile = useUpdatePipedreamProfile();
|
||||
const deleteProfile = useDeletePipedreamProfile();
|
||||
const connectProfile = useConnectPipedreamProfile();
|
||||
|
||||
const handleAppSelect = (app: { app_slug: string; app_name: string }) => {
|
||||
setShowAppBrowser(false);
|
||||
setSelectedAppForProfile(app);
|
||||
if (onConnectNewApp) {
|
||||
onConnectNewApp(app);
|
||||
}
|
||||
setShowProfileManager(true);
|
||||
};
|
||||
|
||||
const handleManageProfile = (profile: PipedreamProfile) => {
|
||||
setSelectedProfile(profile);
|
||||
setSelectedAppForProfile({ app_slug: profile.app_slug, app_name: profile.app_name });
|
||||
setShowProfileManager(true);
|
||||
const handleConnect = (app: PipedreamApp) => {
|
||||
setSelectedApp(app);
|
||||
setShowConnector(true);
|
||||
};
|
||||
|
||||
const handleProfileManagerClose = () => {
|
||||
setShowProfileManager(false);
|
||||
setSelectedAppForProfile(null);
|
||||
setSelectedProfile(null);
|
||||
const handleConnectionComplete = (profileId: string, selectedTools: string[], appName: string, appSlug: string) => {
|
||||
setShowConnector(false);
|
||||
setSelectedApp(null);
|
||||
toast.success(`Connected to ${appName}!`);
|
||||
queryClient.invalidateQueries({ queryKey: pipedreamKeys.profiles.all() });
|
||||
};
|
||||
|
||||
const handleProfileUpdate = async (profile: PipedreamProfile, updates: any) => {
|
||||
setIsUpdating(profile.profile_id);
|
||||
try {
|
||||
await updateProfile.mutateAsync({
|
||||
profileId: profile.profile_id,
|
||||
request: updates,
|
||||
});
|
||||
toast.success('Profile updated successfully');
|
||||
} catch (error) {
|
||||
console.error('Error updating profile:', error);
|
||||
} finally {
|
||||
setIsUpdating(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleProfileDelete = async (profile: PipedreamProfile) => {
|
||||
setIsDeleting(profile.profile_id);
|
||||
try {
|
||||
await deleteProfile.mutateAsync(profile.profile_id);
|
||||
toast.success('Profile deleted successfully');
|
||||
} catch (error) {
|
||||
console.error('Error deleting profile:', error);
|
||||
} finally {
|
||||
setIsDeleting(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleProfileConnect = async (profile: PipedreamProfile) => {
|
||||
setIsConnecting(profile.profile_id);
|
||||
try {
|
||||
await connectProfile.mutateAsync({
|
||||
profileId: profile.profile_id,
|
||||
app: profile.app_slug,
|
||||
});
|
||||
toast.success('Profile connected successfully');
|
||||
} catch (error) {
|
||||
console.error('Error connecting profile:', error);
|
||||
} finally {
|
||||
setIsConnecting(null);
|
||||
}
|
||||
};
|
||||
|
||||
const profilesByApp = profiles?.reduce((acc, profile) => {
|
||||
const key = profile.app_slug;
|
||||
if (!acc[key]) {
|
||||
|
@ -311,7 +669,7 @@ export const PipedreamConnectionsSection: React.FC<PipedreamConnectionsSectionPr
|
|||
placeholder="Search apps and profiles..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 pr-10 h-9"
|
||||
className="pl-12 h-12 rounded-xl bg-muted/50 border-0 focus:bg-background focus:ring-2 focus:ring-primary/20 transition-all"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<Button
|
||||
|
@ -324,8 +682,6 @@ export const PipedreamConnectionsSection: React.FC<PipedreamConnectionsSectionPr
|
|||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Results */}
|
||||
{Object.keys(filteredProfilesByApp).length === 0 ? (
|
||||
<Card className="border-dashed">
|
||||
<CardContent className="p-8 text-center">
|
||||
|
@ -359,7 +715,7 @@ export const PipedreamConnectionsSection: React.FC<PipedreamConnectionsSectionPr
|
|||
return b.length - a.length;
|
||||
})
|
||||
.map(([appSlug, appProfiles]) => {
|
||||
const registryApp = allAppsData?.apps?.find(app =>
|
||||
const registryApp = allAppsData?.apps?.find((app: PipedreamApp) =>
|
||||
app.name_slug === appSlug ||
|
||||
app.name.toLowerCase() === appProfiles[0].app_name.toLowerCase()
|
||||
);
|
||||
|
@ -371,7 +727,14 @@ export const PipedreamConnectionsSection: React.FC<PipedreamConnectionsSectionPr
|
|||
appName={appProfiles[0].app_name}
|
||||
profiles={appProfiles}
|
||||
appImage={registryApp?.img_src}
|
||||
onManageProfile={handleManageProfile}
|
||||
onConnect={handleConnect}
|
||||
onProfileUpdate={handleProfileUpdate}
|
||||
onProfileDelete={handleProfileDelete}
|
||||
onProfileConnect={handleProfileConnect}
|
||||
isUpdating={isUpdating}
|
||||
isDeleting={isDeleting}
|
||||
isConnecting={isConnecting}
|
||||
allAppsData={allAppsData}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
@ -379,7 +742,7 @@ export const PipedreamConnectionsSection: React.FC<PipedreamConnectionsSectionPr
|
|||
)}
|
||||
|
||||
<Dialog open={showAppBrowser} onOpenChange={setShowAppBrowser}>
|
||||
<DialogContent className="p-0 max-w-3xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogContent className="p-0 max-w-6xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader className="sr-only">
|
||||
<DialogTitle>Browse Apps</DialogTitle>
|
||||
<DialogDescription>
|
||||
|
@ -387,38 +750,21 @@ export const PipedreamConnectionsSection: React.FC<PipedreamConnectionsSectionPr
|
|||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<PipedreamRegistry
|
||||
mode="simple"
|
||||
mode='profile-only'
|
||||
onAppSelected={handleAppSelect}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={showProfileManager} onOpenChange={handleProfileManagerClose}>
|
||||
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{selectedAppForProfile
|
||||
? `Manage ${selectedAppForProfile.app_name} Profiles`
|
||||
: 'Manage Pipedream Profiles'
|
||||
}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{selectedAppForProfile
|
||||
? `Create and manage credential profiles for ${selectedAppForProfile.app_name}`
|
||||
: 'Create and manage credential profiles for your Pipedream apps'
|
||||
}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<CredentialProfileManager
|
||||
appSlug={selectedAppForProfile?.app_slug}
|
||||
appName={selectedAppForProfile?.app_name}
|
||||
onProfileSelect={() => {
|
||||
queryClient.invalidateQueries({ queryKey: pipedreamKeys.profiles.all() });
|
||||
handleProfileManagerClose();
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{selectedApp && (
|
||||
<PipedreamConnector
|
||||
app={selectedApp}
|
||||
open={showConnector}
|
||||
onOpenChange={setShowConnector}
|
||||
onComplete={handleConnectionComplete}
|
||||
mode="profile-only"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -222,14 +222,16 @@ export const PipedreamConnector: React.FC<PipedreamConnectorProps> = ({
|
|||
<div>
|
||||
<h3 className="text-lg font-semibold">Connect to {app.name}</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{connectedProfiles.length > 0
|
||||
? 'Select a profile or create a new one to connect different accounts'
|
||||
: 'Create your first profile to get started'
|
||||
{mode === 'profile-only'
|
||||
? 'Create a new profile to connect your account'
|
||||
: (connectedProfiles.length > 0
|
||||
? 'Select a profile or create a new one to connect different accounts'
|
||||
: 'Create your first profile to get started')
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{connectedProfiles.length > 0 && !isCreatingProfile && (
|
||||
{mode !== 'profile-only' && connectedProfiles.length > 0 && !isCreatingProfile && (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="profile-select">Select Profile</Label>
|
||||
|
@ -274,7 +276,7 @@ export const PipedreamConnector: React.FC<PipedreamConnectorProps> = ({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{(connectedProfiles.length === 0 || isCreatingProfile) && (
|
||||
{(mode === 'profile-only' || connectedProfiles.length === 0 || isCreatingProfile) && (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="profile-name">Profile Name</Label>
|
||||
|
@ -284,12 +286,12 @@ export const PipedreamConnector: React.FC<PipedreamConnectorProps> = ({
|
|||
value={newProfileName}
|
||||
onChange={handleProfileNameChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
autoFocus={isCreatingProfile}
|
||||
autoFocus={mode === 'profile-only' || isCreatingProfile}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
{isCreatingProfile && (
|
||||
{mode !== 'profile-only' && isCreatingProfile && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
|
@ -304,7 +306,7 @@ export const PipedreamConnector: React.FC<PipedreamConnectorProps> = ({
|
|||
<Button
|
||||
onClick={handleCreateProfile}
|
||||
disabled={!newProfileName.trim() || isConnecting}
|
||||
className="flex-1"
|
||||
className={mode === 'profile-only' ? 'w-full' : 'flex-1'}
|
||||
>
|
||||
{isConnecting ? (
|
||||
<>
|
||||
|
@ -322,10 +324,10 @@ export const PipedreamConnector: React.FC<PipedreamConnectorProps> = ({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{selectedProfileId && !isCreatingProfile && (
|
||||
{mode !== 'profile-only' && selectedProfileId && !isCreatingProfile && (
|
||||
<div className="pt-4 border-t">
|
||||
<Button
|
||||
onClick={mode === 'profile-only' ? handleProfileOnlyComplete : proceedToTools}
|
||||
onClick={proceedToTools}
|
||||
disabled={!selectedProfileId || isCompletingConnection}
|
||||
className="w-full"
|
||||
>
|
||||
|
@ -334,11 +336,6 @@ export const PipedreamConnector: React.FC<PipedreamConnectorProps> = ({
|
|||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Connecting...
|
||||
</>
|
||||
) : mode === 'profile-only' ? (
|
||||
<>
|
||||
<Zap className="h-4 w-4" />
|
||||
Complete Connection
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Continue to Tools
|
||||
|
|
|
@ -17,7 +17,7 @@ interface PipedreamRegistryProps {
|
|||
onProfileSelected?: (profile: PipedreamProfile) => void;
|
||||
onToolsSelected?: (profileId: string, selectedTools: string[], appName: string, appSlug: string) => void;
|
||||
onAppSelected?: (app: { app_slug: string; app_name: string }) => void;
|
||||
mode?: 'full' | 'simple';
|
||||
mode?: 'full' | 'simple' | 'profile-only';
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
|
@ -320,6 +320,33 @@ export const PipedreamRegistry: React.FC<PipedreamRegistryProps> = ({
|
|||
<Plus className="h-3 w-3" />
|
||||
Connect
|
||||
</Button>
|
||||
) : mode === 'profile-only' ? (
|
||||
<>
|
||||
{connectedProfiles.length > 0 ? (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleConnectApp(app);
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
Add Profile
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleConnectApp(app);
|
||||
}}
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
Connect
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{connectedProfiles.length > 0 ? (
|
||||
|
@ -463,14 +490,19 @@ export const PipedreamRegistry: React.FC<PipedreamRegistryProps> = ({
|
|||
<div className="flex items-center gap-3">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-xl font-semibold text-gray-900 dark:text-white">Integrations</h1>
|
||||
<h1 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
Integrations
|
||||
</h1>
|
||||
<Badge variant="secondary" className="bg-blue-50 text-blue-700 border-blue-200 dark:border-blue-900 dark:bg-blue-900/20 dark:text-blue-400 text-xs">
|
||||
<Sparkles className="h-3 w-3" />
|
||||
New
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-gray-600 dark:text-gray-400 text-sm">
|
||||
Connect your favorite tools and automate workflows
|
||||
{mode === 'profile-only'
|
||||
? 'Manage your accounts and credential profiles'
|
||||
: 'Connect your favorite tools and automate workflows'
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -495,7 +527,9 @@ export const PipedreamRegistry: React.FC<PipedreamRegistryProps> = ({
|
|||
<div className="mb-6">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<User className="h-4 w-4 text-green-600 dark:text-green-400" />
|
||||
<h2 className="text-md font-semibold text-gray-900 dark:text-white">My Connections</h2>
|
||||
<h2 className="text-md font-semibold text-gray-900 dark:text-white">
|
||||
{mode === 'profile-only' ? 'Connected Accounts' : 'My Connections'}
|
||||
</h2>
|
||||
<Badge variant="secondary" className="bg-green-50 text-green-700 border-green-200 dark:border-green-900 dark:bg-green-900/20 dark:text-green-400 text-xs">
|
||||
{connectedApps.length}
|
||||
</Badge>
|
||||
|
@ -513,10 +547,12 @@ export const PipedreamRegistry: React.FC<PipedreamRegistryProps> = ({
|
|||
<div className="mb-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<TrendingUp className="h-4 w-4 text-orange-600 dark:text-orange-400" />
|
||||
<h2 className="text-md font-semibold text-gray-900 dark:text-white">Popular</h2>
|
||||
<h2 className="text-md font-semibold text-gray-900 dark:text-white">
|
||||
{mode === 'profile-only' ? 'Available Apps' : 'Popular'}
|
||||
</h2>
|
||||
<Badge variant="secondary" className="bg-orange-50 text-orange-700 border-orange-200 dark:border-orange-900 dark:bg-orange-900/20 dark:text-orange-400 text-xs">
|
||||
<Star className="h-3 w-3 mr-1" />
|
||||
Recommended
|
||||
{mode === 'profile-only' ? 'Connect' : 'Recommended'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -550,8 +586,10 @@ export const PipedreamRegistry: React.FC<PipedreamRegistryProps> = ({
|
|||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">No integrations found</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4 max-w-md mx-auto">
|
||||
{selectedCategory !== 'All'
|
||||
? `No integrations found in "${selectedCategory}" category. Try a different category or search term.`
|
||||
: "Try adjusting your search criteria or browse our popular integrations."
|
||||
? `No ${mode === 'profile-only' ? 'apps' : 'integrations'} found in "${selectedCategory}" category. Try a different category or search term.`
|
||||
: mode === 'profile-only'
|
||||
? "Try adjusting your search criteria or browse available apps."
|
||||
: "Try adjusting your search criteria or browse our popular integrations."
|
||||
}
|
||||
</p>
|
||||
<Button
|
||||
|
@ -621,6 +659,7 @@ export const PipedreamRegistry: React.FC<PipedreamRegistryProps> = ({
|
|||
open={showStreamlinedConnector}
|
||||
onOpenChange={setShowStreamlinedConnector}
|
||||
onComplete={handleConnectionComplete}
|
||||
mode={mode === 'profile-only' ? 'profile-only' : 'full'}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue