Spaces:
Building
Building
import { Component, Inject, OnInit } from '@angular/core'; | |
import { CommonModule } from '@angular/common'; | |
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule, FormArray } from '@angular/forms'; | |
import { FormsModule } 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 { MatMenuModule } from '@angular/material/menu'; | |
import { MatTableModule } from '@angular/material/table'; | |
import { ApiService } from '../../services/api.service'; | |
import { JsonEditorComponent } from '../../shared/json-editor/json-editor.component'; | |
({ | |
selector: 'app-api-edit-dialog', | |
standalone: true, | |
imports: [ | |
CommonModule, | |
ReactiveFormsModule, | |
FormsModule, | |
MatDialogModule, | |
MatTabsModule, | |
MatFormFieldModule, | |
MatInputModule, | |
MatSelectModule, | |
MatCheckboxModule, | |
MatButtonModule, | |
MatIconModule, | |
MatSnackBarModule, | |
MatDividerModule, | |
MatExpansionModule, | |
MatChipsModule, | |
MatMenuModule, | |
MatTableModule, | |
JsonEditorComponent | |
], | |
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; | |
testRequestJson = '{}'; | |
allIntentParameters: string[] = []; | |
responseMappingVariables: string[] = []; | |
activeTabIndex = 0; | |
httpMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']; | |
retryStrategies = ['static', 'exponential']; | |
variableTypes = ['str', 'int', 'float', 'bool', 'date']; | |
constructor( | |
private fb: FormBuilder, | |
private apiService: ApiService, | |
private snackBar: MatSnackBar, | |
public dialogRef: MatDialogRef<ApiEditDialogComponent>, | |
public data: any (MAT_DIALOG_DATA) | |
) {} | |
ngOnInit() { | |
this.initializeForm(); | |
this.loadIntentParameters(); | |
// Aktif tab'ı ayarla | |
if (this.data.activeTab !== undefined) { | |
this.activeTabIndex = this.data.activeTab; | |
} | |
if ((this.data.mode === 'edit' || this.data.mode === 'test') && 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); | |
} | |
// Test modunda açıldıysa test JSON'ını hazırla | |
if (this.data.mode === 'test') { | |
setTimeout(() => { | |
this.updateTestRequestJson(); | |
}, 100); | |
} | |
// Watch response mappings changes | |
this.form.get('response_mappings')?.valueChanges.subscribe(() => { | |
this.updateResponseMappingVariables(); | |
}); | |
} | |
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: [''], | |
response_mappings: this.fb.array([]), | |
// 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) { | |
console.log('Populating form with API:', api); | |
// Convert headers object to FormArray | |
const headersArray = this.form.get('headers') as FormArray; | |
headersArray.clear(); | |
if (api.headers) { | |
if (Array.isArray(api.headers)) { | |
api.headers.forEach((header: any) => { | |
headersArray.push(this.createHeaderFormGroup(header.key || '', header.value || '')); | |
}); | |
} else if (typeof api.headers === 'object') { | |
Object.entries(api.headers).forEach(([key, value]) => { | |
headersArray.push(this.createHeaderFormGroup(key, value as string)); | |
}); | |
} | |
} | |
// Convert response_mappings to FormArray | |
const responseMappingsArray = this.form.get('response_mappings') as FormArray; | |
responseMappingsArray.clear(); | |
if (api.response_mappings && Array.isArray(api.response_mappings)) { | |
api.response_mappings.forEach((mapping: any) => { | |
responseMappingsArray.push(this.createResponseMappingFormGroup(mapping)); | |
}); | |
} | |
// 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); | |
} | |
} | |
const formData = { ...api }; | |
// headers array'ini kaldır çünkü zaten FormArray'e ekledik | |
delete formData.headers; | |
delete formData.response_mappings; | |
// Patch form values | |
this.form.patchValue(formData); | |
// Disable name field if editing or testing | |
if (this.data.mode === 'edit' || this.data.mode === 'test') { | |
this.form.get('name')?.disable(); | |
} | |
} | |
get headers() { | |
return this.form.get('headers') as FormArray; | |
} | |
get responseMappings() { | |
return this.form.get('response_mappings') as FormArray; | |
} | |
createHeaderFormGroup(key = '', value = ''): FormGroup { | |
return this.fb.group({ | |
key: [key, Validators.required], | |
value: [value, Validators.required] | |
}); | |
} | |
createResponseMappingFormGroup(data: any = {}): FormGroup { | |
return this.fb.group({ | |
variable_name: [data.variable_name || '', [Validators.required, Validators.pattern(/^[a-z_][a-z0-9_]*$/)]], | |
type: [data.type || 'str', Validators.required], | |
json_path: [data.json_path || '', Validators.required], | |
caption: [data.caption || '', Validators.required] | |
}); | |
} | |
addHeader() { | |
this.headers.push(this.createHeaderFormGroup()); | |
} | |
removeHeader(index: number) { | |
this.headers.removeAt(index); | |
} | |
addResponseMapping() { | |
this.responseMappings.push(this.createResponseMappingFormGroup()); | |
} | |
removeResponseMapping(index: number) { | |
this.responseMappings.removeAt(index); | |
} | |
insertHeaderValue(index: number, variable: string) { | |
const headerGroup = this.headers.at(index); | |
if (headerGroup) { | |
const valueControl = headerGroup.get('value'); | |
if (valueControl) { | |
const currentValue = valueControl.value || ''; | |
const newValue = currentValue + `{{${variable}}}`; | |
valueControl.setValue(newValue); | |
} | |
} | |
} | |
getTemplateVariables(includeResponseMappings = true): string[] { | |
const variables = new Set<string>(); | |
// Intent parameters | |
this.allIntentParameters.forEach(param => { | |
variables.add(`variables.${param}`); | |
}); | |
// Auth tokens | |
const apiName = this.form.get('name')?.value || 'api_name'; | |
variables.add(`auth_tokens.${apiName}.token`); | |
// Response mappings | |
if (includeResponseMappings) { | |
this.responseMappingVariables.forEach(varName => { | |
variables.add(`variables.${varName}`); | |
}); | |
} | |
// Config variables | |
variables.add('config.work_mode'); | |
variables.add('config.cloud_token'); | |
return Array.from(variables).sort(); | |
} | |
updateResponseMappingVariables() { | |
this.responseMappingVariables = []; | |
const mappings = this.responseMappings.value; | |
mappings.forEach((mapping: any) => { | |
if (mapping.variable_name) { | |
this.responseMappingVariables.push(mapping.variable_name); | |
} | |
}); | |
} | |
async loadIntentParameters() { | |
try { | |
const projects = await this.apiService.getProjects(false).toPromise(); | |
const params = new Set<string>(); | |
projects?.forEach(project => { | |
project.versions?.forEach(version => { | |
version.intents?.forEach(intent => { | |
intent.parameters?.forEach((param: any) => { | |
if (param.variable_name) { | |
params.add(param.variable_name); | |
} | |
}); | |
}); | |
}); | |
}); | |
this.allIntentParameters = Array.from(params).sort(); | |
} catch (error) { | |
console.error('Failed to load intent parameters:', error); | |
} | |
} | |
// JSON validation için replacer fonksiyonu | |
replaceVariablesForValidation = (jsonStr: string): string => { | |
let processed = jsonStr; | |
processed = processed.replace(/\{\{([^}]+)\}\}/g, (match, variablePath) => { | |
if (variablePath.includes('variables.')) { | |
const varName = variablePath.split('.').pop()?.toLowerCase() || ''; | |
const numericVars = ['count', 'passenger_count', 'timeout_seconds', 'retry_count', 'amount', 'price', 'quantity', 'age', 'id']; | |
const booleanVars = ['enabled', 'published', 'is_active', 'confirmed', 'canceled', 'deleted', 'required']; | |
if (numericVars.some(v => varName.includes(v))) { | |
return '1'; | |
} else if (booleanVars.some(v => varName.includes(v))) { | |
return 'true'; | |
} else { | |
return '"placeholder"'; | |
} | |
} | |
return '"placeholder"'; | |
}); | |
return processed; | |
} | |
async testAPI() { | |
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(); | |
let testRequestData = {}; | |
try { | |
testRequestData = JSON.parse(this.testRequestJson); | |
} catch (e) { | |
this.snackBar.open('Invalid test request JSON', 'Close', { | |
duration: 3000, | |
panelClass: 'error-snackbar' | |
}); | |
this.testing = false; | |
return; | |
} | |
testData.test_request = testRequestData; | |
const result = await this.apiService.testAPI(testData).toPromise(); | |
// Response headers'ı obje olarak sakla | |
if (result.response_headers && typeof result.response_headers === 'string') { | |
try { | |
result.response_headers = JSON.parse(result.response_headers); | |
} catch { | |
// Headers parse edilemezse string olarak bırak | |
} | |
} | |
this.testResult = result; | |
if (result.success) { | |
this.snackBar.open(`API test successful! (${result.status_code})`, 'Close', { | |
duration: 3000 | |
}); | |
} else { | |
const errorMsg = result.error || `API returned status ${result.status_code}`; | |
this.snackBar.open(`API test failed: ${errorMsg}`, '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; | |
} | |
} | |
updateTestRequestJson() { | |
const formValue = this.form.getRawValue(); | |
let bodyTemplate = {}; | |
try { | |
bodyTemplate = JSON.parse(formValue.body_template); | |
} catch { | |
bodyTemplate = {}; | |
} | |
const testData = this.replacePlaceholdersForTest(bodyTemplate); | |
this.testRequestJson = JSON.stringify(testData, null, 2); | |
} | |
replacePlaceholdersForTest(obj: any): any { | |
if (typeof obj === 'string') { | |
let result = obj; | |
result = result.replace(/\{\{variables\.origin\}\}/g, 'Istanbul'); | |
result = result.replace(/\{\{variables\.destination\}\}/g, 'Ankara'); | |
result = result.replace(/\{\{variables\.flight_date\}\}/g, '2025-06-15'); | |
result = result.replace(/\{\{variables\.passenger_count\}\}/g, '2'); | |
result = result.replace(/\{\{variables\.flight_number\}\}/g, 'TK123'); | |
result = result.replace(/\{\{variables\.pnr\}\}/g, 'ABC12'); | |
result = result.replace(/\{\{variables\.surname\}\}/g, 'Test'); | |
result = result.replace(/\{\{auth_tokens\.[^}]+\.token\}\}/g, 'test_token_123'); | |
result = result.replace(/\{\{config\.work_mode\}\}/g, 'hfcloud'); | |
result = result.replace(/\{\{[^}]+\}\}/g, 'test_value'); | |
return result; | |
} else if (typeof obj === 'object' && obj !== null) { | |
const result: any = Array.isArray(obj) ? [] : {}; | |
for (const key in obj) { | |
result[key] = this.replacePlaceholdersForTest(obj[key]); | |
} | |
return result; | |
} | |
return obj; | |
} | |
prepareAPIData(): any { | |
const formValue = this.form.getRawValue(); | |
const headers: any = {}; | |
formValue.headers.forEach((h: any) => { | |
if (h.key && h.value) { | |
headers[h.key] = h.value; | |
} | |
}); | |
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); | |
} | |
const apiData: any = { | |
name: formValue.name, | |
url: formValue.url, | |
method: formValue.method, | |
headers, | |
body_template, | |
timeout_seconds: formValue.timeout_seconds, | |
retry: formValue.retry, | |
response_prompt: formValue.response_prompt, | |
response_mappings: formValue.response_mappings || [] | |
}; | |
// Proxy - null olarak gönder boşsa | |
apiData.proxy = formValue.proxy || null; | |
if (formValue.proxy) { | |
apiData.proxy = formValue.proxy; | |
} | |
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; | |
} | |
}else { | |
// Auth disabled olsa bile null olarak gönder | |
apiData.auth = null; | |
} | |
// Edit modunda last_update_date'i ekle | |
if (this.data.mode === 'edit' && formValue.last_update_date) { | |
apiData.last_update_date = formValue.last_update_date; | |
} | |
console.log('Prepared API data:', apiData); | |
return apiData; | |
} | |
async save() { | |
if (this.data.mode === 'test') { | |
this.cancel(); | |
return; | |
} | |
if (this.form.invalid) { | |
Object.keys(this.form.controls).forEach(key => { | |
this.form.get(key)?.markAsTouched(); | |
}); | |
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); | |
} | |
} |