Switch to angular
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
<mat-card class="upload-card">
|
||||
<mat-card-header>
|
||||
<mat-card-title>
|
||||
<mat-icon>cloud_upload</mat-icon>
|
||||
Upload Artifact
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
|
||||
<mat-card-content>
|
||||
<form (ngSubmit)="uploadArtifact()" #uploadForm="ngForm" class="upload-form">
|
||||
<!-- File Upload Section -->
|
||||
<div class="file-upload-section">
|
||||
<div class="file-input-container">
|
||||
<input
|
||||
#fileInput
|
||||
type="file"
|
||||
id="file"
|
||||
name="file"
|
||||
(change)="onFileSelected($event)"
|
||||
required
|
||||
style="display: none;">
|
||||
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Select File</mat-label>
|
||||
<input matInput
|
||||
[value]="selectedFile?.name || ''"
|
||||
placeholder="No file selected"
|
||||
readonly>
|
||||
<button mat-icon-button
|
||||
matSuffix
|
||||
type="button"
|
||||
(click)="fileInput.click()"
|
||||
[attr.aria-label]="'Select file'">
|
||||
<mat-icon>folder_open</mat-icon>
|
||||
</button>
|
||||
<mat-hint>Supported: CSV, JSON, binary files, PCAP</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div *ngIf="selectedFile" class="selected-file-info">
|
||||
<mat-chip-set>
|
||||
<mat-chip color="primary">
|
||||
<mat-icon matChipAvatar>description</mat-icon>
|
||||
{{ selectedFile.name }}
|
||||
</mat-chip>
|
||||
<mat-chip color="accent">
|
||||
<mat-icon matChipAvatar>data_usage</mat-icon>
|
||||
{{ selectedFile.size | number }} bytes
|
||||
</mat-chip>
|
||||
</mat-chip-set>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event ID and Test Name -->
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Event ID</mat-label>
|
||||
<input matInput
|
||||
name="eventId"
|
||||
[(ngModel)]="formData.eventId"
|
||||
placeholder="e.g., EVENT_001">
|
||||
<mat-icon matSuffix>event</mat-icon>
|
||||
<mat-hint>Groups multiple artifacts under the same event</mat-hint>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Test Name</mat-label>
|
||||
<input matInput
|
||||
name="testName"
|
||||
[(ngModel)]="formData.testName"
|
||||
placeholder="e.g., login_test">
|
||||
<mat-icon matSuffix>quiz</mat-icon>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Test Suite and Result -->
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Test Suite</mat-label>
|
||||
<input matInput
|
||||
name="testSuite"
|
||||
[(ngModel)]="formData.testSuite"
|
||||
placeholder="e.g., integration">
|
||||
<mat-icon matSuffix>category</mat-icon>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Test Result</mat-label>
|
||||
<mat-select name="testResult" [(ngModel)]="formData.testResult">
|
||||
<mat-option value="">-- Select --</mat-option>
|
||||
<mat-option *ngFor="let result of testResults" [value]="result">
|
||||
<mat-icon>{{ getResultIcon(result) }}</mat-icon>
|
||||
{{ result | titlecase }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-icon matSuffix>assignment_turned_in</mat-icon>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Version and Binaries -->
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Version</mat-label>
|
||||
<input matInput
|
||||
name="version"
|
||||
[(ngModel)]="formData.version"
|
||||
placeholder="e.g., v1.0.0">
|
||||
<mat-icon matSuffix>tag</mat-icon>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Associated Binaries</mat-label>
|
||||
<input matInput
|
||||
name="binaries"
|
||||
[(ngModel)]="formData.binaries"
|
||||
placeholder="e.g., app.exe, lib.dll, config.json">
|
||||
<mat-icon matSuffix>code</mat-icon>
|
||||
<mat-hint>Comma-separated list of binaries/files</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Tags -->
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Tags</mat-label>
|
||||
<input matInput
|
||||
name="tags"
|
||||
[(ngModel)]="formData.tags"
|
||||
placeholder="e.g., regression, smoke, critical">
|
||||
<mat-icon matSuffix>label</mat-icon>
|
||||
<mat-hint>Comma-separated tags</mat-hint>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Description -->
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Description</mat-label>
|
||||
<textarea matInput
|
||||
name="description"
|
||||
[(ngModel)]="formData.description"
|
||||
rows="3"
|
||||
placeholder="Describe this artifact..."></textarea>
|
||||
<mat-icon matSuffix>description</mat-icon>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- JSON Fields -->
|
||||
<div class="json-section">
|
||||
<h3>Advanced Configuration</h3>
|
||||
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Test Config (JSON)</mat-label>
|
||||
<textarea matInput
|
||||
name="testConfig"
|
||||
[(ngModel)]="formData.testConfig"
|
||||
rows="4"
|
||||
placeholder='{"browser": "chrome", "timeout": 30}'></textarea>
|
||||
<mat-icon matSuffix>settings</mat-icon>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Custom Metadata (JSON)</mat-label>
|
||||
<textarea matInput
|
||||
name="customMetadata"
|
||||
[(ngModel)]="formData.customMetadata"
|
||||
rows="4"
|
||||
placeholder='{"build": "1234", "commit": "abc123"}'></textarea>
|
||||
<mat-icon matSuffix>data_object</mat-icon>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Upload Progress -->
|
||||
<mat-progress-bar *ngIf="uploading"
|
||||
mode="indeterminate"
|
||||
class="upload-progress"></mat-progress-bar>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div class="form-actions">
|
||||
<button mat-raised-button
|
||||
color="primary"
|
||||
type="submit"
|
||||
[disabled]="uploading || !selectedFile"
|
||||
class="upload-button">
|
||||
<mat-icon>{{ uploading ? 'hourglass_empty' : 'cloud_upload' }}</mat-icon>
|
||||
{{ uploading ? 'Uploading...' : 'Upload Artifact' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Status Messages -->
|
||||
<div *ngIf="uploadStatus" class="status-section">
|
||||
<mat-chip-set>
|
||||
<mat-chip [class]="uploadStatusType">
|
||||
<mat-icon matChipAvatar>
|
||||
{{ uploadStatusType === 'success' ? 'check_circle' : 'error' }}
|
||||
</mat-icon>
|
||||
{{ uploadStatus }}
|
||||
</mat-chip>
|
||||
</mat-chip-set>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
@@ -0,0 +1,124 @@
|
||||
.upload-card {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.upload-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.file-upload-section {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.file-input-container {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.selected-file-info {
|
||||
margin-top: 12px;
|
||||
|
||||
mat-chip-set {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.json-section {
|
||||
margin-top: 24px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
|
||||
h3 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #424242;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-progress {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.upload-button {
|
||||
padding: 12px 32px;
|
||||
font-size: 16px;
|
||||
|
||||
mat-icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.status-section {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
mat-chip-set {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.success {
|
||||
background-color: #c8e6c9 !important;
|
||||
color: #2e7d32 !important;
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: #ffcdd2 !important;
|
||||
color: #d32f2f !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Material Design overrides
|
||||
:host ::ng-deep {
|
||||
.mat-mdc-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mat-mdc-form-field-hint {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.mat-mdc-chip {
|
||||
--mdc-chip-container-height: 28px;
|
||||
}
|
||||
|
||||
.mat-mdc-text-field-wrapper {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.upload-card {
|
||||
margin: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.upload-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
176
frontend/src/app/components/upload-form/upload-form.component.ts
Normal file
176
frontend/src/app/components/upload-form/upload-form.component.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { Component, Output, EventEmitter } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { ArtifactService } from '../../services/artifact.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-upload-form',
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
MatCardModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatChipsModule,
|
||||
MatProgressBarModule,
|
||||
MatSnackBarModule
|
||||
],
|
||||
templateUrl: './upload-form.component.html',
|
||||
styleUrl: './upload-form.component.scss'
|
||||
})
|
||||
export class UploadFormComponent {
|
||||
@Output() uploadSuccess = new EventEmitter<void>();
|
||||
|
||||
selectedFile: File | null = null;
|
||||
uploading = false;
|
||||
uploadStatus = '';
|
||||
uploadStatusType: 'success' | 'error' | '' = '';
|
||||
|
||||
formData = {
|
||||
testName: '',
|
||||
testSuite: '',
|
||||
testResult: '',
|
||||
version: '',
|
||||
description: '',
|
||||
tags: '',
|
||||
testConfig: '',
|
||||
customMetadata: '',
|
||||
eventId: '',
|
||||
binaries: ''
|
||||
};
|
||||
|
||||
testResults = ['pass', 'fail', 'skip', 'error'];
|
||||
|
||||
constructor(private artifactService: ArtifactService) {}
|
||||
|
||||
onFileSelected(event: any): void {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
this.selectedFile = file;
|
||||
}
|
||||
}
|
||||
|
||||
resetForm(): void {
|
||||
this.selectedFile = null;
|
||||
this.formData = {
|
||||
testName: '',
|
||||
testSuite: '',
|
||||
testResult: '',
|
||||
version: '',
|
||||
description: '',
|
||||
tags: '',
|
||||
testConfig: '',
|
||||
customMetadata: '',
|
||||
eventId: '',
|
||||
binaries: ''
|
||||
};
|
||||
|
||||
const fileInput = document.getElementById('file') as HTMLInputElement;
|
||||
if (fileInput) {
|
||||
fileInput.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
showUploadStatus(message: string, success: boolean): void {
|
||||
this.uploadStatus = message;
|
||||
this.uploadStatusType = success ? 'success' : 'error';
|
||||
|
||||
setTimeout(() => {
|
||||
this.uploadStatus = '';
|
||||
this.uploadStatusType = '';
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
uploadArtifact(): void {
|
||||
if (!this.selectedFile) {
|
||||
this.showUploadStatus('Please select a file to upload', false);
|
||||
return;
|
||||
}
|
||||
|
||||
this.uploading = true;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', this.selectedFile);
|
||||
|
||||
const fields = ['testName', 'testSuite', 'testResult', 'version', 'description', 'eventId'];
|
||||
fields.forEach(field => {
|
||||
const key = field === 'testName' ? 'test_name' :
|
||||
field === 'testSuite' ? 'test_suite' :
|
||||
field === 'testResult' ? 'test_result' :
|
||||
field === 'eventId' ? 'event_id' : field;
|
||||
|
||||
const value = this.formData[field as keyof typeof this.formData];
|
||||
if (value) {
|
||||
formData.append(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
if (this.formData.tags) {
|
||||
const tagsArray = this.formData.tags.split(',').map(t => t.trim()).filter(t => t);
|
||||
formData.append('tags', JSON.stringify(tagsArray));
|
||||
}
|
||||
|
||||
if (this.formData.binaries) {
|
||||
const binariesArray = this.formData.binaries.split(',').map(b => b.trim()).filter(b => b);
|
||||
formData.append('binaries', JSON.stringify(binariesArray));
|
||||
}
|
||||
|
||||
if (this.formData.testConfig) {
|
||||
try {
|
||||
JSON.parse(this.formData.testConfig);
|
||||
formData.append('test_config', this.formData.testConfig);
|
||||
} catch (e) {
|
||||
this.showUploadStatus('Invalid Test Config JSON', false);
|
||||
this.uploading = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.formData.customMetadata) {
|
||||
try {
|
||||
JSON.parse(this.formData.customMetadata);
|
||||
formData.append('custom_metadata', this.formData.customMetadata);
|
||||
} catch (e) {
|
||||
this.showUploadStatus('Invalid Custom Metadata JSON', false);
|
||||
this.uploading = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.artifactService.uploadArtifact(formData).subscribe({
|
||||
next: (response) => {
|
||||
this.showUploadStatus(`Successfully uploaded: ${response.filename}`, true);
|
||||
this.resetForm();
|
||||
this.uploadSuccess.emit();
|
||||
this.uploading = false;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Upload error:', error);
|
||||
this.showUploadStatus('Upload failed: ' + (error.error?.detail || error.message), false);
|
||||
this.uploading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getResultIcon(result: string): string {
|
||||
switch (result) {
|
||||
case 'pass': return 'check_circle';
|
||||
case 'fail': return 'cancel';
|
||||
case 'skip': return 'skip_next';
|
||||
case 'error': return 'error';
|
||||
default: return 'help';
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user