Merged in CST-11048 (pull request #905)

CST-11048

Approved-by: Andrea Bollini
This commit is contained in:
Mattia Vianelli
2023-10-06 11:40:14 +00:00
committed by Andrea Bollini
37 changed files with 2300 additions and 3 deletions

View File

@@ -0,0 +1,37 @@
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';
import { LdnServiceFormEditComponent } from './ldn-service-form-edit/ldn-service-form-edit.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' }
},
{
path: 'edit/:serviceId',
resolve: { breadcrumb: I18nBreadcrumbResolver },
component: LdnServiceFormEditComponent,
data: { title: 'ldn-edit-service.title', breadcrumbKey: 'ldn-edit-service' }
},
]),
]
})
export class AdminLdnServicesRoutingModule {
}

View File

@@ -0,0 +1,25 @@
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';
import { LdnServiceFormEditComponent } from './ldn-service-form-edit/ldn-service-form-edit.component';
@NgModule({
imports: [
CommonModule,
SharedModule,
AdminLdnServicesRoutingModule,
],
declarations: [
LdnServicesOverviewComponent,
LdnServiceNewComponent,
LdnServiceFormComponent,
LdnServiceFormEditComponent,
]
})
export class AdminLdnServicesModule { }

View File

@@ -0,0 +1,155 @@
<form (ngSubmit)="submitForm()" [formGroup]="formModel">
<div class="toggle-switch-container">
<label for="enabled" class="status-label">{{ 'ldn-service-status' | translate }}</label>
<div>
<input formControlName="enabled" id="enabled" name="enabled" type="checkbox" hidden>
<div class="toggle-switch" [class.checked]="formModel.get('enabled').value" (click)="toggleEnabled()">
<div class="slider"></div>
</div>
</div>
</div>
<div class="mb-2">
<label for="name">{{ 'ldn-new-service.form.label.name' | translate }}</label>
<input [placeholder]="'ldn-new-service.form.placeholder.name' | translate"
formControlName="name" id="name" name="name"
[class.invalid-field]="formModel.get('name').invalid && formModel.get('name').touched"
type="text">
</div>
<div class="mb-4">
&nbsp;
</div>
<div class="mb-2 d-flex flex-column">
<label for="description">{{ 'ldn-new-service.form.label.description' | translate }}</label>
<textarea [placeholder]="'ldn-new-service.form.placeholder.description' | translate"
formControlName="description" id="description" name="description"></textarea>
</div>
<div class="mb-4">
&nbsp;
</div>
<div class="mb-2">
<label for="url">{{ 'ldn-new-service.form.label.url' | translate }}</label>
<input [placeholder]="'ldn-new-service.form.placeholder.url' | translate"
formControlName="url" id="url" name="url"
[class.invalid-field]="formModel.get('url').invalid && formModel.get('url').touched"
type="text">
</div>
<div class="mb-4">
&nbsp;
</div>
<div class="mb-2">
<label for="ldnUrl">{{ 'ldn-new-service.form.label.ldnUrl' | translate }}</label>
<input [placeholder]="'ldn-new-service.form.placeholder.ldnUrl' | translate"
formControlName="ldnUrl" id="ldnUrl" name="ldnUrl"
[class.invalid-field]="formModel.get('ldnUrl').invalid && formModel.get('ldnUrl').touched"
type="text">
</div>
<div class="mb-4">
&nbsp;
</div>
<div *ngFor="let patternGroup of formModel.get('notifyServiceInboundPatterns')['controls']; let i = index"
class="mb-2 d-flex align-content-center" formGroupName="notifyServiceInboundPatterns">
<ng-container [formGroupName]="i">
<div class="flex-grow-1">
<label *ngIf="i === 0" for="additionalInboundPattern{{i}}">{{ 'ldn-new-service.form.label.inboundPattern' | translate }} </label>
<select #inboundPattern formControlName="pattern" id="additionalInboundPattern{{i}}"
name="additionalInboundPattern{{i}}" required>
<option value="">{{ 'ldn-new-service.form.label.placeholder.inboundPattern' | translate }}</option>
<option *ngFor="let pattern of inboundPatterns" [ngValue]="pattern.name">{{ pattern.name }}</option>
</select>
</div>
<ng-container *ngIf="inboundPattern.value">
<div class="ml-2 flex-grow-1">
<label *ngIf="i === 0" for="constraint{{i}}">{{ 'ldn-new-service.form.label.ItemFilter' | translate }}</label>
<select formControlName="constraint" id="constraint{{i}}" name="constraint{{i}}">
<option value="">{{ 'ldn-new-service.form.label.placeholder.selectedItemFilter' | translate }}</option>
<option *ngFor="let itemFilter of itemFilterList" [value]="itemFilter.name">{{ itemFilter.name }}</option>
</select>
</div>
<div class="ml-2 d-flex flex-column align-content-center">
<label *ngIf="i === 0" for="automatic{{i}}">{{ 'ldn-new-service.form.label.automatic' | translate }}</label>
<div class="d-flex flex-grow-1 align-content-center justify-content-center">
<input formControlName="automatic" id="automatic{{i}}" name="automatic{{i}}" type="checkbox" hidden>
<div class="toggle-switch" [class.checked]="formModel.get('notifyServiceInboundPatterns.' + i + '.automatic').value" (click)="toggleAutomatic(i)">
<div class="slider"></div>
</div>
</div>
</div>
</ng-container>
<div class="d-flex align-items-end justify-content-center">
<button (click)="removeInboundPattern(i)" class="btn btn-outline-dark trash-button ml-2">
<i class="fas fa-trash"></i>
</button>
</div>
</ng-container>
</div>
<span (click)="addInboundPattern()" class="add-pattern-link mb-2">{{ 'ldn-new-service.form.label.addPattern' | translate }}</span>
<div class="mb-4">
&nbsp;
</div>
<div *ngFor="let patternGroup of formModel.get('notifyServiceOutboundPatterns')['controls']; let i = index"
class="mb-2 d-flex align-content-center" formGroupName="notifyServiceOutboundPatterns">
<ng-container [formGroupName]="i">
<div class="flex-grow-1">
<label *ngIf="i === 0" for="additionalOutboundPattern{{i}}">{{ 'ldn-new-service.form.label.outboundPattern' | translate }}</label>
<select #outboundPattern formControlName="pattern" id="additionalOutboundPattern{{i}}"
name="additionalOutboundPattern{{i}}"
required>
<option value="">{{ 'ldn-new-service.form.label.placeholder.outboundPattern' | translate }}</option>
<option *ngFor="let pattern of outboundPatterns" [ngValue]="pattern.name">{{ pattern.name }}</option>
</select>
</div>
<div *ngIf="outboundPattern.value" class="ml-2 flex-grow-1">
<label *ngIf="i === 0" for="constraint{{i}}">{{ 'ldn-new-service.form.label.ItemFilter' | translate }}</label>
<select formControlName="constraint" id="constraint{{i}}" name="constraint{{i}}">
<option value="">{{ 'ldn-new-service.form.label.placeholder.selectedItemFilter' | translate }}</option>
<option *ngFor="let itemFilter of itemFilterList" [value]="itemFilter.name">{{ itemFilter.name }}</option>
</select>
</div>
<div class="d-flex align-items-end justify-content-center">
<button (click)="removeOutboundPattern(i)" class="btn btn-outline-dark trash-button ml-2">
<i class="fas fa-trash"></i>
</button>
</div>
</ng-container>
</div>
<span
(click)="addOutboundPattern()" class="add-pattern-link mb-2">{{ 'ldn-new-service.form.label.addPattern' | translate }}
</span>
<div class="mb-2">
&nbsp;
</div>
<button class="btn btn-primary" type="submit">{{ 'ldn-new-service.form.label.submit' | translate }}</button>
</form>

View File

@@ -0,0 +1,114 @@
form {
display: flex;
flex-direction: column;
align-items: flex-start;
max-width: 800px;
font-size: 14px;
margin-left: 300px;
& > * {
width: 100%;
}
}
input[type="text"],
select {
max-width: 100%;
width: 100%;
padding: 8px;
font-size: 14px;
}
option:not(:first-child) {
font-weight: bold;
}
.trash-button {
width: 40px;
height: 40px;
}
textarea {
height: 200px;
resize: none;
}
.add-pattern-link{
color: #0048ff;
cursor: pointer;
margin-left: 10px;
}
.remove-pattern-link{
color: #e34949;
cursor: pointer;
margin-left: 10px;
}
.status-checkbox {
margin-top: 5px;
}
.invalid-field {
border: 1px solid red;
color: #000000;
}
.toggle-switch {
display: flex;
align-items: center;
opacity: 0.8;
position: relative;
width: 60px;
height: 30px;
background-color: #ccc;
border-radius: 15px;
cursor: pointer;
transition: background-color 0.3s;
}
.toggle-switch.checked {
background-color: #24cc9a;
}
.slider {
position: absolute;
width: 30px;
height: 30px;
border-radius: 50%;
background-color: #fff;
transition: transform 0.3s;
}
.toggle-switch .slider {
width: 22px;
height: 22px;
border-radius: 50%;
margin: 0 auto;
}
.toggle-switch.checked .slider {
transform: translateX(30px);
}
.toggle-switch-container {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-end;
margin-top: 10px;
}
.toggle-switch {
cursor: pointer;
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LdnServiceFormEditComponent } from './ldn-service-form-edit.component';
describe('LdnServiceFormEditComponent', () => {
let component: LdnServiceFormEditComponent;
let fixture: ComponentFixture<LdnServiceFormEditComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ LdnServiceFormEditComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(LdnServiceFormEditComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,374 @@
import { ChangeDetectorRef, Component, Input } from '@angular/core';
import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { LDN_SERVICE } from '../ldn-services-model/ldn-service.resource-type';
import { Router } from '@angular/router';
import { HttpClient } from '@angular/common/http';
import { LdnDirectoryService } from '../ldn-services-services/ldn-directory.service';
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 { ActivatedRoute } from '@angular/router';
import { animate, state, style, transition, trigger } from '@angular/animations';
@Component({
selector: 'ds-ldn-service-form-edit',
templateUrl: './ldn-service-form-edit.component.html',
styleUrls: ['./ldn-service-form-edit.component.scss'],
animations: [
trigger('toggleAnimation', [
state('true', style({})),
state('false', style({})),
transition('true <=> false', animate('300ms ease-in')),
]),
],
})
export class LdnServiceFormEditComponent {
formModel: FormGroup;
showItemFilterDropdown = false;
private originalInboundPatterns: any[] = [];
private originalOutboundPatterns: any[] = [];
public inboundPatterns: object[] = notifyPatterns;
public outboundPatterns: object[] = notifyPatterns;
public itemFilterList: LdnServiceConstraint[];
@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;
private serviceId: string;
constructor(
private ldnServicesService: LdnServicesService,
private ldnDirectoryService: LdnDirectoryService,
private formBuilder: FormBuilder,
private http: HttpClient,
private router: Router,
private route: ActivatedRoute,
private cdRef: ChangeDetectorRef
) {
this.formModel = this.formBuilder.group({
id: [''],
name: ['', Validators.required],
description: ['', Validators.required],
url: ['', Validators.required],
ldnUrl: ['', Validators.required],
inboundPattern: [''],
outboundPattern: [''],
constraintPattern: [''],
enabled: [''],
notifyServiceInboundPatterns: this.formBuilder.array([this.createInboundPatternFormGroup()]),
notifyServiceOutboundPatterns: this.formBuilder.array([this.createOutboundPatternFormGroup()]),
type: LDN_SERVICE.value,
});
}
ngOnInit(): void {
this.route.params.subscribe((params) => {
this.serviceId = params.serviceId;
if (this.serviceId) {
this.fetchServiceData(this.serviceId);
}
});
this.ldnDirectoryService.getItemFilters().subscribe((itemFilters) => {
this.itemFilterList = itemFilters._embedded.itemfilters.map((filter: { id: string; }) => ({
name: filter.id
}));
this.cdRef.detectChanges();
});
}
private getOriginalPattern(formArrayName: string, patternId: number): any {
let originalPatterns: any[] = [];
if (formArrayName === 'notifyServiceInboundPatterns') {
originalPatterns = this.originalInboundPatterns;
} else if (formArrayName === 'notifyServiceOutboundPatterns') {
originalPatterns = this.originalOutboundPatterns;
}
return originalPatterns.find((pattern) => pattern.id === patternId);
}
private patternsAreEqual(patternA: any, patternB: any): boolean {
return (
patternA.pattern === patternB.pattern &&
patternA.constraint === patternB.constraint &&
patternA.automatic === patternB.automatic
);
}
fetchServiceData(serviceId: string): void {
const apiUrl = `http://localhost:8080/server/api/ldn/ldnservices/${serviceId}`;
this.http.get(apiUrl).subscribe(
(data: any) => {
console.log(data);
this.formModel.patchValue({
id: data.id,
name: data.name,
description: data.description,
url: data.url,
ldnUrl: data.ldnUrl,
type: data.type,
enabled: data.enabled
});
const inboundPatternsArray = this.formModel.get('notifyServiceInboundPatterns') as FormArray;
inboundPatternsArray.clear(); // Clear existing rows
data.notifyServiceInboundPatterns.forEach((pattern: any) => {
console.log(pattern);
const patternFormGroup = this.initializeInboundPatternFormGroup();
console.log();
patternFormGroup.patchValue(pattern);
inboundPatternsArray.push(patternFormGroup);
this.cdRef.detectChanges();
});
const outboundPatternsArray = this.formModel.get('notifyServiceOutboundPatterns') as FormArray;
outboundPatternsArray.clear();
data.notifyServiceOutboundPatterns.forEach((pattern: any) => {
const patternFormGroup = this.initializeOutboundPatternFormGroup();
patternFormGroup.patchValue(pattern);
outboundPatternsArray.push(patternFormGroup);
this.cdRef.detectChanges();
});
this.originalInboundPatterns = [...data.notifyServiceInboundPatterns];
this.originalOutboundPatterns = [...data.notifyServiceOutboundPatterns];
},
(error) => {
console.error('Error fetching service data:', error);
}
);
}
generatePatchOperations(): any[] {
const patchOperations: any[] = [];
this.addReplaceOperation(patchOperations, 'name', '/name');
this.addReplaceOperation(patchOperations, 'description', '/description');
this.addReplaceOperation(patchOperations, 'ldnUrl', '/ldnurl');
this.addReplaceOperation(patchOperations, 'url', '/url');
this.handlePatterns(patchOperations, 'notifyServiceInboundPatterns');
this.handlePatterns(patchOperations, 'notifyServiceOutboundPatterns');
return patchOperations;
}
private addReplaceOperation(patchOperations: any[], formControlName: string, path: string): void {
if (this.formModel.get(formControlName).dirty) {
patchOperations.push({
op: 'replace',
path,
value: this.formModel.get(formControlName).value,
});
}
}
private handlePatterns(patchOperations: any[], formArrayName: string): void {
const patternsArray = this.formModel.get(formArrayName) as FormArray;
if (patternsArray.dirty) {
for (let i = 0; i < patternsArray.length; i++) {
const patternGroup = patternsArray.at(i) as FormGroup;
const patternValue = patternGroup.value;
if (patternValue.isNew) {
console.log(this.getOriginalPatternsForFormArray(formArrayName));
console.log(patternGroup);
delete patternValue.isNew;
const addOperation = {
op: 'add',
path: `${formArrayName}/-`,
value: patternValue,
};
patchOperations.push(addOperation);
} else if (patternGroup.dirty) {
const replaceOperation = {
op: 'replace',
path: `${formArrayName}[${i}]`,
value: patternValue,
};
patchOperations.push(replaceOperation);
console.log(patternValue.id);
}
}
}
}
private getOriginalPatternsForFormArray(formArrayName: string): any[] {
if (formArrayName === 'notifyServiceInboundPatterns') {
return this.originalInboundPatterns;
} else if (formArrayName === 'notifyServiceOutboundPatterns') {
return this.originalOutboundPatterns;
}
return [];
}
submitForm() {
const apiUrl = `http://localhost:8080/server/api/ldn/ldnservices/${this.serviceId}`;
const patchOperations = this.generatePatchOperations();
this.http.patch(apiUrl, patchOperations).subscribe(
(response) => {
console.log('Service updated successfully:', response);
this.sendBack();
},
(error) => {
console.error('Error updating service:', error);
}
);
}
addInboundPattern() {
const notifyServiceInboundPatternsArray = this.formModel.get('notifyServiceInboundPatterns') as FormArray;
notifyServiceInboundPatternsArray.push(this.createInboundPatternFormGroup());
}
removeInboundPattern(index: number) {
const notifyServiceInboundPatternsArray = this.formModel.get('notifyServiceInboundPatterns') as FormArray;
if (index >= 0 && index < notifyServiceInboundPatternsArray.length) {
const serviceId = this.formModel.get('id').value;
const patchOperation = [
{
op: 'remove',
path: `notifyServiceInboundPatterns[${index}]`
}
];
const apiUrl = `http://localhost:8080/server/api/ldn/ldnservices/${serviceId}`;
this.http.patch(apiUrl, patchOperation).subscribe(
(response) => {
console.log('Pattern removed successfully:', response);
notifyServiceInboundPatternsArray.removeAt(index);
},
(error) => {
console.error('Error removing pattern:', error);
}
);
}
}
addOutboundPattern() {
const notifyServiceOutboundPatternsArray = this.formModel.get('notifyServiceOutboundPatterns') as FormArray;
notifyServiceOutboundPatternsArray.push(this.createOutboundPatternFormGroup());
}
removeOutboundPattern(index: number) {
const notifyServiceOutboundPatternsArray = this.formModel.get('notifyServiceOutboundPatterns') as FormArray;
if (index >= 0 && index < notifyServiceOutboundPatternsArray.length) {
const serviceId = this.formModel.get('id').value;
const patchOperation = [
{
op: 'remove',
path: `notifyServiceOutboundPatterns[${index}]`
}
];
const apiUrl = `http://localhost:8080/server/api/ldn/ldnservices/${serviceId}`;
this.http.patch(apiUrl, patchOperation).subscribe(
(response) => {
console.log('Pattern removed successfully:', response);
notifyServiceOutboundPatternsArray.removeAt(index);
},
(error) => {
console.error('Error removing pattern:', error);
}
);
}
}
private sendBack() {
this.router.navigateByUrl('admin/ldn/services');
}
private createOutboundPatternFormGroup(): FormGroup {
return this.formBuilder.group({
pattern: '',
constraint: '',
isNew: true,
});
}
private createInboundPatternFormGroup(): FormGroup {
return this.formBuilder.group({
pattern: '',
constraint: '',
automatic: '',
isNew: true
});
}
private initializeOutboundPatternFormGroup(): FormGroup {
return this.formBuilder.group({
pattern: '',
constraint: '',
});
}
private initializeInboundPatternFormGroup(): FormGroup {
return this.formBuilder.group({
pattern: '',
constraint: '',
automatic: '',
});
}
toggleAutomatic(i: number) {
const automaticControl = this.formModel.get(`notifyServiceInboundPatterns.${i}.automatic`);
if (automaticControl) {
automaticControl.setValue(!automaticControl.value);
}
}
toggleEnabled() {
const newStatus = !this.formModel.get('enabled').value;
const serviceId = this.formModel.get('id').value;
const status = this.formModel.get('enabled').value;
const apiUrl = `http://localhost:8080/server/api/ldn/ldnservices/${serviceId}`;
const patchOperation = {
op: 'replace',
path: '/enabled',
value: newStatus,
};
this.http.patch(apiUrl, [patchOperation]).subscribe(
() => {
console.log('Status updated successfully.');
this.formModel.get('enabled').setValue(newStatus);
console.log(this.formModel.get('enabled'));
this.cdRef.detectChanges();
},
(error) => {
console.error('Error updating status:', error);
}
);
}
}

View File

@@ -0,0 +1,163 @@
<form (ngSubmit)="submitForm()" [formGroup]="formModel">
<!-- In the name section -->
<div class="mb-2">
<label for="name">{{ 'ldn-new-service.form.label.name' | translate }}</label>
<input [placeholder]="'ldn-new-service.form.placeholder.name' | translate"
formControlName="name" id="name" name="name"
[class.invalid-field]="formModel.get('name').invalid && formModel.get('name').touched"
type="text">
</div>
<div class="mb-4">
&nbsp;
</div>
<!-- In the description section -->
<div class="mb-2 d-flex flex-column">
<label for="description">{{ 'ldn-new-service.form.label.description' | translate }}</label>
<textarea [placeholder]="'ldn-new-service.form.placeholder.description' | translate"
formControlName="description" id="description" name="description"></textarea>
</div>
<div class="mb-4">
&nbsp;
</div>
<!-- In the url section -->
<div class="mb-2">
<label for="url">{{ 'ldn-new-service.form.label.url' | translate }}</label>
<input [placeholder]="'ldn-new-service.form.placeholder.url' | translate"
formControlName="url" id="url" name="url"
[class.invalid-field]="formModel.get('url').invalid && formModel.get('url').touched"
type="text">
</div>
<div class="mb-4">
&nbsp;
</div>
<!-- In the ldnUrl section -->
<div class="mb-2">
<label for="ldnUrl">{{ 'ldn-new-service.form.label.ldnUrl' | translate }}</label>
<input [placeholder]="'ldn-new-service.form.placeholder.ldnUrl' | translate"
formControlName="ldnUrl" id="ldnUrl" name="ldnUrl"
[class.invalid-field]="formModel.get('ldnUrl').invalid && formModel.get('ldnUrl').touched"
type="text">
</div>
<div class="mb-4">
&nbsp;
</div>
<!-- In the Inbound Patterns section -->
<div *ngFor="let patternGroup of formModel.get('notifyServiceInboundPatterns')['controls']; let i = index"
class="mb-2 d-flex align-content-center" formGroupName="notifyServiceInboundPatterns">
<ng-container [formGroupName]="i">
<div class="flex-grow-1">
<label *ngIf="i === 0" for="additionalInboundPattern{{i}}">{{ 'ldn-new-service.form.label.inboundPattern' | translate }}</label>
<select #inboundPattern formControlName="pattern" id="additionalInboundPattern{{i}}"
name="additionalInboundPattern{{i}}" required>
<option value="">{{ 'ldn-new-service.form.label.placeholder.inboundPattern' | translate }}</option>
<option *ngFor="let pattern of inboundPatterns" [ngValue]="pattern.name">{{ pattern.name }}</option>
</select>
</div>
<ng-container *ngIf="inboundPattern.value">
<div class="ml-2 flex-grow-1">
<label *ngIf="i === 0" for="constraint{{i}}">{{ 'ldn-new-service.form.label.ItemFilter' | translate }}</label>
<select formControlName="constraint" id="constraint{{i}}" name="constraint{{i}}">
<option value="">{{ 'ldn-new-service.form.label.placeholder.selectedItemFilter' | translate }}</option>
<option *ngFor="let itemFilter of itemFilterList"
[value]="itemFilter.name">{{ itemFilter.name }}</option>
</select>
</div>
<div class="ml-2 d-flex flex-column align-content-center">
<label *ngIf="i === 0" for="automatic{{i}}">{{ 'ldn-new-service.form.label.automatic' | translate }}</label>
<div class="d-flex flex-grow-1 align-content-center justify-content-center">
<input formControlName="automatic" id="automatic{{i}}" name="automatic{{i}}" type="checkbox" hidden>
<div class="toggle-switch" [class.checked]="formModel.get('notifyServiceInboundPatterns.' + i + '.automatic').value" (click)="toggleAutomatic(i)">
<div class="slider"></div>
</div>
</div>
</div>
</ng-container>
<div class="d-flex align-items-end justify-content-center">
<button (click)="removeInboundPattern(patternGroup)" class="btn btn-outline-dark trash-button ml-2">
<i class="fas fa-trash"></i>
</button>
</div>
</ng-container>
</div>
<span (click)="addInboundPattern()"
class="add-pattern-link mb-2">{{ 'ldn-new-service.form.label.addPattern' | translate }}</span>
<div class="mb-4">
&nbsp;
</div>
<!-- In the Outbound Patterns section -->
<div *ngFor="let patternGroup of formModel.get('notifyServiceOutboundPatterns')['controls']; let i = index"
class="mb-2 d-flex align-content-center" formGroupName="notifyServiceOutboundPatterns">
<ng-container [formGroupName]="i">
<div class="flex-grow-1">
<label *ngIf="i === 0" for="additionalOutboundPattern{{i}}">{{ 'ldn-new-service.form.label.outboundPattern' | translate }}</label>
<select #outboundPattern formControlName="pattern" id="additionalOutboundPattern{{i}}"
name="additionalOutboundPattern{{i}}"
required>
<option value="">{{ 'ldn-new-service.form.label.placeholder.outboundPattern' | translate }}</option>
<option *ngFor="let pattern of outboundPatterns"
[ngValue]="pattern.name">{{ pattern.name }}</option>
</select>
</div>
<div *ngIf="outboundPattern.value" class="ml-2 flex-grow-1">
<label *ngIf="i === 0" for="constraint{{i}}">{{ 'ldn-new-service.form.label.ItemFilter' | translate }}</label>
<select formControlName="constraint" id="constraint{{i}}" name="constraint{{i}}">
<option value="">{{ 'ldn-new-service.form.label.placeholder.selectedItemFilter' | translate }}</option>
<option *ngFor="let itemFilter of itemFilterList"
[value]="itemFilter.name">{{ itemFilter.name }}</option>
</select>
</div>
<div class="d-flex align-items-end justify-content-center">
<button (click)="removeOutboundPattern(patternGroup)" class="btn btn-outline-dark trash-button ml-2">
<i class="fas fa-trash"></i>
</button>
</div>
</ng-container>
</div>
<span (click)="addOutboundPattern()"
class="add-pattern-link mb-2">{{ 'ldn-new-service.form.label.addPattern' | translate }}
</span>
<div class="mb-2">
&nbsp;
</div>
<button class="btn btn-primary" type="submit">{{ 'ldn-new-service.form.label.submit' | translate }}</button>
</form>

View File

@@ -0,0 +1,114 @@
form {
display: flex;
flex-direction: column;
align-items: flex-start;
max-width: 800px;
font-size: 14px;
margin-left: 300px;
& > * {
width: 100%;
}
}
input[type="text"],
select {
max-width: 100%;
width: 100%;
padding: 8px;
font-size: 14px;
}
option:not(:first-child) {
font-weight: bold;
}
.trash-button {
width: 40px;
height: 40px;
}
textarea {
height: 200px;
resize: none;
}
.add-pattern-link{
color: #0048ff;
cursor: pointer;
margin-left: 10px;
}
.remove-pattern-link{
color: #e34949;
cursor: pointer;
margin-left: 10px;
}
.status-checkbox {
margin-top: 5px;
}
.invalid-field {
border: 1px solid red;
color: #000000;
}
.toggle-switch {
display: flex;
align-items: center;
opacity: 0.8;
position: relative;
width: 60px;
height: 30px;
background-color: #ccc;
border-radius: 15px;
cursor: pointer;
transition: background-color 0.3s;
}
.toggle-switch.checked {
background-color: #24cc9a;
}
.slider {
position: absolute;
width: 30px;
height: 30px;
border-radius: 50%;
background-color: #fff;
transition: transform 0.3s;
}
.toggle-switch .slider {
width: 22px;
height: 22px;
border-radius: 50%;
margin: 0 auto;
}
.toggle-switch.checked .slider {
transform: translateX(30px);
}
.toggle-switch-container {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-end;
margin-top: 10px;
}
.toggle-switch {
cursor: pointer;
}

View File

@@ -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<LdnServiceFormComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ LdnServiceFormComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(LdnServiceFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,171 @@
import { Component, Input, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators, FormArray } from '@angular/forms';
import { HttpClient } 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';
import { animate, state, style, transition, trigger } from '@angular/animations';
@Component({
selector: 'ds-ldn-service-form',
templateUrl: './ldn-service-form.component.html',
styleUrls: ['./ldn-service-form.component.scss'],
animations: [
trigger('toggleAnimation', [
state('true', style({})), // Define animation states (empty style)
state('false', style({})),
transition('true <=> false', animate('300ms ease-in')), // Define animation transition with duration
]),
],
})
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 status: boolean;
@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;
/*
get notifyServiceInboundPatternsFormArray(): FormArray {
return this.formModel.get('notifyServiceInboundPatterns') as FormArray;
}
*/
constructor(
private ldnServicesService: LdnServicesService,
private ldnDirectoryService: LdnDirectoryService,
private formBuilder: FormBuilder,
private http: HttpClient,
private router: Router
) {
this.formModel = this.formBuilder.group({
enabled: true,
id: [''],
name: ['', Validators.required],
description: [''],
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
}));
});
}
submitForm() {
this.formModel.get('name').markAsTouched();
this.formModel.get('url').markAsTouched();
this.formModel.get('ldnUrl').markAsTouched();
const name = this.formModel.get('name').value;
const url = this.formModel.get('url').value;
const ldnUrl = this.formModel.get('ldnUrl').value;
if (!name || !url || !ldnUrl) {
return;
}
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 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: false
});
}
toggleAutomatic(i: number) {
const automaticControl = this.formModel.get(`notifyServiceInboundPatterns.${i}.automatic`);
if (automaticControl) {
automaticControl.setValue(!automaticControl.value);
}
}
}

View File

@@ -0,0 +1 @@
<ds-ldn-service-form></ds-ldn-service-form>

View File

@@ -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<LdnServiceNewComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ LdnServiceNewComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(LdnServiceNewComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -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<LdnService>;
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() {
}
}

View File

@@ -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<RemoteData<PaginatedList<LdnService>>> = of((mockLdnServices as unknown) as RemoteData<PaginatedList<LdnService>>);

View File

@@ -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<LdnService> implements FindAllData<LdnService>, DeleteData<LdnService> {
private findAllData: FindAllDataImpl<LdnService>; // Corrected the type
private deleteData: DeleteDataImpl<LdnService>; // 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<LdnService>[]): Observable<RemoteData<PaginatedList<LdnService>>> {
return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
public delete(objectId: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
return this.deleteData.delete(objectId, copyVirtualMetadata);
}
public deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
return this.deleteData.deleteByHref(href, copyVirtualMetadata);
}
public invoke(serviceName: string, parameters: LdnServiceConstraint[], files: File[]): Observable<RemoteData<LdnService>> {
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<LdnService>(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<boolean> {
return this.findById(scriptName).pipe(
getFirstCompletedRemoteData(),
map((rd: RemoteData<LdnService>) => {
return hasValue(rd.payload);
}),
);
}
}

View File

@@ -0,0 +1,80 @@
<div class="container">
<div class="d-flex">
<h2 class="flex-grow-1">{{ 'ldn-registered-services.title' | translate }}</h2>
</div>
<div class="d-flex justify-content-end">
<button class="btn btn-success" routerLink="/admin/ldn/services/new"><i
class="fas fa-plus pr-2"></i>{{ 'process.overview.new' | translate }}</button>
</div>
<ds-pagination *ngIf="(ldnServicesRD$ | async)?.payload?.totalElements > 0"
[collectionSize]="(ldnServicesRD$ | async)?.payload?.totalElements"
[hideGear]="true"
[hidePagerWhenSinglePage]="true"
[pageInfoState]="(ldnServicesRD$ | async)?.payload"
[paginationOptions]="pageConfig">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th scope="col">{{ 'service.overview.table.name' | translate }}</th>
<th scope="col">{{ 'service.overview.table.description' | translate }}</th>
<th scope="col">{{ 'service.overview.table.status' | translate }}</th>
<th scope="col">{{ 'service.overview.table.actions' | translate }}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let ldnService of servicesData">
<td>{{ ldnService.name }}</td>
<td>{{ ldnService.description }}</td>
<td>
<span [ngClass]="{ 'status-enabled': ldnService.enabled, 'status-disabled': !ldnService.enabled }"
class="status-indicator" (click)="toggleStatus(ldnService)"
[title]="ldnService.enabled ? ('ldn-service.overview.table.clickToDisable' | translate) : ('ldn-service.overview.table.clickToEnable' | translate)">
{{ ldnService.enabled ? ('ldn-service.overview.table.enabled' | translate) : ('ldn-service.overview.table.disabled' | translate) }}
</span>
</td>
<td>
<button (click)="selectServiceToDelete(ldnService.id)" class="btn btn-outline-danger">
<i class="fas fa-trash"></i>
</button>
<button [routerLink]="['/admin/ldn/services/edit/', ldnService.id]"
class="btn btn-outline-dark">
<i class="fas fa-edit"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</ds-pagination>
</div>
<ng-template #deleteModal>
<div>
<div class="modal-header">
<div>
<h4>{{'service.overview.delete.header' | translate }}</h4>
</div>
<button (click)="closeModal()" aria-label="Close"
class="close" type="button">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<div>
{{ 'service.overview.delete.body' | translate }}
</div>
<div class="mt-4">
<button (click)="closeModal()"
class="btn btn-primary mr-2">{{ 'service.detail.delete.cancel' | translate }}</button>
<button (click)="deleteSelected()" class="btn btn-danger"
id="delete-confirm">{{ 'service.overview.delete' | translate }}
</button>
</div>
</div>
</div>
</ng-template>

View File

@@ -0,0 +1,29 @@
.status-indicator {
padding: 2.5px 25px 2.5px 25px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.5s;
}
.status-enabled {
background-color: #daf7a6;
color: #4f5359;
font-size: 85%;
font-weight: bold;
}
.status-enabled:hover {
background-color: #faa0a0;
}
.status-disabled {
background-color: #faa0a0;
color: #4f5359;
font-size: 85%;
font-weight: bold;
}
.status-disabled:hover {
background-color: #daf7a6;
}

View File

@@ -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<ServicesDirectoryComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ServicesDirectoryComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ServicesDirectoryComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,163 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit, TemplateRef, ViewChild } 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 { 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 {
selectedServiceId: number | null = null;
servicesData: any[] = [];
@ViewChild('deleteModal', {static: true}) deleteModal: TemplateRef<any>;
ldnServicesRD$: Observable<RemoteData<PaginatedList<LdnService>>>;
config: FindListOptions = Object.assign(new FindListOptions(), {
elementsPerPage: 20
});
pageConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'po',
pageSize: 20
});
isProcessingSub: Subscription;
private modalRef: any;
constructor(
protected processLdnService: LdnServicesService,
protected paginationService: PaginationService,
protected modalService: NgbModal,
public ldnDirectoryService: LdnDirectoryService,
private http: HttpClient,
private cdRef: ChangeDetectorRef
) {
}
ngOnInit(): void {
/*this.ldnDirectoryService.listLdnServices();*/
this.findAllServices();
this.setLdnServices();
/*this.ldnServicesRD$.subscribe(data => {
console.log('searchByLdnUrl()', data);
});*/
/*this.ldnServicesRD$.pipe(
tap(data => {
console.log('ldnServicesRD$ data:', data);
})
).subscribe(() => {
this.searchByLdnUrl();
});*/
}
setLdnServices() {
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();
this.cdRef.detectChanges();
}
findAllServices(): void {
this.retrieveAll().subscribe(
(response) => {
this.servicesData = response._embedded.ldnservices;
console.log('ServicesData =', this.servicesData);
this.cdRef.detectChanges();
},
(error) => {
console.error('Error:', error);
}
);
}
retrieveAll(): Observable<any> {
const url = 'http://localhost:8080/server/api/ldn/ldnservices';
return this.http.get(url);
}
deleteSelected() {
if (this.selectedServiceId !== null) {
const deleteUrl = `http://localhost:8080/server/api/ldn/ldnservices/${this.selectedServiceId}`;
this.http.delete(deleteUrl).subscribe(
() => {
this.closeModal();
this.findAllServices();
},
(error) => {
console.error('Error deleting service:', error);
}
);
}
}
selectServiceToDelete(serviceId: number) {
this.selectedServiceId = serviceId;
this.openDeleteModal(this.deleteModal);
}
toggleStatus(ldnService: any): void {
const newStatus = !ldnService.enabled;
const apiUrl = `http://localhost:8080/server/api/ldn/ldnservices/${ldnService.id}`;
const patchOperation = {
op: 'replace',
path: '/enabled',
value: newStatus,
};
this.http.patch(apiUrl, [patchOperation]).subscribe(
() => {
console.log('Status updated successfully.');
ldnService.enabled = newStatus;
this.cdRef.detectChanges();
},
(error) => {
console.error('Error updating status:', error);
}
);
}
fetchServiceData(serviceId: string): void {
const apiUrl = `http://localhost:8080/server/api/ldn/ldnservices/${serviceId}`;
this.http.get(apiUrl).subscribe(
(data: any) => {
console.log(data);
},
(error) => {
console.error('Error fetching service data:', error);
}
);
}
}

View File

@@ -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<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
return true;
/*return this.notifyInfoService.isCoarConfigEnabled().pipe(
map(coarLdnEnabled => {
if (coarLdnEnabled) {
return true;
} else {
return this.router.parseUrl('/404');
}
})
);*/
}
}

View File

@@ -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();
});
});

View File

@@ -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'
},
];

View File

@@ -0,0 +1,8 @@
/**
* List of services statuses
*/
export enum LdnServiceStatus {
UNKOWN,
DISABLED,
ENABLED,
}

View File

@@ -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('ldnservices');

View File

@@ -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;
}

View File

@@ -0,0 +1,10 @@
/**
* List of parameter types used for scripts
*/
export enum LdnServiceConstrainType {
STRING = 'String',
DATE = 'date',
BOOLEAN = 'boolean',
FILE = 'InputStream',
OUTPUT = 'OutputStream'
}

View File

@@ -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}`);

View File

@@ -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();
});
});

View File

@@ -0,0 +1,58 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, tap } 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<LdnService[]> {
const endpoint = `${this.baseUrl}`;
return this.http.get<LdnService[]>(endpoint).pipe(
tap(data => {
console.log('listLdnServices() Data:', data);
})
);
}
public getLdnServiceById(id: string): Observable<LdnService> {
const endpoint = `${this.baseUrl}/${id}`;
return this.http.get<LdnService>(endpoint);
}
public createLdnService(ldnService: LdnService): Observable<LdnService> {
return this.http.post<LdnService>(this.baseUrl, ldnService);
}
public updateLdnService(id: string, ldnService: LdnService): Observable<LdnService> {
const endpoint = `${this.baseUrl}/${id}`;
return this.http.put<LdnService>(endpoint, ldnService);
}
public deleteLdnService(id: string): Observable<void> {
const endpoint = `${this.baseUrl}/${id}`;
return this.http.delete<void>(endpoint);
}
public searchLdnServicesByLdnUrl(ldnUrl: string): Observable<LdnService[]> {
const endpoint = `${this.baseUrl}/search/byLdnUrl?ldnUrl=${ldnUrl}`;
return this.http.get<LdnService[]>(endpoint);
}
public getItemFilters(): Observable<any> {
const itemFiltersEndpoint = `${this.itemFilterEndpoint}`;
return this.http.get(itemFiltersEndpoint);
}
}

View File

@@ -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();
});
});

View File

@@ -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<boolean> = new BehaviorSubject<boolean>(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<LdnService>) => {
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<LdnService>) => rd.hasSucceeded),
count(),
).subscribe((value) => {
this.notificationsService.success(this.translateService.get('process.bulk.delete.success', {count: value}));
this.isProcessingBehaviorSubject.next(false);
});
}
}

View File

@@ -11,3 +11,5 @@ export function getRegistriesModuleRoute() {
export function getNotificationsModuleRoute() { export function getNotificationsModuleRoute() {
return new URLCombiner(getAdminModuleRoute(), NOTIFICATIONS_MODULE_PATH).toString(); return new URLCombiner(getAdminModuleRoute(), NOTIFICATIONS_MODULE_PATH).toString();
} }
export const LDN_PATH = 'ldn';

View File

@@ -6,7 +6,7 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso
import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component'; import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component';
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service'; import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component'; 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, NOTIFICATIONS_MODULE_PATH } from './admin-routing-paths';
import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component'; import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component';
@NgModule({ @NgModule({
@@ -52,13 +52,24 @@ import { BatchImportPageComponent } from './admin-import-batch-page/batch-import
component: BatchImportPageComponent, component: BatchImportPageComponent,
data: { title: 'admin.batch-import.title', breadcrumbKey: 'admin.batch-import' } 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', path: 'system-wide-alert',
resolve: { breadcrumb: I18nBreadcrumbResolver }, resolve: { breadcrumb: I18nBreadcrumbResolver },
loadChildren: () => import('../system-wide-alert/system-wide-alert.module').then((m) => m.SystemWideAlertModule), 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'} data: {title: 'admin.system-wide-alert.title', breadcrumbKey: 'admin.system-wide-alert'}
}, },
]) ]),
], ],
providers: [ providers: [
I18nBreadcrumbResolver, I18nBreadcrumbResolver,

View File

@@ -187,6 +187,7 @@ import { NonHierarchicalBrowseDefinition } from './shared/non-hierarchical-brows
import { BulkAccessConditionOptions } from './config/models/bulk-access-condition-options.model'; import { BulkAccessConditionOptions } from './config/models/bulk-access-condition-options.model';
import { SuggestionTarget } from './suggestion-notifications/reciter-suggestions/models/suggestion-target.model'; import { SuggestionTarget } from './suggestion-notifications/reciter-suggestions/models/suggestion-target.model';
import { SuggestionSource } from './suggestion-notifications/reciter-suggestions/models/suggestion-source.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 * When not in production, endpoint responses can be mocked for testing purposes
@@ -309,7 +310,8 @@ const PROVIDERS = [
OrcidAuthService, OrcidAuthService,
OrcidQueueDataService, OrcidQueueDataService,
OrcidHistoryDataService, OrcidHistoryDataService,
SupervisionOrderDataService SupervisionOrderDataService,
LdnServicesService,
]; ];
/** /**

View File

@@ -222,6 +222,18 @@ export class MenuResolver implements Resolve<boolean> {
text: 'menu.section.new_process', text: 'menu.section.new_process',
link: '/processes/new' link: '/processes/new'
} as LinkMenuItemModel, } as LinkMenuItemModel,
},/* ldn_services */
{
id: 'ldn_services_new',
parentID: 'new',
active: false,
visible: isSiteAdmin,
model: {
type: MenuItemType.LINK,
text: 'menu.section.services_new',
link: '/admin/ldn/services/new'
} as LinkMenuItemModel,
icon: '',
}, },
]; ];
const editSubMenuList = [ const editSubMenuList = [
@@ -350,6 +362,19 @@ export class MenuResolver implements Resolve<boolean> {
icon: 'terminal', icon: 'terminal',
index: 10 index: 10
}, },
/* LDN Services */
{
id: 'ldn_services',
active: false,
visible: isSiteAdmin,
model: {
type: MenuItemType.LINK,
text: 'menu.section.services',
link: '/admin/ldn/services'
} as LinkMenuItemModel,
icon: 'inbox',
index: 14
},
{ {
id: 'health', id: 'health',
active: false, active: false,

View File

@@ -902,7 +902,67 @@
"coar-notify-support.message-moderation.content": "To ensure a secure and productive environment, all incoming LDN messages are moderated. If you are planning to exchange information with us, kindly reach out via our dedicated Feedback form. You can access the Feedback form by clicking <a href=\"info/feedback\">here</a>.", "coar-notify-support.message-moderation.content": "To ensure a secure and productive environment, all incoming LDN messages are moderated. If you are planning to exchange information with us, kindly reach out via our dedicated Feedback form. You can access the Feedback form by clicking <a href=\"info/feedback\">here</a>.",
"service.overview.delete.header": "Delete Service",
"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-service.overview.table.enabled": "Enabled",
"ldn-service.overview.table.disabled": "Disabled",
"ldn-service.overview.table.clickToEnable": "Click to enable",
"ldn-service.overview.table.clickToDisable": "Click to disable",
"ldn-service-status": "Status",
"ldn-register-new-service.title": "Register a new service",
"ldn-new-service.form.label.submit": "Submit",
"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.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.ldnUrl": "Please specify the URL of the LDN Inbox",
"ldn-new-service.form.label.inboundPattern": "Inbound Patterns",
"ldn-new-service.form.label.placeholder.inboundPattern": "Select an Inbound Pattern",
"ldn-new-service.form.label.placeholder.selectedItemFilter": "No Item Filter Selected",
"ldn-new-service.form.label.ItemFilter": "Item Filter",
"ldn-new-service.form.label.automatic": "Automatic",
"ldn-new-service.form.label.outboundPattern": "Outbound Patterns",
"ldn-new-service.form.label.placeholder.outboundPattern": "Select an Outbound Pattern",
"ldn-new-service.form.label.addPattern": "+ Add more",
"ldn-new-service.form.label.removeItemFilter": "Remove",
"ldn-register-new-service.breadcrumbs": "New Service",
"service.overview.delete.body": "Are you sure you want to delete this service?",
"service.overview.delete": "Delete service",
"ldn-edit-service.title": "Edit service",
"ldn-edit-service.form.label.name": "Name",
"ldn-edit-service.form.label.description": "Description",
"ldn-edit-service.form.label.url": "Service URL",
"ldn-edit-service.form.label.ldnUrl": "LDN Inbox URL",
"ldn-edit-service.form.label.inboundPattern": "Inbound Pattern",
"ldn-edit-service.form.label.noInboundPatternSelected": "No Inbound Pattern",
"ldn-edit-service.form.label.selectedItemFilter": "Selected Item Filter",
"ldn-edit-service.form.label.selectItemFilter": "No Item Filter",
"ldn-edit-service.form.label.automatic": "Automatic",
"ldn-edit-service.form.label.addInboundPattern": "+ Add more",
"ldn-edit-service.form.label.outboundPattern": "Outbound Pattern",
"ldn-edit-service.form.label.noOutboundPatternSelected": "No Outbound Pattern",
"ldn-edit-service.form.label.addOutboundPattern": "+ Add more",
"ldn-edit-service.form.label.submit": "Submit",
"ldn-edit-service.breadcrumbs": "Edit 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.head": "Create a Collection",
@@ -2920,6 +2980,8 @@
"menu.section.icon.notifications": "Notifications menu section", "menu.section.icon.notifications": "Notifications menu section",
"menu.section.icon.ldn_services": "LDN Services overview",
"menu.section.import": "Import", "menu.section.import": "Import",
"menu.section.import_batch": "Batch Import (ZIP)", "menu.section.import_batch": "Batch Import (ZIP)",
@@ -2958,6 +3020,10 @@
"menu.section.registries_metadata": "Metadata", "menu.section.registries_metadata": "Metadata",
"menu.section.services": "LDN Services",
"menu.section.services_new": "LDN Service",
"menu.section.statistics": "Statistics", "menu.section.statistics": "Statistics",
"menu.section.statistics_task": "Statistics Task", "menu.section.statistics_task": "Statistics Task",
@@ -3346,6 +3412,10 @@
"process.new.breadcrumbs": "Create a new process", "process.new.breadcrumbs": "Create a new process",
"process.detail.arguments": "Arguments", "process.detail.arguments": "Arguments",
"process.detail.arguments.empty": "This process doesn't contain any arguments", "process.detail.arguments.empty": "This process doesn't contain any arguments",
@@ -3428,6 +3498,39 @@
"process.bulk.delete.success": "{{count}} process(es) have been succesfully deleted", "process.bulk.delete.success": "{{count}} process(es) have been succesfully deleted",
"service.detail.delete.cancel": "Cancel",
"service.detail.delete.button": "Delete service",
"service.detail.delete.header": "Delete service",
"service.detail.delete.body": "Are you sure you want to delete the current service?",
"service.detail.delete.confirm": "Delete service",
"service.detail.delete.success": "The service was successfully deleted.",
"service.detail.delete.error": "Something went wrong when deleting the service",
"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.breadcrumbs": "Update Profile",
"profile.card.identify": "Identify", "profile.card.identify": "Identify",