- Updated configuration and deployment files - Improved system architecture and components - Enhanced documentation and testing - Fixed various issues and added new features 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
414 lines
13 KiB
TypeScript
414 lines
13 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import {
|
|
CodeBracketIcon,
|
|
CheckCircleIcon,
|
|
XCircleIcon,
|
|
ArrowPathIcon,
|
|
ExclamationTriangleIcon,
|
|
EyeIcon,
|
|
EyeSlashIcon
|
|
} from '@heroicons/react/24/outline'
|
|
|
|
interface RepositoryProvider {
|
|
name: string
|
|
displayName: string
|
|
description: string
|
|
requiresBaseURL: boolean
|
|
defaultBaseURL?: string
|
|
}
|
|
|
|
interface RepositoryConfig {
|
|
provider: string
|
|
baseURL: string
|
|
accessToken: string
|
|
owner: string
|
|
repository: string
|
|
}
|
|
|
|
interface ValidationResult {
|
|
valid: boolean
|
|
message?: string
|
|
error?: string
|
|
}
|
|
|
|
interface RepositoryConfigurationProps {
|
|
systemInfo: any
|
|
configData: any
|
|
onComplete: (data: any) => void
|
|
onBack?: () => void
|
|
isCompleted: boolean
|
|
}
|
|
|
|
export default function RepositoryConfiguration({
|
|
systemInfo,
|
|
configData,
|
|
onComplete,
|
|
onBack,
|
|
isCompleted
|
|
}: RepositoryConfigurationProps) {
|
|
const [providers, setProviders] = useState<RepositoryProvider[]>([])
|
|
const [config, setConfig] = useState<RepositoryConfig>({
|
|
provider: '',
|
|
baseURL: '',
|
|
accessToken: '',
|
|
owner: '',
|
|
repository: ''
|
|
})
|
|
const [validation, setValidation] = useState<ValidationResult | null>(null)
|
|
const [validating, setValidating] = useState(false)
|
|
const [showToken, setShowToken] = useState(false)
|
|
const [loadingProviders, setLoadingProviders] = useState(true)
|
|
|
|
// Load existing config from configData if available
|
|
useEffect(() => {
|
|
if (configData.repository) {
|
|
setConfig({ ...configData.repository })
|
|
}
|
|
}, [configData])
|
|
|
|
// Load supported providers
|
|
useEffect(() => {
|
|
loadProviders()
|
|
}, [])
|
|
|
|
const loadProviders = async () => {
|
|
try {
|
|
const response = await fetch('/api/setup/repository/providers')
|
|
if (response.ok) {
|
|
const result = await response.json()
|
|
const providerList = result.providers || []
|
|
|
|
// Map provider names to full provider objects
|
|
const providersData: RepositoryProvider[] = providerList.map((name: string) => {
|
|
switch (name.toLowerCase()) {
|
|
case 'gitea':
|
|
return {
|
|
name: 'gitea',
|
|
displayName: 'Gitea',
|
|
description: 'Self-hosted Git service with issue tracking',
|
|
requiresBaseURL: true,
|
|
defaultBaseURL: 'http://gitea.local'
|
|
}
|
|
case 'github':
|
|
return {
|
|
name: 'github',
|
|
displayName: 'GitHub',
|
|
description: 'Cloud-based Git repository hosting service',
|
|
requiresBaseURL: false,
|
|
defaultBaseURL: 'https://api.github.com'
|
|
}
|
|
default:
|
|
return {
|
|
name: name.toLowerCase(),
|
|
displayName: name,
|
|
description: 'Git repository service',
|
|
requiresBaseURL: true
|
|
}
|
|
}
|
|
})
|
|
|
|
setProviders(providersData)
|
|
|
|
// Set default provider if none selected
|
|
if (!config.provider && providersData.length > 0) {
|
|
const defaultProvider = providersData.find(p => p.name === 'gitea') || providersData[0]
|
|
handleProviderChange(defaultProvider.name)
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load providers:', error)
|
|
} finally {
|
|
setLoadingProviders(false)
|
|
}
|
|
}
|
|
|
|
const handleProviderChange = (provider: string) => {
|
|
const providerData = providers.find(p => p.name === provider)
|
|
setConfig(prev => ({
|
|
...prev,
|
|
provider,
|
|
baseURL: providerData?.defaultBaseURL || prev.baseURL
|
|
}))
|
|
setValidation(null)
|
|
}
|
|
|
|
const handleInputChange = (field: keyof RepositoryConfig, value: string) => {
|
|
setConfig(prev => ({ ...prev, [field]: value }))
|
|
setValidation(null)
|
|
}
|
|
|
|
const validateRepository = async () => {
|
|
if (!config.provider || !config.accessToken || !config.owner || !config.repository) {
|
|
setValidation({
|
|
valid: false,
|
|
error: 'Please fill in all required fields'
|
|
})
|
|
return
|
|
}
|
|
|
|
setValidating(true)
|
|
setValidation(null)
|
|
|
|
try {
|
|
const response = await fetch('/api/setup/repository/validate', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(config)
|
|
})
|
|
|
|
const result = await response.json()
|
|
|
|
if (response.ok && result.valid) {
|
|
setValidation({
|
|
valid: true,
|
|
message: result.message || 'Repository connection successful'
|
|
})
|
|
} else {
|
|
setValidation({
|
|
valid: false,
|
|
error: result.error || 'Validation failed'
|
|
})
|
|
}
|
|
} catch (error) {
|
|
setValidation({
|
|
valid: false,
|
|
error: 'Network error: Unable to validate repository'
|
|
})
|
|
} finally {
|
|
setValidating(false)
|
|
}
|
|
}
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
|
|
if (validation?.valid) {
|
|
onComplete({ repository: config })
|
|
} else {
|
|
validateRepository()
|
|
}
|
|
}
|
|
|
|
const selectedProvider = providers.find(p => p.name === config.provider)
|
|
const isFormValid = config.provider && config.accessToken && config.owner && config.repository &&
|
|
(!selectedProvider?.requiresBaseURL || config.baseURL)
|
|
|
|
if (loadingProviders) {
|
|
return (
|
|
<div className="flex items-center justify-center py-12">
|
|
<div className="text-center">
|
|
<ArrowPathIcon className="h-8 w-8 text-bzzz-primary animate-spin mx-auto mb-4" />
|
|
<p className="text-gray-600">Loading repository providers...</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
{/* Repository Provider Selection */}
|
|
<div className="bg-gray-50 rounded-lg p-6">
|
|
<h3 className="text-lg font-medium text-gray-900 mb-4 flex items-center">
|
|
<CodeBracketIcon className="h-6 w-6 text-bzzz-primary mr-2" />
|
|
Repository Provider
|
|
</h3>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{providers.map((provider) => (
|
|
<div
|
|
key={provider.name}
|
|
className={`border-2 rounded-lg p-4 cursor-pointer transition-all ${
|
|
config.provider === provider.name
|
|
? 'border-bzzz-primary bg-bzzz-primary bg-opacity-10'
|
|
: 'border-gray-200 hover:border-gray-300'
|
|
}`}
|
|
onClick={() => handleProviderChange(provider.name)}
|
|
>
|
|
<div className="flex items-center">
|
|
<input
|
|
type="radio"
|
|
name="provider"
|
|
value={provider.name}
|
|
checked={config.provider === provider.name}
|
|
onChange={() => handleProviderChange(provider.name)}
|
|
className="h-4 w-4 text-bzzz-primary focus:ring-bzzz-primary border-gray-300"
|
|
/>
|
|
<div className="ml-3">
|
|
<div className="font-medium text-gray-900">{provider.displayName}</div>
|
|
<div className="text-sm text-gray-600">{provider.description}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Configuration Form */}
|
|
{config.provider && (
|
|
<div className="space-y-6">
|
|
{/* Base URL (for providers that require it) */}
|
|
{selectedProvider?.requiresBaseURL && (
|
|
<div>
|
|
<label className="label">
|
|
Base URL *
|
|
</label>
|
|
<input
|
|
type="url"
|
|
value={config.baseURL}
|
|
onChange={(e) => handleInputChange('baseURL', e.target.value)}
|
|
placeholder={`e.g., ${selectedProvider.defaultBaseURL || 'https://git.example.com'}`}
|
|
className="input-field"
|
|
required
|
|
/>
|
|
<p className="text-sm text-gray-600 mt-1">
|
|
The base URL for your {selectedProvider.displayName} instance
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Access Token */}
|
|
<div>
|
|
<label className="label">
|
|
Access Token *
|
|
</label>
|
|
<div className="relative">
|
|
<input
|
|
type={showToken ? 'text' : 'password'}
|
|
value={config.accessToken}
|
|
onChange={(e) => handleInputChange('accessToken', e.target.value)}
|
|
placeholder={`Your ${selectedProvider?.displayName} access token`}
|
|
className="input-field pr-10"
|
|
required
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowToken(!showToken)}
|
|
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
|
>
|
|
{showToken ? (
|
|
<EyeSlashIcon className="h-5 w-5 text-gray-400" />
|
|
) : (
|
|
<EyeIcon className="h-5 w-5 text-gray-400" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
<p className="text-sm text-gray-600 mt-1">
|
|
{selectedProvider?.name === 'github'
|
|
? 'Generate a personal access token with repo and admin:repo_hook permissions'
|
|
: 'Generate an access token with repository read/write permissions'
|
|
}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Owner/Organization */}
|
|
<div>
|
|
<label className="label">
|
|
Owner/Organization *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={config.owner}
|
|
onChange={(e) => handleInputChange('owner', e.target.value)}
|
|
placeholder="username or organization"
|
|
className="input-field"
|
|
required
|
|
/>
|
|
<p className="text-sm text-gray-600 mt-1">
|
|
The username or organization that owns the repository
|
|
</p>
|
|
</div>
|
|
|
|
{/* Repository Name */}
|
|
<div>
|
|
<label className="label">
|
|
Repository Name *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={config.repository}
|
|
onChange={(e) => handleInputChange('repository', e.target.value)}
|
|
placeholder="repository-name"
|
|
className="input-field"
|
|
required
|
|
/>
|
|
<p className="text-sm text-gray-600 mt-1">
|
|
The name of the repository for task management
|
|
</p>
|
|
</div>
|
|
|
|
{/* Validation Section */}
|
|
<div className="bg-white border border-gray-200 rounded-lg p-6">
|
|
<h4 className="text-md font-medium text-gray-900 mb-3">Connection Test</h4>
|
|
|
|
{validation && (
|
|
<div className={`flex items-center p-3 rounded-lg mb-4 ${
|
|
validation.valid
|
|
? 'bg-eucalyptus-50 border border-eucalyptus-950'
|
|
: 'bg-red-50 border border-red-200'
|
|
}`}>
|
|
{validation.valid ? (
|
|
<CheckCircleIcon className="h-5 w-5 text-eucalyptus-600 mr-2" />
|
|
) : (
|
|
<XCircleIcon className="h-5 w-5 text-red-600 mr-2" />
|
|
)}
|
|
<span className={`text-sm ${
|
|
validation.valid ? 'text-eucalyptus-600' : 'text-red-800'
|
|
}`}>
|
|
{validation.valid ? validation.message : validation.error}
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
<button
|
|
type="button"
|
|
onClick={validateRepository}
|
|
disabled={!isFormValid || validating}
|
|
className="btn-outline w-full sm:w-auto"
|
|
>
|
|
{validating ? (
|
|
<>
|
|
<ArrowPathIcon className="h-4 w-4 animate-spin mr-2" />
|
|
Testing Connection...
|
|
</>
|
|
) : (
|
|
'Test Repository Connection'
|
|
)}
|
|
</button>
|
|
|
|
{!isFormValid && (
|
|
<p className="text-sm text-gray-600 mt-2">
|
|
Please fill in all required fields to test the connection
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Action Buttons */}
|
|
<div className="flex justify-between pt-6 border-t border-gray-200">
|
|
<div>
|
|
{onBack && (
|
|
<button type="button" onClick={onBack} className="btn-outline">
|
|
Back
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={!validation?.valid}
|
|
className="btn-primary"
|
|
>
|
|
{validation?.valid
|
|
? (isCompleted ? 'Continue' : 'Next: Network Configuration')
|
|
: 'Validate & Continue'
|
|
}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
)
|
|
} |