Files
registry/ui/src/app/features/organizations/organization-detail.component.ts
Juergen Kunz ab88ac896f feat: implement account settings and API tokens management
- Added SettingsComponent for user profile management, including display name and password change functionality.
- Introduced TokensComponent for managing API tokens, including creation and revocation.
- Created LayoutComponent for consistent application layout with navigation and user information.
- Established main application structure in index.html and main.ts.
- Integrated Tailwind CSS for styling and responsive design.
- Configured TypeScript settings for strict type checking and module resolution.
2025-11-27 22:15:38 +00:00

152 lines
6.5 KiB
TypeScript

import { Component, inject, signal, OnInit } from '@angular/core';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { ApiService, type IOrganization, type IRepository } from '../../core/services/api.service';
import { ToastService } from '../../core/services/toast.service';
@Component({
selector: 'app-organization-detail',
standalone: true,
imports: [RouterLink],
template: `
<div class="p-6 max-w-7xl mx-auto">
@if (loading()) {
<div class="flex items-center justify-center py-12">
<svg class="animate-spin h-8 w-8 text-primary-600" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
</div>
} @else if (organization()) {
<!-- Header -->
<div class="flex items-start justify-between mb-8">
<div class="flex items-center gap-4">
<div class="w-16 h-16 bg-gray-200 dark:bg-gray-700 rounded-xl flex items-center justify-center">
<span class="text-2xl font-medium text-gray-600 dark:text-gray-300">
{{ organization()!.name.charAt(0).toUpperCase() }}
</span>
</div>
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">{{ organization()!.displayName }}</h1>
<p class="text-gray-500 dark:text-gray-400">&#64;{{ organization()!.name }}</p>
</div>
</div>
<div class="flex items-center gap-3">
@if (organization()!.isPublic) {
<span class="badge-default">Public</span>
} @else {
<span class="badge-warning">Private</span>
}
</div>
</div>
@if (organization()!.description) {
<p class="text-gray-600 dark:text-gray-400 mb-8">{{ organization()!.description }}</p>
}
<!-- Repositories Section -->
<div class="mb-8">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Repositories</h2>
<button class="btn-primary btn-sm">
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
New Repository
</button>
</div>
@if (repositories().length === 0) {
<div class="card card-content text-center py-8">
<p class="text-gray-500 dark:text-gray-400">No repositories yet</p>
</div>
} @else {
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
@for (repo of repositories(); track repo.id) {
<a [routerLink]="['repositories', repo.id]" class="card card-content hover:border-primary-300 dark:hover:border-primary-700 transition-colors">
<div class="flex items-start justify-between">
<div>
<h3 class="font-medium text-gray-900 dark:text-gray-100">{{ repo.displayName }}</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">{{ repo.name }}</p>
</div>
@if (repo.isPublic) {
<span class="badge-default">Public</span>
}
</div>
@if (repo.description) {
<p class="text-sm text-gray-600 dark:text-gray-400 mt-2 line-clamp-2">{{ repo.description }}</p>
}
<div class="mt-3 flex items-center gap-4">
<div class="flex items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
{{ repo.packageCount }} packages
</div>
<div class="flex gap-1">
@for (protocol of repo.protocols; track protocol) {
<span class="badge-primary text-xs">{{ protocol }}</span>
}
</div>
</div>
</a>
}
</div>
}
</div>
<!-- Stats -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="card card-content">
<p class="text-sm text-gray-500 dark:text-gray-400">Members</p>
<p class="text-2xl font-bold text-gray-900 dark:text-gray-100">{{ organization()!.memberCount }}</p>
</div>
<div class="card card-content">
<p class="text-sm text-gray-500 dark:text-gray-400">Repositories</p>
<p class="text-2xl font-bold text-gray-900 dark:text-gray-100">{{ repositories().length }}</p>
</div>
<div class="card card-content">
<p class="text-sm text-gray-500 dark:text-gray-400">Created</p>
<p class="text-2xl font-bold text-gray-900 dark:text-gray-100">{{ formatDate(organization()!.createdAt) }}</p>
</div>
</div>
}
</div>
`,
})
export class OrganizationDetailComponent implements OnInit {
private route = inject(ActivatedRoute);
private apiService = inject(ApiService);
private toastService = inject(ToastService);
organization = signal<IOrganization | null>(null);
repositories = signal<IRepository[]>([]);
loading = signal(true);
ngOnInit(): void {
const orgId = this.route.snapshot.paramMap.get('orgId');
if (orgId) {
this.loadData(orgId);
}
}
private async loadData(orgId: string): Promise<void> {
this.loading.set(true);
try {
const [org, reposResponse] = await Promise.all([
this.apiService.getOrganization(orgId).toPromise(),
this.apiService.getRepositories(orgId).toPromise(),
]);
this.organization.set(org || null);
this.repositories.set(reposResponse?.repositories || []);
} catch (error) {
this.toastService.error('Failed to load organization');
} finally {
this.loading.set(false);
}
}
formatDate(dateStr: string): string {
return new Date(dateStr).toLocaleDateString();
}
}