Spaces:
Paused
Paused
| 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.'; | |
| } | |
| } |