'use client'; import { useEffect, useState } from 'react'; import { registryApi, instanceApi, clusterApi, valuesTemplateApi, storageApi } from '@/lib/api'; import { Package, Database, ChevronRight, Search, Rocket, X, Loader2 } from 'lucide-react'; interface RegistryDTO { id: string; name: string; url: string; description?: string; username?: string; insecure: boolean; isShared: boolean; createdAt: string; updatedAt: string; } interface CreateInstanceRequest { name: string; namespace: string; registryId: string; repository: string; chart: string; version: string; description?: string; valuesYaml?: string; } interface ClusterDTO { id: string; name: string; host: string; description?: string; } interface ValuesTemplateDTO { id: string; name: string; description?: string; values_yaml: string; version: number; is_default: boolean; chart_reference_id: string; } interface StorageDTO { id: string; name: string; type: string; description?: string; } export default function ChartsPage() { const [registries, setRegistries] = useState([]); const [clusters, setClusters] = useState([]); const [templates, setTemplates] = useState([]); const [storages, setStorages] = useState([]); const [selectedRegistry, setSelectedRegistry] = useState(null); const [repositories, setRepositories] = useState([]); const [selectedRepo, setSelectedRepo] = useState(null); const [artifacts, setArtifacts] = useState([]); const [searchQuery, setSearchQuery] = useState(''); const [isLoading, setIsLoading] = useState(true); const [isLoadingRepos, setIsLoadingRepos] = useState(false); const [isLoadingArtifacts, setIsLoadingArtifacts] = useState(false); const [showDeployModal, setShowDeployModal] = useState(false); const [selectedArtifact, setSelectedArtifact] = useState(null); const [isDeploying, setIsDeploying] = useState(false); const [deployError, setDeployError] = useState(null); const [valuesYamlError, setValuesYamlError] = useState(null); const [deployForm, setDeployForm] = useState({ name: '', namespace: 'default', clusterId: '', description: '', valuesYaml: '', selectedTemplateId: '', selectedStorageId: '', }); const fetchRegistries = async () => { try { const response = await registryApi.list(); const data = response.data; setRegistries(Array.isArray(data) ? data : (data?.registries || [])); } catch (error) { console.error('Failed to fetch registries:', error); } finally { setIsLoading(false); } }; const fetchClusters = async () => { try { const response = await clusterApi.list(); const data = response.data; const clusterList = Array.isArray(data) ? data : (data?.clusters || []); setClusters(clusterList); if (clusterList.length > 0 && !deployForm.clusterId) { setDeployForm(prev => ({ ...prev, clusterId: clusterList[0].id })); } } catch (error) { console.error('Failed to fetch clusters:', error); } }; const fetchTemplates = async () => { try { const response = await valuesTemplateApi.list(); const data = response.data; const templateList = Array.isArray(data) ? data : (data?.templates || []); setTemplates(templateList); } catch (error) { console.error('Failed to fetch templates:', error); } }; const fetchStorages = async () => { try { const response = await storageApi.list(); const data = response.data; const storageList = Array.isArray(data) ? data : (data?.storages || []); setStorages(storageList); } catch (error) { console.error('Failed to fetch storages:', error); } }; const handleTemplateChange = (templateId: string) => { const template = templates.find(t => t.id === templateId); if (template) { setDeployForm(prev => ({ ...prev, selectedTemplateId: templateId, valuesYaml: template.values_yaml })); } }; const handleStorageChange = (storageId: string) => { const storage = storages.find(s => s.id === storageId); if (storage) { // Merge storage config into values using proper persistence format let storageConfig = ''; if (storage.type === 'nfs') { // For NFS, use the storage name as storageClass reference storageConfig = `persistence: enabled: true storageClass: "${storage.name}" existingClaim: "" mountOptions: - hard - nfsvers=4.1 `; } else { storageConfig = `persistence: enabled: true storageClass: "${storage.type}" existingClaim: "" `; } setDeployForm(prev => ({ ...prev, selectedStorageId: storageId, valuesYaml: prev.valuesYaml ? prev.valuesYaml + '\n' + storageConfig : storageConfig })); } }; const fetchRepositories = async (registryId: string) => { setIsLoadingRepos(true); setRepositories([]); setSelectedRepo(null); try { const response = await registryApi.listRepositories(registryId); setRepositories(response.data?.repositories || []); } catch (error) { console.error('Failed to fetch repositories:', error); } finally { setIsLoadingRepos(false); } }; const fetchArtifacts = async (registryId: string, repositoryName: string) => { setIsLoadingArtifacts(true); setArtifacts([]); try { const response = await registryApi.listArtifacts(registryId, repositoryName); // Filter to only show chart type artifacts const allArtifacts = Array.isArray(response.data) ? response.data : []; const chartArtifacts = allArtifacts.filter((a: Artifact) => a.type === 'chart'); // Sort by semver descending so index 0 = latest (most recent version) chartArtifacts.sort((a, b) => { const pa = a.tag.split('.').map(Number); const pb = b.tag.split('.').map(Number); for (let i = 0; i < Math.max(pa.length, pb.length); i++) { const na = pa[i] || 0; const nb = pb[i] || 0; if (na !== nb) return nb - na; } return 0; }); setArtifacts(chartArtifacts); } catch (error) { console.error('Failed to fetch artifacts:', error); } finally { setIsLoadingArtifacts(false); } }; useEffect(() => { fetchRegistries(); fetchClusters(); fetchTemplates(); fetchStorages(); }, []); useEffect(() => { if (selectedRegistry) { fetchRepositories(selectedRegistry.id); } }, [selectedRegistry]); useEffect(() => { if (selectedRegistry && selectedRepo) { fetchArtifacts(selectedRegistry.id, selectedRepo); } }, [selectedRepo]); const filteredRepos = repositories.filter(repo => repo.toLowerCase().includes(searchQuery.toLowerCase()) ); // Only show chart repositories (those starting with "charts/") const chartRepos = filteredRepos.filter(repo => repo.startsWith('charts/') || repo.includes('/charts/')); const handleDeploy = async () => { if (!selectedArtifact || !deployForm.clusterId || !deployForm.name) { alert('Please fill in all required fields'); return; } setIsDeploying(true); setDeployError(null); try { const request: CreateInstanceRequest = { name: deployForm.name, namespace: deployForm.namespace || 'default', repository: selectedRepo!, chart: selectedRepo!.split('/').pop() || selectedRepo!, version: selectedArtifact.tag, registryId: selectedRegistry?.id || '', description: deployForm.description, valuesYaml: deployForm.valuesYaml || undefined, }; await instanceApi.create(deployForm.clusterId, request); alert('Deployment created successfully!'); setShowDeployModal(false); setSelectedArtifact(null); setDeployForm({ name: '', namespace: 'default', clusterId: clusters[0]?.id || '', description: '', valuesYaml: '', selectedTemplateId: '', selectedStorageId: '', }); } catch (error: unknown) { console.error('Failed to deploy:', error); let errorMessage = 'Failed to create deployment'; if (error && typeof error === 'object' && 'response' in error) { const axiosError = error as { response?: { data?: { error?: string; message?: string } } }; errorMessage = axiosError.response?.data?.message || axiosError.response?.data?.error || errorMessage; } setDeployError(errorMessage); } finally { setIsDeploying(false); } }; const openDeployModal = (artifact: Artifact) => { setSelectedArtifact(artifact); setDeployForm({ name: artifact.repositoryName.split('/').pop()!.toLowerCase(), namespace: 'default', clusterId: clusters[0]?.id || '', description: '', valuesYaml: '', selectedTemplateId: '', selectedStorageId: '', }); setShowDeployModal(true); }; if (isLoading) { return (
); } return (
{/* Header */}

Helm Charts

Browse and deploy Helm charts from OCI registries

{/* Registry Selection */}
{registries.map((registry) => ( ))}
{/* Search */} {selectedRegistry && (
setSearchQuery(e.target.value)} className="w-full pl-10 pr-4 py-2 bg-[var(--card)] border border-[var(--border)] rounded-lg text-[var(--foreground)] placeholder:text-[var(--muted-foreground)]" />
)} {/* Breadcrumb */} {selectedRegistry && (
{selectedRegistry.name} {selectedRepo && ( <> {selectedRepo} )}
)} {/* Content */} {!selectedRegistry ? (

Select a registry to browse Helm charts

) : isLoadingRepos ? (
) : !selectedRepo ? ( /* Chart Repositories List */
{chartRepos.length === 0 ? (

{searchQuery ? 'No charts found matching your search' : 'No Helm charts found in this registry'}

{searchQuery && ( )}
) : ( chartRepos.map((repo) => ( )) )}
) : isLoadingArtifacts ? (
) : ( /* Artifact/Version List */

Versions of {selectedRepo.split('/').pop()}

{artifacts.length === 0 ? (

No versions found

) : (
{artifacts.map((artifact, index) => (

{artifact.tag}

{artifact.mediaType}

{index === 0 && ( Latest )}

Size: {formatSize(artifact.size)}

))}
)}
)} {/* Deploy Modal */} {showDeployModal && selectedArtifact && (

Deploy Chart

Deploying

{selectedRepo}:{selectedArtifact.tag}

setDeployForm({ ...deployForm, name: e.target.value })} className="w-full px-3 py-2 bg-[var(--background)] border border-[var(--border)] rounded-lg text-[var(--foreground)]" placeholder="my-release" required />
setDeployForm({ ...deployForm, namespace: e.target.value })} className="w-full px-3 py-2 bg-[var(--background)] border border-[var(--border)] rounded-lg text-[var(--foreground)]" placeholder="default" />
{templates.length > 0 && (
)} {storages.length > 0 && (
)}