ciyidogan commited on
Commit
7a224f7
·
verified ·
1 Parent(s): f7560e5

Upload 22 files

Browse files
flare-ui/src/app/components/apis/apis.component.ts ADDED
@@ -0,0 +1,299 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Component, inject, OnInit } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { FormsModule } from '@angular/forms';
4
+ import { ApiService, API } from '../../services/api.service';
5
+
6
+ @Component({
7
+ selector: 'app-apis',
8
+ standalone: true,
9
+ imports: [CommonModule, FormsModule],
10
+ template: `
11
+ <div class="apis-container">
12
+ <div class="toolbar">
13
+ <h2>API Definitions</h2>
14
+ <div class="toolbar-actions">
15
+ <button class="btn btn-primary" (click)="createAPI()">
16
+ New API
17
+ </button>
18
+ <button class="btn btn-secondary" disabled>
19
+ Import
20
+ </button>
21
+ <button class="btn btn-secondary" disabled>
22
+ Export
23
+ </button>
24
+ <input
25
+ type="text"
26
+ placeholder="Search APIs..."
27
+ [(ngModel)]="searchTerm"
28
+ (input)="filterAPIs()"
29
+ class="search-input"
30
+ >
31
+ <label class="checkbox-label">
32
+ <input
33
+ type="checkbox"
34
+ [(ngModel)]="showDeleted"
35
+ (change)="loadAPIs()"
36
+ >
37
+ Display Deleted
38
+ </label>
39
+ </div>
40
+ </div>
41
+
42
+ @if (loading) {
43
+ <div class="loading">
44
+ <span class="spinner"></span> Loading APIs...
45
+ </div>
46
+ } @else if (filteredAPIs.length === 0) {
47
+ <div class="empty-state">
48
+ <p>No APIs found.</p>
49
+ <button class="btn btn-primary" (click)="createAPI()">
50
+ Create your first API
51
+ </button>
52
+ </div>
53
+ } @else {
54
+ <table class="table">
55
+ <thead>
56
+ <tr>
57
+ <th>Name</th>
58
+ <th>URL</th>
59
+ <th>Method</th>
60
+ <th>Timeout</th>
61
+ <th>Auth</th>
62
+ <th>Deleted</th>
63
+ <th>Actions</th>
64
+ </tr>
65
+ </thead>
66
+ <tbody>
67
+ @for (api of filteredAPIs; track api.name) {
68
+ <tr [class.deleted]="api.deleted">
69
+ <td>{{ api.name }}</td>
70
+ <td class="url-cell">{{ api.url }}</td>
71
+ <td>
72
+ <span class="method-badge" [class]="'method-' + api.method.toLowerCase()">
73
+ {{ api.method }}
74
+ </span>
75
+ </td>
76
+ <td>{{ api.timeout_seconds }}s</td>
77
+ <td>
78
+ @if (api.auth?.enabled) {
79
+ <span class="status-badge enabled">✓</span>
80
+ } @else {
81
+ <span class="status-badge">✗</span>
82
+ }
83
+ </td>
84
+ <td>
85
+ @if (api.deleted) {
86
+ <span class="status-badge deleted">✓</span>
87
+ } @else {
88
+ <span class="status-badge">✗</span>
89
+ }
90
+ </td>
91
+ <td class="actions">
92
+ <button class="action-btn" title="Edit" (click)="editAPI(api)">
93
+ 🖊️
94
+ </button>
95
+ <button class="action-btn" title="Test" (click)="testAPI(api)">
96
+ 🧪
97
+ </button>
98
+ <button class="action-btn" title="Duplicate" (click)="duplicateAPI(api)">
99
+ 📋
100
+ </button>
101
+ @if (!api.deleted) {
102
+ <button class="action-btn danger" title="Delete" (click)="deleteAPI(api)">
103
+ 🗑️
104
+ </button>
105
+ }
106
+ </td>
107
+ </tr>
108
+ }
109
+ </tbody>
110
+ </table>
111
+ }
112
+
113
+ @if (message) {
114
+ <div class="alert" [class.alert-success]="!isError" [class.alert-danger]="isError">
115
+ {{ message }}
116
+ </div>
117
+ }
118
+ </div>
119
+ `,
120
+ styles: [`
121
+ .apis-container {
122
+ .toolbar {
123
+ display: flex;
124
+ justify-content: space-between;
125
+ align-items: center;
126
+ margin-bottom: 1.5rem;
127
+
128
+ h2 {
129
+ margin: 0;
130
+ }
131
+
132
+ .toolbar-actions {
133
+ display: flex;
134
+ gap: 0.5rem;
135
+ align-items: center;
136
+ }
137
+ }
138
+
139
+ .search-input {
140
+ padding: 0.375rem 0.75rem;
141
+ border: 1px solid #ced4da;
142
+ border-radius: 0.25rem;
143
+ width: 200px;
144
+ }
145
+
146
+ .checkbox-label {
147
+ display: flex;
148
+ align-items: center;
149
+ gap: 0.25rem;
150
+ cursor: pointer;
151
+ }
152
+
153
+ .loading, .empty-state {
154
+ text-align: center;
155
+ padding: 3rem;
156
+ background-color: white;
157
+ border-radius: 0.25rem;
158
+
159
+ p {
160
+ margin-bottom: 1rem;
161
+ color: #6c757d;
162
+ }
163
+ }
164
+
165
+ .url-cell {
166
+ max-width: 300px;
167
+ overflow: hidden;
168
+ text-overflow: ellipsis;
169
+ white-space: nowrap;
170
+ }
171
+
172
+ .method-badge {
173
+ padding: 0.25rem 0.5rem;
174
+ border-radius: 0.25rem;
175
+ font-size: 0.75rem;
176
+ font-weight: 600;
177
+
178
+ &.method-get { background-color: #28a745; color: white; }
179
+ &.method-post { background-color: #007bff; color: white; }
180
+ &.method-put { background-color: #ffc107; color: #333; }
181
+ &.method-patch { background-color: #17a2b8; color: white; }
182
+ &.method-delete { background-color: #dc3545; color: white; }
183
+ }
184
+
185
+ .status-badge {
186
+ &.enabled { color: #28a745; }
187
+ &.deleted { color: #dc3545; }
188
+ }
189
+
190
+ .actions {
191
+ display: flex;
192
+ gap: 0.25rem;
193
+ }
194
+
195
+ .action-btn {
196
+ background: none;
197
+ border: none;
198
+ cursor: pointer;
199
+ font-size: 1.1rem;
200
+ padding: 0.25rem;
201
+ border-radius: 0.25rem;
202
+
203
+ &:hover {
204
+ background-color: #f8f9fa;
205
+ }
206
+
207
+ &.danger:hover {
208
+ background-color: #f8d7da;
209
+ }
210
+ }
211
+
212
+ tr.deleted {
213
+ opacity: 0.6;
214
+ background-color: #f8f9fa;
215
+ }
216
+ }
217
+ `]
218
+ })
219
+ export class ApisComponent implements OnInit {
220
+ private apiService = inject(ApiService);
221
+
222
+ apis: API[] = [];
223
+ filteredAPIs: API[] = [];
224
+ loading = true;
225
+ showDeleted = false;
226
+ searchTerm = '';
227
+ message = '';
228
+ isError = false;
229
+
230
+ ngOnInit() {
231
+ this.loadAPIs();
232
+ }
233
+
234
+ loadAPIs() {
235
+ this.loading = true;
236
+ this.apiService.getAPIs(this.showDeleted).subscribe({
237
+ next: (apis) => {
238
+ this.apis = apis;
239
+ this.filterAPIs();
240
+ this.loading = false;
241
+ },
242
+ error: (err) => {
243
+ this.showMessage('Failed to load APIs', true);
244
+ this.loading = false;
245
+ }
246
+ });
247
+ }
248
+
249
+ filterAPIs() {
250
+ const term = this.searchTerm.toLowerCase();
251
+ this.filteredAPIs = this.apis.filter(api =>
252
+ api.name.toLowerCase().includes(term) ||
253
+ api.url.toLowerCase().includes(term)
254
+ );
255
+ }
256
+
257
+ createAPI() {
258
+ // TODO: Open create dialog
259
+ console.log('Create API - not implemented yet');
260
+ }
261
+
262
+ editAPI(api: API) {
263
+ // TODO: Open edit dialog
264
+ console.log('Edit API:', api.name);
265
+ }
266
+
267
+ testAPI(api: API) {
268
+ // TODO: Test API
269
+ console.log('Test API:', api.name);
270
+ }
271
+
272
+ duplicateAPI(api: API) {
273
+ // TODO: Duplicate API
274
+ console.log('Duplicate API:', api.name);
275
+ }
276
+
277
+ deleteAPI(api: API) {
278
+ if (confirm(`Are you sure you want to delete "${api.name}"?`)) {
279
+ this.apiService.deleteAPI(api.name).subscribe({
280
+ next: () => {
281
+ this.showMessage(`API "${api.name}" deleted successfully`, false);
282
+ this.loadAPIs();
283
+ },
284
+ error: (err) => {
285
+ this.showMessage(err.error?.detail || 'Failed to delete API', true);
286
+ }
287
+ });
288
+ }
289
+ }
290
+
291
+ private showMessage(message: string, isError: boolean) {
292
+ this.message = message;
293
+ this.isError = isError;
294
+
295
+ setTimeout(() => {
296
+ this.message = '';
297
+ }, 5000);
298
+ }
299
+ }
flare-ui/src/app/components/projects/projects.component.ts ADDED
@@ -0,0 +1,425 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Component, inject, OnInit } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { FormsModule } from '@angular/forms';
4
+ import { ApiService, Project } from '../../services/api.service';
5
+
6
+ @Component({
7
+ selector: 'app-projects',
8
+ standalone: true,
9
+ imports: [CommonModule, FormsModule],
10
+ template: `
11
+ <div class="projects-container">
12
+ <div class="toolbar">
13
+ <h2>Projects</h2>
14
+ <div class="toolbar-actions">
15
+ <button class="btn btn-primary" (click)="createProject()">
16
+ New Project
17
+ </button>
18
+ <button class="btn btn-secondary" disabled>
19
+ Import Project
20
+ </button>
21
+ <input
22
+ type="text"
23
+ placeholder="Search projects..."
24
+ [(ngModel)]="searchTerm"
25
+ (input)="filterProjects()"
26
+ class="search-input"
27
+ >
28
+ <label class="checkbox-label">
29
+ <input
30
+ type="checkbox"
31
+ [(ngModel)]="showDeleted"
32
+ (change)="loadProjects()"
33
+ >
34
+ Display Deleted
35
+ </label>
36
+ <div class="view-toggle">
37
+ <button [class.active]="viewMode === 'card'" (click)="viewMode = 'card'">
38
+ Card
39
+ </button>
40
+ <button [class.active]="viewMode === 'list'" (click)="viewMode = 'list'">
41
+ List
42
+ </button>
43
+ </div>
44
+ </div>
45
+ </div>
46
+
47
+ @if (loading) {
48
+ <div class="loading">
49
+ <span class="spinner"></span> Loading projects...
50
+ </div>
51
+ } @else if (filteredProjects.length === 0) {
52
+ <div class="empty-state">
53
+ <p>No projects found.</p>
54
+ <button class="btn btn-primary" (click)="createProject()">
55
+ Create your first project
56
+ </button>
57
+ </div>
58
+ } @else {
59
+ @if (viewMode === 'card') {
60
+ <div class="project-cards">
61
+ @for (project of filteredProjects; track project.id) {
62
+ <div class="project-card" [class.disabled]="!project.enabled" [class.deleted]="project.deleted">
63
+ <div class="project-icon">🛩️</div>
64
+ <h3>{{ project.name }}</h3>
65
+ <p>{{ project.caption || 'No description' }}</p>
66
+ <div class="project-meta">
67
+ <span>Versions: {{ project.versions?.length || 0 }} ({{ getPublishedCount(project) }} published)</span>
68
+ <span>Status: {{ project.enabled ? '✓ Enabled' : '✗ Disabled' }}</span>
69
+ <span>Last update: {{ getRelativeTime(project.last_update_date) }}</span>
70
+ </div>
71
+ <div class="project-actions">
72
+ <button class="btn btn-secondary" (click)="editProject(project)">Edit</button>
73
+ <button class="btn btn-secondary" (click)="manageVersions(project)">Versions</button>
74
+ <button class="btn btn-secondary" (click)="exportProject(project)">Export</button>
75
+ <button class="btn btn-secondary" (click)="toggleProject(project)">
76
+ {{ project.enabled ? 'Disable' : 'Enable' }}
77
+ </button>
78
+ </div>
79
+ </div>
80
+ }
81
+ </div>
82
+ } @else {
83
+ <table class="table">
84
+ <thead>
85
+ <tr>
86
+ <th>Name</th>
87
+ <th>Caption</th>
88
+ <th>Versions</th>
89
+ <th>Enabled</th>
90
+ <th>Deleted</th>
91
+ <th>Last Update</th>
92
+ <th>Actions</th>
93
+ </tr>
94
+ </thead>
95
+ <tbody>
96
+ @for (project of filteredProjects; track project.id) {
97
+ <tr [class.deleted]="project.deleted">
98
+ <td>{{ project.name }}</td>
99
+ <td>{{ project.caption || '-' }}</td>
100
+ <td>{{ project.versions?.length || 0 }} ({{ getPublishedCount(project) }} published)</td>
101
+ <td>
102
+ @if (project.enabled) {
103
+ <span class="status-badge enabled">✓</span>
104
+ } @else {
105
+ <span class="status-badge">✗</span>
106
+ }
107
+ </td>
108
+ <td>
109
+ @if (project.deleted) {
110
+ <span class="status-badge deleted">✓</span>
111
+ } @else {
112
+ <span class="status-badge">✗</span>
113
+ }
114
+ </td>
115
+ <td>{{ getRelativeTime(project.last_update_date) }}</td>
116
+ <td class="actions">
117
+ <button class="action-btn" title="Edit" (click)="editProject(project)">🖊️</button>
118
+ <button class="action-btn" title="Versions" (click)="manageVersions(project)">📋</button>
119
+ <button class="action-btn" title="Export" (click)="exportProject(project)">📤</button>
120
+ @if (!project.deleted) {
121
+ <button class="action-btn danger" title="Delete" (click)="deleteProject(project)">🗑️</button>
122
+ }
123
+ </td>
124
+ </tr>
125
+ }
126
+ </tbody>
127
+ </table>
128
+ }
129
+ }
130
+
131
+ @if (message) {
132
+ <div class="alert" [class.alert-success]="!isError" [class.alert-danger]="isError">
133
+ {{ message }}
134
+ </div>
135
+ }
136
+ </div>
137
+ `,
138
+ styles: [`
139
+ .projects-container {
140
+ .toolbar {
141
+ display: flex;
142
+ justify-content: space-between;
143
+ align-items: center;
144
+ margin-bottom: 1.5rem;
145
+
146
+ h2 {
147
+ margin: 0;
148
+ }
149
+
150
+ .toolbar-actions {
151
+ display: flex;
152
+ gap: 0.5rem;
153
+ align-items: center;
154
+ }
155
+ }
156
+
157
+ .search-input {
158
+ padding: 0.375rem 0.75rem;
159
+ border: 1px solid #ced4da;
160
+ border-radius: 0.25rem;
161
+ width: 200px;
162
+ }
163
+
164
+ .checkbox-label {
165
+ display: flex;
166
+ align-items: center;
167
+ gap: 0.25rem;
168
+ cursor: pointer;
169
+ }
170
+
171
+ .view-toggle {
172
+ display: flex;
173
+ border: 1px solid #ced4da;
174
+ border-radius: 0.25rem;
175
+ overflow: hidden;
176
+
177
+ button {
178
+ background: white;
179
+ border: none;
180
+ padding: 0.375rem 0.75rem;
181
+ cursor: pointer;
182
+
183
+ &.active {
184
+ background-color: #007bff;
185
+ color: white;
186
+ }
187
+ }
188
+ }
189
+
190
+ .loading, .empty-state {
191
+ text-align: center;
192
+ padding: 3rem;
193
+ background-color: white;
194
+ border-radius: 0.25rem;
195
+
196
+ p {
197
+ margin-bottom: 1rem;
198
+ color: #6c757d;
199
+ }
200
+ }
201
+
202
+ .project-cards {
203
+ display: grid;
204
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
205
+ gap: 1rem;
206
+ }
207
+
208
+ .project-card {
209
+ background: white;
210
+ border: 1px solid #dee2e6;
211
+ border-radius: 0.5rem;
212
+ padding: 1.5rem;
213
+
214
+ &:hover {
215
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
216
+ }
217
+
218
+ &.disabled {
219
+ opacity: 0.7;
220
+ }
221
+
222
+ &.deleted {
223
+ background-color: #f8f9fa;
224
+ }
225
+
226
+ .project-icon {
227
+ font-size: 2rem;
228
+ margin-bottom: 0.5rem;
229
+ }
230
+
231
+ h3 {
232
+ margin: 0 0 0.5rem 0;
233
+ font-size: 1.25rem;
234
+ }
235
+
236
+ p {
237
+ color: #6c757d;
238
+ margin-bottom: 1rem;
239
+ }
240
+
241
+ .project-meta {
242
+ font-size: 0.875rem;
243
+ color: #6c757d;
244
+ margin-bottom: 1rem;
245
+
246
+ span {
247
+ display: block;
248
+ margin-bottom: 0.25rem;
249
+ }
250
+ }
251
+
252
+ .project-actions {
253
+ display: flex;
254
+ gap: 0.5rem;
255
+ flex-wrap: wrap;
256
+
257
+ button {
258
+ flex: 1;
259
+ min-width: 80px;
260
+ font-size: 0.875rem;
261
+ padding: 0.375rem 0.5rem;
262
+ }
263
+ }
264
+ }
265
+
266
+ .status-badge {
267
+ &.enabled { color: #28a745; }
268
+ &.deleted { color: #dc3545; }
269
+ }
270
+
271
+ .actions {
272
+ display: flex;
273
+ gap: 0.25rem;
274
+ }
275
+
276
+ .action-btn {
277
+ background: none;
278
+ border: none;
279
+ cursor: pointer;
280
+ font-size: 1.1rem;
281
+ padding: 0.25rem;
282
+ border-radius: 0.25rem;
283
+
284
+ &:hover {
285
+ background-color: #f8f9fa;
286
+ }
287
+
288
+ &.danger:hover {
289
+ background-color: #f8d7da;
290
+ }
291
+ }
292
+
293
+ tr.deleted {
294
+ opacity: 0.6;
295
+ background-color: #f8f9fa;
296
+ }
297
+ }
298
+ `]
299
+ })
300
+ export class ProjectsComponent implements OnInit {
301
+ private apiService = inject(ApiService);
302
+
303
+ projects: Project[] = [];
304
+ filteredProjects: Project[] = [];
305
+ loading = true;
306
+ showDeleted = false;
307
+ searchTerm = '';
308
+ viewMode: 'card' | 'list' = 'card';
309
+ message = '';
310
+ isError = false;
311
+
312
+ ngOnInit() {
313
+ this.loadProjects();
314
+ }
315
+
316
+ loadProjects() {
317
+ this.loading = true;
318
+ this.apiService.getProjects(this.showDeleted).subscribe({
319
+ next: (projects) => {
320
+ this.projects = projects;
321
+ this.filterProjects();
322
+ this.loading = false;
323
+ },
324
+ error: (err) => {
325
+ this.showMessage('Failed to load projects', true);
326
+ this.loading = false;
327
+ }
328
+ });
329
+ }
330
+
331
+ filterProjects() {
332
+ const term = this.searchTerm.toLowerCase();
333
+ this.filteredProjects = this.projects.filter(project =>
334
+ project.name.toLowerCase().includes(term) ||
335
+ (project.caption || '').toLowerCase().includes(term)
336
+ );
337
+ }
338
+
339
+ getPublishedCount(project: Project): number {
340
+ return project.versions?.filter(v => v.published).length || 0;
341
+ }
342
+
343
+ getRelativeTime(timestamp: string): string {
344
+ const date = new Date(timestamp);
345
+ const now = new Date();
346
+ const diff = now.getTime() - date.getTime();
347
+
348
+ const minutes = Math.floor(diff / 60000);
349
+ const hours = Math.floor(diff / 3600000);
350
+ const days = Math.floor(diff / 86400000);
351
+
352
+ if (minutes < 1) return 'just now';
353
+ if (minutes < 60) return `${minutes} min ago`;
354
+ if (hours < 24) return `${hours} hour${hours > 1 ? 's' : ''} ago`;
355
+ return `${days} day${days > 1 ? 's' : ''} ago`;
356
+ }
357
+
358
+ createProject() {
359
+ // TODO: Open create dialog
360
+ console.log('Create project - not implemented yet');
361
+ }
362
+
363
+ editProject(project: Project) {
364
+ // TODO: Open edit dialog
365
+ console.log('Edit project:', project.name);
366
+ }
367
+
368
+ manageVersions(project: Project) {
369
+ // TODO: Open versions dialog
370
+ console.log('Manage versions:', project.name);
371
+ }
372
+
373
+ exportProject(project: Project) {
374
+ this.apiService.exportProject(project.id).subscribe({
375
+ next: (data) => {
376
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
377
+ const url = window.URL.createObjectURL(blob);
378
+ const a = document.createElement('a');
379
+ a.href = url;
380
+ a.download = `${project.name}_export.json`;
381
+ a.click();
382
+ window.URL.revokeObjectURL(url);
383
+ this.showMessage(`Project "${project.name}" exported successfully`, false);
384
+ },
385
+ error: (err) => {
386
+ this.showMessage('Failed to export project', true);
387
+ }
388
+ });
389
+ }
390
+
391
+ toggleProject(project: Project) {
392
+ this.apiService.toggleProject(project.id).subscribe({
393
+ next: (result) => {
394
+ project.enabled = result.enabled;
395
+ this.showMessage(`Project "${project.name}" ${result.enabled ? 'enabled' : 'disabled'}`, false);
396
+ },
397
+ error: (err) => {
398
+ this.showMessage('Failed to toggle project', true);
399
+ }
400
+ });
401
+ }
402
+
403
+ deleteProject(project: Project) {
404
+ if (confirm(`Are you sure you want to delete "${project.name}"?`)) {
405
+ this.apiService.deleteProject(project.id).subscribe({
406
+ next: () => {
407
+ this.showMessage(`Project "${project.name}" deleted successfully`, false);
408
+ this.loadProjects();
409
+ },
410
+ error: (err) => {
411
+ this.showMessage(err.error?.detail || 'Failed to delete project', true);
412
+ }
413
+ });
414
+ }
415
+ }
416
+
417
+ private showMessage(message: string, isError: boolean) {
418
+ this.message = message;
419
+ this.isError = isError;
420
+
421
+ setTimeout(() => {
422
+ this.message = '';
423
+ }, 5000);
424
+ }
425
+ }
flare-ui/src/app/components/test/test.component.ts ADDED
@@ -0,0 +1,338 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Component, inject } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { ApiService } from '../../services/api.service';
4
+
5
+ interface TestResult {
6
+ name: string;
7
+ status: 'PASS' | 'FAIL' | 'RUNNING';
8
+ duration_ms?: number;
9
+ error?: string;
10
+ }
11
+
12
+ interface TestRun {
13
+ test_type: string;
14
+ start_time: string;
15
+ tests: TestResult[];
16
+ summary: {
17
+ total: number;
18
+ passed: number;
19
+ failed: number;
20
+ duration_ms: number;
21
+ };
22
+ }
23
+
24
+ @Component({
25
+ selector: 'app-test',
26
+ standalone: true,
27
+ imports: [CommonModule],
28
+ template: `
29
+ <div class="test-container">
30
+ <h2>System Tests</h2>
31
+
32
+ <div class="test-controls">
33
+ <button class="btn btn-primary" (click)="runAllTests()" [disabled]="running">
34
+ Run All Tests
35
+ </button>
36
+ <button class="btn btn-secondary" (click)="runSelectedTests()" [disabled]="running || selectedTests.length === 0">
37
+ Run Selected
38
+ </button>
39
+ <button class="btn btn-danger" (click)="stopTests()" [disabled]="!running">
40
+ Stop
41
+ </button>
42
+ </div>
43
+
44
+ <div class="test-categories">
45
+ <div class="category">
46
+ <label>
47
+ <input type="checkbox" [(ngModel)]="allSelected" (change)="toggleAll()">
48
+ <strong>All Tests</strong>
49
+ </label>
50
+ </div>
51
+ <div class="category">
52
+ <label>
53
+ <input type="checkbox" [(ngModel)]="categories.ui" (change)="updateSelection()">
54
+ UI Tests (15 tests)
55
+ </label>
56
+ <div class="sub-tests" *ngIf="categories.ui">
57
+ <label><input type="checkbox"> Login Flow</label>
58
+ <label><input type="checkbox"> Project CRUD</label>
59
+ <label><input type="checkbox"> Version Management</label>
60
+ </div>
61
+ </div>
62
+ <div class="category">
63
+ <label>
64
+ <input type="checkbox" [(ngModel)]="categories.backend" (change)="updateSelection()">
65
+ Backend Tests (22 tests)
66
+ </label>
67
+ <div class="sub-tests" *ngIf="categories.backend">
68
+ <label><input type="checkbox"> Authentication</label>
69
+ <label><input type="checkbox"> API Endpoints</label>
70
+ <label><input type="checkbox"> Race Conditions</label>
71
+ </div>
72
+ </div>
73
+ <div class="category">
74
+ <label>
75
+ <input type="checkbox" [(ngModel)]="categories.integration" (change)="updateSelection()">
76
+ Integration Tests (18 tests)
77
+ </label>
78
+ </div>
79
+ <div class="category">
80
+ <label>
81
+ <input type="checkbox" [(ngModel)]="categories.spark" (change)="updateSelection()">
82
+ Spark Tests (8 tests)
83
+ </label>
84
+ </div>
85
+ </div>
86
+
87
+ @if (currentRun) {
88
+ <div class="test-results">
89
+ <h3>Test Results:</h3>
90
+ <div class="results-list">
91
+ @for (test of currentRun.tests; track test.name) {
92
+ <div class="test-result" [class.pass]="test.status === 'PASS'" [class.fail]="test.status === 'FAIL'">
93
+ @if (test.status === 'PASS') {
94
+ <span class="status">✓</span>
95
+ } @else if (test.status === 'FAIL') {
96
+ <span class="status">✗</span>
97
+ } @else {
98
+ <span class="status spinner"></span>
99
+ }
100
+ <span class="name">{{ test.name }}</span>
101
+ @if (test.duration_ms) {
102
+ <span class="duration">{{ test.duration_ms }}ms</span>
103
+ }
104
+ @if (test.error) {
105
+ <div class="error">{{ test.error }}</div>
106
+ }
107
+ </div>
108
+ }
109
+ </div>
110
+
111
+ @if (!running && currentRun.summary) {
112
+ <div class="test-summary">
113
+ <div class="progress-bar">
114
+ <div
115
+ class="progress-fill"
116
+ [style.width.%]="(currentRun.summary.passed / currentRun.summary.total) * 100"
117
+ [class.success]="currentRun.summary.failed === 0"
118
+ [class.warning]="currentRun.summary.failed > 0"
119
+ ></div>
120
+ </div>
121
+ <div class="summary-text">
122
+ Progress: {{ currentRun.summary.passed + currentRun.summary.failed }}/{{ currentRun.summary.total }}
123
+ ({{ ((currentRun.summary.passed / currentRun.summary.total) * 100).toFixed(0) }}%)
124
+ </div>
125
+ <div class="summary-stats">
126
+ Passed: {{ currentRun.summary.passed }} |
127
+ Failed: {{ currentRun.summary.failed }} |
128
+ Total time: {{ (currentRun.summary.duration_ms / 1000).toFixed(1) }}s
129
+ </div>
130
+ </div>
131
+ }
132
+ </div>
133
+ }
134
+
135
+ @if (!currentRun && !running) {
136
+ <div class="empty-state">
137
+ <p>No test results yet. Click "Run All Tests" to start.</p>
138
+ </div>
139
+ }
140
+ </div>
141
+ `,
142
+ styles: [`
143
+ .test-container {
144
+ h2 {
145
+ margin-bottom: 1.5rem;
146
+ }
147
+ }
148
+
149
+ .test-controls {
150
+ display: flex;
151
+ gap: 0.5rem;
152
+ margin-bottom: 1.5rem;
153
+ }
154
+
155
+ .test-categories {
156
+ background: white;
157
+ border: 1px solid #dee2e6;
158
+ border-radius: 0.25rem;
159
+ padding: 1rem;
160
+ margin-bottom: 1.5rem;
161
+
162
+ .category {
163
+ margin-bottom: 0.5rem;
164
+
165
+ label {
166
+ display: flex;
167
+ align-items: center;
168
+ gap: 0.5rem;
169
+ cursor: pointer;
170
+
171
+ input[type="checkbox"] {
172
+ cursor: pointer;
173
+ }
174
+ }
175
+ }
176
+
177
+ .sub-tests {
178
+ margin-left: 1.5rem;
179
+ margin-top: 0.5rem;
180
+
181
+ label {
182
+ display: block;
183
+ margin-bottom: 0.25rem;
184
+ font-weight: normal;
185
+ color: #6c757d;
186
+ }
187
+ }
188
+ }
189
+
190
+ .test-results {
191
+ background: white;
192
+ border: 1px solid #dee2e6;
193
+ border-radius: 0.25rem;
194
+ padding: 1rem;
195
+
196
+ h3 {
197
+ margin-top: 0;
198
+ margin-bottom: 1rem;
199
+ }
200
+
201
+ .results-list {
202
+ max-height: 400px;
203
+ overflow-y: auto;
204
+ margin-bottom: 1rem;
205
+ }
206
+
207
+ .test-result {
208
+ display: flex;
209
+ align-items: center;
210
+ gap: 0.5rem;
211
+ padding: 0.5rem;
212
+ border-bottom: 1px solid #f0f0f0;
213
+
214
+ &.pass .status { color: #28a745; }
215
+ &.fail .status { color: #dc3545; }
216
+
217
+ .status {
218
+ width: 20px;
219
+ text-align: center;
220
+ }
221
+
222
+ .name {
223
+ flex: 1;
224
+ }
225
+
226
+ .duration {
227
+ color: #6c757d;
228
+ font-size: 0.875rem;
229
+ }
230
+
231
+ .error {
232
+ width: 100%;
233
+ margin-top: 0.5rem;
234
+ padding: 0.5rem;
235
+ background-color: #f8d7da;
236
+ color: #721c24;
237
+ border-radius: 0.25rem;
238
+ font-size: 0.875rem;
239
+ }
240
+ }
241
+ }
242
+
243
+ .test-summary {
244
+ border-top: 1px solid #dee2e6;
245
+ padding-top: 1rem;
246
+
247
+ .progress-bar {
248
+ height: 20px;
249
+ background-color: #e9ecef;
250
+ border-radius: 0.25rem;
251
+ overflow: hidden;
252
+ margin-bottom: 0.5rem;
253
+
254
+ .progress-fill {
255
+ height: 100%;
256
+ transition: width 0.3s ease;
257
+
258
+ &.success { background-color: #28a745; }
259
+ &.warning { background-color: #ffc107; }
260
+ }
261
+ }
262
+
263
+ .summary-text, .summary-stats {
264
+ text-align: center;
265
+ color: #6c757d;
266
+ margin-bottom: 0.5rem;
267
+ }
268
+ }
269
+
270
+ .empty-state {
271
+ text-align: center;
272
+ padding: 3rem;
273
+ background-color: white;
274
+ border-radius: 0.25rem;
275
+
276
+ p {
277
+ color: #6c757d;
278
+ }
279
+ }
280
+ `]
281
+ })
282
+ export class TestComponent {
283
+ private apiService = inject(ApiService);
284
+
285
+ running = false;
286
+ allSelected = false;
287
+ categories = {
288
+ ui: false,
289
+ backend: false,
290
+ integration: false,
291
+ spark: false
292
+ };
293
+ selectedTests: string[] = [];
294
+ currentRun: TestRun | null = null;
295
+
296
+ toggleAll() {
297
+ this.categories.ui = this.allSelected;
298
+ this.categories.backend = this.allSelected;
299
+ this.categories.integration = this.allSelected;
300
+ this.categories.spark = this.allSelected;
301
+ this.updateSelection();
302
+ }
303
+
304
+ updateSelection() {
305
+ this.selectedTests = [];
306
+ if (this.categories.ui) this.selectedTests.push('ui');
307
+ if (this.categories.backend) this.selectedTests.push('backend');
308
+ if (this.categories.integration) this.selectedTests.push('integration');
309
+ if (this.categories.spark) this.selectedTests.push('spark');
310
+
311
+ this.allSelected = this.selectedTests.length === 4;
312
+ }
313
+
314
+ runAllTests() {
315
+ this.running = true;
316
+ this.apiService.runTests('all').subscribe({
317
+ next: (result) => {
318
+ this.currentRun = result;
319
+ this.running = false;
320
+ },
321
+ error: (err) => {
322
+ console.error('Test run failed:', err);
323
+ this.running = false;
324
+ }
325
+ });
326
+ }
327
+
328
+ runSelectedTests() {
329
+ // TODO: Implement selected tests
330
+ console.log('Running selected tests:', this.selectedTests);
331
+ this.runAllTests(); // For now, just run all
332
+ }
333
+
334
+ stopTests() {
335
+ this.running = false;
336
+ // TODO: Implement stop functionality
337
+ }
338
+ }
flare-ui/src/app/components/user-info/user-info.component.ts ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Component } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { FormsModule } from '@angular/forms';
4
+
5
+ @Component({
6
+ selector: 'app-user-info',
7
+ standalone: true,
8
+ imports: [CommonModule, FormsModule],
9
+ template: `
10
+ <div class="user-info-container">
11
+ <h2>User Information</h2>
12
+
13
+ <div class="card">
14
+ <div class="card-body">
15
+ <p class="text-muted">Password change functionality coming soon...</p>
16
+
17
+ <form (ngSubmit)="changePassword()" #passwordForm="ngForm">
18
+ <div class="form-group">
19
+ <label for="currentPassword">Current Password</label>
20
+ <input
21
+ type="password"
22
+ id="currentPassword"
23
+ name="currentPassword"
24
+ [(ngModel)]="currentPassword"
25
+ required
26
+ disabled
27
+ >
28
+ </div>
29
+
30
+ <div class="form-group">
31
+ <label for="newPassword">New Password</label>
32
+ <input
33
+ type="password"
34
+ id="newPassword"
35
+ name="newPassword"
36
+ [(ngModel)]="newPassword"
37
+ required
38
+ disabled
39
+ >
40
+ </div>
41
+
42
+ <div class="form-group">
43
+ <label for="confirmPassword">Confirm New Password</label>
44
+ <input
45
+ type="password"
46
+ id="confirmPassword"
47
+ name="confirmPassword"
48
+ [(ngModel)]="confirmPassword"
49
+ required
50
+ disabled
51
+ >
52
+ </div>
53
+
54
+ <button type="submit" class="btn btn-primary" disabled>
55
+ Change Password
56
+ </button>
57
+ </form>
58
+ </div>
59
+ </div>
60
+ </div>
61
+ `,
62
+ styles: [`
63
+ .user-info-container {
64
+ h2 {
65
+ margin-bottom: 1.5rem;
66
+ }
67
+ }
68
+
69
+ .text-muted {
70
+ color: #6c757d;
71
+ margin-bottom: 1rem;
72
+ }
73
+ `]
74
+ })
75
+ export class UserInfoComponent {
76
+ currentPassword = '';
77
+ newPassword = '';
78
+ confirmPassword = '';
79
+
80
+ changePassword() {
81
+ console.log('Password change not implemented yet');
82
+ }
83
+ }