diff --git a/frontend/src/app/engine/edit-external-source-modal/edit-external-source-modal.component.html b/frontend/src/app/engine/edit-external-source-modal/edit-external-source-modal.component.html
new file mode 100644
index 0000000000..b3df6f83e5
--- /dev/null
+++ b/frontend/src/app/engine/edit-external-source-modal/edit-external-source-modal.component.html
@@ -0,0 +1,24 @@
+
+
+
diff --git a/frontend/src/app/engine/edit-external-source-modal/edit-external-source-modal.component.scss b/frontend/src/app/engine/edit-external-source-modal/edit-external-source-modal.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frontend/src/app/engine/edit-external-source-modal/edit-external-source-modal.component.spec.ts b/frontend/src/app/engine/edit-external-source-modal/edit-external-source-modal.component.spec.ts
new file mode 100644
index 0000000000..e493915162
--- /dev/null
+++ b/frontend/src/app/engine/edit-external-source-modal/edit-external-source-modal.component.spec.ts
@@ -0,0 +1,165 @@
+import { EditExternalSourceModalComponent } from './edit-external-source-modal.component';
+import { ComponentTester, createMock } from 'ngx-speculoos';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { fakeAsync, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { of } from 'rxjs';
+import { MockI18nModule } from '../../../i18n/mock-i18n.spec';
+import { DefaultValidationErrorsComponent } from '../../components/shared/default-validation-errors/default-validation-errors.component';
+import { ExternalSourceService } from '../../services/external-source.service';
+import { ExternalSourceCommandDTO, ExternalSourceDTO } from '../../model/external-sources.model';
+
+class EditExternalSourceModalComponentTester extends ComponentTester {
+ constructor() {
+ super(EditExternalSourceModalComponent);
+ }
+
+ get reference() {
+ return this.input('#reference')!;
+ }
+
+ get description() {
+ return this.textarea('#description')!;
+ }
+
+ get validationErrors() {
+ return this.elements('val-errors div');
+ }
+
+ get save() {
+ return this.button('#save-button')!;
+ }
+
+ get cancel() {
+ return this.button('#cancel-button')!;
+ }
+}
+
+describe('EditExternalSourceModalComponent', () => {
+ let tester: EditExternalSourceModalComponentTester;
+ let fakeActiveModal: NgbActiveModal;
+ let externalSourceService: jasmine.SpyObj;
+
+ beforeEach(() => {
+ fakeActiveModal = createMock(NgbActiveModal);
+ externalSourceService = createMock(ExternalSourceService);
+
+ TestBed.configureTestingModule({
+ imports: [
+ MockI18nModule,
+ ReactiveFormsModule,
+ HttpClientTestingModule,
+ EditExternalSourceModalComponent,
+ DefaultValidationErrorsComponent
+ ],
+ providers: [
+ { provide: NgbActiveModal, useValue: fakeActiveModal },
+ { provide: ExternalSourceService, useValue: externalSourceService }
+ ]
+ });
+
+ TestBed.createComponent(DefaultValidationErrorsComponent).detectChanges();
+
+ tester = new EditExternalSourceModalComponentTester();
+ });
+
+ describe('create mode', () => {
+ beforeEach(() => {
+ tester.componentInstance.prepareForCreation();
+ tester.detectChanges();
+ });
+
+ it('should have an empty form', () => {
+ expect(tester.reference).toHaveValue('');
+ expect(tester.description).toHaveValue('');
+ });
+
+ it('should not save if invalid', () => {
+ tester.save.click();
+
+ // reference
+ expect(tester.validationErrors.length).toBe(1);
+ expect(fakeActiveModal.close).not.toHaveBeenCalled();
+ });
+
+ it('should save if valid', fakeAsync(() => {
+ tester.reference.fillWith('ref');
+ tester.description.fillWith('desc');
+
+ tester.detectChanges();
+
+ const createdExternalSource = {
+ id: 'id1'
+ } as ExternalSourceDTO;
+ externalSourceService.createExternalSource.and.returnValue(of(createdExternalSource));
+
+ tester.save.click();
+
+ const expectedCommand: ExternalSourceCommandDTO = {
+ reference: 'ref',
+ description: 'desc'
+ };
+
+ expect(externalSourceService.createExternalSource).toHaveBeenCalledWith(expectedCommand);
+ expect(fakeActiveModal.close).toHaveBeenCalledWith(createdExternalSource);
+ }));
+
+ it('should cancel', () => {
+ tester.cancel.click();
+ expect(fakeActiveModal.dismiss).toHaveBeenCalled();
+ });
+ });
+
+ describe('edit mode', () => {
+ const externalSourceToUpdate: ExternalSourceDTO = {
+ id: 'id1',
+ reference: 'ref1',
+ description: 'My External source 1'
+ };
+
+ beforeEach(() => {
+ externalSourceService.getExternalSource.and.returnValue(of(externalSourceToUpdate));
+
+ tester.componentInstance.prepareForEdition(externalSourceToUpdate);
+ tester.detectChanges();
+ });
+
+ it('should have a populated form', () => {
+ expect(tester.reference).toHaveValue(externalSourceToUpdate.reference);
+ expect(tester.description).toHaveValue(externalSourceToUpdate.description);
+ });
+
+ it('should not save if invalid', () => {
+ tester.reference.fillWith('');
+ tester.save.click();
+
+ // reference
+ expect(tester.validationErrors.length).toBe(1);
+ expect(fakeActiveModal.close).not.toHaveBeenCalled();
+ });
+
+ it('should save if valid', fakeAsync(() => {
+ externalSourceService.updateExternalSource.and.returnValue(of(undefined));
+
+ tester.reference.fillWith('External source 1 (updated)');
+ tester.description.fillWith('A longer and updated description of my external source');
+
+ tester.save.click();
+
+ const expectedCommand: ExternalSourceCommandDTO = {
+ reference: 'External source 1 (updated)',
+ description: 'A longer and updated description of my external source'
+ };
+
+ expect(externalSourceService.updateExternalSource).toHaveBeenCalledWith('id1', expectedCommand);
+ expect(externalSourceService.getExternalSource).toHaveBeenCalledWith('id1');
+ expect(fakeActiveModal.close).toHaveBeenCalledWith(externalSourceToUpdate);
+ }));
+
+ it('should cancel', () => {
+ tester.cancel.click();
+ expect(fakeActiveModal.dismiss).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/frontend/src/app/engine/edit-external-source-modal/edit-external-source-modal.component.ts b/frontend/src/app/engine/edit-external-source-modal/edit-external-source-modal.component.ts
new file mode 100644
index 0000000000..e2595fe502
--- /dev/null
+++ b/frontend/src/app/engine/edit-external-source-modal/edit-external-source-modal.component.ts
@@ -0,0 +1,77 @@
+import { Component } from '@angular/core';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
+import { Observable, switchMap } from 'rxjs';
+import { ObservableState, SaveButtonComponent } from '../../components/shared/save-button/save-button.component';
+import { ValidationErrorsComponent } from 'ngx-valdemort';
+import { TranslateModule } from '@ngx-translate/core';
+import { ExternalSourceCommandDTO, ExternalSourceDTO } from '../../model/external-sources.model';
+import { ExternalSourceService } from '../../services/external-source.service';
+
+@Component({
+ selector: 'oib-edit-external-source-modal',
+ templateUrl: './edit-external-source-modal.component.html',
+ styleUrls: ['./edit-external-source-modal.component.scss'],
+ imports: [ReactiveFormsModule, TranslateModule, ValidationErrorsComponent, SaveButtonComponent],
+ standalone: true
+})
+export class EditExternalSourceModalComponent {
+ mode: 'create' | 'edit' = 'create';
+ state = new ObservableState();
+ externalSource: ExternalSourceDTO | null = null;
+ form = this.fb.group({
+ reference: ['', Validators.required],
+ description: ''
+ });
+
+ constructor(private modal: NgbActiveModal, private fb: FormBuilder, private externalSourceService: ExternalSourceService) {}
+
+ /**
+ * Prepares the component for creation.
+ */
+ prepareForCreation() {
+ this.mode = 'create';
+ }
+
+ /**
+ * Prepares the component for edition.
+ */
+ prepareForEdition(externalSource: ExternalSourceDTO) {
+ this.mode = 'edit';
+ this.externalSource = externalSource;
+
+ this.form.patchValue({
+ reference: externalSource.reference,
+ description: externalSource.description
+ });
+ }
+
+ cancel() {
+ this.modal.dismiss();
+ }
+
+ save() {
+ if (!this.form.valid) {
+ return;
+ }
+
+ const formValue = this.form.value;
+
+ const command: ExternalSourceCommandDTO = {
+ reference: formValue.reference!,
+ description: formValue.description!
+ };
+
+ let obs: Observable;
+ if (this.mode === 'create') {
+ obs = this.externalSourceService.createExternalSource(command);
+ } else {
+ obs = this.externalSourceService
+ .updateExternalSource(this.externalSource!.id, command)
+ .pipe(switchMap(() => this.externalSourceService.getExternalSource(this.externalSource!.id)));
+ }
+ obs.pipe(this.state.pendingUntilFinalization()).subscribe(externalSource => {
+ this.modal.close(externalSource);
+ });
+ }
+}
diff --git a/frontend/src/app/engine/edit-ip-filter-modal/edit-ip-filter-modal.component.html b/frontend/src/app/engine/edit-ip-filter-modal/edit-ip-filter-modal.component.html
new file mode 100644
index 0000000000..b6adf9635d
--- /dev/null
+++ b/frontend/src/app/engine/edit-ip-filter-modal/edit-ip-filter-modal.component.html
@@ -0,0 +1,24 @@
+
+
+
+
+
diff --git a/frontend/src/app/engine/edit-ip-filter-modal/edit-ip-filter-modal.component.scss b/frontend/src/app/engine/edit-ip-filter-modal/edit-ip-filter-modal.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frontend/src/app/engine/edit-ip-filter-modal/edit-ip-filter-modal.component.spec.ts b/frontend/src/app/engine/edit-ip-filter-modal/edit-ip-filter-modal.component.spec.ts
new file mode 100644
index 0000000000..0c44d8c936
--- /dev/null
+++ b/frontend/src/app/engine/edit-ip-filter-modal/edit-ip-filter-modal.component.spec.ts
@@ -0,0 +1,159 @@
+import { EditIpFilterModalComponent } from './edit-ip-filter-modal.component';
+import { ComponentTester, createMock } from 'ngx-speculoos';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { fakeAsync, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { of } from 'rxjs';
+import { MockI18nModule } from '../../../i18n/mock-i18n.spec';
+import { DefaultValidationErrorsComponent } from '../../components/shared/default-validation-errors/default-validation-errors.component';
+import { IpFilterService } from '../../services/ip-filter.service';
+import { IpFilterCommandDTO, IpFilterDTO } from '../../model/ip-filter.model';
+
+class EditIpFilterModalComponentTester extends ComponentTester {
+ constructor() {
+ super(EditIpFilterModalComponent);
+ }
+
+ get address() {
+ return this.input('#address')!;
+ }
+
+ get description() {
+ return this.textarea('#description')!;
+ }
+
+ get validationErrors() {
+ return this.elements('val-errors div');
+ }
+
+ get save() {
+ return this.button('#save-button')!;
+ }
+
+ get cancel() {
+ return this.button('#cancel-button')!;
+ }
+}
+
+describe('EditIpFilterModalComponent', () => {
+ let tester: EditIpFilterModalComponentTester;
+ let fakeActiveModal: NgbActiveModal;
+ let ipFilterService: jasmine.SpyObj;
+
+ beforeEach(() => {
+ fakeActiveModal = createMock(NgbActiveModal);
+ ipFilterService = createMock(IpFilterService);
+
+ TestBed.configureTestingModule({
+ imports: [MockI18nModule, ReactiveFormsModule, HttpClientTestingModule, EditIpFilterModalComponent, DefaultValidationErrorsComponent],
+ providers: [
+ { provide: NgbActiveModal, useValue: fakeActiveModal },
+ { provide: IpFilterService, useValue: ipFilterService }
+ ]
+ });
+
+ TestBed.createComponent(DefaultValidationErrorsComponent).detectChanges();
+
+ tester = new EditIpFilterModalComponentTester();
+ });
+
+ describe('create mode', () => {
+ beforeEach(() => {
+ tester.componentInstance.prepareForCreation();
+ tester.detectChanges();
+ });
+
+ it('should have an empty form', () => {
+ expect(tester.address).toHaveValue('');
+ expect(tester.description).toHaveValue('');
+ });
+
+ it('should not save if invalid', () => {
+ tester.save.click();
+
+ // address
+ expect(tester.validationErrors.length).toBe(1);
+ expect(fakeActiveModal.close).not.toHaveBeenCalled();
+ });
+
+ it('should save if valid', fakeAsync(() => {
+ tester.address.fillWith('127.0.0.1');
+ tester.description.fillWith('desc');
+
+ tester.detectChanges();
+
+ const createdProxy = {
+ id: 'id1'
+ } as IpFilterDTO;
+ ipFilterService.createIpFilter.and.returnValue(of(createdProxy));
+
+ tester.save.click();
+
+ const expectedCommand: IpFilterCommandDTO = {
+ address: '127.0.0.1',
+ description: 'desc'
+ };
+
+ expect(ipFilterService.createIpFilter).toHaveBeenCalledWith(expectedCommand);
+ expect(fakeActiveModal.close).toHaveBeenCalledWith(createdProxy);
+ }));
+
+ it('should cancel', () => {
+ tester.cancel.click();
+ expect(fakeActiveModal.dismiss).toHaveBeenCalled();
+ });
+ });
+
+ describe('edit mode', () => {
+ const ipFilterToUpdate: IpFilterDTO = {
+ id: 'id1',
+ address: '127.0.0.1',
+ description: 'My IP Filter 1'
+ };
+
+ beforeEach(() => {
+ ipFilterService.getIpFilter.and.returnValue(of(ipFilterToUpdate));
+
+ tester.componentInstance.prepareForEdition(ipFilterToUpdate);
+ tester.detectChanges();
+ });
+
+ it('should have a populated form', () => {
+ expect(tester.address).toHaveValue(ipFilterToUpdate.address);
+ expect(tester.description).toHaveValue(ipFilterToUpdate.description);
+ });
+
+ it('should not save if invalid', () => {
+ tester.address.fillWith('');
+ tester.save.click();
+
+ // address
+ expect(tester.validationErrors.length).toBe(1);
+ expect(fakeActiveModal.close).not.toHaveBeenCalled();
+ });
+
+ it('should save if valid', fakeAsync(() => {
+ ipFilterService.updateIpFilter.and.returnValue(of(undefined));
+
+ tester.address.fillWith('192.168.0.1');
+ tester.description.fillWith('A longer and updated description of my IP filter');
+
+ tester.save.click();
+
+ const expectedCommand: IpFilterCommandDTO = {
+ address: '192.168.0.1',
+ description: 'A longer and updated description of my IP filter'
+ };
+
+ expect(ipFilterService.updateIpFilter).toHaveBeenCalledWith('id1', expectedCommand);
+ expect(ipFilterService.getIpFilter).toHaveBeenCalledWith('id1');
+ expect(fakeActiveModal.close).toHaveBeenCalledWith(ipFilterToUpdate);
+ }));
+
+ it('should cancel', () => {
+ tester.cancel.click();
+ expect(fakeActiveModal.dismiss).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/frontend/src/app/engine/edit-ip-filter-modal/edit-ip-filter-modal.component.ts b/frontend/src/app/engine/edit-ip-filter-modal/edit-ip-filter-modal.component.ts
new file mode 100644
index 0000000000..3693973b0a
--- /dev/null
+++ b/frontend/src/app/engine/edit-ip-filter-modal/edit-ip-filter-modal.component.ts
@@ -0,0 +1,77 @@
+import { Component } from '@angular/core';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
+import { Observable, switchMap } from 'rxjs';
+import { ObservableState, SaveButtonComponent } from '../../components/shared/save-button/save-button.component';
+import { ValidationErrorsComponent } from 'ngx-valdemort';
+import { TranslateModule } from '@ngx-translate/core';
+import { IpFilterCommandDTO, IpFilterDTO } from '../../model/ip-filter.model';
+import { IpFilterService } from '../../services/ip-filter.service';
+
+@Component({
+ selector: 'oib-edit-ip-filter-modal',
+ templateUrl: './edit-ip-filter-modal.component.html',
+ styleUrls: ['./edit-ip-filter-modal.component.scss'],
+ imports: [ReactiveFormsModule, TranslateModule, ValidationErrorsComponent, SaveButtonComponent],
+ standalone: true
+})
+export class EditIpFilterModalComponent {
+ mode: 'create' | 'edit' = 'create';
+ state = new ObservableState();
+ ipFilter: IpFilterDTO | null = null;
+ form = this.fb.group({
+ address: ['', Validators.required],
+ description: ''
+ });
+
+ constructor(private modal: NgbActiveModal, private fb: FormBuilder, private ipFilterService: IpFilterService) {}
+
+ /**
+ * Prepares the component for creation.
+ */
+ prepareForCreation() {
+ this.mode = 'create';
+ }
+
+ /**
+ * Prepares the component for edition.
+ */
+ prepareForEdition(ipFilter: IpFilterDTO) {
+ this.mode = 'edit';
+ this.ipFilter = ipFilter;
+
+ this.form.patchValue({
+ address: ipFilter.address,
+ description: ipFilter.description
+ });
+ }
+
+ cancel() {
+ this.modal.dismiss();
+ }
+
+ save() {
+ if (!this.form.valid) {
+ return;
+ }
+
+ const formValue = this.form.value;
+
+ const command: IpFilterCommandDTO = {
+ address: formValue.address!,
+ description: formValue.description!
+ };
+
+ let obs: Observable;
+ if (this.mode === 'create') {
+ obs = this.ipFilterService.createIpFilter(command);
+ } else {
+ obs = this.ipFilterService
+ .updateIpFilter(this.ipFilter!.id, command)
+ .pipe(switchMap(() => this.ipFilterService.getIpFilter(this.ipFilter!.id)));
+ }
+ obs.pipe(this.state.pendingUntilFinalization()).subscribe(ipFilter => {
+ this.modal.close(ipFilter);
+ });
+ }
+}
diff --git a/frontend/src/app/engine/edit-scan-mode-modal/edit-scan-mode-modal.component.html b/frontend/src/app/engine/edit-scan-mode-modal/edit-scan-mode-modal.component.html
new file mode 100644
index 0000000000..8a98711e34
--- /dev/null
+++ b/frontend/src/app/engine/edit-scan-mode-modal/edit-scan-mode-modal.component.html
@@ -0,0 +1,31 @@
+
+
+
diff --git a/frontend/src/app/engine/edit-scan-mode-modal/edit-scan-mode-modal.component.scss b/frontend/src/app/engine/edit-scan-mode-modal/edit-scan-mode-modal.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frontend/src/app/engine/edit-scan-mode-modal/edit-scan-mode-modal.component.spec.ts b/frontend/src/app/engine/edit-scan-mode-modal/edit-scan-mode-modal.component.spec.ts
new file mode 100644
index 0000000000..20fbb7dab4
--- /dev/null
+++ b/frontend/src/app/engine/edit-scan-mode-modal/edit-scan-mode-modal.component.spec.ts
@@ -0,0 +1,168 @@
+import { EditScanModeModalComponent } from './edit-scan-mode-modal.component';
+import { ComponentTester, createMock } from 'ngx-speculoos';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { fakeAsync, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { of } from 'rxjs';
+import { MockI18nModule } from '../../../i18n/mock-i18n.spec';
+import { DefaultValidationErrorsComponent } from '../../components/shared/default-validation-errors/default-validation-errors.component';
+import { ScanModeService } from '../../services/scan-mode.service';
+import { ScanModeCommandDTO, ScanModeDTO } from '../../model/scan-mode.model';
+
+class EditScanModeModalComponentTester extends ComponentTester {
+ constructor() {
+ super(EditScanModeModalComponent);
+ }
+
+ get name() {
+ return this.input('#name')!;
+ }
+
+ get description() {
+ return this.textarea('#description')!;
+ }
+
+ get cron() {
+ return this.input('#cron')!;
+ }
+
+ get validationErrors() {
+ return this.elements('val-errors div');
+ }
+
+ get save() {
+ return this.button('#save-button')!;
+ }
+
+ get cancel() {
+ return this.button('#cancel-button')!;
+ }
+}
+
+describe('EditScanModeModalComponent', () => {
+ let tester: EditScanModeModalComponentTester;
+ let fakeActiveModal: NgbActiveModal;
+ let scanModeService: jasmine.SpyObj;
+
+ beforeEach(() => {
+ fakeActiveModal = createMock(NgbActiveModal);
+ scanModeService = createMock(ScanModeService);
+
+ TestBed.configureTestingModule({
+ imports: [MockI18nModule, ReactiveFormsModule, HttpClientTestingModule, EditScanModeModalComponent, DefaultValidationErrorsComponent],
+ providers: [
+ { provide: NgbActiveModal, useValue: fakeActiveModal },
+ { provide: ScanModeService, useValue: scanModeService }
+ ]
+ });
+
+ TestBed.createComponent(DefaultValidationErrorsComponent).detectChanges();
+
+ tester = new EditScanModeModalComponentTester();
+ });
+
+ describe('create mode', () => {
+ beforeEach(() => {
+ tester.componentInstance.prepareForCreation();
+ tester.detectChanges();
+ });
+
+ it('should have an empty form', () => {
+ expect(tester.name).toHaveValue('');
+ expect(tester.description).toHaveValue('');
+ expect(tester.cron).toHaveValue('');
+ });
+
+ it('should not save if invalid', () => {
+ tester.save.click();
+
+ // name, cron
+ expect(tester.validationErrors.length).toBe(2);
+ expect(fakeActiveModal.close).not.toHaveBeenCalled();
+ });
+
+ it('should save if valid', fakeAsync(() => {
+ tester.name.fillWith('test');
+ tester.description.fillWith('desc');
+ tester.cron.fillWith('* * * * * *');
+
+ tester.detectChanges();
+
+ const createdScanMode = {
+ id: 'id1'
+ } as ScanModeDTO;
+ scanModeService.createScanMode.and.returnValue(of(createdScanMode));
+
+ tester.save.click();
+
+ const expectedCommand: ScanModeCommandDTO = {
+ name: 'test',
+ description: 'desc',
+ cron: '* * * * * *'
+ };
+
+ expect(scanModeService.createScanMode).toHaveBeenCalledWith(expectedCommand);
+ expect(fakeActiveModal.close).toHaveBeenCalledWith(createdScanMode);
+ }));
+
+ it('should cancel', () => {
+ tester.cancel.click();
+ expect(fakeActiveModal.dismiss).toHaveBeenCalled();
+ });
+ });
+
+ describe('edit mode', () => {
+ const scanModeToUpdate: ScanModeDTO = {
+ id: 'id1',
+ name: 'proxy1',
+ description: 'My Proxy 1',
+ cron: '* * * * * *'
+ };
+
+ beforeEach(() => {
+ scanModeService.getScanMode.and.returnValue(of(scanModeToUpdate));
+
+ tester.componentInstance.prepareForEdition(scanModeToUpdate);
+ tester.detectChanges();
+ });
+
+ it('should have a populated form', () => {
+ expect(tester.name).toHaveValue(scanModeToUpdate.name);
+ expect(tester.description).toHaveValue(scanModeToUpdate.description);
+ });
+
+ it('should not save if invalid', () => {
+ tester.name.fillWith('');
+ tester.save.click();
+
+ // name
+ expect(tester.validationErrors.length).toBe(1);
+ expect(fakeActiveModal.close).not.toHaveBeenCalled();
+ });
+
+ it('should save if valid', fakeAsync(() => {
+ scanModeService.updateScanMode.and.returnValue(of(undefined));
+
+ tester.name.fillWith('Scan Mode 1 (updated)');
+ tester.description.fillWith('A longer and updated description of my Scan Mode');
+
+ tester.save.click();
+
+ const expectedCommand: ScanModeCommandDTO = {
+ name: 'Scan Mode 1 (updated)',
+ description: 'A longer and updated description of my Scan Mode',
+ cron: scanModeToUpdate.cron
+ };
+
+ expect(scanModeService.updateScanMode).toHaveBeenCalledWith('id1', expectedCommand);
+ expect(scanModeService.getScanMode).toHaveBeenCalledWith('id1');
+ expect(fakeActiveModal.close).toHaveBeenCalledWith(scanModeToUpdate);
+ }));
+
+ it('should cancel', () => {
+ tester.cancel.click();
+ expect(fakeActiveModal.dismiss).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/frontend/src/app/engine/edit-scan-mode-modal/edit-scan-mode-modal.component.ts b/frontend/src/app/engine/edit-scan-mode-modal/edit-scan-mode-modal.component.ts
new file mode 100644
index 0000000000..f9f8e99aaf
--- /dev/null
+++ b/frontend/src/app/engine/edit-scan-mode-modal/edit-scan-mode-modal.component.ts
@@ -0,0 +1,80 @@
+import { Component } from '@angular/core';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
+import { Observable, switchMap } from 'rxjs';
+import { ObservableState, SaveButtonComponent } from '../../components/shared/save-button/save-button.component';
+import { ValidationErrorsComponent } from 'ngx-valdemort';
+import { TranslateModule } from '@ngx-translate/core';
+import { ScanModeService } from '../../services/scan-mode.service';
+import { ScanModeCommandDTO, ScanModeDTO } from '../../model/scan-mode.model';
+
+@Component({
+ selector: 'oib-edit-scan-mode-modal',
+ templateUrl: './edit-scan-mode-modal.component.html',
+ styleUrls: ['./edit-scan-mode-modal.component.scss'],
+ imports: [ReactiveFormsModule, TranslateModule, ValidationErrorsComponent, SaveButtonComponent],
+ standalone: true
+})
+export class EditScanModeModalComponent {
+ mode: 'create' | 'edit' = 'create';
+ state = new ObservableState();
+ scanMode: ScanModeDTO | null = null;
+ form = this.fb.group({
+ name: ['', Validators.required],
+ description: '',
+ cron: ['', Validators.required]
+ });
+
+ constructor(private modal: NgbActiveModal, private fb: FormBuilder, private scanModeService: ScanModeService) {}
+
+ /**
+ * Prepares the component for creation.
+ */
+ prepareForCreation() {
+ this.mode = 'create';
+ }
+
+ /**
+ * Prepares the component for edition.
+ */
+ prepareForEdition(scanMode: ScanModeDTO) {
+ this.mode = 'edit';
+ this.scanMode = scanMode;
+
+ this.form.patchValue({
+ name: scanMode.name,
+ description: scanMode.description,
+ cron: scanMode.cron
+ });
+ }
+
+ cancel() {
+ this.modal.dismiss();
+ }
+
+ save() {
+ if (!this.form.valid) {
+ return;
+ }
+
+ const formValue = this.form.value;
+
+ const command: ScanModeCommandDTO = {
+ name: formValue.name!,
+ description: formValue.description!,
+ cron: formValue.cron!
+ };
+
+ let obs: Observable;
+ if (this.mode === 'create') {
+ obs = this.scanModeService.createScanMode(command);
+ } else {
+ obs = this.scanModeService
+ .updateScanMode(this.scanMode!.id, command)
+ .pipe(switchMap(() => this.scanModeService.getScanMode(this.scanMode!.id)));
+ }
+ obs.pipe(this.state.pendingUntilFinalization()).subscribe(scanMode => {
+ this.modal.close(scanMode);
+ });
+ }
+}
diff --git a/frontend/src/app/engine/engine.component.html b/frontend/src/app/engine/engine.component.html
index ef4c39402c..828808cb8e 100644
--- a/frontend/src/app/engine/engine.component.html
+++ b/frontend/src/app/engine/engine.component.html
@@ -60,7 +60,10 @@
-
+
+
+
+
diff --git a/frontend/src/app/engine/engine.component.spec.ts b/frontend/src/app/engine/engine.component.spec.ts
index 88bd9a94f6..7f83073732 100644
--- a/frontend/src/app/engine/engine.component.spec.ts
+++ b/frontend/src/app/engine/engine.component.spec.ts
@@ -9,6 +9,9 @@ import { provideTestingI18n } from '../../i18n/mock-i18n';
import { ProxyListComponent } from './proxy-list/proxy-list.component';
import { provideHttpClient } from '@angular/common/http';
import { provideRouter } from '@angular/router';
+import { ScanModeListComponent } from './scan-mode-list/scan-mode-list.component';
+import { ExternalSourceListComponent } from './external-source-list/external-source-list.component';
+import { IpFilterListComponent } from './ip-filter-list/ip-filter-list.component';
class EngineComponentTester extends ComponentTester {
constructor() {
@@ -58,6 +61,18 @@ class EngineComponentTester extends ComponentTester {
get proxyList() {
return this.element(ProxyListComponent);
}
+
+ get scanModeList() {
+ return this.element(ScanModeListComponent);
+ }
+
+ get externalSourceList() {
+ return this.element(ExternalSourceListComponent);
+ }
+
+ get ipFilterList() {
+ return this.element(IpFilterListComponent);
+ }
}
describe('EngineComponent', () => {
@@ -119,5 +134,8 @@ describe('EngineComponent', () => {
expect(tester.editButton).toContainText('Edit settings');
expect(tester.proxyList).toBeDefined();
+ expect(tester.scanModeList).toBeDefined();
+ expect(tester.externalSourceList).toBeDefined();
+ expect(tester.ipFilterList).toBeDefined();
});
});
diff --git a/frontend/src/app/engine/engine.component.ts b/frontend/src/app/engine/engine.component.ts
index bce5a1fd39..c57d9d8ac0 100644
--- a/frontend/src/app/engine/engine.component.ts
+++ b/frontend/src/app/engine/engine.component.ts
@@ -5,11 +5,22 @@ import { RouterLink } from '@angular/router';
import { EngineService } from '../services/engine.service';
import { EngineSettingsDTO } from '../model/engine.model';
import { NgIf } from '@angular/common';
+import { ScanModeListComponent } from './scan-mode-list/scan-mode-list.component';
+import { ExternalSourceListComponent } from './external-source-list/external-source-list.component';
+import { IpFilterListComponent } from './ip-filter-list/ip-filter-list.component';
@Component({
selector: 'oib-engine',
standalone: true,
- imports: [NgIf, TranslateModule, RouterLink, ProxyListComponent],
+ imports: [
+ NgIf,
+ TranslateModule,
+ RouterLink,
+ ProxyListComponent,
+ ScanModeListComponent,
+ ExternalSourceListComponent,
+ IpFilterListComponent
+ ],
templateUrl: './engine.component.html',
styleUrls: ['./engine.component.scss']
})
diff --git a/frontend/src/app/engine/external-source-list/external-source-list.component.html b/frontend/src/app/engine/external-source-list/external-source-list.component.html
new file mode 100644
index 0000000000..b02838c043
--- /dev/null
+++ b/frontend/src/app/engine/external-source-list/external-source-list.component.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+ |
+ |
+ |
+
+
+
+
+ {{ externalSource.reference }} |
+ {{ externalSource.description }} |
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
diff --git a/frontend/src/app/engine/external-source-list/external-source-list.component.scss b/frontend/src/app/engine/external-source-list/external-source-list.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frontend/src/app/engine/external-source-list/external-source-list.component.spec.ts b/frontend/src/app/engine/external-source-list/external-source-list.component.spec.ts
new file mode 100644
index 0000000000..c3af0be1cb
--- /dev/null
+++ b/frontend/src/app/engine/external-source-list/external-source-list.component.spec.ts
@@ -0,0 +1,77 @@
+import { TestBed } from '@angular/core/testing';
+
+import { ExternalSourceListComponent } from './external-source-list.component';
+import { provideTestingI18n } from '../../../i18n/mock-i18n';
+import { ComponentTester, createMock } from 'ngx-speculoos';
+import { of } from 'rxjs';
+import { ExternalSourceService } from '../../services/external-source.service';
+import { ExternalSourceDTO } from '../../model/external-sources.model';
+
+class ExternalSourceListComponentTester extends ComponentTester {
+ constructor() {
+ super(ExternalSourceListComponent);
+ }
+
+ get title() {
+ return this.element('h2')!;
+ }
+
+ get addExternalSource() {
+ return this.button('#add-external-source')!;
+ }
+
+ get noExternalSource() {
+ return this.element('#no-external-source');
+ }
+ get externalSources() {
+ return this.elements('tbody tr');
+ }
+}
+
+describe('ExternalSourceListComponent', () => {
+ let tester: ExternalSourceListComponentTester;
+ let externalSourceService: jasmine.SpyObj;
+
+ beforeEach(() => {
+ externalSourceService = createMock(ExternalSourceService);
+
+ TestBed.configureTestingModule({
+ imports: [ExternalSourceListComponent],
+ providers: [provideTestingI18n(), { provide: ExternalSourceService, useValue: externalSourceService }]
+ });
+
+ tester = new ExternalSourceListComponentTester();
+ });
+
+ it('should display a list of external sources', () => {
+ const externalSources: Array = [
+ {
+ id: 'id1',
+ reference: 'ref1',
+ description: 'My external source 1'
+ },
+ {
+ id: 'id2',
+ reference: 'ref2',
+ description: 'My external source 2'
+ }
+ ];
+
+ externalSourceService.getExternalSources.and.returnValue(of(externalSources));
+ tester.detectChanges();
+
+ expect(tester.title).toContainText('External source list');
+ expect(tester.externalSources.length).toEqual(2);
+ expect(tester.externalSources[0].elements('td').length).toEqual(3);
+ expect(tester.externalSources[1].elements('td')[0]).toContainText('ref2');
+ expect(tester.externalSources[1].elements('td')[1]).toContainText('My external source 2');
+ });
+
+ it('should display an empty list', () => {
+ externalSourceService.getExternalSources.and.returnValue(of([]));
+ tester.detectChanges();
+
+ expect(tester.title).toContainText('External source list');
+ expect(tester.noExternalSource).toContainText('No external source');
+ });
+});
diff --git a/frontend/src/app/engine/external-source-list/external-source-list.component.ts b/frontend/src/app/engine/external-source-list/external-source-list.component.ts
new file mode 100644
index 0000000000..e752853300
--- /dev/null
+++ b/frontend/src/app/engine/external-source-list/external-source-list.component.ts
@@ -0,0 +1,92 @@
+import { Component, OnInit } from '@angular/core';
+import { NgForOf, NgIf } from '@angular/common';
+import { switchMap } from 'rxjs';
+import { Modal, ModalService } from '../../components/shared/modal.service';
+import { ConfirmationService } from '../../components/shared/confirmation.service';
+import { NotificationService } from '../../components/shared/notification.service';
+import { TranslateModule } from '@ngx-translate/core';
+import { ExternalSourceDTO } from '../../model/external-sources.model';
+import { ExternalSourceService } from '../../services/external-source.service';
+import { EditExternalSourceModalComponent } from '../edit-external-source-modal/edit-external-source-modal.component';
+
+@Component({
+ selector: 'oib-external-source-list',
+ standalone: true,
+ imports: [NgIf, NgForOf, TranslateModule],
+ templateUrl: './external-source-list.component.html',
+ styleUrls: ['./external-source-list.component.scss']
+})
+export class ExternalSourceListComponent implements OnInit {
+ externalSources: Array = [];
+
+ constructor(
+ private confirmationService: ConfirmationService,
+ private modalService: ModalService,
+ private notificationService: NotificationService,
+ private externalSourceService: ExternalSourceService
+ ) {}
+
+ ngOnInit() {
+ this.externalSourceService.getExternalSources().subscribe(externalSources => {
+ this.externalSources = externalSources;
+ });
+ }
+
+ /**
+ * Open a modal to edit an external source
+ */
+ openEditExternalSourceModal(externalSource: ExternalSourceDTO) {
+ const modalRef = this.modalService.open(EditExternalSourceModalComponent);
+ const component: EditExternalSourceModalComponent = modalRef.componentInstance;
+ component.prepareForEdition(externalSource);
+ this.refreshAfterEditExternalSourceModalClosed(modalRef, 'updated');
+ }
+
+ /**
+ * Open a modal to create an external source
+ */
+ openCreationExternalSourceModal() {
+ const modalRef = this.modalService.open(EditExternalSourceModalComponent);
+ const component: EditExternalSourceModalComponent = modalRef.componentInstance;
+ component.prepareForCreation();
+ this.refreshAfterEditExternalSourceModalClosed(modalRef, 'created');
+ }
+
+ /**
+ * Refresh the IP filter list when the external source is edited
+ */
+ private refreshAfterEditExternalSourceModalClosed(modalRef: Modal, mode: 'created' | 'updated') {
+ modalRef.result.subscribe((ipFilter: ExternalSourceDTO) => {
+ this.externalSourceService.getExternalSources().subscribe(externalSources => {
+ this.externalSources = externalSources;
+ });
+ this.notificationService.success(`engine.external-source.${mode}`, {
+ reference: ipFilter.reference
+ });
+ });
+ }
+
+ /**
+ * Delete an IP Filter by its ID
+ */
+ deleteExternalSource(externalSource: ExternalSourceDTO) {
+ this.confirmationService
+ .confirm({
+ messageKey: 'engine.external-source.confirm-deletion',
+ interpolateParams: { reference: externalSource.reference }
+ })
+ .pipe(
+ switchMap(() => {
+ return this.externalSourceService.deleteExternalSource(externalSource.id);
+ })
+ )
+ .subscribe(() => {
+ this.externalSourceService.getExternalSources().subscribe(externalSources => {
+ this.externalSources = externalSources;
+ });
+ this.notificationService.success('engine.external-source.deleted', {
+ address: externalSource.reference
+ });
+ });
+ }
+}
diff --git a/frontend/src/app/engine/ip-filter-list/ip-filter-list.component.html b/frontend/src/app/engine/ip-filter-list/ip-filter-list.component.html
new file mode 100644
index 0000000000..6dd18de20c
--- /dev/null
+++ b/frontend/src/app/engine/ip-filter-list/ip-filter-list.component.html
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+ |
+ |
+ |
+
+
+
+
+ {{ ipFilter.address }} |
+ {{ ipFilter.description }} |
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
diff --git a/frontend/src/app/engine/ip-filter-list/ip-filter-list.component.scss b/frontend/src/app/engine/ip-filter-list/ip-filter-list.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frontend/src/app/engine/ip-filter-list/ip-filter-list.component.spec.ts b/frontend/src/app/engine/ip-filter-list/ip-filter-list.component.spec.ts
new file mode 100644
index 0000000000..8daee1747e
--- /dev/null
+++ b/frontend/src/app/engine/ip-filter-list/ip-filter-list.component.spec.ts
@@ -0,0 +1,77 @@
+import { TestBed } from '@angular/core/testing';
+
+import { IpFilterListComponent } from './ip-filter-list.component';
+import { provideTestingI18n } from '../../../i18n/mock-i18n';
+import { ComponentTester, createMock } from 'ngx-speculoos';
+import { of } from 'rxjs';
+import { IpFilterService } from '../../services/ip-filter.service';
+import { IpFilterDTO } from '../../model/ip-filter.model';
+
+class IpFilterListComponentTester extends ComponentTester {
+ constructor() {
+ super(IpFilterListComponent);
+ }
+
+ get title() {
+ return this.element('h2')!;
+ }
+
+ get addIpFilter() {
+ return this.button('#add-ip-filter')!;
+ }
+
+ get noIpFilter() {
+ return this.element('#no-ip-filter');
+ }
+ get ipFilters() {
+ return this.elements('tbody tr');
+ }
+}
+
+describe('IpFilterListComponent', () => {
+ let tester: IpFilterListComponentTester;
+ let ipFilterService: jasmine.SpyObj;
+
+ beforeEach(() => {
+ ipFilterService = createMock(IpFilterService);
+
+ TestBed.configureTestingModule({
+ imports: [IpFilterListComponent],
+ providers: [provideTestingI18n(), { provide: IpFilterService, useValue: ipFilterService }]
+ });
+
+ tester = new IpFilterListComponentTester();
+ });
+
+ it('should display a list of ip filters', () => {
+ const ipFilters: Array = [
+ {
+ id: 'id1',
+ address: 'http://localhost',
+ description: 'My IP filter 1'
+ },
+ {
+ id: 'id2',
+ address: 'http://localhost',
+ description: 'My IP filter 2'
+ }
+ ];
+
+ ipFilterService.getIpFilters.and.returnValue(of(ipFilters));
+ tester.detectChanges();
+
+ expect(tester.title).toContainText('IP filter list');
+ expect(tester.ipFilters.length).toEqual(2);
+ expect(tester.ipFilters[0].elements('td').length).toEqual(3);
+ expect(tester.ipFilters[1].elements('td')[0]).toContainText('http://localhost');
+ expect(tester.ipFilters[1].elements('td')[1]).toContainText('My IP filter 2');
+ });
+
+ it('should display an empty list', () => {
+ ipFilterService.getIpFilters.and.returnValue(of([]));
+ tester.detectChanges();
+
+ expect(tester.title).toContainText('IP filter list');
+ expect(tester.noIpFilter).toContainText('No IP filter');
+ });
+});
diff --git a/frontend/src/app/engine/ip-filter-list/ip-filter-list.component.ts b/frontend/src/app/engine/ip-filter-list/ip-filter-list.component.ts
new file mode 100644
index 0000000000..faf734ac80
--- /dev/null
+++ b/frontend/src/app/engine/ip-filter-list/ip-filter-list.component.ts
@@ -0,0 +1,92 @@
+import { Component, OnInit } from '@angular/core';
+import { NgForOf, NgIf } from '@angular/common';
+import { switchMap } from 'rxjs';
+import { Modal, ModalService } from '../../components/shared/modal.service';
+import { ConfirmationService } from '../../components/shared/confirmation.service';
+import { NotificationService } from '../../components/shared/notification.service';
+import { TranslateModule } from '@ngx-translate/core';
+import { IpFilterService } from '../../services/ip-filter.service';
+import { IpFilterDTO } from '../../model/ip-filter.model';
+import { EditIpFilterModalComponent } from '../edit-ip-filter-modal/edit-ip-filter-modal.component';
+
+@Component({
+ selector: 'oib-ip-filter-list',
+ standalone: true,
+ imports: [NgIf, NgForOf, TranslateModule],
+ templateUrl: './ip-filter-list.component.html',
+ styleUrls: ['./ip-filter-list.component.scss']
+})
+export class IpFilterListComponent implements OnInit {
+ ipFilters: Array = [];
+
+ constructor(
+ private confirmationService: ConfirmationService,
+ private modalService: ModalService,
+ private notificationService: NotificationService,
+ private ipFilterService: IpFilterService
+ ) {}
+
+ ngOnInit() {
+ this.ipFilterService.getIpFilters().subscribe(ipFilterList => {
+ this.ipFilters = ipFilterList;
+ });
+ }
+
+ /**
+ * Open a modal to edit an IP filter
+ */
+ openEditIpFilterModal(ipFilter: IpFilterDTO) {
+ const modalRef = this.modalService.open(EditIpFilterModalComponent);
+ const component: EditIpFilterModalComponent = modalRef.componentInstance;
+ component.prepareForEdition(ipFilter);
+ this.refreshAfterEditIpFilterModalClosed(modalRef, 'updated');
+ }
+
+ /**
+ * Open a modal to create an IP filter
+ */
+ openCreationIpFilterModal() {
+ const modalRef = this.modalService.open(EditIpFilterModalComponent);
+ const component: EditIpFilterModalComponent = modalRef.componentInstance;
+ component.prepareForCreation();
+ this.refreshAfterEditIpFilterModalClosed(modalRef, 'created');
+ }
+
+ /**
+ * Refresh the IP filter list when the IP filter is edited
+ */
+ private refreshAfterEditIpFilterModalClosed(modalRef: Modal, mode: 'created' | 'updated') {
+ modalRef.result.subscribe((ipFilter: IpFilterDTO) => {
+ this.ipFilterService.getIpFilters().subscribe(ipFilters => {
+ this.ipFilters = ipFilters;
+ });
+ this.notificationService.success(`engine.ip-filter.${mode}`, {
+ address: ipFilter.address
+ });
+ });
+ }
+
+ /**
+ * Delete an IP Filter by its ID
+ */
+ deleteIpFilter(ipFilter: IpFilterDTO) {
+ this.confirmationService
+ .confirm({
+ messageKey: 'engine.ip-filter.confirm-deletion',
+ interpolateParams: { address: ipFilter.address }
+ })
+ .pipe(
+ switchMap(() => {
+ return this.ipFilterService.deleteIpFilter(ipFilter.id);
+ })
+ )
+ .subscribe(() => {
+ this.ipFilterService.getIpFilters().subscribe(ipFilters => {
+ this.ipFilters = ipFilters;
+ });
+ this.notificationService.success('engine.ip-filter.deleted', {
+ address: ipFilter.address
+ });
+ });
+ }
+}
diff --git a/frontend/src/app/engine/proxy-list/proxy-list.component.html b/frontend/src/app/engine/proxy-list/proxy-list.component.html
index ccf5023a11..c1705f2754 100644
--- a/frontend/src/app/engine/proxy-list/proxy-list.component.html
+++ b/frontend/src/app/engine/proxy-list/proxy-list.component.html
@@ -1,5 +1,5 @@
-