ui rebuild

This commit is contained in:
2025-11-24 19:52:35 +00:00
parent c9beae93c8
commit 9aa6906ca5
73 changed files with 8514 additions and 4537 deletions

View File

@@ -1,99 +1,229 @@
import { Component, OnInit, inject, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Component, inject, signal, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ApiService, Registry } from '../../core/services/api.service';
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 {
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';
@Component({
selector: 'app-registries',
standalone: true,
imports: [CommonModule, FormsModule],
imports: [
FormsModule,
CardComponent,
CardHeaderComponent,
CardTitleComponent,
CardDescriptionComponent,
CardContentComponent,
ButtonComponent,
InputComponent,
LabelComponent,
TableComponent,
TableHeaderComponent,
TableBodyComponent,
TableRowComponent,
TableHeadComponent,
TableCellComponent,
SkeletonComponent,
DialogComponent,
DialogHeaderComponent,
DialogTitleComponent,
DialogDescriptionComponent,
DialogFooterComponent,
],
template: `
<div class="px-4 sm:px-0">
<h1 class="text-3xl font-bold text-gray-900 mb-8">Docker Registries</h1>
<!-- Add Registry Form -->
<div class="card mb-8 max-w-2xl">
<h2 class="text-lg font-medium text-gray-900 mb-4">Add Registry</h2>
<form (ngSubmit)="addRegistry()" class="space-y-4">
<div>
<label for="url" class="label">Registry URL</label>
<input type="text" id="url" [(ngModel)]="newRegistry.url" name="url" required placeholder="registry.example.com" class="input" />
</div>
<div>
<label for="username" class="label">Username</label>
<input type="text" id="username" [(ngModel)]="newRegistry.username" name="username" required class="input" />
</div>
<div>
<label for="password" class="label">Password</label>
<input type="password" id="password" [(ngModel)]="newRegistry.password" name="password" required class="input" />
</div>
<button type="submit" class="btn btn-primary">Add Registry</button>
</form>
<div class="space-y-6">
<div>
<h1 class="text-3xl font-bold tracking-tight">Docker Registries</h1>
<p class="text-muted-foreground">Manage Docker registry credentials</p>
</div>
<!-- Add Registry Form -->
<ui-card>
<ui-card-header class="flex flex-col space-y-1.5">
<ui-card-title>Add Registry</ui-card-title>
<ui-card-description>Add credentials for a private Docker registry</ui-card-description>
</ui-card-header>
<ui-card-content>
<form (ngSubmit)="addRegistry()" class="grid gap-4 md:grid-cols-4">
<div class="space-y-2">
<label uiLabel>Registry URL</label>
<input uiInput [(ngModel)]="form.url" name="url" placeholder="registry.example.com" required />
</div>
<div class="space-y-2">
<label uiLabel>Username</label>
<input uiInput [(ngModel)]="form.username" name="username" required />
</div>
<div class="space-y-2">
<label uiLabel>Password</label>
<input uiInput type="password" [(ngModel)]="form.password" name="password" required />
</div>
<div class="flex items-end">
<button uiButton type="submit" [disabled]="loading()">Add Registry</button>
</div>
</form>
</ui-card-content>
</ui-card>
<!-- Registries List -->
@if (registries().length > 0) {
<div class="card overflow-hidden p-0">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">URL</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Username</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Created</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@for (registry of registries(); track registry.id) {
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ registry.url }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ registry.username }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ formatDate(registry.createdAt) }}</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm">
<button (click)="deleteRegistry(registry)" class="text-red-600 hover:text-red-900">Delete</button>
</td>
</tr>
<ui-card>
<ui-card-header class="flex flex-col space-y-1.5">
<ui-card-title>Registered Registries</ui-card-title>
</ui-card-header>
<ui-card-content class="p-0">
@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" />
}
</tbody>
</table>
</div>
}
</div>
} @else if (registries().length === 0) {
<div class="p-12 text-center">
<p class="text-muted-foreground">No registries configured</p>
</div>
} @else {
<ui-table>
<ui-table-header>
<ui-table-row>
<ui-table-head>URL</ui-table-head>
<ui-table-head>Username</ui-table-head>
<ui-table-head>Created</ui-table-head>
<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>
}
</ui-card-content>
</ui-card>
</div>
<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>
`,
})
export class RegistriesComponent implements OnInit {
private apiService = inject(ApiService);
registries = signal<Registry[]>([]);
newRegistry = { url: '', username: '', password: '' };
private api = inject(ApiService);
private toast = inject(ToastService);
registries = signal<IRegistry[]>([]);
loading = signal(false);
deleteDialogOpen = signal(false);
registryToDelete = signal<IRegistry | null>(null);
form: IRegistryCreate = { url: '', username: '', password: '' };
ngOnInit(): void {
this.loadRegistries();
}
loadRegistries(): void {
this.apiService.getRegistries().subscribe({
next: (response) => {
if (response.success && response.data) {
this.registries.set(response.data);
}
},
});
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);
}
}
addRegistry(): void {
this.apiService.createRegistry(this.newRegistry).subscribe({
next: () => {
this.newRegistry = { url: '', username: '', password: '' };
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: '' };
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);
}
}
deleteRegistry(registry: Registry): void {
if (confirm(`Delete registry ${registry.url}?`)) {
this.apiService.deleteRegistry(registry.url).subscribe({
next: () => this.loadRegistries(),
});
confirmDelete(registry: IRegistry): void {
this.registryToDelete.set(registry);
this.deleteDialogOpen.set(true);
}
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);
}
}