import { Component, Inject, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormBuilder, FormGroup, Validators, ReactiveFormsModule, FormArray } from '@angular/forms'; import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; import { MatTabsModule } from '@angular/material/tabs'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; import { MatDividerModule } from '@angular/material/divider'; import { MatExpansionModule } from '@angular/material/expansion'; import { MatChipsModule } from '@angular/material/chips'; import { ApiService } from '../../services/api.service'; @Component({ selector: 'app-api-edit-dialog', standalone: true, imports: [ CommonModule, ReactiveFormsModule, MatDialogModule, MatTabsModule, MatFormFieldModule, MatInputModule, MatSelectModule, MatCheckboxModule, MatButtonModule, MatIconModule, MatSnackBarModule, MatDividerModule, MatExpansionModule, MatChipsModule ], templateUrl: './api-edit-dialog.component.html', styleUrls: ['./api-edit-dialog.component.scss'] }) export default class ApiEditDialogComponent implements OnInit { form!: FormGroup; saving = false; testing = false; testResult: any = null; httpMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']; retryStrategies = ['static', 'exponential']; // Template variable helpers templateVariables = [ { key: 'variables.origin', desc: 'Origin city from session' }, { key: 'variables.destination', desc: 'Destination city from session' }, { key: 'variables.flight_date', desc: 'Flight date from session' }, { key: 'auth_tokens.api_name.token', desc: 'Auth token for this API' }, { key: 'config.work_mode', desc: 'Current work mode' } ]; constructor( private fb: FormBuilder, private apiService: ApiService, private snackBar: MatSnackBar, public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: any ) {} ngOnInit() { this.initializeForm(); if (this.data.mode === 'edit' && this.data.api) { this.populateForm(this.data.api); } else if (this.data.mode === 'duplicate' && this.data.api) { const duplicateData = { ...this.data.api }; duplicateData.name = duplicateData.name + '_copy'; delete duplicateData.last_update_date; this.populateForm(duplicateData); } } initializeForm() { this.form = this.fb.group({ // General Tab name: ['', [Validators.required, Validators.pattern(/^[a-zA-Z0-9_]+$/)]], url: ['', [Validators.required, Validators.pattern(/^https?:\/\/.+/)]], method: ['POST', Validators.required], body_template: ['{}'], timeout_seconds: [10, [Validators.required, Validators.min(1), Validators.max(300)]], response_prompt: [''], // Headers Tab headers: this.fb.array([]), // Retry Settings retry: this.fb.group({ retry_count: [3, [Validators.required, Validators.min(0), Validators.max(10)]], backoff_seconds: [2, [Validators.required, Validators.min(1), Validators.max(60)]], strategy: ['static', Validators.required] }), // Auth Tab auth: this.fb.group({ enabled: [false], token_endpoint: [''], response_token_path: ['token'], token_request_body: ['{}'], token_refresh_endpoint: [''], token_refresh_body: ['{}'] }), // Proxy (optional) proxy: [''], // For race condition handling last_update_date: [''] }); // Watch for auth enabled changes this.form.get('auth.enabled')?.valueChanges.subscribe(enabled => { const authGroup = this.form.get('auth'); if (enabled) { authGroup?.get('token_endpoint')?.setValidators([Validators.required]); authGroup?.get('response_token_path')?.setValidators([Validators.required]); } else { authGroup?.get('token_endpoint')?.clearValidators(); authGroup?.get('response_token_path')?.clearValidators(); } authGroup?.get('token_endpoint')?.updateValueAndValidity(); authGroup?.get('response_token_path')?.updateValueAndValidity(); }); } populateForm(api: any) { // Convert headers object to FormArray const headersArray = this.form.get('headers') as FormArray; headersArray.clear(); if (api.headers && typeof api.headers === 'object') { Object.entries(api.headers).forEach(([key, value]) => { headersArray.push(this.createHeaderFormGroup(key, value as string)); }); } // Convert body_template to JSON string if it's an object if (api.body_template && typeof api.body_template === 'object') { api.body_template = JSON.stringify(api.body_template, null, 2); } // Convert auth bodies to JSON strings if (api.auth) { if (api.auth.token_request_body && typeof api.auth.token_request_body === 'object') { api.auth.token_request_body = JSON.stringify(api.auth.token_request_body, null, 2); } if (api.auth.token_refresh_body && typeof api.auth.token_refresh_body === 'object') { api.auth.token_refresh_body = JSON.stringify(api.auth.token_refresh_body, null, 2); } } // Patch form values this.form.patchValue(api); // Disable name field if editing if (this.data.mode === 'edit') { this.form.get('name')?.disable(); } } get headers() { return this.form.get('headers') as FormArray; } createHeaderFormGroup(key = '', value = ''): FormGroup { return this.fb.group({ key: [key, Validators.required], value: [value, Validators.required] }); } addHeader() { this.headers.push(this.createHeaderFormGroup()); } removeHeader(index: number) { this.headers.removeAt(index); } insertTemplateVariable(field: string, variable: string) { const control = field.includes('.') ? this.form.get(field) : this.form.get(field); if (control) { const currentValue = control.value || ''; const newValue = currentValue + `{{${variable}}}`; control.setValue(newValue); } } validateJSON(field: string): boolean { const control = this.form.get(field); if (!control || !control.value) return true; try { JSON.parse(control.value); return true; } catch { return false; } } async testAPI() { // Validate form first const generalValid = this.form.get('url')?.valid && this.form.get('method')?.valid; if (!generalValid) { this.snackBar.open('Please fill in required fields first', 'Close', { duration: 3000 }); return; } this.testing = true; this.testResult = null; try { const testData = this.prepareAPIData(); // For test, we'll use a sample request const result = await this.apiService.testAPI(testData).toPromise(); this.testResult = result; if (result.success) { this.snackBar.open('API test successful!', 'Close', { duration: 3000 }); } else { this.snackBar.open(`API test failed: ${result.error}`, 'Close', { duration: 5000, panelClass: 'error-snackbar' }); } } catch (error: any) { this.testResult = { success: false, error: error.message || 'Test failed' }; this.snackBar.open('API test failed', 'Close', { duration: 3000, panelClass: 'error-snackbar' }); } finally { this.testing = false; } } prepareAPIData(): any { const formValue = this.form.getRawValue(); // Convert headers array back to object const headers: any = {}; formValue.headers.forEach((h: any) => { if (h.key && h.value) { headers[h.key] = h.value; } }); // Parse JSON fields let body_template = {}; let auth_token_request_body = {}; let auth_token_refresh_body = {}; try { body_template = formValue.body_template ? JSON.parse(formValue.body_template) : {}; } catch (e) { console.error('Invalid body_template JSON:', e); } try { auth_token_request_body = formValue.auth.token_request_body ? JSON.parse(formValue.auth.token_request_body) : {}; } catch (e) { console.error('Invalid auth token_request_body JSON:', e); } try { auth_token_refresh_body = formValue.auth.token_refresh_body ? JSON.parse(formValue.auth.token_refresh_body) : {}; } catch (e) { console.error('Invalid auth token_refresh_body JSON:', e); } // Prepare final data const apiData: any = { name: formValue.name, url: formValue.url, method: formValue.method, headers, // Now it's a dictionary object body_template, timeout_seconds: formValue.timeout_seconds, retry: formValue.retry, response_prompt: formValue.response_prompt }; // Add proxy if specified if (formValue.proxy) { apiData.proxy = formValue.proxy; } // Add auth if enabled if (formValue.auth.enabled) { apiData.auth = { enabled: true, token_endpoint: formValue.auth.token_endpoint, response_token_path: formValue.auth.response_token_path, token_request_body: auth_token_request_body }; if (formValue.auth.token_refresh_endpoint) { apiData.auth.token_refresh_endpoint = formValue.auth.token_refresh_endpoint; apiData.auth.token_refresh_body = auth_token_refresh_body; } } // Add last_update_date for edit mode if (this.data.mode === 'edit' && formValue.last_update_date) { apiData.last_update_date = formValue.last_update_date; } return apiData; } async save() { if (this.form.invalid) { // Mark all fields as touched to show validation errors Object.keys(this.form.controls).forEach(key => { this.form.get(key)?.markAsTouched(); }); // Check specific JSON fields const jsonFields = ['body_template', 'auth.token_request_body', 'auth.token_refresh_body']; for (const field of jsonFields) { if (!this.validateJSON(field)) { this.snackBar.open(`Invalid JSON in ${field}`, 'Close', { duration: 3000, panelClass: 'error-snackbar' }); return; } } this.snackBar.open('Please fix validation errors', 'Close', { duration: 3000 }); return; } this.saving = true; try { const apiData = this.prepareAPIData(); if (this.data.mode === 'create' || this.data.mode === 'duplicate') { await this.apiService.createAPI(apiData).toPromise(); this.snackBar.open('API created successfully', 'Close', { duration: 3000 }); } else { await this.apiService.updateAPI(this.data.api.name, apiData).toPromise(); this.snackBar.open('API updated successfully', 'Close', { duration: 3000 }); } this.dialogRef.close(true); } catch (error: any) { const message = error.error?.detail || (this.data.mode === 'create' ? 'Failed to create API' : 'Failed to update API'); this.snackBar.open(message, 'Close', { duration: 5000, panelClass: 'error-snackbar' }); } finally { this.saving = false; } } cancel() { this.dialogRef.close(false); } }