Spaces:
Running
Running
import { Component, inject, OnInit } from '@angular/core'; | |
import { CommonModule } from '@angular/common'; | |
import { FormsModule } from '@angular/forms'; | |
import { MatDialog, MatDialogModule } from '@angular/material/dialog'; | |
import { ApiService, API } from '../../services/api.service'; | |
({ | |
selector: 'app-apis', | |
standalone: true, | |
imports: [CommonModule, FormsModule, MatDialogModule], | |
template: ` | |
<div class="apis-container"> | |
<div class="toolbar"> | |
<h2>API Definitions</h2> | |
<div class="toolbar-actions"> | |
<button class="btn btn-primary" (click)="createAPI()"> | |
New API | |
</button> | |
<button class="btn btn-secondary" (click)="importAPIs()"> | |
Import | |
</button> | |
<button class="btn btn-secondary" (click)="exportAPIs()"> | |
Export | |
</button> | |
<input | |
type="text" | |
placeholder="Search APIs..." | |
[(ngModel)]="searchTerm" | |
(input)="filterAPIs()" | |
class="search-input" | |
> | |
<label class="checkbox-label"> | |
<input | |
type="checkbox" | |
[(ngModel)]="showDeleted" | |
(change)="loadAPIs()" | |
> | |
Display Deleted | |
</label> | |
</div> | |
</div> | |
@if (loading) { | |
<div class="loading"> | |
<span class="spinner"></span> Loading APIs... | |
</div> | |
} @else if (filteredAPIs.length === 0) { | |
<div class="empty-state"> | |
<p>No APIs found.</p> | |
<button class="btn btn-primary" (click)="createAPI()"> | |
Create your first API | |
</button> | |
</div> | |
} @else { | |
<table class="table"> | |
<thead> | |
<tr> | |
<th>Name</th> | |
<th>URL</th> | |
<th>Method</th> | |
<th>Timeout</th> | |
<th>Auth</th> | |
<th>Deleted</th> | |
<th>Actions</th> | |
</tr> | |
</thead> | |
<tbody> | |
@for (api of filteredAPIs; track api.name) { | |
<tr [class.deleted]="api.deleted"> | |
<td>{{ api.name }}</td> | |
<td class="url-cell">{{ api.url }}</td> | |
<td> | |
<span class="method-badge" [class]="'method-' + api.method.toLowerCase()"> | |
{{ api.method }} | |
</span> | |
</td> | |
<td>{{ api.timeout_seconds }}s</td> | |
<td> | |
@if (api.auth?.enabled) { | |
<span class="status-badge enabled">β</span> | |
} @else { | |
<span class="status-badge">β</span> | |
} | |
</td> | |
<td> | |
@if (api.deleted) { | |
<span class="status-badge deleted">β</span> | |
} @else { | |
<span class="status-badge">β</span> | |
} | |
</td> | |
<td class="actions"> | |
<button class="action-btn" title="Edit" (click)="editAPI(api)"> | |
ποΈ | |
</button> | |
<button class="action-btn" title="Test" (click)="testAPI(api)"> | |
π§ͺ | |
</button> | |
<button class="action-btn" title="Duplicate" (click)="duplicateAPI(api)"> | |
π | |
</button> | |
@if (!api.deleted) { | |
<button class="action-btn danger" title="Delete" (click)="deleteAPI(api)"> | |
ποΈ | |
</button> | |
} | |
</td> | |
</tr> | |
} | |
</tbody> | |
</table> | |
} | |
@if (message) { | |
<div class="alert" [class.alert-success]="!isError" [class.alert-danger]="isError"> | |
{{ message }} | |
</div> | |
} | |
</div> | |
`, | |
styles: [` | |
.apis-container { | |
.toolbar { | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
margin-bottom: 1.5rem; | |
h2 { | |
margin: 0; | |
} | |
.toolbar-actions { | |
display: flex; | |
gap: 0.5rem; | |
align-items: center; | |
} | |
} | |
.search-input { | |
padding: 0.375rem 0.75rem; | |
border: 1px solid #ced4da; | |
border-radius: 0.25rem; | |
width: 200px; | |
} | |
.checkbox-label { | |
display: flex; | |
align-items: center; | |
gap: 0.25rem; | |
cursor: pointer; | |
} | |
.loading, .empty-state { | |
text-align: center; | |
padding: 3rem; | |
background-color: white; | |
border-radius: 0.25rem; | |
p { | |
margin-bottom: 1rem; | |
color: #6c757d; | |
} | |
} | |
.url-cell { | |
max-width: 300px; | |
overflow: hidden; | |
text-overflow: ellipsis; | |
white-space: nowrap; | |
} | |
.method-badge { | |
padding: 0.25rem 0.5rem; | |
border-radius: 0.25rem; | |
font-size: 0.75rem; | |
font-weight: 600; | |
&.method-get { background-color: #28a745; color: white; } | |
&.method-post { background-color: #007bff; color: white; } | |
&.method-put { background-color: #ffc107; color: #333; } | |
&.method-patch { background-color: #17a2b8; color: white; } | |
&.method-delete { background-color: #dc3545; color: white; } | |
} | |
.status-badge { | |
&.enabled { color: #28a745; } | |
&.deleted { color: #dc3545; } | |
} | |
.actions { | |
display: flex; | |
gap: 0.25rem; | |
} | |
.action-btn { | |
background: none; | |
border: none; | |
cursor: pointer; | |
font-size: 1.1rem; | |
padding: 0.25rem; | |
border-radius: 0.25rem; | |
&:hover { | |
background-color: #f8f9fa; | |
} | |
&.danger:hover { | |
background-color: #f8d7da; | |
} | |
} | |
tr.deleted { | |
opacity: 0.6; | |
background-color: #f8f9fa; | |
} | |
} | |
`] | |
}) | |
export class ApisComponent implements OnInit { | |
private apiService = inject(ApiService); | |
private dialog = inject(MatDialog); | |
apis: API[] = []; | |
filteredAPIs: API[] = []; | |
loading = true; | |
showDeleted = false; | |
searchTerm = ''; | |
message = ''; | |
isError = false; | |
ngOnInit() { | |
this.loadAPIs(); | |
} | |
loadAPIs() { | |
this.loading = true; | |
this.apiService.getAPIs(this.showDeleted).subscribe({ | |
next: (apis) => { | |
this.apis = apis; | |
this.filterAPIs(); | |
this.loading = false; | |
}, | |
error: (err) => { | |
this.showMessage('Failed to load APIs', true); | |
this.loading = false; | |
} | |
}); | |
} | |
filterAPIs() { | |
const term = this.searchTerm.toLowerCase(); | |
this.filteredAPIs = this.apis.filter(api => | |
api.name.toLowerCase().includes(term) || | |
api.url.toLowerCase().includes(term) | |
); | |
} | |
async createAPI() { | |
const { default: ApiEditDialogComponent } = await import('../../dialogs/api-edit-dialog/api-edit-dialog.component'); | |
const dialogRef = this.dialog.open(ApiEditDialogComponent, { | |
width: '800px', | |
data: { mode: 'create' } | |
}); | |
dialogRef.afterClosed().subscribe((result: any) => { | |
if (result) { | |
this.loadAPIs(); | |
} | |
}); | |
} | |
async editAPI(api: API) { | |
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 } } | |
}); | |
dialogRef.afterClosed().subscribe((result: any) => { | |
if (result) { | |
this.loadAPIs(); | |
} | |
}); | |
} | |
testAPI(api: API) { | |
this.apiService.testAPI(api).subscribe({ | |
next: (result) => { | |
if (result.success) { | |
this.showMessage(`API test successful! Status: ${result.status_code}`, false); | |
} else { | |
this.showMessage(`API test failed: ${result.error}`, true); | |
} | |
}, | |
error: (err) => { | |
this.showMessage('Failed to test API', true); | |
} | |
}); | |
} | |
async duplicateAPI(api: API) { | |
const { default: ApiEditDialogComponent } = await import('../../dialogs/api-edit-dialog/api-edit-dialog.component'); | |
const dialogRef = this.dialog.open(ApiEditDialogComponent, { | |
width: '800px', | |
data: { mode: 'duplicate', api: { ...api } } | |
}); | |
dialogRef.afterClosed().subscribe((result: any) => { | |
if (result) { | |
this.loadAPIs(); | |
} | |
}); | |
} | |
deleteAPI(api: API) { | |
if (confirm(`Are you sure you want to delete "${api.name}"?`)) { | |
this.apiService.deleteAPI(api.name).subscribe({ | |
next: () => { | |
this.showMessage(`API "${api.name}" deleted successfully`, false); | |
this.loadAPIs(); | |
}, | |
error: (err) => { | |
this.showMessage(err.error?.detail || 'Failed to delete API', true); | |
} | |
}); | |
} | |
} | |
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(); | |
const apis = JSON.parse(text); | |
if (!Array.isArray(apis)) { | |
this.showMessage('Invalid file format. Expected an array of APIs.', true); | |
return; | |
} | |
let imported = 0; | |
let failed = 0; | |
for (const api of apis) { | |
try { | |
await this.apiService.createAPI(api).toPromise(); | |
imported++; | |
} catch (err: any) { | |
failed++; | |
console.error(`Failed to import API ${api.name}:`, err); | |
} | |
} | |
this.showMessage(`Imported ${imported} APIs. ${failed} failed.`, failed > 0); | |
this.loadAPIs(); | |
} catch (error) { | |
this.showMessage('Failed to read file', true); | |
} | |
}; | |
input.click(); | |
} | |
exportAPIs() { | |
const selectedAPIs = this.filteredAPIs.filter(api => !api.deleted); | |
if (selectedAPIs.length === 0) { | |
this.showMessage('No APIs to export', true); | |
return; | |
} | |
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.showMessage(`Exported ${selectedAPIs.length} APIs`, false); | |
} | |
private showMessage(message: string, isError: boolean) { | |
this.message = message; | |
this.isError = isError; | |
setTimeout(() => { | |
this.message = ''; | |
}, 5000); | |
} | |
} |