\ No newline at end of file
diff --git a/ui/src/app/metadata/configuration/container/metadata-comparison.component.html b/ui/src/app/metadata/configuration/container/metadata-comparison.component.html
index 8e652d1c2..5c93f5ec7 100644
--- a/ui/src/app/metadata/configuration/container/metadata-comparison.component.html
+++ b/ui/src/app/metadata/configuration/container/metadata-comparison.component.html
@@ -8,13 +8,23 @@
'container-fluid': (numVersions$ | async) > 2,
'container': (numVersions$ | async) <= 2
}">
-
+
Version History
+
+ Compare Changes
+
+
-
+
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 1b1834fa1..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,10 +1,10 @@
import { Component, ChangeDetectionStrategy, OnDestroy } from '@angular/core';
-import { Observable } from 'rxjs';
+import { Observable, BehaviorSubject, Subscription, combineLatest } from 'rxjs';
import { Store } from '@ngrx/store';
import { ActivatedRoute } from '@angular/router';
-import { map } from 'rxjs/operators';
-import { ConfigurationState, getComparisonConfigurations, getComparisonConfigurationCount } from '../reducer';
-import { CompareVersionRequest, ClearVersions } from '../action/compare.action';
+import { map, withLatestFrom } from 'rxjs/operators';
+import { ConfigurationState, getComparisonConfigurationCount } from '../reducer';
+import { CompareVersionRequest, ClearVersions, ViewChanged } from '../action/compare.action';
import { MetadataConfiguration } from '../model/metadata-configuration';
import * as fromReducer from '../reducer';
@@ -16,10 +16,14 @@ import * as fromReducer from '../reducer';
})
export class MetadataComparisonComponent implements OnDestroy {
+ limiter: BehaviorSubject = new BehaviorSubject(false);
+
versions$: Observable;
numVersions$: Observable;
type$: Observable;
loading$: Observable = this.store.select(fromReducer.getComparisonLoading);
+ limited$: Observable = this.store.select(fromReducer.getViewChangedOnly);
+ sub: Subscription;
constructor(
private store: Store,
@@ -31,12 +35,20 @@ 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))
+ ).subscribe(this.store);
}
ngOnDestroy(): void {
+ this.sub.unsubscribe();
this.store.dispatch(new ClearVersions());
}
}
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/compare.reducer.ts b/ui/src/app/metadata/configuration/reducer/compare.reducer.ts
index d2d346ca3..84eec1558 100644
--- a/ui/src/app/metadata/configuration/reducer/compare.reducer.ts
+++ b/ui/src/app/metadata/configuration/reducer/compare.reducer.ts
@@ -5,16 +5,23 @@ export interface State {
models: Metadata[];
loaded: boolean;
loading: boolean;
+ compareChangedOnly: boolean;
}
export const initialState: State = {
models: [],
loaded: false,
- loading: false
+ loading: false,
+ compareChangedOnly: false
};
export function reducer(state = initialState, action: CompareActionsUnion): State {
switch (action.type) {
+ case CompareActionTypes.SET_VIEW_CHANGED:
+ return {
+ ...state,
+ compareChangedOnly: action.payload
+ };
case CompareActionTypes.COMPARE_METADATA_REQUEST:
return {
...state,
@@ -45,3 +52,4 @@ export function reducer(state = initialState, action: CompareActionsUnion): Stat
export const getVersionModels = (state: State) => state.models;
export const getVersionModelsLoaded = (state: State) => state.loaded;
export const getComparisonLoading = (state: State) => state.loading;
+export const getViewChangedOnly = (state: State) => state.compareChangedOnly;
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 62ecb8404..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,9 +90,11 @@ export const assignValueToProperties = (models, properties, definition: any): an
default:
return {
...prop,
+ differences,
value: models.map(model => {
return model[prop.id];
- })
+ }),
+ widget
};
}
});
@@ -103,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)
+ }))
});
};
@@ -127,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
@@ -156,6 +187,43 @@ export const getComparisonConfigurations = createSelector(
export const getComparisonConfigurationCount = createSelector(getComparisonConfigurations, (config) => config ? config.dates.length : 0);
+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/app/metadata/domain/model/property.ts b/ui/src/app/metadata/domain/model/property.ts
index 4768de9e4..51a4bf06e 100644
--- a/ui/src/app/metadata/domain/model/property.ts
+++ b/ui/src/app/metadata/domain/model/property.ts
@@ -5,10 +5,12 @@ export interface Property {
value: any[];
items: Property;
properties: Property[];
+ differences?: boolean;
widget?: {
id: string;
data?: {key: string, label: string}[];
dataUrl?: string;
+ differences?: string;
[propertyName: string]: any;
};
}
diff --git a/ui/src/app/schema-form/widget/filter-target/filter-target.component.html b/ui/src/app/schema-form/widget/filter-target/filter-target.component.html
index fcbd80ab8..f1e04538a 100644
--- a/ui/src/app/schema-form/widget/filter-target/filter-target.component.html
+++ b/ui/src/app/schema-form/widget/filter-target/filter-target.component.html
@@ -78,7 +78,7 @@
Required for Regex
-
+
,
{{ error.message }}
diff --git a/ui/src/app/shared/validation/regex.validator.spec.ts b/ui/src/app/shared/validation/regex.validator.spec.ts
index ce8af17ee..4835efd94 100644
--- a/ui/src/app/shared/validation/regex.validator.spec.ts
+++ b/ui/src/app/shared/validation/regex.validator.spec.ts
@@ -11,8 +11,8 @@ describe('RegexValidator', () => {
expect(RegexValidator.isValidRegex(')')).toBe(false);
});
- it('should return false if the regex doesnt begin and end with slashes', () => {
- expect(RegexValidator.isValidRegex('abc')).toBe(false);
+ it('should return true even if the regex doesnt begin and end with slashes', () => {
+ expect(RegexValidator.isValidRegex('abc')).toBe(true);
});
});
});
diff --git a/ui/src/app/shared/validation/regex.validator.ts b/ui/src/app/shared/validation/regex.validator.ts
index be567a0f4..f05fadf8d 100644
--- a/ui/src/app/shared/validation/regex.validator.ts
+++ b/ui/src/app/shared/validation/regex.validator.ts
@@ -1,9 +1,7 @@
-const regexChecker = new RegExp('^\/|\/$', 'g');
-
export class RegexValidator {
static isValidRegex(pattern: string): boolean {
if (!pattern) {
- return true;
+ return false;
}
let regex;
try {
@@ -11,7 +9,7 @@ export class RegexValidator {
} catch (err) {
return false;
}
- return regexChecker.test(pattern);
+ return true;
}
}
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