diff --git a/ui/src/app/metadata/configuration/action/compare.action.ts b/ui/src/app/metadata/configuration/action/compare.action.ts new file mode 100644 index 000000000..410a8dcab --- /dev/null +++ b/ui/src/app/metadata/configuration/action/compare.action.ts @@ -0,0 +1,47 @@ +import { Action } from '@ngrx/store'; +import { MetadataHistory } from '../model/history'; +import { MetadataVersion } from '../model/version'; +import { Metadata } from '../../domain/domain.type'; + +export enum CompareActionTypes { + COMPARE_METADATA_REQUEST = '[Compare Version] Compare Version Request', + COMPARE_METADATA_SUCCESS = '[Compare Version] Compare Version Success', + COMPARE_METADATA_ERROR = '[Compare Version] Compare Version Error', + SET_VERSIONS = '[Compare Version] Set Versions', + CLEAR_VERSIONS = '[Compare Version] Clear Versions' +} + +export class CompareVersionRequest implements Action { + readonly type = CompareActionTypes.COMPARE_METADATA_REQUEST; + + constructor(public payload: string[]) { } +} + +export class CompareVersionSuccess implements Action { + readonly type = CompareActionTypes.COMPARE_METADATA_SUCCESS; + + constructor(public payload: Metadata[]) { } +} + +export class CompareVersionError implements Action { + readonly type = CompareActionTypes.COMPARE_METADATA_ERROR; + + constructor(public payload: any) { } +} + +export class SetMetadataVersions implements Action { + readonly type = CompareActionTypes.SET_VERSIONS; + + constructor(public payload: Metadata[]) { } +} + +export class ClearVersions implements Action { + readonly type = CompareActionTypes.CLEAR_VERSIONS; +} + +export type CompareActionsUnion = + | CompareVersionRequest + | CompareVersionSuccess + | CompareVersionError + | SetMetadataVersions + | ClearVersions; diff --git a/ui/src/app/metadata/configuration/component/array-property.component.html b/ui/src/app/metadata/configuration/component/array-property.component.html index ee177445e..969bd175e 100644 --- a/ui/src/app/metadata/configuration/component/array-property.component.html +++ b/ui/src/app/metadata/configuration/component/array-property.component.html @@ -1,18 +1,24 @@
{{ property.name }}
-
-
-
-
- {{ i + 1 }}.  - {{ property.items.properties[prop].title }} +
+
+
+ {{ property.items.properties[prop].title }} +
+ +
+ {{ version[i][prop] }}
-
- {{ value[prop] }} +
+ —
-
+
@@ -25,12 +31,14 @@
- {{ attr.label }} -
- + {{ attr.label }} +
+ true - + false
@@ -40,12 +48,14 @@
- {{ property.name }} -

-
    -
  • - {{ item }} -
  • -
+ {{ 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 index 453d9d484..614738b06 100644 --- a/ui/src/app/metadata/configuration/component/array-property.component.spec.ts +++ b/ui/src/app/metadata/configuration/component/array-property.component.spec.ts @@ -21,12 +21,12 @@ class TestHostComponent { @ViewChild(ArrayPropertyComponent) public componentUnderTest: ArrayPropertyComponent; - property: Property = getStepProperty(SCHEMA.properties.list, { + property: Property = getStepProperty(SCHEMA.properties.list, [{ name: 'foo', type: 'baz', description: 'foo bar baz', list: [] - }, SCHEMA.definitions); + }], SCHEMA.definitions); setProperty(property: Property): void { this.property = property; diff --git a/ui/src/app/metadata/configuration/component/array-property.component.ts b/ui/src/app/metadata/configuration/component/array-property.component.ts index ff9cd0ac4..35d50dedd 100644 --- a/ui/src/app/metadata/configuration/component/array-property.component.ts +++ b/ui/src/app/metadata/configuration/component/array-property.component.ts @@ -1,4 +1,4 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, OnChanges } from '@angular/core'; import { Property } from '../../domain/model/property'; import { Observable, of } from 'rxjs'; import { AttributesService } from '../../domain/service/attributes.service'; @@ -10,15 +10,22 @@ import { ConfigurationPropertyComponent } from './configuration-property.compone styleUrls: [] }) -export class ArrayPropertyComponent extends ConfigurationPropertyComponent { +export class ArrayPropertyComponent extends ConfigurationPropertyComponent implements OnChanges { @Input() property: Property; + range = []; + constructor( private attrService: AttributesService ) { super(); } + ngOnChanges(): void { + const keys = this.property.value.reduce((val, version) => version ? version.length > val ? version.length : val : val, 0); + this.range = [...Array(keys).keys()]; + } + get attributeList$(): Observable<{ key: string, label: string }[]> { if (this.property.widget && this.property.widget.hasOwnProperty('data')) { return of(this.property.widget.data); diff --git a/ui/src/app/metadata/configuration/component/configuration-property.component.ts b/ui/src/app/metadata/configuration/component/configuration-property.component.ts index bcdd45711..001a8235b 100644 --- a/ui/src/app/metadata/configuration/component/configuration-property.component.ts +++ b/ui/src/app/metadata/configuration/component/configuration-property.component.ts @@ -3,12 +3,12 @@ import { Property } from '../../domain/model/property'; @Component({ selector: 'configuration-property', - template: `{{ property | json }}`, - styleUrls: [] + template: `{{ property | json }}` }) export class ConfigurationPropertyComponent { @Input() property: Property; + @Input() columns = 1; constructor() { } @@ -19,5 +19,9 @@ export class ConfigurationPropertyComponent { getItemType(items: Property): string { return items.widget ? items.widget.id : 'default'; } + + get width(): string { + return `${ Math.floor(100 / (this.columns + 1)) }%`; + } } diff --git a/ui/src/app/metadata/configuration/component/history-list.component.spec.ts b/ui/src/app/metadata/configuration/component/history-list.component.spec.ts index 43d49a29f..3c191c300 100644 --- a/ui/src/app/metadata/configuration/component/history-list.component.spec.ts +++ b/ui/src/app/metadata/configuration/component/history-list.component.spec.ts @@ -79,4 +79,14 @@ describe('Metadata History List Component', () => { expect(instance.restore).toHaveBeenCalledWith(selected); }); }); + + describe('toggleVersionSelected method', () => { + it('should add or remove the selected version', () => { + list.toggleVersionSelected(TestData.versions[0]); + fixture.detectChanges(); + list.toggleVersionSelected(TestData.versions[0]); + fixture.detectChanges(); + expect(list.selected.length).toBe(0); + }); + }); }); 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 4f58d07f1..82034cca4 100644 --- a/ui/src/app/metadata/configuration/component/metadata-configuration.component.html +++ b/ui/src/app/metadata/configuration/component/metadata-configuration.component.html @@ -1,12 +1,14 @@
-
+

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

-
+
- Option - Value + Option + + Value + {{ date | date:'medium' }} +
- + +
-
+
\ No newline at end of file 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 index f26c8d65c..c17a469af 100644 --- a/ui/src/app/metadata/configuration/component/metadata-configuration.component.spec.ts +++ b/ui/src/app/metadata/configuration/component/metadata-configuration.component.spec.ts @@ -7,6 +7,7 @@ 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'; +import { Router } from '@angular/router'; @Component({ selector: 'object-property', @@ -14,6 +15,7 @@ import { MockI18nModule } from '../../../../testing/i18n.stub'; }) class ObjectPropertyComponent { @Input() property: Property; + @Input() columns = 1; } @Component({ @@ -25,7 +27,10 @@ class TestHostComponent { @ViewChild(MetadataConfigurationComponent) public componentUnderTest: MetadataConfigurationComponent; - configuration: MetadataConfiguration = {sections: []}; + configuration: MetadataConfiguration = { + dates: [], + sections: [] + }; } describe('Metadata Configuration Component', () => { @@ -33,6 +38,7 @@ describe('Metadata Configuration Component', () => { let fixture: ComponentFixture; let instance: TestHostComponent; let app: MetadataConfigurationComponent; + let router: Router; beforeEach(async(() => { TestBed.configureTestingModule({ @@ -51,10 +57,36 @@ describe('Metadata Configuration Component', () => { fixture = TestBed.createComponent(TestHostComponent); instance = fixture.componentInstance; app = instance.componentUnderTest; + router = TestBed.get(Router); fixture.detectChanges(); })); it('should accept a configuration input', async(() => { expect(app).toBeTruthy(); })); + + describe('edit method', () => { + it('should call router.navigate', () => { + spyOn(router, 'navigate'); + app.edit('foo'); + expect(router.navigate).toHaveBeenCalled(); + }); + }); + + describe('width getter', () => { + it('should default to 100%', () => { + expect(app.width).toBe('100%'); + }); + it('should calculate the width based on dates', () => { + instance.configuration = { + ...instance.configuration, + dates: [ + new Date().toISOString(), + new Date().toISOString() + ] + }; + fixture.detectChanges(); + expect(app.width).toBe('33%'); + }); + }); }); diff --git a/ui/src/app/metadata/configuration/component/metadata-configuration.component.ts b/ui/src/app/metadata/configuration/component/metadata-configuration.component.ts index 9e94bcdbd..fd90fb58b 100644 --- a/ui/src/app/metadata/configuration/component/metadata-configuration.component.ts +++ b/ui/src/app/metadata/configuration/component/metadata-configuration.component.ts @@ -1,6 +1,8 @@ import { Component, ChangeDetectionStrategy, Input } from '@angular/core'; import { Router, ActivatedRoute } from '@angular/router'; import { MetadataConfiguration } from '../model/metadata-configuration'; +import { Property } from '../../domain/model/property'; +import { Observable, of } from 'rxjs'; @Component({ selector: 'metadata-configuration', @@ -19,4 +21,9 @@ export class MetadataConfigurationComponent { edit(id: string): void { this.router.navigate(['../', 'edit', id], { relativeTo: this.activatedRoute.parent }); } + + get width(): string { + const columns = this.configuration.dates.length; + return `${Math.floor(100 / (columns + 1)) }%`; + } } diff --git a/ui/src/app/metadata/configuration/component/object-property.component.html b/ui/src/app/metadata/configuration/component/object-property.component.html index b0aa7b967..0c19ac9d0 100644 --- a/ui/src/app/metadata/configuration/component/object-property.component.html +++ b/ui/src/app/metadata/configuration/component/object-property.component.html @@ -1,7 +1,7 @@ - - - + + + diff --git a/ui/src/app/metadata/configuration/component/primitive-property.component.html b/ui/src/app/metadata/configuration/component/primitive-property.component.html index 9eef2181f..608f4acdd 100644 --- a/ui/src/app/metadata/configuration/component/primitive-property.component.html +++ b/ui/src/app/metadata/configuration/component/primitive-property.component.html @@ -1,5 +1,10 @@
- {{ property.name }} - {{ property.value || property.value === false ? property.value : '-' }} + {{ property.name }} + {{ v ? v : (v === false) ? v : '-' }}
\ No newline at end of file diff --git a/ui/src/app/metadata/configuration/configuration.module.ts b/ui/src/app/metadata/configuration/configuration.module.ts index dc291952c..05fa99298 100644 --- a/ui/src/app/metadata/configuration/configuration.module.ts +++ b/ui/src/app/metadata/configuration/configuration.module.ts @@ -2,7 +2,7 @@ import { NgModule, ModuleWithProviders } from '@angular/core'; import { CommonModule } from '@angular/common'; import { StoreModule } from '@ngrx/store'; import { EffectsModule } from '@ngrx/effects'; - +import { RouterModule } from '@angular/router'; import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'; import { I18nModule } from '../../i18n/i18n.module'; @@ -15,7 +15,6 @@ import { ConfigurationPropertyComponent } from './component/configuration-proper 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'; import { MetadataHeaderComponent } from './component/metadata-header.component'; @@ -24,6 +23,8 @@ import { MetadataHistoryService } from './service/history.service'; import { MetadataHistoryComponent } from './container/metadata-history.component'; import { HistoryListComponent } from './component/history-list.component'; import { DomainModule } from '../domain/domain.module'; +import { MetadataComparisonComponent } from './container/metadata-comparison.component'; +import { CompareVersionEffects } from './effect/compare.effect'; @NgModule({ declarations: [ @@ -37,7 +38,8 @@ import { DomainModule } from '../domain/domain.module'; ConfigurationComponent, MetadataHeaderComponent, MetadataHistoryComponent, - HistoryListComponent + HistoryListComponent, + MetadataComparisonComponent ], entryComponents: [], imports: [ @@ -66,7 +68,12 @@ export class MetadataConfigurationModule { imports: [ MetadataConfigurationModule, StoreModule.forFeature('metadata-configuration', fromConfig.reducers), - EffectsModule.forFeature([MetadataConfigurationEffects, MetadataHistoryEffects]) + EffectsModule.forFeature( + [ + MetadataConfigurationEffects, + MetadataHistoryEffects, + CompareVersionEffects + ]) ], providers: [] }) diff --git a/ui/src/app/metadata/configuration/configuration.routing.ts b/ui/src/app/metadata/configuration/configuration.routing.ts index d27b29872..0a77b0c2b 100644 --- a/ui/src/app/metadata/configuration/configuration.routing.ts +++ b/ui/src/app/metadata/configuration/configuration.routing.ts @@ -3,6 +3,7 @@ import { ConfigurationComponent } from './container/configuration.component'; import { MetadataOptionsComponent } from './container/metadata-options.component'; import { MetadataXmlComponent } from './container/metadata-xml.component'; import { MetadataHistoryComponent } from './container/metadata-history.component'; +import { MetadataComparisonComponent } from './container/metadata-comparison.component'; export const ConfigurationRoutes: Routes = [ { @@ -24,6 +25,10 @@ export const ConfigurationRoutes: Routes = [ { path: 'history', component: MetadataHistoryComponent + }, + { + path: 'compare', + component: MetadataComparisonComponent } ] } diff --git a/ui/src/app/metadata/configuration/container/configuration.component.html b/ui/src/app/metadata/configuration/container/configuration.component.html index e4709611b..b0d813546 100644 --- a/ui/src/app/metadata/configuration/container/configuration.component.html +++ b/ui/src/app/metadata/configuration/container/configuration.component.html @@ -14,7 +14,9 @@

Source Configuration

- + + +
\ 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 index c399fbacf..bc82b8a14 100644 --- a/ui/src/app/metadata/configuration/container/configuration.component.spec.ts +++ b/ui/src/app/metadata/configuration/container/configuration.component.spec.ts @@ -18,7 +18,10 @@ class TestHostComponent { @ViewChild(ConfigurationComponent) public componentUnderTest: ConfigurationComponent; - configuration: MetadataConfiguration = { sections: [] }; + configuration: MetadataConfiguration = { + dates: [], + sections: [] + }; } describe('Metadata Configuration Page Component', () => { diff --git a/ui/src/app/metadata/configuration/container/configuration.component.ts b/ui/src/app/metadata/configuration/container/configuration.component.ts index 357d745fd..17ce4b99c 100644 --- a/ui/src/app/metadata/configuration/container/configuration.component.ts +++ b/ui/src/app/metadata/configuration/container/configuration.component.ts @@ -52,12 +52,7 @@ export class ConfigurationComponent implements OnDestroy { } }); - this.name$ = this.store - .select(fromReducer.getConfigurationModel) - .pipe( - filter(model => !!model), - map(model => model ? ('serviceProviderName' in model) ? model.serviceProviderName : model.name : false) - ); + this.name$ = this.store.select(fromReducer.getConfigurationModelName); } ngOnDestroy() { diff --git a/ui/src/app/metadata/configuration/container/metadata-comparison.component.html b/ui/src/app/metadata/configuration/container/metadata-comparison.component.html new file mode 100644 index 000000000..fabdb5338 --- /dev/null +++ b/ui/src/app/metadata/configuration/container/metadata-comparison.component.html @@ -0,0 +1,15 @@ + \ No newline at end of file diff --git a/ui/src/app/metadata/configuration/component/configuration-property.component.html b/ui/src/app/metadata/configuration/container/metadata-comparison.component.spec.ts similarity index 100% rename from ui/src/app/metadata/configuration/component/configuration-property.component.html rename to ui/src/app/metadata/configuration/container/metadata-comparison.component.spec.ts diff --git a/ui/src/app/metadata/configuration/container/metadata-comparison.component.ts b/ui/src/app/metadata/configuration/container/metadata-comparison.component.ts new file mode 100644 index 000000000..701c99d2a --- /dev/null +++ b/ui/src/app/metadata/configuration/container/metadata-comparison.component.ts @@ -0,0 +1,32 @@ +import { Component, ChangeDetectionStrategy } from '@angular/core'; +import { Observable } from 'rxjs'; +import { Store } from '@ngrx/store'; +import { ActivatedRoute } from '@angular/router'; +import { map } from 'rxjs/operators'; +import { ConfigurationState, getVersionConfigurations } from '../reducer'; +import { Metadata } from '../../domain/domain.type'; +import { CompareVersionRequest } from '../action/compare.action'; +import { MetadataConfiguration } from '../model/metadata-configuration'; + +@Component({ + selector: 'metadata-comparison', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './metadata-comparison.component.html', + styleUrls: [] +}) +export class MetadataComparisonComponent { + + versions$: Observable; + + constructor( + private store: Store, + private activatedRoute: ActivatedRoute + ) { + this.activatedRoute.queryParams.pipe( + map(params => params.versions), + map(versions => new CompareVersionRequest(versions)) + ).subscribe(this.store); + + this.versions$ = this.store.select(getVersionConfigurations); + } +} diff --git a/ui/src/app/metadata/configuration/container/metadata-history.component.html b/ui/src/app/metadata/configuration/container/metadata-history.component.html index 225912756..2bcc27fe5 100644 --- a/ui/src/app/metadata/configuration/container/metadata-history.component.html +++ b/ui/src/app/metadata/configuration/container/metadata-history.component.html @@ -1,3 +1,5 @@
- +
diff --git a/ui/src/app/metadata/configuration/container/metadata-history.component.spec.ts b/ui/src/app/metadata/configuration/container/metadata-history.component.spec.ts index 084d79ef0..43d8cc11b 100644 --- a/ui/src/app/metadata/configuration/container/metadata-history.component.spec.ts +++ b/ui/src/app/metadata/configuration/container/metadata-history.component.spec.ts @@ -8,14 +8,16 @@ import { MetadataHistoryService } from '../service/history.service'; import { of } from 'rxjs'; import { StoreModule, combineReducers } from '@ngrx/store'; import * as fromConfiguration from '../reducer'; +import { Router, ActivatedRoute } from '@angular/router'; +import { RouterStub } from '../../../../testing/router.stub'; +import { ActivatedRouteStub } from '../../../../testing/activated-route.stub'; export const TestData = { versions: [ { - versionNumber: 1, - saveDate: new Date(), - changedBy: 'admin', - actions: [] + id: '1', + date: new Date().toDateString(), + creator: 'admin' } ] }; @@ -37,12 +39,19 @@ const MockHistoryService = { describe('Metadata Version History Component', () => { let fixture: ComponentFixture; let instance: MetadataHistoryComponent; + let router: Router; beforeEach(() => { TestBed.configureTestingModule({ providers: [ { provide: MetadataHistoryService, useValue: MockHistoryService + }, + { + provide: Router, useClass: RouterStub + }, + { + provide: ActivatedRoute, useClass: ActivatedRouteStub } ], imports: [ @@ -59,10 +68,59 @@ describe('Metadata Version History Component', () => { fixture = TestBed.createComponent(MetadataHistoryComponent); instance = fixture.componentInstance; + router = TestBed.get(Router); + spyOn(router, 'navigate'); fixture.detectChanges(); }); it('should compile', () => { expect(instance).toBeDefined(); }); + + describe('compare versions method', () => { + it('should call the router.navigate method', () => { + instance.compareVersions(TestData.versions); + expect(router.navigate).toHaveBeenCalled(); + }); + }); + + describe('sortVersionsByDate method', () => { + it('should sort the versions by their date', () => { + const nowTime = new Date().getTime(); + const futureTime = nowTime + 10000; + const beforeTime = nowTime - 10000; + const nowDate = new Date(nowTime); + const futureDate = new Date(futureTime); + const beforeDate = new Date(beforeTime); + + const versions = [ + { + id: 'foo', + creator: 'bar', + date: nowDate.toISOString() + }, + { + id: 'bar', + creator: 'baz', + date: beforeDate.toISOString() + }, + { + id: 'baz', + creator: 'foo', + date: beforeDate.toISOString() + }, + { + id: 'baz2', + creator: 'foo', + date: futureDate.toISOString() + } + ]; + + const sorted = instance.sortVersionsByDate(versions); + expect(sorted[0].id).toEqual('bar'); + expect(sorted[1].id).toEqual('baz'); + expect(sorted[2].id).toEqual('foo'); + expect(sorted[3].id).toEqual('baz2'); + }); + }); }); diff --git a/ui/src/app/metadata/configuration/container/metadata-history.component.ts b/ui/src/app/metadata/configuration/container/metadata-history.component.ts index 6fb60bdb9..6d62e6ed7 100644 --- a/ui/src/app/metadata/configuration/container/metadata-history.component.ts +++ b/ui/src/app/metadata/configuration/container/metadata-history.component.ts @@ -3,6 +3,8 @@ import { Observable } from 'rxjs'; import { Store } from '@ngrx/store'; import { ConfigurationState, getVersionCollection } from '../reducer'; import { MetadataVersion } from '../model/version'; +import { CompareVersionRequest } from '../action/compare.action'; +import { Router, ActivatedRoute } from '@angular/router'; @Component({ selector: 'metadata-history', @@ -15,8 +17,29 @@ export class MetadataHistoryComponent { history$: Observable; constructor( - private store: Store + private store: Store, + private router: Router, + private route: ActivatedRoute ) { this.history$ = this.store.select(getVersionCollection); } + + sortVersionsByDate(versions: MetadataVersion[]): MetadataVersion[] { + return versions.sort((a, b) => { + const aDate = new Date(a.date).getTime(); + const bDate = new Date(b.date).getTime(); + return aDate === bDate ? 0 : aDate < bDate ? -1 : 1; + }); + } + + compareVersions(versions: MetadataVersion[]): void { + const sorted = this.sortVersionsByDate(versions); + this.router.navigate( + ['../', 'compare'], + { + queryParams: { versions: sorted.map(v => v.id) }, + relativeTo: this.route + } + ); + } } diff --git a/ui/src/app/metadata/configuration/container/metadata-options.component.html b/ui/src/app/metadata/configuration/container/metadata-options.component.html index 0825b10d4..9c4dbb489 100644 --- a/ui/src/app/metadata/configuration/container/metadata-options.component.html +++ b/ui/src/app/metadata/configuration/container/metadata-options.component.html @@ -4,13 +4,11 @@ [version]="version$ | async" [versionNumber]="versionNumber$ | async" [isCurrent]="isCurrent$ | async"> -
-
Options XML 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 index 0cb1bd5b0..a012fcd70 100644 --- a/ui/src/app/metadata/configuration/container/metadata-options.component.spec.ts +++ b/ui/src/app/metadata/configuration/container/metadata-options.component.spec.ts @@ -40,7 +40,10 @@ class TestHostComponent { @ViewChild(MetadataOptionsComponent) public componentUnderTest: MetadataOptionsComponent; - configuration: MetadataConfiguration = { sections: [] }; + configuration: MetadataConfiguration = { + dates: [], + sections: [] + }; } describe('Metadata Options Page Component', () => { diff --git a/ui/src/app/metadata/configuration/container/metadata-options.component.ts b/ui/src/app/metadata/configuration/container/metadata-options.component.ts index 461252f1d..556aa8805 100644 --- a/ui/src/app/metadata/configuration/container/metadata-options.component.ts +++ b/ui/src/app/metadata/configuration/container/metadata-options.component.ts @@ -8,11 +8,13 @@ import { getConfigurationModel, getSelectedVersion, getSelectedVersionNumber, - getSelectedIsCurrent + getSelectedIsCurrent, + getConfigurationModelEnabled } from '../reducer'; import { MetadataConfiguration } from '../model/metadata-configuration'; import { MetadataVersion } from '../model/version'; import { map } from 'rxjs/operators'; +import { Metadata } from '../../domain/domain.type'; @Component({ selector: 'metadata-options-page', @@ -32,9 +34,7 @@ export class MetadataOptionsComponent { private store: Store ) { this.configuration$ = this.store.select(getConfigurationSections); - this.isEnabled$ = this.store.select(getConfigurationModel).pipe( - map(config => config ? ('serviceEnabled' in config) ? config.serviceEnabled : config.enabled : false) - ); + this.isEnabled$ = this.store.select(getConfigurationModelEnabled); this.version$ = this.store.select(getSelectedVersion); this.versionNumber$ = this.store.select(getSelectedVersionNumber); this.isCurrent$ = this.store.select(getSelectedIsCurrent); 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 index 81fbd836f..6f7e052e3 100644 --- a/ui/src/app/metadata/configuration/container/metadata-xml.component.spec.ts +++ b/ui/src/app/metadata/configuration/container/metadata-xml.component.spec.ts @@ -17,8 +17,6 @@ import { MetadataXmlComponent } from './metadata-xml.component'; class TestHostComponent { @ViewChild(MetadataXmlComponent) public componentUnderTest: MetadataXmlComponent; - - configuration: MetadataConfiguration = { sections: [] }; } describe('Metadata Xml Page Component', () => { diff --git a/ui/src/app/metadata/configuration/effect/compare.effect.ts b/ui/src/app/metadata/configuration/effect/compare.effect.ts new file mode 100644 index 000000000..bb6e077d2 --- /dev/null +++ b/ui/src/app/metadata/configuration/effect/compare.effect.ts @@ -0,0 +1,48 @@ +import { Injectable } from '@angular/core'; +import { Effect, Actions, ofType } from '@ngrx/effects'; +import { catchError, withLatestFrom, map, filter, combineLatest, switchMap } from 'rxjs/operators'; +import { of } from 'rxjs'; +import { MetadataHistoryService } from '../service/history.service'; +import { + CompareVersionRequest, + CompareActionTypes, + CompareVersionSuccess, + CompareVersionError, + SetMetadataVersions +} from '../action/compare.action'; +import { Store } from '@ngrx/store'; +import { State, getConfigurationModel } from '../reducer'; + +@Injectable() +export class CompareVersionEffects { + + @Effect() + compareVersionRequest$ = this.actions$.pipe( + ofType(CompareActionTypes.COMPARE_METADATA_REQUEST), + map(action => action.payload), + withLatestFrom( + this.store.select(getConfigurationModel) + ), + switchMap(([versions, model]) => { + const type = '@type' in model ? 'provider' : 'resolver'; + const id = '@type' in model ? model.resourceId : model.id; + return this.historyService.getVersions(id, versions, type).pipe( + map(v => new CompareVersionSuccess(v)), + catchError(err => of(new CompareVersionError(err))) + ); + }) + ); + + @Effect() + setVersionsOnSuccess$ = this.actions$.pipe( + ofType(CompareActionTypes.COMPARE_METADATA_SUCCESS), + map(action => action.payload), + map(versions => new SetMetadataVersions(versions)) + ); + + constructor( + private historyService: MetadataHistoryService, + private store: Store, + private actions$: Actions + ) { } +} diff --git a/ui/src/app/metadata/configuration/model/metadata-configuration.ts b/ui/src/app/metadata/configuration/model/metadata-configuration.ts index b8a37b85e..a06dda3f5 100644 --- a/ui/src/app/metadata/configuration/model/metadata-configuration.ts +++ b/ui/src/app/metadata/configuration/model/metadata-configuration.ts @@ -1,5 +1,6 @@ -import Section from './section'; +import { Section } from './section'; export interface MetadataConfiguration { sections: Section[]; + dates: String[]; } diff --git a/ui/src/app/metadata/configuration/model/section.ts b/ui/src/app/metadata/configuration/model/section.ts index 089a1953a..fd7f9b00f 100644 --- a/ui/src/app/metadata/configuration/model/section.ts +++ b/ui/src/app/metadata/configuration/model/section.ts @@ -1,11 +1,17 @@ -import { Property } from '../../domain/model/property'; - export interface Section { id: string; index: number; label: string; pageNumber: number; - properties: Property[]; + properties: SectionProperty[]; } -export default Section; +export interface SectionProperty { + label: string; + type: string; + value: any[]; + widget?: { + id: string; + [propertyName: string]: any; + }; +} diff --git a/ui/src/app/metadata/configuration/reducer/compare.reducer.spec.ts b/ui/src/app/metadata/configuration/reducer/compare.reducer.spec.ts new file mode 100644 index 000000000..4423c6be9 --- /dev/null +++ b/ui/src/app/metadata/configuration/reducer/compare.reducer.spec.ts @@ -0,0 +1,53 @@ +import { reducer } from './compare.reducer'; +import * as fromCompare from './compare.reducer'; +import { MetadataResolver } from '../../domain/model'; +import { SetMetadataVersions, ClearVersions } from '../action/compare.action'; + +describe('Comparison Reducer', () => { + const initialState: fromCompare.State = { ...fromCompare.initialState }; + const models: MetadataResolver[] = [{ + id: 'foo', + serviceProviderName: 'foo', + '@type': 'MetadataResolver', + createdBy: 'admin' + }]; + + describe('undefined action', () => { + it('should return the default state', () => { + const result = reducer(undefined, {} as any); + + expect(result).toEqual(initialState); + }); + }); + + describe('set versions action', () => { + it('should add the models to the state', () => { + const action = new SetMetadataVersions(models); + const result = reducer(initialState, action); + expect(result.models).toEqual(models); + expect(result.loaded).toBe(true); + }); + }); + + describe('clear versions action', () => { + it('should remove the models from the state', () => { + const action = new ClearVersions(); + const result = reducer(initialState, action); + expect(result.models).toEqual([]); + expect(result.loaded).toBe(false); + }); + }); + + describe('selector functions', () => { + describe('getModel', () => { + it('should retrieve the model from state', () => { + expect(fromCompare.getVersionModels({ ...initialState, models })).toBe(models); + }); + }); + describe('getVersionModelsLoaded', () => { + it('should retrieve the loaded state', () => { + expect(fromCompare.getVersionModelsLoaded({ ...initialState, loaded: true })).toBe(true); + }); + }); + }); +}); diff --git a/ui/src/app/metadata/configuration/reducer/compare.reducer.ts b/ui/src/app/metadata/configuration/reducer/compare.reducer.ts new file mode 100644 index 000000000..ea674238f --- /dev/null +++ b/ui/src/app/metadata/configuration/reducer/compare.reducer.ts @@ -0,0 +1,33 @@ +import { CompareActionTypes, CompareActionsUnion } from '../action/compare.action'; +import { Metadata } from '../../domain/domain.type'; + +export interface State { + models: Metadata[]; + loaded: Boolean; +} + +export const initialState: State = { + models: [], + loaded: false +}; + +export function reducer(state = initialState, action: CompareActionsUnion): State { + switch (action.type) { + case CompareActionTypes.SET_VERSIONS: + return { + ...state, + models: action.payload, + loaded: true + }; + case CompareActionTypes.CLEAR_VERSIONS: + return { + ...initialState + }; + default: { + return state; + } + } +} + +export const getVersionModels = (state: State) => state.models; +export const getVersionModelsLoaded = (state: State) => state.loaded; diff --git a/ui/src/app/metadata/configuration/reducer/index.spec.ts b/ui/src/app/metadata/configuration/reducer/index.spec.ts index 5a675621c..eba8409be 100644 --- a/ui/src/app/metadata/configuration/reducer/index.spec.ts +++ b/ui/src/app/metadata/configuration/reducer/index.spec.ts @@ -1,6 +1,11 @@ -import { getConfigurationSectionsFn } from './index'; +import { + getConfigurationSectionsFn, + getConfigurationModelNameFn, + getConfigurationModelEnabledFn +} 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'; describe('Configuration Reducer', () => { const model = { @@ -12,8 +17,24 @@ describe('Configuration Reducer', () => { describe('getConfigurationSectionsFn', () => { it('should parse the schema, definition, and model into a MetadataConfiguration', () => { - const config = getConfigurationSectionsFn(model, definition, schema); + const config = getConfigurationSectionsFn([model], definition, schema); expect(config.sections).toBeDefined(); }); }); + + describe('getConfigurationModelNameFn function', () => { + it('should return the name attribute', () => { + expect(getConfigurationModelNameFn({ serviceProviderName: 'foo' } as Metadata)).toBe('foo'); + expect(getConfigurationModelNameFn({ name: 'bar' } as Metadata)).toBe('bar'); + expect(getConfigurationModelNameFn(null)).toBe(false); + }); + }); + + describe('getConfigurationModelEnabledFn function', () => { + it('should return the name attribute', () => { + expect(getConfigurationModelEnabledFn({ serviceEnabled: true } as Metadata)).toBe(true); + expect(getConfigurationModelEnabledFn({ enabled: true } as Metadata)).toBe(true); + expect(getConfigurationModelEnabledFn(null)).toBe(false); + }); + }); }); diff --git a/ui/src/app/metadata/configuration/reducer/index.ts b/ui/src/app/metadata/configuration/reducer/index.ts index 702e731f4..baf934164 100644 --- a/ui/src/app/metadata/configuration/reducer/index.ts +++ b/ui/src/app/metadata/configuration/reducer/index.ts @@ -3,20 +3,26 @@ import { createSelector, createFeatureSelector } from '@ngrx/store'; import * as fromRoot from '../../../app.reducer'; import * as fromConfiguration from './configuration.reducer'; import * as fromHistory from './history.reducer'; +import * as fromCompare from './compare.reducer'; import { WizardStep } from '../../../wizard/model'; import * as utils from '../../domain/utility/configuration'; import { getSplitSchema } from '../../../wizard/reducer'; import { getInCollectionFn } from '../../domain/domain.util'; +import { MetadataConfiguration } from '../model/metadata-configuration'; +import { Property } from '../../domain/model/property'; +import { Metadata } from '../../domain/domain.type'; export interface ConfigurationState { configuration: fromConfiguration.State; history: fromHistory.HistoryState; + compare: fromCompare.State; } export const reducers = { configuration: fromConfiguration.reducer, - history: fromHistory.reducer + history: fromHistory.reducer, + compare: fromCompare.reducer }; export interface State extends fromRoot.State { @@ -27,40 +33,80 @@ export const getState = createFeatureSelector('metadata-conf export const getConfigurationStateFn = (state: ConfigurationState) => state.configuration; export const getHistoryStateFn = (state: ConfigurationState) => state.history; +export const getCompareStateFn = (state: ConfigurationState) => state.compare; export const getConfigurationState = createSelector(getState, getConfigurationStateFn); export const getConfigurationModel = createSelector(getConfigurationState, fromConfiguration.getModel); +export const getConfigurationModelList = createSelector(getConfigurationModel, (model) => [model]); 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 assignValueToProperties = (models, properties): any[] => { + return properties.map(prop => { + switch (prop.type) { + case 'object': + return { + ...prop, + properties: assignValueToProperties(models.map(model => model[prop.id] || {}), prop.properties) + }; + default: + return { + ...prop, + value: models.map(model => { + return model[prop.id]; + }) + }; + } }); +}; + +export const getConfigurationSectionsFn = (models, definition, schema): MetadataConfiguration => { + return !definition || !schema ? null : + ({ + dates: models.map(m => m.modifiedDate), + 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({}), + schema.definitions || {} + ) + }); + } + ) + .map((section: any) => { + return { + ...section, + properties: assignValueToProperties(models, section.properties) + }; + }) + }); + }; + export const getConfigurationSections = createSelector( - getConfigurationModel, + getConfigurationModelList, getConfigurationDefinition, getConfigurationSchema, getConfigurationSectionsFn ); +export const getConfigurationModelEnabledFn = + (config: Metadata) => config ? ('serviceEnabled' in config) ? config.serviceEnabled : config.enabled : false; + +export const getConfigurationModelNameFn = + (config: Metadata) => config ? ('serviceProviderName' in config) ? config.serviceProviderName : config.name : false; + +export const getConfigurationModelEnabled = createSelector(getConfigurationModel, getConfigurationModelEnabledFn); +export const getConfigurationModelName = createSelector(getConfigurationModel, getConfigurationModelNameFn); + // Version History export const getHistoryState = createSelector(getState, getHistoryStateFn); @@ -83,3 +129,17 @@ export const getSelectedIsCurrent = createSelector( return selected ? collection[0].id === selected.id : null; } ); + +// Version Comparison + +export const getCompareState = createSelector(getState, getCompareStateFn); +export const getVersionModels = createSelector(getCompareState, fromCompare.getVersionModels); +export const getVersionModelsLoaded = createSelector(getCompareState, fromCompare.getVersionModelsLoaded); +export const getVersionConfigurations = createSelector( + getVersionModels, + getConfigurationDefinition, + getConfigurationSchema, + getConfigurationSectionsFn +); + + diff --git a/ui/src/app/metadata/configuration/service/history.service.ts b/ui/src/app/metadata/configuration/service/history.service.ts index 3125c2091..8dbcf9029 100644 --- a/ui/src/app/metadata/configuration/service/history.service.ts +++ b/ui/src/app/metadata/configuration/service/history.service.ts @@ -1,11 +1,12 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { Observable, of } from 'rxjs'; +import { Observable, of, forkJoin } from 'rxjs'; import { MetadataHistory } from '../model/history'; import { PATHS } from '../../configuration/configuration.values'; import { MetadataVersion } from '../model/version'; import { map } from 'rxjs/operators'; +import { Metadata } from '../../domain/domain.type'; @Injectable() export class MetadataHistoryService { @@ -24,4 +25,14 @@ export class MetadataHistoryService { })) ); } + + getVersions(resourceId: string, versions: string[], type: string): Observable { + return forkJoin(versions.map( + v => this.getVersion(resourceId, type, v) + )); + } + + getVersion(resourceId: string, type: string, versionId: string): Observable { + return this.http.get(`/${this.base}/${PATHS[type]}/${resourceId}/${this.path}/${versionId}`); + } } diff --git a/ui/src/app/metadata/domain/model/property.ts b/ui/src/app/metadata/domain/model/property.ts index a792514d8..dcd5e9f1b 100644 --- a/ui/src/app/metadata/domain/model/property.ts +++ b/ui/src/app/metadata/domain/model/property.ts @@ -2,7 +2,7 @@ export interface Property { title?: string; type: string; name: string; - value: string[]; + value: any[]; items: Property; properties: Property[]; widget?: { diff --git a/ui/src/app/metadata/domain/utility/configuration.ts b/ui/src/app/metadata/domain/utility/configuration.ts index dc641c4dd..2d5572cb4 100644 --- a/ui/src/app/metadata/domain/utility/configuration.ts +++ b/ui/src/app/metadata/domain/utility/configuration.ts @@ -33,10 +33,13 @@ export function getStepProperties(schema: any, model: any, definitions: any = {} return Object .keys(schema.properties) .map(property => { - return getStepProperty( - schema.properties[property], - model && model.hasOwnProperty(property) ? model[property] : null, - definitions - ); + return { + ...getStepProperty( + schema.properties[property], + model && model.hasOwnProperty(property) ? model[property] : null, + definitions + ), + id: property + }; }); } diff --git a/ui/src/app/metadata/resolver/container/new-resolver.component.ts b/ui/src/app/metadata/resolver/container/new-resolver.component.ts index 09efb0f1f..f58392e21 100644 --- a/ui/src/app/metadata/resolver/container/new-resolver.component.ts +++ b/ui/src/app/metadata/resolver/container/new-resolver.component.ts @@ -36,9 +36,7 @@ export class NewResolverComponent implements OnDestroy { this.actionsSubscription = this.route.queryParams.pipe( takeUntil(this.ngUnsubscribe), distinctUntilChanged(), - map(data => { - return new SelectDraftRequest(data.id); - }) + map(data => new SelectDraftRequest(data.id)) ).subscribe(this.store); } ngOnDestroy(): void { diff --git a/ui/src/app/metadata/resolver/effect/collection.effects.ts b/ui/src/app/metadata/resolver/effect/collection.effects.ts index e5f1edd94..ff5c5ea9c 100644 --- a/ui/src/app/metadata/resolver/effect/collection.effects.ts +++ b/ui/src/app/metadata/resolver/effect/collection.effects.ts @@ -10,7 +10,6 @@ import { LoadResolverRequest, LoadResolverSuccess, LoadResolverError, - LoadAdminResolverRequest, AddResolverRequest, AddResolverSuccess, AddResolverFail, diff --git a/ui/src/app/metadata/resolver/reducer/collection.reducer.spec.ts b/ui/src/app/metadata/resolver/reducer/collection.reducer.spec.ts index 26e8dc34d..43b1ebfe3 100644 --- a/ui/src/app/metadata/resolver/reducer/collection.reducer.spec.ts +++ b/ui/src/app/metadata/resolver/reducer/collection.reducer.spec.ts @@ -73,13 +73,13 @@ describe('Resolver Reducer', () => { describe('Select Resolver', () => { it('should update the selected draft id', () => { let id = 'foo', + createdDate = new Date().toDateString(), expected = { ...snapshot, selectedResolverId: id }; - const action = new resolverActions.SelectResolver(id); + const action = new resolverActions.SelectResolverSuccess({ id, createdDate } as MetadataResolver); const result = reducer({ ...snapshot }, action); - expect(result).toEqual( - Object.assign({}, initialState, expected) - ); + expect(result.selectedResolverId).toEqual(id); + expect(result.ids.length).toBe(3); }); }); }); diff --git a/ui/src/app/metadata/resolver/reducer/collection.reducer.ts b/ui/src/app/metadata/resolver/reducer/collection.reducer.ts index 9ce28324b..925255d96 100644 --- a/ui/src/app/metadata/resolver/reducer/collection.reducer.ts +++ b/ui/src/app/metadata/resolver/reducer/collection.reducer.ts @@ -32,11 +32,11 @@ export function reducer(state = initialState, action: ResolverCollectionActionsU return adapter.updateOne(action.payload, state); } - case ResolverCollectionActionTypes.SELECT: { - return { + case ResolverCollectionActionTypes.SELECT_SUCCESS: { + return adapter.addOne(action.payload, { ...state, - selectedResolverId: action.payload, - }; + selectedResolverId: action.payload.id, + }); } case ResolverCollectionActionTypes.LOAD_RESOLVER_ERROR: { diff --git a/ui/src/app/schema-form/widget/array/array.component.html b/ui/src/app/schema-form/widget/array/array.component.html index 253fa2af4..620efd944 100644 --- a/ui/src/app/schema-form/widget/array/array.component.html +++ b/ui/src/app/schema-form/widget/array/array.component.html @@ -12,7 +12,7 @@ - + , {{ error.message }} diff --git a/ui/src/app/schema-form/widget/array/array.component.ts b/ui/src/app/schema-form/widget/array/array.component.ts index 698500fca..44f5a90a8 100644 --- a/ui/src/app/schema-form/widget/array/array.component.ts +++ b/ui/src/app/schema-form/widget/array/array.component.ts @@ -18,9 +18,9 @@ export interface FormError { selector: 'array-component', templateUrl: `./array.component.html` }) -export class CustomArrayComponent extends ArrayWidget implements AfterViewInit, OnDestroy { +export class CustomArrayComponent extends ArrayWidget implements AfterViewInit { errors$: Observable; - hasErrors: boolean; + hasErrors$: Observable; hasErrorSub: Subscription; messages = { @@ -29,18 +29,15 @@ export class CustomArrayComponent extends ArrayWidget implements AfterViewInit, ngAfterViewInit(): void { this.errors$ = this.formProperty.errorsChanges.pipe( - map(errors => errors ? errors.filter(err => err.code !== 'UNRESOLVABLE_REFERENCE').reduce((coll, err) => { - coll[err.code] = err; - return coll; - }, {}) : {}), + map(errors => errors ? + errors.filter(err => err.code !== 'UNRESOLVABLE_REFERENCE').reduce((coll, err) => { + coll[err.code] = err; + return coll; + }, {}) : {}), map(collection => Object.values(collection)) ); - this.hasErrorSub = this.errors$.subscribe(e => this.hasErrors = !!e.length); - } - - ngOnDestroy(): void { - this.hasErrorSub.unsubscribe(); + this.hasErrors$ = this.errors$.pipe(map(errors => !!errors.length)); } removeItem(index: number, item: FormProperty = null): void { diff --git a/ui/src/app/schema-form/widget/array/inline-obj-list.component.html b/ui/src/app/schema-form/widget/array/inline-obj-list.component.html index 876ea01f7..59149b0c1 100644 --- a/ui/src/app/schema-form/widget/array/inline-obj-list.component.html +++ b/ui/src/app/schema-form/widget/array/inline-obj-list.component.html @@ -12,7 +12,7 @@ - + , {{ error.message }} diff --git a/ui/src/theme/breadcrumb.scss b/ui/src/theme/breadcrumb.scss index f1e5a6c93..9482c7233 100644 --- a/ui/src/theme/breadcrumb.scss +++ b/ui/src/theme/breadcrumb.scss @@ -1,5 +1,5 @@ .breadcrumb-bar { - $default-color: $gray-400; + $default-color: $gray-700; padding: 0; margin: 1rem 0; @@ -7,18 +7,7 @@ border-radius: 0; padding: 0.375rem 0; border-bottom: 1px solid $gray-600; - /* - &, & > .breadcrumb-item.active { + & > .breadcrumb-item.active { color: $default-color; } - > .breadcrumb-item > a { - color: $white; - } - - & > .breadcrumb-item + .breadcrumb-item { - &::before { - color: $default-color; - } - } - */ } \ No newline at end of file diff --git a/ui/tsconfig.json b/ui/tsconfig.json index ff13e7e77..8fd46afd8 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -8,6 +8,7 @@ "emitDecoratorMetadata": true, "experimentalDecorators": true, "allowSyntheticDefaultImports": true, + "downlevelIteration": true, "target": "es5", "typeRoots": [ "node_modules/@types"