Spaces:
Building
Building
import { Component, inject, OnInit, OnDestroy } from '@angular/core'; | |
import { CommonModule } from '@angular/common'; | |
import { FormsModule } from '@angular/forms'; | |
import { MatDialog, MatDialogModule } from '@angular/material/dialog'; | |
import { MatTableModule } from '@angular/material/table'; | |
import { MatButtonModule } from '@angular/material/button'; | |
import { MatIconModule } from '@angular/material/icon'; | |
import { MatFormFieldModule } from '@angular/material/form-field'; | |
import { MatInputModule } from '@angular/material/input'; | |
import { MatCheckboxModule } from '@angular/material/checkbox'; | |
import { MatProgressBarModule } from '@angular/material/progress-bar'; | |
import { MatChipsModule } from '@angular/material/chips'; | |
import { MatMenuModule } from '@angular/material/menu'; | |
import { MatTooltipModule } from '@angular/material/tooltip'; | |
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; | |
import { MatDividerModule } from '@angular/material/divider'; | |
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; | |
import { ApiService, API } from '../../services/api.service'; | |
import { Subject, takeUntil } from 'rxjs'; | |
({ | |
selector: 'app-apis', | |
standalone: true, | |
imports: [ | |
CommonModule, | |
FormsModule, | |
MatDialogModule, | |
MatTableModule, | |
MatButtonModule, | |
MatIconModule, | |
MatFormFieldModule, | |
MatInputModule, | |
MatCheckboxModule, | |
MatProgressBarModule, | |
MatChipsModule, | |
MatMenuModule, | |
MatTooltipModule, | |
MatSnackBarModule, | |
MatDividerModule, | |
MatProgressSpinnerModule | |
], | |
template: ` | |
<div class="apis-container"> | |
<div class="toolbar"> | |
<h2>API Definitions</h2> | |
<div class="toolbar-actions"> | |
<button mat-raised-button color="primary" (click)="createAPI()" [disabled]="loading"> | |
<mat-icon>add</mat-icon> | |
New API | |
</button> | |
<button mat-button (click)="importAPIs()" [disabled]="loading"> | |
<mat-icon>upload</mat-icon> | |
Import | |
</button> | |
<button mat-button (click)="exportAPIs()" [disabled]="loading || filteredAPIs.length === 0"> | |
<mat-icon>download</mat-icon> | |
Export | |
</button> | |
<mat-form-field appearance="outline" class="search-field"> | |
<mat-label>Search APIs</mat-label> | |
<input matInput [(ngModel)]="searchTerm" (input)="filterAPIs()"> | |
<mat-icon matSuffix>search</mat-icon> | |
</mat-form-field> | |
<mat-checkbox [(ngModel)]="showDeleted" (change)="loadAPIs()"> | |
Display Deleted | |
</mat-checkbox> | |
</div> | |
</div> | |
<mat-progress-bar mode="indeterminate" *ngIf="loading"></mat-progress-bar> | |
@if (!loading && error) { | |
<div class="error-state"> | |
<mat-icon>error_outline</mat-icon> | |
<p>{{ error }}</p> | |
<button mat-raised-button color="primary" (click)="loadAPIs()"> | |
<mat-icon>refresh</mat-icon> | |
Retry | |
</button> | |
</div> | |
} @else if (!loading && filteredAPIs.length === 0 && !searchTerm) { | |
<div class="empty-state"> | |
<mat-icon>api</mat-icon> | |
<p>No APIs found.</p> | |
<button mat-raised-button color="primary" (click)="createAPI()"> | |
Create your first API | |
</button> | |
</div> | |
} @else if (!loading && filteredAPIs.length === 0 && searchTerm) { | |
<div class="empty-state"> | |
<mat-icon>search_off</mat-icon> | |
<p>No APIs match your search.</p> | |
<button mat-button (click)="searchTerm = ''; filterAPIs()"> | |
Clear search | |
</button> | |
</div> | |
} @else if (!loading) { | |
<table mat-table [dataSource]="filteredAPIs" class="apis-table"> | |
<!-- Name Column --> | |
<ng-container matColumnDef="name"> | |
<th mat-header-cell *matHeaderCellDef>Name</th> | |
<td mat-cell *matCellDef="let api">{{ api.name }}</td> | |
</ng-container> | |
<!-- URL Column --> | |
<ng-container matColumnDef="url"> | |
<th mat-header-cell *matHeaderCellDef>URL</th> | |
<td mat-cell *matCellDef="let api" class="url-cell"> | |
<span [matTooltip]="api.url">{{ api.url }}</span> | |
</td> | |
</ng-container> | |
<!-- Method Column --> | |
<ng-container matColumnDef="method"> | |
<th mat-header-cell *matHeaderCellDef>Method</th> | |
<td mat-cell *matCellDef="let api"> | |
<mat-chip [class]="'method-' + api.method.toLowerCase()"> | |
{{ api.method }} | |
</mat-chip> | |
</td> | |
</ng-container> | |
<!-- Timeout Column --> | |
<ng-container matColumnDef="timeout"> | |
<th mat-header-cell *matHeaderCellDef>Timeout</th> | |
<td mat-cell *matCellDef="let api">{{ api.timeout_seconds }}s</td> | |
</ng-container> | |
<!-- Auth Column --> | |
<ng-container matColumnDef="auth"> | |
<th mat-header-cell *matHeaderCellDef>Auth</th> | |
<td mat-cell *matCellDef="let api"> | |
<mat-icon [color]="api.auth?.enabled ? 'primary' : ''" | |
[matTooltip]="api.auth?.enabled ? 'Authentication enabled' : 'No authentication'"> | |
{{ api.auth?.enabled ? 'lock' : 'lock_open' }} | |
</mat-icon> | |
</td> | |
</ng-container> | |
<!-- Deleted Column --> | |
<ng-container matColumnDef="deleted"> | |
<th mat-header-cell *matHeaderCellDef>Status</th> | |
<td mat-cell *matCellDef="let api"> | |
@if (api.deleted) { | |
<mat-icon color="warn" matTooltip="Deleted">delete</mat-icon> | |
} @else { | |
<mat-icon color="primary" matTooltip="Active">check_circle</mat-icon> | |
} | |
</td> | |
</ng-container> | |
<!-- Actions Column --> | |
<ng-container matColumnDef="actions"> | |
<th mat-header-cell *matHeaderCellDef>Actions</th> | |
<td mat-cell *matCellDef="let api"> | |
<button mat-icon-button [matMenuTriggerFor]="menu" | |
(click)="$event.stopPropagation()" | |
[disabled]="actionLoading[api.name]"> | |
@if (actionLoading[api.name]) { | |
<mat-spinner diameter="20"></mat-spinner> | |
} @else { | |
<mat-icon>more_vert</mat-icon> | |
} | |
</button> | |
<mat-menu #menu="matMenu"> | |
<button mat-menu-item (click)="editAPI(api)" [disabled]="api.deleted"> | |
<mat-icon>edit</mat-icon> | |
<span>Edit</span> | |
</button> | |
<button mat-menu-item (click)="testAPI(api)"> | |
<mat-icon>play_arrow</mat-icon> | |
<span>Test</span> | |
</button> | |
<button mat-menu-item (click)="duplicateAPI(api)"> | |
<mat-icon>content_copy</mat-icon> | |
<span>Duplicate</span> | |
</button> | |
@if (!api.deleted) { | |
<mat-divider></mat-divider> | |
<button mat-menu-item (click)="deleteAPI(api)"> | |
<mat-icon color="warn">delete</mat-icon> | |
<span>Delete</span> | |
</button> | |
} @else { | |
<mat-divider></mat-divider> | |
<button mat-menu-item (click)="restoreAPI(api)"> | |
<mat-icon color="primary">restore</mat-icon> | |
<span>Restore</span> | |
</button> | |
} | |
</mat-menu> | |
</td> | |
</ng-container> | |
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> | |
<tr mat-row *matRowDef="let row; columns: displayedColumns;" | |
[class.deleted-row]="row.deleted" | |
(click)="editAPI(row)"></tr> | |
</table> | |
} | |
</div> | |
`, | |
styles: [` | |
.apis-container { | |
.toolbar { | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
margin-bottom: 24px; | |
flex-wrap: wrap; | |
gap: 16px; | |
h2 { | |
margin: 0; | |
font-size: 24px; | |
} | |
.toolbar-actions { | |
display: flex; | |
gap: 16px; | |
align-items: center; | |
flex-wrap: wrap; | |
.search-field { | |
width: 250px; | |
} | |
} | |
} | |
.empty-state, .error-state { | |
text-align: center; | |
padding: 60px 20px; | |
background-color: white; | |
border-radius: 8px; | |
box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
mat-icon { | |
font-size: 64px; | |
width: 64px; | |
height: 64px; | |
color: #e0e0e0; | |
margin-bottom: 16px; | |
} | |
p { | |
margin-bottom: 24px; | |
color: #666; | |
font-size: 16px; | |
} | |
} | |
.error-state { | |
mat-icon { | |
color: #f44336; | |
} | |
} | |
.apis-table { | |
width: 100%; | |
background: white; | |
box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
.url-cell { | |
max-width: 300px; | |
span { | |
overflow: hidden; | |
text-overflow: ellipsis; | |
white-space: nowrap; | |
display: block; | |
} | |
} | |
mat-chip { | |
font-size: 12px; | |
min-height: 24px; | |
padding: 4px 12px; | |
&.method-get { background-color: #4caf50; color: white; } | |
&.method-post { background-color: #2196f3; color: white; } | |
&.method-put { background-color: #ff9800; color: white; } | |
&.method-patch { background-color: #9c27b0; color: white; } | |
&.method-delete { background-color: #f44336; color: white; } | |
} | |
tr.mat-mdc-row { | |
cursor: pointer; | |
transition: background-color 0.2s; | |
&:hover { | |
background-color: #f5f5f5; | |
} | |
&.deleted-row { | |
opacity: 0.6; | |
background-color: #fafafa; | |
cursor: default; | |
} | |
} | |
mat-spinner { | |
display: inline-block; | |
} | |
} | |
} | |
::ng-deep { | |
.mat-mdc-form-field { | |
font-size: 14px; | |
} | |
.mat-mdc-checkbox { | |
.mdc-form-field { | |
font-size: 14px; | |
} | |
} | |
} | |
`] | |
}) | |
export class ApisComponent implements OnInit, OnDestroy { | |
private apiService = inject(ApiService); | |
private dialog = inject(MatDialog); | |
private snackBar = inject(MatSnackBar); | |
private destroyed$ = new Subject<void>(); | |
apis: API[] = []; | |
filteredAPIs: API[] = []; | |
loading = true; | |
error = ''; | |
showDeleted = false; | |
searchTerm = ''; | |
actionLoading: { [key: string]: boolean } = {}; | |
displayedColumns: string[] = ['name', 'url', 'method', 'timeout', 'auth', 'deleted', 'actions']; | |
ngOnInit() { | |
this.loadAPIs(); | |
} | |
ngOnDestroy() { | |
this.destroyed$.next(); | |
this.destroyed$.complete(); | |
} | |
loadAPIs() { | |
this.loading = true; | |
this.error = ''; | |
this.apiService.getAPIs(this.showDeleted).pipe( | |
takeUntil(this.destroyed$) | |
).subscribe({ | |
next: (apis) => { | |
this.apis = apis; | |
this.filterAPIs(); | |
this.loading = false; | |
}, | |
error: (err) => { | |
this.error = this.getErrorMessage(err); | |
this.snackBar.open(this.error, 'Close', { | |
duration: 5000, | |
panelClass: 'error-snackbar' | |
}); | |
this.loading = false; | |
} | |
}); | |
} | |
filterAPIs() { | |
const term = this.searchTerm.toLowerCase().trim(); | |
if (!term) { | |
this.filteredAPIs = [...this.apis]; | |
} else { | |
this.filteredAPIs = this.apis.filter(api => | |
api.name.toLowerCase().includes(term) || | |
api.url.toLowerCase().includes(term) || | |
api.method.toLowerCase().includes(term) | |
); | |
} | |
} | |
async createAPI() { | |
try { | |
const { default: ApiEditDialogComponent } = await import('../../dialogs/api-edit-dialog/api-edit-dialog.component'); | |
const dialogRef = this.dialog.open(ApiEditDialogComponent, { | |
width: '800px', | |
data: { mode: 'create' }, | |
disableClose: true | |
}); | |
dialogRef.afterClosed().pipe( | |
takeUntil(this.destroyed$) | |
).subscribe((result: any) => { | |
if (result) { | |
this.loadAPIs(); | |
} | |
}); | |
} catch (error) { | |
console.error('Failed to load dialog:', error); | |
this.snackBar.open('Failed to open dialog', 'Close', { | |
duration: 3000, | |
panelClass: 'error-snackbar' | |
}); | |
} | |
} | |
async editAPI(api: API) { | |
if (api.deleted) return; | |
try { | |
const { default: ApiEditDialogComponent } = await import('../../dialogs/api-edit-dialog/api-edit-dialog.component'); | |
const dialogRef = this.dialog.open(ApiEditDialogComponent, { | |
width: '800px', | |
data: { mode: 'edit', api: { ...api } }, | |
disableClose: true | |
}); | |
dialogRef.afterClosed().pipe( | |
takeUntil(this.destroyed$) | |
).subscribe((result: any) => { | |
if (result) { | |
this.loadAPIs(); | |
} | |
}); | |
} catch (error) { | |
console.error('Failed to load dialog:', error); | |
this.snackBar.open('Failed to open dialog', 'Close', { | |
duration: 3000, | |
panelClass: 'error-snackbar' | |
}); | |
} | |
} | |
async testAPI(api: API) { | |
try { | |
const { default: ApiEditDialogComponent } = await import('../../dialogs/api-edit-dialog/api-edit-dialog.component'); | |
const dialogRef = this.dialog.open(ApiEditDialogComponent, { | |
width: '800px', | |
data: { | |
mode: 'test', | |
api: { ...api }, | |
activeTab: 4 | |
}, | |
disableClose: false | |
}); | |
dialogRef.afterClosed().pipe( | |
takeUntil(this.destroyed$) | |
).subscribe((result: any) => { | |
if (result) { | |
this.loadAPIs(); | |
} | |
}); | |
} catch (error) { | |
console.error('Failed to load dialog:', error); | |
this.snackBar.open('Failed to open dialog', 'Close', { | |
duration: 3000, | |
panelClass: 'error-snackbar' | |
}); | |
} | |
} | |
async duplicateAPI(api: API) { | |
try { | |
const { default: ApiEditDialogComponent } = await import('../../dialogs/api-edit-dialog/api-edit-dialog.component'); | |
const duplicatedApi = { ...api }; | |
duplicatedApi.name = `${api.name}_copy`; | |
delete (duplicatedApi as any).last_update_date; | |
const dialogRef = this.dialog.open(ApiEditDialogComponent, { | |
width: '800px', | |
data: { mode: 'duplicate', api: duplicatedApi }, | |
disableClose: true | |
}); | |
dialogRef.afterClosed().pipe( | |
takeUntil(this.destroyed$) | |
).subscribe((result: any) => { | |
if (result) { | |
this.loadAPIs(); | |
} | |
}); | |
} catch (error) { | |
console.error('Failed to load dialog:', error); | |
this.snackBar.open('Failed to open dialog', 'Close', { | |
duration: 3000, | |
panelClass: 'error-snackbar' | |
}); | |
} | |
} | |
async deleteAPI(api: API) { | |
if (api.deleted) return; | |
try { | |
const { default: ConfirmDialogComponent } = await import('../../dialogs/confirm-dialog/confirm-dialog.component'); | |
const dialogRef = this.dialog.open(ConfirmDialogComponent, { | |
width: '400px', | |
data: { | |
title: 'Delete API', | |
message: `Are you sure you want to delete "${api.name}"?`, | |
confirmText: 'Delete', | |
confirmColor: 'warn' | |
} | |
}); | |
dialogRef.afterClosed().pipe( | |
takeUntil(this.destroyed$) | |
).subscribe((confirmed) => { | |
if (confirmed) { | |
this.actionLoading[api.name] = true; | |
this.apiService.deleteAPI(api.name).pipe( | |
takeUntil(this.destroyed$) | |
).subscribe({ | |
next: () => { | |
this.snackBar.open(`API "${api.name}" deleted successfully`, 'Close', { | |
duration: 3000 | |
}); | |
this.loadAPIs(); | |
}, | |
error: (err) => { | |
const errorMsg = this.getErrorMessage(err); | |
this.snackBar.open(errorMsg, 'Close', { | |
duration: 5000, | |
panelClass: 'error-snackbar' | |
}); | |
this.actionLoading[api.name] = false; | |
} | |
}); | |
} | |
}); | |
} catch (error) { | |
console.error('Failed to load dialog:', error); | |
this.snackBar.open('Failed to open dialog', 'Close', { | |
duration: 3000, | |
panelClass: 'error-snackbar' | |
}); | |
} | |
} | |
async restoreAPI(api: API) { | |
if (!api.deleted) return; | |
// Implement restore API functionality | |
this.snackBar.open('Restore functionality not implemented yet', 'Close', { | |
duration: 3000 | |
}); | |
} | |
async importAPIs() { | |
const input = document.createElement('input'); | |
input.type = 'file'; | |
input.accept = '.json'; | |
input.onchange = async (event: any) => { | |
const file = event.target.files[0]; | |
if (!file) return; | |
try { | |
const text = await file.text(); | |
let apis: any[]; | |
try { | |
apis = JSON.parse(text); | |
} catch (parseError) { | |
this.snackBar.open('Invalid JSON file format', 'Close', { | |
duration: 5000, | |
panelClass: 'error-snackbar' | |
}); | |
return; | |
} | |
if (!Array.isArray(apis)) { | |
this.snackBar.open('Invalid file format. Expected an array of APIs.', 'Close', { | |
duration: 5000, | |
panelClass: 'error-snackbar' | |
}); | |
return; | |
} | |
this.loading = true; | |
let imported = 0; | |
let failed = 0; | |
const errors: string[] = []; | |
console.log('Starting API import, total APIs:', apis.length); | |
for (const api of apis) { | |
try { | |
await this.apiService.createAPI(api).toPromise(); | |
imported++; | |
} catch (err: any) { | |
failed++; | |
const apiName = api.name || 'unnamed'; | |
console.error(`❌ Failed to import API ${apiName}:`, err); | |
// Parse error message - daha iyi hata mesajı parse etme | |
let errorMsg = 'Unknown error'; | |
if (err.status === 409) { | |
// DuplicateResourceError durumu | |
errorMsg = `API with name '${apiName}' already exists`; | |
} else if (err.status === 500 && err.error?.detail?.includes('already exists')) { | |
// Backend'den gelen duplicate hatası | |
errorMsg = `API with name '${apiName}' already exists`; | |
} else if (err.error?.message) { | |
errorMsg = err.error.message; | |
} else if (err.error?.detail) { | |
errorMsg = err.error.detail; | |
} else if (err.message) { | |
errorMsg = err.message; | |
} | |
errors.push(`${apiName}: ${errorMsg}`); | |
} | |
} | |
this.loading = false; | |
if (imported > 0) { | |
this.loadAPIs(); | |
} | |
// Always show dialog for import results | |
try { | |
await this.showImportResultsDialog(imported, failed, errors); | |
} catch (dialogError) { | |
console.error('Failed to show import dialog:', dialogError); | |
// Fallback to snackbar | |
this.showImportResultsSnackbar(imported, failed, errors); | |
} | |
} catch (error) { | |
this.loading = false; | |
this.snackBar.open('Failed to read file', 'Close', { | |
duration: 5000, | |
panelClass: 'error-snackbar' | |
}); | |
} | |
}; | |
input.click(); | |
} | |
private async showImportResultsDialog(imported: number, failed: number, errors: string[]) { | |
try { | |
const { default: ImportResultsDialogComponent } = await import('../../dialogs/import-results-dialog/import-results-dialog.component'); | |
this.dialog.open(ImportResultsDialogComponent, { | |
width: '600px', | |
data: { | |
title: 'API Import Results', | |
imported, | |
failed, | |
errors | |
} | |
}); | |
} catch (error) { | |
// Fallback to alert if dialog fails to load | |
alert(`Imported: ${imported}\nFailed: ${failed}\n\nErrors:\n${errors.join('\n')}`); | |
} | |
} | |
// Fallback method | |
private showImportResultsSnackbar(imported: number, failed: number, errors: string[]) { | |
let message = ''; | |
if (imported > 0) { | |
message = `Successfully imported ${imported} API${imported > 1 ? 's' : ''}.`; | |
} | |
if (failed > 0) { | |
if (message) message += '\n\n'; | |
message += `Failed to import ${failed} API${failed > 1 ? 's' : ''}:\n`; | |
message += errors.slice(0, 5).join('\n'); | |
if (errors.length > 5) { | |
message += `\n... and ${errors.length - 5} more errors`; | |
} | |
} | |
this.snackBar.open(message, 'Close', { | |
duration: 10000, | |
panelClass: ['multiline-snackbar', failed > 0 ? 'error-snackbar' : 'success-snackbar'], | |
verticalPosition: 'top', | |
horizontalPosition: 'right' | |
}); | |
} | |
exportAPIs() { | |
const selectedAPIs = this.filteredAPIs.filter(api => !api.deleted); | |
if (selectedAPIs.length === 0) { | |
this.snackBar.open('No APIs to export', 'Close', { | |
duration: 3000 | |
}); | |
return; | |
} | |
try { | |
const data = JSON.stringify(selectedAPIs, null, 2); | |
const blob = new Blob([data], { type: 'application/json' }); | |
const url = window.URL.createObjectURL(blob); | |
const link = document.createElement('a'); | |
link.href = url; | |
link.download = `apis_export_${new Date().getTime()}.json`; | |
link.click(); | |
window.URL.revokeObjectURL(url); | |
this.snackBar.open(`Exported ${selectedAPIs.length} APIs`, 'Close', { | |
duration: 3000 | |
}); | |
} catch (error) { | |
console.error('Export failed:', error); | |
this.snackBar.open('Failed to export APIs', 'Close', { | |
duration: 5000, | |
panelClass: 'error-snackbar' | |
}); | |
} | |
} | |
private getErrorMessage(error: any): string { | |
if (error.status === 0) { | |
return 'Unable to connect to server. Please check your connection.'; | |
} else if (error.status === 401) { | |
return 'Session expired. Please login again.'; | |
} else if (error.status === 403) { | |
return 'You do not have permission to perform this action.'; | |
} else if (error.status === 409) { | |
return 'This API was modified by another user. Please refresh and try again.'; | |
} else if (error.error?.detail) { | |
return error.error.detail; | |
} else if (error.message) { | |
return error.message; | |
} | |
return 'An unexpected error occurred. Please try again.'; | |
} | |
} |