Spaces:
Running
Running
Update flare-ui/src/app/components/projects/projects.component.ts
Browse files
flare-ui/src/app/components/projects/projects.component.ts
CHANGED
@@ -1,4 +1,4 @@
|
|
1 |
-
import { Component, OnInit } from '@angular/core';
|
2 |
import { CommonModule } from '@angular/common';
|
3 |
import { FormsModule } from '@angular/forms';
|
4 |
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
|
@@ -16,9 +16,12 @@ import { MatIconModule } from '@angular/material/icon';
|
|
16 |
import { MatMenuModule } from '@angular/material/menu';
|
17 |
import { MatDividerModule } from '@angular/material/divider';
|
18 |
import { ApiService, Project } from '../../services/api.service';
|
19 |
-
import
|
20 |
-
|
21 |
-
|
|
|
|
|
|
|
22 |
|
23 |
@Component({
|
24 |
selector: 'app-projects',
|
@@ -44,7 +47,7 @@ import ConfirmDialogComponent from '../../dialogs/confirm-dialog/confirm-dialog.
|
|
44 |
templateUrl: './projects.component.html',
|
45 |
styleUrls: ['./projects.component.scss']
|
46 |
})
|
47 |
-
export class ProjectsComponent implements OnInit {
|
48 |
projects: Project[] = [];
|
49 |
filteredProjects: Project[] = [];
|
50 |
searchTerm = '';
|
@@ -56,6 +59,9 @@ export class ProjectsComponent implements OnInit {
|
|
56 |
|
57 |
// For table view
|
58 |
displayedColumns: string[] = ['name', 'caption', 'versions', 'status', 'lastUpdate', 'actions'];
|
|
|
|
|
|
|
59 |
|
60 |
constructor(
|
61 |
private apiService: ApiService,
|
@@ -68,6 +74,11 @@ export class ProjectsComponent implements OnInit {
|
|
68 |
this.loadEnvironment();
|
69 |
}
|
70 |
|
|
|
|
|
|
|
|
|
|
|
71 |
isSparkTabVisible(): boolean {
|
72 |
// Environment bilgisini cache'ten al (eğer varsa)
|
73 |
const env = localStorage.getItem('flare_environment');
|
@@ -78,27 +89,35 @@ export class ProjectsComponent implements OnInit {
|
|
78 |
return true; // Default olarak göster
|
79 |
}
|
80 |
|
81 |
-
|
82 |
this.loading = true;
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
91 |
}
|
92 |
|
93 |
private loadEnvironment() {
|
94 |
-
this.apiService.getEnvironment()
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
|
|
|
|
102 |
}
|
103 |
|
104 |
applyFilter() {
|
@@ -125,114 +144,175 @@ export class ProjectsComponent implements OnInit {
|
|
125 |
this.loadProjects();
|
126 |
}
|
127 |
|
128 |
-
createProject() {
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
139 |
}
|
140 |
|
141 |
-
editProject(project: Project) {
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
dialogRef.afterClosed().subscribe(result => {
|
148 |
-
if (result) {
|
149 |
-
// Listeyi güncelle
|
150 |
-
const index = this.projects.findIndex(p => p.id === result.id);
|
151 |
-
if (index !== -1) {
|
152 |
-
this.projects[index] = result;
|
153 |
-
this.applyFilter(); // Filtreyi yeniden uygula
|
154 |
-
} else {
|
155 |
-
this.loadProjects(); // Bulunamazsa tüm listeyi yenile
|
156 |
-
}
|
157 |
-
}
|
158 |
-
});
|
159 |
-
}
|
160 |
-
|
161 |
-
async toggleProject(project: Project) {
|
162 |
try {
|
163 |
-
const
|
164 |
-
|
165 |
-
this.
|
166 |
-
|
167 |
-
|
168 |
-
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
169 |
} catch (error) {
|
170 |
-
|
|
|
171 |
}
|
172 |
}
|
173 |
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
height: '90vh',
|
179 |
-
data: { project }
|
180 |
-
});
|
181 |
|
182 |
-
|
183 |
-
|
184 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
185 |
}
|
186 |
});
|
187 |
}
|
188 |
|
189 |
-
async
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
190 |
const hasVersions = project.versions && project.versions.length > 0;
|
191 |
const message = hasVersions ?
|
192 |
`Project "${project.name}" has ${project.versions.length} version(s). Are you sure you want to delete it?` :
|
193 |
`Are you sure you want to delete project "${project.name}"?`;
|
194 |
|
195 |
-
|
196 |
-
width: '400px',
|
197 |
-
data: {
|
198 |
-
title: 'Delete Project',
|
199 |
-
message: message,
|
200 |
-
confirmText: 'Delete',
|
201 |
-
confirmColor: 'warn'
|
202 |
-
}
|
203 |
-
});
|
204 |
-
|
205 |
-
dialogRef.afterClosed().subscribe(async confirmed => {
|
206 |
if (confirmed) {
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
|
|
|
|
|
|
|
|
215 |
}
|
216 |
});
|
217 |
}
|
218 |
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
// Create and download file
|
224 |
-
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
225 |
-
const url = window.URL.createObjectURL(blob);
|
226 |
-
const link = document.createElement('a');
|
227 |
-
link.href = url;
|
228 |
-
link.download = `${project.name}_export_${new Date().getTime()}.json`;
|
229 |
-
link.click();
|
230 |
-
window.URL.revokeObjectURL(url);
|
231 |
-
|
232 |
-
this.showMessage('Project exported successfully', false);
|
233 |
-
} catch (error) {
|
234 |
-
this.showMessage('Failed to export project', true);
|
235 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
236 |
}
|
237 |
|
238 |
importProject() {
|
@@ -248,12 +328,20 @@ export class ProjectsComponent implements OnInit {
|
|
248 |
const text = await file.text();
|
249 |
const data = JSON.parse(text);
|
250 |
|
251 |
-
|
252 |
-
|
253 |
-
|
254 |
-
|
255 |
-
|
256 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
257 |
}
|
258 |
};
|
259 |
|
@@ -285,6 +373,65 @@ export class ProjectsComponent implements OnInit {
|
|
285 |
return project.id;
|
286 |
}
|
287 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
288 |
private showMessage(message: string, isError: boolean) {
|
289 |
this.message = message;
|
290 |
this.isError = isError;
|
|
|
1 |
+
import { Component, OnInit, OnDestroy } from '@angular/core';
|
2 |
import { CommonModule } from '@angular/common';
|
3 |
import { FormsModule } from '@angular/forms';
|
4 |
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
|
|
|
16 |
import { MatMenuModule } from '@angular/material/menu';
|
17 |
import { MatDividerModule } from '@angular/material/divider';
|
18 |
import { ApiService, Project } from '../../services/api.service';
|
19 |
+
import { Subject, takeUntil } from 'rxjs';
|
20 |
+
|
21 |
+
// Dynamic imports for dialogs
|
22 |
+
const loadProjectEditDialog = () => import('../../dialogs/project-edit-dialog/project-edit-dialog.component');
|
23 |
+
const loadVersionEditDialog = () => import('../../dialogs/version-edit-dialog/version-edit-dialog.component');
|
24 |
+
const loadConfirmDialog = () => import('../../dialogs/confirm-dialog/confirm-dialog.component');
|
25 |
|
26 |
@Component({
|
27 |
selector: 'app-projects',
|
|
|
47 |
templateUrl: './projects.component.html',
|
48 |
styleUrls: ['./projects.component.scss']
|
49 |
})
|
50 |
+
export class ProjectsComponent implements OnInit, OnDestroy {
|
51 |
projects: Project[] = [];
|
52 |
filteredProjects: Project[] = [];
|
53 |
searchTerm = '';
|
|
|
59 |
|
60 |
// For table view
|
61 |
displayedColumns: string[] = ['name', 'caption', 'versions', 'status', 'lastUpdate', 'actions'];
|
62 |
+
|
63 |
+
// Memory leak prevention
|
64 |
+
private destroyed$ = new Subject<void>();
|
65 |
|
66 |
constructor(
|
67 |
private apiService: ApiService,
|
|
|
74 |
this.loadEnvironment();
|
75 |
}
|
76 |
|
77 |
+
ngOnDestroy() {
|
78 |
+
this.destroyed$.next();
|
79 |
+
this.destroyed$.complete();
|
80 |
+
}
|
81 |
+
|
82 |
isSparkTabVisible(): boolean {
|
83 |
// Environment bilgisini cache'ten al (eğer varsa)
|
84 |
const env = localStorage.getItem('flare_environment');
|
|
|
89 |
return true; // Default olarak göster
|
90 |
}
|
91 |
|
92 |
+
loadProjects() {
|
93 |
this.loading = true;
|
94 |
+
this.apiService.getProjects(this.showDeleted)
|
95 |
+
.pipe(takeUntil(this.destroyed$))
|
96 |
+
.subscribe({
|
97 |
+
next: (projects) => {
|
98 |
+
this.projects = projects || [];
|
99 |
+
this.applyFilter();
|
100 |
+
this.loading = false;
|
101 |
+
},
|
102 |
+
error: (error) => {
|
103 |
+
this.loading = false;
|
104 |
+
this.showMessage('Failed to load projects', true);
|
105 |
+
console.error('Load projects error:', error);
|
106 |
+
}
|
107 |
+
});
|
108 |
}
|
109 |
|
110 |
private loadEnvironment() {
|
111 |
+
this.apiService.getEnvironment()
|
112 |
+
.pipe(takeUntil(this.destroyed$))
|
113 |
+
.subscribe({
|
114 |
+
next: (env) => {
|
115 |
+
localStorage.setItem('flare_environment', JSON.stringify(env));
|
116 |
+
},
|
117 |
+
error: (err) => {
|
118 |
+
console.error('Failed to load environment:', err);
|
119 |
+
}
|
120 |
+
});
|
121 |
}
|
122 |
|
123 |
applyFilter() {
|
|
|
144 |
this.loadProjects();
|
145 |
}
|
146 |
|
147 |
+
async createProject() {
|
148 |
+
try {
|
149 |
+
const { default: ProjectEditDialogComponent } = await loadProjectEditDialog();
|
150 |
+
|
151 |
+
const dialogRef = this.dialog.open(ProjectEditDialogComponent, {
|
152 |
+
width: '500px',
|
153 |
+
data: { mode: 'create' }
|
154 |
+
});
|
155 |
+
|
156 |
+
dialogRef.afterClosed()
|
157 |
+
.pipe(takeUntil(this.destroyed$))
|
158 |
+
.subscribe(result => {
|
159 |
+
if (result) {
|
160 |
+
this.loadProjects();
|
161 |
+
this.showMessage('Project created successfully', false);
|
162 |
+
}
|
163 |
+
});
|
164 |
+
} catch (error) {
|
165 |
+
console.error('Failed to load dialog:', error);
|
166 |
+
this.showMessage('Failed to open dialog', true);
|
167 |
+
}
|
168 |
}
|
169 |
|
170 |
+
async editProject(project: Project, event?: Event) {
|
171 |
+
if (event) {
|
172 |
+
event.stopPropagation();
|
173 |
+
}
|
174 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
175 |
try {
|
176 |
+
const { default: ProjectEditDialogComponent } = await loadProjectEditDialog();
|
177 |
+
|
178 |
+
const dialogRef = this.dialog.open(ProjectEditDialogComponent, {
|
179 |
+
width: '500px',
|
180 |
+
data: { mode: 'edit', project: { ...project } }
|
181 |
+
});
|
182 |
+
|
183 |
+
dialogRef.afterClosed()
|
184 |
+
.pipe(takeUntil(this.destroyed$))
|
185 |
+
.subscribe(result => {
|
186 |
+
if (result) {
|
187 |
+
// Listeyi güncelle
|
188 |
+
const index = this.projects.findIndex(p => p.id === result.id);
|
189 |
+
if (index !== -1) {
|
190 |
+
this.projects[index] = result;
|
191 |
+
this.applyFilter(); // Filtreyi yeniden uygula
|
192 |
+
} else {
|
193 |
+
this.loadProjects(); // Bulunamazsa tüm listeyi yenile
|
194 |
+
}
|
195 |
+
this.showMessage('Project updated successfully', false);
|
196 |
+
}
|
197 |
+
});
|
198 |
} catch (error) {
|
199 |
+
console.error('Failed to load dialog:', error);
|
200 |
+
this.showMessage('Failed to open dialog', true);
|
201 |
}
|
202 |
}
|
203 |
|
204 |
+
toggleProject(project: Project, event?: Event) {
|
205 |
+
if (event) {
|
206 |
+
event.stopPropagation();
|
207 |
+
}
|
|
|
|
|
|
|
208 |
|
209 |
+
const action = project.enabled ? 'disable' : 'enable';
|
210 |
+
const confirmMessage = `Are you sure you want to ${action} "${project.caption}"?`;
|
211 |
+
|
212 |
+
this.confirmAction(
|
213 |
+
`${action.charAt(0).toUpperCase() + action.slice(1)} Project`,
|
214 |
+
confirmMessage,
|
215 |
+
action.charAt(0).toUpperCase() + action.slice(1),
|
216 |
+
!project.enabled
|
217 |
+
).then(confirmed => {
|
218 |
+
if (confirmed) {
|
219 |
+
this.apiService.toggleProject(project.id)
|
220 |
+
.pipe(takeUntil(this.destroyed$))
|
221 |
+
.subscribe({
|
222 |
+
next: (result) => {
|
223 |
+
project.enabled = result.enabled;
|
224 |
+
this.showMessage(
|
225 |
+
`Project ${project.enabled ? 'enabled' : 'disabled'} successfully`,
|
226 |
+
false
|
227 |
+
);
|
228 |
+
},
|
229 |
+
error: (error) => this.handleUpdateError(error, project.caption)
|
230 |
+
});
|
231 |
}
|
232 |
});
|
233 |
}
|
234 |
|
235 |
+
async manageVersions(project: Project, event?: Event) {
|
236 |
+
if (event) {
|
237 |
+
event.stopPropagation();
|
238 |
+
}
|
239 |
+
|
240 |
+
try {
|
241 |
+
const { default: VersionEditDialogComponent } = await loadVersionEditDialog();
|
242 |
+
|
243 |
+
const dialogRef = this.dialog.open(VersionEditDialogComponent, {
|
244 |
+
width: '90vw',
|
245 |
+
maxWidth: '1200px',
|
246 |
+
height: '90vh',
|
247 |
+
data: { project }
|
248 |
+
});
|
249 |
+
|
250 |
+
dialogRef.afterClosed()
|
251 |
+
.pipe(takeUntil(this.destroyed$))
|
252 |
+
.subscribe(result => {
|
253 |
+
if (result) {
|
254 |
+
this.loadProjects();
|
255 |
+
}
|
256 |
+
});
|
257 |
+
} catch (error) {
|
258 |
+
console.error('Failed to load dialog:', error);
|
259 |
+
this.showMessage('Failed to open dialog', true);
|
260 |
+
}
|
261 |
+
}
|
262 |
+
|
263 |
+
deleteProject(project: Project, event?: Event) {
|
264 |
+
if (event) {
|
265 |
+
event.stopPropagation();
|
266 |
+
}
|
267 |
+
|
268 |
const hasVersions = project.versions && project.versions.length > 0;
|
269 |
const message = hasVersions ?
|
270 |
`Project "${project.name}" has ${project.versions.length} version(s). Are you sure you want to delete it?` :
|
271 |
`Are you sure you want to delete project "${project.name}"?`;
|
272 |
|
273 |
+
this.confirmAction('Delete Project', message, 'Delete', true).then(confirmed => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
274 |
if (confirmed) {
|
275 |
+
this.apiService.deleteProject(project.id)
|
276 |
+
.pipe(takeUntil(this.destroyed$))
|
277 |
+
.subscribe({
|
278 |
+
next: () => {
|
279 |
+
this.showMessage('Project deleted successfully', false);
|
280 |
+
this.loadProjects();
|
281 |
+
},
|
282 |
+
error: (error) => {
|
283 |
+
const message = error.error?.detail || 'Failed to delete project';
|
284 |
+
this.showMessage(message, true);
|
285 |
+
}
|
286 |
+
});
|
287 |
}
|
288 |
});
|
289 |
}
|
290 |
|
291 |
+
exportProject(project: Project, event?: Event) {
|
292 |
+
if (event) {
|
293 |
+
event.stopPropagation();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
294 |
}
|
295 |
+
|
296 |
+
this.apiService.exportProject(project.id)
|
297 |
+
.pipe(takeUntil(this.destroyed$))
|
298 |
+
.subscribe({
|
299 |
+
next: (data) => {
|
300 |
+
// Create and download file
|
301 |
+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
302 |
+
const url = window.URL.createObjectURL(blob);
|
303 |
+
const link = document.createElement('a');
|
304 |
+
link.href = url;
|
305 |
+
link.download = `${project.name}_export_${new Date().getTime()}.json`;
|
306 |
+
link.click();
|
307 |
+
window.URL.revokeObjectURL(url);
|
308 |
+
|
309 |
+
this.showMessage('Project exported successfully', false);
|
310 |
+
},
|
311 |
+
error: (error) => {
|
312 |
+
this.showMessage('Failed to export project', true);
|
313 |
+
console.error('Export error:', error);
|
314 |
+
}
|
315 |
+
});
|
316 |
}
|
317 |
|
318 |
importProject() {
|
|
|
328 |
const text = await file.text();
|
329 |
const data = JSON.parse(text);
|
330 |
|
331 |
+
this.apiService.importProject(data)
|
332 |
+
.pipe(takeUntil(this.destroyed$))
|
333 |
+
.subscribe({
|
334 |
+
next: () => {
|
335 |
+
this.showMessage('Project imported successfully', false);
|
336 |
+
this.loadProjects();
|
337 |
+
},
|
338 |
+
error: (error) => {
|
339 |
+
const message = error.error?.detail || 'Failed to import project';
|
340 |
+
this.showMessage(message, true);
|
341 |
+
}
|
342 |
+
});
|
343 |
+
} catch (error) {
|
344 |
+
this.showMessage('Invalid file format', true);
|
345 |
}
|
346 |
};
|
347 |
|
|
|
373 |
return project.id;
|
374 |
}
|
375 |
|
376 |
+
handleUpdateError(error: any, projectName?: string): void {
|
377 |
+
if (error.status === 409 || error.raceCondition) {
|
378 |
+
const details = error.error?.details || error;
|
379 |
+
const lastUpdateUser = details.last_update_user || error.lastUpdateUser || 'another user';
|
380 |
+
const lastUpdateDate = details.last_update_date || error.lastUpdateDate;
|
381 |
+
|
382 |
+
const message = projectName
|
383 |
+
? `Project "${projectName}" was modified by ${lastUpdateUser}. Please reload.`
|
384 |
+
: `Project was modified by ${lastUpdateUser}. Please reload.`;
|
385 |
+
|
386 |
+
this.snackBar.open(
|
387 |
+
message,
|
388 |
+
'Reload',
|
389 |
+
{
|
390 |
+
duration: 0,
|
391 |
+
panelClass: ['error-snackbar', 'race-condition-snackbar']
|
392 |
+
}
|
393 |
+
).onAction().subscribe(() => {
|
394 |
+
this.loadProjects();
|
395 |
+
});
|
396 |
+
|
397 |
+
// Log additional info if available
|
398 |
+
if (lastUpdateDate) {
|
399 |
+
console.info(`Last updated at: ${lastUpdateDate}`);
|
400 |
+
}
|
401 |
+
} else {
|
402 |
+
// Generic error handling
|
403 |
+
this.snackBar.open(
|
404 |
+
error.error?.detail || error.message || 'Operation failed',
|
405 |
+
'Close',
|
406 |
+
{
|
407 |
+
duration: 5000,
|
408 |
+
panelClass: ['error-snackbar']
|
409 |
+
}
|
410 |
+
);
|
411 |
+
}
|
412 |
+
}
|
413 |
+
|
414 |
+
private async confirmAction(title: string, message: string, confirmText: string, dangerous: boolean): Promise<boolean> {
|
415 |
+
try {
|
416 |
+
const { default: ConfirmDialogComponent } = await loadConfirmDialog();
|
417 |
+
|
418 |
+
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
|
419 |
+
width: '400px',
|
420 |
+
data: {
|
421 |
+
title,
|
422 |
+
message,
|
423 |
+
confirmText,
|
424 |
+
confirmColor: dangerous ? 'warn' : 'primary'
|
425 |
+
}
|
426 |
+
});
|
427 |
+
|
428 |
+
return await dialogRef.afterClosed().toPromise() || false;
|
429 |
+
} catch (error) {
|
430 |
+
console.error('Failed to load confirm dialog:', error);
|
431 |
+
return false;
|
432 |
+
}
|
433 |
+
}
|
434 |
+
|
435 |
private showMessage(message: string, isError: boolean) {
|
436 |
this.message = message;
|
437 |
this.isError = isError;
|