# Angular 19 Frontend Setup Guide ## Overview This guide will help you set up the Angular 19 frontend with Material Design for the Test Artifact Data Lake. ## Prerequisites - Node.js 18+ and npm - Angular CLI 19 ## Quick Start ```bash # Install Angular CLI globally npm install -g @angular/cli@19 # Create new Angular 19 application ng new frontend --routing --style=scss --standalone # Navigate to frontend directory cd frontend # Install Angular Material ng add @angular/material # Install additional dependencies npm install --save @angular/material @angular/cdk @angular/animations npm install --save @ng-bootstrap/ng-bootstrap # Start development server ng serve ``` ## Project Structure ``` frontend/ ├── src/ │ ├── app/ │ │ ├── components/ │ │ │ ├── artifact-list/ │ │ │ ├── artifact-upload/ │ │ │ ├── artifact-detail/ │ │ │ └── artifact-query/ │ │ ├── services/ │ │ │ └── artifact.service.ts │ │ ├── models/ │ │ │ └── artifact.model.ts │ │ ├── app.component.ts │ │ └── app.routes.ts │ ├── assets/ │ ├── environments/ │ │ ├── environment.ts │ │ └── environment.prod.ts │ └── styles.scss ├── angular.json ├── package.json └── tsconfig.json ``` ## Configuration Files ### Environment Configuration Create `src/environments/environment.ts`: ```typescript export const environment = { production: false, apiUrl: 'http://localhost:8000/api/v1' }; ``` Create `src/environments/environment.prod.ts`: ```typescript export const environment = { production: true, apiUrl: '/api/v1' // Proxy through same domain in production }; ``` ### Angular Material Theme Update `src/styles.scss`: ```scss @use '@angular/material' as mat; @include mat.core(); $datalake-primary: mat.define-palette(mat.$indigo-palette); $datalake-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); $datalake-warn: mat.define-palette(mat.$red-palette); $datalake-theme: mat.define-light-theme(( color: ( primary: $datalake-primary, accent: $datalake-accent, warn: $datalake-warn, ), typography: mat.define-typography-config(), density: 0, )); @include mat.all-component-themes($datalake-theme); html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } ``` ## Core Files ### Models Create `src/app/models/artifact.model.ts`: ```typescript export interface Artifact { id: number; filename: string; file_type: string; file_size: number; storage_path: string; content_type: string | null; test_name: string | null; test_suite: string | null; test_config: any | null; test_result: string | null; custom_metadata: any | null; description: string | null; tags: string[] | null; created_at: string; updated_at: string; version: string | null; parent_id: number | null; } export interface ArtifactQuery { filename?: string; file_type?: string; test_name?: string; test_suite?: string; test_result?: string; tags?: string[]; start_date?: string; end_date?: string; limit?: number; offset?: number; } export interface ApiInfo { message: string; version: string; docs: string; deployment_mode: string; storage_backend: string; } ``` ### Service Create `src/app/services/artifact.service.ts`: ```typescript import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Observable } from 'rxjs'; import { environment } from '../../environments/environment'; import { Artifact, ArtifactQuery, ApiInfo } from '../models/artifact.model'; @Injectable({ providedIn: 'root' }) export class ArtifactService { private apiUrl = environment.apiUrl; constructor(private http: HttpClient) {} getApiInfo(): Observable { return this.http.get(`${environment.apiUrl.replace('/api/v1', '')}/`); } listArtifacts(limit: number = 100, offset: number = 0): Observable { return this.http.get(`${this.apiUrl}/artifacts/?limit=${limit}&offset=${offset}`); } getArtifact(id: number): Observable { return this.http.get(`${this.apiUrl}/artifacts/${id}`); } queryArtifacts(query: ArtifactQuery): Observable { return this.http.post(`${this.apiUrl}/artifacts/query`, query); } uploadArtifact(file: File, metadata: any): Observable { const formData = new FormData(); formData.append('file', file); if (metadata.test_name) formData.append('test_name', metadata.test_name); if (metadata.test_suite) formData.append('test_suite', metadata.test_suite); if (metadata.test_result) formData.append('test_result', metadata.test_result); if (metadata.test_config) formData.append('test_config', JSON.stringify(metadata.test_config)); if (metadata.custom_metadata) formData.append('custom_metadata', JSON.stringify(metadata.custom_metadata)); if (metadata.description) formData.append('description', metadata.description); if (metadata.tags) formData.append('tags', JSON.stringify(metadata.tags)); if (metadata.version) formData.append('version', metadata.version); return this.http.post(`${this.apiUrl}/artifacts/upload`, formData); } downloadArtifact(id: number): Observable { return this.http.get(`${this.apiUrl}/artifacts/${id}/download`, { responseType: 'blob' }); } getDownloadUrl(id: number, expiration: number = 3600): Observable<{url: string, expires_in: number}> { return this.http.get<{url: string, expires_in: number}>( `${this.apiUrl}/artifacts/${id}/url?expiration=${expiration}` ); } deleteArtifact(id: number): Observable<{message: string}> { return this.http.delete<{message: string}>(`${this.apiUrl}/artifacts/${id}`); } } ``` ### App Routes Create `src/app/app.routes.ts`: ```typescript import { Routes } from '@angular/router'; import { ArtifactListComponent } from './components/artifact-list/artifact-list.component'; import { ArtifactUploadComponent } from './components/artifact-upload/artifact-upload.component'; import { ArtifactDetailComponent } from './components/artifact-detail/artifact-detail.component'; import { ArtifactQueryComponent } from './components/artifact-query/artifact-query.component'; export const routes: Routes = [ { path: '', redirectTo: '/artifacts', pathMatch: 'full' }, { path: 'artifacts', component: ArtifactListComponent }, { path: 'upload', component: ArtifactUploadComponent }, { path: 'query', component: ArtifactQueryComponent }, { path: 'artifacts/:id', component: ArtifactDetailComponent }, ]; ``` ### Main App Component Create `src/app/app.component.ts`: ```typescript import { Component, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterOutlet, RouterLink } from '@angular/router'; import { MatToolbarModule } from '@angular/material/toolbar'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatListModule } from '@angular/material/list'; import { MatBadgeModule } from '@angular/material/badge'; import { ArtifactService } from './services/artifact.service'; import { ApiInfo } from './models/artifact.model'; @Component({ selector: 'app-root', standalone: true, imports: [ CommonModule, RouterOutlet, RouterLink, MatToolbarModule, MatButtonModule, MatIconModule, MatSidenavModule, MatListModule, MatBadgeModule ], template: ` Test Artifact Data Lake {{ apiInfo.deployment_mode === 'cloud' ? 'cloud' : 'dns' }} {{ apiInfo.deployment_mode }} list Artifacts cloud_upload Upload search Query
`, styles: [` .spacer { flex: 1 1 auto; } .mode-badge { display: flex; align-items: center; gap: 4px; font-size: 14px; } mat-sidenav-container { height: calc(100vh - 64px); } mat-sidenav { width: 250px; } .content-container { padding: 20px; } .active { background-color: rgba(0, 0, 0, 0.04); } `] }) export class AppComponent implements OnInit { title = 'Test Artifact Data Lake'; apiInfo: ApiInfo | null = null; constructor(private artifactService: ArtifactService) {} ngOnInit() { this.artifactService.getApiInfo().subscribe( info => this.apiInfo = info ); } } ``` ## Component Examples ### Artifact List Component Create `src/app/components/artifact-list/artifact-list.component.ts`: ```typescript import { Component, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterLink } from '@angular/router'; import { MatTableModule } from '@angular/material/table'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatChipsModule } from '@angular/material/chips'; import { MatPaginatorModule, PageEvent } from '@angular/material/paginator'; import { ArtifactService } from '../../services/artifact.service'; import { Artifact } from '../../models/artifact.model'; @Component({ selector: 'app-artifact-list', standalone: true, imports: [ CommonModule, RouterLink, MatTableModule, MatButtonModule, MatIconModule, MatChipsModule, MatPaginatorModule ], template: `

Artifacts

ID {{ artifact.id }} Filename {{ artifact.filename }} Test Name {{ artifact.test_name }} Result {{ artifact.test_result }} Created {{ artifact.created_at | date:'short' }} Actions
`, styles: [` h2 { margin-bottom: 20px; } table { width: 100%; } mat-paginator { margin-top: 20px; } `] }) export class ArtifactListComponent implements OnInit { artifacts: Artifact[] = []; displayedColumns = ['id', 'filename', 'test_name', 'test_result', 'created_at', 'actions']; pageSize = 25; totalCount = 1000; // You'd get this from a count endpoint constructor(private artifactService: ArtifactService) {} ngOnInit() { this.loadArtifacts(); } loadArtifacts(limit: number = 25, offset: number = 0) { this.artifactService.listArtifacts(limit, offset).subscribe( artifacts => this.artifacts = artifacts ); } onPageChange(event: PageEvent) { this.loadArtifacts(event.pageSize, event.pageIndex * event.pageSize); } getResultColor(result: string | null): string { switch (result) { case 'pass': return 'primary'; case 'fail': return 'warn'; default: return 'accent'; } } downloadArtifact(id: number) { this.artifactService.downloadArtifact(id).subscribe(blob => { const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `artifact_${id}`; a.click(); window.URL.revokeObjectURL(url); }); } deleteArtifact(id: number) { if (confirm('Are you sure you want to delete this artifact?')) { this.artifactService.deleteArtifact(id).subscribe( () => this.loadArtifacts() ); } } } ``` ## Building and Deployment ### Development ```bash ng serve # Access at http://localhost:4200 ``` ### Production Build ```bash ng build --configuration production # Output in dist/frontend/ ``` ### Docker Integration Create `frontend/Dockerfile`: ```dockerfile # Build stage FROM node:18-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build -- --configuration production # Production stage FROM nginx:alpine COPY --from=builder /app/dist/frontend/browser /usr/share/nginx/html COPY nginx.conf /etc/nginx/nginx.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"] ``` Create `frontend/nginx.conf`: ```nginx events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; server { listen 80; server_name localhost; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://api:8000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; } } } ``` ## Next Steps 1. Generate the Angular app: `ng new frontend` 2. Install Material: `ng add @angular/material` 3. Create the components shown above 4. Test locally with `ng serve` 5. Build and dockerize for production 6. Update Helm chart to include frontend deployment For complete component examples and advanced features, refer to: - Angular Material documentation: https://material.angular.io - Angular documentation: https://angular.dev