Migrate frontend to Angular 20 with full Docker support

Implemented a complete Angular 20 migration with modern standalone components architecture and production-ready Docker deployment:

**Frontend Migration:**
- Created Angular 20 application with standalone components (no NgModules)
- Implemented three main components: artifacts-list, upload-form, query-form
- Added TypeScript models and services for type-safe API communication
- Migrated dark theme UI with all existing features
- Configured routing and navigation between views
- Set up development proxy for seamless API integration
- Reactive forms with validation for upload and query functionality
- Auto-refresh artifacts every 5 seconds with RxJS observables
- Client-side sorting, filtering, and search capabilities
- Tags displayed as inline badges, SIM source grouping support

**Docker Integration:**
- Multi-stage Dockerfile for Angular (Node 24 build, nginx Alpine serve)
- nginx configuration for SPA routing and API proxy
- Updated docker-compose.yml with frontend service on port 80
- Health checks for all services
- Production-optimized build with gzip compression and asset caching

**Technical Stack:**
- Angular 20 with standalone components
- TypeScript for type safety
- RxJS for reactive programming
- nginx as reverse proxy
- Multi-stage Docker builds for optimal image size

All features fully functional and tested in Docker environment.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-15 11:35:28 -05:00
parent 2861022ac6
commit d69c209101
36 changed files with 2546 additions and 0 deletions

View File

@@ -0,0 +1,132 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ArtifactService } from '../../services/artifact';
@Component({
selector: 'app-upload-form',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
templateUrl: './upload-form.html',
styleUrls: ['./upload-form.css']
})
export class UploadFormComponent {
uploadForm: FormGroup;
selectedFile: File | null = null;
uploading: boolean = false;
uploadStatus: { message: string, success: boolean } | null = null;
constructor(
private fb: FormBuilder,
private artifactService: ArtifactService
) {
this.uploadForm = this.fb.group({
file: [null, Validators.required],
sim_source: ['', Validators.required],
uploaded_by: ['', Validators.required],
sim_source_id: [''],
tags: ['', Validators.required],
test_result: [''],
version: [''],
description: [''],
test_config: [''],
custom_metadata: ['']
});
}
onFileSelected(event: Event) {
const input = event.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
this.selectedFile = input.files[0];
this.uploadForm.patchValue({ file: this.selectedFile });
}
}
onSubmit() {
if (!this.uploadForm.valid || !this.selectedFile) {
this.showStatus('Please fill in all required fields and select a file', false);
return;
}
// Validate JSON fields
const testConfig = this.uploadForm.value.test_config;
const customMetadata = this.uploadForm.value.custom_metadata;
if (testConfig) {
try {
JSON.parse(testConfig);
} catch (e) {
this.showStatus('Invalid Test Config JSON', false);
return;
}
}
if (customMetadata) {
try {
JSON.parse(customMetadata);
} catch (e) {
this.showStatus('Invalid Custom Metadata JSON', false);
return;
}
}
const formData = new FormData();
formData.append('file', this.selectedFile);
formData.append('test_suite', this.uploadForm.value.sim_source);
formData.append('test_name', this.uploadForm.value.uploaded_by);
if (this.uploadForm.value.sim_source_id) {
formData.append('sim_source_id', this.uploadForm.value.sim_source_id);
}
// Parse and append tags as JSON array
if (this.uploadForm.value.tags) {
const tagsArray = this.uploadForm.value.tags
.split(',')
.map((t: string) => t.trim())
.filter((t: string) => t);
formData.append('tags', JSON.stringify(tagsArray));
}
if (this.uploadForm.value.test_result) {
formData.append('test_result', this.uploadForm.value.test_result);
}
if (this.uploadForm.value.version) {
formData.append('version', this.uploadForm.value.version);
}
if (this.uploadForm.value.description) {
formData.append('description', this.uploadForm.value.description);
}
if (testConfig) {
formData.append('test_config', testConfig);
}
if (customMetadata) {
formData.append('custom_metadata', customMetadata);
}
this.uploading = true;
this.artifactService.uploadArtifact(formData).subscribe({
next: (artifact) => {
this.showStatus(`Successfully uploaded: ${artifact.filename}`, true);
this.uploadForm.reset();
this.selectedFile = null;
this.uploading = false;
},
error: (err) => {
this.showStatus('Upload failed: ' + err.error?.detail || err.message, false);
this.uploading = false;
}
});
}
private showStatus(message: string, success: boolean) {
this.uploadStatus = { message, success };
setTimeout(() => {
this.uploadStatus = null;
}, 5000);
}
}