flare / flare-ui /src /app /dialogs /api-edit-dialog /api-edit-dialog.component.ts
ciyidogan's picture
Upload 118 files
9f79da5 verified
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';
@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>,
@Inject(MAT_DIALOG_DATA) public data: any
) {}
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);
}
}