CFTunnels/frontend/src/app/features/dashboard/tunnel-list/tunnel-list.component.ts
hitanshu310 523c9d941e Add Angular frontend with Authentik OIDC authentication
- Angular 17 with standalone components
- Angular Material + Tailwind CSS
- OIDC authorization code flow with Authentik
- Role-based access control (USER, DEVELOPER, APPROVER, ADMIN)
- Dashboard with pending requests, tunnel list, and create mapping
- Nginx reverse proxy to backend API
- Multi-container Docker Compose setup (frontend, backend, postgres)
- Environment-based configuration (local, test, prod)
2026-02-16 01:47:04 +05:30

183 lines
4.9 KiB
TypeScript

import { Component, Input, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { Tunnel, Mapping } from '../../../shared/models';
import { ApiService } from '../../../core/services/api.service';
@Component({
selector: 'app-tunnel-list',
standalone: true,
imports: [
CommonModule,
MatCardModule,
MatButtonModule,
MatIconModule,
MatExpansionModule,
MatProgressSpinnerModule,
],
template: `
<mat-card class="tunnels-card">
<mat-card-header>
<mat-icon mat-card-avatar>dns</mat-icon>
<mat-card-title>Tunnels</mat-card-title>
<mat-card-subtitle
>{{ tunnels.length }} tunnel(s) configured</mat-card-subtitle
>
</mat-card-header>
<mat-card-content>
@if (isLoading) {
<div class="loading-container">
<mat-spinner diameter="30"></mat-spinner>
</div>
}
@if (!isLoading && tunnels.length === 0) {
<div class="empty-state">
<mat-icon>info</mat-icon>
<p>No tunnels configured</p>
</div>
}
<mat-accordion>
@for (tunnel of tunnels; track tunnel.id) {
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
<mat-icon class="tunnel-icon">hub</mat-icon>
{{ tunnel.name }}
</mat-panel-title>
<mat-panel-description>
{{ tunnel.environment }}
</mat-panel-description>
</mat-expansion-panel-header>
@if (expandedTunnelId === tunnel.id) {
@if (mappingsLoading) {
<mat-spinner diameter="20"></mat-spinner>
} @else if (tunnelMappings[tunnel.id]?.length === 0) {
<p class="no-mappings">No mappings for this tunnel</p>
} @else {
@for (mapping of tunnelMappings[tunnel.id]; track mapping.id) {
<div class="mapping-item">
<span class="hostname"
>{{ mapping.subdomain }}.hithomelabs.com</span
>
<span class="service"
>→ {{ mapping.protocol }}://192.168.0.100:{{
mapping.port
}}</span
>
</div>
}
}
} @else {
<button
mat-button
color="primary"
(click)="loadMappings(tunnel.id)"
>
<mat-icon>visibility</mat-icon>
View Mappings
</button>
}
</mat-expansion-panel>
}
</mat-accordion>
</mat-card-content>
</mat-card>
`,
styles: [
`
.tunnels-card {
margin-bottom: 1.5rem;
}
.loading-container {
display: flex;
justify-content: center;
padding: 2rem;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 2rem;
color: #666;
}
.empty-state mat-icon {
font-size: 48px;
width: 48px;
height: 48px;
}
.tunnel-icon {
margin-right: 0.5rem;
}
.mapping-item {
display: flex;
flex-direction: column;
padding: 0.75rem;
margin: 0.5rem 0;
background: #f5f5f5;
border-radius: 4px;
}
.hostname {
font-weight: 500;
color: #1976d2;
}
.service {
color: #666;
font-size: 0.9rem;
}
.no-mappings {
color: #999;
font-style: italic;
}
mat-panel-title {
display: flex;
align-items: center;
}
`,
],
})
export class TunnelListComponent implements OnInit {
@Input() tunnels: Tunnel[] = [];
isLoading = false;
mappingsLoading = false;
expandedTunnelId: string | null = null;
tunnelMappings: { [key: string]: Mapping[] } = {};
constructor(private apiService: ApiService) {}
ngOnInit(): void {}
loadMappings(tunnelId: string): void {
this.expandedTunnelId = tunnelId;
this.mappingsLoading = true;
this.apiService.getTunnelMappings(tunnelId).subscribe({
next: (response) => {
this.tunnelMappings[tunnelId] = response.data;
this.mappingsLoading = false;
},
error: (err) => {
console.error('Error loading mappings:', err);
this.mappingsLoading = false;
},
});
}
}