forked from Hithomelabs/CFTunnels
- 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)
183 lines
4.9 KiB
TypeScript
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;
|
|
},
|
|
});
|
|
}
|
|
}
|