From c183a812e494047d2f971ca8e201d60e462a87a9 Mon Sep 17 00:00:00 2001 From: Saumya Date: Fri, 11 Jul 2025 10:13:57 +0530 Subject: [PATCH] feat: app profile integrations UI --- .../pipedream-connections-section.tsx | 488 +++++++++++++++--- .../agents/pipedream/pipedream-connector.tsx | 27 +- .../agents/pipedream/pipedream-registry.tsx | 55 +- 3 files changed, 476 insertions(+), 94 deletions(-) diff --git a/frontend/src/components/agents/pipedream/pipedream-connections-section.tsx b/frontend/src/components/agents/pipedream/pipedream-connections-section.tsx index b9be8e54..31fb5c4e 100644 --- a/frontend/src/components/agents/pipedream/pipedream-connections-section.tsx +++ b/frontend/src/components/agents/pipedream/pipedream-connections-section.tsx @@ -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 = ({ appSlug, appName, profiles, appImage, onManageProfile }) => { +const AppTable: React.FC = ({ + appSlug, + appName, + profiles, + appImage, + onConnect, + onProfileUpdate, + onProfileDelete, + onProfileConnect, + isUpdating, + isDeleting, + isConnecting, + allAppsData +}) => { + const [editingProfile, setEditingProfile] = useState(null); + const [editName, setEditName] = useState(''); + const [showDeleteDialog, setShowDeleteDialog] = useState(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[] = [ { id: 'name', header: 'Profile Name', - width: 'w-1/2', + width: 'w-1/3', cell: (profile) => (
- {profile.profile_name} + {editingProfile === profile.profile_id ? ( +
+ setEditName(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') handleSaveEdit(profile); + if (e.key === 'Escape') handleCancelEdit(); + }} + className="h-8 text-sm" + autoFocus + /> + + +
+ ) : ( + {profile.profile_name} + )}
), }, @@ -69,6 +242,11 @@ const AppTable: React.FC = ({ appSlug, appName, profiles, appImag Disconnected )} + {profile.is_default && ( + + Default + + )} {!profile.is_active && ( Inactive @@ -80,22 +258,81 @@ const AppTable: React.FC = ({ appSlug, appName, profiles, appImag { id: 'actions', header: 'Actions', - width: 'w-1/4', + width: 'w-1/3', headerClassName: 'text-right', className: 'text-right', cell: (profile) => ( - +
+ {!profile.is_connected && ( + + )} + + + + + + + handleEdit(profile)}> + + Edit Name + + onProfileUpdate(profile, { is_default: !profile.is_default })} + > + + {profile.is_default ? 'Remove Default' : 'Set as Default'} + + onProfileUpdate(profile, { is_active: !profile.is_active })} + > + {profile.is_active ? ( + <> + + Deactivate + + ) : ( + <> + + Activate + + )} + + {profile.is_connected && ( + onProfileConnect(profile)}> + + Reconnect + + )} + + setShowDeleteDialog(profile)} + className="text-destructive" + > + + Delete + + + +
), }, ]; @@ -131,6 +368,61 @@ const AppTable: React.FC = ({ appSlug, appName, profiles, appImag

+ +
+ {showQuickCreate ? ( +
+ 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 + /> + + +
+ ) : ( + <> + + + )} +
= ({ appSlug, appName, profiles, appImag emptyMessage={`No ${appName} profiles found`} className="bg-card border rounded-lg" /> + + !open && setShowDeleteDialog(null)}> + + + Delete Profile + + Are you sure you want to delete "{showDeleteDialog?.profile_name}"? This action cannot be undone. + + + + Cancel + { + if (showDeleteDialog) { + onProfileDelete(showDeleteDialog); + setShowDeleteDialog(null); + } + }} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + Delete + + + + ); }; -interface PipedreamConnectionsSectionProps { - onConnectNewApp?: (app: { app_slug: string; app_name: string }) => void; -} - export const PipedreamConnectionsSection: React.FC = ({ 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(null); + const [showConnector, setShowConnector] = useState(false); + const [selectedApp, setSelectedApp] = useState(null); const [searchQuery, setSearchQuery] = useState(''); + const [isUpdating, setIsUpdating] = useState(null); + const [isDeleting, setIsDeleting] = useState(null); + const [isConnecting, setIsConnecting] = useState(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 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 && ( + ) : mode === 'profile-only' ? ( + <> + {connectedProfiles.length > 0 ? ( + + ) : ( + + )} + ) : ( <> {connectedProfiles.length > 0 ? ( @@ -463,14 +490,19 @@ export const PipedreamRegistry: React.FC = ({
-

Integrations

+

+ Integrations +

New

- Connect your favorite tools and automate workflows + {mode === 'profile-only' + ? 'Manage your accounts and credential profiles' + : 'Connect your favorite tools and automate workflows' + }

@@ -495,7 +527,9 @@ export const PipedreamRegistry: React.FC = ({
-

My Connections

+

+ {mode === 'profile-only' ? 'Connected Accounts' : 'My Connections'} +

{connectedApps.length} @@ -513,10 +547,12 @@ export const PipedreamRegistry: React.FC = ({
-

Popular

+

+ {mode === 'profile-only' ? 'Available Apps' : 'Popular'} +

- Recommended + {mode === 'profile-only' ? 'Connect' : 'Recommended'}
@@ -550,8 +586,10 @@ export const PipedreamRegistry: React.FC = ({

No integrations found

{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." }