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 && (