diff --git a/src/app/admin/admin-ldn-services/admin-ldn-services-routing.module.ts b/src/app/admin/admin-ldn-services/admin-ldn-services-routing.module.ts new file mode 100644 index 0000000000..740cd39621 --- /dev/null +++ b/src/app/admin/admin-ldn-services/admin-ldn-services-routing.module.ts @@ -0,0 +1,30 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { I18nBreadcrumbResolver } from 'src/app/core/breadcrumbs/i18n-breadcrumb.resolver'; +import { LdnServicesOverviewComponent } from './ldn-services-directory/ldn-services-directory.component'; +import { LdnServicesGuard } from './ldn-services-guard/ldn-services-guard.service'; +import { LdnServiceNewComponent } from './ldn-service-new/ldn-service-new.component'; + +@NgModule({ + imports: [ + RouterModule.forChild([ + { + path: '', + pathMatch: 'full', + component: LdnServicesOverviewComponent, + resolve: { breadcrumb: I18nBreadcrumbResolver }, + data: { title: 'ldn-registered-services.title', breadcrumbKey: 'ldn-registered-services.new' }, + canActivate: [LdnServicesGuard] + }, + { + path: 'new', + resolve: { breadcrumb: I18nBreadcrumbResolver }, + component: LdnServiceNewComponent, + data: { title: 'ldn-register-new-service.title', breadcrumbKey: 'ldn-register-new-service' } + }, + ]), + ] +}) +export class AdminLdnServicesRoutingModule { + +} diff --git a/src/app/admin/admin-ldn-services/admin-ldn-services.module.ts b/src/app/admin/admin-ldn-services/admin-ldn-services.module.ts new file mode 100644 index 0000000000..c03c16109e --- /dev/null +++ b/src/app/admin/admin-ldn-services/admin-ldn-services.module.ts @@ -0,0 +1,23 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { AdminLdnServicesRoutingModule } from './admin-ldn-services-routing.module'; +import { LdnServicesOverviewComponent } from './ldn-services-directory/ldn-services-directory.component'; +import { SharedModule } from '../../shared/shared.module'; +import { LdnServiceNewComponent } from './ldn-service-new/ldn-service-new.component'; +import { LdnServiceFormComponent } from './ldn-service-form/ldn-service-form.component'; + + + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + AdminLdnServicesRoutingModule, + ], + declarations: [ + LdnServicesOverviewComponent, + LdnServiceNewComponent, + LdnServiceFormComponent, + ] +}) +export class AdminLdnServicesModule { } diff --git a/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.html b/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.html new file mode 100644 index 0000000000..64146b31a1 --- /dev/null +++ b/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.html @@ -0,0 +1,96 @@ +
+ +
+ + + + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + + + + + + +
+ + + + + +
+ + - Remove + +
+ +
+ + + Add more + + +
+ + + + + + + + - Remove + + + +
+ + + Add more + + + +
diff --git a/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.scss b/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.scss new file mode 100644 index 0000000000..53bc21d1b3 --- /dev/null +++ b/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.scss @@ -0,0 +1,51 @@ + +form { + display: flex; + flex-direction: column; + align-items: flex-start; + margin: 0 auto; + max-width: 600px; + font-size: 14px; +} + + +.form-group input[type="text"], +.form-group select { + max-width: 100%; + width: 100%; + padding: 8px; + margin-bottom: 5px; + box-sizing: border-box; + font-size: 14px; +} + + +.description { + height: 9em; + width: 100%; +} + + +.form-group select { + position: relative; + z-index: 1; +} + + +.form-group select option { + font-weight: bold; +} + +.add-pattern-link{ + color: #0048ff; + cursor: pointer; + margin-left: 10px; +} +.remove-pattern-link{ + color: #e34949; + cursor: pointer; + margin-left: 10px; +} + + + diff --git a/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.spec.ts b/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.spec.ts new file mode 100644 index 0000000000..3ae834d642 --- /dev/null +++ b/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LdnServiceFormComponent } from './ldn-service-form.component'; + +describe('LdnServiceFormComponent', () => { + let component: LdnServiceFormComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ LdnServiceFormComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(LdnServiceFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); 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 new file mode 100644 index 0000000000..b17cc6902d --- /dev/null +++ b/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.ts @@ -0,0 +1,142 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup, Validators, FormArray } from '@angular/forms'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Router } from '@angular/router'; + +import { LdnServicesService } from '../ldn-services-data/ldn-services-data.service'; +import { LdnServiceConstraint } from '../ldn-services-model/ldn-service-constraint.model'; +import { notifyPatterns } from '../ldn-services-patterns/ldn-service-coar-patterns'; +import { LdnDirectoryService } from '../ldn-services-services/ldn-directory.service'; +import { LDN_SERVICE } from '../ldn-services-model/ldn-service.resource-type'; + +@Component({ + selector: 'ds-ldn-service-form', + templateUrl: './ldn-service-form.component.html', + styleUrls: ['./ldn-service-form.component.scss'], +}) +export class LdnServiceFormComponent implements OnInit { + formModel: FormGroup; + + showItemFilterDropdown = false; + + public inboundPatterns: object[] = notifyPatterns; + public outboundPatterns: object[] = notifyPatterns; + public itemFilterList: LdnServiceConstraint[]; + additionalOutboundPatterns: FormGroup[] = []; + additionalInboundPatterns: FormGroup[] = []; + + @Input() public name: string; + @Input() public description: string; + @Input() public url: string; + @Input() public ldnUrl: string; + @Input() public inboundPattern: string; + @Input() public outboundPattern: string; + @Input() public constraint: string; + @Input() public automatic: boolean; + + @Input() public headerKey: string; + + constructor( + private ldnServicesService: LdnServicesService, + private ldnDirectoryService: LdnDirectoryService, + private formBuilder: FormBuilder, + private http: HttpClient, + private router: Router + ) { + + this.formModel = this.formBuilder.group({ + id: [''], + name: ['', Validators.required], + description: ['', Validators.required], + url: ['', Validators.required], + ldnUrl: ['', Validators.required], + inboundPattern: [''], + outboundPattern: [''], + constraintPattern: [''], + notifyServiceInboundPatterns: this.formBuilder.array([this.createInboundPatternFormGroup()]), + notifyServiceOutboundPatterns: this.formBuilder.array([this.createOutboundPatternFormGroup()]), + type: LDN_SERVICE.value, + }); + } + + ngOnInit(): void { + this.ldnDirectoryService.getItemFilters().subscribe((itemFilters) => { + console.log(itemFilters); + this.itemFilterList = itemFilters._embedded.itemfilters.map((filter: { id: string; }) => ({ + name: filter.id + })); + console.log(this.itemFilterList); + }); + + } + + submitForm() { + this.formModel.removeControl('inboundPattern'); + this.formModel.removeControl('outboundPattern'); + this.formModel.removeControl('constraintPattern'); + console.log('JSON Data:', this.formModel.value); + + const apiUrl = 'http://localhost:8080/server/api/ldn/ldnservices'; + + this.http.post(apiUrl, this.formModel.value ).subscribe( + (response) => { + console.log('Service created successfully:', response); + this.formModel.reset(); + this.sendBack(); + }, + (error) => { + console.error('Error creating service:', error); + } + ); + } + + private validateForm(form: FormGroup): boolean { + let valid = true; + Object.keys(form.controls).forEach((key) => { + if (form.controls[key].invalid) { + form.controls[key].markAsDirty(); + valid = false; + } + }); + return valid; + } + + private sendBack() { + this.router.navigateByUrl('admin/ldn/services'); + } + + addInboundPattern() { + const notifyServiceInboundPatternsArray = this.formModel.get('notifyServiceInboundPatterns') as FormArray; + notifyServiceInboundPatternsArray.push(this.createInboundPatternFormGroup()); + } + + removeInboundPattern(patternGroup: FormGroup) { + const notifyServiceInboundPatternsArray = this.formModel.get('notifyServiceInboundPatterns') as FormArray; + notifyServiceInboundPatternsArray.removeAt(notifyServiceInboundPatternsArray.controls.indexOf(patternGroup)); + } + + addOutboundPattern() { + const notifyServiceOutboundPatternsArray = this.formModel.get('notifyServiceOutboundPatterns') as FormArray; + notifyServiceOutboundPatternsArray.push(this.createOutboundPatternFormGroup()); + } + + removeOutboundPattern(patternGroup: FormGroup) { + const notifyServiceOutboundPatternsArray = this.formModel.get('notifyServiceOutboundPatterns') as FormArray; + notifyServiceOutboundPatternsArray.removeAt(notifyServiceOutboundPatternsArray.controls.indexOf(patternGroup)); + } + + private createOutboundPatternFormGroup(): FormGroup { + return this.formBuilder.group({ + pattern: [''], + constraint: [''], + }); + } + + private createInboundPatternFormGroup(): FormGroup { + return this.formBuilder.group({ + pattern: [''], + constraint: [''], + automatic: [true] + }); + } +} diff --git a/src/app/admin/admin-ldn-services/ldn-service-new/ldn-service-new.component.html b/src/app/admin/admin-ldn-services/ldn-service-new/ldn-service-new.component.html new file mode 100644 index 0000000000..9567cb1abe --- /dev/null +++ b/src/app/admin/admin-ldn-services/ldn-service-new/ldn-service-new.component.html @@ -0,0 +1 @@ + diff --git a/src/app/admin/admin-ldn-services/ldn-service-new/ldn-service-new.component.scss b/src/app/admin/admin-ldn-services/ldn-service-new/ldn-service-new.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/admin/admin-ldn-services/ldn-service-new/ldn-service-new.component.spec.ts b/src/app/admin/admin-ldn-services/ldn-service-new/ldn-service-new.component.spec.ts new file mode 100644 index 0000000000..4994823004 --- /dev/null +++ b/src/app/admin/admin-ldn-services/ldn-service-new/ldn-service-new.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LdnServiceNewComponent } from './ldn-service-new.component'; + +describe('LdnServiceNewComponent', () => { + let component: LdnServiceNewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ LdnServiceNewComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(LdnServiceNewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/admin/admin-ldn-services/ldn-service-new/ldn-service-new.component.ts b/src/app/admin/admin-ldn-services/ldn-service-new/ldn-service-new.component.ts new file mode 100644 index 0000000000..daf1653cd1 --- /dev/null +++ b/src/app/admin/admin-ldn-services/ldn-service-new/ldn-service-new.component.ts @@ -0,0 +1,28 @@ +import { Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { LdnService } from "../ldn-services-model/ldn-services.model"; +import { ActivatedRoute } from "@angular/router"; +import { ProcessDataService } from "../../../core/data/processes/process-data.service"; +import { LinkService } from "../../../core/cache/builders/link.service"; +import { getFirstSucceededRemoteDataPayload } from "../../../core/shared/operators"; + +@Component({ + selector: 'ds-ldn-service-new', + templateUrl: './ldn-service-new.component.html', + styleUrls: ['./ldn-service-new.component.scss'] +}) +export class LdnServiceNewComponent implements OnInit { + /** + * Emits preselected process if there is one + */ + ldnService$?: Observable; + + constructor(private route: ActivatedRoute, private processService: ProcessDataService, private linkService: LinkService) { + } + + /** + * If there's an id parameter, use this the process with this identifier as presets for the form + */ + ngOnInit() { + } +} 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 new file mode 100644 index 0000000000..fccb374b5f --- /dev/null +++ b/src/app/admin/admin-ldn-services/ldn-service-serviceMock/ldnServicesRD$-mock.ts @@ -0,0 +1,73 @@ +import { LdnService } from '../ldn-services-model/ldn-services.model'; +import { LDN_SERVICE } from '../ldn-services-model/ldn-service.resource-type'; +import { RemoteData } from '../../../core/data/remote-data'; +import { PaginatedList } from '../../../core/data/paginated-list.model'; +import { Observable, of } from 'rxjs'; +// Create a mock data object for a single LDN notify service +export const mockLdnService: LdnService = { + id: 1, + name: 'Service Name', + description: 'Service Description', + url: 'Service URL', + ldnUrl: 'Service LDN URL', + notifyServiceInboundPatterns: [ + { + pattern: 'patternA', + constraint: 'itemFilterA', + automatic: false, + }, + { + pattern: 'patternB', + constraint: 'itemFilterB', + automatic: true, + }, + ], + notifyServiceOutboundPatterns: [ + { + pattern: 'patternC', + constraint: 'itemFilterC', + }, + ], + type: LDN_SERVICE, + _links: { + self: { + href: 'http://localhost/api/ldn/ldnservices/1', + }, + }, +}; + + + + +const mockLdnServices = { + payload: { + elementsPerPage: 20, + totalPages: 1, + totalElements: 1, + currentPage: 1, + first: undefined, + prev: undefined, + next: undefined, + last: undefined, + page: [mockLdnService], + type: LDN_SERVICE, + self: undefined, + getPageLength: function() { + return this.page.length; + }, + _links: { + self: { + href: 'http://localhost/api/ldn/ldnservices/1', + }, + page: [], + }, + }, + hasSucceeded: true, + msToLive: 0, +}; + + + + +// Create a mock ldnServicesRD$ observable +export const mockLdnServicesRD$: Observable>> = of((mockLdnServices as unknown) as RemoteData>); 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 new file mode 100644 index 0000000000..43755adb3c --- /dev/null +++ b/src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service.ts @@ -0,0 +1,90 @@ + import { Injectable } from '@angular/core'; +import { dataService } from '../../../core/data/base/data-service.decorator'; +import { LDN_SERVICE } from '../ldn-services-model/ldn-service.resource-type'; +import { IdentifiableDataService } from '../../../core/data/base/identifiable-data.service'; +import { FindAllData, FindAllDataImpl } from '../../../core/data/base/find-all-data'; +import { DeleteData, DeleteDataImpl } from '../../../core/data/base/delete-data'; +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 { FindListOptions } from '../../../core/data/find-list-options.model'; +import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; +import { Observable } from 'rxjs'; +import { RemoteData } from '../../../core/data/remote-data'; +import { PaginatedList } from '../../../core/data/paginated-list.model'; +import { NoContent } from '../../../core/shared/NoContent.model'; +import { map, take } from 'rxjs/operators'; +import { URLCombiner } from '../../../core/url-combiner/url-combiner'; +import { MultipartPostRequest } from '../../../core/data/request.models'; +import { RestRequest } from '../../../core/data/rest-request.model'; +import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; +import { hasValue } from '../../../shared/empty.util'; + +import { LdnService } from '../ldn-services-model/ldn-services.model'; +import { LdnServiceConstraint } from '../ldn-services-model/ldn-service-constraint.model'; + +@Injectable() +@dataService(LDN_SERVICE) +export class LdnServicesService extends IdentifiableDataService implements FindAllData, DeleteData { + private findAllData: FindAllDataImpl; // Corrected the type + private deleteData: DeleteDataImpl; // Corrected the type + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + ) { + super('ldnservices', requestService, rdbService, objectCache, halService); + + this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive); + this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint); + } + + findAll(options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig[]): Observable>> { + return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + } + + public delete(objectId: string, copyVirtualMetadata?: string[]): Observable> { + return this.deleteData.delete(objectId, copyVirtualMetadata); + } + + public deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable> { + return this.deleteData.deleteByHref(href, copyVirtualMetadata); + } + + public invoke(serviceName: string, parameters: LdnServiceConstraint[], files: File[]): Observable> { + const requestId = this.requestService.generateRequestId(); + this.getBrowseEndpoint().pipe( + take(1), + map((endpoint: string) => new URLCombiner(endpoint, serviceName, 'processes').toString()), + map((endpoint: string) => { + const body = this.getInvocationFormData(parameters, files); + return new MultipartPostRequest(requestId, endpoint, body); + }) + ).subscribe((request: RestRequest) => this.requestService.send(request)); + + return this.rdbService.buildFromRequestUUID(requestId); + } + + private getInvocationFormData(constrain: LdnServiceConstraint[], files: File[]): FormData { + const form: FormData = new FormData(); + form.set('properties', JSON.stringify(constrain)); + files.forEach((file: File) => { + form.append('file', file); + }); + return form; + } + + public ldnServiceWithNameExistsAndCanExecute(scriptName: string): Observable { + return this.findById(scriptName).pipe( + getFirstCompletedRemoteData(), + map((rd: RemoteData) => { + return hasValue(rd.payload); + }), + ); + } +} diff --git a/src/app/admin/admin-ldn-services/ldn-services-directory/ldn-services-directory.component.html b/src/app/admin/admin-ldn-services/ldn-services-directory/ldn-services-directory.component.html new file mode 100644 index 0000000000..bd2ec0d418 --- /dev/null +++ b/src/app/admin/admin-ldn-services/ldn-services-directory/ldn-services-directory.component.html @@ -0,0 +1,85 @@ +
+ {{ldnServicesRD$ | async | json }} +
+

{{'ldn-registered-services.title' | translate}}

+
+
+ + + + +
+ +
+ + + + + + + + + + + + + + + + +
{{'service.overview.table.name' | translate}}{{'service.overview.table.description' | translate}}{{'service.overview.table.status' | translate}}{{'service.overview.table.actions' | translate}}
{{ldnService.id}}{{ldnService.description}} + +
+
+
+
+ + + +
+ + + + +
+ + +
+ diff --git a/src/app/admin/admin-ldn-services/ldn-services-directory/ldn-services-directory.component.scss b/src/app/admin/admin-ldn-services/ldn-services-directory/ldn-services-directory.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/admin/admin-ldn-services/ldn-services-directory/ldn-services-directory.component.spec.ts b/src/app/admin/admin-ldn-services/ldn-services-directory/ldn-services-directory.component.spec.ts new file mode 100644 index 0000000000..bedcabb271 --- /dev/null +++ b/src/app/admin/admin-ldn-services/ldn-services-directory/ldn-services-directory.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ServicesDirectoryComponent } from './services-directory.component'; + +describe('ServicesDirectoryComponent', () => { + let component: ServicesDirectoryComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ServicesDirectoryComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ServicesDirectoryComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/admin/admin-ldn-services/ldn-services-directory/ldn-services-directory.component.ts b/src/app/admin/admin-ldn-services/ldn-services-directory/ldn-services-directory.component.ts new file mode 100644 index 0000000000..e8c81ea873 --- /dev/null +++ b/src/app/admin/admin-ldn-services/ldn-services-directory/ldn-services-directory.component.ts @@ -0,0 +1,105 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { LdnDirectoryService } from '../ldn-services-services/ldn-directory.service'; +import { Observable, Subscription } from 'rxjs'; +import { RemoteData } from '../../../core/data/remote-data'; +import { PaginatedList } from '../../../core/data/paginated-list.model'; +import { FindListOptions } from '../../../core/data/find-list-options.model'; +import { LdnService } from '../ldn-services-model/ldn-services.model'; +import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; +import { switchMap } from 'rxjs/operators'; +import { LdnServicesService } from 'src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service'; +import { PaginationService } from 'src/app/core/pagination/pagination.service'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { LdnServicesBulkDeleteService } from 'src/app/admin/admin-ldn-services/ldn-services-services/ldn-service-bulk-delete.service'; +import { hasValue } from '../../../shared/empty.util'; +import { HttpClient } from '@angular/common/http'; + +@Component({ + selector: 'ds-ldn-services-directory', + templateUrl: './ldn-services-directory.component.html', + styleUrls: ['./ldn-services-directory.component.scss'], +}) +export class LdnServicesOverviewComponent implements OnInit, OnDestroy { + + ldnServicesRD$: Observable>>; + config: FindListOptions = Object.assign(new FindListOptions(), { + elementsPerPage: 20 + }); + pageConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { + id: 'po', + pageSize: 20 + }); + private modalRef: any; + isProcessingSub: Subscription; + + constructor( + protected processLdnService: LdnServicesService, + protected paginationService: PaginationService, + protected modalService: NgbModal, + public ldnServicesBulkDeleteService: LdnServicesBulkDeleteService, + public ldnDirectoryService: LdnDirectoryService, + private http: HttpClient + ) {} + + ngOnInit(): void { + this.setLdnServices(); + this.ldnDirectoryService.listLdnServices(); + this.searchByLdnUrl(); + } + + setLdnServices() { + debugger; + this.ldnServicesRD$ = this.paginationService.getFindListOptions(this.pageConfig.id, this.config).pipe( + switchMap((config) => this.processLdnService.findAll(config, true, false)) + ); + console.log() + } + + ngOnDestroy(): void { + this.paginationService.clearPagination(this.pageConfig.id); + if (hasValue(this.isProcessingSub)) { + this.isProcessingSub.unsubscribe(); + } + } + + openDeleteModal(content) { + this.modalRef = this.modalService.open(content); + } + + closeModal() { + this.modalRef.close(); + } + + + findByLdnUrl(): Observable { + const url = 'http://localhost:8080/server/api/ldn/ldnservices'; + + return this.http.get(url); + } + + searchByLdnUrl(): void { + this.findByLdnUrl().subscribe( + (response) => { + console.log('Search results:', response); + }, + (error) => { + console.error('Error:', error); + } + ); + } + + deleteSelected() { + this.ldnServicesBulkDeleteService.deleteSelectedLdnServices(); + + if (hasValue(this.isProcessingSub)) { + this.isProcessingSub.unsubscribe(); + } + this.isProcessingSub = this.ldnServicesBulkDeleteService.isProcessing$() + .subscribe((isProcessing) => { + if (!isProcessing) { + this.closeModal(); + this.setLdnServices(); + } + }); + } +} diff --git a/src/app/admin/admin-ldn-services/ldn-services-guard/ldn-services-guard.service.ts b/src/app/admin/admin-ldn-services/ldn-services-guard/ldn-services-guard.service.ts new file mode 100644 index 0000000000..85235b4370 --- /dev/null +++ b/src/app/admin/admin-ldn-services/ldn-services-guard/ldn-services-guard.service.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +@Injectable({ + providedIn: 'root' +}) +export class LdnServicesGuard implements CanActivate { + + constructor( + //private notifyInfoService: NotifyInfoService, + private router: Router + ) {} + canActivate( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot): Observable | Promise | boolean | UrlTree { + return true; + /*return this.notifyInfoService.isCoarConfigEnabled().pipe( + map(coarLdnEnabled => { + if (coarLdnEnabled) { + return true; + } else { + return this.router.parseUrl('/404'); + } + }) + );*/ + } +} diff --git a/src/app/admin/admin-ldn-services/ldn-services-guard/ldn-services.guard.spec.ts b/src/app/admin/admin-ldn-services/ldn-services-guard/ldn-services.guard.spec.ts new file mode 100644 index 0000000000..30af31cab8 --- /dev/null +++ b/src/app/admin/admin-ldn-services/ldn-services-guard/ldn-services.guard.spec.ts @@ -0,0 +1,17 @@ +import { TestBed } from '@angular/core/testing'; + +import { LdnServicesGuard } from './ldn-services-guard.service'; + +describe('LdnServicesGuard', () => { + let guard: LdnServicesGuard; + + beforeEach(() => { + TestBed.configureTestingModule({}); + guard = TestBed.inject(LdnServicesGuard); + }); + + it('should be created', () => { + // @ts-ignore + expect(guard).toBeTruthy(); + }); +}); diff --git a/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service-constraint.model.ts b/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service-constraint.model.ts new file mode 100644 index 0000000000..704a3e7d8c --- /dev/null +++ b/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service-constraint.model.ts @@ -0,0 +1,26 @@ + +/** + * A cosntrain that can be used when running a service + */ +export class LdnServiceConstraint { + /** + * The name of the constrain + */ + name: string; + + /** + * The value of the constrain + */ + value: string; +} + +export const EndorsmentConstrain = [ + { + name: 'Type 1 Item', + value: 'Type1' + }, + { + name: 'Type 2 Item', + value: 'Type2' + }, +]; diff --git a/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service-status.model.ts b/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service-status.model.ts new file mode 100644 index 0000000000..040e4d37b8 --- /dev/null +++ b/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service-status.model.ts @@ -0,0 +1,8 @@ +/** + * List of services statuses + */ +export enum LdnServiceStatus { + UNKOWN, + DISABLED, + ENABLED, +} diff --git a/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service.resource-type.ts b/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service.resource-type.ts new file mode 100644 index 0000000000..c00d241ee8 --- /dev/null +++ b/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service.resource-type.ts @@ -0,0 +1,9 @@ +/** + * The resource type for Ldn-Services + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +import { ResourceType } from '../../../core/shared/resource-type'; + +export const LDN_SERVICE = new ResourceType('notifyservice'); 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 new file mode 100644 index 0000000000..bdb8bc5123 --- /dev/null +++ b/src/app/admin/admin-ldn-services/ldn-services-model/ldn-services.model.ts @@ -0,0 +1,58 @@ +import { ResourceType } from '../../../core/shared/resource-type'; +import { CacheableObject } from '../../../core/cache/cacheable-object.model'; +import { autoserialize, deserialize } from 'cerialize'; +import { LDN_SERVICE } from './ldn-service.resource-type'; +import { excludeFromEquals } from '../../../core/utilities/equals.decorators'; +import { typedObject } from '../../../core/cache/builders/build-decorators'; + + +@typedObject +export class LdnService extends CacheableObject { + static type = LDN_SERVICE; + + @excludeFromEquals + @autoserialize + type: ResourceType; + + @autoserialize + id?: number; + + @autoserialize + name: string; + + @autoserialize + description: string; + + @autoserialize + url: string; + + @autoserialize + ldnUrl: string; + + @autoserialize + notifyServiceInboundPatterns?: NotifyServicePattern[]; + + @autoserialize + notifyServiceOutboundPatterns?: NotifyServicePattern[]; + + @deserialize + _links: { + self: { + href: string; + }; + }; + + get self(): string { + return this._links.self.href; + } +} + + +class NotifyServicePattern { + @autoserialize + pattern: string; + @autoserialize + constraint?: string; + @autoserialize + automatic?: boolean; +} diff --git a/src/app/admin/admin-ldn-services/ldn-services-model/service-constrain-type.model.ts b/src/app/admin/admin-ldn-services/ldn-services-model/service-constrain-type.model.ts new file mode 100644 index 0000000000..d3f55c815e --- /dev/null +++ b/src/app/admin/admin-ldn-services/ldn-services-model/service-constrain-type.model.ts @@ -0,0 +1,10 @@ +/** + * List of parameter types used for scripts + */ +export enum LdnServiceConstrainType { + STRING = 'String', + DATE = 'date', + BOOLEAN = 'boolean', + FILE = 'InputStream', + OUTPUT = 'OutputStream' +} diff --git a/src/app/admin/admin-ldn-services/ldn-services-patterns/ldn-service-coar-patterns.ts b/src/app/admin/admin-ldn-services/ldn-services-patterns/ldn-service-coar-patterns.ts new file mode 100644 index 0000000000..14d227c131 --- /dev/null +++ b/src/app/admin/admin-ldn-services/ldn-services-patterns/ldn-service-coar-patterns.ts @@ -0,0 +1,73 @@ +export const notifyPatterns = [ + { + name: 'Acknowledge and Accept', + description: 'This pattern is used to acknowledge and accept a request (offer). It implies an intention to act on the request.', + category: 'Acknowledgements' + }, + { + name: 'Acknowledge and Reject', + description: 'This pattern is used to acknowledge and reject a request (offer). It signifies no further action regarding the request.', + category: 'Acknowledgements' + }, + { + name: 'Acknowledge and Tentatively Accept', + description: 'This pattern is used to acknowledge and tentatively accept a request (offer). It implies an intention to act, which may change.', + category: 'Acknowledgements' + }, + { + name: 'Acknowledge and Tentatively Reject', + description: 'This pattern is used to acknowledge and tentatively reject a request (offer). It signifies no further action, subject to change.', + category: 'Acknowledgements' + }, + { + name: 'Announce Endorsement', + description: 'This pattern is used to announce the existence of an endorsement, referencing the endorsed resource.', + category: 'Announcements' + }, + { + name: 'Announce Ingest', + description: 'This pattern is used to announce that a resource has been ingested.', + category: 'Announcements' + }, + { + name: 'Announce Relationship', + description: 'This pattern is used to announce a relationship between two resources.', + category: 'Announcements' + }, + { + name: 'Announce Review', + description: 'This pattern is used to announce the existence of a review, referencing the reviewed resource.', + category: 'Announcements' + }, + { + name: 'Announce Service Result', + description: 'This pattern is used to announce the existence of a "service result", referencing the relevant resource.', + category: 'Announcements' + }, + { + name: 'Request Endorsement', + description: 'This pattern is used to request endorsement of a resource owned by the origin system.', + category: 'Requests' + }, + { + name: 'Request Ingest', + description: 'This pattern is used to request that the target system ingest a resource.', + category: 'Requests' + }, + { + name: 'Request Review', + description: 'This pattern is used to request a review of a resource owned by the origin system.', + category: 'Requests' + }, + { + name: 'Undo Offer', + description: 'This pattern is used to undo (retract) an offer previously made.', + category: 'Undo' + } +]; + + +const pattern = notifyPatterns[0]; +console.log(`Pattern Name: ${pattern.name}`); +console.log(`Pattern Description: ${pattern.description}`); +console.log(`Pattern Category: ${pattern.category}`); diff --git a/src/app/admin/admin-ldn-services/ldn-services-services/ldn-directory.service.spec.ts b/src/app/admin/admin-ldn-services/ldn-services-services/ldn-directory.service.spec.ts new file mode 100644 index 0000000000..e24508e942 --- /dev/null +++ b/src/app/admin/admin-ldn-services/ldn-services-services/ldn-directory.service.spec.ts @@ -0,0 +1,17 @@ +import { TestBed } from '@angular/core/testing'; + +import { LdnDirectoryService } from './ldn-directory.service'; + +describe('LdnDirectoryService', () => { + let service: LdnDirectoryService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(LdnDirectoryService); + }); + + it('should be created', () => { + // @ts-ignore + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/admin/admin-ldn-services/ldn-services-services/ldn-directory.service.ts b/src/app/admin/admin-ldn-services/ldn-services-services/ldn-directory.service.ts new file mode 100644 index 0000000000..d06057800b --- /dev/null +++ b/src/app/admin/admin-ldn-services/ldn-services-services/ldn-directory.service.ts @@ -0,0 +1,54 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { LdnService } from '../ldn-services-model/ldn-services.model'; + +@Injectable({ + providedIn: 'root', +}) +export class LdnDirectoryService { + private baseUrl = 'http://localhost:8080/server/api/ldn/ldnservices'; + private itemFilterEndpoint = 'http://localhost:8080/server/api/config/itemfilters'; + + + constructor(private http: HttpClient) {} + + + public listLdnServices(): Observable { + const endpoint = `${this.baseUrl}`; + return this.http.get(endpoint); + } + + public getLdnServiceById(id: string): Observable { + const endpoint = `${this.baseUrl}/${id}`; + return this.http.get(endpoint); + } + + public createLdnService(ldnService: LdnService): Observable { + return this.http.post(this.baseUrl, ldnService); + } + + public updateLdnService(id: string, ldnService: LdnService): Observable { + const endpoint = `${this.baseUrl}/${id}`; + return this.http.put(endpoint, ldnService); + } + + public deleteLdnService(id: string): Observable { + const endpoint = `${this.baseUrl}/${id}`; + return this.http.delete(endpoint); + } + + public searchLdnServicesByLdnUrl(ldnUrl: string): Observable { + const endpoint = `${this.baseUrl}/search/byLdnUrl?ldnUrl=${ldnUrl}`; + return this.http.get(endpoint); + } + + public getItemFilters(): Observable { + const endpoint = `${this.itemFilterEndpoint}`; + return this.http.get(endpoint); + } + +} + + + diff --git a/src/app/admin/admin-ldn-services/ldn-services-services/ldn-service-bulk-delete.service.spec.ts b/src/app/admin/admin-ldn-services/ldn-services-services/ldn-service-bulk-delete.service.spec.ts new file mode 100644 index 0000000000..38e1a4de2b --- /dev/null +++ b/src/app/admin/admin-ldn-services/ldn-services-services/ldn-service-bulk-delete.service.spec.ts @@ -0,0 +1,17 @@ +import { TestBed } from '@angular/core/testing'; + +import { LdnServicesBulkDeleteService } from './ldn-service-bulk-delete.service'; + +describe('LdnServiceBulkDeleteService', () => { + let service: LdnServicesBulkDeleteService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(LdnServicesBulkDeleteService); + }); + + it('should be created', () => { + // @ts-ignore + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/admin/admin-ldn-services/ldn-services-services/ldn-service-bulk-delete.service.ts b/src/app/admin/admin-ldn-services/ldn-services-services/ldn-service-bulk-delete.service.ts new file mode 100644 index 0000000000..6805cd7f93 --- /dev/null +++ b/src/app/admin/admin-ldn-services/ldn-services-services/ldn-service-bulk-delete.service.ts @@ -0,0 +1,117 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject, count, from } from 'rxjs'; +import { LdnServicesService } from '../ldn-services-data/ldn-services-data.service'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; +import { isNotEmpty } from '../../../shared/empty.util'; +import { concatMap, filter, tap } from 'rxjs/operators'; +import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; +import { RemoteData } from '../../../core/data/remote-data'; +import { LdnService } from '../ldn-services-model/ldn-services.model'; +@Injectable({ + providedIn: 'root' +}) +/** + * Service to facilitate removing ldn services in bulk. + */ +export class LdnServicesBulkDeleteService { + + /** + * Array to track the services to be deleted + */ + ldnServicesToDelete: string[] = []; + + /** + * Behavior subject to track whether the delete is processing + * @protected + */ + protected isProcessingBehaviorSubject: BehaviorSubject = new BehaviorSubject(false); + + constructor( + protected processLdnService: LdnServicesService, + protected notificationsService: NotificationsService, + protected translateService: TranslateService + ) { + } + + /** + * Add or remove a process id to/from the list + * If the id is already present it will be removed, otherwise it will be added. + * + * @param notifyServiceName - The process id to add or remove + */ + toggleDelete(notifyServiceName: string) { + if (this.isToBeDeleted(notifyServiceName)) { + this.ldnServicesToDelete.splice(this.ldnServicesToDelete.indexOf(notifyServiceName), 1); + } else { + this.ldnServicesToDelete.push(notifyServiceName); + } + } + + /** + * Checks if the provided service id is present in the to be deleted list + * @param notifyServiceName + */ + isToBeDeleted(notifyServiceName: string) { + return this.ldnServicesToDelete.includes(notifyServiceName); + } + + /** + * Clear the list of services to be deleted + */ + clearAllServices() { + this.ldnServicesToDelete.splice(0); + } + + /** + * Get the amount of processes selected for deletion + */ + getAmountOfSelectedServices() { + return this.ldnServicesToDelete.length; + } + + /** + * Returns a behavior subject to indicate whether the bulk delete is processing + */ + isProcessing$() { + return this.isProcessingBehaviorSubject; + } + + /** + * Returns whether there currently are values selected for deletion + */ + hasSelected(): boolean { + return isNotEmpty(this.ldnServicesToDelete); + } + + /** + * Delete all selected processes one by one + * When the deletion for a process fails, an error notification will be shown with the process id, + * but it will continue deleting the other processes. + * At the end it will show a notification stating the amount of successful deletes + * The successfully deleted processes will be removed from the list of selected values, the failed ones will be retained. + */ + deleteSelectedLdnServices() { + this.isProcessingBehaviorSubject.next(true); + + from([...this.ldnServicesToDelete]).pipe( + concatMap((notifyServiceName) => { + return this.processLdnService.delete(notifyServiceName).pipe( + getFirstCompletedRemoteData(), + tap((rd: RemoteData) => { + if (rd.hasFailed) { + this.notificationsService.error(this.translateService.get('process.bulk.delete.error.head'), this.translateService.get('process.bulk.delete.error.body', {processId: notifyServiceName})); + } else { + this.toggleDelete(notifyServiceName); + } + }) + ); + }), + filter((rd: RemoteData) => rd.hasSucceeded), + count(), + ).subscribe((value) => { + this.notificationsService.success(this.translateService.get('process.bulk.delete.success', {count: value})); + this.isProcessingBehaviorSubject.next(false); + }); + } +} diff --git a/src/app/admin/admin-routing-paths.ts b/src/app/admin/admin-routing-paths.ts index 30f801cecb..df0459ff53 100644 --- a/src/app/admin/admin-routing-paths.ts +++ b/src/app/admin/admin-routing-paths.ts @@ -11,3 +11,5 @@ export function getRegistriesModuleRoute() { export function getNotificationsModuleRoute() { return new URLCombiner(getAdminModuleRoute(), NOTIFICATIONS_MODULE_PATH).toString(); } + +export const LDN_PATH = 'ldn'; diff --git a/src/app/admin/admin-routing.module.ts b/src/app/admin/admin-routing.module.ts index a7d19a6935..5cca6d3e22 100644 --- a/src/app/admin/admin-routing.module.ts +++ b/src/app/admin/admin-routing.module.ts @@ -7,6 +7,7 @@ import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service'; import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component'; import { REGISTRIES_MODULE_PATH, NOTIFICATIONS_MODULE_PATH } from './admin-routing-paths'; +import { LDN_PATH, REGISTRIES_MODULE_PATH } from './admin-routing-paths'; import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component'; @NgModule({ @@ -52,13 +53,24 @@ import { BatchImportPageComponent } from './admin-import-batch-page/batch-import component: BatchImportPageComponent, data: { title: 'admin.batch-import.title', breadcrumbKey: 'admin.batch-import' } }, + { + path: LDN_PATH, + children: [ + { path: '', pathMatch: 'full', redirectTo: 'services' }, + { + path: 'services', + loadChildren: () => import('./admin-ldn-services/admin-ldn-services.module') + .then((m) => m.AdminLdnServicesModule), + } + ] + }, { path: 'system-wide-alert', resolve: { breadcrumb: I18nBreadcrumbResolver }, loadChildren: () => import('../system-wide-alert/system-wide-alert.module').then((m) => m.SystemWideAlertModule), data: {title: 'admin.system-wide-alert.title', breadcrumbKey: 'admin.system-wide-alert'} }, - ]) + ]), ], providers: [ I18nBreadcrumbResolver, diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 7acf132df9..e1b1fc8811 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -187,6 +187,7 @@ import { NonHierarchicalBrowseDefinition } from './shared/non-hierarchical-brows import { BulkAccessConditionOptions } from './config/models/bulk-access-condition-options.model'; import { SuggestionTarget } from './suggestion-notifications/reciter-suggestions/models/suggestion-target.model'; import { SuggestionSource } from './suggestion-notifications/reciter-suggestions/models/suggestion-source.model'; +import { LdnServicesService } from '../admin/admin-ldn-services/ldn-services-data/ldn-services-data.service'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -309,7 +310,8 @@ const PROVIDERS = [ OrcidAuthService, OrcidQueueDataService, OrcidHistoryDataService, - SupervisionOrderDataService + SupervisionOrderDataService, + LdnServicesService, ]; /** diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 0d90ca693d..9f5249e92f 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -890,6 +890,28 @@ "claimed-declined-task-search-result-list-element.title": "Declined, sent back to Review Manager's workflow", + "ldn-registered-services.title": "Registered Services", + "ldn-registered-services.table.name":"Name", + "ldn-registered-services.table.description": "Description", + "ldn-registered-services.table.status": "Status", + "ldn-registered-services.table.action": "Action", + "ldn-registered-services.new": "NEW", + "ldn-registered-services.new.breadcrumbs": "Registered Services", + + "ldn-register-new-service.title": "Register a new service", + "ldn-register-new-service.name": "Name", + "ldn-register-new-service.url": "Service URL", + "ldn-register-new-service.ldn.inbox.url": "LDN InBox URL", + "ldn-register-new-service.inbound": "Inbound- Patterns supported by the service (i.e. messages that the service is able to receive and understand)", + "ldn-register-new-service.outbound": "Outbound- Patterns supported by the service (i.e. messages that the service is likely to generate and that should be processed by DSpace)", + "ldn-register-new-service.addmore": "+ Add more", + "ldn-register-new-service.breadcrumbs": "New Service", + + "ldn-register-new-service.notification.error.title": "Error", + "ldn-register-new-service.notification.error.content": "An error occurred while creating this process", + "ldn-register-new-service.notification.success.title": "Success", + "ldn-register-new-service.notification.success.content": "The process was successfully created", + "collection.create.head": "Create a Collection", "collection.create.notifications.success": "Successfully created the Collection", @@ -3328,6 +3350,10 @@ "process.new.breadcrumbs": "Create a new process", + + + + "process.detail.arguments": "Arguments", "process.detail.arguments.empty": "This process doesn't contain any arguments", @@ -3410,6 +3436,25 @@ "process.bulk.delete.success": "{{count}} process(es) have been succesfully deleted", + "service.overview.table.id": "Services ID", + + "service.overview.table.name": "Name", + + "service.overview.table.start": "Start time (UTC)", + + "service.overview.table.status": "Status", + + "service.overview.table.user": "User", + + "service.overview.title": "Services Overview", + + "service.overview.breadcrumbs": "Services Overview", + + "service.overview.table.actions": "Actions", + + "service.overview.table.description": "Description", + + "profile.breadcrumbs": "Update Profile", "profile.card.identify": "Identify",