Files
hive/frontend/src/pages/GitRepositories.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

592 lines
20 KiB
TypeScript

/**
* Git Repositories Management Page
* Allows users to add, manage, and configure git repositories for their projects
*/
import React, { useState, useEffect } from 'react';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Badge } from '@/components/ui/badge';
import {
GitBranch,
Plus,
Settings,
RefreshCw,
Trash2,
FolderOpen,
FileText,
ExternalLink,
Key,
Lock,
Unlock,
CheckCircle,
AlertCircle,
Loader2,
Eye,
EyeOff
} from 'lucide-react';
import { apiConfig } from '../config/api';
import { useAuthenticatedFetch } from '../contexts/AuthContext';
interface GitRepository {
id: string;
name: string;
url: string;
project_id?: string;
local_path?: string;
default_branch: string;
status: string;
last_updated?: string;
commit_hash?: string;
commit_message?: string;
error_message?: string;
credentials: {
auth_type: string;
has_username: boolean;
has_password: boolean;
has_ssh_key: boolean;
};
}
interface Project {
id: string;
name: string;
}
export const GitRepositories: React.FC = () => {
const [repositories, setRepositories] = useState<GitRepository[]>([]);
const [projects, setProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(true);
const [showAddForm, setShowAddForm] = useState(false);
const [formData, setFormData] = useState({
name: '',
url: '',
project_id: '',
auth_type: 'https',
username: '',
password: '',
ssh_key_content: ''
});
const [formLoading, setFormLoading] = useState(false);
const [error, setError] = useState<string>('');
const [showPassword, setShowPassword] = useState(false);
const [selectedRepo, setSelectedRepo] = useState<GitRepository | null>(null);
const [showFiles, setShowFiles] = useState<string | null>(null);
const [fileStructure, setFileStructure] = useState<any>(null);
const authenticatedFetch = useAuthenticatedFetch();
const API_BASE_URL = apiConfig.baseURL + '/api';
useEffect(() => {
loadRepositories();
loadProjects();
}, []);
const loadRepositories = async () => {
try {
const response = await authenticatedFetch(`${API_BASE_URL}/git-repositories/`);
const data = await response.json();
if (data.success) {
setRepositories(data.data.repositories);
} else {
setError('Failed to load repositories');
}
} catch (err: any) {
setError(`Error loading repositories: ${err.message}`);
} finally {
setLoading(false);
}
};
const loadProjects = async () => {
try {
const response = await authenticatedFetch(`${API_BASE_URL}/projects`);
const data = await response.json();
if (data.success) {
setProjects(data.data.projects || []);
}
} catch (err: any) {
console.error('Error loading projects:', err);
}
};
const handleAddRepository = async () => {
try {
setFormLoading(true);
setError('');
const credentials = {
auth_type: formData.auth_type,
username: formData.username || undefined,
password: formData.password || undefined,
ssh_key_content: formData.ssh_key_content || undefined
};
const response = await authenticatedFetch(`${API_BASE_URL}/git-repositories/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: formData.name,
url: formData.url,
project_id: formData.project_id || undefined,
credentials
})
});
const data = await response.json();
if (data.success) {
setShowAddForm(false);
setFormData({
name: '',
url: '',
project_id: '',
auth_type: 'https',
username: '',
password: '',
ssh_key_content: ''
});
loadRepositories();
} else {
setError(data.data?.error || 'Failed to add repository');
}
} catch (err: any) {
setError(`Error adding repository: ${err.message}`);
} finally {
setFormLoading(false);
}
};
const handleUpdateRepository = async (repoId: string) => {
try {
const response = await authenticatedFetch(`${API_BASE_URL}/git-repositories/${repoId}/update`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
loadRepositories();
} else {
setError(data.data?.error || 'Failed to update repository');
}
} catch (err: any) {
setError(`Error updating repository: ${err.message}`);
}
};
const handleRemoveRepository = async (repoId: string) => {
if (!confirm('Are you sure you want to remove this repository?')) return;
try {
const response = await authenticatedFetch(`${API_BASE_URL}/git-repositories/${repoId}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
loadRepositories();
} else {
setError(data.data?.error || 'Failed to remove repository');
}
} catch (err: any) {
setError(`Error removing repository: ${err.message}`);
}
};
const loadFileStructure = async (repoId: string) => {
try {
const response = await authenticatedFetch(`${API_BASE_URL}/git-repositories/${repoId}/files`);
const data = await response.json();
if (data.success) {
setFileStructure(data.data.structure);
setShowFiles(repoId);
} else {
setError(data.data?.error || 'Failed to load file structure');
}
} catch (err: any) {
setError(`Error loading files: ${err.message}`);
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'ready':
return <CheckCircle className="w-4 h-4 text-green-600" />;
case 'cloning':
return <Loader2 className="w-4 h-4 text-blue-600 animate-spin" />;
case 'error':
return <AlertCircle className="w-4 h-4 text-red-600" />;
default:
return <AlertCircle className="w-4 h-4 text-gray-400" />;
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'ready': return 'bg-green-100 text-green-800';
case 'cloning': return 'bg-blue-100 text-blue-800';
case 'error': return 'bg-red-100 text-red-800';
default: return 'bg-gray-100 text-gray-800';
}
};
const getAuthIcon = (authType: string) => {
switch (authType) {
case 'ssh': return <Key className="w-4 h-4" />;
case 'https': return <Lock className="w-4 h-4" />;
default: return <Unlock className="w-4 h-4" />;
}
};
if (loading) {
return (
<div className="p-6">
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-blue-600" />
<span className="ml-2 text-gray-600">Loading repositories...</span>
</div>
</div>
);
}
return (
<div className="p-6 space-y-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold text-gray-900">Git Repositories</h1>
<p className="text-gray-600 mt-2">
Manage git repositories for your projects with secure credential storage
</p>
</div>
<Dialog open={showAddForm} onOpenChange={setShowAddForm}>
<DialogTrigger asChild>
<Button>
<Plus className="w-4 h-4 mr-2" />
Add Repository
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Add Git Repository</DialogTitle>
</DialogHeader>
<div className="space-y-4">
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="name">Repository Name</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
placeholder="My Project Repository"
/>
</div>
<div>
<Label htmlFor="project">Project (Optional)</Label>
<Select
value={formData.project_id}
onValueChange={(value) => setFormData(prev => ({ ...prev, project_id: value }))}
>
<SelectTrigger>
<SelectValue placeholder="Select project" />
</SelectTrigger>
<SelectContent>
<SelectItem value="">No project</SelectItem>
{projects.map((project) => (
<SelectItem key={project.id} value={project.id}>
{project.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div>
<Label htmlFor="url">Repository URL</Label>
<Input
id="url"
value={formData.url}
onChange={(e) => setFormData(prev => ({ ...prev, url: e.target.value }))}
placeholder="https://github.com/user/repo.git or git@github.com:user/repo.git"
/>
</div>
<div>
<Label htmlFor="auth_type">Authentication Type</Label>
<Select
value={formData.auth_type}
onValueChange={(value) => setFormData(prev => ({ ...prev, auth_type: value }))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="https">HTTPS (Username/Password)</SelectItem>
<SelectItem value="ssh">SSH Key</SelectItem>
<SelectItem value="token">Personal Access Token</SelectItem>
</SelectContent>
</Select>
</div>
{formData.auth_type === 'https' && (
<>
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="username">Username</Label>
<Input
id="username"
value={formData.username}
onChange={(e) => setFormData(prev => ({ ...prev, username: e.target.value }))}
placeholder="Git username"
/>
</div>
<div>
<Label htmlFor="password">Password/Token</Label>
<div className="relative">
<Input
id="password"
type={showPassword ? 'text' : 'password'}
value={formData.password}
onChange={(e) => setFormData(prev => ({ ...prev, password: e.target.value }))}
placeholder="Password or personal access token"
className="pr-10"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-3 text-gray-400 hover:text-gray-600"
>
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
</div>
</div>
</>
)}
{formData.auth_type === 'ssh' && (
<div>
<Label htmlFor="ssh_key">SSH Private Key</Label>
<Textarea
id="ssh_key"
value={formData.ssh_key_content}
onChange={(e) => setFormData(prev => ({ ...prev, ssh_key_content: e.target.value }))}
placeholder="-----BEGIN OPENSSH PRIVATE KEY-----&#10;...&#10;-----END OPENSSH PRIVATE KEY-----"
rows={8}
/>
<p className="text-sm text-gray-500 mt-1">
Paste your private SSH key content here
</p>
</div>
)}
{formData.auth_type === 'token' && (
<div>
<Label htmlFor="token">Personal Access Token</Label>
<div className="relative">
<Input
id="token"
type={showPassword ? 'text' : 'password'}
value={formData.password}
onChange={(e) => setFormData(prev => ({ ...prev, password: e.target.value }))}
placeholder="ghp_xxxxxxxxxxxxxxxxxxxx"
className="pr-10"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-3 text-gray-400 hover:text-gray-600"
>
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
<p className="text-sm text-gray-500 mt-1">
Generate a personal access token from your git provider
</p>
</div>
)}
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setShowAddForm(false)}>
Cancel
</Button>
<Button
onClick={handleAddRepository}
disabled={formLoading || !formData.name || !formData.url}
>
{formLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Adding...
</>
) : (
'Add Repository'
)}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{repositories.length === 0 ? (
<Card className="col-span-full">
<CardContent className="text-center py-12">
<GitBranch className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-gray-900 mb-2">No Repositories</h3>
<p className="text-gray-600 mb-4">
Add your first git repository to start working with your codebase
</p>
<Button onClick={() => setShowAddForm(true)}>
<Plus className="w-4 h-4 mr-2" />
Add Repository
</Button>
</CardContent>
</Card>
) : (
repositories.map((repo) => (
<Card key={repo.id}>
<CardHeader>
<div className="flex items-start justify-between">
<div className="flex-1">
<CardTitle className="flex items-center gap-2">
<GitBranch className="w-5 h-5" />
{repo.name}
<Badge className={`ml-2 ${getStatusColor(repo.status)}`}>
{getStatusIcon(repo.status)}
<span className="ml-1">{repo.status}</span>
</Badge>
</CardTitle>
<p className="text-sm text-gray-600 mt-1">{repo.url}</p>
</div>
<div className="flex items-center gap-2">
{getAuthIcon(repo.credentials.auth_type)}
<span className="text-xs text-gray-500">{repo.credentials.auth_type}</span>
</div>
</div>
</CardHeader>
<CardContent>
<div className="space-y-3">
{repo.commit_hash && (
<div className="text-sm">
<span className="font-medium">Latest commit:</span>
<div className="mt-1 p-2 bg-gray-50 rounded text-xs">
<div className="font-mono text-gray-700">{repo.commit_hash.substring(0, 8)}</div>
{repo.commit_message && (
<div className="text-gray-600 mt-1">{repo.commit_message}</div>
)}
</div>
</div>
)}
{repo.error_message && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription className="text-xs">{repo.error_message}</AlertDescription>
</Alert>
)}
<div className="flex flex-wrap gap-2">
<Button
variant="outline"
size="sm"
onClick={() => handleUpdateRepository(repo.id)}
disabled={repo.status === 'cloning'}
>
<RefreshCw className="w-4 h-4 mr-1" />
Update
</Button>
<Button
variant="outline"
size="sm"
onClick={() => loadFileStructure(repo.id)}
disabled={repo.status !== 'ready'}
>
<FolderOpen className="w-4 h-4 mr-1" />
Browse
</Button>
<Button
variant="outline"
size="sm"
onClick={() => window.open(repo.url, '_blank')}
>
<ExternalLink className="w-4 h-4 mr-1" />
Remote
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleRemoveRepository(repo.id)}
className="text-red-600 hover:text-red-700"
>
<Trash2 className="w-4 h-4 mr-1" />
Remove
</Button>
</div>
</div>
</CardContent>
</Card>
))
)}
</div>
{/* File Browser Modal */}
{showFiles && fileStructure && (
<Dialog open={!!showFiles} onOpenChange={() => setShowFiles(null)}>
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Repository Files</DialogTitle>
</DialogHeader>
<div className="space-y-2">
<div className="text-sm text-gray-600 mb-4">
Browsing: {repositories.find(r => r.id === showFiles)?.name}
</div>
<div className="border rounded p-4 bg-gray-50 font-mono text-sm">
<pre>{JSON.stringify(fileStructure, null, 2)}</pre>
</div>
</div>
</DialogContent>
</Dialog>
)}
</div>
);
};
export default GitRepositories;