Skip to content

Commit

Permalink
Showing 20 changed files with 171 additions and 50 deletions.
@@ -4,6 +4,7 @@
import edu.internet2.tier.shibboleth.admin.ui.domain.exceptions.MetadataFileNotFoundException;
import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver;
import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@@ -12,6 +13,7 @@
import org.springframework.web.client.HttpClientErrorException;

import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.springframework.http.HttpStatus.NOT_FOUND;

/**
@@ -42,6 +44,11 @@ public ResponseEntity<?> notFoundHandler(HttpClientErrorException ex) {
throw ex;
}

@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ErrorResponse> handleDatabaseConstraintViolation(ConstraintViolationException ex) {
return ResponseEntity.status(BAD_REQUEST).body(new ErrorResponse("400", "message.database-constraint"));
}

@ExceptionHandler(Exception.class)
public final ResponseEntity<ErrorResponse> handleAllOtherExceptions(Exception ex) {
ErrorResponse errorResponse = new ErrorResponse("400", ex.getLocalizedMessage());
1 change: 1 addition & 0 deletions backend/src/main/resources/i18n/messages.properties
@@ -398,6 +398,7 @@ message.entity-id-min-unique=You must add at least one entity id target and they
message.required-for-scripts=Required for Scripts
message.required-for-regex=Required for Regex
message.file-doesnt-exist=The requested file to be processed does not exist on the server.
message.database-constraint=There was a database constraint problem processing the request. Check the request to ensure that fields that must be unique are truly unique.

tooltip.entity-id=Entity ID
tooltip.service-provider-name=Service Provider Name (Dashboard Display Only)
1 change: 1 addition & 0 deletions backend/src/main/resources/i18n/messages_en.properties
@@ -404,6 +404,7 @@ message.entity-id-min-unique=You must add at least one entity id target and they
message.required-for-scripts=Required for Scripts
message.required-for-regex=Required for Regex
message.file-doesnt-exist=The requested file to be processed does not exist on the server.
message.database-constraint=There was a database constraint problem processing the request. Check the request to ensure that fields that must be unique are truly unique.

tooltip.entity-id=Entity ID
tooltip.service-provider-name=Service Provider Name (Dashboard Display Only)
@@ -28,7 +28,7 @@
<sf-form
[schema]="schema$ | async"
[model]="model$ | async"
[validators]="definition.getValidators()"
[validators]="validators$ | async"
[actions]="actions"
(onChange)="valueChangeSubject.next($event)"
(onErrorChange)="statusChangeSubject.next($event)"></sf-form>
13 changes: 12 additions & 1 deletion ui/src/app/metadata/filter/container/edit-filter.component.ts
@@ -11,7 +11,7 @@ import { UpdateFilterRequest } from '../action/collection.action';
import { CancelCreateFilter, UpdateFilterChanges } from '../action/filter.action';
import { PreviewEntity } from '../../domain/action/entity.action';
import { EntityAttributesFilterEntity } from '../../domain/entity';
import { shareReplay } from 'rxjs/operators';
import { shareReplay, map, withLatestFrom } from 'rxjs/operators';

@Component({
selector: 'edit-filter-page',
@@ -33,6 +33,8 @@ export class EditFilterComponent {
filter: MetadataFilter;
isValid: boolean;

validators$: Observable<{ [key: string]: any }>;

actions: any;

constructor(
@@ -50,6 +52,15 @@ export class EditFilterComponent {
this.isValid = valid.value ? valid.value.length === 0 : true;
});

this.validators$ = this.store.select(fromFilter.getFilterNames).pipe(
withLatestFrom(
this.store.select(fromFilter.getSelectedFilter)
),
map(([names, provider]) => this.definition.getValidators(
names.filter(n => n !== provider.name)
))
);

this.store
.select(fromFilter.getFilter)
.subscribe(filter => this.filter = filter);
2 changes: 1 addition & 1 deletion ui/src/app/metadata/filter/container/filter.component.html
@@ -1,3 +1,3 @@
<ng-container *ngIf="filter$ | async">
<ng-container>
<router-outlet></router-outlet>
</ng-container>
16 changes: 6 additions & 10 deletions ui/src/app/metadata/filter/container/filter.component.ts
@@ -3,35 +3,31 @@ import { ActivatedRoute } from '@angular/router';
import { Observable, Subscription } from 'rxjs';
import { distinctUntilChanged, map } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { NgbPopoverConfig } from '@ng-bootstrap/ng-bootstrap';

import { MetadataFilter } from '../../domain/model/metadata-filter';
import { SelectFilter } from '../action/collection.action';
import { LoadFilterRequest } from '../action/collection.action';
import * as fromFilter from '../reducer';


@Component({
selector: 'filter-page',
templateUrl: './filter.component.html',
styleUrls: ['./filter.component.scss'],
providers: [NgbPopoverConfig]
styleUrls: [],
providers: []
})
export class FilterComponent implements OnDestroy {
actionsSubscription: Subscription;
filter$: Observable<MetadataFilter>;
filters$: Observable<MetadataFilter[]>;

constructor(
private store: Store<fromFilter.State>,
private route: ActivatedRoute
) {
this.actionsSubscription = this.route.params.pipe(
this.actionsSubscription = this.route.parent.params.pipe(
distinctUntilChanged(),
map(params => {
return new SelectFilter(params.id);
return new LoadFilterRequest(params.providerId);
})
).subscribe(store);

this.filter$ = this.store.select(fromFilter.getSelectedFilter);
}

ngOnDestroy() {
@@ -28,7 +28,7 @@
<sf-form
[schema]="schema$ | async"
[model]="model$ | async"
[validators]="definition.getValidators()"
[validators]="validators$ | async"
(onChange)="valueChangeSubject.next($event)"
(onErrorChange)="statusChangeSubject.next($event)"></sf-form>
</div>
8 changes: 7 additions & 1 deletion ui/src/app/metadata/filter/container/new-filter.component.ts
@@ -1,7 +1,7 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { Subject, Observable, of } from 'rxjs';
import { takeUntil, shareReplay } from 'rxjs/operators';
import { takeUntil, shareReplay, withLatestFrom, map, filter } from 'rxjs/operators';

import * as fromFilter from '../reducer';
import { MetadataFilterTypes } from '../model';
@@ -33,6 +33,8 @@ export class NewFilterComponent implements OnDestroy, OnInit {
filter: MetadataFilter;
isValid: boolean;

validators$: Observable<{ [key: string]: any }>;

constructor(
private store: Store<fromFilter.State>,
private schemaService: SchemaService
@@ -42,6 +44,10 @@ export class NewFilterComponent implements OnDestroy, OnInit {
this.schema$ = this.schemaService.get(this.definition.schema).pipe(shareReplay());
this.isSaving$ = this.store.select(fromFilter.getCollectionSaving);
this.model$ = of(<MetadataFilter>{});

this.validators$ = this.store.select(fromFilter.getFilterNames).pipe(
map((names) => this.definition.getValidators(names))
);
}

ngOnInit(): void {
@@ -0,0 +1,3 @@
<ng-container *ngIf="filter$ | async">
<router-outlet></router-outlet>
</ng-container>
Empty file.
39 changes: 39 additions & 0 deletions ui/src/app/metadata/filter/container/select-filter.component.ts
@@ -0,0 +1,39 @@
import { Component, OnDestroy } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable, Subscription } from 'rxjs';
import { distinctUntilChanged, map } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { NgbPopoverConfig } from '@ng-bootstrap/ng-bootstrap';

import { MetadataFilter } from '../../domain/model/metadata-filter';
import { SelectFilter } from '../action/collection.action';
import * as fromFilter from '../reducer';

@Component({
selector: 'select-filter-page',
templateUrl: './select-filter.component.html',
styleUrls: ['./select-filter.component.scss'],
providers: [NgbPopoverConfig]
})
export class SelectFilterComponent implements OnDestroy {
actionsSubscription: Subscription;
filter$: Observable<MetadataFilter>;

constructor(
private store: Store<fromFilter.State>,
private route: ActivatedRoute
) {
this.actionsSubscription = this.route.params.pipe(
distinctUntilChanged(),
map(params => {
return new SelectFilter(params.id);
})
).subscribe(store);

this.filter$ = this.store.select(fromFilter.getSelectedFilter);
}

ngOnDestroy() {
this.actionsSubscription.unsubscribe();
}
} /* istanbul ignore next */
8 changes: 0 additions & 8 deletions ui/src/app/metadata/filter/effect/collection.effect.ts
@@ -166,14 +166,6 @@ export class FilterCollectionEffects {
)
);

/*
@Effect()
reloadOrderAfterChange$ = this.actions$.pipe(
ofType<SetOrderFilterSuccess>(FilterCollectionActionTypes.SET_ORDER_FILTER_SUCCESS),
map(() => new GetOrderFilterRequest())
);
*/

@Effect()
setOrder$ = this.actions$.pipe(
ofType<SetOrderFilterRequest>(FilterCollectionActionTypes.SET_ORDER_FILTER_REQUEST),
8 changes: 5 additions & 3 deletions ui/src/app/metadata/filter/filter.module.ts
@@ -13,21 +13,23 @@ import { NgbPopoverModule, NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
import { SearchDialogComponent } from './component/search-dialog.component';
import { SharedModule } from '../../shared/shared.module';
import { EditFilterComponent } from './container/edit-filter.component';
import { FilterComponent } from './container/filter.component';
import { SelectFilterComponent } from './container/select-filter.component';
import { SearchIdEffects } from './effect/search.effect';
import { FilterExistsGuard } from './guard/filter-exists.guard';
import { DomainModule } from '../domain/domain.module';
import { ModuleWithProviders } from '@angular/compiler/src/core';
import { FilterCollectionEffects } from './effect/collection.effect';
import { FormModule } from '../../schema-form/schema-form.module';
import { I18nModule } from '../../i18n/i18n.module';
import { FilterComponent } from './container/filter.component';

@NgModule({
declarations: [
NewFilterComponent,
EditFilterComponent,
FilterComponent,
SearchDialogComponent
SelectFilterComponent,
SearchDialogComponent,
FilterComponent
],
entryComponents: [
SearchDialogComponent
33 changes: 31 additions & 2 deletions ui/src/app/metadata/filter/model/entity-attributes.filter.spec.ts
@@ -1,8 +1,37 @@
import { EntityAttributesFilter } from './entity-attributes.filter';

describe('Entity Attributes filter form', () => {
it('should return an empty object for validators', () => {
expect(EntityAttributesFilter.getValidators()).toEqual({});
describe('getValidators', () => {
it('should return an empty object for validators', () => {
expect(Object.keys(EntityAttributesFilter.getValidators())).toEqual([
'/',
'/name'
]);
});

describe('name `/name` validator', () => {
const validators = EntityAttributesFilter.getValidators(['foo', 'bar']);

it('should return an invalid object when provided values are invalid based on name', () => {
expect(validators['/name']('foo', { path: '/name' })).toBeDefined();
});

it('should return null when provided values are valid based on name', () => {
expect(validators['/name']('baz', { path: '/name' })).toBeNull();
});
});

describe('parent `/` validator', () => {
const validators = EntityAttributesFilter.getValidators(['foo', 'bar']);

it('should return a list of child errors', () => {
expect(validators['/']({ name: 'foo' }, { path: '/name' }, {}).length).toBe(1);
});

it('should ignore properties that don\'t exist a list of child errors', () => {
expect(validators['/']({ foo: 'bar' }, { path: '/foo' }, {})).toBeUndefined();
});
});
});

describe('transformer', () => {
30 changes: 28 additions & 2 deletions ui/src/app/metadata/filter/model/entity-attributes.filter.ts
@@ -5,8 +5,34 @@ export const EntityAttributesFilter: FormDefinition<MetadataFilter> = {
label: 'EntityAttributes',
type: 'EntityAttributes',
schema: '/api/ui/EntityAttributesFilters',
getValidators(): any {
const validators = {};
getValidators(namesList: string[] = []): any {
const validators = {
'/': (value, property, form_current) => {
let errors;
// iterate all customer
Object.keys(value).forEach((key) => {
const item = value[key];
const validatorKey = `/${key}`;
const validator = validators.hasOwnProperty(validatorKey) ? validators[validatorKey] : null;
const error = validator ? validator(item, { path: `/${key}` }, form_current) : null;
if (error) {
errors = errors || [];
errors.push(error);
}
});
return errors;
},
'/name': (value, property, form) => {
console.log(namesList);
const err = namesList.indexOf(value) > -1 ? {
code: 'INVALID_NAME',
path: `#${property.path}`,
message: 'message.name-must-be-unique',
params: [value]
} : null;
return err;
}
};
return validators;
},
parser: (changes: any): MetadataFilter => changes,
3 changes: 3 additions & 0 deletions ui/src/app/metadata/filter/reducer/index.ts
@@ -4,6 +4,7 @@ import * as fromFilter from './filter.reducer';
import * as fromSearch from './search.reducer';
import * as fromCollection from './collection.reducer';
import * as utils from '../../domain/domain.util';
import { MetadataFilter } from '../../domain/model';

export interface FilterState {
filter: fromFilter.FilterState;
@@ -71,6 +72,8 @@ export const getAdditionalFilterOrder = createSelector(getFilterEntities, getCol
export const getAdditionalFilters = createSelector(getFilterList, getAdditionalFilterOrder, utils.mergeOrderFn);
export const getPluginFilterOrder = createSelector(getFilterEntities, getCollectionOrder, pluginOrderFn);

export const getFilterNames = createSelector(getAllFilters, (filters: MetadataFilter[]) => filters.map(f => f.name).filter(f => !!f));

/*
* Combine pieces of State
*/
@@ -31,8 +31,6 @@ export class ProviderSelectComponent implements OnDestroy {
map(params => new SelectProviderRequest(params.providerId))
).subscribe(store);

this.route.params.subscribe(params => console.log(params));

this.provider$ = this.store.select(fromProviders.getSelectedProvider).pipe(skipWhile(p => !p));

this.provider$.subscribe(provider => this.setDefinition(provider));
43 changes: 25 additions & 18 deletions ui/src/app/metadata/provider/provider.routing.ts
@@ -8,9 +8,10 @@ import { ProviderEditStepComponent } from './container/provider-edit-step.compon
import { ProviderSelectComponent } from './container/provider-select.component';
import { ProviderFilterListComponent } from './container/provider-filter-list.component';
import { NewFilterComponent } from '../filter/container/new-filter.component';
import { FilterComponent } from '../filter/container/filter.component';
import { SelectFilterComponent } from '../filter/container/select-filter.component';
import { EditFilterComponent } from '../filter/container/edit-filter.component';
import { CanDeactivateGuard } from '../../core/service/can-deactivate.guard';
import { FilterComponent } from '../filter/container/filter.component';

export const ProviderRoutes: Routes = [
{
@@ -37,36 +38,42 @@ export const ProviderRoutes: Routes = [
component: ProviderSelectComponent,
children: [
{
path: 'edit',
component: ProviderEditComponent,
path: '',
component: FilterComponent,
children: [
{ path: '', redirectTo: 'common', pathMatch: 'prefix' },
{
path: ':form',
component: ProviderEditStepComponent
path: 'filter/new',
component: NewFilterComponent
},
{
path: 'filter/:id',
component: SelectFilterComponent,
canActivate: [],
children: [
{
path: 'edit',
component: EditFilterComponent
}
]
}
],
canDeactivate: [
CanDeactivateGuard
]
},
{
path: 'filters',
component: ProviderFilterListComponent
},
{
path: 'filter/new',
component: NewFilterComponent
},
{
path: 'filter/:id',
component: FilterComponent,
canActivate: [],
path: 'edit',
component: ProviderEditComponent,
children: [
{ path: '', redirectTo: 'common', pathMatch: 'prefix' },
{
path: 'edit',
component: EditFilterComponent
path: ':form',
component: ProviderEditStepComponent
}
],
canDeactivate: [
CanDeactivateGuard
]
}
]

0 comments on commit e0fcb55

Please sign in to comment.