2025-11-24 19:52:35 +00:00
|
|
|
import { Component, inject, signal, OnInit } from '@angular/core';
|
2025-11-18 00:03:24 +00:00
|
|
|
import { FormsModule } from '@angular/forms';
|
2025-11-25 04:20:19 +00:00
|
|
|
import { RouterLink } from '@angular/router';
|
2025-11-24 19:52:35 +00:00
|
|
|
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';
|
2025-11-25 04:20:19 +00:00
|
|
|
import { BadgeComponent } from '../../ui/badge/badge.component';
|
2025-11-24 19:52:35 +00:00
|
|
|
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';
|
2025-11-26 13:49:11 +00:00
|
|
|
import { TabsComponent, TabComponent } from '../../ui/tabs/tabs.component';
|
|
|
|
|
|
|
|
|
|
type TRegistriesTab = 'onebox' | 'external';
|
2025-11-18 00:03:24 +00:00
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
|
selector: 'app-registries',
|
|
|
|
|
standalone: true,
|
2025-11-24 19:52:35 +00:00
|
|
|
imports: [
|
|
|
|
|
FormsModule,
|
2025-11-25 04:20:19 +00:00
|
|
|
RouterLink,
|
2025-11-24 19:52:35 +00:00
|
|
|
CardComponent,
|
|
|
|
|
CardHeaderComponent,
|
|
|
|
|
CardTitleComponent,
|
|
|
|
|
CardDescriptionComponent,
|
|
|
|
|
CardContentComponent,
|
|
|
|
|
ButtonComponent,
|
|
|
|
|
InputComponent,
|
|
|
|
|
LabelComponent,
|
2025-11-25 04:20:19 +00:00
|
|
|
BadgeComponent,
|
2025-11-24 19:52:35 +00:00
|
|
|
TableComponent,
|
|
|
|
|
TableHeaderComponent,
|
|
|
|
|
TableBodyComponent,
|
|
|
|
|
TableRowComponent,
|
|
|
|
|
TableHeadComponent,
|
|
|
|
|
TableCellComponent,
|
|
|
|
|
SkeletonComponent,
|
|
|
|
|
DialogComponent,
|
|
|
|
|
DialogHeaderComponent,
|
|
|
|
|
DialogTitleComponent,
|
|
|
|
|
DialogDescriptionComponent,
|
|
|
|
|
DialogFooterComponent,
|
2025-11-26 13:49:11 +00:00
|
|
|
TabsComponent,
|
|
|
|
|
TabComponent,
|
2025-11-24 19:52:35 +00:00
|
|
|
],
|
2025-11-18 00:03:24 +00:00
|
|
|
template: `
|
2025-11-24 19:52:35 +00:00
|
|
|
<div class="space-y-6">
|
|
|
|
|
<div>
|
2025-11-25 04:20:19 +00:00
|
|
|
<h1 class="text-3xl font-bold tracking-tight">Registries</h1>
|
|
|
|
|
<p class="text-muted-foreground">Manage container image registries</p>
|
2025-11-24 19:52:35 +00:00
|
|
|
</div>
|
2025-11-18 00:03:24 +00:00
|
|
|
|
2025-11-26 13:49:11 +00:00
|
|
|
<!-- Tabs -->
|
|
|
|
|
<ui-tabs class="block">
|
|
|
|
|
<ui-tab [active]="activeTab() === 'onebox'" (tabClick)="setTab('onebox')">Onebox Registry</ui-tab>
|
|
|
|
|
<ui-tab [active]="activeTab() === 'external'" (tabClick)="setTab('external')">External Registries</ui-tab>
|
|
|
|
|
</ui-tabs>
|
|
|
|
|
|
|
|
|
|
<!-- Tab Content -->
|
|
|
|
|
@switch (activeTab()) {
|
|
|
|
|
@case ('onebox') {
|
|
|
|
|
<!-- Onebox Registry Card -->
|
|
|
|
|
<ui-card class="border-primary/50">
|
2025-11-25 04:20:19 +00:00
|
|
|
<ui-card-header class="flex flex-row items-start justify-between space-y-0">
|
|
|
|
|
<div class="space-y-1">
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
<svg class="h-5 w-5 text-primary" viewBox="0 0 24 24" fill="currentColor">
|
|
|
|
|
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
<ui-card-title>Onebox Registry (Built-in)</ui-card-title>
|
|
|
|
|
<ui-badge>Default</ui-badge>
|
|
|
|
|
</div>
|
|
|
|
|
<ui-card-description>Built-in container registry for your services</ui-card-description>
|
|
|
|
|
</div>
|
2025-11-24 19:52:35 +00:00
|
|
|
</ui-card-header>
|
|
|
|
|
<ui-card-content>
|
2025-11-25 04:20:19 +00:00
|
|
|
<div class="grid gap-6 md:grid-cols-3">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="text-sm font-medium text-muted-foreground">Status</div>
|
|
|
|
|
<div class="flex items-center gap-2 mt-1">
|
|
|
|
|
<span class="h-2 w-2 rounded-full bg-success animate-pulse"></span>
|
|
|
|
|
<span class="font-medium text-success">Running</span>
|
|
|
|
|
</div>
|
2025-11-24 19:52:35 +00:00
|
|
|
</div>
|
2025-11-25 04:20:19 +00:00
|
|
|
<div>
|
|
|
|
|
<div class="text-sm font-medium text-muted-foreground">Registry URL</div>
|
|
|
|
|
<div class="font-mono text-sm mt-1">localhost:3000/v2</div>
|
2025-11-24 19:52:35 +00:00
|
|
|
</div>
|
2025-11-25 04:20:19 +00:00
|
|
|
<div>
|
|
|
|
|
<div class="text-sm font-medium text-muted-foreground">Authentication</div>
|
|
|
|
|
<div class="mt-1">
|
|
|
|
|
<a routerLink="/tokens" class="text-primary hover:underline text-sm">
|
|
|
|
|
Manage Tokens
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
2025-11-24 19:52:35 +00:00
|
|
|
</div>
|
2025-11-25 04:20:19 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="mt-6 p-4 bg-muted rounded-lg">
|
|
|
|
|
<h4 class="font-medium mb-2">Quick Start</h4>
|
|
|
|
|
<p class="text-sm text-muted-foreground mb-3">
|
|
|
|
|
To push images to the Onebox registry, use a CI or Global token:
|
|
|
|
|
</p>
|
|
|
|
|
<div class="font-mono text-xs bg-background p-3 rounded border overflow-x-auto">
|
|
|
|
|
<div class="text-muted-foreground"># Login to the registry</div>
|
|
|
|
|
<div>docker login localhost:3000 -u onebox -p YOUR_TOKEN</div>
|
|
|
|
|
<div class="mt-2 text-muted-foreground"># Tag and push your image</div>
|
|
|
|
|
<div>docker tag myapp localhost:3000/myservice:latest</div>
|
|
|
|
|
<div>docker push localhost:3000/myservice:latest</div>
|
2025-11-24 19:52:35 +00:00
|
|
|
</div>
|
2025-11-25 04:20:19 +00:00
|
|
|
</div>
|
2025-11-24 19:52:35 +00:00
|
|
|
</ui-card-content>
|
2025-11-26 13:49:11 +00:00
|
|
|
</ui-card>
|
|
|
|
|
}
|
|
|
|
|
@case ('external') {
|
|
|
|
|
<!-- External Registries Section -->
|
|
|
|
|
<div class="flex items-center justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<h2 class="text-xl font-semibold">External Registries</h2>
|
|
|
|
|
<p class="text-sm text-muted-foreground">Add credentials for private Docker registries</p>
|
|
|
|
|
</div>
|
|
|
|
|
<button uiButton variant="outline" (click)="addDialogOpen.set(true)">
|
|
|
|
|
Add Registry
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
2025-11-25 04:20:19 +00:00
|
|
|
|
2025-11-26 13:49:11 +00:00
|
|
|
<ui-card>
|
|
|
|
|
<ui-card-content class="p-0">
|
2025-11-24 19:52:35 +00:00
|
|
|
@if (loading() && registries().length === 0) {
|
|
|
|
|
<div class="p-6 space-y-4">
|
|
|
|
|
@for (_ of [1,2]; track $index) {
|
|
|
|
|
<ui-skeleton class="h-12 w-full" />
|
2025-11-18 00:03:24 +00:00
|
|
|
}
|
2025-11-24 19:52:35 +00:00
|
|
|
</div>
|
|
|
|
|
} @else if (registries().length === 0) {
|
|
|
|
|
<div class="p-12 text-center">
|
2025-11-25 04:20:19 +00:00
|
|
|
<svg class="h-12 w-12 mx-auto text-muted-foreground/50 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
|
|
|
|
</svg>
|
|
|
|
|
<h3 class="text-lg font-medium">No external registries</h3>
|
|
|
|
|
<p class="text-muted-foreground mt-1">
|
|
|
|
|
Add credentials for Docker Hub, GitHub Container Registry, or other private registries.
|
|
|
|
|
</p>
|
|
|
|
|
<button uiButton variant="outline" class="mt-4" (click)="addDialogOpen.set(true)">
|
|
|
|
|
Add External Registry
|
|
|
|
|
</button>
|
2025-11-24 19:52:35 +00:00
|
|
|
</div>
|
|
|
|
|
} @else {
|
|
|
|
|
<ui-table>
|
|
|
|
|
<ui-table-header>
|
|
|
|
|
<ui-table-row>
|
2025-11-25 04:20:19 +00:00
|
|
|
<ui-table-head>Registry URL</ui-table-head>
|
2025-11-24 19:52:35 +00:00
|
|
|
<ui-table-head>Username</ui-table-head>
|
2025-11-25 04:20:19 +00:00
|
|
|
<ui-table-head>Added</ui-table-head>
|
2025-11-24 19:52:35 +00:00
|
|
|
<ui-table-head class="text-right">Actions</ui-table-head>
|
|
|
|
|
</ui-table-row>
|
|
|
|
|
</ui-table-header>
|
|
|
|
|
<ui-table-body>
|
|
|
|
|
@for (registry of registries(); track registry.id) {
|
|
|
|
|
<ui-table-row>
|
|
|
|
|
<ui-table-cell class="font-medium">{{ registry.url }}</ui-table-cell>
|
|
|
|
|
<ui-table-cell>{{ registry.username }}</ui-table-cell>
|
|
|
|
|
<ui-table-cell>{{ formatDate(registry.createdAt) }}</ui-table-cell>
|
|
|
|
|
<ui-table-cell class="text-right">
|
|
|
|
|
<button uiButton variant="destructive" size="sm" (click)="confirmDelete(registry)">
|
|
|
|
|
Delete
|
|
|
|
|
</button>
|
|
|
|
|
</ui-table-cell>
|
|
|
|
|
</ui-table-row>
|
|
|
|
|
}
|
|
|
|
|
</ui-table-body>
|
|
|
|
|
</ui-table>
|
|
|
|
|
}
|
2025-11-26 13:49:11 +00:00
|
|
|
</ui-card-content>
|
|
|
|
|
</ui-card>
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-18 00:03:24 +00:00
|
|
|
</div>
|
2025-11-24 19:52:35 +00:00
|
|
|
|
2025-11-25 04:20:19 +00:00
|
|
|
<!-- Add Registry Dialog -->
|
|
|
|
|
<ui-dialog [open]="addDialogOpen()" (openChange)="addDialogOpen.set($event)">
|
|
|
|
|
<ui-dialog-header>
|
|
|
|
|
<ui-dialog-title>Add External Registry</ui-dialog-title>
|
|
|
|
|
<ui-dialog-description>
|
|
|
|
|
Add credentials for a private Docker registry
|
|
|
|
|
</ui-dialog-description>
|
|
|
|
|
</ui-dialog-header>
|
|
|
|
|
<div class="grid gap-4 py-4">
|
|
|
|
|
<div class="space-y-2">
|
|
|
|
|
<label uiLabel>Registry URL</label>
|
|
|
|
|
<input uiInput [(ngModel)]="form.url" placeholder="registry.example.com" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="space-y-2">
|
|
|
|
|
<label uiLabel>Username</label>
|
|
|
|
|
<input uiInput [(ngModel)]="form.username" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="space-y-2">
|
|
|
|
|
<label uiLabel>Password</label>
|
|
|
|
|
<input uiInput type="password" [(ngModel)]="form.password" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<ui-dialog-footer>
|
|
|
|
|
<button uiButton variant="outline" (click)="addDialogOpen.set(false)">Cancel</button>
|
|
|
|
|
<button uiButton (click)="addRegistry()" [disabled]="loading()">Add Registry</button>
|
|
|
|
|
</ui-dialog-footer>
|
|
|
|
|
</ui-dialog>
|
|
|
|
|
|
|
|
|
|
<!-- Delete Confirmation Dialog -->
|
2025-11-24 19:52:35 +00:00
|
|
|
<ui-dialog [open]="deleteDialogOpen()" (openChange)="deleteDialogOpen.set($event)">
|
|
|
|
|
<ui-dialog-header>
|
|
|
|
|
<ui-dialog-title>Delete Registry</ui-dialog-title>
|
|
|
|
|
<ui-dialog-description>
|
|
|
|
|
Are you sure you want to delete "{{ registryToDelete()?.url }}"?
|
|
|
|
|
</ui-dialog-description>
|
|
|
|
|
</ui-dialog-header>
|
|
|
|
|
<ui-dialog-footer>
|
|
|
|
|
<button uiButton variant="outline" (click)="deleteDialogOpen.set(false)">Cancel</button>
|
|
|
|
|
<button uiButton variant="destructive" (click)="deleteRegistry()">Delete</button>
|
|
|
|
|
</ui-dialog-footer>
|
|
|
|
|
</ui-dialog>
|
2025-11-18 00:03:24 +00:00
|
|
|
`,
|
|
|
|
|
})
|
|
|
|
|
export class RegistriesComponent implements OnInit {
|
2025-11-24 19:52:35 +00:00
|
|
|
private api = inject(ApiService);
|
|
|
|
|
private toast = inject(ToastService);
|
|
|
|
|
|
2025-11-26 13:49:11 +00:00
|
|
|
activeTab = signal<TRegistriesTab>('onebox');
|
2025-11-24 19:52:35 +00:00
|
|
|
registries = signal<IRegistry[]>([]);
|
|
|
|
|
loading = signal(false);
|
2025-11-25 04:20:19 +00:00
|
|
|
addDialogOpen = signal(false);
|
2025-11-24 19:52:35 +00:00
|
|
|
deleteDialogOpen = signal(false);
|
|
|
|
|
registryToDelete = signal<IRegistry | null>(null);
|
|
|
|
|
|
|
|
|
|
form: IRegistryCreate = { url: '', username: '', password: '' };
|
2025-11-18 00:03:24 +00:00
|
|
|
|
2025-11-26 13:49:11 +00:00
|
|
|
setTab(tab: TRegistriesTab): void {
|
|
|
|
|
this.activeTab.set(tab);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-18 00:03:24 +00:00
|
|
|
ngOnInit(): void {
|
|
|
|
|
this.loadRegistries();
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-24 19:52:35 +00:00
|
|
|
async loadRegistries(): Promise<void> {
|
|
|
|
|
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);
|
|
|
|
|
}
|
2025-11-18 00:03:24 +00:00
|
|
|
}
|
|
|
|
|
|
2025-11-24 19:52:35 +00:00
|
|
|
async addRegistry(): Promise<void> {
|
|
|
|
|
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: '' };
|
2025-11-25 04:20:19 +00:00
|
|
|
this.addDialogOpen.set(false);
|
2025-11-18 00:03:24 +00:00
|
|
|
this.loadRegistries();
|
2025-11-24 19:52:35 +00:00
|
|
|
} 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);
|
2025-11-18 00:03:24 +00:00
|
|
|
}
|
|
|
|
|
|
2025-11-24 19:52:35 +00:00
|
|
|
async deleteRegistry(): Promise<void> {
|
|
|
|
|
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);
|
2025-11-18 00:03:24 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
formatDate(timestamp: number): string {
|
|
|
|
|
return new Date(timestamp).toLocaleDateString();
|
|
|
|
|
}
|
|
|
|
|
}
|