mirror of https://github.com/kortix-ai/suna.git
ui revamp
This commit is contained in:
parent
164a647f9e
commit
84541d9f1d
|
@ -354,37 +354,54 @@ async def get_available_pipedream_tools(
|
||||||
|
|
||||||
@router.get("/apps", response_model=Dict[str, Any])
|
@router.get("/apps", response_model=Dict[str, Any])
|
||||||
async def get_pipedream_apps(
|
async def get_pipedream_apps(
|
||||||
page: int = Query(1, ge=1),
|
after: Optional[str] = Query(None, description="Cursor for pagination"),
|
||||||
q: Optional[str] = Query(None),
|
q: Optional[str] = Query(None),
|
||||||
category: Optional[str] = Query(None)
|
category: Optional[str] = Query(None)
|
||||||
):
|
):
|
||||||
logger.info(f"Fetching Pipedream apps registry, page: {page}, search: {q}")
|
logger.info(f"Fetching Pipedream apps registry, after: {after}, search: {q}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import httpx
|
client = get_pipedream_client()
|
||||||
|
access_token = await client._obtain_access_token()
|
||||||
|
await client._ensure_rate_limit_token()
|
||||||
|
|
||||||
async with httpx.AsyncClient() as client:
|
url = f"https://api.pipedream.com/v1/apps"
|
||||||
url = f"https://mcp.pipedream.com/api/apps"
|
params = {}
|
||||||
params = {"page": page}
|
|
||||||
|
|
||||||
|
if after:
|
||||||
|
params["after"] = after
|
||||||
if q:
|
if q:
|
||||||
params["q"] = q
|
params["q"] = q
|
||||||
if category:
|
if category:
|
||||||
params["category"] = category
|
params["category"] = category
|
||||||
|
|
||||||
response = await client.get(url, params=params, timeout=30.0)
|
headers = {
|
||||||
response.raise_for_status()
|
"Authorization": f"Bearer {access_token}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await client._make_request_with_retry(
|
||||||
|
"GET",
|
||||||
|
url,
|
||||||
|
headers=headers,
|
||||||
|
params=params
|
||||||
|
)
|
||||||
|
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
# print(data)
|
page_info = data.get("page_info", {})
|
||||||
|
|
||||||
|
# For cursor-based pagination, has_more is determined by presence of end_cursor
|
||||||
|
page_info["has_more"] = bool(page_info.get("end_cursor"))
|
||||||
|
|
||||||
logger.info(f"Successfully fetched {len(data.get('data', []))} apps from Pipedream registry")
|
logger.info(f"Successfully fetched {len(data.get('data', []))} apps from Pipedream registry")
|
||||||
|
logger.info(f"Pagination: after={after}, total_count={page_info.get('total_count', 0)}, current_count={page_info.get('count', 0)}, has_more={page_info['has_more']}, end_cursor={page_info.get('end_cursor', 'None')}")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"apps": data.get("data", []),
|
"apps": data.get("data", []),
|
||||||
"page_info": data.get("page_info", {}),
|
"page_info": page_info,
|
||||||
"total_count": data.get("page_info", {}).get("total_count", 0)
|
"total_count": page_info.get("total_count", 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
@ -81,11 +81,11 @@ export const MCPConfigurationNew: React.FC<MCPConfigurationProps> = ({
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-2 justify-center">
|
<div className="flex gap-2 justify-center">
|
||||||
<Button onClick={() => setShowRegistryDialog(true)} variant="default">
|
<Button onClick={() => setShowRegistryDialog(true)} variant="default">
|
||||||
<Store className="h-4 w-4 mr-2" />
|
<Store className="h-4 w-4" />
|
||||||
Browse Apps
|
Browse Apps
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => setShowCustomDialog(true)} variant="outline">
|
<Button onClick={() => setShowCustomDialog(true)} variant="outline">
|
||||||
<Server className="h-4 w-4 mr-2" />
|
<Server className="h-4 w-4" />
|
||||||
Custom MCP
|
Custom MCP
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Search, Loader2, Grid, User, CheckCircle2, Plus, X, Settings, Star } from 'lucide-react';
|
import { Search, Loader2, Grid, User, CheckCircle2, Plus, X, Settings, Star, Sparkles, TrendingUp, Filter, ChevronRight, ChevronLeft, ArrowRight } from 'lucide-react';
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
|
||||||
import { usePipedreamApps } from '@/hooks/react-query/pipedream/use-pipedream';
|
import { usePipedreamApps } from '@/hooks/react-query/pipedream/use-pipedream';
|
||||||
import { usePipedreamProfiles } from '@/hooks/react-query/pipedream/use-pipedream-profiles';
|
import { usePipedreamProfiles } from '@/hooks/react-query/pipedream/use-pipedream-profiles';
|
||||||
|
@ -25,6 +25,45 @@ interface PipedreamRegistryProps {
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const categoryEmojis: Record<string, string> = {
|
||||||
|
'All': '🌟',
|
||||||
|
'Communication': '💬',
|
||||||
|
'Artificial Intelligence (AI)': '🤖',
|
||||||
|
'Social Media': '📱',
|
||||||
|
'CRM': '👥',
|
||||||
|
'Marketing': '📈',
|
||||||
|
'Analytics': '📊',
|
||||||
|
'Commerce': '📊',
|
||||||
|
'Databases': '🗄️',
|
||||||
|
'File Storage': '🗂️',
|
||||||
|
'Help Desk & Support': '🎧',
|
||||||
|
'Infrastructure & Cloud': '🌐',
|
||||||
|
'E-commerce': '🛒',
|
||||||
|
'Developer Tools': '🔧',
|
||||||
|
'Productivity': '⚡',
|
||||||
|
'Finance': '💰',
|
||||||
|
'Email': '📧',
|
||||||
|
'Project Management': '📋',
|
||||||
|
'Storage': '💾',
|
||||||
|
'AI/ML': '🤖',
|
||||||
|
'Data & Databases': '🗄️',
|
||||||
|
'Video': '🎥',
|
||||||
|
'Calendar': '📅',
|
||||||
|
'Forms': '📝',
|
||||||
|
'Security': '🔒',
|
||||||
|
'HR': '👔',
|
||||||
|
'Sales': '💼',
|
||||||
|
'Support': '🎧',
|
||||||
|
'Design': '🎨',
|
||||||
|
'Business Intelligence': '📈',
|
||||||
|
'Automation': '🔄',
|
||||||
|
'News': '📰',
|
||||||
|
'Weather': '🌤️',
|
||||||
|
'Travel': '✈️',
|
||||||
|
'Education': '🎓',
|
||||||
|
'Health': '🏥',
|
||||||
|
};
|
||||||
|
|
||||||
export const PipedreamRegistry: React.FC<PipedreamRegistryProps> = ({
|
export const PipedreamRegistry: React.FC<PipedreamRegistryProps> = ({
|
||||||
onProfileSelected,
|
onProfileSelected,
|
||||||
onToolsSelected,
|
onToolsSelected,
|
||||||
|
@ -34,63 +73,134 @@ export const PipedreamRegistry: React.FC<PipedreamRegistryProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [selectedCategory, setSelectedCategory] = useState<string>('All');
|
const [selectedCategory, setSelectedCategory] = useState<string>('All');
|
||||||
const [page, setPage] = useState(1);
|
const [after, setAfter] = useState<string | undefined>(undefined);
|
||||||
|
const [paginationHistory, setPaginationHistory] = useState<string[]>([]);
|
||||||
const [showToolSelector, setShowToolSelector] = useState(false);
|
const [showToolSelector, setShowToolSelector] = useState(false);
|
||||||
const [selectedProfile, setSelectedProfile] = useState<PipedreamProfile | null>(null);
|
const [selectedProfile, setSelectedProfile] = useState<PipedreamProfile | null>(null);
|
||||||
const [showProfileManager, setShowProfileManager] = useState(false);
|
const [showProfileManager, setShowProfileManager] = useState(false);
|
||||||
const [selectedAppForProfile, setSelectedAppForProfile] = useState<{ app_slug: string; app_name: string } | null>(null);
|
const [selectedAppForProfile, setSelectedAppForProfile] = useState<{ app_slug: string; app_name: string } | null>(null);
|
||||||
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { data: appsData, isLoading, error, refetch } = usePipedreamApps(page, search, selectedCategory === 'All' ? '' : selectedCategory);
|
const { data: appsData, isLoading, error, refetch } = usePipedreamApps(after, search);
|
||||||
const { data: profiles } = usePipedreamProfiles();
|
const { data: profiles } = usePipedreamProfiles();
|
||||||
|
|
||||||
const { data: allAppsData } = usePipedreamApps(1, '', '');
|
const { data: allAppsData } = usePipedreamApps(undefined, '');
|
||||||
|
|
||||||
|
// Get all apps data for categories and matching
|
||||||
|
const allApps = useMemo(() => {
|
||||||
|
return allAppsData?.apps || [];
|
||||||
|
}, [allAppsData?.apps]);
|
||||||
|
|
||||||
const categories = useMemo(() => {
|
const categories = useMemo(() => {
|
||||||
const dataToUse = allAppsData?.apps || appsData?.apps || [];
|
|
||||||
const categorySet = new Set<string>();
|
const categorySet = new Set<string>();
|
||||||
dataToUse.forEach((app: PipedreamApp) => {
|
allApps.forEach((app: PipedreamApp) => {
|
||||||
app.categories.forEach(cat => categorySet.add(cat));
|
app.categories.forEach(cat => categorySet.add(cat));
|
||||||
});
|
});
|
||||||
const sortedCategories = Array.from(categorySet).sort();
|
const sortedCategories = Array.from(categorySet).sort();
|
||||||
return ['All', ...sortedCategories];
|
return ['All', ...sortedCategories];
|
||||||
}, [allAppsData?.apps, appsData?.apps]);
|
}, [allApps]);
|
||||||
|
|
||||||
const connectedProfiles = useMemo(() => {
|
const connectedProfiles = useMemo(() => {
|
||||||
return profiles?.filter(p => p.is_connected) || [];
|
return profiles?.filter(p => p.is_connected) || [];
|
||||||
}, [profiles]);
|
}, [profiles]);
|
||||||
|
|
||||||
|
const filteredAppsData = useMemo(() => {
|
||||||
|
if (!appsData) return appsData;
|
||||||
|
|
||||||
|
if (selectedCategory === 'All') {
|
||||||
|
return appsData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredApps = appsData.apps.filter((app: PipedreamApp) =>
|
||||||
|
app.categories.includes(selectedCategory)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...appsData,
|
||||||
|
apps: filteredApps,
|
||||||
|
page_info: {
|
||||||
|
...appsData.page_info,
|
||||||
|
count: filteredApps.length
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [appsData, selectedCategory]);
|
||||||
|
|
||||||
const popularApps = useMemo(() => {
|
const popularApps = useMemo(() => {
|
||||||
const dataToUse = allAppsData?.apps || appsData?.apps || [];
|
return allApps
|
||||||
return dataToUse
|
|
||||||
.filter((app: PipedreamApp) => app.featured_weight > 0)
|
.filter((app: PipedreamApp) => app.featured_weight > 0)
|
||||||
.sort((a: PipedreamApp, b: PipedreamApp) => b.featured_weight - a.featured_weight)
|
.sort((a: PipedreamApp, b: PipedreamApp) => b.featured_weight - a.featured_weight)
|
||||||
.slice(0, 6);
|
.slice(0, 6);
|
||||||
}, [allAppsData?.apps, appsData?.apps]);
|
}, [allApps]);
|
||||||
|
|
||||||
// Create apps from connected profiles for display
|
// Create apps from connected profiles with images from registry
|
||||||
const connectedApps = useMemo(() => {
|
const connectedApps = useMemo(() => {
|
||||||
return connectedProfiles.map(profile => ({
|
return connectedProfiles.map(profile => {
|
||||||
|
// Try to find the app in the registry by matching name_slug
|
||||||
|
const registryApp = allApps.find(app =>
|
||||||
|
app.name_slug === profile.app_slug ||
|
||||||
|
app.name.toLowerCase() === profile.app_name.toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
id: profile.profile_id,
|
id: profile.profile_id,
|
||||||
name: profile.app_name,
|
name: profile.app_name,
|
||||||
name_slug: profile.app_slug,
|
name_slug: profile.app_slug,
|
||||||
app_hid: profile.app_slug,
|
auth_type: "keys",
|
||||||
description: `Access your ${profile.app_name} workspace (Gmail, Calendar, Drive, etc.)`,
|
description: `Access your ${profile.app_name} workspace and tools`,
|
||||||
categories: [],
|
img_src: registryApp?.img_src || "",
|
||||||
|
custom_fields_json: registryApp?.custom_fields_json || "[]",
|
||||||
|
categories: registryApp?.categories || [],
|
||||||
featured_weight: 0,
|
featured_weight: 0,
|
||||||
api_docs_url: null,
|
connect: {
|
||||||
status: 1,
|
allowed_domains: registryApp?.connect?.allowed_domains || null,
|
||||||
}));
|
base_proxy_target_url: registryApp?.connect?.base_proxy_target_url || "",
|
||||||
}, [connectedProfiles]);
|
proxy_enabled: registryApp?.connect?.proxy_enabled || false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [connectedProfiles, allApps]);
|
||||||
|
|
||||||
const handleSearch = (value: string) => {
|
const handleSearch = (value: string) => {
|
||||||
setSearch(value);
|
setSearch(value);
|
||||||
setPage(1);
|
setAfter(undefined);
|
||||||
|
setPaginationHistory([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCategorySelect = (category: string) => {
|
const handleCategorySelect = (category: string) => {
|
||||||
setSelectedCategory(category);
|
setSelectedCategory(category);
|
||||||
setPage(1);
|
setAfter(undefined);
|
||||||
|
setPaginationHistory([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNextPage = () => {
|
||||||
|
if (appsData?.page_info?.end_cursor) {
|
||||||
|
if (after) {
|
||||||
|
setPaginationHistory(prev => [...prev, after]);
|
||||||
|
} else {
|
||||||
|
setPaginationHistory(prev => [...prev, 'FIRST_PAGE']);
|
||||||
|
}
|
||||||
|
setAfter(appsData.page_info.end_cursor);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePrevPage = () => {
|
||||||
|
if (paginationHistory.length > 0) {
|
||||||
|
const newHistory = [...paginationHistory];
|
||||||
|
const previousCursor = newHistory.pop();
|
||||||
|
setPaginationHistory(newHistory);
|
||||||
|
|
||||||
|
if (previousCursor === 'FIRST_PAGE') {
|
||||||
|
setAfter(undefined);
|
||||||
|
} else {
|
||||||
|
setAfter(previousCursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetPagination = () => {
|
||||||
|
setAfter(undefined);
|
||||||
|
setPaginationHistory([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleProfileSelect = async (profileId: string | null, app: PipedreamApp) => {
|
const handleProfileSelect = async (profileId: string | null, app: PipedreamApp) => {
|
||||||
|
@ -139,81 +249,93 @@ export const PipedreamRegistry: React.FC<PipedreamRegistryProps> = ({
|
||||||
const [selectedProfileId, setSelectedProfileId] = useState<string | undefined>(undefined);
|
const [selectedProfileId, setSelectedProfileId] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="group transition-all duration-200 hover:shadow-md border-border/50 hover:border-border/80 bg-card/50 hover:bg-card/80 w-full">
|
<Card className="group p-0">
|
||||||
<CardContent className={cn("p-4", compact && "p-3")}>
|
<CardContent className={cn("p-4", compact && "p-3")}>
|
||||||
<div className="flex items-start gap-3 w-full">
|
<div className="flex items-start gap-3">
|
||||||
{/* App Logo */}
|
<div className="flex-shrink-0 relative">
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"rounded-lg border border-border/50 bg-primary/20 flex items-center justify-center text-primary font-semibold",
|
"rounded-lg flex items-center justify-center text-primary font-semibold overflow-hidden",
|
||||||
compact ? "h-10 w-10 text-sm" : "h-12 w-12 text-base"
|
compact ? "h-6 w-6 text-sm" : "h-8 w-8 text-base"
|
||||||
|
)}>
|
||||||
|
{app.img_src ? (
|
||||||
|
<img
|
||||||
|
src={app.img_src}
|
||||||
|
alt={app.name}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
onError={(e) => {
|
||||||
|
const target = e.target as HTMLImageElement;
|
||||||
|
target.style.display = 'none';
|
||||||
|
target.nextElementSibling?.classList.remove('hidden');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<span className={cn(
|
||||||
|
"font-semibold",
|
||||||
|
app.img_src ? "hidden" : "block"
|
||||||
)}>
|
)}>
|
||||||
{app.name.charAt(0).toUpperCase()}
|
{app.name.charAt(0).toUpperCase()}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
{connectedProfiles.length > 0 && (
|
||||||
|
<div className="absolute -top-1 -right-1 h-4 w-4 bg-green-500 rounded-full flex items-center justify-center">
|
||||||
|
<CheckCircle2 className="h-2.5 w-2.5 text-white" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* App Details */}
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex-1 min-w-0 w-full">
|
<div className="flex items-start justify-between mb-1">
|
||||||
{/* Header */}
|
<div>
|
||||||
<div className="flex items-start justify-between mb-2 w-full">
|
|
||||||
<h3 className={cn(
|
<h3 className={cn(
|
||||||
"font-semibold text-foreground truncate flex-1 pr-2",
|
"font-medium text-sm text-gray-900 dark:text-white"
|
||||||
compact ? "text-sm" : "text-base"
|
|
||||||
)}>
|
)}>
|
||||||
{app.name}
|
{app.name}
|
||||||
</h3>
|
</h3>
|
||||||
{connectedProfiles.length > 0 && (
|
|
||||||
<Badge variant="secondary" className="bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400 text-xs flex-shrink-0">
|
|
||||||
<CheckCircle2 className="h-3 w-3 mr-1" />
|
|
||||||
Connected
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{/* Description */}
|
|
||||||
<p className={cn(
|
<p className={cn(
|
||||||
"text-muted-foreground line-clamp-2 mb-3 w-full",
|
"text-gray-600 text-xs dark:text-gray-400 line-clamp-2 mb-3",
|
||||||
compact ? "text-xs" : "text-sm"
|
|
||||||
)}>
|
)}>
|
||||||
{app.description}
|
{app.description}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Categories */}
|
|
||||||
{!compact && app.categories.length > 0 && (
|
{!compact && app.categories.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-1 mb-3 w-full">
|
<div className="flex flex-wrap gap-1 mb-3">
|
||||||
{app.categories.slice(0, 3).map((category) => (
|
{app.categories.slice(0, 2).map((category) => (
|
||||||
<Badge
|
<Badge
|
||||||
key={category}
|
key={category}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="text-xs px-2 py-0.5 bg-muted/30 hover:bg-muted/50 cursor-pointer border-border/50 flex-shrink-0"
|
className="text-xs px-1.5 py-0.5 bg-gray-50 hover:bg-gray-100 cursor-pointer border-gray-200 text-gray-700 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-700"
|
||||||
onClick={() => handleCategorySelect(category)}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleCategorySelect(category);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{category}
|
{categoryEmojis[category] || '🔧'} {category}
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
{app.categories.length > 3 && (
|
{app.categories.length > 2 && (
|
||||||
<Badge variant="outline" className="text-xs px-2 py-0.5 bg-muted/30 border-border/50 flex-shrink-0">
|
<Badge variant="outline" className="text-xs px-1.5 py-0.5 bg-gray-50 border-gray-200 text-gray-700 dark:bg-gray-800 dark:text-gray-300">
|
||||||
+{app.categories.length - 3}
|
+{app.categories.length - 2}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div>
|
||||||
{/* Actions */}
|
|
||||||
<div className="w-full">
|
|
||||||
{mode === 'simple' ? (
|
{mode === 'simple' ? (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
onClick={(e) => {
|
||||||
onClick={() => onAppSelected?.({ app_slug: app.name_slug, app_name: app.name })}
|
e.stopPropagation();
|
||||||
className="h-8 text-xs w-full"
|
onAppSelected?.({ app_slug: app.name_slug, app_name: app.name });
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Connect App
|
<Plus className="h-3 w-3" />
|
||||||
|
Connect
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{connectedProfiles.length > 0 ? (
|
{connectedProfiles.length > 0 ? (
|
||||||
<div className="space-y-2 w-full">
|
<div className="space-y-2">
|
||||||
<CredentialProfileSelector
|
<CredentialProfileSelector
|
||||||
appSlug={app.name_slug}
|
appSlug={app.name_slug}
|
||||||
appName={app.name}
|
appName={app.name}
|
||||||
|
@ -229,11 +351,12 @@ export const PipedreamRegistry: React.FC<PipedreamRegistryProps> = ({
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
onClick={(e) => {
|
||||||
onClick={() => handleCreateProfile(app)}
|
e.stopPropagation();
|
||||||
className="h-8 text-xs w-full hover:bg-primary/10"
|
handleCreateProfile(app);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Plus className="h-3 w-3 mr-1" />
|
<Plus className="h-3 w-3" />
|
||||||
Connect
|
Connect
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
@ -247,37 +370,70 @@ export const PipedreamRegistry: React.FC<PipedreamRegistryProps> = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const CategoryTabs = () => (
|
const Sidebar: React.FC<{
|
||||||
<div className="w-full">
|
isCollapsed: boolean;
|
||||||
<div className="flex gap-2 overflow-x-auto pb-2 scrollbar-hide">
|
onToggle: () => void;
|
||||||
|
categories: string[];
|
||||||
|
selectedCategory: string;
|
||||||
|
onCategorySelect: (category: string) => void;
|
||||||
|
allApps: PipedreamApp[];
|
||||||
|
}> = ({ isCollapsed, onToggle, categories, selectedCategory, onCategorySelect, allApps }) => (
|
||||||
|
<div className="border-r bg-sidebar flex-shrink-0 sticky top-0 h-[calc(100vh-12vh)] overflow-hidden">
|
||||||
|
<div className="p-3 border-b flex-shrink-0">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
|
<div className={cn(
|
||||||
|
"overflow-hidden transition-all duration-300 ease-in-out",
|
||||||
|
isCollapsed ? "w-0 opacity-0" : "w-auto opacity-100"
|
||||||
|
)}>
|
||||||
|
<h3 className="font-semibold text-sm whitespace-nowrap">Categories</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={onToggle}
|
||||||
|
className="h-7 w-7"
|
||||||
|
>
|
||||||
|
<ChevronRight className={cn(
|
||||||
|
"h-3 w-3 transition-transform duration-300 ease-in-out",
|
||||||
|
isCollapsed ? "rotate-0" : "rotate-180"
|
||||||
|
)} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-2 space-y-0.5 flex-1 overflow-y-auto">
|
||||||
{categories.map((category) => {
|
{categories.map((category) => {
|
||||||
const categoryCount = category === 'All'
|
const categoryCount = category === 'All'
|
||||||
? (allAppsData?.total_count || appsData?.total_count || 0)
|
? allApps.length
|
||||||
: (allAppsData?.apps || appsData?.apps || []).filter((app: PipedreamApp) =>
|
: allApps.filter((app: PipedreamApp) =>
|
||||||
app.categories.includes(category)
|
app.categories.includes(category)
|
||||||
).length;
|
).length;
|
||||||
|
|
||||||
|
const isActive = selectedCategory === category;
|
||||||
|
const emoji = categoryEmojis[category] || '🔧';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<button
|
||||||
key={category}
|
key={category}
|
||||||
variant={selectedCategory === category ? "default" : "outline"}
|
onClick={() => onCategorySelect(category)}
|
||||||
size="sm"
|
|
||||||
onClick={() => handleCategorySelect(category)}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-1.5 h-8 px-3 text-xs whitespace-nowrap flex-shrink-0",
|
"w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left transition-all duration-200 overflow-hidden",
|
||||||
selectedCategory === category
|
isActive
|
||||||
? "bg-primary text-primary-foreground"
|
? "bg-primary/10 text-primary"
|
||||||
: "hover:bg-muted/50"
|
: "hover:bg-primary/5"
|
||||||
)}
|
)}
|
||||||
|
title={isCollapsed ? category : undefined}
|
||||||
>
|
>
|
||||||
<Grid className="h-4 w-4" />
|
<span className="text-sm flex-shrink-0">{emoji}</span>
|
||||||
<span>{category}</span>
|
<div className={cn(
|
||||||
{categoryCount > 0 && (
|
"flex items-center justify-between flex-1 min-w-0 overflow-hidden transition-all duration-300 ease-in-out",
|
||||||
<Badge variant="secondary" className="ml-1 h-4 px-1.5 text-xs bg-muted/50">
|
isCollapsed ? "w-0 opacity-0" : "w-auto opacity-100"
|
||||||
{categoryCount}
|
)}>
|
||||||
</Badge>
|
<span className="text-sm truncate whitespace-nowrap">{category}</span>
|
||||||
)}
|
</div>
|
||||||
</Button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
@ -286,68 +442,80 @@ export const PipedreamRegistry: React.FC<PipedreamRegistryProps> = ({
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="text-center py-8">
|
<div className="flex items-center justify-center h-full">
|
||||||
<div className="text-red-500 mb-2">Failed to load integrations</div>
|
<div className="text-center">
|
||||||
<Button onClick={() => refetch()} variant="outline">
|
<div className="text-red-500 mb-4">
|
||||||
|
<X className="h-12 w-12 mx-auto mb-2" />
|
||||||
|
<p className="text-lg font-semibold">Failed to load integrations</p>
|
||||||
|
</div>
|
||||||
|
<Button onClick={() => refetch()} className="bg-primary hover:bg-primary/90">
|
||||||
Try Again
|
Try Again
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col max-w-full overflow-hidden">
|
<div className="h-full flex">
|
||||||
{/* Header */}
|
<div className={cn(
|
||||||
<div className="flex items-center justify-between p-4 sm:p-6 pb-4 flex-shrink-0">
|
"transition-all duration-300 ease-in-out",
|
||||||
<div className="min-w-0 flex-1 pr-4">
|
sidebarCollapsed ? "w-12" : "w-62"
|
||||||
<h2 className="text-xl font-semibold text-foreground truncate">Integrations</h2>
|
)}>
|
||||||
<p className="text-sm text-muted-foreground">
|
<Sidebar
|
||||||
Connect your integrations
|
isCollapsed={sidebarCollapsed}
|
||||||
|
onToggle={() => setSidebarCollapsed(!sidebarCollapsed)}
|
||||||
|
categories={categories}
|
||||||
|
selectedCategory={selectedCategory}
|
||||||
|
onCategorySelect={handleCategorySelect}
|
||||||
|
allApps={allApps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 relative">
|
||||||
|
<div className="absolute top-0 left-0 right-0 z-10 border-b px-4 py-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<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>
|
||||||
|
<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
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{onClose && (
|
|
||||||
<Button variant="ghost" size="icon" onClick={onClose} className="h-8 w-8 flex-shrink-0">
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{/* Search */}
|
<div className="mt-3 max-w-md">
|
||||||
<div className="px-4 sm:px-6 pb-4 flex-shrink-0">
|
<div className="relative">
|
||||||
<div className="relative max-w-md">
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
||||||
<Input
|
<Input
|
||||||
placeholder="Search integrations..."
|
placeholder="Search 2700+ apps..."
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => handleSearch(e.target.value)}
|
onChange={(e) => handleSearch(e.target.value)}
|
||||||
className="pl-10 h-10 bg-background/50 border-border/50 focus:border-border w-full"
|
className="pl-10 h-9 focus:border-primary/50 focus:ring-primary/20 rounded-lg text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Categories */}
|
|
||||||
<div className="px-4 sm:px-6 pb-4 flex-shrink-0">
|
|
||||||
<CategoryTabs />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
<div className="absolute inset-0 pt-[120px] pb-[60px]">
|
||||||
<div className="flex-1 overflow-auto px-4 sm:px-6 pb-6">
|
<div className="h-full overflow-y-auto p-4">
|
||||||
<div className="max-w-full">
|
<div className="max-w-6xl mx-auto">
|
||||||
{/* My Connections */}
|
|
||||||
{connectedApps.length > 0 && (
|
{connectedApps.length > 0 && (
|
||||||
<div className="mb-8">
|
<div className="mb-6">
|
||||||
<div className="flex items-center gap-2 mb-4">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<User className="h-4 w-4 text-muted-foreground" />
|
<User className="h-4 w-4 text-green-600 dark:text-green-400" />
|
||||||
<h3 className="font-semibold text-foreground">My Connections</h3>
|
<h2 className="text-md font-semibold text-gray-900 dark:text-white">My Connections</h2>
|
||||||
<Badge variant="secondary" className="bg-muted/50">
|
<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}
|
{connectedApps.length}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground mb-4">
|
|
||||||
Apps and services you've connected
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-3 sm:gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||||
{connectedApps.map((app) => (
|
{connectedApps.map((app) => (
|
||||||
<AppCard key={app.id} app={app} />
|
<AppCard key={app.id} app={app} />
|
||||||
))}
|
))}
|
||||||
|
@ -355,109 +523,119 @@ export const PipedreamRegistry: React.FC<PipedreamRegistryProps> = ({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Popular/Recommended */}
|
{selectedCategory === 'All' ? (
|
||||||
{popularApps.length > 0 && selectedCategory === 'All' && (
|
<div className="mb-4">
|
||||||
<div className="mb-8">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<div className="flex items-center gap-2 mb-4">
|
<TrendingUp className="h-4 w-4 text-orange-600 dark:text-orange-400" />
|
||||||
<Star className="h-4 w-4 text-muted-foreground" />
|
<h2 className="text-md font-semibold text-gray-900 dark:text-white">Popular</h2>
|
||||||
<h3 className="font-semibold text-foreground">Popular</h3>
|
<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">
|
||||||
<Badge variant="secondary" className="bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400 text-xs">
|
<Star className="h-3 w-3 mr-1" />
|
||||||
Recommended
|
Recommended
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground mb-4">
|
</div>
|
||||||
Most commonly used integrations to get started quickly
|
) : (
|
||||||
</p>
|
<div className="mb-4">
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-3 sm:gap-4">
|
<span className="text-lg">{categoryEmojis[selectedCategory] || '🔧'}</span>
|
||||||
{popularApps.map((app: PipedreamApp) => (
|
<h2 className="text-md font-medium text-gray-900 dark:text-white">{selectedCategory}</h2>
|
||||||
<AppCard key={app.id} app={app} compact={true} />
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Current Category Results */}
|
|
||||||
{selectedCategory !== 'All' && (
|
|
||||||
<div className="mb-6">
|
|
||||||
<div className="flex items-center gap-2 mb-4">
|
|
||||||
<Grid className="h-4 w-4 text-muted-foreground" />
|
|
||||||
<h3 className="font-semibold text-foreground">{selectedCategory}</h3>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-muted-foreground mb-4">
|
|
||||||
Explore {selectedCategory} integrations and tools
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Apps Grid */}
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-8">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
<Loader2 className="h-4 w-4 animate-spin text-primary" />
|
||||||
<span className="text-sm text-muted-foreground">Loading integrations...</span>
|
<span className="text-sm text-gray-600 dark:text-gray-400">Loading integrations...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : appsData?.apps && appsData.apps.length > 0 ? (
|
) : filteredAppsData?.apps && filteredAppsData.apps.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-3 sm:gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||||
{appsData.apps.map((app: PipedreamApp) => (
|
{filteredAppsData.apps.map((app: PipedreamApp) => (
|
||||||
<AppCard key={app.id} app={app} />
|
<AppCard key={app.id} app={app} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{appsData.page_info && appsData.page_info.has_more && (
|
|
||||||
<div className="flex justify-center pt-8">
|
|
||||||
<Button
|
|
||||||
onClick={() => setPage(page + 1)}
|
|
||||||
disabled={isLoading}
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="h-10"
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
|
||||||
Loading...
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
'Load More'
|
<div className="text-center py-8">
|
||||||
)}
|
<div className="text-4xl mb-3">🔍</div>
|
||||||
</Button>
|
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">No integrations found</h3>
|
||||||
</div>
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4 max-w-md mx-auto">
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="text-center py-12">
|
|
||||||
<div className="text-4xl mb-4">🔍</div>
|
|
||||||
<h3 className="text-lg font-medium mb-2">No integrations found</h3>
|
|
||||||
<p className="text-sm text-muted-foreground mb-4">
|
|
||||||
{selectedCategory !== 'All'
|
{selectedCategory !== 'All'
|
||||||
? `No integrations found in "${selectedCategory}" category`
|
? `No integrations found in "${selectedCategory}" category. Try a different category or search term.`
|
||||||
: "Try adjusting your search criteria"
|
: "Try adjusting your search criteria or browse our popular integrations."
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSearch('');
|
setSearch('');
|
||||||
setSelectedCategory('All');
|
setSelectedCategory('All');
|
||||||
setPage(1);
|
resetPagination();
|
||||||
}}
|
}}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
className="bg-primary hover:bg-primary/90 text-white"
|
||||||
>
|
>
|
||||||
|
<Filter className="h-4 w-4 mr-2" />
|
||||||
Clear Filters
|
Clear Filters
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Dialogs */}
|
{filteredAppsData?.apps && filteredAppsData.apps.length > 0 && (paginationHistory.length > 0 || appsData?.page_info?.has_more) && (
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 z-10 border-t px-4 py-3 bg-background">
|
||||||
|
<div className="flex items-center justify-end gap-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={handlePrevPage}
|
||||||
|
disabled={isLoading || paginationHistory.length === 0}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-9 px-3"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center gap-1 px-4 py-2 text-sm rounded-lg border">
|
||||||
|
<div className="font-medium text-gray-900 dark:text-white">
|
||||||
|
Page {paginationHistory.length + 1}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleNextPage}
|
||||||
|
disabled={isLoading || !appsData?.page_info?.has_more}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-9 px-3"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<Dialog open={showToolSelector} onOpenChange={setShowToolSelector}>
|
<Dialog open={showToolSelector} onOpenChange={setShowToolSelector}>
|
||||||
<DialogContent className='max-w-3xl max-h-[80vh] overflow-y-auto'>
|
<DialogContent className='max-w-4xl max-h-[80vh] overflow-y-auto'>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Select Tools for {selectedProfile?.app_name}</DialogTitle>
|
<DialogTitle className="text-xl">Select Tools for {selectedProfile?.app_name}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Choose which tools you'd like to add from {selectedProfile?.app_name}
|
||||||
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<PipedreamToolSelector
|
<PipedreamToolSelector
|
||||||
appSlug={selectedProfile?.app_slug || ''}
|
appSlug={selectedProfile?.app_slug || ''}
|
||||||
|
@ -470,11 +648,11 @@ export const PipedreamRegistry: React.FC<PipedreamRegistryProps> = ({
|
||||||
<Dialog open={showProfileManager} onOpenChange={handleProfileManagerClose}>
|
<Dialog open={showProfileManager} onOpenChange={handleProfileManagerClose}>
|
||||||
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
|
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle className="text-xl">
|
||||||
Create {selectedAppForProfile?.app_name} Profile
|
Create {selectedAppForProfile?.app_name} Profile
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Create a credential profile for {selectedAppForProfile?.app_name} to connect and use its tools
|
Set up your credential profile for {selectedAppForProfile?.app_name} to access its tools and features
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<CredentialProfileManager
|
<CredentialProfileManager
|
||||||
|
|
|
@ -51,11 +51,11 @@ export const useInvalidatePipedreamQueries = () => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const usePipedreamApps = (page: number = 1, search?: string, category?: string) => {
|
export const usePipedreamApps = (after?: string, search?: string) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['pipedream', 'apps', page, search, category],
|
queryKey: ['pipedream', 'apps', after, search],
|
||||||
queryFn: async (): Promise<PipedreamAppResponse> => {
|
queryFn: async (): Promise<PipedreamAppResponse> => {
|
||||||
return await pipedreamApi.getApps(page, search, category);
|
return await pipedreamApi.getApps(after, search);
|
||||||
},
|
},
|
||||||
staleTime: 5 * 60 * 1000,
|
staleTime: 5 * 60 * 1000,
|
||||||
retry: 2,
|
retry: 2,
|
||||||
|
|
|
@ -84,12 +84,17 @@ export interface PipedreamApp {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
name_slug: string;
|
name_slug: string;
|
||||||
app_hid: string;
|
auth_type: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
img_src: string;
|
||||||
|
custom_fields_json: string;
|
||||||
categories: string[];
|
categories: string[];
|
||||||
featured_weight: number;
|
featured_weight: number;
|
||||||
api_docs_url: string | null;
|
connect: {
|
||||||
status: number;
|
allowed_domains: string[] | null;
|
||||||
|
base_proxy_target_url: string;
|
||||||
|
proxy_enabled: boolean;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PipedreamAppResponse {
|
export interface PipedreamAppResponse {
|
||||||
|
@ -97,10 +102,8 @@ export interface PipedreamAppResponse {
|
||||||
apps: PipedreamApp[];
|
apps: PipedreamApp[];
|
||||||
page_info: {
|
page_info: {
|
||||||
total_count: number;
|
total_count: number;
|
||||||
current_page: number;
|
count: number;
|
||||||
page_size: number;
|
|
||||||
has_more: boolean;
|
has_more: boolean;
|
||||||
count?: number;
|
|
||||||
start_cursor?: string;
|
start_cursor?: string;
|
||||||
end_cursor?: string;
|
end_cursor?: string;
|
||||||
};
|
};
|
||||||
|
@ -178,21 +181,19 @@ export const pipedreamApi = {
|
||||||
return result.data!;
|
return result.data!;
|
||||||
},
|
},
|
||||||
|
|
||||||
async getApps(page: number = 1, search?: string, category?: string): Promise<PipedreamAppResponse> {
|
async getApps(after?: string, search?: string): Promise<PipedreamAppResponse> {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams();
|
||||||
page: page.toString(),
|
|
||||||
});
|
if (after) {
|
||||||
|
params.append('after', after);
|
||||||
|
}
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
params.append('q', search);
|
params.append('q', search);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (category) {
|
|
||||||
params.append('category', category);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await backendApi.get<PipedreamAppResponse>(
|
const result = await backendApi.get<PipedreamAppResponse>(
|
||||||
`/pipedream/apps?${params.toString()}`,
|
`/pipedream/apps${params.toString() ? `?${params.toString()}` : ''}`,
|
||||||
{
|
{
|
||||||
errorContext: { operation: 'load apps', resource: 'Pipedream apps' },
|
errorContext: { operation: 'load apps', resource: 'Pipedream apps' },
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue