From 940d78cf85851840e620dbfc3b200b923ce1b5a6 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Thu, 27 Jul 2023 12:47:48 +0200 Subject: [PATCH 01/19] refactor(Cluster):actions own component --- .../clustercard/clustercard.component.html | 179 +----------- .../clustercard/clustercard.component.ts | 216 +------------- .../cluster-actions.component.html | 169 +++++++++++ .../cluster-actions.component.scss | 0 .../cluster-actions.component.spec.ts | 21 ++ .../cluster-actions.component.ts | 263 ++++++++++++++++++ .../clusterdetail.component.html | 20 +- .../clusterdetail/clusterdetail.component.ts | 55 ++++ src/app/virtualmachines/vm.module.ts | 2 + 9 files changed, 526 insertions(+), 399 deletions(-) create mode 100644 src/app/virtualmachines/clusters/cluster-actions/cluster-actions.component.html create mode 100644 src/app/virtualmachines/clusters/cluster-actions/cluster-actions.component.scss create mode 100644 src/app/virtualmachines/clusters/cluster-actions/cluster-actions.component.spec.ts create mode 100644 src/app/virtualmachines/clusters/cluster-actions/cluster-actions.component.ts diff --git a/src/app/virtualmachines/clustercard/clustercard.component.html b/src/app/virtualmachines/clustercard/clustercard.component.html index 4f7bc7e165..618a057807 100644 --- a/src/app/virtualmachines/clustercard/clustercard.component.html +++ b/src/app/virtualmachines/clustercard/clustercard.component.html @@ -19,7 +19,7 @@
{{ cluster?.userlogin }}
- {{ cluster?.launch_date | date : 'yyyy.MM.dd HH:mm' }} + {{ cluster?.launch_date | date: 'yyyy.MM.dd HH:mm' }}
-
- -
-
-
- You are not able to control this instance via this platform as it is part of a migrated project! -
-
-
-
- + diff --git a/src/app/virtualmachines/clustercard/clustercard.component.ts b/src/app/virtualmachines/clustercard/clustercard.component.ts index c64db5de7e..243b30bc3f 100644 --- a/src/app/virtualmachines/clustercard/clustercard.component.ts +++ b/src/app/virtualmachines/clustercard/clustercard.component.ts @@ -7,15 +7,11 @@ import { BsModalService } from 'ngx-bootstrap/modal'; import { VirtualMachineStates } from '../virtualmachinemodels/virtualmachinestates'; import { VirtualmachineService } from '../../api-connector/virtualmachine.service'; import { ImageService } from '../../api-connector/image.service'; -import { Clusterinfo, WorkerBatch } from '../clusters/clusterinfo'; -import { DeleteClusterComponent } from '../modals/delete-cluster/delete-cluster.component'; -import { PasswordClusterComponent } from '../modals/password-cluster/password-cluster.component'; -import { ScaleClusterComponent } from '../modals/scale-cluster/scale-cluster.component'; +import { Clusterinfo } from '../clusters/clusterinfo'; + import { SharedModal } from '../../shared/shared_modules/baseClass/shared-modal'; -import { ResumeClusterComponent } from '../modals/resume-cluster/resume-cluster.component'; -import { StopClusterComponent } from '../modals/stop-cluster/stop-cluster.component'; + import { CLOUD_PORTAL_SUPPORT_MAIL } from '../../../links/links'; -import { RenameClusterComponent } from '../modals/rename-cluster/rename-cluster.component'; /** * Vm card component to be used by vm-overview. Holds information about a virtual machine. @@ -28,9 +24,6 @@ import { RenameClusterComponent } from '../modals/rename-cluster/rename-cluster. }) export class ClustercardComponent extends SharedModal implements OnInit, OnDestroy { CLOUD_PORTAL_SUPPORT_MAIL: string = CLOUD_PORTAL_SUPPORT_MAIL; - SCALE_UP: string = 'scale_up'; - SCALE_DOWN: string = 'scale_down'; - SCALE_SUCCESS: string = 'scale_success'; /** * The virtual machine this card is for. @@ -42,16 +35,6 @@ export class ClustercardComponent extends SharedModal implements OnInit, OnDestr */ VirtualMachineStates: VirtualMachineStates = new VirtualMachineStates(); - /** - * Is the vm checked. - */ - is_checked: boolean = false; - - /** - * If connection info are shown. - */ - show_connection_info: boolean = false; - /** * Eventemitter when the vm is checked/unchecked. */ @@ -156,103 +139,6 @@ export class ClustercardComponent extends SharedModal implements OnInit, OnDestr this.stopCheckWorkerStatusTimer(); } - /** - * Show Cluster Resume modal - */ - showResumeModal(): void { - const initialState = { cluster: this.cluster }; - - this.bsModalRef = this.modalService.show(ResumeClusterComponent, { initialState }); - this.bsModalRef.setClass('modal-lg'); - this.subscribeToBsModalRef(); - } - - /** - * Show Cluster Stop modal - */ - showStopModal(): void { - const initialState = { cluster: this.cluster }; - - this.bsModalRef = this.modalService.show(StopClusterComponent, { initialState }); - this.bsModalRef.setClass('modal-lg'); - this.subscribeToBsModalRef(); - } - - /** - * Show deletion modal - */ - showDeleteModal(): void { - this.stopAllCheckStatusTimer(); - const all_loaded: boolean = this.get_all_batches_loaded(); - const initialState = { cluster: this.cluster, all_loaded }; - - this.bsModalRef = this.modalService.show(DeleteClusterComponent, { initialState }); - this.bsModalRef.setClass('modal-lg'); - this.subscribeToBsModalRef(); - } - - /** - * Show rename modal - */ - showRenameModal(): void { - this.stopAllCheckStatusTimer(); - const all_loaded: boolean = this.get_all_batches_loaded(); - const initialState = { cluster: this.cluster, all_loaded }; - - this.bsModalRef = this.modalService.show(RenameClusterComponent, { initialState }); - this.bsModalRef.setClass('modal-lg'); - this.subscribeToBsModalRef(); - } - - /** - * Show password modal - */ - showPasswordModal(): void { - this.stopCheckStatusTimer(); - const initialState = { cluster: this.cluster }; - - this.bsModalRef = this.modalService.show(PasswordClusterComponent, { initialState }); - this.bsModalRef.setClass('modal-lg'); - this.subscribeToBsModalRef(); - } - - /** - * Show password modal - */ - showScaleModal(mode: string, msg?: string): void { - this.hideCurrentModal(); - this.stopCheckStatusTimer(); - const initialState = { cluster: this.cluster, mode, msg }; - this.bsModalRef = this.modalService.show(ScaleClusterComponent, { initialState }); - this.bsModalRef.setClass('modal-xl'); - this.subscribeToBsModalRef(); - } - - scaleUpCluster(selectedBatch: WorkerBatch): void { - const scale_up_count: number = selectedBatch.upscale_count; - this.showNotificationModal('Upscaling Cluster', `Starting ${scale_up_count} additional workers..`, 'info'); - - this.subscription.add( - this.virtualmachineservice - .scaleCluster( - this.cluster.cluster_id, - encodeURIComponent(selectedBatch.flavor.name), - selectedBatch.upscale_count, - ) - .subscribe((res: any): void => { - selectedBatch.setNewScalingUpWorkerCount(); - this.cluster.password = res['password']; - this.all_worker_loaded = this.get_all_batches_loaded(); - - this.check_worker_count_loop(); - this.showScaleModal( - this.SCALE_SUCCESS, - `The start of ${scale_up_count} workers was successfully initiated. Remember to configure your cluster after the machines are active!'`, - ); - }), - ); - } - get_all_batches_loaded(): boolean { let worker_amount: number = 0; for (const worker_batch of this.cluster.worker_batches) { @@ -309,102 +195,6 @@ export class ClustercardComponent extends SharedModal implements OnInit, OnDestr }, this.checkStatusTimeout); } - scaleDownCluster(cluster: Clusterinfo): void { - this.cluster = cluster; - let scale_down_count: number = 0; - - const scale_down_batches: any = []; - this.cluster.worker_batches.forEach((batch: WorkerBatch): void => { - if (batch.delete_count > 0) { - scale_down_batches.push({ worker_flavor_name: batch.flavor.name, downscale_count: batch.delete_count }); - scale_down_count += batch.delete_count; - } - }); - let msg: string = 'Scaling Down Batches: '; - for (const batch of scale_down_batches) { - msg += ` \n[Batch with Flavor ${batch.worker_flavor_name} by ${batch.downscale_count} instances ]`; - } - this.showNotificationModal('Scaling Down', msg, 'info'); - - this.subscription.add( - this.virtualmachineservice - .scaleDownCluster(this.cluster.cluster_id, scale_down_batches) - .subscribe((res: any): void => { - this.cluster.password = res['password']; - - this.cluster.setScaleDownBatchesCount(); - - this.cluster.instances_count -= scale_down_count; - this.all_worker_loaded = this.get_all_batches_loaded(); - this.showScaleModal(this.SCALE_SUCCESS, 'Successfully Scaled Down!'); - }), - ); - } - - /** - * Run function to delete a cluster. - */ - deleteCluster(): void { - this.cluster.status = VirtualMachineStates.DELETING; - this.subscription.add( - this.virtualmachineservice.deleteCluster(this.cluster.cluster_id).subscribe((): void => { - this.cluster.status = VirtualMachineStates.DELETED; - }), - ); - } - - resumeCluster(): void { - this.cluster.status = VirtualMachineStates.POWERING_ON; - this.subscription.add( - this.virtualmachineservice.resumeCluster(this.cluster.cluster_id).subscribe((): void => { - this.check_status_loop(); - }), - ); - } - - stopCluster(): void { - this.cluster.status = VirtualMachineStates.POWERING_OFF; - this.subscription.add( - this.virtualmachineservice.stopCluster(this.cluster.cluster_id).subscribe((): void => { - this.check_status_loop(); - }), - ); - } - - renameCluster(name: string): void { - this.subscription.add( - this.virtualmachineservice.renameCluster(this.cluster.cluster_id, name).subscribe((cl: Clusterinfo): void => { - this.cluster.name = cl.name; - this.check_status_loop(); - }), - ); - } - - /** - * Function to listen to modal results. - */ - subscribeToBsModalRef(): void { - this.subscription.add( - this.bsModalRef.content.event.subscribe((result: any) => { - if ('new_name' in result) { - this.renameCluster(result['new_name']); - } else if ('deleteCluster' in result) { - this.deleteCluster(); - } else if ('scaleDownCluster' in result) { - this.scaleDownCluster(result['cluster']); - } else if ('scaleUpCluster' in result) { - this.scaleUpCluster(result['selectedBatch']); - } else if ('resumeCluster' in result) { - this.resumeCluster(); - } else if ('stopCluster' in result) { - this.stopCluster(); - } else { - this.check_status_loop(); - } - }), - ); - } - /** * Copy some text to clipboard. */ diff --git a/src/app/virtualmachines/clusters/cluster-actions/cluster-actions.component.html b/src/app/virtualmachines/clusters/cluster-actions/cluster-actions.component.html new file mode 100644 index 0000000000..e9a8669414 --- /dev/null +++ b/src/app/virtualmachines/clusters/cluster-actions/cluster-actions.component.html @@ -0,0 +1,169 @@ +
+
+ + + + +
+ + Stop +
+ + + + +
+
+
+ You are not able to control this instance via this platform as it is part of a migrated project! +
+
+
+
+
+ diff --git a/src/app/virtualmachines/clusters/cluster-actions/cluster-actions.component.scss b/src/app/virtualmachines/clusters/cluster-actions/cluster-actions.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/virtualmachines/clusters/cluster-actions/cluster-actions.component.spec.ts b/src/app/virtualmachines/clusters/cluster-actions/cluster-actions.component.spec.ts new file mode 100644 index 0000000000..77fe7a065a --- /dev/null +++ b/src/app/virtualmachines/clusters/cluster-actions/cluster-actions.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ClusterActionsComponent } from './cluster-actions.component'; + +describe('ClusterActionsComponent', () => { + let component: ClusterActionsComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ClusterActionsComponent], + }); + fixture = TestBed.createComponent(ClusterActionsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/virtualmachines/clusters/cluster-actions/cluster-actions.component.ts b/src/app/virtualmachines/clusters/cluster-actions/cluster-actions.component.ts new file mode 100644 index 0000000000..925a1d95a9 --- /dev/null +++ b/src/app/virtualmachines/clusters/cluster-actions/cluster-actions.component.ts @@ -0,0 +1,263 @@ +import { + Component, EventEmitter, Input, OnDestroy, Output, +} from '@angular/core'; +import { ClipboardService } from 'ngx-clipboard'; +import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal'; +import { Subscription } from 'rxjs'; +import { CLOUD_PORTAL_SUPPORT_MAIL } from '../../../../links/links'; +import { Clusterinfo, WorkerBatch } from '../clusterinfo'; +import { VirtualMachineStates } from '../../virtualmachinemodels/virtualmachinestates'; +import { ResumeClusterComponent } from '../../modals/resume-cluster/resume-cluster.component'; +import { StopClusterComponent } from '../../modals/stop-cluster/stop-cluster.component'; +import { DeleteClusterComponent } from '../../modals/delete-cluster/delete-cluster.component'; +import { RenameClusterComponent } from '../../modals/rename-cluster/rename-cluster.component'; +import { PasswordClusterComponent } from '../../modals/password-cluster/password-cluster.component'; +import { ScaleClusterComponent } from '../../modals/scale-cluster/scale-cluster.component'; +import { VirtualmachineService } from '../../../api-connector/virtualmachine.service'; +import { NotificationModalComponent } from '../../../shared/modal/notification-modal'; + +@Component({ + selector: 'app-cluster-actions', + templateUrl: './cluster-actions.component.html', + styleUrls: ['./cluster-actions.component.scss'], +}) +export class ClusterActionsComponent implements OnDestroy { + @Input() cluster: Clusterinfo; + bsModalRef: BsModalRef; + subscription: Subscription = new Subscription(); + SCALE_UP: string = 'scale_up'; + SCALE_DOWN: string = 'scale_down'; + SCALE_SUCCESS: string = 'scale_success'; + show_connection_info: boolean = false; + + VirtualMachineStates: VirtualMachineStates = new VirtualMachineStates(); + @Output() readonly stopStatusLoop: EventEmitter = new EventEmitter(); + @Output() readonly startStatusLoop: EventEmitter = new EventEmitter(); + + protected readonly CLOUD_PORTAL_SUPPORT_MAIL = CLOUD_PORTAL_SUPPORT_MAIL; + + constructor( + private clipboardService: ClipboardService, + private modalService: BsModalService, + private virtualmachineservice: VirtualmachineService, + ) {} + + copyToClipboard(text: string): void { + if (this.clipboardService.isSupported) { + this.clipboardService.copy(text); + } + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + // this.statusSubscription.unsubscribe(); + // this.stopAllCheckStatusTimer(); + } + + showResumeModal(): void { + const initialState = { cluster: this.cluster }; + + this.bsModalRef = this.modalService.show(ResumeClusterComponent, { initialState }); + this.bsModalRef.setClass('modal-lg'); + this.subscribeToBsModalRef(); + } + + /** + * Show Cluster Stop modal + */ + showStopModal(): void { + const initialState = { cluster: this.cluster }; + + this.bsModalRef = this.modalService.show(StopClusterComponent, { initialState }); + this.bsModalRef.setClass('modal-lg'); + this.subscribeToBsModalRef(); + } + + showDeleteModal(): void { + // this.stopAllCheckStatusTimer(); + // const all_loaded: boolean = this.get_all_batches_loaded(); + const initialState = { cluster: this.cluster }; + + this.bsModalRef = this.modalService.show(DeleteClusterComponent, { initialState }); + this.bsModalRef.setClass('modal-lg'); + this.subscribeToBsModalRef(); + } + + /** + * Show rename modal + */ + showRenameModal(): void { + // this.stopAllCheckStatusTimer(); + // const all_loaded: boolean = this.get_all_batches_loaded(); + const initialState = { cluster: this.cluster }; + + this.bsModalRef = this.modalService.show(RenameClusterComponent, { initialState }); + this.bsModalRef.setClass('modal-lg'); + this.subscribeToBsModalRef(); + } + + showPasswordModal(): void { + // this.stopCheckStatusTimer(); + const initialState = { cluster: this.cluster }; + + this.bsModalRef = this.modalService.show(PasswordClusterComponent, { initialState }); + this.bsModalRef.setClass('modal-lg'); + this.subscribeToBsModalRef(); + } + + showScaleModal(mode: string, msg?: string): void { + this.hideCurrentModal(); + // this.stopCheckStatusTimer(); + const initialState = { cluster: this.cluster, mode, msg }; + this.bsModalRef = this.modalService.show(ScaleClusterComponent, { initialState }); + this.bsModalRef.setClass('modal-xl'); + this.subscribeToBsModalRef(); + } + + hideCurrentModal() { + if (this.bsModalRef) { + this.modalService.hide(this.bsModalRef.id); + } + } + + stopCluster(): void { + this.stopStatusLoop.emit(); + + this.cluster.status = VirtualMachineStates.POWERING_OFF; + this.subscription.add( + this.virtualmachineservice.stopCluster(this.cluster.cluster_id).subscribe((): void => { + this.cluster.status === VirtualMachineStates.POWERING_OFF; + this.startStatusLoop.emit(); + }), + ); + } + + renameCluster(name: string): void { + this.stopStatusLoop.emit(); + + this.subscription.add( + this.virtualmachineservice.renameCluster(this.cluster.cluster_id, name).subscribe((cl: Clusterinfo): void => { + this.cluster.name = cl.name; + this.startStatusLoop.emit(); + }), + ); + } + + deleteCluster(): void { + this.stopStatusLoop.emit(); + + this.cluster.status = VirtualMachineStates.DELETING; + this.subscription.add( + this.virtualmachineservice.deleteCluster(this.cluster.cluster_id).subscribe((): void => { + this.cluster.status = VirtualMachineStates.DELETED; + this.startStatusLoop.emit(); + }), + ); + } + + resumeCluster(): void { + this.stopStatusLoop.emit(); + this.cluster.status = VirtualMachineStates.POWERING_ON; + this.subscription.add( + this.virtualmachineservice.resumeCluster(this.cluster.cluster_id).subscribe((): void => { + this.startStatusLoop.emit(); + }), + ); + } + + showNotificationModal( + notificationModalTitle: string, + notificationModalMessage: string, + notificationModalType: string, + ) { + const initialState = { notificationModalTitle, notificationModalType, notificationModalMessage }; + if (this.bsModalRef) { + this.bsModalRef.hide(); + } + + this.bsModalRef = this.modalService.show(NotificationModalComponent, { initialState }); + this.bsModalRef.setClass('modal-lg'); + } + + scaleDownCluster(cluster: Clusterinfo): void { + this.stopStatusLoop.emit(); + + this.cluster = cluster; + let scale_down_count: number = 0; + + const scale_down_batches: any = []; + this.cluster.worker_batches.forEach((batch: WorkerBatch): void => { + if (batch.delete_count > 0) { + scale_down_batches.push({ worker_flavor_name: batch.flavor.name, downscale_count: batch.delete_count }); + scale_down_count += batch.delete_count; + } + }); + let msg: string = 'Scaling Down Batches: '; + for (const batch of scale_down_batches) { + msg += ` \n[Batch with Flavor ${batch.worker_flavor_name} by ${batch.downscale_count} instances ]`; + } + this.showNotificationModal('Scaling Down', msg, 'info'); + + this.subscription.add( + this.virtualmachineservice + .scaleDownCluster(this.cluster.cluster_id, scale_down_batches) + .subscribe((res: any): void => { + this.cluster.password = res['password']; + + this.cluster.setScaleDownBatchesCount(); + + this.cluster.instances_count -= scale_down_count; + this.startStatusLoop.emit(); + this.showScaleModal(this.SCALE_SUCCESS, 'Successfully Scaled Down!'); + }), + ); + } + + scaleUpCluster(selectedBatch: WorkerBatch): void { + this.stopStatusLoop.emit(); + + const scale_up_count: number = selectedBatch.upscale_count; + this.showNotificationModal('Upscaling Cluster', `Starting ${scale_up_count} additional workers..`, 'info'); + + this.subscription.add( + this.virtualmachineservice + .scaleCluster( + this.cluster.cluster_id, + encodeURIComponent(selectedBatch.flavor.name), + selectedBatch.upscale_count, + ) + .subscribe((res: any): void => { + selectedBatch.setNewScalingUpWorkerCount(); + this.cluster.password = res['password']; + this.startStatusLoop.emit(); + + this.showScaleModal( + this.SCALE_SUCCESS, + `The start of ${scale_up_count} workers was successfully initiated. Remember to configure your cluster after the machines are active!'`, + ); + }), + ); + } + + subscribeToBsModalRef(): void { + this.subscription.add( + this.bsModalRef.content.event.subscribe((result: any) => { + if ('new_name' in result) { + this.renameCluster(result['new_name']); + } else if ('deleteCluster' in result) { + this.deleteCluster(); + } else if ('scaleDownCluster' in result) { + this.scaleDownCluster(result['cluster']); + } else if ('scaleUpCluster' in result) { + this.scaleUpCluster(result['selectedBatch']); + } else if ('resumeCluster' in result) { + this.resumeCluster(); + } else if ('stopCluster' in result) { + this.stopCluster(); + } else { + // this.check_status_loop(); + } + }), + ); + } +} diff --git a/src/app/virtualmachines/clusters/clusterdetail/clusterdetail.component.html b/src/app/virtualmachines/clusters/clusterdetail/clusterdetail.component.html index ae43bcf5a1..956b1366ad 100644 --- a/src/app/virtualmachines/clusters/clusterdetail/clusterdetail.component.html +++ b/src/app/virtualmachines/clusters/clusterdetail/clusterdetail.component.html @@ -87,19 +87,11 @@
Actions
-
-
- -
-
+
@@ -109,6 +101,7 @@
Instance: {{ cluster?.master_instance.name }} +
@@ -119,6 +112,7 @@
Instance: {{ worker.name }} +
diff --git a/src/app/virtualmachines/clusters/clusterdetail/clusterdetail.component.ts b/src/app/virtualmachines/clusters/clusterdetail/clusterdetail.component.ts index a500956411..8511a567b3 100644 --- a/src/app/virtualmachines/clusters/clusterdetail/clusterdetail.component.ts +++ b/src/app/virtualmachines/clusters/clusterdetail/clusterdetail.component.ts @@ -43,7 +43,11 @@ export class ClusterdetailComponent implements OnInit, OnDestroy { errorOnLoading = false; STATUS_LINK: string = STATUS_LINK; bsModalRef: BsModalRef; + all_worker_loaded: boolean = false; + checkStatusTimer: ReturnType; + VirtualMachineStates: VirtualMachineStates = new VirtualMachineStates(); + statusSubscription: Subscription = new Subscription(); constructor( private activatedRoute: ActivatedRoute, @@ -93,6 +97,7 @@ export class ClusterdetailComponent implements OnInit, OnDestroy { (cluster_info: Clusterinfo): void => { this.cluster = new Clusterinfo(cluster_info); this.isLoaded = true; + this.check_status_loop(); this.check_status_loop_cluster_vms(); }, () => { @@ -133,6 +138,56 @@ export class ClusterdetailComponent implements OnInit, OnDestroy { ); } + get_all_batches_loaded(): boolean { + let worker_amount: number = 0; + for (const worker_batch of this.cluster.worker_batches) { + worker_amount += worker_batch.worker_count; + } + + return this.cluster.worker_instances.length === worker_amount; + } + + stopAllCheckStatusTimer(): void { + this.stopCheckStatusTimer(); + } + + stopCheckStatusTimer(): void { + if (this.checkStatusTimer) { + clearTimeout(this.checkStatusTimer); + } + if (this.statusSubscription) { + this.statusSubscription.unsubscribe(); + } + } + + /** + * Stop and clear the worker check status loop. + */ + + check_status_loop(): void { + this.all_worker_loaded = this.get_all_batches_loaded(); + this.stopAllCheckStatusTimer(); + this.statusSubscription = new Subscription(); + this.checkStatusTimer = setTimeout((): void => { + this.statusSubscription.add( + this.virtualmachineService + .getClusterInfo(this.cluster.cluster_id) + .subscribe((updated_cluster: Clusterinfo): void => { + const password: string = this.cluster.password; + this.cluster = new Clusterinfo(updated_cluster); + this.cluster.password = password; + if ( + this.cluster.status !== VirtualMachineStates.DELETED + || this.cluster.status !== VirtualMachineStates.MIGRATED + ) { + this.check_status_loop(); + this.check_status_loop_cluster_vms(); + } + }), + ); + }, this.checkStatusTimeout); + } + check_status_loop_cluster_vms(): void { this.cluster.worker_instances.forEach((vm: VirtualMachine): void => { if ( diff --git a/src/app/virtualmachines/vm.module.ts b/src/app/virtualmachines/vm.module.ts index b0da375ed6..fa2a08e7cf 100644 --- a/src/app/virtualmachines/vm.module.ts +++ b/src/app/virtualmachines/vm.module.ts @@ -50,6 +50,7 @@ import { RecreateBackendVmComponent } from './modals/recreate-backend-vm/recreat import { DatePickerComponent } from '../shared/datepicking/datepicker.component'; import { TimepickerComponent } from '../shared/datepicking/timepicker.component'; import { SharedModuleModule } from '../shared/shared_modules/shared-module.module'; +import { ClusterActionsComponent } from './clusters/cluster-actions/cluster-actions.component'; /** * VM module. @@ -77,6 +78,7 @@ import { SharedModuleModule } from '../shared/shared_modules/shared-module.modul ], declarations: [ ImageCarouselSlideComponent, + ClusterActionsComponent, ImageDetailComponent, VirtualMachineComponent, FlavorDetailComponent, From 0ef9f2c0a48c3b8a4b654e61212276e1023d8fc6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 28 Jul 2023 16:24:41 +0000 Subject: [PATCH 02/19] feat(Dependencies): Update all non-major dependencies | datasource | package | from | to | | ---------- | -------------------- | ------ | ------ | | npm | eslint | 8.45.0 | 8.46.0 | | npm | eslint-plugin-import | 2.27.5 | 2.28.0 | --- package-lock.json | 115 +++++++++++++++++++++++++--------------------- 1 file changed, 62 insertions(+), 53 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7e5b91982e..026e5663fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3253,18 +3253,18 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", - "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz", + "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.0.tgz", - "integrity": "sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.1.tgz", + "integrity": "sha512-9t7ZA7NGGK8ckelF0PQCfcxIUzs1Md5rrO6U/c+FIQNanea5UZC0wqKXH4vHBccmu4ZJgZ2idtPeW7+Q2npOEA==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -3352,9 +3352,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.44.0.tgz", - "integrity": "sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw==", + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.46.0.tgz", + "integrity": "sha512-a8TLtmPi8xzPkCbp/OGFUo5yhRkHM2Ko9kOWP4znJr0WAhWyThaw3PnwX4vOTWOAMsV2uRt32PPDcEz63esSaA==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -8606,27 +8606,27 @@ } }, "node_modules/eslint": { - "version": "8.45.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.45.0.tgz", - "integrity": "sha512-pd8KSxiQpdYRfYa9Wufvdoct3ZPQQuVuU5O6scNgMuOMYuxvH0IGaYK0wUFjo4UYYQQCUndlXiMbnxopwvvTiw==", + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.46.0.tgz", + "integrity": "sha512-cIO74PvbW0qU8e0mIvk5IV3ToWdCq5FYG6gWPHHkx6gNdjlbAYvtfHmlCMXxjcoVaIdwy/IAt3+mDkZkfvb2Dg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.1.0", - "@eslint/js": "8.44.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.1", + "@eslint/js": "^8.46.0", "@humanwhocodes/config-array": "^0.11.10", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.6.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.2", + "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -8730,8 +8730,9 @@ }, "node_modules/eslint-plugin-import": { "version": "2.27.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz", + "integrity": "sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==", "dev": true, - "license": "MIT", "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", @@ -8758,16 +8759,18 @@ }, "node_modules/eslint-plugin-import/node_modules/debug": { "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, - "license": "MIT", "dependencies": { "ms": "^2.1.1" } }, "node_modules/eslint-plugin-import/node_modules/doctrine": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, - "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, @@ -8873,9 +8876,9 @@ } }, "node_modules/eslint-scope": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", - "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", @@ -8889,9 +8892,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", - "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.2.tgz", + "integrity": "sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -20981,15 +20984,15 @@ } }, "@eslint-community/regexpp": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", - "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz", + "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==", "dev": true }, "@eslint/eslintrc": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.0.tgz", - "integrity": "sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.1.tgz", + "integrity": "sha512-9t7ZA7NGGK8ckelF0PQCfcxIUzs1Md5rrO6U/c+FIQNanea5UZC0wqKXH4vHBccmu4ZJgZ2idtPeW7+Q2npOEA==", "dev": true, "requires": { "ajv": "^6.12.4", @@ -21054,9 +21057,9 @@ } }, "@eslint/js": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.44.0.tgz", - "integrity": "sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw==", + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.46.0.tgz", + "integrity": "sha512-a8TLtmPi8xzPkCbp/OGFUo5yhRkHM2Ko9kOWP4znJr0WAhWyThaw3PnwX4vOTWOAMsV2uRt32PPDcEz63esSaA==", "dev": true }, "@foliojs-fork/fontkit": { @@ -24718,27 +24721,27 @@ } }, "eslint": { - "version": "8.45.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.45.0.tgz", - "integrity": "sha512-pd8KSxiQpdYRfYa9Wufvdoct3ZPQQuVuU5O6scNgMuOMYuxvH0IGaYK0wUFjo4UYYQQCUndlXiMbnxopwvvTiw==", + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.46.0.tgz", + "integrity": "sha512-cIO74PvbW0qU8e0mIvk5IV3ToWdCq5FYG6gWPHHkx6gNdjlbAYvtfHmlCMXxjcoVaIdwy/IAt3+mDkZkfvb2Dg==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.1.0", - "@eslint/js": "8.44.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.1", + "@eslint/js": "^8.46.0", "@humanwhocodes/config-array": "^0.11.10", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.6.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.2", + "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -24967,6 +24970,8 @@ }, "eslint-plugin-import": { "version": "2.27.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz", + "integrity": "sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==", "dev": true, "requires": { "array-includes": "^3.1.6", @@ -24988,6 +24993,8 @@ "dependencies": { "debug": { "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "requires": { "ms": "^2.1.1" @@ -24995,6 +25002,8 @@ }, "doctrine": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, "requires": { "esutils": "^2.0.2" @@ -25070,9 +25079,9 @@ "requires": {} }, "eslint-scope": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", - "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "requires": { "esrecurse": "^4.3.0", @@ -25080,9 +25089,9 @@ } }, "eslint-visitor-keys": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", - "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.2.tgz", + "integrity": "sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw==", "dev": true }, "espree": { From db1ba2fb7a7456fd3ec93640669d1f5e64719a65 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 31 Jul 2023 19:49:24 +0000 Subject: [PATCH 03/19] feat(Dependencies): Update all non-major dependencies | datasource | package | from | to | | ---------- | -------------------- | ------ | ------ | | npm | @ng-select/ng-select | 11.0.0 | 11.1.1 | | npm | eslint-plugin-import | 2.27.5 | 2.28.0 | --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 026e5663fa..9758ca104e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3651,9 +3651,9 @@ } }, "node_modules/@ng-select/ng-select": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/@ng-select/ng-select/-/ng-select-11.0.0.tgz", - "integrity": "sha512-zuqZ/9LVV4nxiOFWo0hWncTqDV2QcUcUqZyMa4kKZxJALRmPumo4+BXca1h1KY6SHYFJeINIriK93LApeMSwQQ==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/@ng-select/ng-select/-/ng-select-11.1.1.tgz", + "integrity": "sha512-Z5wV/u2HgaKl7CQSG3Sy1oF+BPQolmVV6jBuPqHa2+OWg0Nn2e9eXYdcZT8Q3BahfP5j5rHNIBrkkESg/m4YiQ==", "dependencies": { "tslib": "^2.3.1" }, @@ -21277,9 +21277,9 @@ } }, "@ng-select/ng-select": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/@ng-select/ng-select/-/ng-select-11.0.0.tgz", - "integrity": "sha512-zuqZ/9LVV4nxiOFWo0hWncTqDV2QcUcUqZyMa4kKZxJALRmPumo4+BXca1h1KY6SHYFJeINIriK93LApeMSwQQ==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/@ng-select/ng-select/-/ng-select-11.1.1.tgz", + "integrity": "sha512-Z5wV/u2HgaKl7CQSG3Sy1oF+BPQolmVV6jBuPqHa2+OWg0Nn2e9eXYdcZT8Q3BahfP5j5rHNIBrkkESg/m4YiQ==", "requires": { "tslib": "^2.3.1" } From 1453b9eded57d14b870984710c3d858309f4c1b1 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Tue, 1 Aug 2023 13:25:41 +0200 Subject: [PATCH 04/19] feat(Email):added csv templated mails --- src/app/api-connector/email.service.ts | 37 ++++++++++- .../facilityprojectsoverview.component.html | 28 ++++---- .../facilityprojectsoverview.component.ts | 31 ++++++++- .../shared/classes/csvMailTemplate.model.ts | 8 +++ .../project-email-modal.component.html | 65 +++++++++++++++++-- .../project-email-modal.component.ts | 39 ++++++++++- .../projext-email-modal.component.scss | 15 +++++ src/app/vo_manager/VoOverviewComponent.ts | 32 ++++++++- src/app/vo_manager/voOverview.component.html | 36 +++++----- 9 files changed, 253 insertions(+), 38 deletions(-) create mode 100644 src/app/shared/classes/csvMailTemplate.model.ts diff --git a/src/app/api-connector/email.service.ts b/src/app/api-connector/email.service.ts index 33c609c7a5..eb313a36d2 100644 --- a/src/app/api-connector/email.service.ts +++ b/src/app/api-connector/email.service.ts @@ -3,11 +3,14 @@ import { Observable } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { ApiSettings } from './api-settings.service'; import { IResponseTemplate } from './response-template'; +import { CsvMailTemplateModel } from '../shared/classes/csvMailTemplate.model'; /** * Service which provides methods for Flavors. */ -@Injectable() +@Injectable({ + providedIn: 'root', +}) export class EmailService { constructor(private http: HttpClient) {} @@ -17,6 +20,38 @@ export class EmailService { }); } + sendCsvTemplate(csvFile: File): Observable { + const formData = new FormData(); + formData.append('csv_file', csvFile, csvFile.name); + + return this.http.post(`${ApiSettings.getApiBaseURL()}emails/templated/csv/`, formData, { + withCredentials: true, + }); + } + + sendCsvTemplatedMail( + csvFile: File, + projectIds: (string | number)[], + subject: string, + message: string, + adminsOnly: boolean, + reply?: string, + ): Observable { + const formData = new FormData(); + formData.append('csv_file', csvFile, csvFile.name); + formData.append('project_ids', JSON.stringify(projectIds)); + formData.append('subject', subject); + formData.append('message', message); + formData.append('adminsOnly', String(adminsOnly)); + if (reply !== undefined) { + formData.append('reply', reply); + } + + return this.http.post(`${ApiSettings.getApiBaseURL()}emails/templated/csv/projects/`, formData, { + withCredentials: true, + }); + } + sendMailToProjects( projectIds: (string | number)[], subject: string, diff --git a/src/app/facility_manager/facilityprojectsoverview.component.html b/src/app/facility_manager/facilityprojectsoverview.component.html index 46192d4a46..5bce4b8159 100644 --- a/src/app/facility_manager/facilityprojectsoverview.component.html +++ b/src/app/facility_manager/facilityprojectsoverview.component.html @@ -34,6 +34,22 @@ Send email
+
+ +
+
+ + +
-
+
-
- -
Projects
diff --git a/src/app/facility_manager/facilityprojectsoverview.component.ts b/src/app/facility_manager/facilityprojectsoverview.component.ts index f4d6acc5d0..7047290e56 100644 --- a/src/app/facility_manager/facilityprojectsoverview.component.ts +++ b/src/app/facility_manager/facilityprojectsoverview.component.ts @@ -20,6 +20,8 @@ import { AbstractBaseClass } from '../shared/shared_modules/baseClass/abstract-b import { ProjectEmailModalComponent } from '../shared/modal/email/project-email-modal/project-email-modal.component'; import { NotificationModalComponent } from '../shared/modal/notification-modal'; import { MembersListModalComponent } from '../shared/modal/members/members-list-modal.component'; +import { EmailService } from '../api-connector/email.service'; +import { CsvMailTemplateModel } from '../shared/classes/csvMailTemplate.model'; /** * Facility Project overview component. @@ -95,6 +97,7 @@ export class FacilityProjectsOverviewComponent extends AbstractBaseClass impleme private newsService: NewsService, public sortProjectService: ProjectSortService, private modalService: BsModalService, + private emailService: EmailService, ) { super(); } @@ -155,6 +158,21 @@ export class FacilityProjectsOverviewComponent extends AbstractBaseClass impleme }); * */ } + onCsvFileSelected(event): void { + const inputElement = event.target as HTMLInputElement; + if (inputElement.files && inputElement.files.length > 0) { + this.emailService.sendCsvTemplate(inputElement.files[0]).subscribe( + (csvTemplate: CsvMailTemplateModel) => { + this.openProjectMailsModal(inputElement.files[0], csvTemplate); + }, + (error: CsvMailTemplateModel) => { + console.log(error['error']); + this.openProjectMailsModal(inputElement.files[0], error['error']); + }, + ); + } + } + onSort({ column, direction }: SortEvent) { // resetting other headers this.headers.forEach(header => { @@ -442,9 +460,18 @@ export class FacilityProjectsOverviewComponent extends AbstractBaseClass impleme }); } - openProjectMailsModal(): void { - const initialState = { selectedProjects: this.selectedEmailProjects }; + openProjectMailsModal(csvFile: File = null, csvTemplate: CsvMailTemplateModel = null): void { + let initialState = {}; + if (csvFile) { + initialState = { + selectedProjects: csvTemplate.valid_projects, + csvFile, + csvMailTemplate: csvTemplate, + }; + } else { + initialState = { selectedProjects: this.selectedEmailProjects }; + } this.bsModalRef = this.modalService.show(ProjectEmailModalComponent, { initialState, class: 'modal-lg' }); this.bsModalRef.content.event.subscribe((sent_successfully: boolean) => { if (sent_successfully) { diff --git a/src/app/shared/classes/csvMailTemplate.model.ts b/src/app/shared/classes/csvMailTemplate.model.ts new file mode 100644 index 0000000000..5c7543683a --- /dev/null +++ b/src/app/shared/classes/csvMailTemplate.model.ts @@ -0,0 +1,8 @@ +import { Application } from '../../applications/application.model/application.model'; + +export class CsvMailTemplateModel { + errors: string[] = []; + warnings: string[] = []; + valid_projects: Application[] = []; + keys: string[] = []; +} diff --git a/src/app/shared/modal/email/project-email-modal/project-email-modal.component.html b/src/app/shared/modal/email/project-email-modal/project-email-modal.component.html index 657e40f143..c880a809cb 100644 --- a/src/app/shared/modal/email/project-email-modal/project-email-modal.component.html +++ b/src/app/shared/modal/email/project-email-modal/project-email-modal.component.html @@ -4,6 +4,7 @@
+ +
+

+ The following keys were provided by the CSV file: {{ csvFile.name }} +

+
+ {{ '{' + template + '}' }} +
+
+
-

The following keys can be used as variables:

+

You can use the following keys as variables:

- {{ '{' + template + '}' }}, + {{ '{' + template + '}' }}
+
Please consider: In case any dates are part of the sent E-Mails, they will be formatted in the german TT.MM.YYYY-format. @@ -108,11 +155,21 @@
diff --git a/src/app/shared/modal/email/project-email-modal/project-email-modal.component.ts b/src/app/shared/modal/email/project-email-modal/project-email-modal.component.ts index 1b8798084f..40c9dce8ef 100644 --- a/src/app/shared/modal/email/project-email-modal/project-email-modal.component.ts +++ b/src/app/shared/modal/email/project-email-modal/project-email-modal.component.ts @@ -6,6 +6,8 @@ import { Application } from '../../../../applications/application.model/applicat import { VoService } from '../../../../api-connector/vo.service'; import { IResponseTemplate } from '../../../../api-connector/response-template'; import { EmailService } from '../../../../api-connector/email.service'; +import { STATUS_LINK } from '../../../../../links/links'; +import { CsvMailTemplateModel } from '../../../classes/csvMailTemplate.model'; @Component({ selector: 'app-project-email-modal', @@ -14,17 +16,26 @@ import { EmailService } from '../../../../api-connector/email.service'; providers: [EmailService], }) export class ProjectEmailModalComponent implements OnInit, OnDestroy { - // currently only for vo @Input() selectedProjects: Application[]; + @Input() csvMailTemplate: CsvMailTemplateModel; + @Input() csvFile: File; + emailAdminsOnly: boolean; emailSubject: string; emailReply: string; emailText: string; templates: string[]; + validCSVExample = `Project, Key1, Key2 +Proj1, ValK1, ValK2 +Proj2, ValK1, ValK2`; public event: EventEmitter = new EventEmitter(); - constructor(public bsModalRef: BsModalRef, private voService: VoService, private emailService: EmailService) { + constructor( + public bsModalRef: BsModalRef, + private voService: VoService, + private emailService: EmailService, + ) { // eslint-disable-next-line no-empty-function } @@ -38,6 +49,28 @@ export class ProjectEmailModalComponent implements OnInit, OnDestroy { }); } + sentProjectsTemplatedMail(): void { + const project_ids = this.selectedProjects.map((pr: Application) => pr.project_application_perun_id); + + this.emailService + .sendCsvTemplatedMail( + this.csvFile, + project_ids, + this.emailSubject, + this.emailText, + this.emailAdminsOnly, + this.emailReply, + ) + .subscribe( + (res: IResponseTemplate) => { + this.event.emit(res.value as boolean); + }, + () => { + this.event.emit(false); + }, + ); + } + sentProjectsMail(): void { const project_ids = this.selectedProjects.map((pr: Application) => pr.project_application_perun_id); @@ -56,4 +89,6 @@ export class ProjectEmailModalComponent implements OnInit, OnDestroy { ngOnDestroy(): void { this.bsModalRef.hide(); } + + protected readonly STATUS_LINK = STATUS_LINK; } diff --git a/src/app/shared/modal/email/project-email-modal/projext-email-modal.component.scss b/src/app/shared/modal/email/project-email-modal/projext-email-modal.component.scss index 6e91f2f7da..a86e9ffc41 100644 --- a/src/app/shared/modal/email/project-email-modal/projext-email-modal.component.scss +++ b/src/app/shared/modal/email/project-email-modal/projext-email-modal.component.scss @@ -8,3 +8,18 @@ margin: 10px 0; font-family: monospace; } + +.valid-example { + border: 1px solid #ccc; + padding: 10px; + background-color: #f9f9f9; + margin-top: 10px; +} + +.valid-example-heading { + margin-bottom: 5px; +} + +.valid-example-content { + font-family: monospace; +} \ No newline at end of file diff --git a/src/app/vo_manager/VoOverviewComponent.ts b/src/app/vo_manager/VoOverviewComponent.ts index 710297627a..ba38963185 100644 --- a/src/app/vo_manager/VoOverviewComponent.ts +++ b/src/app/vo_manager/VoOverviewComponent.ts @@ -23,6 +23,8 @@ import { ProjectEmailModalComponent } from '../shared/modal/email/project-email- import { ConfirmationModalComponent } from '../shared/modal/confirmation-modal.component'; import { ConfirmationActions } from '../shared/modal/confirmation_actions'; import { MembersListModalComponent } from '../shared/modal/members/members-list-modal.component'; +import { EmailService } from '../api-connector/email.service'; +import { CsvMailTemplateModel } from '../shared/classes/csvMailTemplate.model'; /** * Vo Overview component. @@ -95,6 +97,7 @@ export class VoOverviewComponent extends AbstractBaseClass implements OnInit, On private facilityService: FacilityService, public sortProjectService: ProjectSortService, private modalService: BsModalService, + private emailService: EmailService, ) { super(); } @@ -112,6 +115,21 @@ export class VoOverviewComponent extends AbstractBaseClass implements OnInit, On this.subscription.unsubscribe(); } + onCsvFileSelected(event): void { + const inputElement = event.target as HTMLInputElement; + if (inputElement.files && inputElement.files.length > 0) { + this.emailService.sendCsvTemplate(inputElement.files[0]).subscribe( + (csvTemplate: CsvMailTemplateModel) => { + this.openProjectMailsModal(inputElement.files[0], csvTemplate); + }, + (error: CsvMailTemplateModel) => { + console.log(error['error']); + this.openProjectMailsModal(inputElement.files[0], error['error']); + }, + ); + } + } + getTSVInformation(timeout: number = this.checkTSVTimeout): void { this.stopCheckTSVTimer(); this.subscription.add( @@ -227,8 +245,18 @@ export class VoOverviewComponent extends AbstractBaseClass implements OnInit, On } } - openProjectMailsModal(): void { - const initialState = { selectedProjects: this.selectedEmailProjects }; + openProjectMailsModal(csvFile: File = null, csvTemplate: CsvMailTemplateModel = null): void { + let initialState = {}; + + if (csvFile) { + initialState = { + selectedProjects: csvTemplate.valid_projects, + csvFile, + csvMailTemplate: csvTemplate, + }; + } else { + initialState = { selectedProjects: this.selectedEmailProjects }; + } this.bsModalRef = this.modalService.show(ProjectEmailModalComponent, { initialState, class: 'modal-lg' }); this.bsModalRef.content.event.subscribe((sent_successfully: boolean) => { diff --git a/src/app/vo_manager/voOverview.component.html b/src/app/vo_manager/voOverview.component.html index ea86751c6f..87ff0dd170 100644 --- a/src/app/vo_manager/voOverview.component.html +++ b/src/app/vo_manager/voOverview.component.html @@ -36,6 +36,10 @@ {{ selectedEmailProjects?.length }} + + + *ngIf="project | hasstatusinlist : project_states.ACTIVE" + style="margin-left: 5px; margin-bottom: 5px" + type="button" + class="btn btn-warning" + (click)="selectedProject = project; suspendModal.show()" + > +  Suspend Project + + *ngIf="project | hasstatusinlist : project_states.SUSPENDED" + style="margin-left: 5px; margin-bottom: 5px" + type="button" + class="btn btn-success" + (click)="selectedProject = project; resumeModal.show()" + > +  Resume Project +