dashboard.tsx•8.9 kB
import { useState, useEffect } from "react";
import { useLocation } from "wouter";
import { useQuery } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Bolt, Search, Key } from "lucide-react";
import ToolCard from "@/components/tool-card";
import ToolModal from "@/components/tool-modal";
import TokenManager from "@/components/tool-manager";
import LoadingSpinner from "@/components/ui/loading-spinner";
import { useToast } from "@/hooks/use-toast";
interface Tool {
name: string;
description: string;
category?: string;
inputSchema: {
type: "object";
properties: Record<string, any>;
required?: string[];
};
}
export default function Dashboard() {
const [, setLocation] = useLocation();
const [searchTerm, setSearchTerm] = useState("");
const [selectedCategory, setSelectedCategory] = useState("");
const [selectedTool, setSelectedTool] = useState<Tool | null>(null);
const [showTokenManager, setShowTokenManager] = useState(false);
const { toast } = useToast();
// Check token status
const { data: tokenStatus, isLoading: tokenLoading } = useQuery({
queryKey: ["/api/token/status"],
});
// Fetch tools
const { data: toolsData, isLoading: toolsLoading, error: toolsError } = useQuery({
queryKey: ["/api/tools"],
enabled: (tokenStatus as any)?.hasToken,
});
// Redirect if no token
useEffect(() => {
if (!tokenLoading && !(tokenStatus as any)?.hasToken) {
toast({
title: "No Token Found",
description: "Please provide a GitHub token first.",
variant: "destructive",
});
setLocation("/");
}
}, [tokenStatus, tokenLoading, setLocation, toast]);
if (tokenLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<LoadingSpinner className="h-8 w-8" />
</div>
);
}
if (!(tokenStatus as any)?.hasToken) {
return null;
}
const tools: Tool[] = (toolsData as any)?.tools || [];
// Group tools by category
const groupedTools = tools.reduce((acc, tool) => {
const category = tool.category || "Uncategorized";
if (!acc[category]) {
acc[category] = [];
}
acc[category].push(tool);
return acc;
}, {} as Record<string, Tool[]>);
// Filter tools based on search and category
const filteredGroups = Object.entries(groupedTools).reduce((acc, [category, categoryTools]) => {
if (selectedCategory && selectedCategory !== "all" && category !== selectedCategory) {
return acc;
}
const filteredTools = categoryTools.filter(tool =>
tool.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
tool.description.toLowerCase().includes(searchTerm.toLowerCase())
);
if (filteredTools.length > 0) {
acc[category] = filteredTools;
}
return acc;
}, {} as Record<string, Tool[]>);
const categories = Object.keys(groupedTools);
return (
<div className="min-h-screen bg-background">
{/* Header */}
<header className="bg-card border-b border-border sticky top-0 z-40">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
<div className="flex items-center">
<div className="w-8 h-8 bg-gradient-to-r from-purple-600 to-blue-600 rounded-lg flex items-center justify-center">
<svg className="h-5 w-5 text-white" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="currentColor">
<title>GitHub</title>
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/>
</svg>
</div>
<h1 className="ml-3 text-xl font-semibold">GitHub MCP</h1>
</div>
{/* User Menu */}
<div className="flex items-center space-x-4">
<Button
variant="ghost"
size="sm"
onClick={() => setShowTokenManager(true)}
className="text-muted-foreground hover:text-foreground"
>
<h3>Update Token</h3>
<Key className="h-4 w-4" />
</Button>
<div className="flex items-center text-sm text-muted-foreground">
<div className="w-2 h-2 bg-green-500 rounded-full mr-2"></div>
Connected
</div>
</div>
</div>
</div>
</header>
{/* Main Content */}
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Search and Filters */}
<div className="mb-8">
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
<div>
<h2 className="text-2xl font-bold">Available Tools</h2>
<p className="text-muted-foreground mt-1">Select a tool to configure and execute GitHub operations</p>
</div>
<div className="flex flex-col sm:flex-row gap-3 w-full sm:w-auto">
<div className="relative">
<Input
type="text"
placeholder="Search tools..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 w-full sm:w-64"
/>
<Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
</div>
<Select value={selectedCategory} onValueChange={setSelectedCategory}>
<SelectTrigger className="w-full sm:w-48">
<SelectValue placeholder="All Categories" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Categories</SelectItem>
{categories.map((category) => (
<SelectItem key={category} value={category}>
{category}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
{/* Bolt Content */}
{toolsLoading ? (
<div className="flex items-center justify-center py-12">
<LoadingSpinner className="h-8 w-8" />
</div>
) : toolsError ? (
<div className="text-center py-12">
<p className="text-destructive">Failed to load tools. Please try again.</p>
</div>
) : Object.keys(filteredGroups).length === 0 ? (
<div className="text-center py-12">
<p className="text-muted-foreground">No tools found matching your criteria.</p>
</div>
) : (
<div className="space-y-8">
{Object.entries(filteredGroups).map(([category, categoryTools]) => (
<div key={category} className="tool-category">
<h3 className="text-lg font-semibold text-primary mb-4 flex items-center">
<Bolt className="mr-2 h-5 w-5" />
{category}
<span className="ml-2 text-sm bg-muted text-muted-foreground px-2 py-1 rounded-full">
{categoryTools.length}
</span>
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{categoryTools.map((tool) => (
<ToolCard
key={tool.name}
tool={tool}
onClick={() => setSelectedTool(tool)}
/>
))}
</div>
</div>
))}
</div>
)}
</main>
{/* Modals */}
{selectedTool && (
<ToolModal
tool={selectedTool}
onClose={() => setSelectedTool(null)}
/>
)}
{showTokenManager && (
<TokenManager onClose={() => setShowTokenManager(false)} />
)}
</div>
);
}