diff --git a/backend/src/main/resources/i18n/messages.properties b/backend/src/main/resources/i18n/messages.properties index 7cadee855..03ae27513 100644 --- a/backend/src/main/resources/i18n/messages.properties +++ b/backend/src/main/resources/i18n/messages.properties @@ -236,6 +236,7 @@ label.filter-name=Filter Name label.filter-enabled=Filter Enabled label.filter-target=FilterTarget label.filter-type=Filter Type +label.option=Option label.value=Value label.binding-type=Binding Type label.sign-assertion=Sign Assertions diff --git a/backend/src/main/resources/metadata-sources-ui-schema.json b/backend/src/main/resources/metadata-sources-ui-schema.json index 3f002253a..9d64e75b4 100644 --- a/backend/src/main/resources/metadata-sources-ui-schema.json +++ b/backend/src/main/resources/metadata-sources-ui-schema.json @@ -310,6 +310,7 @@ }, "attributeRelease": { "type": "array", + "title": "label.attribute-release", "description": "Attribute release table - select the attributes you want to release (default unchecked)", "widget": { "id": "checklist", diff --git a/ui/package.json b/ui/package.json index 6cfe807cd..60a73244f 100644 --- a/ui/package.json +++ b/ui/package.json @@ -6,7 +6,7 @@ "ng": "ng", "start": "ng serve --proxy-config proxy.conf.json", "build": "ng build", - "test": "ng test --code-coverage --source-map=false", + "test": "ng test --code-coverage --source-map=true", "lint": "ng lint", "e2e": "ng e2e", "build:static": "node-sass src/static.scss ./dist/unsecured/static.css", diff --git a/ui/src/app/metadata/configuration/action/configuration.action.ts b/ui/src/app/metadata/configuration/action/configuration.action.ts new file mode 100644 index 000000000..1aa442601 --- /dev/null +++ b/ui/src/app/metadata/configuration/action/configuration.action.ts @@ -0,0 +1,130 @@ +import { Action } from '@ngrx/store'; +import { Metadata } from '../../domain/domain.type'; +import { Schema } from '../model/schema'; +import { Wizard } from '../../../wizard/model'; + +export enum ConfigurationActionTypes { + LOAD_METADATA_REQUEST = '[Metadata Configuration] Load Metadata Request', + LOAD_METADATA_SUCCESS = '[Metadata Configuration] Load Metadata Success', + LOAD_METADATA_ERROR = '[Metadata Configuration] Load Metadata Error', + + LOAD_SCHEMA_REQUEST = '[Metadata Configuration] Load Schema Request', + LOAD_SCHEMA_SUCCESS = '[Metadata Configuration] Load Schema Success', + LOAD_SCHEMA_ERROR = '[Metadata Configuration] Load Schema Error', + + LOAD_XML_REQUEST = '[Metadata Configuration] Load XML Request', + LOAD_XML_SUCCESS = '[Metadata Configuration] Load XML Success', + LOAD_XML_ERROR = '[Metadata Configuration] Load XML Error', + + SET_METADATA = '[Metadata Configuration] Set Metadata Model', + SET_DEFINITION = '[Metadata Configuration] Set Metadata Definition', + SET_SCHEMA = '[Metadata Configuration] Set Metadata Schema', + SET_XML = '[Metadata Configuration] Set Metadata Xml', + + DOWNLOAD_XML = '[Metadata Configuration] Download Metadata Xml', + + CLEAR = '[Metadata Configuration] Clear' +} + +export class LoadMetadataRequest implements Action { + readonly type = ConfigurationActionTypes.LOAD_METADATA_REQUEST; + + constructor(public payload: { id: string, type: string }) { } +} + +export class LoadMetadataSuccess implements Action { + readonly type = ConfigurationActionTypes.LOAD_METADATA_SUCCESS; + + constructor(public payload: Metadata) { } +} + +export class LoadMetadataError implements Action { + readonly type = ConfigurationActionTypes.LOAD_METADATA_ERROR; + + constructor(public payload: any) { } +} + +export class LoadSchemaRequest implements Action { + readonly type = ConfigurationActionTypes.LOAD_SCHEMA_REQUEST; + + constructor(public payload: string) { } +} + +export class LoadSchemaSuccess implements Action { + readonly type = ConfigurationActionTypes.LOAD_SCHEMA_SUCCESS; + + constructor(public payload: Schema) { } +} + +export class LoadSchemaError implements Action { + readonly type = ConfigurationActionTypes.LOAD_SCHEMA_ERROR; + + constructor(public payload: any) { } +} + +export class LoadXmlRequest implements Action { + readonly type = ConfigurationActionTypes.LOAD_XML_REQUEST; + + constructor(public payload: string) { } +} + +export class LoadXmlSuccess implements Action { + readonly type = ConfigurationActionTypes.LOAD_XML_SUCCESS; + + constructor(public payload: string) { } +} + +export class LoadXmlError implements Action { + readonly type = ConfigurationActionTypes.LOAD_XML_ERROR; + + constructor(public payload: any) { } +} + +export class SetMetadata implements Action { + readonly type = ConfigurationActionTypes.SET_METADATA; + + constructor(public payload: Metadata) { } +} + +export class SetDefinition implements Action { + readonly type = ConfigurationActionTypes.SET_DEFINITION; + + constructor(public payload: Wizard) { } +} + +export class SetSchema implements Action { + readonly type = ConfigurationActionTypes.SET_SCHEMA; + + constructor(public payload: Schema) { } +} + +export class SetXml implements Action { + readonly type = ConfigurationActionTypes.SET_XML; + + constructor(public payload: string) { } +} + +export class DownloadXml implements Action { + readonly type = ConfigurationActionTypes.DOWNLOAD_XML; +} + +export class ClearConfiguration implements Action { + readonly type = ConfigurationActionTypes.CLEAR; +} + +export type ConfigurationActionsUnion = + | LoadMetadataRequest + | LoadMetadataSuccess + | LoadMetadataError + | LoadSchemaRequest + | LoadSchemaSuccess + | LoadSchemaError + | LoadXmlRequest + | LoadXmlSuccess + | LoadXmlError + | SetMetadata + | SetDefinition + | SetSchema + | SetXml + | DownloadXml + | ClearConfiguration; diff --git a/ui/src/app/metadata/configuration/component/array-property.component.html b/ui/src/app/metadata/configuration/component/array-property.component.html new file mode 100644 index 000000000..ee177445e --- /dev/null +++ b/ui/src/app/metadata/configuration/component/array-property.component.html @@ -0,0 +1,51 @@ +
+ +
{{ property.name }}
+
+
+
+
+ {{ i + 1 }}.  + {{ property.items.properties[prop].title }} +
+
+ {{ value[prop] }} +
+
+
+
+
+ + + + + + + + +
+ {{ attr.label }} +
+ + true + + + false + +
+
+
+
+
+ +
+ {{ property.name }} +

+
    +
  • + {{ item }} +
  • +
+
+
\ No newline at end of file diff --git a/ui/src/app/metadata/configuration/component/array-property.component.spec.ts b/ui/src/app/metadata/configuration/component/array-property.component.spec.ts new file mode 100644 index 000000000..453d9d484 --- /dev/null +++ b/ui/src/app/metadata/configuration/component/array-property.component.spec.ts @@ -0,0 +1,115 @@ +import { Component, ViewChild, Input } from '@angular/core'; +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { NgbDropdownModule, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'; +import { Property } from '../../domain/model/property'; +import { MockI18nModule } from '../../../../testing/i18n.stub'; +import { SCHEMA } from '../../../../testing/form-schema.stub'; +import { getStepProperty } from '../../domain/utility/configuration'; +import { ArrayPropertyComponent } from './array-property.component'; +import { AttributesService } from '../../domain/service/attributes.service'; +import { MockAttributeService } from '../../../../testing/attributes.stub'; +import { of } from 'rxjs'; + +@Component({ + template: ` + + ` +}) +class TestHostComponent { + @ViewChild(ArrayPropertyComponent) + public componentUnderTest: ArrayPropertyComponent; + + property: Property = getStepProperty(SCHEMA.properties.list, { + name: 'foo', + type: 'baz', + description: 'foo bar baz', + list: [] + }, SCHEMA.definitions); + + setProperty(property: Property): void { + this.property = property; + } +} + +describe('Array Property Component', () => { + + let fixture: ComponentFixture; + let instance: TestHostComponent; + let app: ArrayPropertyComponent; + let service: AttributesService; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + NgbPopoverModule, + MockI18nModule, + RouterTestingModule + ], + declarations: [ + ArrayPropertyComponent, + TestHostComponent + ], + providers: [ + { provide: AttributesService, useClass: MockAttributeService } + ] + }).compileComponents(); + + fixture = TestBed.createComponent(TestHostComponent); + instance = fixture.componentInstance; + app = instance.componentUnderTest; + service = TestBed.get(AttributesService); + fixture.detectChanges(); + })); + + it('should accept a property input', async(() => { + expect(app).toBeTruthy(); + })); + + describe('attributeList$ getter', () => { + it('should return an empty list when no data or dataUrl is set', () => { + app.attributeList$.subscribe((list) => { + expect(list).toEqual([]); + }); + }); + it('should return a list of data items from the schema', () => { + const datalist = [ + { key: 'foo', label: 'foo' }, + { key: 'bar', label: 'bar' }, + { key: 'baz', label: 'baz' }, + ]; + instance.setProperty({ + ...instance.property, + widget: { + id: 'datalist', + data: datalist + } + }); + fixture.detectChanges(); + app.attributeList$.subscribe(list => { + expect(list).toEqual(datalist); + }); + }); + + it('should call the attribute service with a provided dataUrl', () => { + const datalist = [ + { key: 'foo', label: 'foo' }, + { key: 'bar', label: 'bar' }, + { key: 'baz', label: 'baz' }, + ]; + spyOn(service, 'query').and.returnValue(of(datalist)); + instance.setProperty({ + ...instance.property, + widget: { + id: 'datalist', + dataUrl: '/foo' + } + }); + fixture.detectChanges(); + app.attributeList$.subscribe(list => { + expect(list).toEqual(datalist); + }); + }); + }); +}); diff --git a/ui/src/app/metadata/configuration/component/array-property.component.ts b/ui/src/app/metadata/configuration/component/array-property.component.ts new file mode 100644 index 000000000..ff9cd0ac4 --- /dev/null +++ b/ui/src/app/metadata/configuration/component/array-property.component.ts @@ -0,0 +1,32 @@ +import { Component, Input } from '@angular/core'; +import { Property } from '../../domain/model/property'; +import { Observable, of } from 'rxjs'; +import { AttributesService } from '../../domain/service/attributes.service'; +import { ConfigurationPropertyComponent } from './configuration-property.component'; + +@Component({ + selector: 'array-property', + templateUrl: './array-property.component.html', + styleUrls: [] +}) + +export class ArrayPropertyComponent extends ConfigurationPropertyComponent { + @Input() property: Property; + + constructor( + private attrService: AttributesService + ) { + super(); + } + + get attributeList$(): Observable<{ key: string, label: string }[]> { + if (this.property.widget && this.property.widget.hasOwnProperty('data')) { + return of(this.property.widget.data); + } + if (this.property.widget && this.property.widget.hasOwnProperty('dataUrl')) { + return this.attrService.query(this.property.widget.dataUrl); + } + return of([]); + } +} + diff --git a/ui/src/app/metadata/configuration/component/configuration-property.component.html b/ui/src/app/metadata/configuration/component/configuration-property.component.html new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/metadata/configuration/component/configuration-property.component.spec.ts b/ui/src/app/metadata/configuration/component/configuration-property.component.spec.ts new file mode 100644 index 000000000..5ef0452e0 --- /dev/null +++ b/ui/src/app/metadata/configuration/component/configuration-property.component.spec.ts @@ -0,0 +1,69 @@ +import { Component, ViewChild, Input } from '@angular/core'; +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { NgbDropdownModule, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'; +import { Property } from '../../domain/model/property'; +import { MockI18nModule } from '../../../../testing/i18n.stub'; +import { SCHEMA } from '../../../../testing/form-schema.stub'; +import { getStepProperties, getStepProperty } from '../../domain/utility/configuration'; +import { ConfigurationPropertyComponent } from './configuration-property.component'; + +@Component({ + template: ` + + ` +}) +class TestHostComponent { + @ViewChild(ConfigurationPropertyComponent) + public componentUnderTest: ConfigurationPropertyComponent; + + property: Property = getStepProperty(SCHEMA.properties.name, { + name: 'foo', + type: 'baz', + description: 'foo bar baz' + }, SCHEMA.definitions); +} + +describe('Configuration Property Component', () => { + + let fixture: ComponentFixture; + let instance: TestHostComponent; + let app: ConfigurationPropertyComponent; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + NgbPopoverModule, + MockI18nModule, + RouterTestingModule + ], + declarations: [ + ConfigurationPropertyComponent, + TestHostComponent + ] + }).compileComponents(); + + fixture = TestBed.createComponent(TestHostComponent); + instance = fixture.componentInstance; + app = instance.componentUnderTest; + fixture.detectChanges(); + })); + + it('should accept a property input', async(() => { + expect(app).toBeTruthy(); + })); + + describe('getKeys method', () => { + it('should return the property`s child keys', () => { + expect(app.getKeys({ properties: { foo: 'bar', baz: 'bar' } })).toEqual(['foo', 'baz']); + }); + }); + + describe('getItemType method', () => { + it('should return the item`s type', () => { + expect(app.getItemType({ widget: { id: 'string' } } as Property)).toBe('string'); + expect(app.getItemType({} as Property)).toBe('default'); + }); + }); +}); diff --git a/ui/src/app/metadata/configuration/component/configuration-property.component.ts b/ui/src/app/metadata/configuration/component/configuration-property.component.ts new file mode 100644 index 000000000..bcdd45711 --- /dev/null +++ b/ui/src/app/metadata/configuration/component/configuration-property.component.ts @@ -0,0 +1,23 @@ +import { Component, Input } from '@angular/core'; +import { Property } from '../../domain/model/property'; + +@Component({ + selector: 'configuration-property', + template: `{{ property | json }}`, + styleUrls: [] +}) + +export class ConfigurationPropertyComponent { + @Input() property: Property; + + constructor() { } + + getKeys(schema): string[] { + return Object.keys(schema.properties); + } + + getItemType(items: Property): string { + return items.widget ? items.widget.id : 'default'; + } +} + diff --git a/ui/src/app/metadata/configuration/component/metadata-configuration.component.html b/ui/src/app/metadata/configuration/component/metadata-configuration.component.html new file mode 100644 index 000000000..258b9aa77 --- /dev/null +++ b/ui/src/app/metadata/configuration/component/metadata-configuration.component.html @@ -0,0 +1,23 @@ +
+
+
+
+

+ 0{{ i + 1 }} + {{ section.label | translate }} +

+ +
+
+ Option + Value +
+ +
+
+
diff --git a/ui/src/app/metadata/configuration/component/metadata-configuration.component.spec.ts b/ui/src/app/metadata/configuration/component/metadata-configuration.component.spec.ts new file mode 100644 index 000000000..f26c8d65c --- /dev/null +++ b/ui/src/app/metadata/configuration/component/metadata-configuration.component.spec.ts @@ -0,0 +1,60 @@ +import { Component, ViewChild, Input } from '@angular/core'; +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { MetadataConfigurationComponent } from './metadata-configuration.component'; + +import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; +import { MetadataConfiguration } from '../model/metadata-configuration'; +import { Property } from '../../domain/model/property'; +import { MockI18nModule } from '../../../../testing/i18n.stub'; + +@Component({ + selector: 'object-property', + template: `` +}) +class ObjectPropertyComponent { + @Input() property: Property; +} + +@Component({ + template: ` + + ` +}) +class TestHostComponent { + @ViewChild(MetadataConfigurationComponent) + public componentUnderTest: MetadataConfigurationComponent; + + configuration: MetadataConfiguration = {sections: []}; +} + +describe('Metadata Configuration Component', () => { + + let fixture: ComponentFixture; + let instance: TestHostComponent; + let app: MetadataConfigurationComponent; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + NgbDropdownModule, + MockI18nModule, + RouterTestingModule + ], + declarations: [ + MetadataConfigurationComponent, + ObjectPropertyComponent, + TestHostComponent + ], + }).compileComponents(); + + fixture = TestBed.createComponent(TestHostComponent); + instance = fixture.componentInstance; + app = instance.componentUnderTest; + fixture.detectChanges(); + })); + + it('should accept a configuration input', async(() => { + expect(app).toBeTruthy(); + })); +}); diff --git a/ui/src/app/metadata/configuration/component/metadata-configuration.component.ts b/ui/src/app/metadata/configuration/component/metadata-configuration.component.ts new file mode 100644 index 000000000..732b920d3 --- /dev/null +++ b/ui/src/app/metadata/configuration/component/metadata-configuration.component.ts @@ -0,0 +1,14 @@ +import { Component, ChangeDetectionStrategy, Input } from '@angular/core'; +import { MetadataConfiguration } from '../model/metadata-configuration'; + +@Component({ + selector: 'metadata-configuration', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './metadata-configuration.component.html', + styleUrls: [] +}) +export class MetadataConfigurationComponent { + @Input() configuration: MetadataConfiguration; + + constructor() { } +} diff --git a/ui/src/app/metadata/configuration/component/object-property.component.html b/ui/src/app/metadata/configuration/component/object-property.component.html new file mode 100644 index 000000000..b0aa7b967 --- /dev/null +++ b/ui/src/app/metadata/configuration/component/object-property.component.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/ui/src/app/metadata/configuration/component/object-property.component.spec.ts b/ui/src/app/metadata/configuration/component/object-property.component.spec.ts new file mode 100644 index 000000000..6299b20d5 --- /dev/null +++ b/ui/src/app/metadata/configuration/component/object-property.component.spec.ts @@ -0,0 +1,60 @@ +import { Component, ViewChild, Input } from '@angular/core'; +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { NgbDropdownModule, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'; +import { Property } from '../../domain/model/property'; +import { MockI18nModule } from '../../../../testing/i18n.stub'; +import { ObjectPropertyComponent } from './object-property.component'; +import { SCHEMA } from '../../../../testing/form-schema.stub'; +import { getStepProperties, getStepProperty } from '../../domain/utility/configuration'; +import { PrimitivePropertyComponent } from './primitive-property.component'; +import { ArrayPropertyComponent } from './array-property.component'; + +@Component({ + template: ` + + ` +}) +class TestHostComponent { + @ViewChild(ObjectPropertyComponent) + public componentUnderTest: ObjectPropertyComponent; + + property: Property = getStepProperty(SCHEMA.properties.name, { + name: 'foo', + type: 'baz', + description: 'foo bar baz' + }, SCHEMA.definitions); +} + +describe('Object Property Component', () => { + + let fixture: ComponentFixture; + let instance: TestHostComponent; + let app: ObjectPropertyComponent; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + NgbPopoverModule, + MockI18nModule, + RouterTestingModule + ], + declarations: [ + ObjectPropertyComponent, + PrimitivePropertyComponent, + ArrayPropertyComponent, + TestHostComponent + ] + }).compileComponents(); + + fixture = TestBed.createComponent(TestHostComponent); + instance = fixture.componentInstance; + app = instance.componentUnderTest; + fixture.detectChanges(); + })); + + it('should accept a property input', async(() => { + expect(app).toBeTruthy(); + })); +}); diff --git a/ui/src/app/metadata/configuration/component/object-property.component.ts b/ui/src/app/metadata/configuration/component/object-property.component.ts new file mode 100644 index 000000000..f7892877e --- /dev/null +++ b/ui/src/app/metadata/configuration/component/object-property.component.ts @@ -0,0 +1,18 @@ +import { Component, Input } from '@angular/core'; +import { Property } from '../../domain/model/property'; +import { ConfigurationPropertyComponent } from './configuration-property.component'; + +@Component({ + selector: 'object-property', + templateUrl: './object-property.component.html', + styleUrls: [] +}) + +export class ObjectPropertyComponent extends ConfigurationPropertyComponent { + @Input() property: Property; + + constructor() { + super(); + } +} + diff --git a/ui/src/app/metadata/configuration/component/primitive-property.component.html b/ui/src/app/metadata/configuration/component/primitive-property.component.html new file mode 100644 index 000000000..9eef2181f --- /dev/null +++ b/ui/src/app/metadata/configuration/component/primitive-property.component.html @@ -0,0 +1,5 @@ +
+ {{ property.name }} + {{ property.value || property.value === false ? property.value : '-' }} +
\ No newline at end of file diff --git a/ui/src/app/metadata/configuration/component/primitive-property.component.spec.ts b/ui/src/app/metadata/configuration/component/primitive-property.component.spec.ts new file mode 100644 index 000000000..9ce81bfb5 --- /dev/null +++ b/ui/src/app/metadata/configuration/component/primitive-property.component.spec.ts @@ -0,0 +1,60 @@ +import { Component, ViewChild, Input } from '@angular/core'; +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; +import { Property } from '../../domain/model/property'; +import { MockI18nModule } from '../../../../testing/i18n.stub'; +import { PrimitivePropertyComponent } from './primitive-property.component'; + +@Component({ + template: ` + + ` +}) +class TestHostComponent { + @ViewChild(PrimitivePropertyComponent) + public componentUnderTest: PrimitivePropertyComponent; + + property: Property = { + title: 'foo', + type: 'string', + name: 'foo', + value: ['bar'], + items: null, + properties: null, + widget: { + id: 'string' + } + }; +} + +describe('Primitive Property Component', () => { + + let fixture: ComponentFixture; + let instance: TestHostComponent; + let app: PrimitivePropertyComponent; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + NgbDropdownModule, + MockI18nModule, + RouterTestingModule + ], + declarations: [ + PrimitivePropertyComponent, + TestHostComponent + ], + }).compileComponents(); + + fixture = TestBed.createComponent(TestHostComponent); + instance = fixture.componentInstance; + app = instance.componentUnderTest; + fixture.detectChanges(); + })); + + it('should accept a property input', async(() => { + expect(app).toBeTruthy(); + })); +}); diff --git a/ui/src/app/metadata/configuration/component/primitive-property.component.ts b/ui/src/app/metadata/configuration/component/primitive-property.component.ts new file mode 100644 index 000000000..951d2c72a --- /dev/null +++ b/ui/src/app/metadata/configuration/component/primitive-property.component.ts @@ -0,0 +1,15 @@ +import { Component, Input } from '@angular/core'; +import { ConfigurationPropertyComponent } from './configuration-property.component'; + +@Component({ + selector: 'primitive-property', + templateUrl: './primitive-property.component.html', + styleUrls: [] +}) + +export class PrimitivePropertyComponent extends ConfigurationPropertyComponent { + constructor() { + super(); + } +} + diff --git a/ui/src/app/metadata/configuration/configuration.module.ts b/ui/src/app/metadata/configuration/configuration.module.ts new file mode 100644 index 000000000..2cf56580d --- /dev/null +++ b/ui/src/app/metadata/configuration/configuration.module.ts @@ -0,0 +1,63 @@ +import { NgModule, ModuleWithProviders } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { StoreModule } from '@ngrx/store'; +import { EffectsModule } from '@ngrx/effects'; + +import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'; + +import { I18nModule } from '../../i18n/i18n.module'; +import { MetadataConfigurationComponent } from './component/metadata-configuration.component'; +import { ConfigurationComponent } from './container/configuration.component'; +import { MetadataConfigurationService } from './service/configuration.service'; +import * as fromConfig from './reducer'; +import { MetadataConfigurationEffects } from './effect/configuration.effect'; +import { ConfigurationPropertyComponent } from './component/configuration-property.component'; +import { DomainModule } from '../domain/domain.module'; +import { PrimitivePropertyComponent } from './component/primitive-property.component'; +import { ObjectPropertyComponent } from './component/object-property.component'; +import { ArrayPropertyComponent } from './component/array-property.component'; +import { RouterModule } from '@angular/router'; +import { MetadataOptionsComponent } from './container/metadata-options.component'; +import { MetadataXmlComponent } from './container/metadata-xml.component'; + +@NgModule({ + declarations: [ + MetadataConfigurationComponent, + MetadataOptionsComponent, + MetadataXmlComponent, + ConfigurationPropertyComponent, + PrimitivePropertyComponent, + ObjectPropertyComponent, + ArrayPropertyComponent, + ConfigurationComponent + ], + entryComponents: [], + imports: [ + CommonModule, + I18nModule, + NgbPopoverModule, + RouterModule + ], + exports: [], + providers: [] +}) +export class MetadataConfigurationModule { + static forRoot(): ModuleWithProviders { + return { + ngModule: RootMetadataConfigurationModule, + providers: [ + MetadataConfigurationService + ] + }; + } +} + +@NgModule({ + imports: [ + MetadataConfigurationModule, + StoreModule.forFeature('metadata-configuration', fromConfig.reducers), + EffectsModule.forFeature([MetadataConfigurationEffects]) + ], + providers: [] +}) +export class RootMetadataConfigurationModule { } diff --git a/ui/src/app/metadata/configuration/configuration.routing.ts b/ui/src/app/metadata/configuration/configuration.routing.ts new file mode 100644 index 000000000..8e7327792 --- /dev/null +++ b/ui/src/app/metadata/configuration/configuration.routing.ts @@ -0,0 +1,25 @@ +import { Routes } from '@angular/router'; +import { ConfigurationComponent } from './container/configuration.component'; +import { MetadataOptionsComponent } from './container/metadata-options.component'; +import { MetadataXmlComponent } from './container/metadata-xml.component'; + +export const ConfigurationRoutes: Routes = [ + { + path: ':type/:id/configuration', + component: ConfigurationComponent, + children: [ + { + path: '', + redirectTo: 'options' + }, + { + path: 'options', + component: MetadataOptionsComponent + }, + { + path: 'xml', + component: MetadataXmlComponent + } + ] + } +]; diff --git a/ui/src/app/metadata/configuration/container/configuration.component.html b/ui/src/app/metadata/configuration/container/configuration.component.html new file mode 100644 index 000000000..36626f240 --- /dev/null +++ b/ui/src/app/metadata/configuration/container/configuration.component.html @@ -0,0 +1,17 @@ +
+
+
+
+
+ + + Metadata resolver history + +
+
+
+
+ +
+
+
\ No newline at end of file diff --git a/ui/src/app/metadata/configuration/container/configuration.component.spec.ts b/ui/src/app/metadata/configuration/container/configuration.component.spec.ts new file mode 100644 index 000000000..c399fbacf --- /dev/null +++ b/ui/src/app/metadata/configuration/container/configuration.component.spec.ts @@ -0,0 +1,55 @@ +import { Component, ViewChild, Input } from '@angular/core'; +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { StoreModule, combineReducers } from '@ngrx/store'; +import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; + +import { MetadataConfiguration } from '../model/metadata-configuration'; +import { ConfigurationComponent } from './configuration.component'; +import * as fromConfiguration from '../reducer'; +import { MockI18nModule } from '../../../../testing/i18n.stub'; + +@Component({ + template: ` + + ` +}) +class TestHostComponent { + @ViewChild(ConfigurationComponent) + public componentUnderTest: ConfigurationComponent; + + configuration: MetadataConfiguration = { sections: [] }; +} + +describe('Metadata Configuration Page Component', () => { + + let fixture: ComponentFixture; + let instance: TestHostComponent; + let app: ConfigurationComponent; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + NgbDropdownModule, + StoreModule.forRoot({ + 'metadata-configuration': combineReducers(fromConfiguration.reducers), + }), + MockI18nModule, + RouterTestingModule + ], + declarations: [ + ConfigurationComponent, + TestHostComponent + ], + }).compileComponents(); + + fixture = TestBed.createComponent(TestHostComponent); + instance = fixture.componentInstance; + app = instance.componentUnderTest; + fixture.detectChanges(); + })); + + it('should load metadata objects', async(() => { + expect(app).toBeTruthy(); + })); +}); diff --git a/ui/src/app/metadata/configuration/container/configuration.component.ts b/ui/src/app/metadata/configuration/container/configuration.component.ts new file mode 100644 index 000000000..d18e5ab34 --- /dev/null +++ b/ui/src/app/metadata/configuration/container/configuration.component.ts @@ -0,0 +1,35 @@ +import { Store } from '@ngrx/store'; +import { Component, ChangeDetectionStrategy, OnDestroy } from '@angular/core'; +import { Observable, Subject } from 'rxjs'; +import { ActivatedRoute, Params } from '@angular/router'; + +import * as fromConfiguration from '../reducer'; +import { MetadataConfiguration } from '../model/metadata-configuration'; +import { takeUntil, map } from 'rxjs/operators'; +import { LoadMetadataRequest, ClearConfiguration } from '../action/configuration.action'; + +@Component({ + selector: 'configuration-page', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './configuration.component.html', + styleUrls: [] +}) +export class ConfigurationComponent implements OnDestroy { + private ngUnsubscribe: Subject = new Subject(); + + constructor( + private store: Store, + private routerState: ActivatedRoute + ) { + this.routerState.params.pipe( + takeUntil(this.ngUnsubscribe), + map(params => new LoadMetadataRequest({id: params.id, type: params.type})) + ).subscribe(store); + } + + ngOnDestroy() { + this.ngUnsubscribe.next(); + this.ngUnsubscribe.complete(); + this.store.dispatch(new ClearConfiguration()); + } +} diff --git a/ui/src/app/metadata/configuration/container/metadata-options.component.html b/ui/src/app/metadata/configuration/container/metadata-options.component.html new file mode 100644 index 000000000..5e6b85384 --- /dev/null +++ b/ui/src/app/metadata/configuration/container/metadata-options.component.html @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/ui/src/app/metadata/configuration/container/metadata-options.component.spec.ts b/ui/src/app/metadata/configuration/container/metadata-options.component.spec.ts new file mode 100644 index 000000000..2e35d8c75 --- /dev/null +++ b/ui/src/app/metadata/configuration/container/metadata-options.component.spec.ts @@ -0,0 +1,70 @@ +import { Component, ViewChild, Input } from '@angular/core'; +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { StoreModule, combineReducers, Store } from '@ngrx/store'; +import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; + +import { MetadataConfiguration } from '../model/metadata-configuration'; +import * as fromConfiguration from '../reducer'; +import { MockI18nModule } from '../../../../testing/i18n.stub'; +import { MetadataOptionsComponent } from './metadata-options.component'; + +@Component({ + selector: 'metadata-configuration', + template: `` +}) +class MetadataConfigurationComponent { + @Input() configuration: MetadataConfiguration; +} + +@Component({ + template: ` + + ` +}) +class TestHostComponent { + @ViewChild(MetadataOptionsComponent) + public componentUnderTest: MetadataOptionsComponent; + + configuration: MetadataConfiguration = { sections: [] }; +} + +describe('Metadata Options Page Component', () => { + + let fixture: ComponentFixture; + let instance: TestHostComponent; + let app: MetadataOptionsComponent; + let store: Store; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + NgbDropdownModule, + StoreModule.forRoot({ + 'metadata-configuration': combineReducers(fromConfiguration.reducers), + }), + MockI18nModule, + RouterTestingModule + ], + declarations: [ + MetadataOptionsComponent, + MetadataConfigurationComponent, + TestHostComponent + ], + }).compileComponents(); + + store = TestBed.get(Store); + spyOn(store, 'dispatch'); + spyOn(store, 'select'); + + fixture = TestBed.createComponent(TestHostComponent); + instance = fixture.componentInstance; + app = instance.componentUnderTest; + fixture.detectChanges(); + })); + + it('should load metadata objects', async(() => { + expect(app).toBeTruthy(); + expect(store.select).toHaveBeenCalled(); + })); +}); diff --git a/ui/src/app/metadata/configuration/container/metadata-options.component.ts b/ui/src/app/metadata/configuration/container/metadata-options.component.ts new file mode 100644 index 000000000..bc2d0bb0d --- /dev/null +++ b/ui/src/app/metadata/configuration/container/metadata-options.component.ts @@ -0,0 +1,23 @@ +import { Store } from '@ngrx/store'; +import { Component, ChangeDetectionStrategy } from '@angular/core'; +import { Observable } from 'rxjs'; + +import * as fromConfiguration from '../reducer'; +import { MetadataConfiguration } from '../model/metadata-configuration'; + +@Component({ + selector: 'metadata-options-page', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './metadata-options.component.html', + styleUrls: [] +}) +export class MetadataOptionsComponent { + + configuration$: Observable; + + constructor( + private store: Store + ) { + this.configuration$ = this.store.select(fromConfiguration.getConfigurationSections); + } +} diff --git a/ui/src/app/metadata/configuration/container/metadata-xml.component.html b/ui/src/app/metadata/configuration/container/metadata-xml.component.html new file mode 100644 index 000000000..57560ea78 --- /dev/null +++ b/ui/src/app/metadata/configuration/container/metadata-xml.component.html @@ -0,0 +1,7 @@ +
+
{{ xml$ | async }}
+ +
\ No newline at end of file diff --git a/ui/src/app/metadata/configuration/container/metadata-xml.component.spec.ts b/ui/src/app/metadata/configuration/container/metadata-xml.component.spec.ts new file mode 100644 index 000000000..81fbd836f --- /dev/null +++ b/ui/src/app/metadata/configuration/container/metadata-xml.component.spec.ts @@ -0,0 +1,61 @@ +import { Component, ViewChild, Input } from '@angular/core'; +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { StoreModule, combineReducers, Store } from '@ngrx/store'; +import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; + +import { MetadataConfiguration } from '../model/metadata-configuration'; +import * as fromConfiguration from '../reducer'; +import { MockI18nModule } from '../../../../testing/i18n.stub'; +import { MetadataXmlComponent } from './metadata-xml.component'; + +@Component({ + template: ` + + ` +}) +class TestHostComponent { + @ViewChild(MetadataXmlComponent) + public componentUnderTest: MetadataXmlComponent; + + configuration: MetadataConfiguration = { sections: [] }; +} + +describe('Metadata Xml Page Component', () => { + + let fixture: ComponentFixture; + let instance: TestHostComponent; + let app: MetadataXmlComponent; + let store: Store; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + NgbDropdownModule, + StoreModule.forRoot({ + 'metadata-configuration': combineReducers(fromConfiguration.reducers), + }), + MockI18nModule, + RouterTestingModule + ], + declarations: [ + MetadataXmlComponent, + TestHostComponent + ], + }).compileComponents(); + + store = TestBed.get(Store); + spyOn(store, 'dispatch'); + spyOn(store, 'select'); + + fixture = TestBed.createComponent(TestHostComponent); + instance = fixture.componentInstance; + app = instance.componentUnderTest; + fixture.detectChanges(); + })); + + it('should load metadata objects', async(() => { + expect(app).toBeTruthy(); + expect(store.select).toHaveBeenCalledTimes(2); + })); +}); diff --git a/ui/src/app/metadata/configuration/container/metadata-xml.component.ts b/ui/src/app/metadata/configuration/container/metadata-xml.component.ts new file mode 100644 index 000000000..26b62323e --- /dev/null +++ b/ui/src/app/metadata/configuration/container/metadata-xml.component.ts @@ -0,0 +1,33 @@ +import { Store } from '@ngrx/store'; +import { Component } from '@angular/core'; +import { Observable, Subject } from 'rxjs'; + +import * as fromConfiguration from '../reducer'; +import { Metadata } from '../../domain/domain.type'; +import { DownloadXml } from '../action/configuration.action'; + +@Component({ + selector: 'metadata-xml-page', + templateUrl: './metadata-xml.component.html', + styleUrls: [] +}) +export class MetadataXmlComponent { + + private ngUnsubscribe: Subject = new Subject(); + + entity: Metadata; + entity$: Observable; + xml: string; + xml$: Observable; + + constructor( + private store: Store + ) { + this.xml$ = this.store.select(fromConfiguration.getConfigurationXml); + this.entity$ = this.store.select(fromConfiguration.getConfigurationModel); + } + + preview(): void { + this.store.dispatch(new DownloadXml()); + } +} diff --git a/ui/src/app/metadata/configuration/effect/configuration.effect.ts b/ui/src/app/metadata/configuration/effect/configuration.effect.ts new file mode 100644 index 000000000..ff76c8125 --- /dev/null +++ b/ui/src/app/metadata/configuration/effect/configuration.effect.ts @@ -0,0 +1,132 @@ +import { Injectable } from '@angular/core'; +import { Effect, Actions, ofType } from '@ngrx/effects'; +import { switchMap, catchError, map, tap, withLatestFrom } from 'rxjs/operators'; +import { of, Observable } from 'rxjs'; +import * as FileSaver from 'file-saver'; +import { Store } from '@ngrx/store'; + +import { MetadataConfigurationService } from '../service/configuration.service'; +import { + LoadMetadataRequest, + LoadMetadataSuccess, + LoadMetadataError, + ConfigurationActionTypes, + SetMetadata, + SetDefinition, + LoadSchemaRequest, + LoadSchemaSuccess, + SetSchema, + LoadSchemaError, + LoadXmlSuccess, + LoadXmlError, + SetXml, + DownloadXml +} from '../action/configuration.action'; +import { ResolverService } from '../../domain/service/resolver.service'; +import { EntityIdService } from '../../domain/service/entity-id.service'; +import { State } from '../reducer/configuration.reducer'; +import { getConfigurationModel, getConfigurationXml } from '../reducer'; +import { MetadataResolver } from '../../domain/model'; + +@Injectable() +export class MetadataConfigurationEffects { + + @Effect() + loadMetadata$ = this.actions$.pipe( + ofType(ConfigurationActionTypes.LOAD_METADATA_REQUEST), + switchMap(action => + this.configService + .find(action.payload.id, action.payload.type) + .pipe( + map(md => new LoadMetadataSuccess(md)), + catchError(error => of(new LoadMetadataError(error))) + ) + ) + ); + + @Effect() + loadMetadataXml$ = this.actions$.pipe( + ofType(ConfigurationActionTypes.LOAD_METADATA_REQUEST), + switchMap(action => { + let loader: Observable; + switch (action.payload.type) { + case 'filter': + loader = this.entityService.preview(action.payload.id); + break; + default: + loader = this.providerService.preview(action.payload.id); + break; + } + + return loader.pipe( + map(xml => new LoadXmlSuccess(xml)), + catchError(error => of(new LoadXmlError(error))) + ); + }) + ); + + @Effect() + setXmlOnLoad$ = this.actions$.pipe( + ofType(ConfigurationActionTypes.LOAD_XML_SUCCESS), + map(action => new SetXml(action.payload)) + ); + + @Effect() + setMetadataOnLoad$ = this.actions$.pipe( + ofType(ConfigurationActionTypes.LOAD_METADATA_SUCCESS), + map(action => new SetMetadata(action.payload)) + ); + + @Effect() + setDefinition$ = this.actions$.pipe( + ofType(ConfigurationActionTypes.SET_METADATA), + map(action => new SetDefinition(this.configService.getDefinition(action.payload['@type']))) + ); + + @Effect() + loadSchemaOnDefinitionSet$ = this.actions$.pipe( + ofType(ConfigurationActionTypes.SET_DEFINITION), + map(action => new LoadSchemaRequest(action.payload.schema)) + ); + + @Effect() + loadSchemaData$ = this.actions$.pipe( + ofType(ConfigurationActionTypes.LOAD_SCHEMA_REQUEST), + switchMap(action => + this.configService + .loadSchema(action.payload) + .pipe( + map(schema => new LoadSchemaSuccess(schema)), + catchError(error => of(new LoadSchemaError(error))) + ) + ) + ); + + @Effect() + setSchema$ = this.actions$.pipe( + ofType(ConfigurationActionTypes.LOAD_SCHEMA_SUCCESS), + map(action => new SetSchema(action.payload)) + ); + + @Effect({dispatch: false}) + downloadXml$ = this.actions$.pipe( + ofType(ConfigurationActionTypes.DOWNLOAD_XML), + withLatestFrom( + this.store.select(getConfigurationModel), + this.store.select(getConfigurationXml) + ), + tap(([action, entity, xml]) => { + const name = entity.name ? entity.name : (entity as MetadataResolver).serviceProviderName; + const blob = new Blob([xml], { type: 'text/xml;charset=utf-8' }); + FileSaver.saveAs(blob, `${name}.xml`); + }) + ); + + constructor( + private configService: MetadataConfigurationService, + private actions$: Actions, + private providerService: ResolverService, + private entityService: EntityIdService, + private store: Store + ) { } +} diff --git a/ui/src/app/metadata/configuration/model/metadata-configuration.ts b/ui/src/app/metadata/configuration/model/metadata-configuration.ts new file mode 100644 index 000000000..b8a37b85e --- /dev/null +++ b/ui/src/app/metadata/configuration/model/metadata-configuration.ts @@ -0,0 +1,5 @@ +import Section from './section'; + +export interface MetadataConfiguration { + sections: Section[]; +} diff --git a/ui/src/app/metadata/configuration/model/schema.ts b/ui/src/app/metadata/configuration/model/schema.ts new file mode 100644 index 000000000..469650111 --- /dev/null +++ b/ui/src/app/metadata/configuration/model/schema.ts @@ -0,0 +1,3 @@ +export interface Schema { + [prop: string]: any; +} diff --git a/ui/src/app/metadata/configuration/model/section.ts b/ui/src/app/metadata/configuration/model/section.ts new file mode 100644 index 000000000..089a1953a --- /dev/null +++ b/ui/src/app/metadata/configuration/model/section.ts @@ -0,0 +1,11 @@ +import { Property } from '../../domain/model/property'; + +export interface Section { + id: string; + index: number; + label: string; + pageNumber: number; + properties: Property[]; +} + +export default Section; diff --git a/ui/src/app/metadata/configuration/reducer/configuration.reducer.spec.ts b/ui/src/app/metadata/configuration/reducer/configuration.reducer.spec.ts new file mode 100644 index 000000000..fed39ff3f --- /dev/null +++ b/ui/src/app/metadata/configuration/reducer/configuration.reducer.spec.ts @@ -0,0 +1,105 @@ +import { reducer } from './configuration.reducer'; +import * as fromConfig from './configuration.reducer'; +import * as actions from '../action/configuration.action'; +import { MetadataSourceEditor } from '../../domain/model/wizards/metadata-source-editor'; +import { SCHEMA as schema } from '../../../../testing/form-schema.stub'; +import { MetadataResolver } from '../../domain/model'; + +describe('Configuration Reducer', () => { + const initialState: fromConfig.State = { ...fromConfig.initialState }; + + const definition = new MetadataSourceEditor(); + const model: MetadataResolver = { + id: 'foo', + serviceProviderName: 'foo', + '@type': 'MetadataResolver' + }; + const xml = ``; + + describe('undefined action', () => { + it('should return the default state', () => { + const result = reducer(undefined, {} as any); + + expect(result).toEqual(initialState); + }); + }); + + describe('SET_DEFINITION action', () => { + it('should set the state definition', () => { + const action = new actions.SetDefinition(definition); + const result = reducer(initialState, action); + + expect(result).toEqual({ ...initialState, definition }); + }); + }); + + describe('SET_SCHEMA action', () => { + it('should set the state schema', () => { + const action = new actions.SetSchema(schema); + const result = reducer(initialState, action); + + expect(result).toEqual({ ...initialState, schema }); + }); + }); + + describe('SET_METADATA action', () => { + it('should set the state metadata model', () => { + const action = new actions.SetMetadata(model as MetadataResolver); + const result = reducer(initialState, action); + + expect(result).toEqual({ ...initialState, model }); + }); + }); + + describe('SET_XML action', () => { + it('should set the state metadata model', () => { + const action = new actions.SetXml(xml); + const result = reducer(initialState, action); + + expect(result).toEqual({ ...initialState, xml }); + }); + }); + + describe('CLEAR action', () => { + it('should clear the state and reset to initial state', () => { + const action = new actions.ClearConfiguration(); + const result = reducer({ + ...initialState, + model, + definition, + schema + }, action); + + expect(result).toEqual(initialState); + }); + }); + + describe('selector functions', () => { + /* + export const getModel = (state: State) => state.model; + export const getDefinition = (state: State) => state.definition; + export const getSchema = (state: State) => state.schema; + export const getXml = (state: State) => state.xml; + */ + describe('getModel', () => { + it('should retrieve the model from state', () => { + expect(fromConfig.getModel({...initialState, model})).toBe(model); + }); + }); + describe('getDefinition', () => { + it('should retrieve the definition from state', () => { + expect(fromConfig.getDefinition({ ...initialState, definition })).toBe(definition); + }); + }); + describe('getSchema', () => { + it('should retrieve the schema from state', () => { + expect(fromConfig.getSchema({ ...initialState, schema })).toBe(schema); + }); + }); + describe('getXml', () => { + it('should retrieve the schema from state', () => { + expect(fromConfig.getXml({ ...initialState, xml })).toBe(xml); + }); + }); + }); +}); diff --git a/ui/src/app/metadata/configuration/reducer/configuration.reducer.ts b/ui/src/app/metadata/configuration/reducer/configuration.reducer.ts new file mode 100644 index 000000000..a35e20dd7 --- /dev/null +++ b/ui/src/app/metadata/configuration/reducer/configuration.reducer.ts @@ -0,0 +1,55 @@ +import { ConfigurationActionTypes, ConfigurationActionsUnion } from '../action/configuration.action'; +import { Metadata } from '../../domain/domain.type'; +import { Wizard } from '../../../wizard/model'; +import { Schema } from '../model/schema'; + +export interface State { + model: Metadata; + schema: Schema; + definition: Wizard; + xml: string; +} + +export const initialState: State = { + model: null, + schema: null, + definition: null, + xml: '' +}; + +export function reducer(state = initialState, action: ConfigurationActionsUnion): State { + switch (action.type) { + case ConfigurationActionTypes.SET_SCHEMA: + return { + ...state, + schema: action.payload + }; + case ConfigurationActionTypes.SET_DEFINITION: + return { + ...state, + definition: action.payload + }; + case ConfigurationActionTypes.SET_METADATA: + return { + ...state, + model: action.payload + }; + case ConfigurationActionTypes.SET_XML: + return { + ...state, + xml: action.payload + }; + case ConfigurationActionTypes.CLEAR: + return { + ...initialState + }; + default: { + return state; + } + } +} + +export const getModel = (state: State) => state.model; +export const getDefinition = (state: State) => state.definition; +export const getSchema = (state: State) => state.schema; +export const getXml = (state: State) => state.xml; diff --git a/ui/src/app/metadata/configuration/reducer/index.spec.ts b/ui/src/app/metadata/configuration/reducer/index.spec.ts new file mode 100644 index 000000000..5a675621c --- /dev/null +++ b/ui/src/app/metadata/configuration/reducer/index.spec.ts @@ -0,0 +1,19 @@ +import { getConfigurationSectionsFn } from './index'; +import { SCHEMA as schema } from '../../../../testing/form-schema.stub'; +import { MetadataSourceEditor } from '../../domain/model/wizards/metadata-source-editor'; + +describe('Configuration Reducer', () => { + const model = { + name: 'foo', + '@type': 'MetadataResolver' + }; + + const definition = new MetadataSourceEditor(); + + describe('getConfigurationSectionsFn', () => { + it('should parse the schema, definition, and model into a MetadataConfiguration', () => { + const config = getConfigurationSectionsFn(model, definition, schema); + expect(config.sections).toBeDefined(); + }); + }); +}); diff --git a/ui/src/app/metadata/configuration/reducer/index.ts b/ui/src/app/metadata/configuration/reducer/index.ts new file mode 100644 index 000000000..d4c2fcc9c --- /dev/null +++ b/ui/src/app/metadata/configuration/reducer/index.ts @@ -0,0 +1,58 @@ +import { createSelector, createFeatureSelector } from '@ngrx/store'; +import merge from 'deepmerge'; + +import * as fromRoot from '../../../app.reducer'; +import * as fromConfiguration from './configuration.reducer'; +import { WizardStep } from '../../../wizard/model'; + +import * as utils from '../../domain/utility/configuration'; +import { getSplitSchema } from '../../../wizard/reducer'; + +export interface ConfigurationState { + configuration: fromConfiguration.State; +} + +export const reducers = { + configuration: fromConfiguration.reducer +}; + +export interface State extends fromRoot.State { + 'metadata-configuration': ConfigurationState; +} + +export const getState = createFeatureSelector('metadata-configuration'); + +export const getConfigurationStateFn = (state: ConfigurationState) => state.configuration; + +export const getConfigurationState = createSelector(getState, getConfigurationStateFn); +export const getConfigurationModel = createSelector(getConfigurationState, fromConfiguration.getModel); +export const getConfigurationDefinition = createSelector(getConfigurationState, fromConfiguration.getDefinition); +export const getConfigurationSchema = createSelector(getConfigurationState, fromConfiguration.getSchema); +export const getConfigurationXml = createSelector(getConfigurationState, fromConfiguration.getXml); + +export const getConfigurationSectionsFn = (model, definition, schema) => !definition || !schema ? null : + ({ + sections: definition.steps + .filter(step => step.id !== 'summary') + .map( + (step: WizardStep, num: number) => { + return ({ + id: step.id, + pageNumber: num + 1, + index: step.index, + label: step.label, + properties: utils.getStepProperties( + getSplitSchema(schema, step), + definition.formatter(model), + schema.definitions || {} + ) + }); + } + ) + }); +export const getConfigurationSections = createSelector( + getConfigurationModel, + getConfigurationDefinition, + getConfigurationSchema, + getConfigurationSectionsFn +); diff --git a/ui/src/app/metadata/configuration/service/configuration.service.spec.ts b/ui/src/app/metadata/configuration/service/configuration.service.spec.ts new file mode 100644 index 000000000..b3588c174 --- /dev/null +++ b/ui/src/app/metadata/configuration/service/configuration.service.spec.ts @@ -0,0 +1,63 @@ +import { TestBed, async, inject } from '@angular/core/testing'; +import { HttpClientModule, HttpRequest } from '@angular/common/http'; +import { HttpTestingController, HttpClientTestingModule } from '@angular/common/http/testing'; +import { MetadataConfigurationService, PATHS } from './configuration.service'; +import { FileBackedHttpMetadataProviderEditor } from '../../provider/model'; +import { MetadataSourceEditor } from '../../domain/model/wizards/metadata-source-editor'; + +describe(`Attributes Service`, () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientModule, + HttpClientTestingModule + ], + providers: [ + MetadataConfigurationService + ] + }); + }); + + describe('find method', () => { + it(`should send an expected GET request`, async(inject([MetadataConfigurationService, HttpTestingController], + (service: MetadataConfigurationService, backend: HttpTestingController) => { + const type = 'resolver'; + const id = 'foo'; + service.find(id, type).subscribe(); + backend.expectOne((req: HttpRequest) => { + return req.url === `${service.base}/${PATHS[type]}/${id}` + && req.method === 'GET'; + }, `GET metadata by id and type`); + } + ))); + }); + + describe('loadSchema method', () => { + it(`should send an expected GET request`, async(inject([MetadataConfigurationService, HttpTestingController], + (service: MetadataConfigurationService, backend: HttpTestingController) => { + const path = '/foo.json'; + service.loadSchema(path).subscribe(); + backend.expectOne((req: HttpRequest) => { + return req.url === `${path}` + && req.method === 'GET'; + }, `GET schema by path`); + } + ))); + }); + + describe('getDefinition method', () => { + it(`should retrieve the editor definition by model type`, async(inject([MetadataConfigurationService, HttpTestingController], + (service: MetadataConfigurationService, backend: HttpTestingController) => { + const def = service.getDefinition('FileBackedHttpMetadataResolver'); + expect(def).toBe(FileBackedHttpMetadataProviderEditor); + } + ))); + + it(`should instantiate an editor for resolvers`, async(inject([MetadataConfigurationService], + (service: MetadataConfigurationService) => { + const def = service.getDefinition('foo'); + expect(def instanceof MetadataSourceEditor).toBe(true); + } + ))); + }); +}); diff --git a/ui/src/app/metadata/configuration/service/configuration.service.ts b/ui/src/app/metadata/configuration/service/configuration.service.ts new file mode 100644 index 000000000..2f0420c49 --- /dev/null +++ b/ui/src/app/metadata/configuration/service/configuration.service.ts @@ -0,0 +1,40 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { Metadata } from '../../domain/domain.type'; +import { Wizard } from '../../../wizard/model'; +import { MetadataSourceEditor } from '../../domain/model/wizards/metadata-source-editor'; +import { MetadataProviderEditorTypes } from '../../provider/model'; +import { Schema } from '../model/schema'; + +export enum PATHS { + resolver = 'EntityDescriptor', + provider = 'MetadataResolvers' +} + +export const DEFINITIONS = { + resolver: MetadataSourceEditor +}; + +@Injectable() +export class MetadataConfigurationService { + + readonly base = '/api'; + + constructor( + private http: HttpClient + ) {} + + find(id: string, type: string): Observable { + return this.http.get(`${this.base}/${PATHS[type]}/${id}`); + } + + getDefinition(type: string): Wizard { + return MetadataProviderEditorTypes.find(def => def.type === type) || new MetadataSourceEditor(); + } + + loadSchema(path: string): Observable { + return this.http.get(path); + } +} + diff --git a/ui/src/app/metadata/domain/component/editor-nav.component.spec.ts b/ui/src/app/metadata/domain/component/editor-nav.component.spec.ts index 3a93f2f8c..3d6627a4e 100644 --- a/ui/src/app/metadata/domain/component/editor-nav.component.spec.ts +++ b/ui/src/app/metadata/domain/component/editor-nav.component.spec.ts @@ -35,8 +35,7 @@ describe('Editor Nav Component', () => { id: 'common', label: 'Common Attributes', index: 2, - initialValues: [], - schema: 'assets/schema/provider/filebacked-http-common.schema.json' + initialValues: [] }; beforeEach(async(() => { diff --git a/ui/src/app/metadata/domain/component/wizard-summary.component.spec.ts b/ui/src/app/metadata/domain/component/wizard-summary.component.spec.ts index 335cd634f..107ef7241 100644 --- a/ui/src/app/metadata/domain/component/wizard-summary.component.spec.ts +++ b/ui/src/app/metadata/domain/component/wizard-summary.component.spec.ts @@ -4,7 +4,7 @@ import { RouterTestingModule } from '@angular/router/testing'; import { NgbDropdownModule, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'; -import { getStepProperties, WizardSummaryComponent } from './wizard-summary.component'; +import { WizardSummaryComponent } from './wizard-summary.component'; import { SchemaFormModule, WidgetRegistry, DefaultWidgetRegistry } from 'ngx-schema-form'; import { Wizard } from '../../../wizard/model'; import { MetadataProvider } from '../../domain/model'; @@ -68,17 +68,6 @@ describe('Provider Wizard Summary Component', () => { expect(app).toBeTruthy(); })); - describe('getStepProperties function', () => { - it('should return an empty array of schema or schema.properties is not defined', () => { - expect(getStepProperties(null, {})).toEqual([]); - expect(getStepProperties({}, {})).toEqual([]); - }); - - it('should return a formatted list of properties', () => { - expect(getStepProperties(SCHEMA, {}).length).toBe(2); - }); - }); - describe('gotoPage function', () => { it('should emit an empty string if page is null', () => { spyOn(app.onPageSelect, 'emit'); diff --git a/ui/src/app/metadata/domain/component/wizard-summary.component.ts b/ui/src/app/metadata/domain/component/wizard-summary.component.ts index fa570a1c0..e8b8deb34 100644 --- a/ui/src/app/metadata/domain/component/wizard-summary.component.ts +++ b/ui/src/app/metadata/domain/component/wizard-summary.component.ts @@ -1,10 +1,11 @@ import { Component, Input, SimpleChanges, OnChanges, Output, EventEmitter } from '@angular/core'; +import merge from 'deepmerge'; import { Wizard, WizardStep } from '../../../wizard/model'; import { MetadataProvider, MetadataResolver } from '../../domain/model'; import { Property } from '../model/property'; import { getSplitSchema } from '../../../wizard/reducer'; -import merge from 'deepmerge'; +import { getStepProperties } from '../utility/configuration'; interface Section { id: string; @@ -14,47 +15,6 @@ interface Section { properties: Property[]; } -export function getDefinition(path: string, definitions: any): any { - let def = path.split('/').pop(); - return definitions[def]; -} - -export function getPropertyItemSchema(items: any, definitions: any): any { - if (!items) { return null; } - return items.$ref ? getDefinition(items.$ref, definitions) : items; -} - -export function getStepProperty(property, model, definitions): Property { - if (!property) { return null; } - property = property.$ref ? { ...property, ...getDefinition(property.$ref, definitions) } : property; - return { - name: property.title, - value: model, - type: property.type, - items: getPropertyItemSchema(property.items, definitions), - properties: getStepProperties( - property, - model, - definitions - ), - widget: property.widget instanceof String ? { id: property.widget } : { ...property.widget } - }; -} - - -export function getStepProperties(schema: any, model: any, definitions: any = {}): Property[] { - if (!schema || !schema.properties) { return []; } - return Object - .keys(schema.properties) - .map(property => { - return getStepProperty( - schema.properties[property], - model && model.hasOwnProperty(property) ? model[property] : null, - definitions - ); - }); -} - @Component({ selector: 'wizard-summary', templateUrl: './wizard-summary.component.html', diff --git a/ui/src/app/metadata/domain/model/metadata-resolver.ts b/ui/src/app/metadata/domain/model/metadata-resolver.ts index 01505cff5..e97c08d07 100644 --- a/ui/src/app/metadata/domain/model/metadata-resolver.ts +++ b/ui/src/app/metadata/domain/model/metadata-resolver.ts @@ -25,4 +25,6 @@ export interface MetadataResolver extends MetadataBase { serviceEnabled?: boolean; relyingPartyOverrides?: RelyingPartyOverrides; attributeRelease?: string[]; + + [property: string]: unknown; } diff --git a/ui/src/app/metadata/domain/model/property.ts b/ui/src/app/metadata/domain/model/property.ts index f54829916..a792514d8 100644 --- a/ui/src/app/metadata/domain/model/property.ts +++ b/ui/src/app/metadata/domain/model/property.ts @@ -1,4 +1,5 @@ export interface Property { + title?: string; type: string; name: string; value: string[]; diff --git a/ui/src/app/metadata/domain/model/wizards/metadata-source-base.ts b/ui/src/app/metadata/domain/model/wizards/metadata-source-base.ts index 673f176c1..aab844d5e 100644 --- a/ui/src/app/metadata/domain/model/wizards/metadata-source-base.ts +++ b/ui/src/app/metadata/domain/model/wizards/metadata-source-base.ts @@ -9,6 +9,7 @@ export class MetadataSourceBase implements Wizard { label = 'Metadata Source'; type = '@MetadataProvider'; steps: WizardStep[] = []; + schema = ''; bindings = { '/securityInfo/x509CertificateAvailable': [ diff --git a/ui/src/app/metadata/domain/model/wizards/metadata-source-editor.ts b/ui/src/app/metadata/domain/model/wizards/metadata-source-editor.ts index cbe98d5de..13136ddfc 100644 --- a/ui/src/app/metadata/domain/model/wizards/metadata-source-editor.ts +++ b/ui/src/app/metadata/domain/model/wizards/metadata-source-editor.ts @@ -2,13 +2,13 @@ import { Wizard, WizardStep } from '../../../../wizard/model'; import { MetadataResolver } from '../metadata-resolver'; import { MetadataSourceBase } from './metadata-source-base'; -export class MetadataSourceEditor extends MetadataSourceBase implements Wizard { +export class MetadataSourceEditor extends MetadataSourceBase implements Wizard { + schema = '/api/ui/MetadataSources'; steps: WizardStep[] = [ { index: 1, id: 'common', label: 'label.sp-org-info', - schema: '/api/ui/MetadataSources', fields: [ 'serviceProviderName', 'entityId', @@ -38,7 +38,6 @@ export class MetadataSourceEditor extends MetadataSourceBase implements Wizard< index: 3, id: 'metadata-ui', label: 'label.metadata-ui', - schema: '/api/ui/MetadataSources', fields: [ 'mdui' ] @@ -47,7 +46,6 @@ export class MetadataSourceEditor extends MetadataSourceBase implements Wizard< index: 4, id: 'descriptor-info', label: 'label.descriptor-info', - schema: '/api/ui/MetadataSources', fields: [ 'serviceProviderSsoDescriptor' ] @@ -56,7 +54,6 @@ export class MetadataSourceEditor extends MetadataSourceBase implements Wizard< index: 5, id: 'logout-endpoints', label: 'label.logout-endpoints', - schema: '/api/ui/MetadataSources', fields: [ 'logoutEndpoints' ], @@ -73,7 +70,6 @@ export class MetadataSourceEditor extends MetadataSourceBase implements Wizard< index: 6, id: 'key-info', label: 'label.key-info', - schema: '/api/ui/MetadataSources', fields: [ 'securityInfo' ] @@ -82,7 +78,6 @@ export class MetadataSourceEditor extends MetadataSourceBase implements Wizard< index: 7, id: 'assertion', label: 'label.assertion', - schema: '/api/ui/MetadataSources', fields: [ 'assertionConsumerServices' ], @@ -99,7 +94,6 @@ export class MetadataSourceEditor extends MetadataSourceBase implements Wizard< index: 8, id: 'relying-party', label: 'label.relying-party', - schema: '/api/ui/MetadataSources', fields: [ 'relyingPartyOverrides' ], @@ -116,7 +110,6 @@ export class MetadataSourceEditor extends MetadataSourceBase implements Wizard< index: 9, id: 'attribute', label: 'label.attribute-release', - schema: '/api/ui/MetadataSources', fields: [ 'attributeRelease' ], diff --git a/ui/src/app/metadata/domain/model/wizards/metadata-source-wizard.ts b/ui/src/app/metadata/domain/model/wizards/metadata-source-wizard.ts index bd7ad9fd9..10b7c4e79 100644 --- a/ui/src/app/metadata/domain/model/wizards/metadata-source-wizard.ts +++ b/ui/src/app/metadata/domain/model/wizards/metadata-source-wizard.ts @@ -3,12 +3,12 @@ import { MetadataResolver } from '../metadata-resolver'; import { MetadataSourceBase } from './metadata-source-base'; export class MetadataSourceWizard extends MetadataSourceBase implements Wizard { + schema = '/api/ui/MetadataSources'; steps: WizardStep[] = [ { index: 1, id: 'common', label: 'label.name-and-entity-id', - schema: '/api/ui/MetadataSources', fields: [ 'serviceProviderName', 'entityId' @@ -28,7 +28,6 @@ export class MetadataSourceWizard extends MetadataSourceBase implements Wizard { + describe('getStepProperties function', () => { + it('should return an empty array of schema or schema.properties is not defined', () => { + expect(getStepProperties(null, {})).toEqual([]); + expect(getStepProperties({}, {})).toEqual([]); + }); + + it('should return a formatted list of properties', () => { + expect(getStepProperties(SCHEMA, {}).length).toBe(3); + }); + }); + + describe('getDefinitions method', () => { + it('should retrieve the definitions from the json schema', () => { + const definition = { + id: 'foo', + title: 'bar', + description: 'baz', + type: 'string' + }; + expect(getDefinition('/foo/bar', {bar: definition})).toBe(definition); + }); + }); + + describe('getPropertyItemSchema method', () => { + it('should return null if no items are provided', () => { + expect(getPropertyItemSchema(null, SCHEMA.definitions)).toBeNull(); + }); + it('should retrieve the definitions from the items schema', () => { + expect(getPropertyItemSchema({$ref: 'description'}, SCHEMA.definitions)).toBe(SCHEMA.definitions.description); + }); + it('should return the item itself if no $ref', () => { + let item = {}; + expect(getPropertyItemSchema(item, SCHEMA.definitions)).toBe(item); + }); + }); + + describe('getStepProperty method', () => { + const model = { + name: 'foo', + type: 'bar', + description: 'baz' + }; + it('should return null if no items are provided', () => { + expect(getStepProperty(null, null, SCHEMA.definitions)).toBeNull(); + }); + + it('should retrieve the property $ref definition if available', () => { + const property = getStepProperty( + { $ref: 'description' }, + model, + SCHEMA.definitions + ); + expect(property.type).toBe('string'); + }); + }); +}); + diff --git a/ui/src/app/metadata/domain/utility/configuration.ts b/ui/src/app/metadata/domain/utility/configuration.ts new file mode 100644 index 000000000..dc641c4dd --- /dev/null +++ b/ui/src/app/metadata/domain/utility/configuration.ts @@ -0,0 +1,42 @@ +import { Property } from '../model/property'; + +export function getDefinition(path: string, definitions: any): any { + let def = path.split('/').pop(); + return definitions[def]; +} + +export function getPropertyItemSchema(items: any, definitions: any): any { + if (!items) { return null; } + return items.$ref ? getDefinition(items.$ref, definitions) : items; +} + +export function getStepProperty(property, model, definitions): Property { + if (!property) { return null; } + property = property.$ref ? { ...property, ...getDefinition(property.$ref, definitions) } : property; + return { + name: property.title, + value: model, + type: property.type, + items: getPropertyItemSchema(property.items, definitions), + properties: getStepProperties( + property, + model, + definitions + ), + widget: property.widget instanceof String ? { id: property.widget } : { ...property.widget } + }; +} + + +export function getStepProperties(schema: any, model: any, definitions: any = {}): Property[] { + if (!schema || !schema.properties) { return []; } + return Object + .keys(schema.properties) + .map(property => { + return getStepProperty( + schema.properties[property], + model && model.hasOwnProperty(property) ? model[property] : null, + definitions + ); + }); +} diff --git a/ui/src/app/metadata/manager/container/dashboard-providers-list.component.spec.ts b/ui/src/app/metadata/manager/container/dashboard-providers-list.component.spec.ts index 95e8537f0..1a8c447e3 100644 --- a/ui/src/app/metadata/manager/container/dashboard-providers-list.component.spec.ts +++ b/ui/src/app/metadata/manager/container/dashboard-providers-list.component.spec.ts @@ -15,6 +15,7 @@ import { ProviderItemComponent } from '../component/provider-item.component'; import { FileBackedHttpMetadataResolver } from '../../domain/entity'; import { MockI18nModule } from '../../../../testing/i18n.stub'; import { CustomDatePipe } from '../../../shared/pipe/date.pipe'; +import { Observable, of } from 'rxjs'; describe('Dashboard Providers List Page', () => { @@ -61,6 +62,7 @@ describe('Dashboard Providers List Page', () => { modal = TestBed.get(NgbModal); spyOn(store, 'dispatch').and.callThrough(); + spyOn(store, 'select').and.returnValues(of([]), of({'foo': true})); }); it('should compile', () => { diff --git a/ui/src/app/metadata/manager/container/dashboard-providers-list.component.ts b/ui/src/app/metadata/manager/container/dashboard-providers-list.component.ts index 35ece7de0..de7c3644f 100644 --- a/ui/src/app/metadata/manager/container/dashboard-providers-list.component.ts +++ b/ui/src/app/metadata/manager/container/dashboard-providers-list.component.ts @@ -9,6 +9,7 @@ import { getOpenProviders } from '../reducer'; import { ToggleEntityDisplay } from '../action/manager.action'; import { map } from 'rxjs/operators'; import { ChangeProviderOrderUp, ChangeProviderOrderDown } from '../../provider/action/collection.action'; +import { Metadata } from '../../domain/domain.type'; @Component({ selector: 'dashboard-providers-list', @@ -17,7 +18,7 @@ import { ChangeProviderOrderUp, ChangeProviderOrderDown } from '../../provider/a export class DashboardProvidersListComponent implements OnInit { - providers$: Observable; + providers$: Observable; providersOpen$: Observable<{ [key: string]: boolean }>; constructor( @@ -26,7 +27,7 @@ export class DashboardProvidersListComponent implements OnInit { ) { } ngOnInit(): void { - this.providers$ = this.store.select(getOrderedProviders) as Observable; + this.providers$ = this.store.select(getOrderedProviders); this.providersOpen$ = this.store.select(getOpenProviders); } diff --git a/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.html b/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.html index e2136c4a1..e9aea1c2b 100644 --- a/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.html +++ b/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.html @@ -24,7 +24,7 @@ [isOpen]="(entitiesOpen$ | async)[resolver.getId()]" (select)="edit(resolver)" (toggle)="toggleEntity(resolver)" - (preview)="openPreviewDialog(resolver)" + (preview)="viewConfiguration(resolver)" (delete)="deleteResolver(resolver)" (history)="viewMetadataHistory(resolver)" [allowDelete]="resolver.isDraft()"> diff --git a/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.spec.ts b/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.spec.ts index d4e81b761..41a8bd30e 100644 --- a/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.spec.ts +++ b/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.spec.ts @@ -6,7 +6,6 @@ import { StoreModule, Store, combineReducers } from '@ngrx/store'; import { NgbPaginationModule, NgbModal, NgbModalModule, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import * as fromDashboard from '../reducer'; import { ProviderSearchComponent } from '../component/provider-search.component'; -import { EntityItemComponent } from '../component/entity-item.component'; import { DeleteDialogComponent } from '../component/delete-dialog.component'; import { RouterStub } from '../../../../testing/router.stub'; import { NgbModalStub } from '../../../../testing/modal.stub'; @@ -100,13 +99,6 @@ describe('Dashboard Resolvers List Page', () => { }); }); - describe('openPreviewDialog method', () => { - it('should fire a redux action', () => { - instance.openPreviewDialog(resolver); - expect(store.dispatch).toHaveBeenCalled(); - }); - }); - describe('search method', () => { it('should fire a redux action', () => { instance.search(); diff --git a/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.ts b/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.ts index 540d5b694..4de58be3a 100644 --- a/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.ts +++ b/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.ts @@ -86,8 +86,9 @@ export class DashboardResolversListComponent implements OnInit { this.store.dispatch(new ToggleEntityDisplay(entity.getId())); } - openPreviewDialog(entity: MetadataEntity): void { - this.store.dispatch(new PreviewEntity({ id: entity.getId(), entity })); + viewConfiguration(entity: MetadataEntity): void { + // this.store.dispatch(new PreviewEntity({ id: entity.getId(), entity })); + this.router.navigate(['metadata', 'resolver', entity.getId(), 'configuration', 'options']); } viewMetadataHistory(entity: MetadataEntity): void { diff --git a/ui/src/app/metadata/metadata.module.ts b/ui/src/app/metadata/metadata.module.ts index 80f663b2a..231f39f5f 100644 --- a/ui/src/app/metadata/metadata.module.ts +++ b/ui/src/app/metadata/metadata.module.ts @@ -12,7 +12,7 @@ import { CustomWidgetRegistry } from '../schema-form/registry'; import { WidgetRegistry, SchemaValidatorFactory } from 'ngx-schema-form'; import { CustomSchemaValidatorFactory } from '../schema-form/service/schema-validator'; import { MetadataVersionModule } from './version/version.module'; - +import { MetadataConfigurationModule } from './configuration/configuration.module'; @NgModule({ imports: [ @@ -22,6 +22,7 @@ import { MetadataVersionModule } from './version/version.module'; ManagerModule.forRoot(), ProviderModule.forRoot(), MetadataVersionModule.forRoot(), + MetadataConfigurationModule.forRoot(), MetadataRoutingModule, I18nModule ], diff --git a/ui/src/app/metadata/metadata.routing.ts b/ui/src/app/metadata/metadata.routing.ts index a1d8964f2..35902415c 100644 --- a/ui/src/app/metadata/metadata.routing.ts +++ b/ui/src/app/metadata/metadata.routing.ts @@ -6,6 +6,7 @@ import { ResolverRoutes } from './resolver/resolver.routing'; import { ProviderRoutes } from './provider/provider.routing'; import { ManagerRoutes } from './manager/manager.routing'; import { VersionRoutes } from './version/version.routing'; +import { ConfigurationRoutes } from './configuration/configuration.routing'; const routes: Routes = [ { @@ -15,7 +16,8 @@ const routes: Routes = [ ...ManagerRoutes, ...ResolverRoutes, ...ProviderRoutes, - ...VersionRoutes + ...VersionRoutes, + ...ConfigurationRoutes ], }, ]; diff --git a/ui/src/app/metadata/provider/model/base.provider.form.ts b/ui/src/app/metadata/provider/model/base.provider.form.ts index e341a2fa5..a27a1cbc6 100644 --- a/ui/src/app/metadata/provider/model/base.provider.form.ts +++ b/ui/src/app/metadata/provider/model/base.provider.form.ts @@ -4,6 +4,7 @@ import { BaseMetadataProvider } from '../../domain/model/providers'; export const BaseMetadataProviderEditor: Wizard = { label: 'BaseMetadataProvider', type: 'BaseMetadataResolver', + schema: '', getValidators(namesList: string[]): any { const validators = { '/': (value, property, form_current) => { diff --git a/ui/src/app/metadata/provider/model/dynamic-http.provider.form.ts b/ui/src/app/metadata/provider/model/dynamic-http.provider.form.ts index be0654dec..a0ca22143 100644 --- a/ui/src/app/metadata/provider/model/dynamic-http.provider.form.ts +++ b/ui/src/app/metadata/provider/model/dynamic-http.provider.form.ts @@ -80,13 +80,13 @@ export const DynamicHttpMetadataProviderWizard: Wizard = { ...FileBackedHttpMetadataProviderWizard, + schema: 'assets/schema/provider/filebacked-http.schema.json', steps: [ { id: 'common', label: 'label.common-attributes', index: 1, initialValues: [], - schema: 'assets/schema/provider/filebacked-http-common.editor.schema.json', fields: [ 'enabled', 'xmlId', @@ -120,7 +117,6 @@ export const FileBackedHttpMetadataProviderEditor: Wizard = { ...BaseMetadataProviderEditor, label: 'MetadataProvider', type: 'MetadataProvider', + schema: 'assets/schema/provider/metadata-provider.schema.json', steps: [ { id: 'new', label: 'label.select-metadata-provider-type', index: 1, initialValues: [], - schema: 'assets/schema/provider/metadata-provider.schema.json', fields: [ 'name', '@type' diff --git a/ui/src/app/metadata/resolver/container/resolver-edit.component.html b/ui/src/app/metadata/resolver/container/resolver-edit.component.html index bf04c16ae..88eb1946a 100644 --- a/ui/src/app/metadata/resolver/container/resolver-edit.component.html +++ b/ui/src/app/metadata/resolver/container/resolver-edit.component.html @@ -31,7 +31,8 @@   diff --git a/ui/src/app/wizard/model/wizard.ts b/ui/src/app/wizard/model/wizard.ts index 3115ed041..5729c4f24 100644 --- a/ui/src/app/wizard/model/wizard.ts +++ b/ui/src/app/wizard/model/wizard.ts @@ -2,13 +2,13 @@ import { FormDefinition } from './form-definition'; export interface Wizard extends FormDefinition { steps: WizardStep[]; + schema: string; } export interface WizardStep { id: string; label: string; initialValues?: WizardValue[]; - schema?: string; index: number; locked?: boolean; fields?: string[]; diff --git a/ui/src/app/wizard/reducer/index.spec.ts b/ui/src/app/wizard/reducer/index.spec.ts index dd407ba01..5f72dd71f 100644 --- a/ui/src/app/wizard/reducer/index.spec.ts +++ b/ui/src/app/wizard/reducer/index.spec.ts @@ -5,12 +5,12 @@ describe('wizard index selectors', () => { describe('getSchema method', () => { it('should return the schema by index name', () => { expect( - selectors.getSchemaPath('common', FileBackedHttpMetadataProviderWizard) - ).toBe(FileBackedHttpMetadataProviderWizard.steps[0].schema); + selectors.getSchemaPath(FileBackedHttpMetadataProviderWizard) + ).toBe(FileBackedHttpMetadataProviderWizard.schema); }); it('should return nothing if no schema is found', () => { expect( - selectors.getSchemaPath('common', null) + selectors.getSchemaPath(null) ).toBeFalsy(); }); }); diff --git a/ui/src/app/wizard/reducer/index.ts b/ui/src/app/wizard/reducer/index.ts index 3446f3713..4c3321c62 100644 --- a/ui/src/app/wizard/reducer/index.ts +++ b/ui/src/app/wizard/reducer/index.ts @@ -48,11 +48,7 @@ export const getWizardIsDisabled = createSelector(getState, fromWizard.getDisabl export const getWizardDefinition = createSelector(getState, fromWizard.getDefinition); export const getSchemaCollection = createSelector(getState, fromWizard.getCollection); -export const getSchemaPath = (index: string, wizard: Wizard) => { - if (!wizard) { return null; } - const step = wizard.steps.find(s => s.id === index); - return step ? step.schema : null; -}; +export const getSchemaPath = (wizard: Wizard) => wizard ? wizard.schema : null; export const getSplitSchema = (schema: any, step: WizardStep) => { if (!schema || !step.fields || !step.fields.length || !schema.properties) { @@ -93,7 +89,7 @@ export const getSplitSchema = (schema: any, step: WizardStep) => { return s; }; -export const getCurrentWizardSchema = createSelector(getWizardIndex, getWizardDefinition, getSchemaPath); +export const getCurrentWizardSchema = createSelector(getWizardDefinition, getSchemaPath); export const getPreviousFn = (index: string, wizard: Wizard) => { if (!wizard) { return null; } diff --git a/ui/src/assets/schema/provider/filebacked-http-advanced.schema.json b/ui/src/assets/schema/provider/filebacked-http-advanced.schema.json deleted file mode 100644 index 2e19da247..000000000 --- a/ui/src/assets/schema/provider/filebacked-http-advanced.schema.json +++ /dev/null @@ -1,242 +0,0 @@ -{ - "type": "object", - "title": "", - "properties": { - "httpMetadataResolverAttributes": { - "order": [], - "type": "object", - "fieldsets": [ - { - "title": "label.http-connection-attributes", - "type": "section", - "fields": [ - "connectionRequestTimeout", - "connectionTimeout", - "socketTimeout" - ] - }, - { - "title": "label.http-security-attributes", - "type": "section", - "class": "col-12", - "fields": [ - "disregardTLSCertificate" - ] - }, - { - "title": "label.http-proxy-attributes", - "type": "section", - "class": "col-12", - "fields": [ - "proxyHost", - "proxyPort", - "proxyUser", - "proxyPassword" - ] - }, - { - "title": "label.http-caching-attributes", - "type": "section", - "class": "col-12", - "fields": [ - "httpCaching", - "httpCacheDirectory", - "httpMaxCacheEntries", - "httpMaxCacheEntrySize" - ] - }, - { - "title": "", - "type": "hidden", - "class": "col-12", - "fields": [ - "tlsTrustEngineRef", - "httpClientSecurityParametersRef", - "httpClientRef" - ] - } - ], - "properties": { - "httpClientRef": { - "type": "string", - "title": "", - "description": "", - "placeholder": "", - "widget": "hidden" - }, - "connectionRequestTimeout": { - "type": "string", - "title": "label.connection-request-timeout", - "description": "tooltip.connection-request-timeout", - "placeholder": "label.duration", - "widget": { - "id": "datalist", - "data": [ - "PT0S", - "PT30S", - "PT1M", - "PT10M", - "PT30M", - "PT1H", - "PT4H", - "PT12H", - "PT24H" - ] - }, - "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" - }, - "connectionTimeout": { - "type": "string", - "title": "label.connection-timeout", - "description": "tooltip.connection-timeout", - "placeholder": "label.duration", - "widget": { - "id": "datalist", - "data": [ - "PT0S", - "PT30S", - "PT1M", - "PT10M", - "PT30M", - "PT1H", - "PT4H", - "PT12H", - "PT24H" - ] - }, - "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" - }, - "socketTimeout": { - "type": "string", - "title": "label.socket-timeout", - "description": "tooltip.socket-timeout", - "placeholder": "label.duration", - "widget": { - "id": "datalist", - "data": [ - "PT0S", - "PT30S", - "PT1M", - "PT10M", - "PT30M", - "PT1H", - "PT4H", - "PT12H", - "PT24H" - ] - }, - "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" - }, - "disregardTLSCertificate": { - "type": "boolean", - "title": "label.disregard-tls-cert", - "description": "tooltip.disregard-tls-cert", - "widget": { - "id": "boolean-radio" - }, - "oneOf": [ - { - "enum": [ - true - ], - "description": "True" - }, - { - "enum": [ - false - ], - "description": "False" - } - ] - }, - "tlsTrustEngineRef": { - "type": "string", - "title": "", - "description": "", - "placeholder": "", - "widget": "hidden" - }, - "httpClientSecurityParametersRef": { - "type": "string", - "title": "", - "description": "", - "placeholder": "", - "widget": "hidden" - }, - "proxyHost": { - "type": "string", - "title": "label.proxy-host", - "description": "tooltip.proxy-host", - "placeholder": "" - }, - "proxyPort": { - "type": "string", - "title": "label.proxy-port", - "description": "tooltip.proxy-port", - "placeholder": "" - }, - "proxyUser": { - "type": "string", - "title": "label.proxy-user", - "description": "tooltip.proxy-user", - "placeholder": "" - }, - "proxyPassword": { - "type": "string", - "title": "label.proxy-password", - "description": "tooltip.proxy-password", - "placeholder": "" - }, - "httpCaching": { - "type": "string", - "title": "label.http-caching", - "description": "tooltip.http-caching", - "placeholder": "label.select-caching-type", - "widget": { - "id": "select" - }, - "oneOf": [ - { - "enum": [ - "none" - ], - "description": "value.none" - }, - { - "enum": [ - "file" - ], - "description": "value.file" - }, - { - "enum": [ - "memory" - ], - "description": "value.memory" - } - ] - }, - "httpCacheDirectory": { - "type": "string", - "title": "label.http-caching-directory", - "description": "tooltip.http-caching-directory", - "placeholder": "" - }, - "httpMaxCacheEntries": { - "type": "integer", - "title": "label.http-max-cache-entries", - "description": "tooltip.http-max-cache-entries", - "placeholder": "", - "minimum": 0 - }, - "httpMaxCacheEntrySize": { - "type": "integer", - "title": "label.max-cache-entry-size", - "description": "tooltip.max-cache-entry-size", - "placeholder": "", - "minimum": 0 - } - } - } - } -} diff --git a/ui/src/assets/schema/provider/filebacked-http-common.editor.schema.json b/ui/src/assets/schema/provider/filebacked-http-common.editor.schema.json deleted file mode 100644 index 9e1d37308..000000000 --- a/ui/src/assets/schema/provider/filebacked-http-common.editor.schema.json +++ /dev/null @@ -1,259 +0,0 @@ -{ - "type": "object", - "order": [ - "name", - "@type", - "xmlId", - "metadataURL", - "initializeFromBackupFile", - "backingFile", - "backupFileInitNextRefreshDelay", - "requireValidMetadata", - "failFastInitialization", - "useDefaultPredicateRegistry", - "satisfyAnyPredicates" - ], - "required": [ - "name", - "xmlId", - "metadataURL", - "backingFile", - "backupFileInitNextRefreshDelay" - ], - "anyOf": [ - { - "properties": { - "initializeFromBackupFile": { - "enum": [ - true - ] - } - } - }, - { - "properties": { - "initializeFromBackupFile": { - "enum": [ - false - ] - } - } - } - ], - "fieldsets": [ - { - "type": "section", - "fields": [ - "name", - "@type", - "enabled" - ] - }, - { - "type": "group-lg", - "fields": [ - "xmlId", - "metadataURL", - "initializeFromBackupFile", - "backingFile", - "backupFileInitNextRefreshDelay", - "requireValidMetadata", - "failFastInitialization", - "useDefaultPredicateRegistry", - "satisfyAnyPredicates" - ] - } - ], - "properties": { - "name": { - "title": "label.metadata-provider-name", - "description": "tooltip.metadata-provider-name", - "type": "string", - "widget": { - "id": "string", - "help": "message.must-be-unique" - } - }, - "@type": { - "title": "label.metadata-provider-type", - "description": "tooltip.metadata-provider-type", - "placeholder": "label.select-metadata-type", - "type": "string", - "readOnly": true, - "widget": { - "id": "select", - "disabled": true - }, - "oneOf": [ - { - "enum": [ - "FileBackedHttpMetadataResolver" - ], - "description": "value.file-backed-http-metadata-provider" - } - ] - }, - "enabled": { - "title": "label.enable-service", - "description": "tooltip.enable-service", - "type": "boolean", - "default": false - }, - "xmlId": { - "title": "label.xml-id", - "description": "tooltip.xml-id", - "type": "string", - "default": "", - "minLength": 1 - }, - "metadataURL": { - "title": "label.metadata-url", - "description": "tooltip.metadata-url", - "type": "string", - "default": "", - "minLength": 1 - }, - "initializeFromBackupFile": { - "title": "label.init-from-backup", - "description": "tooltip.init-from-backup", - "type": "boolean", - "widget": { - "id": "boolean-radio" - }, - "oneOf": [ - { - "enum": [ - true - ], - "description": "value.true" - }, - { - "enum": [ - false - ], - "description": "value.false" - } - ], - "default": true - }, - "backingFile": { - "title": "label.backing-file", - "description": "tooltip.backing-file", - "type": "string", - "default": "" - }, - "backupFileInitNextRefreshDelay": { - "title": "label.backup-file-init-refresh-delay", - "description": "tooltip.backup-file-init-refresh-delay", - "type": "string", - "widget": { - "id": "datalist", - "data": [ - "PT0S", - "PT30S", - "PT1M", - "PT10M", - "PT30M", - "PT1H", - "PT4H", - "PT12H", - "PT24H" - ] - }, - "default": null, - "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" - }, - "requireValidMetadata": { - "title": "label.require-valid-metadata", - "description": "tooltip.require-valid-metadata", - "type": "boolean", - "widget": { - "id": "boolean-radio" - }, - "oneOf": [ - { - "enum": [ - true - ], - "description": "value.true" - }, - { - "enum": [ - false - ], - "description": "value.false" - } - ], - "default": true - }, - "failFastInitialization": { - "title": "label.fail-fast-init", - "description": "tooltip.fail-fast-init", - "type": "boolean", - "widget": { - "id": "boolean-radio" - }, - "oneOf": [ - { - "enum": [ - true - ], - "description": "value.true" - }, - { - "enum": [ - false - ], - "description": "value.false" - } - ], - "default": true - }, - "useDefaultPredicateRegistry": { - "title": "label.use-default-predicate-reg", - "description": "tooltip.use-default-predicate-reg", - "type": "boolean", - "widget": { - "id": "boolean-radio" - }, - "oneOf": [ - { - "enum": [ - true - ], - "description": "value.true" - }, - { - "enum": [ - false - ], - "description": "value.false" - } - ], - "default": true - }, - "satisfyAnyPredicates": { - "title": "label.satisfy-any-predicates", - "description": "tooltip.satisfy-any-predicates", - "type": "boolean", - "widget": { - "id": "boolean-radio" - }, - "oneOf": [ - { - "enum": [ - true - ], - "description": "value.true" - }, - { - "enum": [ - false - ], - "description": "value.false" - } - ], - "default": false - } - } -} \ No newline at end of file diff --git a/ui/src/assets/schema/provider/filebacked-http-common.schema.json b/ui/src/assets/schema/provider/filebacked-http-common.schema.json deleted file mode 100644 index 5db366d07..000000000 --- a/ui/src/assets/schema/provider/filebacked-http-common.schema.json +++ /dev/null @@ -1,173 +0,0 @@ -{ - "type": "object", - "order": [ - "xmlId", - "metadataURL", - "initializeFromBackupFile", - "backingFile", - "backupFileInitNextRefreshDelay", - "requireValidMetadata", - "failFastInitialization", - "useDefaultPredicateRegistry", - "satisfyAnyPredicates" - ], - "required": ["xmlId", "metadataURL", "backingFile", "backupFileInitNextRefreshDelay"], - "properties": { - "xmlId": { - "title": "label.xml-id", - "description": "tooltip.xml-id", - "type": "string", - "default": "", - "minLength": 1 - }, - "metadataURL": { - "title": "label.metadata-url", - "description": "tooltip.metadata-url", - "type": "string", - "default": "", - "minLength": 1 - }, - "initializeFromBackupFile": { - "title": "label.init-from-backup", - "description": "tooltip.init-from-backup", - "type": "boolean", - "widget": { - "id": "boolean-radio" - }, - "oneOf": [ - { - "enum": [ - true - ], - "description": "value.true" - }, - { - "enum": [ - false - ], - "description": "value.false" - } - ], - "default": true - }, - "backingFile": { - "title": "label.backing-file", - "description": "tooltip.backing-file", - "type": "string", - "default": "" - }, - "backupFileInitNextRefreshDelay": { - "title": "label.backup-file-init-refresh-delay", - "description": "tooltip.backup-file-init-refresh-delay", - "type": "string", - "widget": { - "id": "datalist", - "data": [ - "PT0S", - "PT30S", - "PT1M", - "PT10M", - "PT30M", - "PT1H", - "PT4H", - "PT12H", - "PT24H" - ] - }, - "default": null, - "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" - }, - "requireValidMetadata": { - "title": "label.require-valid-metadata", - "description": "tooltip.require-valid-metadata", - "type": "boolean", - "widget": { - "id": "boolean-radio" - }, - "oneOf": [ - { - "enum": [ - true - ], - "description": "value.true" - }, - { - "enum": [ - false - ], - "description": "value.false" - } - ], - "default": true - }, - "failFastInitialization": { - "title": "label.fail-fast-init", - "description": "tooltip.fail-fast-init", - "type": "boolean", - "widget": { - "id": "boolean-radio" - }, - "oneOf": [ - { - "enum": [ - true - ], - "description": "value.true" - }, - { - "enum": [ - false - ], - "description": "value.false" - } - ], - "default": true - }, - "useDefaultPredicateRegistry": { - "title": "label.use-default-predicate-reg", - "description": "tooltip.use-default-predicate-reg", - "type": "boolean", - "widget": { - "id": "boolean-radio" - }, - "oneOf": [ - { - "enum": [ - true - ], - "description": "value.true" - }, - { - "enum": [ - false - ], - "description": "value.false" - } - ], - "default": true - }, - "satisfyAnyPredicates": { - "title": "label.satisfy-any-predicates", - "description": "tooltip.satisfy-any-predicates", - "type": "boolean", - "widget": { - "id": "boolean-radio" - }, - "oneOf": [ - { - "enum": [ - true - ], - "description": "value.true" - }, - { - "enum": [ - false - ], - "description": "value.false" - } - ], - "default": false - } - } -} diff --git a/ui/src/assets/schema/provider/filebacked-http-filters.schema.json b/ui/src/assets/schema/provider/filebacked-http-filters.schema.json deleted file mode 100644 index 820063bfa..000000000 --- a/ui/src/assets/schema/provider/filebacked-http-filters.schema.json +++ /dev/null @@ -1,129 +0,0 @@ -{ - "type": "object", - "properties": { - "metadataFilters": { - "title": "", - "description": "", - "type": "object", - "properties": { - "RequiredValidUntil": { - "title": "label.required-valid-until", - "type": "object", - "widget": { - "id": "fieldset" - }, - "properties": { - "maxValidityInterval": { - "title": "label.max-validity-interval", - "description": "tooltip.max-validity-interval", - "type": "string", - "placeholder": "label.duration", - "widget": { - "id": "datalist", - "data": [ - "PT0S", - "P14D", - "P7D", - "P1D", - "PT12H" - ] - }, - "default": null, - "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" - } - } - }, - "SignatureValidation": { - "title": "label.signature-validation-filter", - "type": "object", - "widget": { - "id": "fieldset" - }, - "properties": { - "requireSignedRoot": { - "title": "label.require-signed-root", - "description": "tooltip.require-signed-root", - "type": "boolean", - "default": true - }, - "certificateFile": { - "title": "label.certificate-file", - "description": "tooltip.certificate-file", - "type": "string", - "widget": "textline" - } - }, - "anyOf": [ - { - "properties": { - "requireSignedRoot": { - "enum": [ true ] - }, - "certificateFile": { - "minLength": 1, - "type": "string" - } - }, - "required": [ - "certificateFile" - ] - }, - { - "properties": { - "requireSignedRoot": { - "enum": [ false ] - } - } - } - ] - }, - "EntityRoleWhiteList": { - "title": "label.entity-role-whitelist", - "type": "object", - "widget": { - "id": "fieldset" - }, - "properties": { - "retainedRoles": { - "title": "label.retained-roles", - "description": "tooltip.retained-roles", - "type": "array", - "items": { - "widget": { - "id": "select" - }, - "type": "string", - "oneOf": [ - { - "enum": [ - "md:SPSSODescriptor" - ], - "description": "value.spdescriptor" - }, - { - "enum": [ - "md:AttributeAuthorityDescriptor" - ], - "description": "value.attr-auth-descriptor" - } - ] - } - }, - "removeRolelessEntityDescriptors": { - "title": "label.remove-roleless-entity-descriptors", - "description": "tooltip.remove-roleless-entity-descriptors", - "type": "boolean", - "default": true - }, - "removeEmptyEntitiesDescriptors": { - "title": "label.remove-empty-entities-descriptors", - "description": "tooltip.remove-empty-entities-descriptors", - "type": "boolean", - "default": true - } - } - } - } - } - } -} diff --git a/ui/src/assets/schema/provider/filebacked-http-reloading.schema.json b/ui/src/assets/schema/provider/filebacked-http-reloading.schema.json deleted file mode 100644 index 6723e4cb1..000000000 --- a/ui/src/assets/schema/provider/filebacked-http-reloading.schema.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "type": "object", - "properties": { - "reloadableMetadataResolverAttributes": { - "type": "object", - "properties": { - "minRefreshDelay": { - "title": "label.min-refresh-delay", - "description": "tooltip.min-refresh-delay", - "type": "string", - "placeholder": "label.duration", - "widget": { - "id": "datalist", - "data": [ - "PT0S", - "PT30S", - "PT1M", - "PT10M", - "PT30M", - "PT1H", - "PT4H", - "PT12H", - "PT24H" - ] - }, - "default": null, - "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" - }, - "maxRefreshDelay": { - "title": "label.max-refresh-delay", - "description": "tooltip.max-refresh-delay", - "type": "string", - "placeholder": "label.duration", - "widget": { - "id": "datalist", - "data": [ - "PT0S", - "PT30S", - "PT1M", - "PT10M", - "PT30M", - "PT1H", - "PT4H", - "PT12H", - "PT24H" - ] - }, - "default": null, - "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" - }, - "refreshDelayFactor": { - "title": "label.refresh-delay-factor", - "description": "tooltip.refresh-delay-factor", - "type": "string", - "widget": { - "id": "string", - "help": "message.real-number" - }, - "placeholder": "label.real-number", - "minimum": 0, - "maximum": 1, - "default": "", - "pattern": "^(?:([0]*(\\.[0-9]+)?|[0]*\\.[0-9]*[1-9][0-9]*)|)$" - } - } - } - } -} diff --git a/ui/src/assets/schema/provider/filebacked-http.schema.json b/ui/src/assets/schema/provider/filebacked-http.schema.json new file mode 100644 index 000000000..f543e9e89 --- /dev/null +++ b/ui/src/assets/schema/provider/filebacked-http.schema.json @@ -0,0 +1,686 @@ +{ + "type": "object", + "order": [ + "name", + "@type", + "xmlId", + "metadataURL", + "initializeFromBackupFile", + "backingFile", + "backupFileInitNextRefreshDelay", + "requireValidMetadata", + "failFastInitialization", + "useDefaultPredicateRegistry", + "satisfyAnyPredicates" + ], + "required": [ + "name", + "xmlId", + "metadataURL", + "backingFile", + "backupFileInitNextRefreshDelay" + ], + "anyOf": [ + { + "properties": { + "initializeFromBackupFile": { + "enum": [ + true + ] + } + } + }, + { + "properties": { + "initializeFromBackupFile": { + "enum": [ + false + ] + } + } + } + ], + "fieldsets": [ + { + "type": "section", + "fields": [ + "name", + "@type", + "enabled" + ] + }, + { + "type": "group-lg", + "fields": [ + "xmlId", + "metadataURL", + "initializeFromBackupFile", + "backingFile", + "backupFileInitNextRefreshDelay", + "requireValidMetadata", + "failFastInitialization", + "useDefaultPredicateRegistry", + "satisfyAnyPredicates" + ] + } + ], + "properties": { + "name": { + "title": "label.metadata-provider-name", + "description": "tooltip.metadata-provider-name", + "type": "string", + "widget": { + "id": "string", + "help": "message.must-be-unique" + } + }, + "@type": { + "title": "label.metadata-provider-type", + "description": "tooltip.metadata-provider-type", + "placeholder": "label.select-metadata-type", + "type": "string", + "readOnly": true, + "widget": { + "id": "select", + "disabled": true + }, + "oneOf": [ + { + "enum": [ + "FileBackedHttpMetadataResolver" + ], + "description": "value.file-backed-http-metadata-provider" + } + ] + }, + "enabled": { + "title": "label.enable-service", + "description": "tooltip.enable-service", + "type": "boolean", + "default": false + }, + "xmlId": { + "title": "label.xml-id", + "description": "tooltip.xml-id", + "type": "string", + "default": "", + "minLength": 1 + }, + "metadataURL": { + "title": "label.metadata-url", + "description": "tooltip.metadata-url", + "type": "string", + "default": "", + "minLength": 1 + }, + "initializeFromBackupFile": { + "title": "label.init-from-backup", + "description": "tooltip.init-from-backup", + "type": "boolean", + "widget": { + "id": "boolean-radio" + }, + "oneOf": [ + { + "enum": [ + true + ], + "description": "value.true" + }, + { + "enum": [ + false + ], + "description": "value.false" + } + ], + "default": true + }, + "backingFile": { + "title": "label.backing-file", + "description": "tooltip.backing-file", + "type": "string", + "default": "" + }, + "backupFileInitNextRefreshDelay": { + "title": "label.backup-file-init-refresh-delay", + "description": "tooltip.backup-file-init-refresh-delay", + "type": "string", + "widget": { + "id": "datalist", + "data": [ + "PT0S", + "PT30S", + "PT1M", + "PT10M", + "PT30M", + "PT1H", + "PT4H", + "PT12H", + "PT24H" + ] + }, + "default": null, + "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" + }, + "requireValidMetadata": { + "title": "label.require-valid-metadata", + "description": "tooltip.require-valid-metadata", + "type": "boolean", + "widget": { + "id": "boolean-radio" + }, + "oneOf": [ + { + "enum": [ + true + ], + "description": "value.true" + }, + { + "enum": [ + false + ], + "description": "value.false" + } + ], + "default": true + }, + "failFastInitialization": { + "title": "label.fail-fast-init", + "description": "tooltip.fail-fast-init", + "type": "boolean", + "widget": { + "id": "boolean-radio" + }, + "oneOf": [ + { + "enum": [ + true + ], + "description": "value.true" + }, + { + "enum": [ + false + ], + "description": "value.false" + } + ], + "default": true + }, + "useDefaultPredicateRegistry": { + "title": "label.use-default-predicate-reg", + "description": "tooltip.use-default-predicate-reg", + "type": "boolean", + "widget": { + "id": "boolean-radio" + }, + "oneOf": [ + { + "enum": [ + true + ], + "description": "value.true" + }, + { + "enum": [ + false + ], + "description": "value.false" + } + ], + "default": true + }, + "satisfyAnyPredicates": { + "title": "label.satisfy-any-predicates", + "description": "tooltip.satisfy-any-predicates", + "type": "boolean", + "widget": { + "id": "boolean-radio" + }, + "oneOf": [ + { + "enum": [ + true + ], + "description": "value.true" + }, + { + "enum": [ + false + ], + "description": "value.false" + } + ], + "default": false + }, + "httpMetadataResolverAttributes": { + "order": [], + "type": "object", + "fieldsets": [ + { + "title": "label.http-connection-attributes", + "type": "section", + "fields": [ + "connectionRequestTimeout", + "connectionTimeout", + "socketTimeout" + ] + }, + { + "title": "label.http-security-attributes", + "type": "section", + "class": "col-12", + "fields": [ + "disregardTLSCertificate" + ] + }, + { + "title": "label.http-proxy-attributes", + "type": "section", + "class": "col-12", + "fields": [ + "proxyHost", + "proxyPort", + "proxyUser", + "proxyPassword" + ] + }, + { + "title": "label.http-caching-attributes", + "type": "section", + "class": "col-12", + "fields": [ + "httpCaching", + "httpCacheDirectory", + "httpMaxCacheEntries", + "httpMaxCacheEntrySize" + ] + }, + { + "title": "", + "type": "hidden", + "class": "col-12", + "fields": [ + "tlsTrustEngineRef", + "httpClientSecurityParametersRef", + "httpClientRef" + ] + } + ], + "properties": { + "httpClientRef": { + "type": "string", + "title": "", + "description": "", + "placeholder": "", + "widget": "hidden" + }, + "connectionRequestTimeout": { + "type": "string", + "title": "label.connection-request-timeout", + "description": "tooltip.connection-request-timeout", + "placeholder": "label.duration", + "widget": { + "id": "datalist", + "data": [ + "PT0S", + "PT30S", + "PT1M", + "PT10M", + "PT30M", + "PT1H", + "PT4H", + "PT12H", + "PT24H" + ] + }, + "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" + }, + "connectionTimeout": { + "type": "string", + "title": "label.connection-timeout", + "description": "tooltip.connection-timeout", + "placeholder": "label.duration", + "widget": { + "id": "datalist", + "data": [ + "PT0S", + "PT30S", + "PT1M", + "PT10M", + "PT30M", + "PT1H", + "PT4H", + "PT12H", + "PT24H" + ] + }, + "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" + }, + "socketTimeout": { + "type": "string", + "title": "label.socket-timeout", + "description": "tooltip.socket-timeout", + "placeholder": "label.duration", + "widget": { + "id": "datalist", + "data": [ + "PT0S", + "PT30S", + "PT1M", + "PT10M", + "PT30M", + "PT1H", + "PT4H", + "PT12H", + "PT24H" + ] + }, + "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" + }, + "disregardTLSCertificate": { + "type": "boolean", + "title": "label.disregard-tls-cert", + "description": "tooltip.disregard-tls-cert", + "widget": { + "id": "boolean-radio" + }, + "oneOf": [ + { + "enum": [ + true + ], + "description": "True" + }, + { + "enum": [ + false + ], + "description": "False" + } + ] + }, + "tlsTrustEngineRef": { + "type": "string", + "title": "", + "description": "", + "placeholder": "", + "widget": "hidden" + }, + "httpClientSecurityParametersRef": { + "type": "string", + "title": "", + "description": "", + "placeholder": "", + "widget": "hidden" + }, + "proxyHost": { + "type": "string", + "title": "label.proxy-host", + "description": "tooltip.proxy-host", + "placeholder": "" + }, + "proxyPort": { + "type": "string", + "title": "label.proxy-port", + "description": "tooltip.proxy-port", + "placeholder": "" + }, + "proxyUser": { + "type": "string", + "title": "label.proxy-user", + "description": "tooltip.proxy-user", + "placeholder": "" + }, + "proxyPassword": { + "type": "string", + "title": "label.proxy-password", + "description": "tooltip.proxy-password", + "placeholder": "" + }, + "httpCaching": { + "type": "string", + "title": "label.http-caching", + "description": "tooltip.http-caching", + "placeholder": "label.select-caching-type", + "widget": { + "id": "select" + }, + "oneOf": [ + { + "enum": [ + "none" + ], + "description": "value.none" + }, + { + "enum": [ + "file" + ], + "description": "value.file" + }, + { + "enum": [ + "memory" + ], + "description": "value.memory" + } + ] + }, + "httpCacheDirectory": { + "type": "string", + "title": "label.http-caching-directory", + "description": "tooltip.http-caching-directory", + "placeholder": "" + }, + "httpMaxCacheEntries": { + "type": "integer", + "title": "label.http-max-cache-entries", + "description": "tooltip.http-max-cache-entries", + "placeholder": "", + "minimum": 0 + }, + "httpMaxCacheEntrySize": { + "type": "integer", + "title": "label.max-cache-entry-size", + "description": "tooltip.max-cache-entry-size", + "placeholder": "", + "minimum": 0 + } + } + }, + "reloadableMetadataResolverAttributes": { + "type": "object", + "properties": { + "minRefreshDelay": { + "title": "label.min-refresh-delay", + "description": "tooltip.min-refresh-delay", + "type": "string", + "placeholder": "label.duration", + "widget": { + "id": "datalist", + "data": [ + "PT0S", + "PT30S", + "PT1M", + "PT10M", + "PT30M", + "PT1H", + "PT4H", + "PT12H", + "PT24H" + ] + }, + "default": null, + "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" + }, + "maxRefreshDelay": { + "title": "label.max-refresh-delay", + "description": "tooltip.max-refresh-delay", + "type": "string", + "placeholder": "label.duration", + "widget": { + "id": "datalist", + "data": [ + "PT0S", + "PT30S", + "PT1M", + "PT10M", + "PT30M", + "PT1H", + "PT4H", + "PT12H", + "PT24H" + ] + }, + "default": null, + "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" + }, + "refreshDelayFactor": { + "title": "label.refresh-delay-factor", + "description": "tooltip.refresh-delay-factor", + "type": "string", + "widget": { + "id": "string", + "help": "message.real-number" + }, + "placeholder": "label.real-number", + "minimum": 0, + "maximum": 1, + "default": "", + "pattern": "^(?:([0]*(\\.[0-9]+)?|[0]*\\.[0-9]*[1-9][0-9]*)|)$" + } + } + }, + "metadataFilters": { + "title": "", + "description": "", + "type": "object", + "properties": { + "RequiredValidUntil": { + "title": "label.required-valid-until", + "type": "object", + "widget": { + "id": "fieldset" + }, + "properties": { + "maxValidityInterval": { + "title": "label.max-validity-interval", + "description": "tooltip.max-validity-interval", + "type": "string", + "placeholder": "label.duration", + "widget": { + "id": "datalist", + "data": [ + "PT0S", + "P14D", + "P7D", + "P1D", + "PT12H" + ] + }, + "default": null, + "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" + } + } + }, + "SignatureValidation": { + "title": "label.signature-validation-filter", + "type": "object", + "widget": { + "id": "fieldset" + }, + "properties": { + "requireSignedRoot": { + "title": "label.require-signed-root", + "description": "tooltip.require-signed-root", + "type": "boolean", + "default": true + }, + "certificateFile": { + "title": "label.certificate-file", + "description": "tooltip.certificate-file", + "type": "string", + "widget": "textline" + } + }, + "anyOf": [ + { + "properties": { + "requireSignedRoot": { + "enum": [ + true + ] + }, + "certificateFile": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "certificateFile" + ] + }, + { + "properties": { + "requireSignedRoot": { + "enum": [ + false + ] + } + } + } + ] + }, + "EntityRoleWhiteList": { + "title": "label.entity-role-whitelist", + "type": "object", + "widget": { + "id": "fieldset" + }, + "properties": { + "retainedRoles": { + "title": "label.retained-roles", + "description": "tooltip.retained-roles", + "type": "array", + "items": { + "widget": { + "id": "select" + }, + "type": "string", + "oneOf": [ + { + "enum": [ + "md:SPSSODescriptor" + ], + "description": "value.spdescriptor" + }, + { + "enum": [ + "md:AttributeAuthorityDescriptor" + ], + "description": "value.attr-auth-descriptor" + } + ] + } + }, + "removeRolelessEntityDescriptors": { + "title": "label.remove-roleless-entity-descriptors", + "description": "tooltip.remove-roleless-entity-descriptors", + "type": "boolean", + "default": true + }, + "removeEmptyEntitiesDescriptors": { + "title": "label.remove-empty-entities-descriptors", + "description": "tooltip.remove-empty-entities-descriptors", + "type": "boolean", + "default": true + } + } + } + } + } + } +} \ No newline at end of file diff --git a/ui/src/styles.scss b/ui/src/styles.scss index 454ccba2e..a3f814d8a 100644 --- a/ui/src/styles.scss +++ b/ui/src/styles.scss @@ -6,6 +6,7 @@ @import './theme/alert'; @import './theme/typography'; @import './theme/list'; +@import './theme/utility'; body { background-color: theme-color("light"); diff --git a/ui/src/testing/form-schema.stub.ts b/ui/src/testing/form-schema.stub.ts index c91f63bf1..1ef7d42ae 100644 --- a/ui/src/testing/form-schema.stub.ts +++ b/ui/src/testing/form-schema.stub.ts @@ -30,6 +30,31 @@ export const SCHEMA = { 'description': 'FileBackedHttpMetadataProvider' } ] + }, + 'list': { + 'title': 'label.retained-roles', + 'description': 'tooltip.retained-roles', + 'type': 'array', + 'items': { + 'widget': { + 'id': 'select' + }, + 'type': 'string', + 'oneOf': [ + { + 'enum': [ + 'SPSSODescriptor' + ], + 'description': 'value.spdescriptor' + }, + { + 'enum': [ + 'AttributeAuthorityDescriptor' + ], + 'description': 'value.attr-auth-descriptor' + } + ] + } } }, 'required': [ @@ -44,5 +69,13 @@ export const SCHEMA = { '@type' ] } - ] + ], + 'definitions': { + 'description': { + 'title': 'Description', + 'description': 'A description of the object', + 'type': 'string', + 'widget': 'string' + } + } }; diff --git a/ui/src/theme/utility.scss b/ui/src/theme/utility.scss new file mode 100644 index 000000000..f6c3c0e9e --- /dev/null +++ b/ui/src/theme/utility.scss @@ -0,0 +1,7 @@ +.border-2 { + border-width: 2px !important; +} + +.bg-lighter { + background: #FAFAFA !important; +} \ No newline at end of file