Files
hive/frontend/src/components/projects/ProjectList.tsx
anthonyrawlins 268214d971 Major WHOOSH system refactoring and feature enhancements
- Migrated from HIVE branding to WHOOSH across all components
- Enhanced backend API with new services: AI models, BZZZ integration, templates, members
- Added comprehensive testing suite with security, performance, and integration tests
- Improved frontend with new components for project setup, AI models, and team management
- Updated MCP server implementation with WHOOSH-specific tools and resources
- Enhanced deployment configurations with production-ready Docker setups
- Added comprehensive documentation and setup guides
- Implemented age encryption service and UCXL integration

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-27 08:34:48 +10:00

374 lines
17 KiB
TypeScript

import { useState } from 'react';
import { Link } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import {
PlusIcon,
FolderIcon,
EllipsisVerticalIcon,
MagnifyingGlassIcon,
FunnelIcon,
ChartBarIcon,
ClockIcon,
TagIcon,
Cog6ToothIcon
} from '@heroicons/react/24/outline';
import { Menu, Transition } from '@headlessui/react';
import { Fragment } from 'react';
import { formatDistanceToNow } from 'date-fns';
import { projectApi } from '../../services/api';
// Project data will come from the API
export default function ProjectList() {
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'inactive' | 'arcwhooshd'>('all');
const [bzzzFilter, setBzzzFilter] = useState<'all' | 'enabled' | 'disabled'>('all');
// Fetch real projects from API
const { data: projects = [], isLoading, error } = useQuery({
queryKey: ['projects'],
queryFn: async () => {
return await projectApi.getProjects();
}
});
const filteredProjects = projects.filter(project => {
const matchesSearch = project.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
project.description?.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === 'all' || project.status === statusFilter;
const bzzzEnabled = (project as any).bzzz_config?.bzzz_enabled || false;
const matchesBzzz = bzzzFilter === 'all' ||
(bzzzFilter === 'enabled' && bzzzEnabled) ||
(bzzzFilter === 'disabled' && !bzzzEnabled);
return matchesSearch && matchesStatus && matchesBzzz;
});
const getStatusBadge = (status: string) => {
const baseClasses = 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium';
switch (status) {
case 'active':
return `${baseClasses} bg-green-100 text-green-800`;
case 'inactive':
return `${baseClasses} bg-gray-100 text-gray-800`;
case 'arcwhooshd':
return `${baseClasses} bg-red-100 text-red-800`;
default:
return `${baseClasses} bg-gray-100 text-gray-800`;
}
};
if (isLoading) {
return (
<div className="p-6">
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-1/4 mb-6"></div>
<div className="space-y-4">
{[1, 2, 3].map(i => (
<div key={i} className="bg-white rounded-lg border p-6">
<div className="h-6 bg-gray-200 rounded w-1/3 mb-4"></div>
<div className="h-4 bg-gray-200 rounded w-2/3 mb-2"></div>
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
</div>
))}
</div>
</div>
</div>
);
}
if (error) {
return (
<div className="p-6">
<div className="bg-red-50 border border-red-200 rounded-md p-4">
<h3 className="text-sm font-medium text-red-800">Error loading projects</h3>
<p className="mt-1 text-sm text-red-700">
{error instanceof Error ? error.message : 'Failed to load projects'}
</p>
</div>
</div>
);
}
return (
<div className="p-6">
{/* Header */}
<div className="sm:flex sm:items-center sm:justify-between mb-6">
<div>
<h1 className="text-2xl font-bold text-gray-900">Projects</h1>
<p className="mt-1 text-sm text-gray-500">
Manage your workflow projects and track their performance
</p>
</div>
<div className="mt-4 sm:mt-0">
<Link
to="/projects/new"
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<PlusIcon className="h-4 w-4 mr-2" />
New Project
</Link>
</div>
</div>
{/* Filters */}
<div className="mb-6 flex flex-col sm:flex-row gap-4">
<div className="flex-1 relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<MagnifyingGlassIcon className="h-5 w-5 text-gray-400" />
</div>
<input
type="text"
placeholder="Search projects..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2">
<FunnelIcon className="h-5 w-5 text-gray-400" />
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as any)}
className="border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
>
<option value="all">All Status</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
<option value="arcwhooshd">Arcwhooshd</option>
</select>
</div>
<div className="flex items-center space-x-2">
<span className="text-sm text-gray-500">🐝</span>
<select
value={bzzzFilter}
onChange={(e) => setBzzzFilter(e.target.value as any)}
className="border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
>
<option value="all">All Projects</option>
<option value="enabled">Bzzz Enabled</option>
<option value="disabled">Bzzz Disabled</option>
</select>
</div>
</div>
</div>
{/* Projects Grid */}
{filteredProjects.length === 0 ? (
<div className="text-center py-12">
<FolderIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No projects found</h3>
<p className="text-gray-500 mb-4">
{searchTerm || statusFilter !== 'all'
? 'Try adjusting your search or filter criteria.'
: 'Get started by creating your first project.'
}
</p>
<Link
to="/projects/new"
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700"
>
<PlusIcon className="h-4 w-4 mr-2" />
Create Project
</Link>
</div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
{filteredProjects.map((project) => {
// Real project data from API includes metrics directly
return (
<div key={project.id} className="bg-white rounded-lg border border-gray-200 hover:shadow-md transition-shadow">
{/* Card Header */}
<div className="p-6 pb-4">
<div className="flex items-start justify-between">
<div className="flex-1">
<Link
to={`/projects/${project.id}`}
className="text-lg font-semibold text-gray-900 hover:text-blue-600 line-clamp-1"
>
{project.name}
</Link>
<p className="text-sm text-gray-500 mt-1 line-clamp-2">
{project.description}
</p>
</div>
<Menu as="div" className="relative">
<Menu.Button className="p-1 rounded-full hover:bg-gray-100">
<EllipsisVerticalIcon className="h-5 w-5 text-gray-400" />
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 z-10 mt-2 w-48 bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="py-1">
<Menu.Item>
{({ active }) => (
<Link
to={`/projects/${project.id}/edit`}
className={`${active ? 'bg-gray-100' : ''} block px-4 py-2 text-sm text-gray-700`}
>
Edit Project
</Link>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<Link
to={`/projects/${project.id}/workflows`}
className={`${active ? 'bg-gray-100' : ''} block px-4 py-2 text-sm text-gray-700`}
>
Manage Workflows
</Link>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<Link
to={`/projects/${project.id}/bzzz`}
className={`${active ? 'bg-gray-100' : ''} block px-4 py-2 text-sm text-gray-700`}
>
🐝 Bzzz Integration
</Link>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<button
className={`${active ? 'bg-gray-100' : ''} block w-full text-left px-4 py-2 text-sm text-red-700`}
onClick={() => {
// Handle arcwhoosh/delete
}}
>
Arcwhoosh Project
</button>
)}
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</Menu>
</div>
{/* Status and Tags */}
<div className="flex items-center justify-between mt-4">
<div className="flex items-center space-x-2">
<span className={getStatusBadge(project.status)}>
{project.status}
</span>
{/* Bzzz Integration Status */}
{(project as any).bzzz_config?.bzzz_enabled && (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
🐝 Bzzz
{(project as any).bzzz_config?.ready_to_claim && (
<span className="ml-1 inline-block w-2 h-2 bg-green-400 rounded-full"></span>
)}
</span>
)}
</div>
<div className="flex items-center space-x-1">
{project.tags?.slice(0, 2).map((tag) => (
<span key={tag} className="inline-flex items-center px-2 py-1 rounded text-xs bg-gray-100 text-gray-600">
<TagIcon className="h-3 w-3 mr-1" />
{tag}
</span>
))}
{project.tags && project.tags.length > 2 && (
<span className="text-xs text-gray-500">+{project.tags.length - 2}</span>
)}
</div>
</div>
{/* GitHub Repository Info for Bzzz-enabled projects */}
{(project as any).bzzz_config?.bzzz_enabled && (project as any).bzzz_config?.git_url && (
<div className="mt-3 text-xs text-gray-500">
<div className="flex items-center space-x-1">
<svg className="h-3 w-3" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z" clipRule="evenodd" />
</svg>
<span>{(project as any).bzzz_config.git_owner}/{(project as any).bzzz_config.git_repository}</span>
{(project as any).bzzz_config.ready_to_claim && (
<span className="text-green-600"> Ready for tasks</span>
)}
</div>
</div>
)}
</div>
{/* Metrics */}
<div className="border-t px-6 py-4">
<div className="grid grid-cols-2 gap-4">
<div className="flex items-center space-x-2">
<Cog6ToothIcon className="h-4 w-4 text-gray-400" />
<div>
<p className="text-sm font-medium text-gray-900">{(project as any).workflow_count || 0}</p>
<p className="text-xs text-gray-500">Workflows</p>
</div>
</div>
<div className="flex items-center space-x-2">
<FolderIcon className="h-4 w-4 text-gray-400" />
<div>
<p className="text-sm font-medium text-gray-900">{(project as any).file_count || 0}</p>
<p className="text-xs text-gray-500">Files</p>
</div>
</div>
<div className="flex items-center space-x-2">
<ChartBarIcon className="h-4 w-4 text-gray-400" />
<div>
<p className="text-sm font-medium text-gray-900">
{(project as any).has_project_plan ? 'Yes' : 'No'}
</p>
<p className="text-xs text-gray-500">Project Plan</p>
</div>
</div>
<div className="flex items-center space-x-2">
<ClockIcon className="h-4 w-4 text-gray-400" />
<div>
<p className="text-sm font-medium text-gray-900">
{formatDistanceToNow(new Date(project.updated_at), { addSuffix: true })}
</p>
<p className="text-xs text-gray-500">Last Update</p>
</div>
</div>
</div>
</div>
{/* Quick Actions */}
<div className="border-t px-6 py-3 bg-gray-50 rounded-b-lg">
<div className="flex justify-between">
<Link
to={`/projects/${project.id}/workflows`}
className="text-sm text-blue-600 hover:text-blue-800 font-medium"
>
View Workflows
</Link>
<Link
to={`/projects/${project.id}`}
className="text-sm text-gray-600 hover:text-gray-800 font-medium"
>
View Details
</Link>
</div>
</div>
</div>
);
})}
</div>
)}
</div>
);
}