Switch to angular
This commit is contained in:
596
docs/FRONTEND_SETUP.md
Normal file
596
docs/FRONTEND_SETUP.md
Normal file
@@ -0,0 +1,596 @@
|
||||
# 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<ApiInfo> {
|
||||
return this.http.get<ApiInfo>(`${environment.apiUrl.replace('/api/v1', '')}/`);
|
||||
}
|
||||
|
||||
listArtifacts(limit: number = 100, offset: number = 0): Observable<Artifact[]> {
|
||||
return this.http.get<Artifact[]>(`${this.apiUrl}/artifacts/?limit=${limit}&offset=${offset}`);
|
||||
}
|
||||
|
||||
getArtifact(id: number): Observable<Artifact> {
|
||||
return this.http.get<Artifact>(`${this.apiUrl}/artifacts/${id}`);
|
||||
}
|
||||
|
||||
queryArtifacts(query: ArtifactQuery): Observable<Artifact[]> {
|
||||
return this.http.post<Artifact[]>(`${this.apiUrl}/artifacts/query`, query);
|
||||
}
|
||||
|
||||
uploadArtifact(file: File, metadata: any): Observable<Artifact> {
|
||||
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<Artifact>(`${this.apiUrl}/artifacts/upload`, formData);
|
||||
}
|
||||
|
||||
downloadArtifact(id: number): Observable<Blob> {
|
||||
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: `
|
||||
<mat-toolbar color="primary">
|
||||
<button mat-icon-button (click)="sidenav.toggle()">
|
||||
<mat-icon>menu</mat-icon>
|
||||
</button>
|
||||
<span>Test Artifact Data Lake</span>
|
||||
<span class="spacer"></span>
|
||||
<span *ngIf="apiInfo" class="mode-badge">
|
||||
<mat-icon>{{ apiInfo.deployment_mode === 'cloud' ? 'cloud' : 'dns' }}</mat-icon>
|
||||
{{ apiInfo.deployment_mode }}
|
||||
</span>
|
||||
</mat-toolbar>
|
||||
|
||||
<mat-sidenav-container>
|
||||
<mat-sidenav #sidenav mode="side" opened>
|
||||
<mat-nav-list>
|
||||
<a mat-list-item routerLink="/artifacts" routerLinkActive="active">
|
||||
<mat-icon matListItemIcon>list</mat-icon>
|
||||
<span matListItemTitle>Artifacts</span>
|
||||
</a>
|
||||
<a mat-list-item routerLink="/upload" routerLinkActive="active">
|
||||
<mat-icon matListItemIcon>cloud_upload</mat-icon>
|
||||
<span matListItemTitle>Upload</span>
|
||||
</a>
|
||||
<a mat-list-item routerLink="/query" routerLinkActive="active">
|
||||
<mat-icon matListItemIcon>search</mat-icon>
|
||||
<span matListItemTitle>Query</span>
|
||||
</a>
|
||||
</mat-nav-list>
|
||||
</mat-sidenav>
|
||||
|
||||
<mat-sidenav-content>
|
||||
<div class="content-container">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</mat-sidenav-content>
|
||||
</mat-sidenav-container>
|
||||
`,
|
||||
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: `
|
||||
<h2>Artifacts</h2>
|
||||
|
||||
<table mat-table [dataSource]="artifacts" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="id">
|
||||
<th mat-header-cell *matHeaderCellDef>ID</th>
|
||||
<td mat-cell *matCellDef="let artifact">{{ artifact.id }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="filename">
|
||||
<th mat-header-cell *matHeaderCellDef>Filename</th>
|
||||
<td mat-cell *matCellDef="let artifact">
|
||||
<a [routerLink]="['/artifacts', artifact.id]">{{ artifact.filename }}</a>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="test_name">
|
||||
<th mat-header-cell *matHeaderCellDef>Test Name</th>
|
||||
<td mat-cell *matCellDef="let artifact">{{ artifact.test_name }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="test_result">
|
||||
<th mat-header-cell *matHeaderCellDef>Result</th>
|
||||
<td mat-cell *matCellDef="let artifact">
|
||||
<mat-chip [color]="getResultColor(artifact.test_result)">
|
||||
{{ artifact.test_result }}
|
||||
</mat-chip>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="created_at">
|
||||
<th mat-header-cell *matHeaderCellDef>Created</th>
|
||||
<td mat-cell *matCellDef="let artifact">
|
||||
{{ artifact.created_at | date:'short' }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
||||
<td mat-cell *matCellDef="let artifact">
|
||||
<button mat-icon-button (click)="downloadArtifact(artifact.id)">
|
||||
<mat-icon>download</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" (click)="deleteArtifact(artifact.id)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
|
||||
<mat-paginator
|
||||
[length]="totalCount"
|
||||
[pageSize]="pageSize"
|
||||
[pageSizeOptions]="[10, 25, 50, 100]"
|
||||
(page)="onPageChange($event)">
|
||||
</mat-paginator>
|
||||
`,
|
||||
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
|
||||
Reference in New Issue
Block a user