import { Component, inject, signal, OnInit, OnDestroy } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { RouterLink, ActivatedRoute, Router } from '@angular/router'; import { Subscription } from 'rxjs'; import { ApiService } from '../../core/services/api.service'; import { ToastService } from '../../core/services/toast.service'; import { IRegistry, IRegistryCreate } from '../../core/types/api.types'; import { CardComponent, CardHeaderComponent, CardTitleComponent, CardDescriptionComponent, CardContentComponent, } from '../../ui/card/card.component'; import { ButtonComponent } from '../../ui/button/button.component'; import { InputComponent } from '../../ui/input/input.component'; import { LabelComponent } from '../../ui/label/label.component'; import { BadgeComponent } from '../../ui/badge/badge.component'; import { TableComponent, TableHeaderComponent, TableBodyComponent, TableRowComponent, TableHeadComponent, TableCellComponent, } from '../../ui/table/table.component'; import { SkeletonComponent } from '../../ui/skeleton/skeleton.component'; import { DialogComponent, DialogHeaderComponent, DialogTitleComponent, DialogDescriptionComponent, DialogFooterComponent, } from '../../ui/dialog/dialog.component'; import { TabsComponent, TabComponent } from '../../ui/tabs/tabs.component'; type TRegistriesTab = 'onebox' | 'external'; @Component({ selector: 'app-registries', standalone: true, imports: [ FormsModule, RouterLink, CardComponent, CardHeaderComponent, CardTitleComponent, CardDescriptionComponent, CardContentComponent, ButtonComponent, InputComponent, LabelComponent, BadgeComponent, TableComponent, TableHeaderComponent, TableBodyComponent, TableRowComponent, TableHeadComponent, TableCellComponent, SkeletonComponent, DialogComponent, DialogHeaderComponent, DialogTitleComponent, DialogDescriptionComponent, DialogFooterComponent, TabsComponent, TabComponent, ], template: `

Registries

Manage container image registries

Onebox Registry External Registries @switch (activeTab()) { @case ('onebox') {
Onebox Registry (Built-in) Default
Built-in container registry for your services
Status
Running
Registry URL
localhost:3000/v2
Authentication

Quick Start

To push images to the Onebox registry, use a CI or Global token:

# Login to the registry
docker login localhost:3000 -u onebox -p YOUR_TOKEN
# Tag and push your image
docker tag myapp localhost:3000/myservice:latest
docker push localhost:3000/myservice:latest
} @case ('external') {

External Registries

Add credentials for private Docker registries

@if (loading() && registries().length === 0) {
@for (_ of [1,2]; track $index) { }
} @else if (registries().length === 0) {

No external registries

Add credentials for Docker Hub, GitHub Container Registry, or other private registries.

} @else { Registry URL Username Added Actions @for (registry of registries(); track registry.id) { {{ registry.url }} {{ registry.username }} {{ formatDate(registry.createdAt) }} } }
} }
Add External Registry Add credentials for a private Docker registry
Delete Registry Are you sure you want to delete "{{ registryToDelete()?.url }}"? `, }) export class RegistriesComponent implements OnInit, OnDestroy { private api = inject(ApiService); private toast = inject(ToastService); private route = inject(ActivatedRoute); private router = inject(Router); private routeSub?: Subscription; activeTab = signal('onebox'); registries = signal([]); loading = signal(false); addDialogOpen = signal(false); deleteDialogOpen = signal(false); registryToDelete = signal(null); form: IRegistryCreate = { url: '', username: '', password: '' }; setTab(tab: TRegistriesTab): void { this.router.navigate(['/registries', tab]); } ngOnInit(): void { // Subscribe to route params to sync tab state with URL this.routeSub = this.route.paramMap.subscribe((params) => { const tab = params.get('tab') as TRegistriesTab; if (tab && ['onebox', 'external'].includes(tab)) { this.activeTab.set(tab); } }); this.loadRegistries(); } ngOnDestroy(): void { this.routeSub?.unsubscribe(); } async loadRegistries(): Promise { this.loading.set(true); try { const response = await this.api.getRegistries(); if (response.success && response.data) { this.registries.set(response.data); } } catch { this.toast.error('Failed to load registries'); } finally { this.loading.set(false); } } async addRegistry(): Promise { if (!this.form.url || !this.form.username || !this.form.password) { this.toast.error('Please fill in all fields'); return; } this.loading.set(true); try { const response = await this.api.createRegistry(this.form); if (response.success) { this.toast.success('Registry added'); this.form = { url: '', username: '', password: '' }; this.addDialogOpen.set(false); this.loadRegistries(); } else { this.toast.error(response.error || 'Failed to add registry'); } } catch { this.toast.error('Failed to add registry'); } finally { this.loading.set(false); } } confirmDelete(registry: IRegistry): void { this.registryToDelete.set(registry); this.deleteDialogOpen.set(true); } async deleteRegistry(): Promise { const registry = this.registryToDelete(); if (!registry?.id) return; try { const response = await this.api.deleteRegistry(registry.id); if (response.success) { this.toast.success('Registry deleted'); this.loadRegistries(); } else { this.toast.error(response.error || 'Failed to delete registry'); } } catch { this.toast.error('Failed to delete registry'); } finally { this.deleteDialogOpen.set(false); this.registryToDelete.set(null); } } formatDate(timestamp: number): string { return new Date(timestamp).toLocaleDateString(); } }