diff --git a/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.ts b/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.ts
index b7e0f8e7eb1..41fdd71160c 100644
--- a/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.ts
+++ b/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.ts
@@ -1,11 +1,8 @@
import {
ChangeDetectorRef,
Component,
- EventEmitter,
- Input,
OnDestroy,
OnInit,
- Output,
TemplateRef,
ViewChild
} from '@angular/core';
@@ -29,6 +26,7 @@ import {combineLatestWith, Observable, Subscription} from 'rxjs';
import {PaginationService} from '../../../core/pagination/pagination.service';
import {FindListOptions} from '../../../core/data/find-list-options.model';
import {NotifyServicePattern} from '../ldn-services-model/ldn-service-patterns.model';
+import { IpV4Validator } from '../../../shared/utils/ipV4.validator';
/**
@@ -48,35 +46,26 @@ import {NotifyServicePattern} from '../ldn-services-model/ldn-service-patterns.m
})
export class LdnServiceFormComponent implements OnInit, OnDestroy {
formModel: FormGroup;
+
@ViewChild('confirmModal', {static: true}) confirmModal: TemplateRef
;
@ViewChild('resetFormModal', {static: true}) resetFormModal: TemplateRef;
public inboundPatterns: string[] = notifyPatterns;
public isNewService: boolean;
public areControlsInitialized: boolean;
- itemfiltersRD$: Observable>>;
- config: FindListOptions = Object.assign(new FindListOptions(), {
+ public itemfiltersRD$: Observable>>;
+ public config: FindListOptions = Object.assign(new FindListOptions(), {
elementsPerPage: 20
});
+ public markedForDeletionInboundPattern: number[] = [];
+ public selectedInboundPatterns: string[];
+ public selectedInboundItemfilters: string[];
- @Input() public name: string;
- @Input() public description: string;
- @Input() public url: string;
- @Input() public ldnUrl: string;
- @Input() public score: number;
- @Input() public inboundPattern: string;
- @Input() public constraint: string;
- @Input() public automatic: boolean;
- @Input() public headerKey: string;
- @Output() submitForm: EventEmitter = new EventEmitter();
- @Output() cancelForm: EventEmitter = new EventEmitter();
- markedForDeletionInboundPattern: number[] = [];
- selectedInboundPatterns: string[];
- selectedInboundItemfilters: string[];
protected serviceId: string;
+
private deletedInboundPatterns: number[] = [];
private modalRef: any;
- private service: LdnService;
+ private ldnService: LdnService;
private selectPatternDefaultLabeli18Key = 'ldn-service.form.label.placeholder.default-select';
private routeSubscription: Subscription;
@@ -99,6 +88,8 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy {
description: [''],
url: ['', Validators.required],
ldnUrl: ['', Validators.required],
+ lowerIp: ['', [Validators.required, new IpV4Validator()]],
+ upperIp: ['', [Validators.required, new IpV4Validator()]],
score: ['', [Validators.required, Validators.pattern('^0*(\.[0-9]+)?$|^1(\.0+)?$')]], inboundPattern: [''],
constraintPattern: [''],
enabled: [''],
@@ -139,15 +130,9 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy {
*/
createService() {
this.formModel.markAllAsTouched();
-
- const name = this.formModel.get('name').value;
- const url = this.formModel.get('url').value;
- const score = this.formModel.get('score').value;
- const ldnUrl = this.formModel.get('ldnUrl').value;
-
const hasInboundPattern = this.checkPatterns(this.formModel.get('notifyServiceInboundPatterns') as FormArray);
- if (!name || !url || !ldnUrl || (!score && score !== 0) || this.formModel.get('score').invalid) {
+ if (this.formModel.invalid) {
this.closeModal();
return;
}
@@ -177,9 +162,8 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy {
if (rd.hasSucceeded) {
this.notificationService.success(this.translateService.get('ldn-service-notification.created.success.title'),
this.translateService.get('ldn-service-notification.created.success.body'));
-
- this.sendBack();
this.closeModal();
+ this.sendBack();
} else {
this.notificationService.error(this.translateService.get('ldn-service-notification.created.failure.title'),
this.translateService.get('ldn-service-notification.created.failure.body'));
@@ -214,18 +198,21 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy {
).subscribe(
(data: RemoteData) => {
if (data.hasSucceeded) {
- this.service = data.payload;
+ this.ldnService = data.payload;
this.formModel.patchValue({
- id: this.service.id,
- name: this.service.name,
- description: this.service.description,
- url: this.service.url,
- score: this.service.score, ldnUrl: this.service.ldnUrl,
- type: this.service.type,
- enabled: this.service.enabled
+ id: this.ldnService.id,
+ name: this.ldnService.name,
+ description: this.ldnService.description,
+ url: this.ldnService.url,
+ score: this.ldnService.score,
+ ldnUrl: this.ldnService.ldnUrl,
+ type: this.ldnService.type,
+ enabled: this.ldnService.enabled,
+ lowerIp: this.ldnService.lowerIp,
+ upperIp: this.ldnService.upperIp,
});
- this.filterPatternObjectsAndPickLabel('notifyServiceInboundPatterns');
+ this.filterPatternObjectsAndAssignLabel('notifyServiceInboundPatterns');
}
},
);
@@ -235,11 +222,11 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy {
* Filters pattern objects, initializes form groups, assigns labels, and adds them to the specified form array so the correct string is shown in the dropdown..
* @param formArrayName - The name of the form array to be populated
*/
- filterPatternObjectsAndPickLabel(formArrayName: string) {
+ filterPatternObjectsAndAssignLabel(formArrayName: string) {
const PatternsArray = this.formModel.get(formArrayName) as FormArray;
PatternsArray.clear();
- let servicesToUse;
- servicesToUse = this.service.notifyServiceInboundPatterns;
+
+ let servicesToUse = this.ldnService.notifyServiceInboundPatterns;
servicesToUse.forEach((patternObj: NotifyServicePattern) => {
let patternFormGroup;
@@ -253,8 +240,6 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy {
PatternsArray.push(patternFormGroup);
this.cdRef.detectChanges();
});
-
-
}
/**
@@ -269,6 +254,8 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy {
this.createReplaceOperation(patchOperations, 'ldnUrl', '/ldnurl');
this.createReplaceOperation(patchOperations, 'url', '/url');
this.createReplaceOperation(patchOperations, 'score', '/score');
+ this.createReplaceOperation(patchOperations, 'lowerIp', '/lowerIp');
+ this.createReplaceOperation(patchOperations, 'upperIp', '/upperIp');
this.handlePatterns(patchOperations, 'notifyServiceInboundPatterns');
this.deletedInboundPatterns.forEach(index => {
@@ -342,11 +329,10 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy {
value: newStatus,
};
- this.ldnServicesService.patch(this.service, [patchOperation]).pipe(
+ this.ldnServicesService.patch(this.ldnService, [patchOperation]).pipe(
getFirstCompletedRemoteData()
).subscribe(
() => {
-
this.formModel.get('enabled').setValue(newStatus);
this.cdRef.detectChanges();
}
@@ -402,7 +388,7 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy {
return;
}
- this.ldnServicesService.patch(this.service, patchOperations).pipe(
+ this.ldnServicesService.patch(this.ldnService, patchOperations).pipe(
getFirstCompletedRemoteData()
).subscribe(
(rd: RemoteData) => {
diff --git a/src/app/admin/admin-ldn-services/ldn-service-serviceMock/ldnServicesRD$-mock.ts b/src/app/admin/admin-ldn-services/ldn-service-serviceMock/ldnServicesRD$-mock.ts
index 3b3f3723a72..d8534dde037 100644
--- a/src/app/admin/admin-ldn-services/ldn-service-serviceMock/ldnServicesRD$-mock.ts
+++ b/src/app/admin/admin-ldn-services/ldn-service-serviceMock/ldnServicesRD$-mock.ts
@@ -10,6 +10,8 @@ export const mockLdnService: LdnService = {
enabled: false,
score: 0,
id: 1,
+ lowerIp: '192.0.2.146',
+ upperIp: '192.0.2.255',
name: 'Service Name',
description: 'Service Description',
url: 'Service URL',
@@ -45,6 +47,8 @@ export const mockLdnServices: LdnService[] = [{
enabled: false,
score: 0,
id: 1,
+ lowerIp: '192.0.2.146',
+ upperIp: '192.0.2.255',
name: 'Service Name',
description: 'Service Description',
url: 'Service URL',
@@ -75,6 +79,8 @@ export const mockLdnServices: LdnService[] = [{
enabled: false,
score: 0,
id: 2,
+ lowerIp: '192.0.2.146',
+ upperIp: '192.0.2.255',
name: 'Service Name',
description: 'Service Description',
url: 'Service URL',
diff --git a/src/app/admin/admin-ldn-services/ldn-services-data/ldn-itemfilter-data.service.spec.ts b/src/app/admin/admin-ldn-services/ldn-services-data/ldn-itemfilter-data.service.spec.ts
new file mode 100644
index 00000000000..b5b08817271
--- /dev/null
+++ b/src/app/admin/admin-ldn-services/ldn-services-data/ldn-itemfilter-data.service.spec.ts
@@ -0,0 +1,89 @@
+import { TestScheduler } from 'rxjs/testing';
+import { LdnItemfiltersService } from './ldn-itemfilters-data.service';
+import { RequestService } from '../../../core/data/request.service';
+import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service';
+import { ObjectCacheService } from '../../../core/cache/object-cache.service';
+import { HALEndpointService } from '../../../core/shared/hal-endpoint.service';
+import { NotificationsService } from '../../../shared/notifications/notifications.service';
+import { RequestEntry } from '../../../core/data/request-entry.model';
+import { RemoteData } from '../../../core/data/remote-data';
+import { RequestEntryState } from '../../../core/data/request-entry-state.model';
+import { cold, getTestScheduler } from 'jasmine-marbles';
+import { RestResponse } from '../../../core/cache/response.models';
+import { of } from 'rxjs';
+import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
+import { FindAllData } from '../../../core/data/base/find-all-data';
+import { testFindAllDataImplementation } from '../../../core/data/base/find-all-data.spec';
+
+describe('LdnItemfiltersService test', () => {
+ let scheduler: TestScheduler;
+ let service: LdnItemfiltersService;
+ let requestService: RequestService;
+ let rdbService: RemoteDataBuildService;
+ let objectCache: ObjectCacheService;
+ let halService: HALEndpointService;
+ let notificationsService: NotificationsService;
+ let responseCacheEntry: RequestEntry;
+
+ const endpointURL = `https://rest.api/rest/api/ldn/itemfilters`;
+ const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
+
+ const remoteDataMocks = {
+ Success: new RemoteData(null, null, null, RequestEntryState.Success, null, null, 200),
+ };
+
+ function initTestService() {
+ return new LdnItemfiltersService(
+ requestService,
+ rdbService,
+ objectCache,
+ halService,
+ notificationsService,
+ );
+ }
+
+ beforeEach(() => {
+ scheduler = getTestScheduler();
+
+ objectCache = {} as ObjectCacheService;
+ notificationsService = {} as NotificationsService;
+ responseCacheEntry = new RequestEntry();
+ responseCacheEntry.request = { href: 'https://rest.api/' } as any;
+ responseCacheEntry.response = new RestResponse(true, 200, 'Success');
+
+ requestService = jasmine.createSpyObj('requestService', {
+ generateRequestId: requestUUID,
+ send: true,
+ removeByHrefSubstring: {},
+ getByHref: of(responseCacheEntry),
+ getByUUID: of(responseCacheEntry),
+ });
+
+ halService = jasmine.createSpyObj('halService', {
+ getEndpoint: of(endpointURL)
+ });
+
+ rdbService = jasmine.createSpyObj('rdbService', {
+ buildSingle: createSuccessfulRemoteDataObject$({}, 500),
+ buildList: cold('a', { a: remoteDataMocks.Success })
+ });
+
+
+ service = initTestService();
+ });
+
+ describe('composition', () => {
+ const initFindAllService = () => new LdnItemfiltersService(null, null, null, null, null) as unknown as FindAllData;
+ testFindAllDataImplementation(initFindAllService);
+ });
+
+ describe('get endpoint', () => {
+ it('should retrieve correct endpoint', (done) => {
+ service.getEndpoint().subscribe(() => {
+ expect(halService.getEndpoint).toHaveBeenCalledWith('itemfilters');
+ done();
+ });
+ });
+ });
+
+});
diff --git a/src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service.spec.ts b/src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service.spec.ts
new file mode 100644
index 00000000000..9d17fc244c4
--- /dev/null
+++ b/src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service.spec.ts
@@ -0,0 +1,116 @@
+import { TestScheduler } from 'rxjs/testing';
+import { RequestService } from '../../../core/data/request.service';
+import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service';
+import { ObjectCacheService } from '../../../core/cache/object-cache.service';
+import { HALEndpointService } from '../../../core/shared/hal-endpoint.service';
+import { NotificationsService } from '../../../shared/notifications/notifications.service';
+import { RequestEntry } from '../../../core/data/request-entry.model';
+import { RemoteData } from '../../../core/data/remote-data';
+import { RequestEntryState } from '../../../core/data/request-entry-state.model';
+import { cold, getTestScheduler } from 'jasmine-marbles';
+import { RestResponse } from '../../../core/cache/response.models';
+import { of as observableOf } from 'rxjs';
+import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
+import { FindAllData } from '../../../core/data/base/find-all-data';
+import { testFindAllDataImplementation } from '../../../core/data/base/find-all-data.spec';
+import { LdnServicesService } from './ldn-services-data.service';
+import { testDeleteDataImplementation } from '../../../core/data/base/delete-data.spec';
+import { DeleteData } from '../../../core/data/base/delete-data';
+import { testSearchDataImplementation } from '../../../core/data/base/search-data.spec';
+import { SearchData } from '../../../core/data/base/search-data';
+import { testPatchDataImplementation } from '../../../core/data/base/patch-data.spec';
+import { PatchData } from '../../../core/data/base/patch-data';
+import { CreateData } from '../../../core/data/base/create-data';
+import { testCreateDataImplementation } from '../../../core/data/base/create-data.spec';
+import { FindListOptions } from '../../../core/data/find-list-options.model';
+import { RequestParam } from '../../../core/cache/models/request-param.model';
+import { mockLdnService } from '../ldn-service-serviceMock/ldnServicesRD$-mock';
+import { createPaginatedList } from '../../../shared/testing/utils.test';
+
+
+describe('LdnServicesService test', () => {
+ let scheduler: TestScheduler;
+ let service: LdnServicesService;
+ let requestService: RequestService;
+ let rdbService: RemoteDataBuildService;
+ let objectCache: ObjectCacheService;
+ let halService: HALEndpointService;
+ let notificationsService: NotificationsService;
+ let responseCacheEntry: RequestEntry;
+
+ const endpointURL = `https://rest.api/rest/api/ldn/ldnservices`;
+ const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
+
+ const remoteDataMocks = {
+ Success: new RemoteData(null, null, null, RequestEntryState.Success, null, null, 200),
+ };
+
+ function initTestService() {
+ return new LdnServicesService(
+ requestService,
+ rdbService,
+ objectCache,
+ halService,
+ notificationsService,
+ );
+ }
+
+ beforeEach(() => {
+ scheduler = getTestScheduler();
+
+ objectCache = {} as ObjectCacheService;
+ notificationsService = {} as NotificationsService;
+ responseCacheEntry = new RequestEntry();
+ responseCacheEntry.request = { href: 'https://rest.api/' } as any;
+ responseCacheEntry.response = new RestResponse(true, 200, 'Success');
+
+ requestService = jasmine.createSpyObj('requestService', {
+ generateRequestId: requestUUID,
+ send: true,
+ removeByHrefSubstring: {},
+ getByHref: observableOf(responseCacheEntry),
+ getByUUID: observableOf(responseCacheEntry),
+ });
+
+ halService = jasmine.createSpyObj('halService', {
+ getEndpoint: observableOf(endpointURL)
+ });
+
+ rdbService = jasmine.createSpyObj('rdbService', {
+ buildSingle: createSuccessfulRemoteDataObject$({}, 500),
+ buildList: cold('a', { a: remoteDataMocks.Success })
+ });
+
+
+ service = initTestService();
+ });
+
+ describe('composition', () => {
+ const initFindAllService = () => new LdnServicesService(null, null, null, null, null) as unknown as FindAllData;
+ const initDeleteService = () => new LdnServicesService(null, null, null, null, null) as unknown as DeleteData;
+ const initSearchService = () => new LdnServicesService(null, null, null, null, null) as unknown as SearchData;
+ const initPatchService = () => new LdnServicesService(null, null, null, null, null) as unknown as PatchData;
+ const initCreateService = () => new LdnServicesService(null, null, null, null, null) as unknown as CreateData;
+
+ testFindAllDataImplementation(initFindAllService);
+ testDeleteDataImplementation(initDeleteService);
+ testSearchDataImplementation(initSearchService);
+ testPatchDataImplementation(initPatchService);
+ testCreateDataImplementation(initCreateService);
+ });
+
+ describe('custom methods', () => {
+ it('should find service by inbound pattern', (done) => {
+ const params = [new RequestParam('pattern', 'testPattern')];
+ const findListOptions = Object.assign(new FindListOptions(), {}, {searchParams: params});
+ spyOn(service, 'searchBy').and.returnValue(observableOf(null));
+ spyOn((service as any).searchData, 'searchBy').and.returnValue(createSuccessfulRemoteDataObject$(createPaginatedList([mockLdnService])));
+
+ service.findByInboundPattern('testPattern').subscribe(() => {
+ expect(service.searchBy).toHaveBeenCalledWith('byInboundPattern', findListOptions, undefined, undefined );
+ done();
+ });
+ });
+ });
+
+});
diff --git a/src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service.ts b/src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service.ts
index e7c2f471591..d1541e6bd81 100644
--- a/src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service.ts
+++ b/src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service.ts
@@ -29,8 +29,6 @@ import {Operation} from 'fast-json-patch';
import {RestRequestMethod} from '../../../core/data/rest-request-method';
import {CreateData, CreateDataImpl} from '../../../core/data/base/create-data';
import {LdnServiceConstrain} from '../ldn-services-model/ldn-service.constrain.model';
-import {getFirstCompletedRemoteData} from '../../../core/shared/operators';
-import {hasValue} from '../../../shared/empty.util';
import {SearchDataImpl} from '../../../core/data/base/search-data';
import {RequestParam} from '../../../core/cache/models/request-param.model';
@@ -77,10 +75,11 @@ export class LdnServicesService extends IdentifiableDataService impl
* Creates an LDN service by sending a POST request to the REST API.
*
* @param {LdnService} object - The LDN service object to be created.
+ * @param params Array with additional params to combine with query string
* @returns {Observable>} - Observable containing the result of the creation operation.
*/
- create(object: LdnService): Observable> {
- return this.createData.create(object);
+ create(object: LdnService, ...params: RequestParam[]): Observable> {
+ return this.createData.create(object, ...params);
}
/**
@@ -149,7 +148,7 @@ export class LdnServicesService extends IdentifiableDataService impl
findByInboundPattern(pattern: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig[]): Observable>> {
const params = [new RequestParam('pattern', pattern)];
const findListOptions = Object.assign(new FindListOptions(), options, {searchParams: params});
- return this.searchData.searchBy(this.findByPatternEndpoint, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
+ return this.searchBy(this.findByPatternEndpoint, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
/**
@@ -174,6 +173,25 @@ export class LdnServicesService extends IdentifiableDataService impl
return this.deleteData.deleteByHref(href, copyVirtualMetadata);
}
+
+ /**
+ * Make a new FindListRequest with given search method
+ *
+ * @param searchMethod The search method for the object
+ * @param options The [[FindListOptions]] object
+ * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
+ * no valid cached version. Defaults to true
+ * @param reRequestOnStale Whether or not the request should automatically be re-
+ * requested after the response becomes stale
+ * @param linksToFollow List of {@link FollowLinkConfig} that indicate which
+ * {@link HALLink}s should be automatically resolved
+ * @return {Observable>}
+ * Return an observable that emits response from the server
+ */
+ public searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig[]): Observable>> {
+ return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
+ }
+
public invoke(serviceName: string, serviceId: string, parameters: LdnServiceConstrain[], files: File[]): Observable> {
const requestId = this.requestService.generateRequestId();
this.getBrowseEndpoint().pipe(
@@ -188,15 +206,6 @@ export class LdnServicesService extends IdentifiableDataService impl
return this.rdbService.buildFromRequestUUID(requestId);
}
- public ldnServiceWithNameExistsAndCanExecute(scriptName: string): Observable {
- return this.findById(scriptName).pipe(
- getFirstCompletedRemoteData(),
- map((rd: RemoteData) => {
- return hasValue(rd.payload);
- }),
- );
- }
-
private getInvocationFormData(constrain: LdnServiceConstrain[], files: File[]): FormData {
const form: FormData = new FormData();
form.set('properties', JSON.stringify(constrain));
diff --git a/src/app/admin/admin-ldn-services/ldn-services-model/ldn-services.model.ts b/src/app/admin/admin-ldn-services/ldn-services-model/ldn-services.model.ts
index d297e1e0af3..8beb7fda687 100644
--- a/src/app/admin/admin-ldn-services/ldn-services-model/ldn-services.model.ts
+++ b/src/app/admin/admin-ldn-services/ldn-services-model/ldn-services.model.ts
@@ -46,6 +46,12 @@ export class LdnService extends CacheableObject {
@autoserialize
ldnUrl: string;
+ @autoserialize
+ lowerIp: string;
+
+ @autoserialize
+ upperIp: string;
+
@autoserialize
notifyServiceInboundPatterns?: NotifyServicePattern[];
diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts
index 7f59016f8cf..f348d0c9811 100644
--- a/src/app/shared/shared.module.ts
+++ b/src/app/shared/shared.module.ts
@@ -285,6 +285,7 @@ import { NgxPaginationModule } from 'ngx-pagination';
import { SplitPipe } from './utils/split.pipe';
import { ThemedUserMenuComponent } from './auth-nav-menu/user-menu/themed-user-menu.component';
import { ThemedLangSwitchComponent } from './lang-switch/themed-lang-switch.component';
+import { IpV4Validator } from './utils/ipV4.validator';
const MODULES = [
CommonModule,
@@ -495,6 +496,7 @@ const DIRECTIVES = [
MetadataFieldValidator,
HoverClassDirective,
ContextHelpDirective,
+ IpV4Validator
];
@NgModule({
diff --git a/src/app/shared/utils/ipV4.validator.spec.ts b/src/app/shared/utils/ipV4.validator.spec.ts
new file mode 100644
index 00000000000..93f5ee86e9a
--- /dev/null
+++ b/src/app/shared/utils/ipV4.validator.spec.ts
@@ -0,0 +1,36 @@
+import { IpV4Validator } from './ipV4.validator';
+import { TestBed } from '@angular/core/testing';
+import { UntypedFormControl, UntypedFormGroup } from '@angular/forms';
+
+
+describe('IpV4 validator', () => {
+
+ let ipV4Validator: IpV4Validator;
+ const validIp = '192.168.0.1';
+ const formGroup = new UntypedFormGroup({
+ ip: new UntypedFormControl(''),
+ });
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [
+ IpV4Validator,
+ ],
+ }).compileComponents();
+
+ ipV4Validator = TestBed.inject(IpV4Validator);
+ });
+
+ it('should return null for valid ipV4', () => {
+ formGroup.controls.ip.setValue(validIp);
+ expect(ipV4Validator.validate(formGroup.controls.ip as UntypedFormControl)).toBeNull();
+ });
+
+ it('should return {isValidIp: false} for invalid Ip', () => {
+ formGroup.controls.ip.setValue('100.260.45.1');
+ expect(ipV4Validator.validate(formGroup.controls.ip as UntypedFormControl)).toEqual({isValidIp: false});
+ formGroup.controls.ip.setValue('100');
+ expect(ipV4Validator.validate(formGroup.controls.ip as UntypedFormControl)).toEqual({isValidIp: false});
+ formGroup.controls.ip.setValue('testString');
+ expect(ipV4Validator.validate(formGroup.controls.ip as UntypedFormControl)).toEqual({isValidIp: false});
+ });
+});
diff --git a/src/app/shared/utils/ipV4.validator.ts b/src/app/shared/utils/ipV4.validator.ts
new file mode 100644
index 00000000000..170dbeb5470
--- /dev/null
+++ b/src/app/shared/utils/ipV4.validator.ts
@@ -0,0 +1,26 @@
+import {Directive} from '@angular/core';
+import {NG_VALIDATORS, Validator, UntypedFormControl} from '@angular/forms';
+
+@Directive({
+ // eslint-disable-next-line @angular-eslint/directive-selector
+ selector: '[ipV4format]',
+ providers: [
+ { provide: NG_VALIDATORS, useExisting: IpV4Validator, multi: true },
+ ]
+})
+/**
+ * Validator to validate if an Ip is in the right format
+ */
+export class IpV4Validator implements Validator {
+ validate(formControl: UntypedFormControl): {[key: string]: boolean} | null {
+ const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
+ const ipValue = formControl.value;
+ const ipParts = ipValue?.split('.');
+
+ if (ipv4Regex.test(ipValue) && ipParts.every(part => parseInt(part, 10) <= 255)) {
+ return null;
+ }
+
+ return {isValidIp: false};
+ }
+}
diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5
index 2a014622fa0..9afc51c3855 100644
--- a/src/assets/i18n/en.json5
+++ b/src/assets/i18n/en.json5
@@ -935,11 +935,14 @@
"ldn-new-service.form.label.name": "Name",
"ldn-new-service.form.label.description": "Description",
"ldn-new-service.form.label.url": "Service URL",
+ "ldn-new-service.form.label.ip-range": "Service IP range",
"ldn-new-service.form.label.score": "Level of trust",
"ldn-new-service.form.label.ldnUrl": "LDN Inbox URL",
"ldn-new-service.form.placeholder.name": "Please provide service name",
"ldn-new-service.form.placeholder.description": "Please provide a description regarding your service",
"ldn-new-service.form.placeholder.url": "Please input the URL for users to check out more information about the service",
+ "ldn-new-service.form.placeholder.lowerIp": "IPv4 range lower bound",
+ "ldn-new-service.form.placeholder.upperIp": "IPv4 range upper bound",
"ldn-new-service.form.placeholder.ldnUrl": "Please specify the URL of the LDN Inbox",
"ldn-new-service.form.placeholder.score": "Please enter a value between 0 and 1. Use the “.” as decimal separator",
"ldn-service.form.label.placeholder.default-select": "Select a pattern",
@@ -1001,6 +1004,8 @@
"ldn-new-service.form.label.automatic": "Automatic",
"ldn-new-service.form.error.name": "Name is required",
"ldn-new-service.form.error.url": "URL is required",
+ "ldn-new-service.form.error.ipRange": "Please enter a valid IP range",
+ "ldn-new-service.form.hint.ipRange": "Please enter a valid IpV4 in both range bounds (note: for single IP, please enter the same value in both fields)",
"ldn-new-service.form.error.ldnurl": "LDN URL is required",
"ldn-new-service.form.error.patterns": "At least a pattern is required",
"ldn-new-service.form.error.score": "Please enter a valid score (between 0 and 1). Use the “.” as decimal separator",