diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ErrorResponse.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ErrorResponse.java
new file mode 100644
index 000000000..fa91aa3e6
--- /dev/null
+++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ErrorResponse.java
@@ -0,0 +1,18 @@
+package edu.internet2.tier.shibboleth.admin.ui.controller;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+/**
+ * @author Bill Smith (wsmith@unicon.net)
+ */
+@AllArgsConstructor
+@Getter
+@Setter
+@ToString
+public class ErrorResponse {
+ private String errorCode;
+ private String errorMessage;
+}
diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java
index ae75b3f6d..5b24f5788 100644
--- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java
+++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java
@@ -1,15 +1,16 @@
package edu.internet2.tier.shibboleth.admin.ui.controller;
+import com.fasterxml.jackson.databind.exc.InvalidTypeIdException;
import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver;
import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolverValidationService;
-import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolverValidator;
import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository;
-
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
+import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
@@ -19,6 +20,7 @@
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
+import java.io.IOException;
import java.net.URI;
import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolverValidator.ValidationResult;
@@ -34,6 +36,11 @@ public class MetadataResolversController {
@Autowired
MetadataResolverValidationService metadataResolverValidationService;
+ @ExceptionHandler({InvalidTypeIdException.class, IOException.class, HttpMessageNotReadableException.class})
+ public ResponseEntity> unableToParseJson(Exception ex) {
+ return ResponseEntity.badRequest().body(new ErrorResponse(HttpStatus.BAD_REQUEST.toString(), ex.getMessage()));
+ }
+
@GetMapping("/MetadataResolvers")
@Transactional(readOnly = true)
public ResponseEntity> getAll() {
@@ -56,6 +63,10 @@ public ResponseEntity> getOne(@PathVariable String resourceId) {
@PostMapping("/MetadataResolvers")
@Transactional
public ResponseEntity> create(@RequestBody MetadataResolver newResolver) {
+ if (resolverRepository.findByName(newResolver.getName()) != null) {
+ return ResponseEntity.status(HttpStatus.CONFLICT).build();
+ }
+
//TODO: we are disregarding attached filters if any sent from UI.
//Only deal with filters via filters endpoints?
newResolver.clearAllFilters();
diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolverValidationService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolverValidationService.java
index 26039b81b..4151faeb6 100644
--- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolverValidationService.java
+++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolverValidationService.java
@@ -8,7 +8,7 @@
/**
* A facade that aggregates {@link MetadataResolverValidator}s available to call just one of them supporting the type of a given resolver.
- * If no {@link MetadataResolverValidator}s are configured, conciders provided MetadataResolver as valid.
+ * If no {@link MetadataResolverValidator}s are configured, considers provided MetadataResolver as valid.
*
* Uses chain-of-responsibility design pattern
*
@@ -22,7 +22,7 @@ public MetadataResolverValidationService(List> vali
this.validators = validators != null ? validators : new ArrayList<>();
}
- @SuppressWarnings("Uncheked")
+ @SuppressWarnings("Unchecked")
public ValidationResult validateIfNecessary(T metadataResolver) {
Optional> validator =
this.validators
diff --git a/ui/src/app/metadata/domain/domain.module.ts b/ui/src/app/metadata/domain/domain.module.ts
index 3349342d9..c51bbb260 100644
--- a/ui/src/app/metadata/domain/domain.module.ts
+++ b/ui/src/app/metadata/domain/domain.module.ts
@@ -12,6 +12,7 @@ import { EntityDraftService } from './service/draft.service';
import { MetadataProviderService } from './service/provider.service';
import { EntityEffects } from './effect/entity.effect';
import { PreviewDialogComponent } from './component/preview-dialog.component';
+import { MetadataFilterService } from './service/filter.service';
export const COMPONENTS = [
PreviewDialogComponent
@@ -42,7 +43,8 @@ export class DomainModule {
ListValuesService,
ProviderStatusEmitter,
ProviderValueEmitter,
- MetadataProviderService
+ MetadataProviderService,
+ MetadataFilterService
]
};
}
diff --git a/ui/src/app/metadata/domain/domain.type.ts b/ui/src/app/metadata/domain/domain.type.ts
index 78def16d7..3cda36b83 100644
--- a/ui/src/app/metadata/domain/domain.type.ts
+++ b/ui/src/app/metadata/domain/domain.type.ts
@@ -6,9 +6,11 @@ import {
import {
EntityAttributesFilter,
- FileBackedHttpMetadataResolver,
- FileBackedHttpMetadataProvider
+ FileBackedHttpMetadataResolver
} from './entity';
+import {
+ FileBackedHttpMetadataProvider
+} from './model/providers';
export type Filter =
| EntityAttributesFilter;
diff --git a/ui/src/app/metadata/domain/entity/index.ts b/ui/src/app/metadata/domain/entity/index.ts
index 1adb3735e..648255369 100644
--- a/ui/src/app/metadata/domain/entity/index.ts
+++ b/ui/src/app/metadata/domain/entity/index.ts
@@ -1,3 +1,2 @@
export * from './filter/entity-attributes-filter';
-export * from './provider/file-backed-http-metadata-provider';
export * from './resolver/file-backed-http-metadata-resolver';
diff --git a/ui/src/app/metadata/domain/entity/provider/file-backed-http-metadata-provider.spec.ts b/ui/src/app/metadata/domain/entity/provider/file-backed-http-metadata-provider.spec.ts
deleted file mode 100644
index 70b0245a4..000000000
--- a/ui/src/app/metadata/domain/entity/provider/file-backed-http-metadata-provider.spec.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-import { FileBackedHttpMetadataProvider } from './file-backed-http-metadata-provider';
-
-describe('FileBackedHttmMetadataProvider construct', () => {
-
- const config = {
- id: 'foo',
- entityId: 'string',
- serviceProviderName: 'string',
- organization: {
- 'name': 'string',
- 'displayName': 'string',
- 'url': 'string'
- },
- contacts: [
- {
- 'name': 'string',
- 'type': 'string',
- 'emailAddress': 'string'
- }
- ],
- mdui: {
- 'displayName': 'string',
- 'informationUrl': 'string',
- 'privacyStatementUrl': 'string',
- 'logoUrl': 'string',
- 'logoHeight': 100,
- 'logoWidth': 100,
- 'description': 'string'
- },
- securityInfo: {
- 'x509CertificateAvailable': true,
- 'authenticationRequestsSigned': true,
- 'wantAssertionsSigned': true,
- 'x509Certificates': [
- {
- 'name': 'string',
- 'type': 'string',
- 'value': 'string'
- }
- ]
- },
- assertionConsumerServices: [
- {
- 'binding': 'string',
- 'locationUrl': 'string',
- 'makeDefault': true
- }
- ],
- serviceProviderSsoDescriptor: {
- 'protocolSupportEnum': 'string',
- 'nameIdFormats': [
- 'string'
- ]
- },
-
- logoutEndpoints: [
- {
- 'url': 'string',
- 'bindingType': 'string'
- }
- ],
- serviceEnabled: true,
- createdDate: new Date().toDateString(),
- modifiedDate: new Date().toDateString(),
- relyingPartyOverrides: {
- 'signAssertion': true,
- 'dontSignResponse': true,
- 'turnOffEncryption': true,
- 'useSha': true,
- 'ignoreAuthenticationMethod': true,
- 'omitNotBefore': true,
- 'responderId': 'string',
- 'nameIdFormats': [
- 'string'
- ],
- 'authenticationMethods': [
- 'string'
- ]
- },
- attributeRelease: [
- 'eduPersonPrincipalName',
- 'uid',
- 'mail'
- ]
- };
-
- let entity;
-
- beforeEach(() => {
- entity = new FileBackedHttpMetadataProvider(config);
- });
-
- it('should populate its own values', () => {
- Object.keys(config).forEach(key => {
- expect(entity[key]).toEqual(config[key]);
- });
- });
-
- describe('interface methods', () => {
- it('should return a date object from getCreationDate', () => {
- expect(entity.getCreationDate()).toEqual(new Date(config.createdDate));
- });
- });
-});
diff --git a/ui/src/app/metadata/domain/entity/provider/file-backed-http-metadata-provider.ts b/ui/src/app/metadata/domain/entity/provider/file-backed-http-metadata-provider.ts
deleted file mode 100644
index 2f64a98ba..000000000
--- a/ui/src/app/metadata/domain/entity/provider/file-backed-http-metadata-provider.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import {
- MetadataProvider
-} from '../../model';
-
-export class FileBackedHttpMetadataProvider implements MetadataProvider {
- id: string;
- name: string;
- '@type': string;
-
- createdDate?: string;
- modifiedDate?: string;
- version: string;
-
- constructor(descriptor?: Partial) {
- Object.assign(this, descriptor);
- }
-
- getCreationDate(): Date {
- return new Date(this.createdDate);
- }
-}
diff --git a/ui/src/app/metadata/domain/model/metadata-provider.ts b/ui/src/app/metadata/domain/model/metadata-provider.ts
index 3239bd4b0..4dc283368 100644
--- a/ui/src/app/metadata/domain/model/metadata-provider.ts
+++ b/ui/src/app/metadata/domain/model/metadata-provider.ts
@@ -1,16 +1,10 @@
import {
MetadataBase,
- Organization,
- Contact,
- MDUI,
- SecurityInfo,
- SsoService,
- IdpSsoDescriptor,
- LogoutEndpoint,
- RelyingPartyOverrides
} from '../model';
export interface MetadataProvider extends MetadataBase {
name: string;
'@type': string;
+ enabled: boolean;
+ resourceId: string;
}
diff --git a/ui/src/app/metadata/domain/model/providers/file-backed-http-metadata-provider.ts b/ui/src/app/metadata/domain/model/providers/file-backed-http-metadata-provider.ts
new file mode 100644
index 000000000..7d107036b
--- /dev/null
+++ b/ui/src/app/metadata/domain/model/providers/file-backed-http-metadata-provider.ts
@@ -0,0 +1,5 @@
+import { MetadataProvider } from '../metadata-provider';
+
+export interface FileBackedHttpMetadataProvider extends MetadataProvider {
+ metadataFilters: any[];
+}
diff --git a/ui/src/app/metadata/domain/model/providers/index.ts b/ui/src/app/metadata/domain/model/providers/index.ts
new file mode 100644
index 000000000..0cd02d47d
--- /dev/null
+++ b/ui/src/app/metadata/domain/model/providers/index.ts
@@ -0,0 +1 @@
+export * from './file-backed-http-metadata-provider';
\ No newline at end of file
diff --git a/ui/src/app/metadata/domain/service/filter.service.spec.ts b/ui/src/app/metadata/domain/service/filter.service.spec.ts
new file mode 100644
index 000000000..1410f6723
--- /dev/null
+++ b/ui/src/app/metadata/domain/service/filter.service.spec.ts
@@ -0,0 +1,74 @@
+import { TestBed, async, inject } from '@angular/core/testing';
+import { HttpTestingController, HttpClientTestingModule } from '@angular/common/http/testing';
+import { HttpClientModule, HttpRequest } from '@angular/common/http';
+import { MetadataFilterService } from './filter.service';
+import { EntityAttributesFilter } from '../entity';
+
+describe(`Metadata Filter Service`, () => {
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ HttpClientModule,
+ HttpClientTestingModule
+ ],
+ providers: [
+ MetadataFilterService
+ ]
+ });
+ });
+
+ describe('query method', () => {
+ it(`should send an expected GET[] request`, async(inject([MetadataFilterService, HttpTestingController],
+ (service: MetadataFilterService, backend: HttpTestingController) => {
+ service.query().subscribe();
+
+ backend.expectOne((req: HttpRequest) => {
+ return req.url === `${service.base}${service.endpoint}`
+ && req.method === 'GET';
+ }, `GET MetadataResolvers collection`);
+ }
+ )));
+ });
+ describe('find method', () => {
+ it(`should send an expected GET request`, async(inject([MetadataFilterService, HttpTestingController],
+ (service: MetadataFilterService, backend: HttpTestingController) => {
+ const id = 'foo';
+ service.find(id).subscribe();
+
+ backend.expectOne((req: HttpRequest) => {
+ return req.url === `${service.base}${service.endpoint}/${id}`
+ && req.method === 'GET';
+ }, `GET MetadataResolvers collection`);
+ }
+ )));
+ });
+ describe('update method', () => {
+ it(`should send an expected PUT request`, async(inject([MetadataFilterService, HttpTestingController],
+ (service: MetadataFilterService, backend: HttpTestingController) => {
+ const id = 'foo';
+ const filter = new EntityAttributesFilter({ id });
+ service.update(filter).subscribe();
+
+ backend.expectOne((req: HttpRequest) => {
+ return req.url === `${service.base}${service.endpoint}/${id}`
+ && req.method === 'PUT';
+ }, `PUT (update) MetadataResolvers collection`);
+ }
+ )));
+ });
+ describe('save method', () => {
+ it(`should send an expected POST request`, async(inject([MetadataFilterService, HttpTestingController],
+ (service: MetadataFilterService, backend: HttpTestingController) => {
+ const id = 'foo';
+ const filter = new EntityAttributesFilter({ id });
+ service.save(filter).subscribe();
+
+ backend.expectOne((req: HttpRequest) => {
+ return req.url === `${service.base}${service.endpoint}`
+ && req.method === 'POST';
+ }, `POST MetadataResolvers collection`);
+ }
+ )));
+ });
+});
diff --git a/ui/src/app/metadata/domain/service/filter.service.ts b/ui/src/app/metadata/domain/service/filter.service.ts
new file mode 100644
index 000000000..b96907eca
--- /dev/null
+++ b/ui/src/app/metadata/domain/service/filter.service.ts
@@ -0,0 +1,32 @@
+import { Injectable } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import { Observable } from 'rxjs';
+
+import { MetadataFilter } from '../../domain/model';
+
+@Injectable()
+export class MetadataFilterService {
+
+ readonly endpoint = '/MetadataResolvers';
+ readonly base = '/api';
+
+ constructor(
+ private http: HttpClient
+ ) { }
+ query(): Observable {
+ return this.http.get(`${this.base}${this.endpoint}`, {});
+ }
+
+ find(id: string): Observable {
+ // console.log(id);
+ return this.http.get(`${this.base}${this.endpoint}/${id}`);
+ }
+
+ update(filter: MetadataFilter): Observable {
+ return this.http.put(`${this.base}${this.endpoint}/${filter.id}`, filter);
+ }
+
+ save(filter: MetadataFilter): Observable {
+ return this.http.post(`${this.base}${this.endpoint}`, filter);
+ }
+}
diff --git a/ui/src/app/metadata/domain/service/provider.service.spec.ts b/ui/src/app/metadata/domain/service/provider.service.spec.ts
index 814c413ac..356bf5964 100644
--- a/ui/src/app/metadata/domain/service/provider.service.spec.ts
+++ b/ui/src/app/metadata/domain/service/provider.service.spec.ts
@@ -2,7 +2,7 @@ import { TestBed, async, inject } from '@angular/core/testing';
import { HttpTestingController, HttpClientTestingModule } from '@angular/common/http/testing';
import { HttpClientModule, HttpRequest } from '@angular/common/http';
import { MetadataProviderService } from './provider.service';
-import { EntityAttributesFilter } from '../entity';
+import { MetadataProvider } from '../model';
describe(`Metadata Provider Service`, () => {
@@ -47,8 +47,8 @@ describe(`Metadata Provider Service`, () => {
it(`should send an expected PUT request`, async(inject([MetadataProviderService, HttpTestingController],
(service: MetadataProviderService, backend: HttpTestingController) => {
const id = 'foo';
- const filter = new EntityAttributesFilter({id});
- service.update(filter).subscribe();
+ const provider = { resourceId: id };
+ service.update(provider).subscribe();
backend.expectOne((req: HttpRequest) => {
return req.url === `${service.base}${service.endpoint}/${id}`
@@ -61,8 +61,8 @@ describe(`Metadata Provider Service`, () => {
it(`should send an expected POST request`, async(inject([MetadataProviderService, HttpTestingController],
(service: MetadataProviderService, backend: HttpTestingController) => {
const id = 'foo';
- const filter = new EntityAttributesFilter({ id });
- service.save(filter).subscribe();
+ const provider = { resourceId: id };
+ service.save(provider).subscribe();
backend.expectOne((req: HttpRequest) => {
return req.url === `${service.base}${service.endpoint}`
diff --git a/ui/src/app/metadata/domain/service/provider.service.ts b/ui/src/app/metadata/domain/service/provider.service.ts
index 0644e4ab8..d82ee3d4b 100644
--- a/ui/src/app/metadata/domain/service/provider.service.ts
+++ b/ui/src/app/metadata/domain/service/provider.service.ts
@@ -2,31 +2,33 @@ import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
-import { MetadataFilter } from '../../domain/model';
+import { MetadataProvider } from '../../domain/model';
+import { FileBackedHttpMetadataProvider } from '../model/providers';
@Injectable()
export class MetadataProviderService {
- readonly endpoint = '/MetadataResolver/incommon/Filters';
+ readonly endpoint = '/MetadataResolvers';
readonly base = '/api';
constructor(
private http: HttpClient
) {}
- query(): Observable {
- return this.http.get(`${this.base}${this.endpoint}`, {});
+ query(): Observable {
+ return this.http.get(`${this.base}${this.endpoint}`, {});
}
- find(id: string): Observable {
+ find(id: string): Observable {
// console.log(id);
- return this.http.get(`${this.base}${this.endpoint}/${id}`);
+ return this.http.get(`${this.base}${this.endpoint}/${id}`);
}
- update(filter: MetadataFilter): Observable {
- return this.http.put(`${this.base}${this.endpoint}/${filter.id}`, filter);
+ update(provider: MetadataProvider): Observable {
+ return this.http.put(`${this.base}${this.endpoint}/${provider.resourceId}`, provider);
}
- save(filter: MetadataFilter): Observable {
- return this.http.post(`${this.base}${this.endpoint}`, filter);
+ save(provider: MetadataProvider): Observable {
+ const { metadataFilters, id, ...pruned } = provider as FileBackedHttpMetadataProvider;
+ return this.http.post(`${this.base}${this.endpoint}`, pruned);
}
}
diff --git a/ui/src/app/metadata/filter/effect/collection.effect.ts b/ui/src/app/metadata/filter/effect/collection.effect.ts
index da2a08a84..5422f2d99 100644
--- a/ui/src/app/metadata/filter/effect/collection.effect.ts
+++ b/ui/src/app/metadata/filter/effect/collection.effect.ts
@@ -9,11 +9,10 @@ import { switchMap, map, catchError, tap } from 'rxjs/operators';
import * as actions from '../action/collection.action';
import { FilterCollectionActionTypes } from '../action/collection.action';
import * as fromFilter from '../reducer';
-
-import { MetadataProviderService } from '../../domain/service/provider.service';
import { MetadataFilter } from '../../domain/model';
import { removeNulls } from '../../../shared/util';
import { EntityAttributesFilter } from '../../domain/entity/filter/entity-attributes-filter';
+import { MetadataFilterService } from '../../domain/service/filter.service';
/* istanbul ignore next */
@Injectable()
@@ -23,7 +22,7 @@ export class FilterCollectionEffects {
loadFilters$ = this.actions$.pipe(
ofType(FilterCollectionActionTypes.LOAD_FILTER_REQUEST),
switchMap(() =>
- this.resolverService
+ this.filterService
.query()
.pipe(
map(filters => new actions.LoadFilterSuccess(filters)),
@@ -36,7 +35,7 @@ export class FilterCollectionEffects {
ofType(FilterCollectionActionTypes.SELECT_FILTER),
map(action => action.payload),
switchMap(id => {
- return this.resolverService
+ return this.filterService
.find(id)
.pipe(
map(p => new actions.SelectFilterSuccess(p)),
@@ -57,7 +56,7 @@ export class FilterCollectionEffects {
};
}),
switchMap(unsaved =>
- this.resolverService
+ this.filterService
.save(unsaved as MetadataFilter)
.pipe(
map(saved => new actions.AddFilterSuccess(saved)),
@@ -86,7 +85,7 @@ export class FilterCollectionEffects {
switchMap(filter => {
delete filter.modifiedDate;
delete filter.createdDate;
- return this.resolverService
+ return this.filterService
.update(filter)
.pipe(
map(p => new actions.UpdateFilterSuccess({
@@ -113,7 +112,7 @@ export class FilterCollectionEffects {
constructor(
private actions$: Actions,
private router: Router,
- private resolverService: MetadataProviderService,
+ private filterService: MetadataFilterService,
private store: Store
) { }
}
diff --git a/ui/src/app/metadata/metadata.component.spec.ts b/ui/src/app/metadata/metadata.component.spec.ts
index fb9d5f329..dffc99361 100644
--- a/ui/src/app/metadata/metadata.component.spec.ts
+++ b/ui/src/app/metadata/metadata.component.spec.ts
@@ -17,7 +17,7 @@ class TestHostComponent {
public componentUnderTest: MetadataPageComponent;
}
-describe('AppComponent', () => {
+describe('Metadata Root Component', () => {
let fixture: ComponentFixture;
let instance: TestHostComponent;
@@ -48,8 +48,8 @@ describe('AppComponent', () => {
fixture.detectChanges();
}));
- it('should create the app', async(() => {
+ it('should load metadata objects', async(() => {
expect(app).toBeTruthy();
- expect(store.dispatch).toHaveBeenCalledTimes(3);
+ expect(store.dispatch).toHaveBeenCalledTimes(4);
}));
});
diff --git a/ui/src/app/metadata/metadata.component.ts b/ui/src/app/metadata/metadata.component.ts
index 52ca814e2..62170d286 100644
--- a/ui/src/app/metadata/metadata.component.ts
+++ b/ui/src/app/metadata/metadata.component.ts
@@ -5,6 +5,7 @@ import { LoadResolverRequest } from './resolver/action/collection.action';
import { LoadFilterRequest } from './filter/action/collection.action';
import { LoadDraftRequest } from './resolver/action/draft.action';
import * as fromRoot from '../app.reducer';
+import { LoadProviderRequest } from './provider/action/collection.action';
@Component({
selector: 'metadata-page',
@@ -20,5 +21,6 @@ export class MetadataPageComponent {
this.store.dispatch(new LoadResolverRequest());
this.store.dispatch(new LoadFilterRequest());
this.store.dispatch(new LoadDraftRequest());
+ this.store.dispatch(new LoadProviderRequest());
}
}
diff --git a/ui/src/app/metadata/provider/action/collection.action.ts b/ui/src/app/metadata/provider/action/collection.action.ts
new file mode 100644
index 000000000..99e71c758
--- /dev/null
+++ b/ui/src/app/metadata/provider/action/collection.action.ts
@@ -0,0 +1,107 @@
+import { Action } from '@ngrx/store';
+import { MetadataProvider } from '../../domain/model/metadata-provider';
+import { Update } from '@ngrx/entity';
+
+export enum ProviderCollectionActionTypes {
+ UPDATE_PROVIDER_REQUEST = '[Metadata Provider] Update Request',
+ UPDATE_PROVIDER_SUCCESS = '[Metadata Provider] Update Success',
+ UPDATE_PROVIDER_FAIL = '[Metadata Provider] Update Fail',
+
+ LOAD_PROVIDER_REQUEST = '[Metadata Provider Collection] Provider REQUEST',
+ LOAD_PROVIDER_SUCCESS = '[Metadata Provider Collection] Provider SUCCESS',
+ LOAD_PROVIDER_ERROR = '[Metadata Provider Collection] Provider ERROR',
+
+ ADD_PROVIDER_REQUEST = '[Metadata Provider Collection] Add Provider',
+ ADD_PROVIDER_SUCCESS = '[Metadata Provider Collection] Add Provider Success',
+ ADD_PROVIDER_FAIL = '[Metadata Provider Collection] Add Provider Fail',
+
+ REMOVE_PROVIDER_REQUEST = '[Metadata Provider Collection] Remove Provider Request',
+ REMOVE_PROVIDER_SUCCESS = '[Metadata Provider Collection] Remove Provider Success',
+ REMOVE_PROVIDER_FAIL = '[Metadata Provider Collection] Remove Provider Fail'
+}
+
+export class LoadProviderRequest implements Action {
+ readonly type = ProviderCollectionActionTypes.LOAD_PROVIDER_REQUEST;
+
+ constructor() { }
+}
+
+export class LoadProviderSuccess implements Action {
+ readonly type = ProviderCollectionActionTypes.LOAD_PROVIDER_SUCCESS;
+
+ constructor(public payload: MetadataProvider[]) { }
+}
+
+export class LoadProviderError implements Action {
+ readonly type = ProviderCollectionActionTypes.LOAD_PROVIDER_ERROR;
+
+ constructor(public payload: any) { }
+}
+
+export class UpdateProviderRequest implements Action {
+ readonly type = ProviderCollectionActionTypes.UPDATE_PROVIDER_REQUEST;
+
+ constructor(public payload: MetadataProvider) { }
+}
+
+export class UpdateProviderSuccess implements Action {
+ readonly type = ProviderCollectionActionTypes.UPDATE_PROVIDER_SUCCESS;
+
+ constructor(public payload: Update) { }
+}
+
+export class UpdateProviderFail implements Action {
+ readonly type = ProviderCollectionActionTypes.UPDATE_PROVIDER_FAIL;
+
+ constructor(public payload: MetadataProvider) { }
+}
+
+export class AddProviderRequest implements Action {
+ readonly type = ProviderCollectionActionTypes.ADD_PROVIDER_REQUEST;
+
+ constructor(public payload: MetadataProvider) { }
+}
+
+export class AddProviderSuccess implements Action {
+ readonly type = ProviderCollectionActionTypes.ADD_PROVIDER_SUCCESS;
+
+ constructor(public payload: MetadataProvider) { }
+}
+
+export class AddProviderFail implements Action {
+ readonly type = ProviderCollectionActionTypes.ADD_PROVIDER_FAIL;
+
+ constructor(public payload: any) { }
+}
+
+export class RemoveProviderRequest implements Action {
+ readonly type = ProviderCollectionActionTypes.REMOVE_PROVIDER_REQUEST;
+
+ constructor(public payload: MetadataProvider) { }
+}
+
+export class RemoveProviderSuccess implements Action {
+ readonly type = ProviderCollectionActionTypes.REMOVE_PROVIDER_SUCCESS;
+
+ constructor(public payload: MetadataProvider) { }
+}
+
+export class RemoveProviderFail implements Action {
+ readonly type = ProviderCollectionActionTypes.REMOVE_PROVIDER_FAIL;
+
+ constructor(public payload: MetadataProvider) { }
+}
+
+export type ProviderCollectionActionsUnion =
+ | LoadProviderRequest
+ | LoadProviderSuccess
+ | LoadProviderError
+ | AddProviderRequest
+ | AddProviderSuccess
+ | AddProviderFail
+ | RemoveProviderRequest
+ | RemoveProviderSuccess
+ | RemoveProviderFail
+ | UpdateProviderRequest
+ | UpdateProviderSuccess
+ | UpdateProviderFail;
diff --git a/ui/src/app/metadata/provider/action/editor.action.ts b/ui/src/app/metadata/provider/action/editor.action.ts
index ca195716f..85ea00edd 100644
--- a/ui/src/app/metadata/provider/action/editor.action.ts
+++ b/ui/src/app/metadata/provider/action/editor.action.ts
@@ -6,7 +6,9 @@ export enum EditorActionTypes {
LOAD_SCHEMA_SUCCESS = '[Provider Editor] Load Schema Success',
LOAD_SCHEMA_FAIL = '[Provider Editor] Load Schema Fail',
- SELECT_PROVIDER_TYPE = '[Provider Editor] Select Provider Type'
+ SELECT_PROVIDER_TYPE = '[Provider Editor] Select Provider Type',
+
+ CLEAR = '[Provider Editor] Clear'
}
export class UpdateStatus implements Action {
@@ -39,9 +41,14 @@ export class SelectProviderType implements Action {
constructor(public payload: string) { }
}
+export class ClearEditor implements Action {
+ readonly type = EditorActionTypes.CLEAR;
+}
+
export type EditorActionUnion =
| UpdateStatus
| LoadSchemaRequest
| LoadSchemaSuccess
| LoadSchemaFail
- | SelectProviderType;
+ | SelectProviderType
+ | ClearEditor;
diff --git a/ui/src/app/metadata/provider/action/entity.action.ts b/ui/src/app/metadata/provider/action/entity.action.ts
index bff45b5f4..84f7dbd4a 100644
--- a/ui/src/app/metadata/provider/action/entity.action.ts
+++ b/ui/src/app/metadata/provider/action/entity.action.ts
@@ -3,13 +3,9 @@ import { MetadataProvider } from '../../domain/model';
export enum EntityActionTypes {
SELECT_PROVIDER = '[Provider Entity] Select Provider',
- CREATE_PROVIDER = '[Provider Entity] Create Provider',
UPDATE_PROVIDER = '[Provider Entity] Update Provider',
- SAVE_PROVIDER_REQUEST = '[Provider Entity] Save Provider Request',
- SAVE_PROVIDER_SUCCESS = '[Provider Entity] Save Provider Success',
- SAVE_PROVIDER_FAIL = '[Provider Entity] Save Provider Fail',
-
- RESET_CHANGES = '[Provider Entity] Reset Provider Changes'
+ CLEAR_PROVIDER = '[Provider Entity] Clear',
+ RESET_CHANGES = '[Provider Entity] Reset Changes'
}
export class SelectProvider implements Action {
@@ -18,34 +14,14 @@ export class SelectProvider implements Action {
constructor(public payload: MetadataProvider) { }
}
-export class CreateProvider implements Action {
- readonly type = EntityActionTypes.CREATE_PROVIDER;
-
- constructor(public payload: MetadataProvider) { }
-}
-
export class UpdateProvider implements Action {
readonly type = EntityActionTypes.UPDATE_PROVIDER;
constructor(public payload: Partial) { }
}
-export class SaveProviderRequest implements Action {
- readonly type = EntityActionTypes.SAVE_PROVIDER_REQUEST;
-
- constructor(public payload: MetadataProvider) { }
-}
-
-export class SaveProviderSuccess implements Action {
- readonly type = EntityActionTypes.SAVE_PROVIDER_SUCCESS;
-
- constructor(public payload: MetadataProvider) { }
-}
-
-export class SaveProviderFail implements Action {
- readonly type = EntityActionTypes.SAVE_PROVIDER_FAIL;
-
- constructor(public payload: Error) { }
+export class ClearProvider implements Action {
+ readonly type = EntityActionTypes.CLEAR_PROVIDER;
}
export class ResetChanges implements Action {
@@ -55,8 +31,5 @@ export class ResetChanges implements Action {
export type EntityActionUnion =
| SelectProvider
| UpdateProvider
- | SaveProviderRequest
- | SaveProviderSuccess
- | SaveProviderFail
- | CreateProvider
+ | ClearProvider
| ResetChanges;
diff --git a/ui/src/app/metadata/provider/component/provider-wizard-summary.component.html b/ui/src/app/metadata/provider/component/provider-wizard-summary.component.html
new file mode 100644
index 000000000..a60ef4c1e
--- /dev/null
+++ b/ui/src/app/metadata/provider/component/provider-wizard-summary.component.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/app/metadata/provider/component/provider-wizard-summary.component.spec.ts b/ui/src/app/metadata/provider/component/provider-wizard-summary.component.spec.ts
new file mode 100644
index 000000000..d951464ad
--- /dev/null
+++ b/ui/src/app/metadata/provider/component/provider-wizard-summary.component.spec.ts
@@ -0,0 +1,94 @@
+import { Component, ViewChild } from '@angular/core';
+import { TestBed, async, ComponentFixture } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+import { StoreModule, Store, combineReducers } from '@ngrx/store';
+
+import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { ProviderWizardSummaryComponent } from './provider-wizard-summary.component';
+import * as fromRoot from '../reducer';
+import { SchemaFormModule, WidgetRegistry, DefaultWidgetRegistry } from 'ngx-schema-form';
+import * as fromWizard from '../../../wizard/reducer';
+import { Wizard } from '../../../wizard/model';
+import { MetadataProvider } from '../../domain/model';
+import { SummaryPropertyComponent } from './summary-property.component';
+import { SCHEMA } from '../../../../testing/form-schema.stub';
+import { MetadataProviderWizard } from '../model';
+
+@Component({
+ template: `
+
+ `
+})
+class TestHostComponent {
+ @ViewChild(ProviderWizardSummaryComponent)
+ public componentUnderTest: ProviderWizardSummaryComponent;
+
+ private _summary;
+
+ get summary(): { definition: Wizard, schema: { [id: string]: any }, model: any } {
+ return this._summary;
+ }
+
+ set summary(summary: { definition: Wizard, schema: { [id: string]: any }, model: any }) {
+ this._summary = summary;
+ }
+}
+
+describe('Provider Wizard Summary Component', () => {
+
+ let fixture: ComponentFixture;
+ let instance: TestHostComponent;
+ let app: ProviderWizardSummaryComponent;
+ let store: Store;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ NgbDropdownModule.forRoot(),
+ RouterTestingModule,
+ SchemaFormModule.forRoot(),
+ StoreModule.forRoot({
+ provider: combineReducers(fromRoot.reducers),
+ wizard: combineReducers(fromWizard.reducers)
+ })
+ ],
+ declarations: [
+ ProviderWizardSummaryComponent,
+ SummaryPropertyComponent,
+ TestHostComponent
+ ],
+ providers: [
+ { provide: WidgetRegistry, useClass: DefaultWidgetRegistry }
+ ]
+ }).compileComponents();
+
+ store = TestBed.get(Store);
+ spyOn(store, 'dispatch');
+
+ fixture = TestBed.createComponent(TestHostComponent);
+ instance = fixture.componentInstance;
+ app = instance.componentUnderTest;
+ fixture.detectChanges();
+ }));
+
+ it('should instantiate the component', async(() => {
+ expect(app).toBeTruthy();
+ }));
+
+ describe('ngOnChanges', () => {
+ it('should set columns and sections if summary is provided', () => {
+ instance.summary = {
+ model: {
+ name: 'foo',
+ '@type': 'MetadataProvider'
+ },
+ schema: SCHEMA,
+ definition: MetadataProviderWizard
+ };
+ fixture.detectChanges();
+ expect(app.sections).toBeDefined();
+ expect(app.columns).toBeDefined();
+ });
+ });
+});
diff --git a/ui/src/app/metadata/provider/component/provider-wizard-summary.component.ts b/ui/src/app/metadata/provider/component/provider-wizard-summary.component.ts
new file mode 100644
index 000000000..822e51337
--- /dev/null
+++ b/ui/src/app/metadata/provider/component/provider-wizard-summary.component.ts
@@ -0,0 +1,83 @@
+import { Component, Input, SimpleChanges, OnChanges, Output, EventEmitter } from '@angular/core';
+import { Store } from '@ngrx/store';
+
+import * as fromProvider from '../reducer';
+import { Wizard, WizardStep } from '../../../wizard/model';
+import { MetadataProvider } from '../../domain/model';
+import { Property } from '../model/property';
+
+interface Section {
+ id: string;
+ index: number;
+ label: string;
+ properties: Property[];
+}
+
+function getStepProperties(schema: any, model: any): Property[] {
+ if (!schema || !schema.properties) { return []; }
+ return Object.keys(schema.properties).map(property => ({
+ name: schema.properties[property].title,
+ value: (model && model.hasOwnProperty(property)) ? model[property] : null,
+ type: schema.properties[property].type,
+ properties: schema.properties ? getStepProperties(
+ schema.properties[property],
+ (model && model.hasOwnProperty(property)) ? model[property] : null
+ ) : []
+ }));
+}
+
+@Component({
+ selector: 'provider-wizard-summary',
+ templateUrl: './provider-wizard-summary.component.html',
+ styleUrls: []
+})
+
+export class ProviderWizardSummaryComponent implements OnChanges {
+ @Input() summary: { definition: Wizard, schema: { [id: string]: any }, model: any };
+
+ @Output() onPageSelect: EventEmitter = new EventEmitter();
+
+ sections: Section[];
+ columns: Array[];
+
+ constructor(
+ private store: Store
+ ) {}
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes.summary && this.summary) {
+ const schemas = this.summary.schema;
+ const model = this.summary.model;
+ const def = this.summary.definition;
+ const steps = def.steps;
+
+ this.sections = steps
+ .filter(step => step.id !== 'summary')
+ .map(
+ (step: WizardStep) => ({
+ id: step.id,
+ index: step.index,
+ label: step.label,
+ properties: getStepProperties(schemas[step.id], def.translate.formatter(model))
+ })
+ );
+
+ this.columns = this.sections.reduce((resultArray, item, index) => {
+ const chunkIndex = Math.floor(index / Math.round(this.sections.length / 2));
+
+ if (!resultArray[chunkIndex]) {
+ resultArray[chunkIndex] = [];
+ }
+
+ resultArray[chunkIndex].push(item);
+
+ return resultArray;
+ }, []);
+ }
+ }
+
+ gotoPage(page: string = ''): void {
+ this.onPageSelect.emit(page);
+ }
+}
+
diff --git a/ui/src/app/metadata/provider/component/summary-property.component.html b/ui/src/app/metadata/provider/component/summary-property.component.html
new file mode 100644
index 000000000..8f4781e47
--- /dev/null
+++ b/ui/src/app/metadata/provider/component/summary-property.component.html
@@ -0,0 +1,18 @@
+
+
+
+
+ {{ property.name }}
+ {{ property.value || '-' }}
+
+
+
+
+ {{ property.name }}
+
+
+
+
+
+
+
diff --git a/ui/src/app/metadata/provider/component/summary-property.component.spec.ts b/ui/src/app/metadata/provider/component/summary-property.component.spec.ts
new file mode 100644
index 000000000..8a0fe498b
--- /dev/null
+++ b/ui/src/app/metadata/provider/component/summary-property.component.spec.ts
@@ -0,0 +1,75 @@
+import { Component, ViewChild } from '@angular/core';
+import { TestBed, async, ComponentFixture } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+import { StoreModule, Store, combineReducers } from '@ngrx/store';
+
+import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { SummaryPropertyComponent } from './summary-property.component';
+import * as fromRoot from '../reducer';
+import { SchemaFormModule, WidgetRegistry, DefaultWidgetRegistry } from 'ngx-schema-form';
+import * as fromWizard from '../../../wizard/reducer';
+import { Wizard } from '../../../wizard/model';
+import { MetadataProvider } from '../../domain/model';
+import { Property } from '../model/property';
+
+@Component({
+ template: `
+
+ `
+})
+class TestHostComponent {
+ @ViewChild(SummaryPropertyComponent)
+ public componentUnderTest: SummaryPropertyComponent;
+
+ private _property;
+
+ get property(): Property {
+ return this._property;
+ }
+
+ set property(prop: Property) {
+ this._property = prop;
+ }
+}
+
+describe('Summary Property Component', () => {
+
+ let fixture: ComponentFixture;
+ let instance: TestHostComponent;
+ let app: SummaryPropertyComponent;
+ let store: Store;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ NgbDropdownModule.forRoot(),
+ RouterTestingModule,
+ SchemaFormModule.forRoot(),
+ StoreModule.forRoot({
+ provider: combineReducers(fromRoot.reducers),
+ wizard: combineReducers(fromWizard.reducers)
+ })
+ ],
+ declarations: [
+ SummaryPropertyComponent,
+ TestHostComponent
+ ],
+ providers: [
+ { provide: WidgetRegistry, useClass: DefaultWidgetRegistry }
+ ]
+ }).compileComponents();
+
+ store = TestBed.get(Store);
+ spyOn(store, 'dispatch');
+
+ fixture = TestBed.createComponent(TestHostComponent);
+ instance = fixture.componentInstance;
+ app = instance.componentUnderTest;
+ fixture.detectChanges();
+ }));
+
+ it('should instantiate the component', async(() => {
+ expect(app).toBeTruthy();
+ }));
+});
diff --git a/ui/src/app/metadata/provider/component/summary-property.component.ts b/ui/src/app/metadata/provider/component/summary-property.component.ts
new file mode 100644
index 000000000..6dbd0c716
--- /dev/null
+++ b/ui/src/app/metadata/provider/component/summary-property.component.ts
@@ -0,0 +1,13 @@
+import { Component, Input } from '@angular/core';
+import { Property } from '../model/property';
+
+@Component({
+ selector: 'summary-property',
+ templateUrl: './summary-property.component.html',
+ styleUrls: []
+})
+
+export class SummaryPropertyComponent {
+ @Input() property: Property;
+}
+
diff --git a/ui/src/app/metadata/provider/container/new-provider.component.scss b/ui/src/app/metadata/provider/container/new-provider.component.scss
deleted file mode 100644
index e3d0c671e..000000000
--- a/ui/src/app/metadata/provider/container/new-provider.component.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-// :host {
-// .provider-nav-option {
-// width: 160px;
-// }
-// }
diff --git a/ui/src/app/metadata/provider/container/new-provider.component.spec.ts b/ui/src/app/metadata/provider/container/new-provider.component.spec.ts
deleted file mode 100644
index e69de29bb..000000000
diff --git a/ui/src/app/metadata/provider/container/provider-wizard-step.component.html b/ui/src/app/metadata/provider/container/provider-wizard-step.component.html
new file mode 100644
index 000000000..831e71fbb
--- /dev/null
+++ b/ui/src/app/metadata/provider/container/provider-wizard-step.component.html
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/ui/src/app/metadata/provider/container/provider-wizard-step.component.spec.ts b/ui/src/app/metadata/provider/container/provider-wizard-step.component.spec.ts
new file mode 100644
index 000000000..eac91d512
--- /dev/null
+++ b/ui/src/app/metadata/provider/container/provider-wizard-step.component.spec.ts
@@ -0,0 +1,98 @@
+import { Component, ViewChild } from '@angular/core';
+import { TestBed, async, ComponentFixture } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+import { StoreModule, Store, combineReducers } from '@ngrx/store';
+
+import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { ProviderWizardStepComponent } from './provider-wizard-step.component';
+import * as fromRoot from '../reducer';
+import { SchemaFormModule, WidgetRegistry, DefaultWidgetRegistry } from 'ngx-schema-form';
+import * as fromWizard from '../../../wizard/reducer';
+import { SCHEMA } from '../../../../testing/form-schema.stub';
+import { MetadataProviderWizard } from '../model';
+
+@Component({
+ template: `
+
+ `
+})
+class TestHostComponent {
+ @ViewChild(ProviderWizardStepComponent)
+ public componentUnderTest: ProviderWizardStepComponent;
+}
+
+describe('Provider Wizard Step Component', () => {
+
+ let fixture: ComponentFixture;
+ let instance: TestHostComponent;
+ let app: ProviderWizardStepComponent;
+ let store: Store;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ NgbDropdownModule.forRoot(),
+ RouterTestingModule,
+ SchemaFormModule.forRoot(),
+ StoreModule.forRoot({
+ provider: combineReducers(fromRoot.reducers),
+ wizard: combineReducers(fromWizard.reducers)
+ })
+ ],
+ declarations: [
+ ProviderWizardStepComponent,
+ TestHostComponent
+ ],
+ providers: [
+ { provide: WidgetRegistry, useClass: DefaultWidgetRegistry }
+ ]
+ }).compileComponents();
+
+ store = TestBed.get(Store);
+ spyOn(store, 'dispatch');
+
+ fixture = TestBed.createComponent(TestHostComponent);
+ instance = fixture.componentInstance;
+ app = instance.componentUnderTest;
+ fixture.detectChanges();
+ }));
+
+ it('should instantiate the component', async(() => {
+ expect(app).toBeTruthy();
+ }));
+
+ describe('resetSelectedType method', () => {
+ it('should dispatch a SetDefinition action if the type has changed', () => {
+ app.resetSelectedType({ value: { name: 'foo', '@type': 'FileBackedHttpMetadataResolver' } }, SCHEMA, MetadataProviderWizard);
+ expect(store.dispatch).toHaveBeenCalled();
+ });
+
+ it('should NOT dispatch a SetDefinition action if the type hasn\'t changed', () => {
+ app.resetSelectedType({ value: { name: 'foo', '@type': 'MetadataProvider' } }, SCHEMA, MetadataProviderWizard);
+ expect(store.dispatch).not.toHaveBeenCalled();
+ });
+
+ it('should NOT dispatch a SetDefinition action if the type isn\'t found', () => {
+ app.resetSelectedType({ value: { name: 'foo', '@type': 'FooProvider' } }, SCHEMA, MetadataProviderWizard);
+ expect(store.dispatch).not.toHaveBeenCalled();
+ });
+
+ it('should return changes and definition if no type supplied', () => {
+ app.resetSelectedType({ value: { name: 'foo' } }, SCHEMA, MetadataProviderWizard);
+ expect(store.dispatch).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('updateStatus method', () => {
+ it('should dispatch an UpdateStatus action', () => {
+ app.updateStatus({value: { name: 'notfound'} });
+ expect(store.dispatch).toHaveBeenCalled();
+ });
+
+ it('should NOT dispatch a SetDefinition action if the type hasn\'t changed', () => {
+ app.updateStatus({ value: null });
+ expect(store.dispatch).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/ui/src/app/metadata/provider/container/provider-wizard-step.component.ts b/ui/src/app/metadata/provider/container/provider-wizard-step.component.ts
new file mode 100644
index 000000000..7d09b8ec1
--- /dev/null
+++ b/ui/src/app/metadata/provider/container/provider-wizard-step.component.ts
@@ -0,0 +1,134 @@
+import { Component, OnDestroy } from '@angular/core';
+import { Observable, Subject } from 'rxjs';
+import { withLatestFrom, map, distinctUntilChanged, skipWhile } from 'rxjs/operators';
+import { Store } from '@ngrx/store';
+
+import * as fromProvider from '../reducer';
+import * as fromWizard from '../../../wizard/reducer';
+
+import { SetDefinition } from '../../../wizard/action/wizard.action';
+import { UpdateStatus } from '../action/editor.action';
+import { Wizard } from '../../../wizard/model';
+import { MetadataProvider } from '../../domain/model';
+import { MetadataProviderTypes, MetadataProviderWizard } from '../model';
+import { UpdateProvider } from '../action/entity.action';
+import { pick } from '../../../shared/util';
+
+@Component({
+ selector: 'provider-wizard-step',
+ templateUrl: './provider-wizard-step.component.html',
+ styleUrls: []
+})
+
+export class ProviderWizardStepComponent implements OnDestroy {
+ valueChangeSubject = new Subject>();
+ private valueChangeEmitted$ = this.valueChangeSubject.asObservable();
+
+ statusChangeSubject = new Subject>();
+ private statusChangeEmitted$ = this.statusChangeSubject.asObservable();
+
+ schema$: Observable;
+ schema: any;
+ definition$: Observable>;
+ changes$: Observable;
+ currentPage: string;
+ valid$: Observable;
+ model$: Observable;
+
+ namesList: string[] = [];
+
+ validators = {
+ '/': (value, property, form_current) => {
+ let errors;
+ // iterate all customer
+ Object.keys(value).forEach((key) => {
+ const item = value[key];
+ const validatorKey = `/${key}`;
+ const validator = this.validators.hasOwnProperty(validatorKey) ? this.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) => {
+ const err = this.namesList.indexOf(value) > -1 ? {
+ code: 'INVALID_NAME',
+ path: `#${property.path}`,
+ message: 'Name must be unique.',
+ params: [value]
+ } : null;
+ return err;
+ }
+ };
+
+ constructor(
+ private store: Store,
+ ) {
+ this.schema$ = this.store.select(fromProvider.getSchema);
+ this.definition$ = this.store.select(fromWizard.getWizardDefinition);
+ this.changes$ = this.store.select(fromProvider.getEntityChanges);
+
+ this.store.select(fromProvider.getProviderNames).subscribe(list => this.namesList = list);
+
+ this.model$ = this.schema$.pipe(
+ withLatestFrom(
+ this.store.select(fromWizard.getModel),
+ this.changes$,
+ this.definition$
+ ),
+ map(([schema, model, changes, definition]) => ({
+ model: {
+ ...model,
+ ...changes
+ },
+ definition
+ })),
+ skipWhile(({ model, definition }) => !definition || !model),
+ map(({ model, definition }) => definition.translate.formatter(model))
+ );
+
+ this.valueChangeEmitted$.pipe(
+ withLatestFrom(this.schema$, this.definition$),
+ map(([changes, schema, definition]) => this.resetSelectedType(changes, schema, definition)),
+ skipWhile(({ changes, definition }) => !definition || !changes),
+ map(({ changes, definition }) => definition.translate.parser(changes))
+ )
+ .subscribe(changes => this.store.dispatch(new UpdateProvider(changes)));
+
+ this.statusChangeEmitted$.pipe(distinctUntilChanged()).subscribe(errors => this.updateStatus(errors));
+
+ this.store.select(fromWizard.getWizardIndex).subscribe(i => this.currentPage = i);
+ }
+
+ resetSelectedType(changes: any, schema: any, definition: any): { changes: any, definition: any } {
+ const type = changes.value['@type'];
+ if (type && type !== definition.type) {
+ const newDefinition = MetadataProviderTypes.find(def => def.type === type);
+ if (newDefinition) {
+ this.store.dispatch(new SetDefinition({
+ ...MetadataProviderWizard,
+ ...newDefinition,
+ steps: [
+ ...MetadataProviderWizard.steps,
+ ...newDefinition.steps
+ ]
+ }));
+ changes = { value: pick(Object.keys(schema.properties))(changes.value) };
+ }
+ }
+ return { changes: changes.value, definition };
+ }
+
+ updateStatus(errors: any): void {
+ const status = { [this.currentPage]: !(errors.value) ? 'VALID' : 'INVALID' };
+ this.store.dispatch(new UpdateStatus(status));
+ }
+
+ ngOnDestroy() {
+ this.valueChangeSubject.complete();
+ }
+}
+
diff --git a/ui/src/app/metadata/provider/container/provider-wizard.component.html b/ui/src/app/metadata/provider/container/provider-wizard.component.html
index 58adb7221..196081a1f 100644
--- a/ui/src/app/metadata/provider/container/provider-wizard.component.html
+++ b/ui/src/app/metadata/provider/container/provider-wizard.component.html
@@ -1,18 +1,17 @@
diff --git a/ui/src/app/metadata/provider/container/provider-wizard.component.scss b/ui/src/app/metadata/provider/container/provider-wizard.component.scss
deleted file mode 100644
index e69de29bb..000000000
diff --git a/ui/src/app/metadata/provider/container/provider-wizard.component.spec.ts b/ui/src/app/metadata/provider/container/provider-wizard.component.spec.ts
new file mode 100644
index 000000000..043663260
--- /dev/null
+++ b/ui/src/app/metadata/provider/container/provider-wizard.component.spec.ts
@@ -0,0 +1,63 @@
+import { Component, ViewChild } from '@angular/core';
+import { TestBed, async, ComponentFixture } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+import { StoreModule, Store, combineReducers } from '@ngrx/store';
+
+import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { ProviderWizardComponent } from './provider-wizard.component';
+import * as fromRoot from '../reducer';
+import { WizardModule } from '../../../wizard/wizard.module';
+import { ProviderWizardSummaryComponent } from '../component/provider-wizard-summary.component';
+import { SummaryPropertyComponent } from '../component/summary-property.component';
+import * as fromWizard from '../../../wizard/reducer';
+
+@Component({
+ template: `
+
+ `
+})
+class TestHostComponent {
+ @ViewChild(ProviderWizardComponent)
+ public componentUnderTest: ProviderWizardComponent;
+}
+
+describe('Provider Wizard Component', () => {
+
+ let fixture: ComponentFixture;
+ let instance: TestHostComponent;
+ let app: ProviderWizardComponent;
+ let store: Store;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ WizardModule,
+ NgbDropdownModule.forRoot(),
+ RouterTestingModule,
+ StoreModule.forRoot({
+ provider: combineReducers(fromRoot.reducers),
+ wizard: combineReducers(fromWizard.reducers)
+ })
+ ],
+ declarations: [
+ ProviderWizardComponent,
+ SummaryPropertyComponent,
+ ProviderWizardSummaryComponent,
+ TestHostComponent
+ ]
+ }).compileComponents();
+
+ store = TestBed.get(Store);
+ spyOn(store, 'dispatch');
+
+ fixture = TestBed.createComponent(TestHostComponent);
+ instance = fixture.componentInstance;
+ app = instance.componentUnderTest;
+ fixture.detectChanges();
+ }));
+
+ it('should instantiate the component', async(() => {
+ expect(app).toBeTruthy();
+ }));
+});
diff --git a/ui/src/app/metadata/provider/container/provider-wizard.component.ts b/ui/src/app/metadata/provider/container/provider-wizard.component.ts
index 36861d3cf..ee18b72b0 100644
--- a/ui/src/app/metadata/provider/container/provider-wizard.component.ts
+++ b/ui/src/app/metadata/provider/container/provider-wizard.component.ts
@@ -1,61 +1,56 @@
-
import { Component, OnDestroy } from '@angular/core';
-import { ActivatedRoute, Router } from '@angular/router';
-import { Subscription, Observable, Subject } from 'rxjs';
-import { distinctUntilChanged, map, withLatestFrom } from 'rxjs/operators';
+import { Observable, combineLatest } from 'rxjs';
import { Store } from '@ngrx/store';
import * as fromProvider from '../reducer';
import * as fromWizard from '../../../wizard/reducer';
-
-import { SetIndex, SetDisabled, UpdateDefinition, WizardActionTypes, Next, SetDefinition } from '../../../wizard/action/wizard.action';
-import { LoadSchemaRequest, UpdateStatus } from '../action/editor.action';
+import { SetIndex, SetDisabled, ClearWizard } from '../../../wizard/action/wizard.action';
+import { LoadSchemaRequest, ClearEditor } from '../action/editor.action';
import { startWith } from 'rxjs/operators';
import { Wizard, WizardStep } from '../../../wizard/model';
import { MetadataProvider } from '../../domain/model';
-import { MetadataProviderTypes, MetadataProviderWizard } from '../model';
-import { UpdateProvider } from '../action/entity.action';
-import { pick } from '../../../shared/util';
+import { ClearProvider } from '../action/entity.action';
+import { Router, ActivatedRoute } from '@angular/router';
+import { map } from 'rxjs/operators';
+import { AddProviderRequest } from '../action/collection.action';
+
@Component({
- selector: 'provider-wizard-page',
+ selector: 'provider-wizard',
templateUrl: './provider-wizard.component.html',
- styleUrls: ['./provider-wizard.component.scss']
+ styleUrls: []
})
export class ProviderWizardComponent implements OnDestroy {
- actionsSubscription: Subscription;
-
- changeSubject = new Subject>();
- private changeEmitted$ = this.changeSubject.asObservable();
-
- schema$: Observable;
- schema: any;
definition$: Observable>;
changes$: Observable;
+ model$: Observable;
currentPage: string;
valid$: Observable;
- formModel: any;
-
nextStep: WizardStep;
previousStep: WizardStep;
+ summary$: Observable<{ definition: Wizard, schema: { [id: string]: any }, model: any }>;
+
+ provider: MetadataProvider;
+
constructor(
- private store: Store
+ private store: Store,
+ private router: Router,
+ private route: ActivatedRoute
) {
this.store
.select(fromWizard.getCurrentWizardSchema)
.subscribe(s => {
this.store.dispatch(new LoadSchemaRequest(s));
});
-
- this.schema$ = this.store.select(fromProvider.getSchema);
this.valid$ = this.store.select(fromProvider.getEditorIsValid);
- this.definition$ = this.store.select(fromWizard.getWizardDefinition);
this.changes$ = this.store.select(fromProvider.getEntityChanges);
+
this.store.select(fromWizard.getNext).subscribe(n => this.nextStep = n);
this.store.select(fromWizard.getPrevious).subscribe(p => this.previousStep = p);
+ this.store.select(fromWizard.getWizardIndex).subscribe(i => this.currentPage = i);
this.valid$
.pipe(startWith(false))
@@ -63,43 +58,25 @@ export class ProviderWizardComponent implements OnDestroy {
this.store.dispatch(new SetDisabled(!valid));
});
- this.schema$.subscribe(s => this.schema = s);
-
- this.changeEmitted$
- .pipe(
- withLatestFrom(this.schema$, this.definition$),
- )
- .subscribe(
- ([changes, schema, definition]) => {
- const type = changes.value['@type'];
- if (type && type !== definition.type) {
- const newDefinition = MetadataProviderTypes.find(def => def.type === type);
- if (newDefinition) {
- this.store.dispatch(new SetDefinition({
- ...MetadataProviderWizard,
- ...newDefinition,
- steps: [
- ...MetadataProviderWizard.steps,
- ...newDefinition.steps
- ]
- }));
- changes = { value: pick(Object.keys(schema.properties))(changes.value) };
- }
- }
- this.store.dispatch(new UpdateProvider(changes.value));
- }
- );
+ this.summary$ = combineLatest(
+ this.store.select(fromWizard.getWizardDefinition),
+ this.store.select(fromWizard.getSchemaCollection),
+ this.store.select(fromProvider.getEntityChanges)
+ ).pipe(
+ map(([ definition, schema, model ]) => ({ definition, schema, model }))
+ );
+
+ this.changes$.subscribe(c => this.provider = c);
}
ngOnDestroy() {
- this.actionsSubscription.unsubscribe();
- this.changeSubject.complete();
+ this.store.dispatch(new ClearProvider());
+ this.store.dispatch(new ClearWizard());
+ this.store.dispatch(new ClearEditor());
}
next(): void {
- if (this.nextStep) {
- this.store.dispatch(new SetIndex(this.nextStep.id));
- }
+ this.store.dispatch(new SetIndex(this.nextStep.id));
}
previous(): void {
@@ -107,12 +84,11 @@ export class ProviderWizardComponent implements OnDestroy {
}
save(): void {
- console.log('Save!');
+ this.store.dispatch(new AddProviderRequest(this.provider));
}
- onStatusChange(value): void {
- const status = { [this.currentPage]: value ? 'VALID' : 'INVALID' };
- this.store.dispatch(new UpdateStatus(status));
+ gotoPage(page: string): void {
+ this.store.dispatch(new SetIndex(page));
}
}
diff --git a/ui/src/app/metadata/provider/container/new-provider.component.html b/ui/src/app/metadata/provider/container/provider.component.html
similarity index 100%
rename from ui/src/app/metadata/provider/container/new-provider.component.html
rename to ui/src/app/metadata/provider/container/provider.component.html
diff --git a/ui/src/app/metadata/provider/container/provider.component.spec.ts b/ui/src/app/metadata/provider/container/provider.component.spec.ts
new file mode 100644
index 000000000..3f1363cdf
--- /dev/null
+++ b/ui/src/app/metadata/provider/container/provider.component.spec.ts
@@ -0,0 +1,57 @@
+import { Component, ViewChild } from '@angular/core';
+import { TestBed, async, ComponentFixture } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+import { StoreModule, Store, combineReducers } from '@ngrx/store';
+
+import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { ProviderComponent } from './provider.component';
+import * as fromRoot from '../reducer';
+import * as fromWizard from '../../../wizard/reducer';
+
+@Component({
+ template: `
+
+ `
+})
+class TestHostComponent {
+ @ViewChild(ProviderComponent)
+ public componentUnderTest: ProviderComponent;
+}
+
+describe('Provider Component', () => {
+
+ let fixture: ComponentFixture;
+ let instance: TestHostComponent;
+ let app: ProviderComponent;
+ let store: Store;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ NgbDropdownModule.forRoot(),
+ RouterTestingModule,
+ StoreModule.forRoot({
+ provider: combineReducers(fromRoot.reducers),
+ wizard: combineReducers(fromWizard.reducers)
+ })
+ ],
+ declarations: [
+ ProviderComponent,
+ TestHostComponent
+ ],
+ }).compileComponents();
+
+ store = TestBed.get(Store);
+ spyOn(store, 'dispatch');
+
+ fixture = TestBed.createComponent(TestHostComponent);
+ instance = fixture.componentInstance;
+ app = instance.componentUnderTest;
+ fixture.detectChanges();
+ }));
+
+ it('should instantiate the component', async(() => {
+ expect(app).toBeTruthy();
+ }));
+});
diff --git a/ui/src/app/metadata/provider/container/new-provider.component.ts b/ui/src/app/metadata/provider/container/provider.component.ts
similarity index 55%
rename from ui/src/app/metadata/provider/container/new-provider.component.ts
rename to ui/src/app/metadata/provider/container/provider.component.ts
index d7581c566..afaf48a27 100644
--- a/ui/src/app/metadata/provider/container/new-provider.component.ts
+++ b/ui/src/app/metadata/provider/container/provider.component.ts
@@ -1,25 +1,17 @@
import { Component } from '@angular/core';
-import { FormGroup, Validators, FormBuilder } from '@angular/forms';
import { Store } from '@ngrx/store';
-import { ActivatedRoute } from '@angular/router';
-
-
-import { MetadataProviderTypes } from '../model';
import * as fromProvider from '../reducer';
import { SetDefinition, SetIndex } from '../../../wizard/action/wizard.action';
import { MetadataProviderWizard } from '../model';
@Component({
- selector: 'new-provider-page',
- templateUrl: './new-provider.component.html',
- styleUrls: ['./new-provider.component.scss']
+ selector: 'provider-page',
+ templateUrl: './provider.component.html',
+ styleUrls: []
})
-export class NewProviderComponent {
- types = MetadataProviderTypes;
-
+export class ProviderComponent {
constructor(
- private fb: FormBuilder,
private store: Store
) {
this.store.dispatch(new SetDefinition(MetadataProviderWizard));
diff --git a/ui/src/app/metadata/provider/effect/collection.effect.ts b/ui/src/app/metadata/provider/effect/collection.effect.ts
new file mode 100644
index 000000000..5a471dfdd
--- /dev/null
+++ b/ui/src/app/metadata/provider/effect/collection.effect.ts
@@ -0,0 +1,69 @@
+import { Injectable } from '@angular/core';
+import { Effect, Actions, ofType } from '@ngrx/effects';
+import { Router } from '@angular/router';
+
+import { of } from 'rxjs';
+import { map, catchError, switchMap, tap } from 'rxjs/operators';
+import {
+ ProviderCollectionActionsUnion,
+ ProviderCollectionActionTypes,
+ AddProviderRequest,
+ AddProviderSuccess,
+ AddProviderFail,
+ LoadProviderRequest,
+ LoadProviderSuccess,
+ LoadProviderError
+} from '../action/collection.action';
+import { MetadataProviderService } from '../../domain/service/provider.service';
+
+/* istanbul ignore next */
+@Injectable()
+export class CollectionEffects {
+
+ @Effect()
+ loadFilters$ = this.actions$.pipe(
+ ofType(ProviderCollectionActionTypes.LOAD_PROVIDER_REQUEST),
+ switchMap(() =>
+ this.providerService
+ .query()
+ .pipe(
+ map(providers => new LoadProviderSuccess(providers)),
+ catchError(error => of(new LoadProviderError(error)))
+ )
+ )
+ );
+
+ @Effect()
+ createProvider$ = this.actions$.pipe(
+ ofType(ProviderCollectionActionTypes.ADD_PROVIDER_REQUEST),
+ map(action => action.payload),
+ switchMap(provider =>
+ this.providerService
+ .save(provider)
+ .pipe(
+ map(p => new AddProviderSuccess(p)),
+ catchError((e) => of(new AddProviderFail(e)))
+ )
+ )
+ );
+
+ @Effect({ dispatch: false })
+ createProviderSuccessRedirect$ = this.actions$.pipe(
+ ofType(ProviderCollectionActionTypes.ADD_PROVIDER_SUCCESS),
+ map(action => action.payload),
+ tap(provider => this.router.navigate(['metadata']))
+ );
+
+ @Effect()
+ addResolverSuccessReload$ = this.actions$.pipe(
+ ofType(ProviderCollectionActionTypes.ADD_PROVIDER_SUCCESS),
+ map(action => action.payload),
+ map(provider => new LoadProviderRequest())
+ );
+
+ constructor(
+ private actions$: Actions,
+ private router: Router,
+ private providerService: MetadataProviderService
+ ) { }
+} /* istanbul ignore next */
diff --git a/ui/src/app/metadata/provider/effect/editor.effect.ts b/ui/src/app/metadata/provider/effect/editor.effect.ts
index 7fc04f3bc..bbf1bcc48 100644
--- a/ui/src/app/metadata/provider/effect/editor.effect.ts
+++ b/ui/src/app/metadata/provider/effect/editor.effect.ts
@@ -8,11 +8,14 @@ import {
LoadSchemaFail,
EditorActionTypes
} from '../action/editor.action';
-import { map, switchMap, catchError } from 'rxjs/operators';
+import { map, switchMap, catchError, withLatestFrom } from 'rxjs/operators';
import { of } from 'rxjs';
-import { SetDefinition, WizardActionTypes } from '../../../wizard/action/wizard.action';
+import { SetDefinition, WizardActionTypes, AddSchema } from '../../../wizard/action/wizard.action';
import { ResetChanges } from '../action/entity.action';
+import * as fromWizard from '../../../wizard/reducer';
+import { Store } from '@ngrx/store';
+
@Injectable()
export class EditorEffects {
@@ -30,6 +33,14 @@ export class EditorEffects {
)
);
+ @Effect()
+ $loadSchemaSuccess = this.actions$.pipe(
+ ofType(EditorActionTypes.LOAD_SCHEMA_SUCCESS),
+ map(action => action.payload),
+ withLatestFrom(this.store.select(fromWizard.getWizardIndex)),
+ map(([schema, id]) => new AddSchema({ id, schema }))
+ );
+
@Effect()
$resetChanges = this.actions$.pipe(
ofType(WizardActionTypes.SET_DEFINITION),
@@ -38,6 +49,7 @@ export class EditorEffects {
constructor(
private schemaService: SchemaService,
+ private store: Store,
private actions$: Actions
) { }
} /* istanbul ignore next */
diff --git a/ui/src/app/metadata/provider/model/file-backed-http.provider.form.spec.ts b/ui/src/app/metadata/provider/model/file-backed-http.provider.form.spec.ts
new file mode 100644
index 000000000..4bba62c7d
--- /dev/null
+++ b/ui/src/app/metadata/provider/model/file-backed-http.provider.form.spec.ts
@@ -0,0 +1,110 @@
+import { FileBackedHttpMetadataProviderWizard } from './file-backed-http.provider.form';
+import { FileBackedHttpMetadataProvider } from '../../domain/model/providers';
+
+describe('FileBackedHttpMetadataProviderWizard', () => {
+
+ const parser = FileBackedHttpMetadataProviderWizard.translate.parser;
+ const formatter = FileBackedHttpMetadataProviderWizard.translate.formatter;
+
+ const requiredValidUntilFilter = {
+ maxValidityInterval: 1,
+ '@type': 'RequiredValidUntil'
+ };
+
+ const signatureValidationFilter = {
+ requireSignedRoot: true,
+ certificateFile: 'foo',
+ '@type': 'SignatureValidation'
+ };
+
+ const entityRoleWhiteListFilter = {
+ retainedRoles: ['foo', 'bar'],
+ removeRolelessEntityDescriptors: true,
+ removeEmptyEntitiesDescriptors: true,
+ '@type': 'EntityRoleWhiteList'
+ };
+
+ describe('parser', () => {
+ it('should transform the filters object to an array', () => {
+ let model = {
+ name: 'foo',
+ '@type': 'FileBackedHttpMetadataProvider',
+ enabled: true,
+ resourceId: 'foo',
+ metadataFilters: {
+ RequiredValidUntil: requiredValidUntilFilter,
+ SignatureValidation: signatureValidationFilter,
+ EntityRoleWhiteList: entityRoleWhiteListFilter
+ }
+ };
+ expect(
+ parser(model)
+ ).toEqual(
+ {
+ ...model,
+ metadataFilters: [
+ requiredValidUntilFilter,
+ signatureValidationFilter,
+ entityRoleWhiteListFilter
+ ]
+ }
+ );
+ });
+
+ it('should return the object if metadataFilters is not provided', () => {
+ let model = {
+ name: 'foo',
+ '@type': 'FileBackedHttpMetadataProvider',
+ enabled: true,
+ resourceId: 'foo'
+ };
+ expect(
+ parser(model)
+ ).toEqual(
+ model
+ );
+ });
+ });
+
+ describe('formatter', () => {
+ it('should transform the filters object to an array', () => {
+ let model = {
+ name: 'foo',
+ '@type': 'FileBackedHttpMetadataProvider',
+ enabled: true,
+ resourceId: 'foo',
+ metadataFilters: [
+ requiredValidUntilFilter,
+ signatureValidationFilter,
+ entityRoleWhiteListFilter
+ ]
+ };
+ expect(
+ formatter(model)
+ ).toEqual(
+ {
+ ...model,
+ metadataFilters: {
+ RequiredValidUntil: requiredValidUntilFilter,
+ SignatureValidation: signatureValidationFilter,
+ EntityRoleWhiteList: entityRoleWhiteListFilter
+ }
+ }
+ );
+ });
+
+ it('should return the object if metadataFilters is not provided', () => {
+ let model = {
+ name: 'foo',
+ '@type': 'FileBackedHttpMetadataProvider',
+ enabled: true,
+ resourceId: 'foo'
+ };
+ expect(
+ formatter(model)
+ ).toEqual(
+ model
+ );
+ });
+ });
+});
diff --git a/ui/src/app/metadata/provider/model/file-backed-http.provider.form.ts b/ui/src/app/metadata/provider/model/file-backed-http.provider.form.ts
index 4f77f9e35..41723be8f 100644
--- a/ui/src/app/metadata/provider/model/file-backed-http.provider.form.ts
+++ b/ui/src/app/metadata/provider/model/file-backed-http.provider.form.ts
@@ -1,17 +1,39 @@
import { Wizard } from '../../../wizard/model';
-import { MetadataProvider } from '../../domain/model';
+import { FileBackedHttpMetadataProvider } from '../../domain/model/providers/file-backed-http-metadata-provider';
-export const FileBackedHttpMetadataProviderWizard: Wizard = {
+export const FileBackedHttpMetadataProviderWizard: Wizard = {
label: 'FileBackedHttpMetadataProvider',
- type: '@FileBackedHttpMetadataProvider',
+ type: 'FileBackedHttpMetadataResolver',
+ translate: {
+ parser: (changes: any): FileBackedHttpMetadataProvider => changes.metadataFilters ? ({
+ ...changes,
+ metadataFilters: [
+ ...Object.keys(changes.metadataFilters).reduce((collection, filterName) => ([
+ ...collection,
+ {
+ ...changes.metadataFilters[filterName],
+ '@type': filterName
+ }
+ ]), [])
+ ]
+ }) : changes,
+ formatter: (changes: FileBackedHttpMetadataProvider): any => changes.metadataFilters ? ({
+ ...changes,
+ metadataFilters: {
+ ...(changes.metadataFilters || []).reduce((collection, filter) => ({
+ ...collection,
+ [filter['@type']]: filter
+ }), {})
+ }
+ }) : changes
+ },
steps: [
{
id: 'common',
label: 'Common Attributes',
index: 2,
initialValues: [],
- schema: 'assets/schema/provider/filebacked-http-common.schema.json',
- parser: (changes: Partial, schema: any) => ({ name: '', '@type': '' })
+ schema: 'assets/schema/provider/filebacked-http-common.schema.json'
},
{
id: 'reloading',
@@ -24,8 +46,17 @@ export const FileBackedHttpMetadataProviderWizard: Wizard = {
id: 'filters',
label: 'Metadata Filter Plugins',
index: 4,
- initialValues: [],
+ initialValues: [
+ { key: 'metadataFilters', value: [] }
+ ],
schema: 'assets/schema/provider/filebacked-http-filters.schema.json'
+ },
+ {
+ id: 'summary',
+ label: 'FINISH SUMMARY AND VALIDATION',
+ index: null,
+ initialValues: [],
+ schema: 'assets/schema/provider/metadata-provider-summary.schema.json'
}
]
};
diff --git a/ui/src/app/metadata/provider/model/property.ts b/ui/src/app/metadata/provider/model/property.ts
new file mode 100644
index 000000000..031dce75c
--- /dev/null
+++ b/ui/src/app/metadata/provider/model/property.ts
@@ -0,0 +1,6 @@
+export interface Property {
+ type: string;
+ name: string;
+ value: string[];
+ properties: Property[];
+}
diff --git a/ui/src/app/metadata/provider/model/provider.form.ts b/ui/src/app/metadata/provider/model/provider.form.ts
index 133de467f..698d20134 100644
--- a/ui/src/app/metadata/provider/model/provider.form.ts
+++ b/ui/src/app/metadata/provider/model/provider.form.ts
@@ -4,7 +4,11 @@ import { Metadata } from '../../domain/domain.type';
export const MetadataProviderWizard: Wizard = {
label: 'MetadataProvider',
- type: '@MetadataProvider',
+ type: 'MetadataProvider',
+ translate: {
+ parser: changes => changes,
+ formatter: model => model
+ },
steps: [
{
id: 'new',
diff --git a/ui/src/app/metadata/provider/provider.module.ts b/ui/src/app/metadata/provider/provider.module.ts
index 5379d9c94..104bae13d 100644
--- a/ui/src/app/metadata/provider/provider.module.ts
+++ b/ui/src/app/metadata/provider/provider.module.ts
@@ -5,22 +5,29 @@ import { RouterModule } from '@angular/router';
import { StoreModule } from '@ngrx/store';
import { ProviderWizardComponent } from './container/provider-wizard.component';
-import { NewProviderComponent } from './container/new-provider.component';
+import { ProviderWizardStepComponent } from './container/provider-wizard-step.component';
+import { ProviderWizardSummaryComponent } from './component/provider-wizard-summary.component';
+import { ProviderComponent } from './container/provider.component';
import { WizardModule } from '../../wizard/wizard.module';
import * as fromProvider from './reducer';
import { EffectsModule } from '@ngrx/effects';
import { EditorEffects } from './effect/editor.effect';
-// import { SchemaFormModule } from '../../schema-form/form.module';
-import { SchemaFormModule, WidgetRegistry, DefaultWidgetRegistry } from 'ngx-schema-form';
+import { WidgetRegistry} from 'ngx-schema-form';
import { FormModule } from '../../schema-form/schema-form.module';
import { CustomWidgetRegistry } from '../../schema-form/registry';
+import { SummaryPropertyComponent } from './component/summary-property.component';
+import { CollectionEffects } from './effect/collection.effect';
+import { SharedModule } from '../../shared/shared.module';
@NgModule({
declarations: [
- NewProviderComponent,
- ProviderWizardComponent
+ ProviderComponent,
+ ProviderWizardComponent,
+ ProviderWizardStepComponent,
+ ProviderWizardSummaryComponent,
+ SummaryPropertyComponent
],
entryComponents: [],
imports: [
@@ -28,6 +35,7 @@ import { CustomWidgetRegistry } from '../../schema-form/registry';
CommonModule,
WizardModule,
RouterModule,
+ SharedModule,
FormModule
],
exports: []
@@ -47,7 +55,7 @@ export class ProviderModule {
imports: [
ProviderModule,
StoreModule.forFeature('provider', fromProvider.reducers),
- EffectsModule.forFeature([EditorEffects])
+ EffectsModule.forFeature([EditorEffects, CollectionEffects])
]
})
export class RootProviderModule { }
diff --git a/ui/src/app/metadata/provider/provider.routing.ts b/ui/src/app/metadata/provider/provider.routing.ts
index ad783dc6a..98cdddd85 100644
--- a/ui/src/app/metadata/provider/provider.routing.ts
+++ b/ui/src/app/metadata/provider/provider.routing.ts
@@ -1,12 +1,13 @@
import { Routes } from '@angular/router';
-import { NewProviderComponent } from './container/new-provider.component';
+import { ProviderComponent } from './container/provider.component';
import { ProviderWizardComponent } from './container/provider-wizard.component';
+import { ProviderWizardStepComponent } from './container/provider-wizard-step.component';
export const ProviderRoutes: Routes = [
{
path: 'provider',
- component: NewProviderComponent,
+ component: ProviderComponent,
children: [
{
path: 'wizard',
@@ -14,9 +15,15 @@ export const ProviderRoutes: Routes = [
pathMatch: 'prefix'
},
{
- path: 'wizard/new',
+ path: 'wizard',
component: ProviderWizardComponent,
- canActivate: []
+ canActivate: [],
+ children: [
+ {
+ path: 'new',
+ component: ProviderWizardStepComponent
+ }
+ ]
}
]
}
diff --git a/ui/src/app/metadata/provider/reducer/collection.reducer.spec.ts b/ui/src/app/metadata/provider/reducer/collection.reducer.spec.ts
new file mode 100644
index 000000000..4204ebe65
--- /dev/null
+++ b/ui/src/app/metadata/provider/reducer/collection.reducer.spec.ts
@@ -0,0 +1,50 @@
+import { reducer } from './collection.reducer';
+import * as fromProvider from './collection.reducer';
+import {
+ ProviderCollectionActionTypes,
+ LoadProviderSuccess,
+ UpdateProviderSuccess
+} from '../action/collection.action';
+
+const snapshot: fromProvider.CollectionState = {
+ ids: [],
+ entities: {},
+ selectedProviderId: null,
+ loaded: false
+};
+
+describe('Provider Collection Reducer', () => {
+ describe('undefined action', () => {
+ it('should return the default state', () => {
+ const result = reducer(snapshot, {} as any);
+
+ expect(result).toEqual(snapshot);
+ });
+ });
+
+ describe(`${ProviderCollectionActionTypes.LOAD_PROVIDER_SUCCESS}`, () => {
+ it('should add the loaded providers to the collection', () => {
+ spyOn(fromProvider.adapter, 'addAll').and.callThrough();
+ const providers = [
+ { resourceId: 'foo', name: 'foo', '@type': 'foo', enabled: true, createdDate: new Date().toLocaleDateString() },
+ { resourceId: 'bar', name: 'bar', '@type': 'bar', enabled: false, createdDate: new Date().toLocaleDateString() }
+ ];
+ const action = new LoadProviderSuccess(providers);
+ const result = reducer(snapshot, action);
+ expect(fromProvider.adapter.addAll).toHaveBeenCalled();
+ });
+ });
+
+ describe(`${ProviderCollectionActionTypes.UPDATE_PROVIDER_SUCCESS}`, () => {
+ it('should add the loaded providers to the collection', () => {
+ spyOn(fromProvider.adapter, 'updateOne').and.callThrough();
+ const update = {
+ id: 'foo',
+ changes: { resourceId: 'foo', name: 'bar', createdDate: new Date().toLocaleDateString() },
+ };
+ const action = new UpdateProviderSuccess(update);
+ const result = reducer(snapshot, action);
+ expect(fromProvider.adapter.updateOne).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/ui/src/app/metadata/provider/reducer/collection.reducer.ts b/ui/src/app/metadata/provider/reducer/collection.reducer.ts
new file mode 100644
index 000000000..9a625fa79
--- /dev/null
+++ b/ui/src/app/metadata/provider/reducer/collection.reducer.ts
@@ -0,0 +1,52 @@
+import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity';
+import { ProviderCollectionActionTypes, ProviderCollectionActionsUnion } from '../action/collection.action';
+import { MetadataProvider } from '../../domain/model';
+
+export interface CollectionState extends EntityState {
+ selectedProviderId: string | null;
+ loaded: boolean;
+}
+
+export function sortByDate(a: MetadataProvider, b: MetadataProvider): number {
+ return a.createdDate.localeCompare(b.createdDate);
+}
+
+export const adapter: EntityAdapter = createEntityAdapter({
+ sortComparer: sortByDate,
+ selectId: (model: MetadataProvider) => model.resourceId
+});
+
+export const initialState: CollectionState = adapter.getInitialState({
+ selectedProviderId: null,
+ loaded: false
+});
+
+export function reducer(state = initialState, action: ProviderCollectionActionsUnion): CollectionState {
+ switch (action.type) {
+ case ProviderCollectionActionTypes.LOAD_PROVIDER_SUCCESS: {
+ let s = adapter.addAll(action.payload, {
+ ...state,
+ selectedProviderId: state.selectedProviderId,
+ loaded: true
+ });
+ return s;
+ }
+
+ case ProviderCollectionActionTypes.UPDATE_PROVIDER_SUCCESS: {
+ return adapter.updateOne(action.payload, state);
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
+
+export const getSelectedProviderId = (state: CollectionState) => state.selectedProviderId;
+export const getIsLoaded = (state: CollectionState) => state.loaded;
+export const {
+ selectIds: selectProviderIds,
+ selectEntities: selectProviderEntities,
+ selectAll: selectAllProviders,
+ selectTotal: selectProviderTotal
+} = adapter.getSelectors();
diff --git a/ui/src/app/metadata/provider/reducer/editor.reducer.spec.ts b/ui/src/app/metadata/provider/reducer/editor.reducer.spec.ts
new file mode 100644
index 000000000..d1b58da34
--- /dev/null
+++ b/ui/src/app/metadata/provider/reducer/editor.reducer.spec.ts
@@ -0,0 +1,18 @@
+import { reducer, initialState as snapshot } from './editor.reducer';
+import { EditorActionTypes, ClearEditor } from '../action/editor.action';
+
+describe('Provider Editor Reducer', () => {
+ describe('undefined action', () => {
+ it('should return the default state', () => {
+ const result = reducer(snapshot, {} as any);
+
+ expect(result).toEqual(snapshot);
+ });
+ });
+
+ describe(`${EditorActionTypes.CLEAR}`, () => {
+ it('should reset to initial state', () => {
+ expect(reducer(snapshot, new ClearEditor())).toEqual(snapshot);
+ });
+ });
+});
diff --git a/ui/src/app/metadata/provider/reducer/editor.reducer.ts b/ui/src/app/metadata/provider/reducer/editor.reducer.ts
index 351a17917..609f707f0 100644
--- a/ui/src/app/metadata/provider/reducer/editor.reducer.ts
+++ b/ui/src/app/metadata/provider/reducer/editor.reducer.ts
@@ -24,6 +24,11 @@ export function reducer(state = initialState, action: EditorActionUnion): Editor
type: action.payload
};
}
+ case EditorActionTypes.CLEAR: {
+ return {
+ ...initialState
+ };
+ }
case EditorActionTypes.UPDATE_STATUS: {
return {
...state,
diff --git a/ui/src/app/metadata/provider/reducer/entity.reducer.spec.ts b/ui/src/app/metadata/provider/reducer/entity.reducer.spec.ts
new file mode 100644
index 000000000..3079a99cd
--- /dev/null
+++ b/ui/src/app/metadata/provider/reducer/entity.reducer.spec.ts
@@ -0,0 +1,18 @@
+import { reducer, initialState as snapshot } from './entity.reducer';
+import { EntityActionTypes, ClearProvider } from '../action/entity.action';
+
+describe('Provider Editor Reducer', () => {
+ describe('undefined action', () => {
+ it('should return the default state', () => {
+ const result = reducer(snapshot, {} as any);
+
+ expect(result).toEqual(snapshot);
+ });
+ });
+
+ describe(`${EntityActionTypes.CLEAR_PROVIDER}`, () => {
+ it('should reset to initial state', () => {
+ expect(reducer(snapshot, new ClearProvider())).toEqual(snapshot);
+ });
+ });
+});
diff --git a/ui/src/app/metadata/provider/reducer/entity.reducer.ts b/ui/src/app/metadata/provider/reducer/entity.reducer.ts
index 101674749..5aa7eceb1 100644
--- a/ui/src/app/metadata/provider/reducer/entity.reducer.ts
+++ b/ui/src/app/metadata/provider/reducer/entity.reducer.ts
@@ -15,14 +15,20 @@ export const initialState: EntityState = {
export function reducer(state = initialState, action: EntityActionUnion): EntityState {
switch (action.type) {
+ case EntityActionTypes.CLEAR_PROVIDER: {
+ return {
+ ...initialState
+ };
+ }
case EntityActionTypes.RESET_CHANGES: {
return {
...state,
- changes: initialState.changes
+ changes: {
+ ...initialState.changes
+ }
};
}
- case EntityActionTypes.SELECT_PROVIDER:
- case EntityActionTypes.CREATE_PROVIDER: {
+ case EntityActionTypes.SELECT_PROVIDER: {
return {
...state,
base: {
@@ -39,23 +45,6 @@ export function reducer(state = initialState, action: EntityActionUnion): Entity
}
};
}
- case EntityActionTypes.SAVE_PROVIDER_REQUEST: {
- return {
- ...state,
- saving: true
- };
- }
- case EntityActionTypes.SAVE_PROVIDER_SUCCESS: {
- return {
- ...initialState,
- };
- }
- case EntityActionTypes.SAVE_PROVIDER_FAIL: {
- return {
- ...state,
- saving: false
- };
- }
default: {
return state;
}
diff --git a/ui/src/app/metadata/provider/reducer/index.ts b/ui/src/app/metadata/provider/reducer/index.ts
index c736201eb..ee98fec25 100644
--- a/ui/src/app/metadata/provider/reducer/index.ts
+++ b/ui/src/app/metadata/provider/reducer/index.ts
@@ -2,15 +2,20 @@ import { createSelector, createFeatureSelector } from '@ngrx/store';
import * as fromRoot from '../../../app.reducer';
import * as fromEditor from './editor.reducer';
import * as fromEntity from './entity.reducer';
+import * as fromCollection from './collection.reducer';
+import * as utils from '../../domain/domain.util';
+import { MetadataProvider } from '../../domain/model';
export interface ProviderState {
editor: fromEditor.EditorState;
entity: fromEntity.EntityState;
+ collection: fromCollection.CollectionState;
}
export const reducers = {
editor: fromEditor.reducer,
- entity: fromEntity.reducer
+ entity: fromEntity.reducer,
+ collection: fromCollection.reducer
};
export interface State extends fromRoot.State {
@@ -21,9 +26,11 @@ export const getProviderState = createFeatureSelector('provider')
export const getEditorStateFn = (state: ProviderState) => state.editor;
export const getEntityStateFn = (state: ProviderState) => state.entity;
+export const getCollectionStateFn = (state: ProviderState) => state.collection;
export const getEditorState = createSelector(getProviderState, getEditorStateFn);
export const getEntityState = createSelector(getProviderState, getEntityStateFn);
+export const getCollectionState = createSelector(getProviderState, getCollectionStateFn);
/*
Editor State
@@ -43,4 +50,16 @@ Entity State
export const getEntityIsSaved = createSelector(getEntityState, fromEntity.isEntitySaved);
export const getEntityChanges = createSelector(getEntityState, fromEntity.getEntityChanges);
export const getEntityIsSaving = createSelector(getEntityState, fromEntity.isEditorSaving);
-export const getUpdatedEntity = createSelector(getEntityState, fromEntity.getUpdatedEntity);
\ No newline at end of file
+export const getUpdatedEntity = createSelector(getEntityState, fromEntity.getUpdatedEntity);
+
+/*
+ * Select pieces of Provider Collection
+*/
+export const getAllProviders = createSelector(getCollectionState, fromCollection.selectAllProviders);
+export const getProviderEntities = createSelector(getCollectionState, fromCollection.selectProviderEntities);
+export const getSelectedProviderId = createSelector(getCollectionState, fromCollection.getSelectedProviderId);
+export const getSelectedProvider = createSelector(getProviderEntities, getSelectedProviderId, utils.getInCollectionFn);
+export const getProviderIds = createSelector(getCollectionState, fromCollection.selectProviderIds);
+export const getProviderCollectionIsLoaded = createSelector(getCollectionState, fromCollection.getIsLoaded);
+
+export const getProviderNames = createSelector(getAllProviders, (providers: MetadataProvider[]) => providers.map(p => p.name));
diff --git a/ui/src/app/schema-form/registry.ts b/ui/src/app/schema-form/registry.ts
index 85e7d3a7d..d197dcf2c 100644
--- a/ui/src/app/schema-form/registry.ts
+++ b/ui/src/app/schema-form/registry.ts
@@ -4,17 +4,17 @@ import { CustomStringComponent } from './widget/text/string.component';
import { WidgetRegistry } from 'ngx-schema-form';
-import { ArrayWidget } from 'ngx-schema-form';
import { ButtonWidget } from 'ngx-schema-form';
-import { CheckboxWidget } from 'ngx-schema-form';
import { FileWidget } from 'ngx-schema-form';
import { IntegerWidget } from 'ngx-schema-form';
import { ObjectWidget } from 'ngx-schema-form';
import { RadioWidget } from 'ngx-schema-form';
import { RangeWidget } from 'ngx-schema-form';
-import { TextAreaWidget } from 'ngx-schema-form';
import { CustomSelectComponent } from './widget/select/select.component';
import { DatalistComponent } from './widget/datalist/datalist.component';
+import { CustomCheckboxComponent } from './widget/check/checkbox.component';
+import { CustomTextAreaComponent } from './widget/textarea/textarea.component';
+import { CustomArrayComponent } from './widget/array/array.component';
export class CustomWidgetRegistry extends WidgetRegistry {
@@ -34,26 +34,27 @@ export class CustomWidgetRegistry extends WidgetRegistry {
this.register('time', CustomStringComponent);
this.register('boolean-radio', BooleanRadioComponent);
+
this.register('fieldset', FieldsetComponent);
+ this.register('array', CustomArrayComponent);
this.register('select', CustomSelectComponent);
+ this.register('boolean', CustomCheckboxComponent);
+ this.register('checkbox', CustomCheckboxComponent);
+
+ this.register('textarea', CustomTextAreaComponent);
this.register('datalist', DatalistComponent);
/* NGX-Form */
- this.register('array', ArrayWidget);
this.register('object', ObjectWidget);
this.register('integer', IntegerWidget);
this.register('number', IntegerWidget);
this.register('range', RangeWidget);
- this.register('textarea', TextAreaWidget);
-
this.register('file', FileWidget);
this.register('radio', RadioWidget);
- this.register('boolean', CheckboxWidget);
- this.register('checkbox', CheckboxWidget);
this.register('button', ButtonWidget);
diff --git a/ui/src/app/schema-form/schema-form.module.ts b/ui/src/app/schema-form/schema-form.module.ts
index 95549b7de..d51ae2269 100644
--- a/ui/src/app/schema-form/schema-form.module.ts
+++ b/ui/src/app/schema-form/schema-form.module.ts
@@ -11,13 +11,19 @@ import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap';
import { SharedModule } from '../shared/shared.module';
import { CustomSelectComponent } from './widget/select/select.component';
import { DatalistComponent } from './widget/datalist/datalist.component';
+import { CustomCheckboxComponent } from './widget/check/checkbox.component';
+import { CustomTextAreaComponent } from './widget/textarea/textarea.component';
+import { CustomArrayComponent } from './widget/array/array.component';
export const COMPONENTS = [
BooleanRadioComponent,
FieldsetComponent,
CustomStringComponent,
CustomSelectComponent,
- DatalistComponent
+ DatalistComponent,
+ CustomCheckboxComponent,
+ CustomTextAreaComponent,
+ CustomArrayComponent
];
@NgModule({
diff --git a/ui/src/app/schema-form/widget/array/array.component.html b/ui/src/app/schema-form/widget/array/array.component.html
new file mode 100644
index 000000000..577c084f9
--- /dev/null
+++ b/ui/src/app/schema-form/widget/array/array.component.html
@@ -0,0 +1,23 @@
+
\ No newline at end of file
diff --git a/ui/src/app/schema-form/widget/array/array.component.ts b/ui/src/app/schema-form/widget/array/array.component.ts
new file mode 100644
index 000000000..52987cda7
--- /dev/null
+++ b/ui/src/app/schema-form/widget/array/array.component.ts
@@ -0,0 +1,9 @@
+import { Component } from '@angular/core';
+
+import { ArrayWidget } from 'ngx-schema-form';
+
+@Component({
+ selector: 'array-component',
+ templateUrl: `./array.component.html`
+})
+export class CustomArrayComponent extends ArrayWidget {}
diff --git a/ui/src/app/schema-form/widget/check/checkbox.component.html b/ui/src/app/schema-form/widget/check/checkbox.component.html
new file mode 100644
index 000000000..f8f94af1a
--- /dev/null
+++ b/ui/src/app/schema-form/widget/check/checkbox.component.html
@@ -0,0 +1,37 @@
+
diff --git a/ui/src/app/schema-form/widget/check/checkbox.component.ts b/ui/src/app/schema-form/widget/check/checkbox.component.ts
new file mode 100644
index 000000000..ebb784d71
--- /dev/null
+++ b/ui/src/app/schema-form/widget/check/checkbox.component.ts
@@ -0,0 +1,9 @@
+import { Component } from '@angular/core';
+
+import { CheckboxWidget } from 'ngx-schema-form';
+
+@Component({
+ selector: 'checkbox-component',
+ templateUrl: `./checkbox.component.html`
+})
+export class CustomCheckboxComponent extends CheckboxWidget { }
diff --git a/ui/src/app/schema-form/widget/select/select.component.html b/ui/src/app/schema-form/widget/select/select.component.html
index 831a7e852..7b4bf8173 100644
--- a/ui/src/app/schema-form/widget/select/select.component.html
+++ b/ui/src/app/schema-form/widget/select/select.component.html
@@ -1,7 +1,7 @@
-
\ No newline at end of file
diff --git a/ui/src/app/schema-form/widget/text/string.component.ts b/ui/src/app/schema-form/widget/text/string.component.ts
index 26c1f5c44..cf46aa6e2 100644
--- a/ui/src/app/schema-form/widget/text/string.component.ts
+++ b/ui/src/app/schema-form/widget/text/string.component.ts
@@ -1,18 +1,9 @@
import { Component } from '@angular/core';
-import { ControlWidget } from 'ngx-schema-form';
+import { StringWidget } from 'ngx-schema-form';
@Component({
selector: 'custom-string',
templateUrl: `./string.component.html`,
styleUrls: ['../widget.component.scss']
})
-export class CustomStringComponent extends ControlWidget {
-
- getInputType() {
- if (!this.schema.widget.id || this.schema.widget.id === 'string') {
- return 'text';
- } else {
- return this.schema.widget.id;
- }
- }
-}
+export class CustomStringComponent extends StringWidget {}
diff --git a/ui/src/app/schema-form/widget/textarea/textarea.component.html b/ui/src/app/schema-form/widget/textarea/textarea.component.html
new file mode 100644
index 000000000..55260ac43
--- /dev/null
+++ b/ui/src/app/schema-form/widget/textarea/textarea.component.html
@@ -0,0 +1,21 @@
+
+
+ {{ schema.title }}
+
+ {{ schema.description }}
+
+
+
+ {{schema.description}}
+
+
\ No newline at end of file
diff --git a/ui/src/app/schema-form/widget/textarea/textarea.component.ts b/ui/src/app/schema-form/widget/textarea/textarea.component.ts
new file mode 100644
index 000000000..6586d7d1f
--- /dev/null
+++ b/ui/src/app/schema-form/widget/textarea/textarea.component.ts
@@ -0,0 +1,9 @@
+import { Component } from '@angular/core';
+
+import { TextAreaWidget } from 'ngx-schema-form';
+
+@Component({
+ selector: 'textarea-component',
+ templateUrl: `./textarea.component.html`
+})
+export class CustomTextAreaComponent extends TextAreaWidget {}
diff --git a/ui/src/app/shared/shared.module.ts b/ui/src/app/shared/shared.module.ts
index 4af6af8ce..f166ad254 100644
--- a/ui/src/app/shared/shared.module.ts
+++ b/ui/src/app/shared/shared.module.ts
@@ -35,6 +35,7 @@ import { PrettyXml } from './pipe/pretty-xml.pipe';
InputDefaultsDirective,
I18nTextComponent,
ValidFormIconComponent,
+ ValidationClassDirective,
InfoLabelDirective
]
})
diff --git a/ui/src/app/wizard/action/wizard.action.ts b/ui/src/app/wizard/action/wizard.action.ts
index b144601ae..ecb507cb4 100644
--- a/ui/src/app/wizard/action/wizard.action.ts
+++ b/ui/src/app/wizard/action/wizard.action.ts
@@ -7,8 +7,12 @@ export enum WizardActionTypes {
UPDATE_DEFINITION = '[Wizard] Update Definition',
SET_DISABLED = '[Wizard] Set Disabled',
+ ADD_SCHEMA = '[Wizard] Add Schema',
+
NEXT = '[Wizard] Next Page',
- PREVIOUS = '[Wizard] Previous Page'
+ PREVIOUS = '[Wizard] Previous Page',
+
+ CLEAR = '[Wizard] Clear'
}
export class SetIndex implements Action {
@@ -47,10 +51,22 @@ export class Previous implements Action {
constructor(public payload: string) { }
}
+export class AddSchema implements Action {
+ readonly type = WizardActionTypes.ADD_SCHEMA;
+
+ constructor(public payload: { id: string, schema: any }) { }
+}
+
+export class ClearWizard implements Action {
+ readonly type = WizardActionTypes.CLEAR;
+}
+
export type WizardActionUnion =
| SetIndex
| SetDefinition
| UpdateDefinition
| SetDisabled
| Next
- | Previous;
+ | Previous
+ | ClearWizard
+ | AddSchema;
diff --git a/ui/src/app/wizard/component/wizard.component.html b/ui/src/app/wizard/component/wizard.component.html
index e5921c6ac..3d62f7d7a 100644
--- a/ui/src/app/wizard/component/wizard.component.html
+++ b/ui/src/app/wizard/component/wizard.component.html
@@ -8,21 +8,26 @@
Back
- {{ (previous$ | async).index }}. {{ (previous$ | async).label }}
+ {{ (previous$ | async).index }}.
+ {{ (previous$ | async).label }}
- {{ (current$ | async).index }}
- {{ (current$ | async).index }}. {{ (current$ | async).label }}
+ {{ (current$ | async).index }}
+
+ {{ (current$ | async).index }}.
+
+ {{ (current$ | async).label }}
-
-
-
diff --git a/ui/src/app/wizard/component/wizard.component.ts b/ui/src/app/wizard/component/wizard.component.ts
index 5ab006519..da9cdafb0 100644
--- a/ui/src/app/wizard/component/wizard.component.ts
+++ b/ui/src/app/wizard/component/wizard.component.ts
@@ -13,6 +13,7 @@ import { Observable } from 'rxjs';
export class WizardComponent implements OnChanges {
@Output() onNext = new EventEmitter();
@Output() onPrevious = new EventEmitter();
+ @Output() onLast = new EventEmitter();
@Output() onSave = new EventEmitter();
currentPage: any = {};
diff --git a/ui/src/app/wizard/guard/step-exists.guard.ts b/ui/src/app/wizard/guard/step-exists.guard.ts
deleted file mode 100644
index cd382ee14..000000000
--- a/ui/src/app/wizard/guard/step-exists.guard.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { Injectable } from '@angular/core';
-import {
- CanActivate,
- Router,
- ActivatedRouteSnapshot,
- RouterStateSnapshot
-} from '@angular/router';
-import { Store } from '@ngrx/store';
-import { Observable, of } from 'rxjs';
-import { map, tap } from 'rxjs/operators';
-
-import * as fromWizard from '../reducer';
-
-@Injectable()
-export class StepExistsGuard implements CanActivate {
- constructor(
- private store: Store,
- private router: Router
- ) { }
-
- canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable {
- this.store.select(fromWizard.getWizardDefinition).pipe(
- map(current => !!current)
- ).subscribe(defined => console.log(defined));
- return of(true);
- }
-}
-
-// !isDefined ? this.router.navigate(['metadata/provider/wizard/new']) : isDefined
diff --git a/ui/src/app/wizard/model/wizard.ts b/ui/src/app/wizard/model/wizard.ts
index bcceb9f00..8b5fe344e 100644
--- a/ui/src/app/wizard/model/wizard.ts
+++ b/ui/src/app/wizard/model/wizard.ts
@@ -1,9 +1,11 @@
-import { MetadataProvider } from '../../metadata/domain/model';
-
export interface Wizard {
label: string;
type: string;
steps: WizardStep[];
+ translate: {
+ parser(changes: Partial, schema?: any),
+ formatter(changes: Partial, schema?: any)
+ };
}
export interface WizardStep {
@@ -12,7 +14,6 @@ export interface WizardStep {
initialValues?: WizardValue[];
schema: string;
index: number;
- parser?(changes: Partial, schema: any);
}
export interface WizardValue {
diff --git a/ui/src/app/wizard/reducer/index.spec.ts b/ui/src/app/wizard/reducer/index.spec.ts
new file mode 100644
index 000000000..78ff09441
--- /dev/null
+++ b/ui/src/app/wizard/reducer/index.spec.ts
@@ -0,0 +1,91 @@
+import * as selectors from './';
+import { FileBackedHttpMetadataProviderWizard } from '../../metadata/provider/model';
+
+describe('wizard index selectors', () => {
+ describe('getSchema method', () => {
+ it('should return the schema by index name', () => {
+ expect(
+ selectors.getSchema('common', FileBackedHttpMetadataProviderWizard)
+ ).toBe(FileBackedHttpMetadataProviderWizard.steps[0].schema);
+ });
+ it('should return nothing if no schema is found', () => {
+ expect(
+ selectors.getSchema('common', null)
+ ).toBeFalsy();
+ });
+ });
+ describe('getPreviousFn method', () => {
+ it('should return the previous step', () => {
+ expect(
+ selectors.getPreviousFn('reloading', FileBackedHttpMetadataProviderWizard)
+ ).toBe(FileBackedHttpMetadataProviderWizard.steps[0]);
+ });
+ it('should return null if the index is the first step', () => {
+ expect(
+ selectors.getPreviousFn('common', FileBackedHttpMetadataProviderWizard)
+ ).toBeFalsy();
+ });
+ it('should return nothing if no schema is found', () => {
+ expect(
+ selectors.getPreviousFn('common', null)
+ ).toBeFalsy();
+ });
+ });
+
+ describe('getNextFn method', () => {
+ it('should return the previous step', () => {
+ expect(
+ selectors.getNextFn('common', FileBackedHttpMetadataProviderWizard)
+ ).toBe(FileBackedHttpMetadataProviderWizard.steps[1]);
+ });
+ it('should return null if the index is the last step', () => {
+ expect(
+ selectors.getNextFn('summary', FileBackedHttpMetadataProviderWizard)
+ ).toBeFalsy();
+ });
+ it('should return nothing if no schema is found', () => {
+ expect(
+ selectors.getNextFn('common', null)
+ ).toBeFalsy();
+ });
+ });
+
+ describe('getCurrentFn method', () => {
+ it('should return the current step', () => {
+ expect(
+ selectors.getCurrentFn('common', FileBackedHttpMetadataProviderWizard)
+ ).toBe(FileBackedHttpMetadataProviderWizard.steps[0]);
+ });
+ it('should return nothing if no schema is found', () => {
+ expect(
+ selectors.getCurrentFn('common', null)
+ ).toBeFalsy();
+ });
+ });
+
+ describe('getLastFn method', () => {
+ it('should return the last step', () => {
+ expect(
+ selectors.getLastFn('summary', FileBackedHttpMetadataProviderWizard)
+ ).toBe(FileBackedHttpMetadataProviderWizard.steps.find(step => step.id === 'summary'));
+ });
+ it('should return nothing if no definition is provided', () => {
+ expect(
+ selectors.getLastFn('common', null)
+ ).toBeFalsy();
+ });
+ it('should return nothing if no schema is found', () => {
+ expect(
+ selectors.getLastFn('common', FileBackedHttpMetadataProviderWizard)
+ ).toBeFalsy();
+ });
+ });
+
+ describe('getModelFn method', () => {
+ it('should return the model', () => {
+ const step = FileBackedHttpMetadataProviderWizard.steps.find(s => s.id === 'filters');
+ console.log(step);
+ expect(selectors.getModelFn(step)).toEqual({ metadataFilters: [] });
+ });
+ });
+});
diff --git a/ui/src/app/wizard/reducer/index.ts b/ui/src/app/wizard/reducer/index.ts
index 1119d5d14..7e3db4c01 100644
--- a/ui/src/app/wizard/reducer/index.ts
+++ b/ui/src/app/wizard/reducer/index.ts
@@ -1,7 +1,7 @@
import * as fromRoot from '../../app.reducer';
import * as fromWizard from './wizard.reducer';
import { createFeatureSelector, createSelector } from '@ngrx/store';
-import { Wizard } from '../model';
+import { Wizard, WizardStep } from '../model';
export interface WizardState {
wizard: fromWizard.State;
@@ -22,13 +22,14 @@ export const getState = createSelector(getWizardState, getWizardStateFn);
export const getWizardIndex = createSelector(getState, fromWizard.getIndex);
export const getWizardIsDisabled = createSelector(getState, fromWizard.getDisabled);
export const getWizardDefinition = createSelector(getState, fromWizard.getDefinition);
+export const getSchemaCollection = createSelector(getState, fromWizard.getCollection);
export const getSchema = (index: string, wizard: Wizard) => {
+ if (!wizard) { return null; }
const step = wizard.steps.find(s => s.id === index);
return step ? step.schema : null;
};
-
export const getCurrentWizardSchema = createSelector(getWizardIndex, getWizardDefinition, getSchema);
export const getPreviousFn = (index: string, wizard: Wizard) => {
@@ -54,7 +55,13 @@ export const getLastFn = (index: string, wizard: Wizard) => {
return index === step.id ? step : null;
};
+export const getModelFn = (currentStep: WizardStep) => {
+ const model = (currentStep && currentStep.initialValues) ? currentStep.initialValues : [];
+ return model.reduce((m, property) => ({...m, [property.key]: property.value }), {});
+};
+
export const getPrevious = createSelector(getWizardIndex, getWizardDefinition, getPreviousFn);
export const getCurrent = createSelector(getWizardIndex, getWizardDefinition, getCurrentFn);
export const getNext = createSelector(getWizardIndex, getWizardDefinition, getNextFn);
export const getLast = createSelector(getWizardIndex, getWizardDefinition, getLastFn);
+export const getModel = createSelector(getCurrent, getModelFn);
diff --git a/ui/src/app/wizard/reducer/wizard.reducer.spec.ts b/ui/src/app/wizard/reducer/wizard.reducer.spec.ts
new file mode 100644
index 000000000..c922c226d
--- /dev/null
+++ b/ui/src/app/wizard/reducer/wizard.reducer.spec.ts
@@ -0,0 +1,74 @@
+import { reducer, initialState as snapshot } from './wizard.reducer';
+import * as selectors from './wizard.reducer';
+import { WizardActionTypes, ClearWizard, AddSchema, SetDisabled, SetDefinition, SetIndex, UpdateDefinition } from '../action/wizard.action';
+import { SCHEMA } from '../../../testing/form-schema.stub';
+import { MetadataProviderWizard, FileBackedHttpMetadataProviderWizard } from '../../metadata/provider/model';
+
+
+
+describe('Wizard Reducer', () => {
+ describe('undefined action', () => {
+ it('should return the default state', () => {
+ const result = reducer(snapshot, {} as any);
+
+ expect(result).toEqual(snapshot);
+ });
+ });
+
+ describe(`${WizardActionTypes.CLEAR}`, () => {
+ it('should reset to initial state', () => {
+ expect(reducer(snapshot, new ClearWizard())).toEqual(snapshot);
+ });
+ });
+
+ describe(`${WizardActionTypes.ADD_SCHEMA}`, () => {
+ it('should add the payload to the schema collection', () => {
+ expect(reducer(snapshot, new AddSchema({id: 'foo', schema: SCHEMA })).schemaCollection).toEqual({ 'foo': SCHEMA });
+ });
+ });
+
+ describe(`${WizardActionTypes.SET_DISABLED}`, () => {
+ it('should set the disabled property on the wizard', () => {
+ expect(reducer(snapshot, new SetDisabled(true)).disabled).toBe(true);
+ expect(reducer(snapshot, new SetDisabled(false)).disabled).toBe(false);
+ });
+ });
+
+ describe(`${WizardActionTypes.SET_DEFINITION}`, () => {
+ it('should set the definition property on the wizard', () => {
+ expect(reducer(snapshot, new SetDefinition(MetadataProviderWizard)).definition).toBe(MetadataProviderWizard);
+ });
+ });
+
+ describe(`${WizardActionTypes.SET_INDEX}`, () => {
+ it('should set the definition property on the wizard', () => {
+ expect(reducer(snapshot, new SetIndex(MetadataProviderWizard.steps[0].id)).index).toBe('new');
+ });
+ });
+
+ describe(`${WizardActionTypes.SET_INDEX}`, () => {
+ let state = reducer(snapshot, new SetDefinition(MetadataProviderWizard));
+ it('should set the definition property on the wizard', () => {
+ expect(reducer(state, new UpdateDefinition(FileBackedHttpMetadataProviderWizard))).toEqual({
+ ...state,
+ definition: {
+ ...MetadataProviderWizard,
+ ...FileBackedHttpMetadataProviderWizard,
+ steps: [
+ ...MetadataProviderWizard.steps,
+ ...FileBackedHttpMetadataProviderWizard.steps
+ ]
+ }
+ });
+ });
+ });
+
+ describe('selector functions', () => {
+ it('should return pieces of state', () => {
+ expect(selectors.getCollection(snapshot)).toEqual(snapshot.schemaCollection);
+ expect(selectors.getDefinition(snapshot)).toEqual(snapshot.definition);
+ expect(selectors.getDisabled(snapshot)).toEqual(snapshot.disabled);
+ expect(selectors.getIndex(snapshot)).toEqual(snapshot.index);
+ });
+ });
+});
diff --git a/ui/src/app/wizard/reducer/wizard.reducer.ts b/ui/src/app/wizard/reducer/wizard.reducer.ts
index de40f86a3..9ecae96ce 100644
--- a/ui/src/app/wizard/reducer/wizard.reducer.ts
+++ b/ui/src/app/wizard/reducer/wizard.reducer.ts
@@ -5,16 +5,27 @@ export interface State {
index: string;
disabled: boolean;
definition: Wizard;
+ schemaCollection: { [id: string]: any };
}
export const initialState: State = {
index: null,
disabled: false,
- definition: null
+ definition: null,
+ schemaCollection: {}
};
export function reducer(state = initialState, action: WizardActionUnion): State {
switch (action.type) {
+ case WizardActionTypes.ADD_SCHEMA: {
+ return {
+ ...state,
+ schemaCollection: {
+ ...state.schemaCollection,
+ [action.payload.id]: action.payload.schema
+ }
+ };
+ }
case WizardActionTypes.SET_DISABLED: {
return {
...state,
@@ -56,3 +67,4 @@ export function reducer(state = initialState, action: WizardActionUnion): State
export const getIndex = (state: State) => state.index;
export const getDisabled = (state: State) => state.disabled;
export const getDefinition = (state: State) => state.definition;
+export const getCollection = (state: State) => state.schemaCollection;
diff --git a/ui/src/app/wizard/wizard.module.ts b/ui/src/app/wizard/wizard.module.ts
index 6664ec16a..8612649b6 100644
--- a/ui/src/app/wizard/wizard.module.ts
+++ b/ui/src/app/wizard/wizard.module.ts
@@ -5,7 +5,6 @@ import { EffectsModule } from '@ngrx/effects';
import { WizardComponent } from './component/wizard.component';
import { reducers } from './reducer';
-import { StepExistsGuard } from './guard/step-exists.guard';
@NgModule({
declarations: [
@@ -23,9 +22,7 @@ export class WizardModule {
static forRoot(): ModuleWithProviders {
return {
ngModule: RootWizardModule,
- providers: [
- StepExistsGuard
- ]
+ providers: []
};
}
}
diff --git a/ui/src/assets/schema/provider/filebacked-http-common.schema.json b/ui/src/assets/schema/provider/filebacked-http-common.schema.json
index 806c7c053..3bf433b29 100644
--- a/ui/src/assets/schema/provider/filebacked-http-common.schema.json
+++ b/ui/src/assets/schema/provider/filebacked-http-common.schema.json
@@ -11,16 +11,19 @@
"useDefaultPredicateRegistry",
"satisfyAnyPredicates"
],
+ "required": ["id", "metadataURL"],
"properties": {
"id": {
"title": "ID",
"description": "Unique Identifier",
- "type": "string"
+ "type": "string",
+ "default": ""
},
"metadataURL": {
"title": "Metadata URL",
"description": "Metadata URL",
- "type": "string"
+ "type": "string",
+ "default": ""
},
"initializeFromBackupFile": {
"title": "Initialize From Backup File?",
diff --git a/ui/src/assets/schema/provider/filebacked-http-filters.schema.json b/ui/src/assets/schema/provider/filebacked-http-filters.schema.json
index 2b3d32846..98ec3eed9 100644
--- a/ui/src/assets/schema/provider/filebacked-http-filters.schema.json
+++ b/ui/src/assets/schema/provider/filebacked-http-filters.schema.json
@@ -6,7 +6,7 @@
"description": "",
"type": "object",
"properties": {
- "@RequiredValidUntilFilter": {
+ "RequiredValidUntil": {
"title": "Required Valid Until Filter",
"type": "object",
"widget": {
@@ -15,12 +15,13 @@
"properties": {
"maxValidityInterval": {
"title": "Max Validity Interval",
- "description": "",
- "type": "number"
+ "description": "Max Validity Interval",
+ "type": "number",
+ "default": 0
}
}
},
- "@SignatureValidationFilter": {
+ "SignatureValidation": {
"title": "Signature Validation Filter",
"type": "object",
"widget": {
@@ -29,18 +30,20 @@
"properties": {
"requireSignedRoot": {
"title": "Require Signed Root",
- "description": "",
+ "description": "Require Signed Root",
"type": "boolean",
"default": true
},
"certificateFile": {
"title": "Certificate File",
- "description": "",
- "type": "string"
+ "description": "Certificate File",
+ "type": "string",
+ "widget": "textarea",
+ "default": ""
}
}
},
- "@EntityRoleWhiteListFilter": {
+ "EntityRoleWhiteList": {
"title": "Entity Role Whitelist Filter",
"type": "object",
"widget": {
@@ -49,7 +52,7 @@
"properties": {
"retainedRoles": {
"title": "Retained Roles",
- "description": "",
+ "description": "Retained Roles",
"type": "array",
"items": {
"widget": {
@@ -74,13 +77,13 @@
},
"removeRolelessEntityDescriptors": {
"title": "Remove Roleless Entity Descriptors?",
- "description": "",
+ "description": "Remove Roleless Entity Descriptors?",
"type": "boolean",
"default": true
},
"removeEmptyEntitiesDescriptors": {
"title": "Remove Empty Entities Descriptors?",
- "description": "",
+ "description": "Remove Empty Entities Descriptors?",
"type": "boolean",
"default": true
}
diff --git a/ui/src/assets/schema/provider/filebacked-http-reloading.schema.json b/ui/src/assets/schema/provider/filebacked-http-reloading.schema.json
index 9595d904c..9fa60f808 100644
--- a/ui/src/assets/schema/provider/filebacked-http-reloading.schema.json
+++ b/ui/src/assets/schema/provider/filebacked-http-reloading.schema.json
@@ -21,7 +21,8 @@
"PT12H",
"PT24H"
]
- }
+ },
+ "default": ""
},
"maxRefreshDelay": {
"title": "Max Refresh Delay",
@@ -40,12 +41,14 @@
"PT12H",
"PT24H"
]
- }
+ },
+ "default": ""
},
"refreshDelayFactor": {
"title": "Refresh Delay Factor",
"description": "",
- "type": "number"
+ "type": "number",
+ "default": null
},
"resolveViaPredicatesOnly": {
"title": "Resolve Via Predicates Only?",
@@ -87,7 +90,8 @@
"PT12H",
"PT24H"
]
- }
+ },
+ "default": ""
}
}
}
diff --git a/ui/src/assets/schema/provider/metadata-provider-summary.schema.json b/ui/src/assets/schema/provider/metadata-provider-summary.schema.json
new file mode 100644
index 000000000..b3cc3b73c
--- /dev/null
+++ b/ui/src/assets/schema/provider/metadata-provider-summary.schema.json
@@ -0,0 +1,11 @@
+{
+ "type": "object",
+ "properties": {
+ "enabled": {
+ "title": "Enable Metadata Provider",
+ "description": "Enable Metadata Provider",
+ "type": "boolean",
+ "default": true
+ }
+ }
+}
\ No newline at end of file
diff --git a/ui/src/assets/schema/provider/metadata-provider.schema.json b/ui/src/assets/schema/provider/metadata-provider.schema.json
index 241a57e85..f5a840f35 100644
--- a/ui/src/assets/schema/provider/metadata-provider.schema.json
+++ b/ui/src/assets/schema/provider/metadata-provider.schema.json
@@ -1,5 +1,5 @@
{
- "title": "@MetadataResolver",
+ "title": "MetadataResolver",
"type": "object",
"widget": {
"id": "fieldset"
@@ -8,7 +8,11 @@
"name": {
"title": "Metadata Provider Name (Dashboard Display Only)",
"description": "Metadata Provider Name (Dashboard Display Only)",
- "type": "string"
+ "type": "string",
+ "widget": {
+ "id": "string",
+ "help": "Must be unique."
+ }
},
"@type": {
"title": "Metadata Provider Type",
@@ -21,7 +25,7 @@
"oneOf": [
{
"enum": [
- "@FileBackedHttpMetadataProvider"
+ "FileBackedHttpMetadataResolver"
],
"description": "FileBackedHttpMetadataProvider"
}
diff --git a/ui/src/testing/form-schema.stub.ts b/ui/src/testing/form-schema.stub.ts
new file mode 100644
index 000000000..c91f63bf1
--- /dev/null
+++ b/ui/src/testing/form-schema.stub.ts
@@ -0,0 +1,48 @@
+export const SCHEMA = {
+ 'title': 'MetadataResolver',
+ 'type': 'object',
+ 'widget': {
+ 'id': 'fieldset'
+ },
+ 'properties': {
+ 'name': {
+ 'title': 'Metadata Provider Name (Dashboard Display Only)',
+ 'description': 'Metadata Provider Name (Dashboard Display Only)',
+ 'type': 'string',
+ 'widget': {
+ 'id': 'string',
+ 'help': 'Must be unique.'
+ }
+ },
+ '@type': {
+ 'title': 'Metadata Provider Type',
+ 'description': 'Metadata Provider Type',
+ 'ui:placeholder': 'Select a metadata provider type',
+ 'type': 'string',
+ 'widget': {
+ 'id': 'select'
+ },
+ 'oneOf': [
+ {
+ 'enum': [
+ 'FileBackedHttpMetadataResolver'
+ ],
+ 'description': 'FileBackedHttpMetadataProvider'
+ }
+ ]
+ }
+ },
+ 'required': [
+ 'name',
+ '@type'
+ ],
+ 'fieldsets': [
+ {
+ 'type': 'section',
+ 'fields': [
+ 'name',
+ '@type'
+ ]
+ }
+ ]
+};