Files
bzzz/install/config-ui/app/setup/components/RepositoryConfiguration.tsx
anthonyrawlins be761cfe20 Enhance deployment system with retry functionality and improved UX
Major Improvements:
- Added retry deployment buttons in machine list for failed deployments
- Added retry button in SSH console modal footer for enhanced UX
- Enhanced deployment process with comprehensive cleanup of existing services
- Improved binary installation with password-based sudo authentication
- Updated configuration generation to include all required sections (agent, ai, network, security)
- Fixed deployment verification and error handling

Security Enhancements:
- Enhanced verifiedStopExistingServices with thorough cleanup process
- Improved binary copying with proper sudo authentication
- Added comprehensive configuration validation

UX Improvements:
- Users can retry deployments without re-running machine discovery
- Retry buttons available from both machine list and console modal
- Real-time deployment progress with detailed console output
- Clear error states with actionable retry options

Technical Changes:
- Modified ServiceDeployment.tsx with retry button components
- Enhanced api/setup_manager.go with improved deployment functions
- Updated main.go with command line argument support (--config, --setup)
- Added comprehensive zero-trust security validation system

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

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

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>
)
}