From 205287bb7fb786d8331588c2aeb6e04a0a7dcd4f Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Thu, 29 Mar 2018 21:38:46 +0000 Subject: [PATCH] Merged in feature/SHIBUI-285 (pull request #33) Updated new filter container to use filter-form component --- ui/src/app/app.component.html | 2 +- ui/src/app/app.component.spec.ts | 10 +- ui/src/app/app.component.ts | 10 +- ui/src/app/core/reducer/index.spec.ts | 22 ++ ui/src/app/core/reducer/index.ts | 4 +- .../app/core/reducer/version.reducer.spec.ts | 98 ++++++ .../container/editor.component.html | 8 +- .../container/wizard.component.html | 8 +- .../metadata-filter/action/filter.action.ts | 56 +++- .../component/filter-form.component.html | 143 +++++++++ .../component/filter-form.component.scss | 0 .../component/filter-form.component.spec.ts | 73 +++++ .../component/filter-form.component.ts | 93 ++++++ .../component/preview-filter.component.html | 14 + .../component/preview-filter.component.ts | 23 ++ .../component/search-dialog.component.html | 10 +- .../component/search-dialog.component.spec.ts | 19 ++ .../component/search-dialog.component.ts | 15 +- .../container/new-filter.component.html | 104 +----- .../container/new-filter.component.spec.ts | 30 +- .../container/new-filter.component.ts | 86 +++-- .../metadata-filter/effect/filter.effect.ts | 25 +- ui/src/app/metadata-filter/filter.module.ts | 14 +- .../reducer/collection.reducer.ts | 30 ++ .../reducer/filter.reducer.spec.ts | 3 +- .../metadata-filter/reducer/filter.reducer.ts | 30 +- ui/src/app/metadata-filter/reducer/index.ts | 13 +- .../metadata-filter/service/filter.service.ts | 18 ++ .../attribute-release-form.component.html | 62 ++-- .../forms/attribute-release-form.component.ts | 22 +- .../forms/descriptor-info-form.component.html | 1 - .../forms/relying-party-form.component.html | 296 +++++++++--------- .../model/metadata-provider.ts | 2 +- .../autocomplete/autocomplete.component.html | 4 +- .../autocomplete.component.spec.ts | 44 --- .../autocomplete/autocomplete.component.ts | 52 +-- ui/src/theme/buttons.scss | 7 + 37 files changed, 954 insertions(+), 497 deletions(-) create mode 100644 ui/src/app/core/reducer/index.spec.ts create mode 100644 ui/src/app/core/reducer/version.reducer.spec.ts create mode 100644 ui/src/app/metadata-filter/component/filter-form.component.html create mode 100644 ui/src/app/metadata-filter/component/filter-form.component.scss create mode 100644 ui/src/app/metadata-filter/component/filter-form.component.spec.ts create mode 100644 ui/src/app/metadata-filter/component/filter-form.component.ts create mode 100644 ui/src/app/metadata-filter/component/preview-filter.component.html create mode 100644 ui/src/app/metadata-filter/component/preview-filter.component.ts create mode 100644 ui/src/app/metadata-filter/reducer/collection.reducer.ts create mode 100644 ui/src/app/metadata-filter/service/filter.service.ts diff --git a/ui/src/app/app.component.html b/ui/src/app/app.component.html index 8361b6f80..4de6ae629 100644 --- a/ui/src/app/app.component.html +++ b/ui/src/app/app.component.html @@ -70,7 +70,7 @@

- {{ version }} + {{ formatted$ | async }}  |  Copyright © {{ today | date:'yyyy' }} Internet2 diff --git a/ui/src/app/app.component.spec.ts b/ui/src/app/app.component.spec.ts index 4b1943f20..1d3703df5 100644 --- a/ui/src/app/app.component.spec.ts +++ b/ui/src/app/app.component.spec.ts @@ -4,25 +4,27 @@ import { StoreModule, Store, combineReducers } from '@ngrx/store'; import { AppComponent } from './app.component'; import { User } from './core/model/user'; -import * as fromUser from './core/reducer/user.reducer'; +import * as fromRoot from './core/reducer'; import { NotificationModule } from './notification/notification.module'; import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; describe('AppComponent', () => { + let store: Store; beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ NgbDropdownModule.forRoot(), RouterTestingModule, - StoreModule.forRoot({ - user: combineReducers(fromUser.reducer), - }), + StoreModule.forRoot({}), NotificationModule ], declarations: [ AppComponent ], }).compileComponents(); + + store = TestBed.get(Store); + spyOn(store, 'dispatch'); })); it('should create the app', async(() => { diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts index 851158697..a832e3713 100644 --- a/ui/src/app/app.component.ts +++ b/ui/src/app/app.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import { Store } from '@ngrx/store'; -import 'rxjs/add/operator/takeWhile'; +import 'rxjs/add/operator/map'; import * as fromRoot from './core/reducer'; import { VersionInfo } from './core/model/version'; @@ -17,21 +17,17 @@ export class AppComponent implements OnInit { title = 'Shib UI'; version$: Observable; version: string; + formatted$: Observable; today = new Date(); constructor(private store: Store) { this.version$ = this.store.select(fromRoot.getVersionInfo); + this.formatted$ = this.version$.map(v => v && v.build ? `${v.build.version}-${v.git.commit.id}` : ''); } ngOnInit(): void { this.store.dispatch(new LoadProviderRequest()); this.store.dispatch(new LoadDraftRequest()); this.store.dispatch(new VersionInfoLoadRequestAction()); - - this.version$.subscribe(v => { - if (v && v.build) { - this.version = `${v.build.version}-${v.git.commit.id}`; - } - }); } } diff --git a/ui/src/app/core/reducer/index.spec.ts b/ui/src/app/core/reducer/index.spec.ts new file mode 100644 index 000000000..b6336a397 --- /dev/null +++ b/ui/src/app/core/reducer/index.spec.ts @@ -0,0 +1,22 @@ +import * as fromIndex from './index'; +import * as fromUser from './user.reducer'; +import * as fromVersion from './version.reducer'; +import { VersionInfo } from '../model/version'; + +describe('Core index reducers', () => { + const state: fromIndex.CoreState = { + user: fromUser.initialState as fromUser.UserState, + version: fromVersion.initialState as fromVersion.VersionState + }; + + describe('getUserStateFn function', () => { + it('should return the user state', () => { + expect(fromIndex.getUserStateFn(state)).toEqual(state.user); + }); + }); + describe('getVersionStateFn function', () => { + it('should return the version state', () => { + expect(fromIndex.getVersionStateFn(state)).toEqual(state.version); + }); + }); +}); diff --git a/ui/src/app/core/reducer/index.ts b/ui/src/app/core/reducer/index.ts index 16d5ae45f..de0686765 100644 --- a/ui/src/app/core/reducer/index.ts +++ b/ui/src/app/core/reducer/index.ts @@ -27,8 +27,10 @@ export const reducers = { }; export const getCoreFeature = createFeatureSelector('core'); +export const getUserStateFn = (state: CoreState) => state.user; +export const getVersionStateFn = (state: CoreState) => state.version; -export const getUserState = createSelector(getCoreFeature, (state: CoreState) => state.user); +export const getUserState = createSelector(getCoreFeature, getUserStateFn); export const getUser = createSelector(getUserState, fromUser.getUser); export const isFetching = createSelector(getUserState, fromUser.isFetching); export const getUserError = createSelector(getUserState, fromUser.getError); diff --git a/ui/src/app/core/reducer/version.reducer.spec.ts b/ui/src/app/core/reducer/version.reducer.spec.ts new file mode 100644 index 000000000..1536eeaa6 --- /dev/null +++ b/ui/src/app/core/reducer/version.reducer.spec.ts @@ -0,0 +1,98 @@ +import { reducer } from './version.reducer'; +import * as fromVersion from './version.reducer'; +import * as actions from '../action/version.action'; +import { VersionInfo } from '../model/version'; + +describe('Version Reducer', () => { + const initialState: fromVersion.VersionState = { + info: {}, + loading: false, + error: null + }; + + const version: VersionInfo = { + git: { + commit: { + time: '2018-03-28T20:14:36Z', + id: '40aff48' + }, + branch: 'feature/SHIBUI-285' + }, + build: { + version: '1.0.0', + artifact: 'shibui', + name: 'master', + group: 'foo', + time: '2018-03-29T14:51:38.975Z' + } + }; + + describe('undefined action', () => { + it('should return the default state', () => { + const result = reducer(undefined, {} as any); + expect(result).toEqual(initialState); + }); + }); + + describe('Version Load Request', () => { + it('should set loading to true', () => { + const action = new actions.VersionInfoLoadRequestAction(); + const result = reducer(initialState, action); + expect(result.loading).toBe(true); + }); + }); + + describe('Version Load Success', () => { + it('should set loading to false', () => { + const action = new actions.VersionInfoLoadSuccessAction(version); + const result = reducer(initialState, action); + expect(result.loading).toBe(false); + }); + + it('should set the version data to the payload', () => { + const action = new actions.VersionInfoLoadSuccessAction(version); + const result = reducer(initialState, action); + expect(result.info).toEqual(version); + }); + }); + + describe('Version Load Success', () => { + it('should set loading to false', () => { + const action = new actions.VersionInfoLoadErrorAction(new Error()); + const result = reducer(initialState, action); + expect(result.loading).toBe(false); + }); + + it('should add an error to state', () => { + const err = new Error('fail!'); + const action = new actions.VersionInfoLoadErrorAction(err); + const result = reducer(initialState, action); + expect(result.error).toEqual(err); + }); + }); + + describe('getVersionInfo selector', () => { + it('should return the version info from state', () => { + const action = new actions.VersionInfoLoadSuccessAction(version); + const result = reducer(initialState, action); + expect(fromVersion.getVersionInfo(result)).toEqual(version); + }); + }); + + describe('getError selector', () => { + it('should return the version error from state', () => { + const err = new Error('fail'); + const action = new actions.VersionInfoLoadErrorAction(err); + const result = reducer(initialState, action); + expect(fromVersion.getVersionError(result)).toEqual(err); + }); + }); + + describe('getLoading selector', () => { + it('should return the version loading status from state', () => { + const action = new actions.VersionInfoLoadRequestAction(); + const result = reducer(initialState, action); + expect(fromVersion.getVersionIsLoading(result)).toBe(true); + }); + }); +}); diff --git a/ui/src/app/edit-provider/container/editor.component.html b/ui/src/app/edit-provider/container/editor.component.html index 7c7853a0e..767f8f177 100644 --- a/ui/src/app/edit-provider/container/editor.component.html +++ b/ui/src/app/edit-provider/container/editor.component.html @@ -88,8 +88,12 @@ - - +

+ +
+
+ +
diff --git a/ui/src/app/edit-provider/container/wizard.component.html b/ui/src/app/edit-provider/container/wizard.component.html index 3cc781ccc..7034db1c8 100644 --- a/ui/src/app/edit-provider/container/wizard.component.html +++ b/ui/src/app/edit-provider/container/wizard.component.html @@ -27,8 +27,12 @@ - - +
+ +
+
+ +
diff --git a/ui/src/app/metadata-filter/action/filter.action.ts b/ui/src/app/metadata-filter/action/filter.action.ts index 15791c2c4..f1fa4afde 100644 --- a/ui/src/app/metadata-filter/action/filter.action.ts +++ b/ui/src/app/metadata-filter/action/filter.action.ts @@ -1,25 +1,28 @@ import { Action } from '@ngrx/store'; import { QueryParams } from '../../core/model/query'; +import { MetadataProvider } from '../../metadata-provider/model/metadata-provider'; export const QUERY_ENTITY_IDS = '[Filter] Query Entity Ids'; export const VIEW_MORE_IDS = '[Filter] View More Ids Modal'; export const CANCEL_VIEW_MORE = '[Filter] Cancel View More'; export const SELECT_ID = '[Filter] Select Entity ID'; -export const CANCEL_CREATE_FILTER = '[Filter] Cancel Create Filter'; export const LOAD_ENTITY_IDS_SUCCESS = '[Entity ID Collection] Load Entity Ids Success'; export const LOAD_ENTITY_IDS_ERROR = '[Entity ID Collection] Load Entity Ids Error'; +export const CREATE_FILTER = '[Filter] Create Filter'; +export const UPDATE_FILTER = '[Filter] Update Filter'; +export const SAVE_FILTER = '[Filter] Save Filter'; +export const SAVE_FILTER_SUCCESS = '[Filter] Save Filter Success'; +export const SAVE_FILTER_ERROR = '[Filter] Save Filter Error'; +export const CANCEL_CREATE_FILTER = '[Filter] Cancel Create Filter'; + export class QueryEntityIds implements Action { readonly type = QUERY_ENTITY_IDS; constructor(public payload: QueryParams) { } } -export class CancelCreateFilter implements Action { - readonly type = CANCEL_CREATE_FILTER; -} - export class ViewMoreIds implements Action { readonly type = VIEW_MORE_IDS; @@ -48,11 +51,50 @@ export class LoadEntityIdsError implements Action { constructor(public payload: Error) { } } +export class CreateFilter implements Action { + readonly type = CREATE_FILTER; + + constructor(public payload: MetadataProvider) { } +} + +export class UpdateFilter implements Action { + readonly type = UPDATE_FILTER; + + constructor(public payload: Partial) { } +} + +export class SaveFilter implements Action { + readonly type = SAVE_FILTER; + + constructor(public payload: Partial) { } +} + +export class SaveFilterSuccess implements Action { + readonly type = SAVE_FILTER_SUCCESS; + + constructor(public payload: MetadataProvider) { } +} + +export class SaveFilterError implements Action { + readonly type = SAVE_FILTER_ERROR; + + constructor(public payload: Error) { } +} + +export class CancelCreateFilter implements Action { + readonly type = CANCEL_CREATE_FILTER; +} + export type Actions = | ViewMoreIds | CancelViewMore - | CancelCreateFilter | SelectId | LoadEntityIdsSuccess | LoadEntityIdsError - | QueryEntityIds; + | QueryEntityIds + | CreateFilter + | UpdateFilter + | SaveFilter + | SaveFilterSuccess + | SaveFilterError + | CancelCreateFilter; diff --git a/ui/src/app/metadata-filter/component/filter-form.component.html b/ui/src/app/metadata-filter/component/filter-form.component.html new file mode 100644 index 000000000..4609c6f85 --- /dev/null +++ b/ui/src/app/metadata-filter/component/filter-form.component.html @@ -0,0 +1,143 @@ +
+
+
+
+
+
+ + + Filter Name + + +
+ + + + Filter Name is required + + +
+
+
+ + + Search Entity ID + + +
+ + + + + Entity ID is required + Entity ID not found + + +
+
+
+
+ + + +
+
+
+
+
+
+
+
+ + + XML Preview + + +
+ +
+
+
+
+
+
+
+ + +
+ Enable this filter upon saving popover + +
+
+
+
+
+ +
+
+ +
+
+
+
diff --git a/ui/src/app/metadata-filter/component/filter-form.component.scss b/ui/src/app/metadata-filter/component/filter-form.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/metadata-filter/component/filter-form.component.spec.ts b/ui/src/app/metadata-filter/component/filter-form.component.spec.ts new file mode 100644 index 000000000..30790e2cc --- /dev/null +++ b/ui/src/app/metadata-filter/component/filter-form.component.spec.ts @@ -0,0 +1,73 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { ReactiveFormsModule, FormBuilder } from '@angular/forms'; +import { StoreModule, Store, combineReducers } from '@ngrx/store'; +import { FilterFormComponent } from './filter-form.component'; +import * as fromFilter from '../reducer'; +import { ProviderEditorFormModule } from '../../metadata-provider/component'; +import { ProviderStatusEmitter, ProviderValueEmitter } from '../../metadata-provider/service/provider-change-emitter.service'; +import { NgbPopoverModule, NgbPopoverConfig, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; +import { NavigatorService } from '../../core/service/navigator.service'; +import { SharedModule } from '../../shared/shared.module'; + +describe('Metadata Filter Form Component', () => { + let fixture: ComponentFixture; + let store: Store; + let instance: FilterFormComponent; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + ProviderStatusEmitter, + ProviderValueEmitter, + FormBuilder, + NgbPopoverConfig, + NavigatorService + ], + imports: [ + StoreModule.forRoot({ + 'metadata-filter': combineReducers(fromFilter.reducers), + }), + ReactiveFormsModule, + ProviderEditorFormModule, + NgbPopoverModule, + NgbModalModule, + SharedModule + ], + declarations: [FilterFormComponent], + }); + + fixture = TestBed.createComponent(FilterFormComponent); + instance = fixture.componentInstance; + store = TestBed.get(Store); + + spyOn(store, 'dispatch').and.callThrough(); + }); + + it('should compile', () => { + fixture.detectChanges(); + + expect(fixture).toBeDefined(); + }); + + describe('searchEntityIds method', () => { + it('should NOT dispatch a loadEntityIds action until there are 4 or more characters', () => { + fixture.detectChanges(); + instance.searchEntityIds('foo'); + expect(store.dispatch).not.toHaveBeenCalled(); + }); + + it('should dispatch a loadEntityIds action', () => { + fixture.detectChanges(); + instance.searchEntityIds('foo-'); + expect(store.dispatch).toHaveBeenCalled(); + }); + }); + + describe('onViewMore method', () => { + it('should dispatch a viewMoreEntityIds action', () => { + fixture.detectChanges(); + instance.onViewMore('foo'); + expect(store.dispatch).toHaveBeenCalled(); + }); + }); +}); diff --git a/ui/src/app/metadata-filter/component/filter-form.component.ts b/ui/src/app/metadata-filter/component/filter-form.component.ts new file mode 100644 index 000000000..d00213d74 --- /dev/null +++ b/ui/src/app/metadata-filter/component/filter-form.component.ts @@ -0,0 +1,93 @@ +import { Component, OnInit, OnChanges, OnDestroy, Input, Output, EventEmitter, SimpleChanges } from '@angular/core'; +import { FormBuilder, FormArray, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/observable/of'; +import 'rxjs/add/operator/takeWhile'; + +import * as fromFilter from '../reducer'; +import { ProviderFormFragmentComponent } from '../../metadata-provider/component/forms/provider-form-fragment.component'; +import { ProviderStatusEmitter, ProviderValueEmitter } from '../../metadata-provider/service/provider-change-emitter.service'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { SearchDialogComponent } from '../component/search-dialog.component'; +import { ViewMoreIds, CancelCreateFilter, QueryEntityIds } from '../action/filter.action'; +import { EntityValidators } from '../../metadata-provider/service/entity-validators.service'; +import { MetadataProvider } from '../../metadata-provider/model/metadata-provider'; + +@Component({ + selector: 'filter-form', + templateUrl: './filter-form.component.html' +}) +export class FilterFormComponent extends ProviderFormFragmentComponent implements OnInit, OnDestroy { + + @Input() provider: MetadataProvider; + + @Output() onSave: EventEmitter = new EventEmitter(); + @Output() onCancel: EventEmitter = new EventEmitter(); + @Output() onPreview: EventEmitter = new EventEmitter(); + + ids: string[]; + entityIds$: Observable; + showMore$: Observable; + selected$: Observable; + loading$: Observable; + processing$: Observable; + + nameIdFormatList: FormArray; + authenticationMethodList: FormArray; + + constructor( + private store: Store, + protected statusEmitter: ProviderStatusEmitter, + protected valueEmitter: ProviderValueEmitter, + protected fb: FormBuilder + ) { + super(fb, statusEmitter, valueEmitter); + + this.showMore$ = this.store.select(fromFilter.getViewingMore); + this.selected$ = this.store.select(fromFilter.getSelected); + this.entityIds$ = this.store.select(fromFilter.getEntityCollection); + this.loading$ = this.store.select(fromFilter.getIsLoading); + this.processing$ = this.loading$.withLatestFrom(this.showMore$, (l, s) => !s && l); + + this.entityIds$.subscribe(ids => this.ids = ids); + + this.selected$.subscribe(s => { + this.form.patchValue({ + entityId: s + }); + }); + } + + createForm(): void { + this.form = this.fb.group({ + entityId: ['', [Validators.required]], + serviceProviderName: ['', [Validators.required]], + filterEnabled: [false] + }); + } + + ngOnInit(): void { + super.ngOnInit(); + let id = this.form.get('entityId'); + id.setAsyncValidators([EntityValidators.existsInCollection(this.entityIds$)]); + id.valueChanges + .distinctUntilChanged() + .subscribe(query => this.searchEntityIds(query)); + } + + ngOnDestroy(): void { } + + searchEntityIds(term: string): void { + if (term.length >= 4 && this.ids.indexOf(term) < 0) { + this.store.dispatch(new QueryEntityIds({ + term, + limit: 10 + })); + } + } + + onViewMore(query: string): void { + this.store.dispatch(new ViewMoreIds(query)); + } +} diff --git a/ui/src/app/metadata-filter/component/preview-filter.component.html b/ui/src/app/metadata-filter/component/preview-filter.component.html new file mode 100644 index 000000000..ecf5de875 --- /dev/null +++ b/ui/src/app/metadata-filter/component/preview-filter.component.html @@ -0,0 +1,14 @@ + + + \ No newline at end of file diff --git a/ui/src/app/metadata-filter/component/preview-filter.component.ts b/ui/src/app/metadata-filter/component/preview-filter.component.ts new file mode 100644 index 000000000..560321a9c --- /dev/null +++ b/ui/src/app/metadata-filter/component/preview-filter.component.ts @@ -0,0 +1,23 @@ +import { Component, AfterViewInit, Input, OnInit, SimpleChange, SimpleChanges } from '@angular/core'; +import { NgbModal, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { Observable } from 'rxjs/Observable'; +import { Store } from '@ngrx/store'; + +import * as fromFilter from '../reducer'; +import { QueryEntityIds } from '../action/filter.action'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { MetadataProvider } from '../../metadata-provider/model/metadata-provider'; + +@Component({ + selector: 'preview-filter', + templateUrl: './preview-filter.component.html' +}) +export class PreviewFilterComponent { + filter$: Observable; + constructor( + public activeModal: NgbActiveModal, + private store: Store + ) { + this.filter$ = this.store.select(fromFilter.getFilter); + } +} diff --git a/ui/src/app/metadata-filter/component/search-dialog.component.html b/ui/src/app/metadata-filter/component/search-dialog.component.html index 447f7312e..ea20d5ce0 100644 --- a/ui/src/app/metadata-filter/component/search-dialog.component.html +++ b/ui/src/app/metadata-filter/component/search-dialog.component.html @@ -19,8 +19,12 @@