feat(playground): load all tools and add hide broken toggle
- Paginate through all tools from registry API (was limited to 20) - Add 'Hide broken tools' checkbox (enabled by default) - Filter out tools with BROKEN import or execution health
This commit is contained in:
parent
aa1a5cd246
commit
4413ac00f6
2 changed files with 88 additions and 32 deletions
|
|
@ -3,40 +3,74 @@ import { NextResponse } from 'next/server';
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
interface RawTool {
|
||||||
|
id: string;
|
||||||
|
exportName: string;
|
||||||
|
description: string;
|
||||||
|
qualityScore: number;
|
||||||
|
importHealth: 'HEALTHY' | 'BROKEN' | 'UNKNOWN';
|
||||||
|
executionHealth: 'HEALTHY' | 'BROKEN' | 'UNKNOWN';
|
||||||
|
healthCheckError: string | null;
|
||||||
|
lastHealthCheck: string | null;
|
||||||
|
package?: {
|
||||||
|
npmPackageName: string;
|
||||||
|
npmVersion: string;
|
||||||
|
category: string;
|
||||||
|
frameworks: string[];
|
||||||
|
env: Array<{ name: string; description: string; required?: boolean; default?: string }>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformTool(tool: RawTool) {
|
||||||
|
return {
|
||||||
|
toolId: tool.id,
|
||||||
|
packageName: tool.package?.npmPackageName,
|
||||||
|
exportName: tool.exportName,
|
||||||
|
description: tool.description,
|
||||||
|
category: tool.package?.category,
|
||||||
|
version: tool.package?.npmVersion,
|
||||||
|
qualityScore: tool.qualityScore,
|
||||||
|
frameworks: tool.package?.frameworks,
|
||||||
|
env: tool.package?.env,
|
||||||
|
importUrl: `https://esm.sh/${tool.package?.npmPackageName}@${tool.package?.npmVersion}`,
|
||||||
|
importHealth: tool.importHealth,
|
||||||
|
executionHealth: tool.executionHealth,
|
||||||
|
healthCheckError: tool.healthCheckError,
|
||||||
|
lastHealthCheck: tool.lastHealthCheck,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
const baseUrl = process.env.TPMJS_API_URL || 'https://tpmjs.com';
|
const baseUrl = process.env.TPMJS_API_URL || 'https://tpmjs.com';
|
||||||
const response = await fetch(`${baseUrl}/api/tools`);
|
const allTools: RawTool[] = [];
|
||||||
|
let offset = 0;
|
||||||
|
const limit = 50; // Max allowed by the API
|
||||||
|
let hasMore = true;
|
||||||
|
|
||||||
if (!response.ok) {
|
// Paginate through all tools
|
||||||
throw new Error(`Failed to fetch tools: ${response.statusText}`);
|
while (hasMore) {
|
||||||
|
const response = await fetch(`${baseUrl}/api/tools?limit=${limit}&offset=${offset}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch tools: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const tools = data.data || [];
|
||||||
|
allTools.push(...tools);
|
||||||
|
|
||||||
|
hasMore = data.pagination?.hasMore ?? false;
|
||||||
|
offset += limit;
|
||||||
|
|
||||||
|
// Safety limit to prevent infinite loops
|
||||||
|
if (offset > 1000) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
// Transform web app response format to playground format
|
|
||||||
// Web app returns { success, data: Tool[] }
|
|
||||||
// Playground expects { success, tools: Tool[], total }
|
|
||||||
const tools = data.data || [];
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: data.success,
|
success: true,
|
||||||
tools: tools.map((tool: any) => ({
|
tools: allTools.map(transformTool),
|
||||||
toolId: tool.id,
|
total: allTools.length,
|
||||||
packageName: tool.package?.npmPackageName,
|
|
||||||
exportName: tool.exportName,
|
|
||||||
description: tool.description,
|
|
||||||
category: tool.package?.category,
|
|
||||||
version: tool.package?.npmVersion,
|
|
||||||
qualityScore: tool.qualityScore,
|
|
||||||
frameworks: tool.package?.frameworks,
|
|
||||||
env: tool.package?.env,
|
|
||||||
importUrl: `https://esm.sh/${tool.package?.npmPackageName}@${tool.package?.npmVersion}`,
|
|
||||||
importHealth: tool.importHealth,
|
|
||||||
executionHealth: tool.executionHealth,
|
|
||||||
healthCheckError: tool.healthCheckError,
|
|
||||||
lastHealthCheck: tool.lastHealthCheck,
|
|
||||||
})),
|
|
||||||
total: tools.length,
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch tools:', error);
|
console.error('Failed to fetch tools:', error);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import { Badge } from '@tpmjs/ui/Badge/Badge';
|
import { Badge } from '@tpmjs/ui/Badge/Badge';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@tpmjs/ui/Card/Card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@tpmjs/ui/Card/Card';
|
||||||
|
import { Checkbox } from '@tpmjs/ui/Checkbox/Checkbox';
|
||||||
import { Input } from '@tpmjs/ui/Input/Input';
|
import { Input } from '@tpmjs/ui/Input/Input';
|
||||||
import { ToolHealthBadge } from '@tpmjs/ui/ToolHealthBadge/ToolHealthBadge';
|
import { ToolHealthBadge } from '@tpmjs/ui/ToolHealthBadge/ToolHealthBadge';
|
||||||
import { ToolHealthBanner } from '@tpmjs/ui/ToolHealthBanner/ToolHealthBanner';
|
import { ToolHealthBanner } from '@tpmjs/ui/ToolHealthBanner/ToolHealthBanner';
|
||||||
|
|
@ -28,6 +29,7 @@ export function ToolsSidebar(): React.ReactElement {
|
||||||
const [tools, setTools] = useState<Tool[]>([]);
|
const [tools, setTools] = useState<Tool[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [filter, setFilter] = useState('');
|
const [filter, setFilter] = useState('');
|
||||||
|
const [hideBroken, setHideBroken] = useState(true);
|
||||||
const [selectedTool, setSelectedTool] = useState<Tool | null>(null);
|
const [selectedTool, setSelectedTool] = useState<Tool | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -57,13 +59,20 @@ export function ToolsSidebar(): React.ReactElement {
|
||||||
fetchTools();
|
fetchTools();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const filteredTools = tools.filter(
|
const filteredTools = tools.filter((tool) => {
|
||||||
(tool) =>
|
// Filter by search text
|
||||||
|
const matchesFilter =
|
||||||
tool.packageName?.toLowerCase().includes(filter.toLowerCase()) ||
|
tool.packageName?.toLowerCase().includes(filter.toLowerCase()) ||
|
||||||
tool.exportName?.toLowerCase().includes(filter.toLowerCase()) ||
|
tool.exportName?.toLowerCase().includes(filter.toLowerCase()) ||
|
||||||
tool.description?.toLowerCase().includes(filter.toLowerCase()) ||
|
tool.description?.toLowerCase().includes(filter.toLowerCase()) ||
|
||||||
tool.category?.toLowerCase().includes(filter.toLowerCase())
|
tool.category?.toLowerCase().includes(filter.toLowerCase());
|
||||||
);
|
|
||||||
|
// Filter out broken tools if hideBroken is true
|
||||||
|
const isBroken = tool.importHealth === 'BROKEN' || tool.executionHealth === 'BROKEN';
|
||||||
|
const matchesHealth = !hideBroken || !isBroken;
|
||||||
|
|
||||||
|
return matchesFilter && matchesHealth;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -78,9 +87,22 @@ export function ToolsSidebar(): React.ReactElement {
|
||||||
placeholder="Filter tools..."
|
placeholder="Filter tools..."
|
||||||
value={filter}
|
value={filter}
|
||||||
onChange={(e) => setFilter(e.target.value)}
|
onChange={(e) => setFilter(e.target.value)}
|
||||||
className="mb-4"
|
className="mb-3"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className="mb-4 flex items-center gap-2 text-sm text-foreground-secondary">
|
||||||
|
<Checkbox
|
||||||
|
id="hide-broken"
|
||||||
|
checked={hideBroken}
|
||||||
|
onChange={(e) => setHideBroken(e.target.checked)}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
{/* biome-ignore lint/a11y/noLabelWithoutControl: Checkbox is associated via htmlFor */}
|
||||||
|
<label htmlFor="hide-broken" className="cursor-pointer">
|
||||||
|
Hide broken tools
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 space-y-2 overflow-y-auto">
|
<div className="flex-1 space-y-2 overflow-y-auto">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<p className="text-sm text-foreground-secondary">Loading tools...</p>
|
<p className="text-sm text-foreground-secondary">Loading tools...</p>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue