From 9d2b7c1d9f3fed1333b1269d9b714502cf8b01af Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Thu, 12 Sep 2019 12:50:42 -0700 Subject: [PATCH] SHIBUI-1408 Added toggle for displaying only changes in comparison --- .../metadata-configuration.component.html | 34 +++-- .../property/array-property.component.html | 26 +--- .../property/array-property.component.spec.ts | 39 +---- .../property/array-property.component.ts | 16 +-- .../metadata-comparison.component.ts | 8 +- .../metadata/configuration/model/section.ts | 4 + .../configuration/reducer/index.spec.ts | 136 +++++++++++++++++- .../metadata/configuration/reducer/index.ts | 82 +++++++++-- ui/src/testing/mockMetadataWizard.ts | 78 ++++++++++ ui/src/testing/sample-fbhttp-provider.json | 94 ++++++++++++ 10 files changed, 414 insertions(+), 103 deletions(-) create mode 100644 ui/src/testing/mockMetadataWizard.ts create mode 100644 ui/src/testing/sample-fbhttp-provider.json diff --git a/ui/src/app/metadata/configuration/component/metadata-configuration.component.html b/ui/src/app/metadata/configuration/component/metadata-configuration.component.html index f4826cfe6..e0685e743 100644 --- a/ui/src/app/metadata/configuration/component/metadata-configuration.component.html +++ b/ui/src/app/metadata/configuration/component/metadata-configuration.component.html @@ -18,20 +18,26 @@

-
- Option - - Value - {{ date | date:DATE_FORMAT }} - -
- - + +
+ Option + + Value + {{ date | date:DATE_FORMAT }} + +
+ + +
+ +
+ No Changes +
+
diff --git a/ui/src/app/metadata/configuration/component/property/array-property.component.html b/ui/src/app/metadata/configuration/component/property/array-property.component.html index 380e0105b..ed7591702 100644 --- a/ui/src/app/metadata/configuration/component/property/array-property.component.html +++ b/ui/src/app/metadata/configuration/component/property/array-property.component.html @@ -20,20 +20,6 @@ - -
- {{ version[i][prop] }} -
-
- - -
-
@@ -47,18 +33,18 @@
-
-
+
- {{ attr.label }} + [ngClass]="{'bg-diff': property.differences && item.differences}"> + {{ item.label }}
- + true - + false
diff --git a/ui/src/app/metadata/configuration/component/property/array-property.component.spec.ts b/ui/src/app/metadata/configuration/component/property/array-property.component.spec.ts index 6260a4e50..363b3163a 100644 --- a/ui/src/app/metadata/configuration/component/property/array-property.component.spec.ts +++ b/ui/src/app/metadata/configuration/component/property/array-property.component.spec.ts @@ -67,22 +67,9 @@ describe('Array Property Component', () => { expect(app).toBeTruthy(); })); - describe('isDifferent method', () => { - it('should return true if the value is different between any of the lists', () => { - expect(app.isDifferent('foo', [['foo', 'bar', 'baz'], ['bar', 'baz']])).toBe(true); - expect(app.isDifferent('bar', [['bar'], null])).toBe(true); - }); - - it('should return false if the list of values is the same', () => { - expect(app.isDifferent('foo', [['foo', 'baz'], ['foo', 'bar']])).toBe(false); - }); - }); - describe('attributeList$ getter', () => { it('should return an empty list when no data or dataUrl is set', () => { - app.attributeList$.subscribe((list) => { - expect(list).toEqual([]); - }); + expect(app.dataList).toBeUndefined(); }); it('should return a list of data items from the schema', () => { const datalist = [ @@ -98,29 +85,7 @@ describe('Array Property Component', () => { } }); 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); - }); + expect(app.dataList).toEqual(datalist); }); }); }); diff --git a/ui/src/app/metadata/configuration/component/property/array-property.component.ts b/ui/src/app/metadata/configuration/component/property/array-property.component.ts index 7a042fa46..e3e995c2b 100644 --- a/ui/src/app/metadata/configuration/component/property/array-property.component.ts +++ b/ui/src/app/metadata/configuration/component/property/array-property.component.ts @@ -33,20 +33,8 @@ export class ArrayPropertyComponent extends ConfigurationPropertyComponent imple return UriValidator.isUri(str); } - isDifferent(key: string, model: any): boolean { - return model - .map((value) => value ? value.indexOf(key) > -1 : false) - .reduce((current, val) => current !== val ? true : false, false); - } - - 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([]); + get dataList(): { key: string, label: string }[] { + return this.property.widget.data; } } diff --git a/ui/src/app/metadata/configuration/container/metadata-comparison.component.ts b/ui/src/app/metadata/configuration/container/metadata-comparison.component.ts index 18ba345bb..06f821633 100644 --- a/ui/src/app/metadata/configuration/container/metadata-comparison.component.ts +++ b/ui/src/app/metadata/configuration/container/metadata-comparison.component.ts @@ -1,9 +1,9 @@ import { Component, ChangeDetectionStrategy, OnDestroy } from '@angular/core'; -import { Observable, BehaviorSubject, Subscription } from 'rxjs'; +import { Observable, BehaviorSubject, Subscription, combineLatest } from 'rxjs'; import { Store } from '@ngrx/store'; import { ActivatedRoute } from '@angular/router'; import { map, withLatestFrom } from 'rxjs/operators'; -import { ConfigurationState, getComparisonConfigurations, getComparisonConfigurationCount } from '../reducer'; +import { ConfigurationState, getComparisonConfigurationCount } from '../reducer'; import { CompareVersionRequest, ClearVersions, ViewChanged } from '../action/compare.action'; import { MetadataConfiguration } from '../model/metadata-configuration'; import * as fromReducer from '../reducer'; @@ -35,10 +35,12 @@ export class MetadataComparisonComponent implements OnDestroy { map(versions => new CompareVersionRequest(versions)) ).subscribe(this.store); - this.versions$ = this.store.select(getComparisonConfigurations); + this.versions$ = this.store.select(fromReducer.getLimitedComparisonConfigurations); this.numVersions$ = this.store.select(getComparisonConfigurationCount); this.type$ = this.store.select(fromReducer.getConfigurationModelType); + this.versions$.subscribe(console.log); + this.sub = this.limiter.pipe( withLatestFrom(this.limited$), map(([compare, limit]) => new ViewChanged(!limit)) diff --git a/ui/src/app/metadata/configuration/model/section.ts b/ui/src/app/metadata/configuration/model/section.ts index fd7f9b00f..50c6a5492 100644 --- a/ui/src/app/metadata/configuration/model/section.ts +++ b/ui/src/app/metadata/configuration/model/section.ts @@ -4,14 +4,18 @@ export interface Section { label: string; pageNumber: number; properties: SectionProperty[]; + differences?: boolean; } export interface SectionProperty { label: string; type: string; value: any[]; + differences?: boolean; + properties?: SectionProperty[]; widget?: { id: string; + data?: any[]; [propertyName: string]: any; }; } diff --git a/ui/src/app/metadata/configuration/reducer/index.spec.ts b/ui/src/app/metadata/configuration/reducer/index.spec.ts index eba8409be..8a1aa3cb7 100644 --- a/ui/src/app/metadata/configuration/reducer/index.spec.ts +++ b/ui/src/app/metadata/configuration/reducer/index.spec.ts @@ -1,19 +1,89 @@ import { getConfigurationSectionsFn, getConfigurationModelNameFn, - getConfigurationModelEnabledFn + getConfigurationModelEnabledFn, + assignValueToProperties, + getLimitedPropertiesFn, + getConfigurationModelTypeFn, + getSelectedVersionNumberFn, + getSelectedIsCurrentFn } from './index'; import { SCHEMA as schema } from '../../../../testing/form-schema.stub'; -import { MetadataSourceEditor } from '../../domain/model/wizards/metadata-source-editor'; import { Metadata } from '../../domain/domain.type'; +import { MockMetadataWizard } from '../../../../testing/mockMetadataWizard'; describe('Configuration Reducer', () => { const model = { name: 'foo', - '@type': 'MetadataResolver' + serviceEnabled: true, + foo: { + bar: 'bar', + baz: 'baz' + }, + list: [ + 'super', + 'cool' + ] }; - const definition = new MetadataSourceEditor(); + const props = [ + { + id: 'name', + items: null, + name: 'label.metadata-provider-name-dashboard-display-only', + properties: [], + type: 'string', + value: null, + widget: { id: 'string', help: 'message.must-be-unique' } + }, + { + id: 'serviceEnabled', + items: null, + name: 'serviceEnabled', + properties: [], + type: 'string', + value: null, + widget: { id: 'select', disabled: true } + }, + { + id: 'foo', + items: null, + name: 'foo', + type: 'object', + properties: [ + { + id: 'bar', + name: 'bar', + type: 'string', + properties: [] + }, + { + id: 'baz', + name: 'baz', + type: 'string', + properties: [] + } + ] + }, + { + id: 'list', + name: 'list', + type: 'array', + items: { + type: 'string' + }, + widget: { + id: 'datalist', + data: [ + { key: 'super', label: 'super' }, + { key: 'cool', label: 'cool' }, + { key: 'notcool', label: 'notcool' } + ] + } + } + ]; + + const definition = MockMetadataWizard; describe('getConfigurationSectionsFn', () => { it('should parse the schema, definition, and model into a MetadataConfiguration', () => { @@ -37,4 +107,62 @@ describe('Configuration Reducer', () => { expect(getConfigurationModelEnabledFn(null)).toBe(false); }); }); + + describe('assignValueToProperties function', () => { + it('should assign appropriate values to the given schema properties', () => { + const assigned = assignValueToProperties([model], props, definition); + expect(assigned[0].value).toEqual(['foo']); + expect(assigned[1].value).toEqual([true]); + }); + + it('should assign differences when passed multiple models', () => { + const assigned = assignValueToProperties([model, { + ...model, + name: 'bar', + list: [ + 'super', + 'notcool' + ] + }], props, definition); + expect(assigned[0].differences).toBe(true); + }); + }); + + describe('getLimitedPropertiesFn function', () => { + it('should filter properties without differences', () => { + const assigned = assignValueToProperties([model, { + ...model, + name: 'bar' + }], props, definition); + expect(getLimitedPropertiesFn(assigned).length).toBe(1); + }); + }); + + describe('getConfigurationModelTypeFn function ', () => { + it('should return provider type if the object has an @type property', () => { + const md = { '@type': 'FilebackedHttpMetadataResolver' } as Metadata; + expect(getConfigurationModelTypeFn(md)).toBe('FilebackedHttpMetadataResolver'); + }); + it('should return resolver if no type is detected', () => { + const md = { serviceEnabled: true } as Metadata; + expect(getConfigurationModelTypeFn(md)).toBe('resolver'); + }); + }); + + describe('getSelectedVersionNumberFn function ', () => { + it('should return the selected version by id', () => { + const versions = [ { id: 'foo' }, { id: 'bar' } ]; + const id = 'foo'; + expect(getSelectedVersionNumberFn(versions, id)).toBe(1); + }); + }); + + describe('getSelectedIsCurrentFn function ', () => { + it('should return a boolean of whether the selected version is the most current version', () => { + const versions = [{ id: 'foo' }, { id: 'bar' }]; + const id = 'foo'; + expect(getSelectedIsCurrentFn(versions[0], versions)).toBe(true); + expect(getSelectedIsCurrentFn(versions[1], versions)).toBe(false); + }); + }); }); diff --git a/ui/src/app/metadata/configuration/reducer/index.ts b/ui/src/app/metadata/configuration/reducer/index.ts index 807c1ab17..38087a034 100644 --- a/ui/src/app/metadata/configuration/reducer/index.ts +++ b/ui/src/app/metadata/configuration/reducer/index.ts @@ -16,6 +16,7 @@ import { Metadata } from '../../domain/domain.type'; import * as fromResolver from '../../resolver/reducer'; import * as fromProvider from '../../provider/reducer'; +import { SectionProperty } from '../model/section'; export interface ConfigurationState { configuration: fromConfiguration.State; @@ -55,10 +56,31 @@ export const getConfigurationXml = createSelector(getConfigurationState, fromCon export const assignValueToProperties = (models, properties, definition: any): any[] => { return properties.map(prop => { + const differences = models.some((model, index, array) => { + if (!array) { + return false; + } + return JSON.stringify(model[prop.id]) !== JSON.stringify(array[0][prop.id]); + }); + + const widget = prop.type === 'array' && prop.widget && prop.widget.data ? ({ + ...prop.widget, + data: prop.widget.data.map(item => ({ + ...item, + differences: models + .map((model) => { + const value = model[prop.id]; + return value ? value.indexOf(item.key) > -1 : false; + }) + .reduce((current, val) => current !== val ? true : false, false) + })) + }) : null; + switch (prop.type) { case 'object': return { ...prop, + differences, properties: assignValueToProperties( models.map(model => definition.formatter(model)[prop.id] || {}), prop.properties, @@ -68,15 +90,11 @@ export const assignValueToProperties = (models, properties, definition: any): an default: return { ...prop, - differences: models.some((model, index, array) => { - if (!array) { - return false; - } - return JSON.stringify(model[prop.id]) !== JSON.stringify(array[0][prop.id]); - }), + differences, value: models.map(model => { return model[prop.id]; - }) + }), + widget }; } }); @@ -109,6 +127,10 @@ export const getConfigurationSectionsFn = (models, definition, schema): Metadata properties: assignValueToProperties(models, section.properties, definition) }; }) + .map((section: any) => ({ + ...section, + differences: section.properties.some(prop => prop.differences) + })) }); }; @@ -133,18 +155,21 @@ export const getSelectedVersionId = createSelector(getHistoryState, fromHistory. export const getVersionIds = createSelector(getHistoryState, fromHistory.selectVersionIds); export const getVersionCollection = createSelector(getHistoryState, getVersionIds, fromHistory.selectAllVersions); export const getSelectedVersion = createSelector(getVersionEntities, getSelectedVersionId, getInCollectionFn); +export const getSelectedVersionNumberFn = (versions, selectedId) => versions.indexOf(versions.find(v => v.id === selectedId)) + 1; export const getSelectedVersionNumber = createSelector( getVersionCollection, getSelectedVersionId, - (versions, selectedId) => versions.indexOf(versions.find(v => v.id === selectedId)) + 1 + getSelectedVersionNumberFn ); +export const getSelectedIsCurrentFn = (selected, collection) => { + return selected ? collection[0].id === selected.id : false; +}; + export const getSelectedIsCurrent = createSelector( getSelectedVersion, getVersionCollection, - (selected, collection) => { - return selected ? collection[0].id === selected.id : null; - } + getSelectedIsCurrentFn ); // Version Comparison @@ -164,6 +189,41 @@ export const getComparisonConfigurationCount = createSelector(getComparisonConfi export const getViewChangedOnly = createSelector(getCompareState, fromCompare.getViewChangedOnly); +export const getLimitedPropertiesFn = (properties: SectionProperty[]) => { + return ([ + ...properties + .filter(p => p.differences) + .map(p => { + const parsed = { ...p }; + if (p.widget && p.widget.data) { + parsed.widget = { + ...p.widget, + data: p.widget.data.filter(item => item.differences) + }; + } + if (p.properties) { + parsed.properties = getLimitedPropertiesFn(p.properties); + } + return parsed; + }) + ]); +}; + +export const getLimitedConfigurationsFn = (configurations, limited) => configurations ? ({ + ...configurations, + sections: limited ? configurations.sections : + configurations.sections.map(s => ({ + ...s, + properties: getLimitedPropertiesFn(s.properties), + })) +}) : configurations; + +export const getLimitedComparisonConfigurations = createSelector( + getComparisonConfigurations, + getViewChangedOnly, + getLimitedConfigurationsFn +); + // Version Restoration export const getRestoreState = createSelector(getState, getRestoreStateFn); diff --git a/ui/src/testing/mockMetadataWizard.ts b/ui/src/testing/mockMetadataWizard.ts new file mode 100644 index 000000000..6c0135e29 --- /dev/null +++ b/ui/src/testing/mockMetadataWizard.ts @@ -0,0 +1,78 @@ +import { Wizard } from '../app/wizard/model/wizard'; + +export interface MockMetadata { + name: string; + serviceEnabled: boolean; + foo: { + bar: string; + baz: string; + }; +} + +export const MockMetadataWizard: Wizard = { + label: 'Metadata Source', + type: '@MetadataProvider', + validatorParams: [], + bindings: {}, + parser(changes: Partial, schema?: any): any { + return changes; + }, + formatter(changes: Partial, schema?: any): any { + return changes; + }, + getValidators(): { [key: string]: any } { + return {}; + }, + schema: '/api/ui/MetadataSources', + steps: [ + { + index: 1, + id: 'common', + label: 'label.sp-org-info', + fields: [ + 'name', + 'serviceEnabled' + ], + fieldsets: [ + { + type: 'group', + fields: [ + 'serviceProviderName', + 'entityId', + 'serviceEnabled', + 'organization' + ] + }, + { + type: 'group', + fields: [ + 'contacts' + ] + } + ] + }, + { + index: 2, + id: 'next', + label: 'something', + fields: [ + 'foo', + 'list' + ], + fieldsets: [ + { + type: 'group', + fields: [ + 'foo' + ] + }, + { + type: 'group', + fields: [ + 'list' + ] + } + ] + } + ] +}; diff --git a/ui/src/testing/sample-fbhttp-provider.json b/ui/src/testing/sample-fbhttp-provider.json new file mode 100644 index 000000000..c5d344120 --- /dev/null +++ b/ui/src/testing/sample-fbhttp-provider.json @@ -0,0 +1,94 @@ +{ + "createdDate": "2019-09-12T10:40:41.941", + "modifiedDate": "2019-09-12T10:41:31.787", + "createdBy": "root", + "modifiedBy": "root", + "current": true, + "name": "Update Provider Name", + "resourceId": "e34f39a7-9934-43bf-80c1-3a4e76e6dee8", + "xmlId": "updateid", + "enabled": true, + "requireValidMetadata": true, + "failFastInitialization": true, + "useDefaultPredicateRegistry": true, + "satisfyAnyPredicates": false, + "doInitialization": true, + "metadataFilters": [ + { + "createdDate": "2019-09-12T10:40:41.945", + "modifiedDate": "2019-09-12T10:40:41.945", + "createdBy": "root", + "modifiedBy": "root", + "current": false, + "resourceId": "315bca8c-cc61-4425-af1a-72e0b913102f", + "filterEnabled": false, + "requireSignedRoot": false, + "audId": 10, + "@type": "SignatureValidation", + "version": -230919157 + }, + { + "createdDate": "2019-09-12T10:40:41.946", + "modifiedDate": "2019-09-12T10:40:41.946", + "createdBy": "root", + "modifiedBy": "root", + "current": false, + "resourceId": "0d46d4a9-b65e-440e-99c4-07203fe50921", + "filterEnabled": false, + "removeRolelessEntityDescriptors": true, + "removeEmptyEntitiesDescriptors": true, + "retainedRoles": [], + "audId": 11, + "@type": "EntityRoleWhiteList", + "version": 198629661 + }, + { + "createdDate": "2019-09-12T10:42:38.904", + "modifiedDate": "2019-09-12T10:42:38.904", + "createdBy": "root", + "modifiedBy": "root", + "current": false, + "resourceId": "860bf7f3-3b3c-4dc7-b19b-111c9043d446", + "filterEnabled": false, + "requireSignedRoot": false, + "audId": 15, + "@type": "SignatureValidation", + "version": -747367480 + }, + { + "createdDate": "2019-09-12T10:42:38.905", + "modifiedDate": "2019-09-12T10:42:38.905", + "createdBy": "root", + "modifiedBy": "root", + "current": false, + "resourceId": "7f24ff1d-cb52-422f-8594-682dec40b3b7", + "filterEnabled": false, + "removeRolelessEntityDescriptors": true, + "removeEmptyEntitiesDescriptors": true, + "retainedRoles": [ + "md:SPSSODescriptor" + ], + "audId": 16, + "@type": "EntityRoleWhiteList", + "version": -280104078 + } + ], + "metadataURL": "https://idp.unicon.net/idp/shibboleth", + "backingFile": "test", + "initializeFromBackupFile": false, + "backupFileInitNextRefreshDelay": "PT1H", + "reloadableMetadataResolverAttributes": { + "minRefreshDelay": "PT30S", + "maxRefreshDelay": "PT10M", + "refreshDelayFactor": 0.5 + }, + "httpMetadataResolverAttributes": { + "connectionRequestTimeout": "PT10M", + "connectionTimeout": "PT30M", + "socketTimeout": "PT12H", + "disregardTLSCertificate": false + }, + "audId": 9, + "@type": "FileBackedHttpMetadataResolver", + "version": -1622542052 +} \ No newline at end of file