flare / flare-ui /src /app /components /apis /apis.component.ts
ciyidogan's picture
Update flare-ui/src/app/components/apis/apis.component.ts
47790b0 verified
raw
history blame
10.9 kB
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';
@Component({
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);
}
}