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
Registry URL
localhost:3000/v2
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();
}
}