modification in git-service oct 14
This commit is contained in:
parent
0d3f89da0f
commit
6698d11597
7
package-lock.json
generated
7
package-lock.json
generated
@ -3708,7 +3708,7 @@
|
|||||||
"version": "19.1.10",
|
"version": "19.1.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz",
|
||||||
"integrity": "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg==",
|
"integrity": "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
@ -3718,7 +3718,7 @@
|
|||||||
"version": "19.1.7",
|
"version": "19.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz",
|
||||||
"integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==",
|
"integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.0.0"
|
"@types/react": "^19.0.0"
|
||||||
@ -4896,7 +4896,7 @@
|
|||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/d3-array": {
|
"node_modules/d3-array": {
|
||||||
@ -8393,7 +8393,6 @@
|
|||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/react-redux": {
|
"node_modules/react-redux": {
|
||||||
|
|||||||
422
src/app/vcs/repos/page.tsx
Normal file
422
src/app/vcs/repos/page.tsx
Normal file
@ -0,0 +1,422 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import {
|
||||||
|
Github,
|
||||||
|
Gitlab,
|
||||||
|
FolderOpen,
|
||||||
|
Search,
|
||||||
|
RefreshCw,
|
||||||
|
ExternalLink,
|
||||||
|
GitBranch,
|
||||||
|
Star,
|
||||||
|
Eye,
|
||||||
|
Code,
|
||||||
|
Calendar,
|
||||||
|
GitCompare,
|
||||||
|
Brain,
|
||||||
|
GitCommit,
|
||||||
|
Server
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { authApiClient } from '@/components/apis/authApiClients';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
interface VcsRepoSummary {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
full_name: string;
|
||||||
|
description?: string;
|
||||||
|
language?: string;
|
||||||
|
visibility: 'public' | 'private';
|
||||||
|
provider: 'github' | 'gitlab' | 'bitbucket' | 'gitea';
|
||||||
|
html_url: string;
|
||||||
|
clone_url: string;
|
||||||
|
default_branch: string;
|
||||||
|
stargazers_count?: number;
|
||||||
|
watchers_count?: number;
|
||||||
|
forks_count?: number;
|
||||||
|
updated_at: string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VcsReposPage: React.FC = () => {
|
||||||
|
const [repositories, setRepositories] = useState<VcsRepoSummary[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [filter, setFilter] = useState<'all' | 'public' | 'private'>('all');
|
||||||
|
const [providerFilter, setProviderFilter] = useState<'all' | 'github' | 'gitlab' | 'bitbucket' | 'gitea'>('all');
|
||||||
|
const [aiAnalysisLoading, setAiAnalysisLoading] = useState<string | null>(null);
|
||||||
|
const [aiAnalysisError, setAiAnalysisError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Handle OAuth callback for all providers
|
||||||
|
useEffect(() => {
|
||||||
|
const handleVcsCallback = async () => {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const code = urlParams.get('code');
|
||||||
|
const state = urlParams.get('state');
|
||||||
|
const error = urlParams.get('error');
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('VCS OAuth error:', error);
|
||||||
|
alert(`OAuth error: ${error}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code && state) {
|
||||||
|
console.log('🔐 [VCS OAuth] Callback received, processing...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if we have a pending repository attachment
|
||||||
|
const pendingAttach = sessionStorage.getItem('pending_git_attach');
|
||||||
|
if (pendingAttach) {
|
||||||
|
const { repository_url, branch_name, provider } = JSON.parse(pendingAttach);
|
||||||
|
console.log(`🔐 [VCS OAuth] Resuming repository attachment:`, { repository_url, branch_name, provider });
|
||||||
|
|
||||||
|
// Clear the pending attach
|
||||||
|
sessionStorage.removeItem('pending_git_attach');
|
||||||
|
|
||||||
|
// Now attach the repository
|
||||||
|
try {
|
||||||
|
const response = await authApiClient.post(`/api/vcs/${provider}/attach-repository`, {
|
||||||
|
repository_url,
|
||||||
|
branch_name: branch_name || undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
alert(`${provider?.toUpperCase()} account connected and repository attached successfully!`);
|
||||||
|
|
||||||
|
// Clean up URL parameters
|
||||||
|
const newUrl = window.location.pathname;
|
||||||
|
window.history.replaceState({}, document.title, newUrl);
|
||||||
|
|
||||||
|
// Refresh the page to show the attached repository
|
||||||
|
window.location.reload();
|
||||||
|
} catch (attachError) {
|
||||||
|
console.error('Failed to attach repository after OAuth:', attachError);
|
||||||
|
alert(`${provider?.toUpperCase()} account connected, but failed to attach repository. Please try again.`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('🔐 [VCS OAuth] No pending repository attachment, just connecting account');
|
||||||
|
alert('Account connected successfully!');
|
||||||
|
|
||||||
|
// Clean up URL parameters
|
||||||
|
const newUrl = window.location.pathname;
|
||||||
|
window.history.replaceState({}, document.title, newUrl);
|
||||||
|
|
||||||
|
// Refresh repositories
|
||||||
|
loadRepositories();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error handling VCS OAuth callback:', error);
|
||||||
|
alert('Error processing OAuth callback');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleVcsCallback();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Load repositories from all providers
|
||||||
|
const loadRepositories = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const allRepos: VcsRepoSummary[] = [];
|
||||||
|
|
||||||
|
// Try to load from each provider
|
||||||
|
const providers = ['github', 'gitlab', 'bitbucket', 'gitea'];
|
||||||
|
|
||||||
|
for (const provider of providers) {
|
||||||
|
try {
|
||||||
|
const response = await authApiClient.get(`/api/vcs/${provider}/repositories`);
|
||||||
|
const repos = response.data?.data || [];
|
||||||
|
|
||||||
|
// Add provider info to each repo
|
||||||
|
const reposWithProvider = repos.map((repo: any) => ({
|
||||||
|
...repo,
|
||||||
|
provider: provider
|
||||||
|
}));
|
||||||
|
|
||||||
|
allRepos.push(...reposWithProvider);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`No ${provider} repositories or not connected:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setRepositories(allRepos);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to load repositories');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadRepositories();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
await loadRepositories();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAiAnalysis = async (repositoryId: string) => {
|
||||||
|
try {
|
||||||
|
setAiAnalysisLoading(repositoryId);
|
||||||
|
setAiAnalysisError(null);
|
||||||
|
|
||||||
|
const response = await authApiClient.get(`/api/ai-stream/repository/${repositoryId}/ai-stream`);
|
||||||
|
const data = response.data;
|
||||||
|
|
||||||
|
console.log('AI Analysis Result:', data);
|
||||||
|
|
||||||
|
alert('AI Analysis completed successfully!');
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'AI Analysis failed';
|
||||||
|
setAiAnalysisError(errorMessage);
|
||||||
|
console.error('AI Analysis Error:', err);
|
||||||
|
} finally {
|
||||||
|
setAiAnalysisLoading(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProviderIcon = (provider: string) => {
|
||||||
|
switch (provider) {
|
||||||
|
case 'github': return <Github className="h-4 w-4" />;
|
||||||
|
case 'gitlab': return <Gitlab className="h-4 w-4" />;
|
||||||
|
case 'bitbucket': return <GitCommit className="h-4 w-4" />;
|
||||||
|
case 'gitea': return <Server className="h-4 w-4" />;
|
||||||
|
default: return <GitCommit className="h-4 w-4" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProviderColor = (provider: string) => {
|
||||||
|
switch (provider) {
|
||||||
|
case 'github': return 'bg-gray-100 text-gray-800';
|
||||||
|
case 'gitlab': return 'bg-orange-100 text-orange-800';
|
||||||
|
case 'bitbucket': return 'bg-blue-100 text-blue-800';
|
||||||
|
case 'gitea': return 'bg-green-100 text-green-800';
|
||||||
|
default: return 'bg-gray-100 text-gray-800';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredRepositories = repositories.filter(repo => {
|
||||||
|
const matchesSearch = repo.full_name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
repo.description?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
repo.language?.toLowerCase().includes(searchQuery.toLowerCase());
|
||||||
|
|
||||||
|
const matchesFilter = filter === 'all' ||
|
||||||
|
(filter === 'public' && repo.visibility === 'public') ||
|
||||||
|
(filter === 'private' && repo.visibility === 'private');
|
||||||
|
|
||||||
|
const matchesProvider = providerFilter === 'all' || repo.provider === providerFilter;
|
||||||
|
|
||||||
|
return matchesSearch && matchesFilter && matchesProvider;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-6">
|
||||||
|
<div className="mb-6">
|
||||||
|
<h1 className="text-3xl font-bold mb-2">My Repositories</h1>
|
||||||
|
<p className="text-gray-600">Manage and analyze your repositories from all connected providers</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search and Filters */}
|
||||||
|
<div className="mb-6 space-y-4">
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
|
||||||
|
<Input
|
||||||
|
placeholder="Search repositories..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="pl-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button onClick={handleRefresh} disabled={isLoading} variant="outline">
|
||||||
|
<RefreshCw className={`h-4 w-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant={filter === 'all' ? 'default' : 'outline'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setFilter('all')}
|
||||||
|
>
|
||||||
|
All
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={filter === 'public' ? 'default' : 'outline'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setFilter('public')}
|
||||||
|
>
|
||||||
|
Public
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={filter === 'private' ? 'default' : 'outline'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setFilter('private')}
|
||||||
|
>
|
||||||
|
Private
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant={providerFilter === 'all' ? 'default' : 'outline'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setProviderFilter('all')}
|
||||||
|
>
|
||||||
|
All Providers
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={providerFilter === 'github' ? 'default' : 'outline'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setProviderFilter('github')}
|
||||||
|
>
|
||||||
|
<Github className="h-4 w-4 mr-1" />
|
||||||
|
GitHub
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={providerFilter === 'gitlab' ? 'default' : 'outline'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setProviderFilter('gitlab')}
|
||||||
|
>
|
||||||
|
<Gitlab className="h-4 w-4 mr-1" />
|
||||||
|
GitLab
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={providerFilter === 'bitbucket' ? 'default' : 'outline'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setProviderFilter('bitbucket')}
|
||||||
|
>
|
||||||
|
<GitCommit className="h-4 w-4 mr-1" />
|
||||||
|
Bitbucket
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={providerFilter === 'gitea' ? 'default' : 'outline'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setProviderFilter('gitea')}
|
||||||
|
>
|
||||||
|
<Server className="h-4 w-4 mr-1" />
|
||||||
|
Gitea
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error State */}
|
||||||
|
{error && (
|
||||||
|
<div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||||
|
<p className="text-red-800">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Loading State */}
|
||||||
|
{isLoading && (
|
||||||
|
<div className="flex justify-center items-center py-8">
|
||||||
|
<RefreshCw className="h-8 w-8 animate-spin text-gray-400" />
|
||||||
|
<span className="ml-2 text-gray-600">Loading repositories...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Repositories Grid */}
|
||||||
|
{!isLoading && (
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{filteredRepositories.map((repo) => (
|
||||||
|
<Card key={repo.id} className="hover:shadow-lg transition-shadow">
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
{getProviderIcon(repo.provider)}
|
||||||
|
<CardTitle className="text-lg truncate">{repo.name}</CardTitle>
|
||||||
|
</div>
|
||||||
|
<Badge variant={repo.visibility === 'public' ? 'default' : 'secondary'}>
|
||||||
|
{repo.visibility}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Badge className={getProviderColor(repo.provider)}>
|
||||||
|
{repo.provider.toUpperCase()}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
{repo.description && (
|
||||||
|
<p className="text-sm text-gray-600 line-clamp-2">{repo.description}</p>
|
||||||
|
)}
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="pt-0">
|
||||||
|
<div className="flex items-center space-x-4 text-sm text-gray-500 mb-4">
|
||||||
|
{repo.language && (
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<Code className="h-4 w-4" />
|
||||||
|
<span>{repo.language}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<GitBranch className="h-4 w-4" />
|
||||||
|
<span>{repo.default_branch}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => window.open(repo.html_url, '_blank')}
|
||||||
|
>
|
||||||
|
<ExternalLink className="h-4 w-4 mr-1" />
|
||||||
|
View
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleAiAnalysis(repo.id)}
|
||||||
|
disabled={aiAnalysisLoading === repo.id}
|
||||||
|
>
|
||||||
|
<Brain className="h-4 w-4 mr-1" />
|
||||||
|
{aiAnalysisLoading === repo.id ? 'Analyzing...' : 'AI Analysis'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{aiAnalysisError && (
|
||||||
|
<p className="text-red-600 text-sm mt-2">{aiAnalysisError}</p>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Empty State */}
|
||||||
|
{!isLoading && filteredRepositories.length === 0 && (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<FolderOpen className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-2">No repositories found</h3>
|
||||||
|
<p className="text-gray-600 mb-4">
|
||||||
|
{searchQuery || filter !== 'all' || providerFilter !== 'all'
|
||||||
|
? 'Try adjusting your search or filters'
|
||||||
|
: 'Connect your accounts to see your repositories'
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
<Link href="/dashboard">
|
||||||
|
<Button>Go to Dashboard</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VcsReposPage;
|
||||||
@ -8,7 +8,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Checkbox } from "@/components/ui/checkbox"
|
import { Checkbox } from "@/components/ui/checkbox"
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"
|
||||||
import { ArrowRight, Plus, Globe, BarChart3, Zap, Code, Search, Star, Clock, Users, Layers, AlertCircle, Edit, Trash2, User, Palette, GitBranch } from "lucide-react"
|
import { ArrowRight, Plus, Globe, BarChart3, Zap, Code, Search, Star, Clock, Users, Layers, AlertCircle, Edit, Trash2, User, Palette, GitBranch, FolderGit2 } from "lucide-react"
|
||||||
import { useTemplates } from "@/hooks/useTemplates"
|
import { useTemplates } from "@/hooks/useTemplates"
|
||||||
import { CustomTemplateForm } from "@/components/custom-template-form"
|
import { CustomTemplateForm } from "@/components/custom-template-form"
|
||||||
import { EditTemplateForm } from "@/components/edit-template-form"
|
import { EditTemplateForm } from "@/components/edit-template-form"
|
||||||
@ -23,7 +23,8 @@ import PromptSidePanel from "@/components/prompt-side-panel"
|
|||||||
import { DualCanvasEditor } from "@/components/dual-canvas-editor"
|
import { DualCanvasEditor } from "@/components/dual-canvas-editor"
|
||||||
import { getAccessToken } from "@/components/apis/authApiClients"
|
import { getAccessToken } from "@/components/apis/authApiClients"
|
||||||
import TechStackSummary from "@/components/tech-stack-summary"
|
import TechStackSummary from "@/components/tech-stack-summary"
|
||||||
import { attachRepository, getGitHubAuthStatus, AttachRepositoryResponse, connectGitHubWithRepo, initiateGitHubOAuth } from "@/lib/api/github"
|
import { attachRepository, detectProvider, connectProvider, AttachRepositoryResponse } from "@/lib/api/vcs"
|
||||||
|
import { getGitHubAuthStatus } from "@/lib/api/github"
|
||||||
import ViewUserReposButton from "@/components/github/ViewUserReposButton"
|
import ViewUserReposButton from "@/components/github/ViewUserReposButton"
|
||||||
import { ErrorBanner } from "@/components/ui/error-banner"
|
import { ErrorBanner } from "@/components/ui/error-banner"
|
||||||
|
|
||||||
@ -126,7 +127,7 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
|
|||||||
throw new Error('Repository attachment failed')
|
throw new Error('Repository attachment failed')
|
||||||
}
|
}
|
||||||
|
|
||||||
const isPrivate = result?.data?.is_public === false || result?.data?.requires_auth === true
|
const isPrivate = result?.data?.is_public === false
|
||||||
|
|
||||||
console.log('✅ Repository attached successfully:', result)
|
console.log('✅ Repository attached successfully:', result)
|
||||||
const repoType = isPrivate ? 'private' : 'public'
|
const repoType = isPrivate ? 'private' : 'public'
|
||||||
@ -155,15 +156,18 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (status === 401 && (data?.requires_auth || data?.auth_url || data?.service_auth_url)) {
|
if (status === 401 && (data?.requires_auth || data?.auth_url || data?.service_auth_url)) {
|
||||||
console.log('🔐 Private repository detected - initiating GitHub OAuth with repository context')
|
console.log('🔐 Private repository detected - initiating OAuth with repository context')
|
||||||
// Reset loading state before redirect
|
// Reset loading state before redirect
|
||||||
setIsGeneratingAuth(false)
|
setIsGeneratingAuth(false)
|
||||||
|
|
||||||
// Use the new OAuth helper that will auto-attach the repo after authentication
|
// Detect provider from URL
|
||||||
|
const detectedProvider = detectProvider(gitUrl.trim());
|
||||||
|
|
||||||
|
// Use the universal OAuth helper that will auto-attach the repo after authentication
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
connectGitHubWithRepo(gitUrl.trim(), gitBranch?.trim() || 'main').catch((oauthError) => {
|
connectProvider(detectedProvider, gitUrl.trim(), gitBranch?.trim() || 'main').catch((oauthError) => {
|
||||||
console.error('OAuth initiation failed:', oauthError)
|
console.error('OAuth initiation failed:', oauthError)
|
||||||
alert('Failed to initiate GitHub authentication. Please try again.')
|
alert(`Failed to initiate ${detectedProvider.toUpperCase()} authentication. Please try again.`)
|
||||||
})
|
})
|
||||||
}, 100)
|
}, 100)
|
||||||
return
|
return
|
||||||
@ -173,15 +177,18 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
|
|||||||
// Reset loading state before showing dialog
|
// Reset loading state before showing dialog
|
||||||
setIsGeneratingAuth(false)
|
setIsGeneratingAuth(false)
|
||||||
|
|
||||||
// Repository not accessible with current GitHub account - prompt to re-authenticate
|
// Repository not accessible with current account - prompt to re-authenticate
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const confirmReauth = confirm('Repository not accessible with your current GitHub account.\n\nThis could mean:\n- The repository belongs to a different GitHub account\n- Your token expired or lacks permissions\n\nWould you like to re-authenticate with GitHub?')
|
// Detect provider from URL
|
||||||
|
const detectedProvider = detectProvider(gitUrl.trim());
|
||||||
|
|
||||||
|
const confirmReauth = confirm(`Repository not accessible with your current ${detectedProvider.toUpperCase()} account.\n\nThis could mean:\n- The repository belongs to a different ${detectedProvider.toUpperCase()} account\n- Your token expired or lacks permissions\n\nWould you like to re-authenticate with ${detectedProvider.toUpperCase()}?`)
|
||||||
|
|
||||||
if (confirmReauth) {
|
if (confirmReauth) {
|
||||||
console.log('🔐 Re-authenticating with GitHub for private repository access')
|
console.log(`🔐 Re-authenticating with ${detectedProvider.toUpperCase()} for private repository access`)
|
||||||
connectGitHubWithRepo(gitUrl.trim(), gitBranch?.trim() || 'main').catch((oauthError) => {
|
connectProvider(detectedProvider, gitUrl.trim(), gitBranch?.trim() || 'main').catch((oauthError) => {
|
||||||
console.error('OAuth initiation failed:', oauthError)
|
console.error('OAuth initiation failed:', oauthError)
|
||||||
alert('Failed to initiate GitHub authentication. Please try again.')
|
alert(`Failed to initiate ${detectedProvider.toUpperCase()} authentication. Please try again.`)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, 100)
|
}, 100)
|
||||||
@ -205,21 +212,10 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
|
|||||||
const handleOAuthAuth = async (provider: string) => {
|
const handleOAuthAuth = async (provider: string) => {
|
||||||
setAuthLoading(true)
|
setAuthLoading(true)
|
||||||
try {
|
try {
|
||||||
const providerConfig = gitProviders[provider as keyof typeof gitProviders]
|
console.log(`🔐 [handleOAuthAuth] Starting OAuth for provider: ${provider}`)
|
||||||
if (!providerConfig.oauthEndpoint) {
|
|
||||||
throw new Error('OAuth not supported for this provider')
|
|
||||||
}
|
|
||||||
|
|
||||||
// For GitHub, use the new OAuth helper
|
// Use the new VCS API for all providers
|
||||||
if (provider === 'github') {
|
await connectProvider(provider)
|
||||||
console.log('Initiating GitHub OAuth flow...')
|
|
||||||
initiateGitHubOAuth()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// For other providers, use the old method
|
|
||||||
window.open(providerConfig.oauthEndpoint, '_blank', 'width=600,height=700')
|
|
||||||
alert(`Redirecting to ${providerConfig.name} OAuth...`)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('OAuth error:', error)
|
console.error('OAuth error:', error)
|
||||||
alert('OAuth authentication failed')
|
alert('OAuth authentication failed')
|
||||||
@ -294,7 +290,7 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
|
|||||||
icon: 'M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z',
|
icon: 'M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z',
|
||||||
placeholder: 'https://github.com/org/repo.git',
|
placeholder: 'https://github.com/org/repo.git',
|
||||||
authMethods: ['token', 'ssh', 'oauth'],
|
authMethods: ['token', 'ssh', 'oauth'],
|
||||||
oauthEndpoint: '/api/auth/github',
|
oauthEndpoint: '/api/vcs/github/auth/start',
|
||||||
apiEndpoint: 'https://api.github.com'
|
apiEndpoint: 'https://api.github.com'
|
||||||
},
|
},
|
||||||
bitbucket: {
|
bitbucket: {
|
||||||
@ -302,7 +298,7 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
|
|||||||
icon: 'M.778 1.213a.768.768 0 00-.768.892l3.263 19.81c.084.5.515.868 1.022.873H19.95a.772.772 0 00.77-.646l3.27-20.03a.768.768 0 00-.768-.891L.778 1.213zM14.518 18.197H9.482l-1.4-8.893h7.436l-1.4 8.893z',
|
icon: 'M.778 1.213a.768.768 0 00-.768.892l3.263 19.81c.084.5.515.868 1.022.873H19.95a.772.772 0 00.77-.646l3.27-20.03a.768.768 0 00-.768-.891L.778 1.213zM14.518 18.197H9.482l-1.4-8.893h7.436l-1.4 8.893z',
|
||||||
placeholder: 'https://bitbucket.org/org/repo.git',
|
placeholder: 'https://bitbucket.org/org/repo.git',
|
||||||
authMethods: ['username_password', 'app_password', 'oauth'],
|
authMethods: ['username_password', 'app_password', 'oauth'],
|
||||||
oauthEndpoint: '/api/auth/bitbucket',
|
oauthEndpoint: '/api/vcs/bitbucket/auth/start',
|
||||||
apiEndpoint: 'https://api.bitbucket.org/2.0'
|
apiEndpoint: 'https://api.bitbucket.org/2.0'
|
||||||
},
|
},
|
||||||
gitlab: {
|
gitlab: {
|
||||||
@ -310,7 +306,7 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
|
|||||||
icon: 'M23.6004 9.5927l-.0337-.0862L20.3.9814a.851.851 0 0 0-.3362-.405.874.874 0 0 0-.9997.0539.874.874 0 0 0-.29.4399l-2.5465 7.7838H7.2162l-2.5465-7.7838a.857.857 0 0 0-.29-.4412.874.874 0 0 0-.9997-.0537.858.858 0 0 0-.3362.4049L.4332 9.5015l-.0325.0862a6.065 6.065 0 0 0 2.0119 7.0105l.0113.0087.03.0213 4.976 3.7264 2.462 1.8633 1.4995 1.1321a1.008 1.008 0 0 0 1.2197 0l1.4995-1.1321 2.4619-1.8633 5.006-3.7466.0125-.01a6.068 6.068 0 0 0 2.0094-7.003z',
|
icon: 'M23.6004 9.5927l-.0337-.0862L20.3.9814a.851.851 0 0 0-.3362-.405.874.874 0 0 0-.9997.0539.874.874 0 0 0-.29.4399l-2.5465 7.7838H7.2162l-2.5465-7.7838a.857.857 0 0 0-.29-.4412.874.874 0 0 0-.9997-.0537.858.858 0 0 0-.3362.4049L.4332 9.5015l-.0325.0862a6.065 6.065 0 0 0 2.0119 7.0105l.0113.0087.03.0213 4.976 3.7264 2.462 1.8633 1.4995 1.1321a1.008 1.008 0 0 0 1.2197 0l1.4995-1.1321 2.4619-1.8633 5.006-3.7466.0125-.01a6.068 6.068 0 0 0 2.0094-7.003z',
|
||||||
placeholder: 'https://gitlab.com/org/repo.git',
|
placeholder: 'https://gitlab.com/org/repo.git',
|
||||||
authMethods: ['token', 'oauth', 'ssh'],
|
authMethods: ['token', 'oauth', 'ssh'],
|
||||||
oauthEndpoint: '/api/auth/gitlab',
|
oauthEndpoint: '/api/vcs/gitlab/auth/start',
|
||||||
apiEndpoint: 'https://gitlab.com/api/v4'
|
apiEndpoint: 'https://gitlab.com/api/v4'
|
||||||
},
|
},
|
||||||
other: {
|
other: {
|
||||||
@ -550,6 +546,12 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
|
|||||||
alert('Please enter a Git repository URL');
|
alert('Please enter a Git repository URL');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detect provider from URL
|
||||||
|
const detectedProvider = detectProvider(gitUrl.trim());
|
||||||
|
console.log('🔍 [handleCreateFromGit] Detected provider:', detectedProvider);
|
||||||
|
console.log('🔍 [handleCreateFromGit] Git URL:', gitUrl.trim());
|
||||||
|
|
||||||
// Attach the repository via backend (skip template creation)
|
// Attach the repository via backend (skip template creation)
|
||||||
try {
|
try {
|
||||||
await attachRepository({
|
await attachRepository({
|
||||||
@ -568,15 +570,15 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
|
|||||||
// Return a mock template object to proceed to next step
|
// Return a mock template object to proceed to next step
|
||||||
return {
|
return {
|
||||||
id: 'git-imported',
|
id: 'git-imported',
|
||||||
title: `Imported from ${gitProvider === 'other' ? 'Git' : gitProvider.charAt(0).toUpperCase() + gitProvider.slice(1)}`,
|
title: `Imported from ${detectedProvider.charAt(0).toUpperCase() + detectedProvider.slice(1)}`,
|
||||||
description: `Template imported from ${gitProvider === 'other' ? 'Git' : gitProvider.charAt(0).toUpperCase() + gitProvider.slice(1)}: ${gitUrl}`,
|
description: `Template imported from ${detectedProvider.charAt(0).toUpperCase() + detectedProvider.slice(1)}: ${gitUrl}`,
|
||||||
type: 'custom',
|
type: 'custom',
|
||||||
category: 'imported',
|
category: 'imported',
|
||||||
is_custom: true,
|
is_custom: true,
|
||||||
source: 'git',
|
source: 'git',
|
||||||
git_url: gitUrl.trim(),
|
git_url: gitUrl.trim(),
|
||||||
git_branch: gitBranch?.trim() || 'main',
|
git_branch: gitBranch?.trim() || 'main',
|
||||||
git_provider: gitProvider
|
git_provider: detectedProvider
|
||||||
}
|
}
|
||||||
} catch (attachErr) {
|
} catch (attachErr) {
|
||||||
console.error('[TemplateSelectionStep] attachRepository failed:', attachErr)
|
console.error('[TemplateSelectionStep] attachRepository failed:', attachErr)
|
||||||
@ -587,10 +589,10 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
|
|||||||
|
|
||||||
console.log('🔍 HandleCreateFromGit error response:', { status, data })
|
console.log('🔍 HandleCreateFromGit error response:', { status, data })
|
||||||
|
|
||||||
// If backend signals GitHub auth required, open the OAuth URL for this user
|
// If backend signals auth required, redirect to provider OAuth
|
||||||
if (status === 401 && (data?.requires_auth || data?.message?.includes('authentication'))) {
|
if ((status === 401 || status === 200) && (data?.requires_auth || data?.message?.includes('authentication'))) {
|
||||||
const url: string = data?.auth_url
|
const authUrl: string = data?.auth_url
|
||||||
if (!url) {
|
if (!authUrl) {
|
||||||
alert('Authentication URL is missing.');
|
alert('Authentication URL is missing.');
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -599,13 +601,15 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
|
|||||||
try {
|
try {
|
||||||
sessionStorage.setItem('pending_git_attach', JSON.stringify({
|
sessionStorage.setItem('pending_git_attach', JSON.stringify({
|
||||||
repository_url: gitUrl.trim(),
|
repository_url: gitUrl.trim(),
|
||||||
branch_name: (gitBranch?.trim() || undefined)
|
branch_name: (gitBranch?.trim() || undefined),
|
||||||
|
provider: detectedProvider
|
||||||
}))
|
}))
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
console.log('🔐 Redirecting to GitHub OAuth for repository attachment:', url)
|
console.log(`🔐 Redirecting to ${detectedProvider.toUpperCase()} OAuth for repository attachment:`, authUrl)
|
||||||
// Force same-tab redirect directly to GitHub consent screen
|
console.log(`🔐 Provider being passed to connectProvider:`, detectedProvider)
|
||||||
window.location.replace(url)
|
// Use connectProvider function to handle OAuth flow properly
|
||||||
|
await connectProvider(detectedProvider, gitUrl.trim(), gitBranch?.trim())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -863,6 +867,12 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
|
|||||||
{/* Right-aligned quick navigation to user repos */}
|
{/* Right-aligned quick navigation to user repos */}
|
||||||
<div className="flex justify-end space-x-2">
|
<div className="flex justify-end space-x-2">
|
||||||
<ViewUserReposButton className="bg-orange-500 hover:bg-orange-400 text-black" label="My GitHub Repos" />
|
<ViewUserReposButton className="bg-orange-500 hover:bg-orange-400 text-black" label="My GitHub Repos" />
|
||||||
|
<Link href="/vcs/repos">
|
||||||
|
<Button className="bg-blue-500 hover:bg-blue-400 text-white">
|
||||||
|
<FolderGit2 className="mr-2 h-5 w-5" />
|
||||||
|
All My Repos
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
|||||||
63
src/components/vcs/VcsConnectionButton.tsx
Normal file
63
src/components/vcs/VcsConnectionButton.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Github, Gitlab, GitCommit, Server } from "lucide-react"
|
||||||
|
import { connectProvider } from "@/lib/api/vcs"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
provider: 'github' | 'gitlab' | 'bitbucket' | 'gitea'
|
||||||
|
className?: string
|
||||||
|
size?: "default" | "sm" | "lg" | "icon"
|
||||||
|
label?: string
|
||||||
|
repoUrl?: string
|
||||||
|
branch?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function VcsConnectionButton({
|
||||||
|
provider,
|
||||||
|
className,
|
||||||
|
size = "default",
|
||||||
|
label,
|
||||||
|
repoUrl,
|
||||||
|
branch
|
||||||
|
}: Props) {
|
||||||
|
const getProviderIcon = () => {
|
||||||
|
switch (provider) {
|
||||||
|
case 'github': return <Github className="mr-2 h-5 w-5" />;
|
||||||
|
case 'gitlab': return <Gitlab className="mr-2 h-5 w-5" />;
|
||||||
|
case 'bitbucket': return <GitCommit className="mr-2 h-5 w-5" />;
|
||||||
|
case 'gitea': return <Server className="mr-2 h-5 w-5" />;
|
||||||
|
default: return <GitCommit className="mr-2 h-5 w-5" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProviderColor = () => {
|
||||||
|
switch (provider) {
|
||||||
|
case 'github': return 'bg-gray-600 hover:bg-gray-700';
|
||||||
|
case 'gitlab': return 'bg-orange-600 hover:bg-orange-700';
|
||||||
|
case 'bitbucket': return 'bg-blue-600 hover:bg-blue-700';
|
||||||
|
case 'gitea': return 'bg-green-600 hover:bg-green-700';
|
||||||
|
default: return 'bg-gray-600 hover:bg-gray-700';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConnect = async () => {
|
||||||
|
try {
|
||||||
|
await connectProvider(provider, repoUrl, branch);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to connect ${provider}:`, error);
|
||||||
|
alert(`Failed to connect ${provider.toUpperCase()} account. Please try again.`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className={`${getProviderColor()} text-white ${className}`}
|
||||||
|
size={size}
|
||||||
|
onClick={handleConnect}
|
||||||
|
>
|
||||||
|
{getProviderIcon()}
|
||||||
|
{label || `Connect ${provider.charAt(0).toUpperCase() + provider.slice(1)}`}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
239
src/lib/api/vcs.ts
Normal file
239
src/lib/api/vcs.ts
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
import { authApiClient } from '@/components/apis/authApiClients'
|
||||||
|
|
||||||
|
export interface AttachRepositoryPayload {
|
||||||
|
repository_url: string
|
||||||
|
branch_name?: string
|
||||||
|
user_id?: string
|
||||||
|
template_id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AttachRepositoryResponse {
|
||||||
|
success: boolean
|
||||||
|
message: string
|
||||||
|
data?: {
|
||||||
|
repository_id: string
|
||||||
|
repository_name: string
|
||||||
|
owner_name: string
|
||||||
|
branch_name: string
|
||||||
|
is_public: boolean
|
||||||
|
sync_status: string
|
||||||
|
webhook_result?: {
|
||||||
|
created: boolean
|
||||||
|
hook_id: string | number
|
||||||
|
}
|
||||||
|
storage_info?: {
|
||||||
|
total_files: number
|
||||||
|
total_directories: number
|
||||||
|
total_size_bytes: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
requires_auth?: boolean
|
||||||
|
auth_url?: string
|
||||||
|
auth_error?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provider detection function
|
||||||
|
export function detectProvider(repoUrl: string): string {
|
||||||
|
if (repoUrl.includes('github.com')) return 'github';
|
||||||
|
if (repoUrl.includes('gitlab.com') || repoUrl.includes('gitlab')) return 'gitlab';
|
||||||
|
if (repoUrl.includes('bitbucket.org')) return 'bitbucket';
|
||||||
|
if (repoUrl.includes('gitea')) return 'gitea';
|
||||||
|
throw new Error('Unsupported repository provider');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Universal repository attachment function
|
||||||
|
export async function attachRepository(payload: AttachRepositoryPayload, retries = 3): Promise<AttachRepositoryResponse> {
|
||||||
|
const rawUser = (typeof window !== 'undefined') ? localStorage.getItem('codenuk_user') : null
|
||||||
|
const userId = rawUser ? (JSON.parse(rawUser)?.id || null) : null
|
||||||
|
|
||||||
|
// Detect provider from URL
|
||||||
|
const provider = detectProvider(payload.repository_url);
|
||||||
|
console.log('🔍 [attachRepository] Detected provider:', provider);
|
||||||
|
|
||||||
|
const url = userId
|
||||||
|
? `/api/vcs/${provider}/attach-repository?user_id=${encodeURIComponent(userId)}`
|
||||||
|
: `/api/vcs/${provider}/attach-repository`;
|
||||||
|
|
||||||
|
// Retry logic for connection issues
|
||||||
|
for (let i = 0; i < retries; i++) {
|
||||||
|
try {
|
||||||
|
const response = await authApiClient.post(url, {
|
||||||
|
...payload,
|
||||||
|
user_id: userId
|
||||||
|
}, {
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
timeout: 60000 // 60 seconds for repository operations
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`📡 [attachRepository] ${provider.toUpperCase()} response:`, response.data);
|
||||||
|
|
||||||
|
// Normalize response
|
||||||
|
let parsed: any = response.data;
|
||||||
|
if (typeof parsed === 'string') {
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(parsed);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('📡 [attachRepository] Failed to parse string response');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized: AttachRepositoryResponse = {
|
||||||
|
...(parsed || {}),
|
||||||
|
success: (parsed?.success === true || parsed?.success === 'true')
|
||||||
|
};
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
} catch (error: any) {
|
||||||
|
// If it's the last retry or not a connection error, throw immediately
|
||||||
|
if (i === retries - 1 || (error.code !== 'ECONNREFUSED' && error.code !== 'ECONNRESET')) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait before retrying (exponential backoff)
|
||||||
|
const waitTime = Math.min(1000 * Math.pow(2, i), 5000);
|
||||||
|
console.log(`📡 [attachRepository] Retry ${i + 1}/${retries} in ${waitTime}ms...`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, waitTime));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Max retries exceeded');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Universal OAuth connection function
|
||||||
|
export async function connectProvider(provider: string, repoUrl?: string, branch?: string): Promise<void> {
|
||||||
|
const rawUser = (typeof window !== 'undefined') ? localStorage.getItem('codenuk_user') : null;
|
||||||
|
const userId = rawUser ? (JSON.parse(rawUser)?.id || null) : null;
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
alert('Please sign in first to connect your account');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build state with repository context if provided
|
||||||
|
const stateBase = Math.random().toString(36).substring(7);
|
||||||
|
let state = stateBase;
|
||||||
|
|
||||||
|
if (repoUrl) {
|
||||||
|
const encodedRepoUrl = encodeURIComponent(repoUrl);
|
||||||
|
const encodedBranch = encodeURIComponent(branch || 'main');
|
||||||
|
state = `${stateBase}|uid=${userId}|repo=${encodedRepoUrl}|branch=${encodedBranch}`;
|
||||||
|
|
||||||
|
// Store in sessionStorage for recovery
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem('pending_git_attach', JSON.stringify({
|
||||||
|
repository_url: repoUrl,
|
||||||
|
branch_name: branch || 'main',
|
||||||
|
provider: provider
|
||||||
|
}));
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to store pending attach in sessionStorage:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🔗 [connectProvider] Connecting ${provider.toUpperCase()} account for user:`, userId);
|
||||||
|
console.log(`🔗 [connectProvider] API URL: /api/vcs/${provider}/auth/start?user_id=${encodeURIComponent(userId)}&state=${encodeURIComponent(state)}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await authApiClient.get(
|
||||||
|
`/api/vcs/${provider}/auth/start?user_id=${encodeURIComponent(userId)}&state=${encodeURIComponent(state)}`
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`🔗 [connectProvider] Backend response:`, response.data);
|
||||||
|
|
||||||
|
const authUrl = response.data?.auth_url;
|
||||||
|
if (authUrl) {
|
||||||
|
console.log(`🔗 [connectProvider] Redirecting to ${provider.toUpperCase()} OAuth:`, authUrl);
|
||||||
|
console.log(`🔗 [connectProvider] Auth URL domain:`, new URL(authUrl).hostname);
|
||||||
|
window.location.replace(authUrl);
|
||||||
|
} else {
|
||||||
|
console.error('No auth URL in response:', response.data);
|
||||||
|
throw new Error('No auth URL received from backend');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to get ${provider.toUpperCase()} OAuth URL:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provider-specific OAuth functions (for backward compatibility)
|
||||||
|
export async function connectGitHub(repoUrl?: string, branch?: string): Promise<void> {
|
||||||
|
return connectProvider('github', repoUrl, branch);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function connectGitLab(repoUrl?: string, branch?: string): Promise<void> {
|
||||||
|
return connectProvider('gitlab', repoUrl, branch);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function connectBitbucket(repoUrl?: string, branch?: string): Promise<void> {
|
||||||
|
return connectProvider('bitbucket', repoUrl, branch);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function connectGitea(repoUrl?: string, branch?: string): Promise<void> {
|
||||||
|
return connectProvider('gitea', repoUrl, branch);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repository structure and file content (works for all providers)
|
||||||
|
export interface RepoStructureEntry {
|
||||||
|
name: string
|
||||||
|
type: 'file' | 'directory'
|
||||||
|
size?: number
|
||||||
|
path: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RepoStructureResponse {
|
||||||
|
repository_id: string
|
||||||
|
directory_path: string
|
||||||
|
structure: RepoStructureEntry[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRepositoryStructure(repositoryId: string, path: string = ''): Promise<RepoStructureResponse> {
|
||||||
|
// Try to detect provider from repository ID or use a generic endpoint
|
||||||
|
const url = `/api/github/repository/${encodeURIComponent(repositoryId)}/structure${path ? `?path=${encodeURIComponent(path)}` : ''}`;
|
||||||
|
const res = await authApiClient.get(url);
|
||||||
|
return res.data?.data as RepoStructureResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileContentResponse {
|
||||||
|
file_info: {
|
||||||
|
id: string
|
||||||
|
filename: string
|
||||||
|
file_extension?: string
|
||||||
|
relative_path: string
|
||||||
|
file_size_bytes?: number
|
||||||
|
mime_type?: string
|
||||||
|
is_binary?: boolean
|
||||||
|
language_detected?: string
|
||||||
|
line_count?: number
|
||||||
|
char_count?: number
|
||||||
|
}
|
||||||
|
content: string | null
|
||||||
|
preview?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRepositoryFileContent(repositoryId: string, filePath: string): Promise<FileContentResponse> {
|
||||||
|
const url = `/api/github/repository/${encodeURIComponent(repositoryId)}/file-content?file_path=${encodeURIComponent(filePath)}`;
|
||||||
|
const res = await authApiClient.get(url);
|
||||||
|
return res.data?.data as FileContentResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI Streaming (works for all providers)
|
||||||
|
export async function startAIStreaming(repositoryId: string, options: {
|
||||||
|
fileTypes?: string[]
|
||||||
|
maxSize?: number
|
||||||
|
includeBinary?: boolean
|
||||||
|
chunkSize?: number
|
||||||
|
directoryFilter?: string
|
||||||
|
excludePatterns?: string[]
|
||||||
|
} = {}): Promise<EventSource> {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
if (options.fileTypes) params.append('file_types', options.fileTypes.join(','));
|
||||||
|
if (options.maxSize) params.append('max_size', options.maxSize.toString());
|
||||||
|
if (options.includeBinary) params.append('include_binary', 'true');
|
||||||
|
if (options.chunkSize) params.append('chunk_size', options.chunkSize.toString());
|
||||||
|
if (options.directoryFilter) params.append('directory_filter', options.directoryFilter);
|
||||||
|
if (options.excludePatterns) params.append('exclude_patterns', options.excludePatterns.join(','));
|
||||||
|
|
||||||
|
const url = `/api/ai-stream/repository/${encodeURIComponent(repositoryId)}/ai-stream?${params.toString()}`;
|
||||||
|
return new EventSource(url);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user