 c177363a19
			
		
	
	c177363a19
	
	
	
		
			
			🤖 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-green-50 border border-green-200' 
 | |
|                   : 'bg-red-50 border border-red-200'
 | |
|               }`}>
 | |
|                 {validation.valid ? (
 | |
|                   <CheckCircleIcon className="h-5 w-5 text-green-600 mr-2" />
 | |
|                 ) : (
 | |
|                   <XCircleIcon className="h-5 w-5 text-red-600 mr-2" />
 | |
|                 )}
 | |
|                 <span className={`text-sm ${
 | |
|                   validation.valid ? 'text-green-800' : '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>
 | |
|   )
 | |
| } |