diff --git a/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerVersionEndpointsIntegrationTests.groovy b/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerVersionEndpointsIntegrationTests.groovy
index 99b5810ce..82b143c00 100644
--- a/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerVersionEndpointsIntegrationTests.groovy
+++ b/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerVersionEndpointsIntegrationTests.groovy
@@ -1,11 +1,22 @@
package edu.internet2.tier.shibboleth.admin.ui.controller
import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor
+import edu.internet2.tier.shibboleth.admin.ui.domain.Organization
+import edu.internet2.tier.shibboleth.admin.ui.domain.OrganizationDisplayName
+import edu.internet2.tier.shibboleth.admin.ui.domain.OrganizationName
+import edu.internet2.tier.shibboleth.admin.ui.domain.OrganizationURL
import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation
import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository
+import groovy.json.JsonOutput
+import groovy.json.JsonSlurper
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.web.client.TestRestTemplate
+import org.springframework.http.HttpEntity
+import org.springframework.http.HttpHeaders
+import org.springframework.http.HttpMethod
+import org.springframework.http.MediaType
+import org.springframework.test.annotation.DirtiesContext
import org.springframework.test.context.ActiveProfiles
import spock.lang.Specification
@@ -100,6 +111,46 @@ class EntityDescriptorControllerVersionEndpointsIntegrationTests extends Specifi
edv2.body.serviceProviderName == 'SP2'
}
+ @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD)
+ def 'SHIBUI-1414'() {
+ given:
+ def ed = new EntityDescriptor(entityID: 'testme', serviceProviderName: 'testme').with {
+ entityDescriptorRepository.save(it)
+ }.with {
+ it.setOrganization(new Organization().with {
+ it.organizationNames = [new OrganizationName(value: 'testme', XMLLang: 'en')]
+ it.organizationDisplayNames = [new OrganizationDisplayName(value: 'testme', XMLLang: 'en')]
+ it.organizationURLs = [new OrganizationURL(value: 'http://testme.org', XMLLang: 'en')]
+ it
+ })
+ entityDescriptorRepository.save(it)
+ }
+
+ when:
+ def headers = new HttpHeaders().with {
+ it.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
+ it
+ }
+
+ def allVersions = getAllEntityDescriptorVersions(ed.resourceId, List)
+ def edv1 = getEntityDescriptorForVersion(ed.resourceId, allVersions.body[0].id, String).body
+ def tedv2 = getEntityDescriptorForVersion(ed.resourceId, allVersions.body[1].id, EntityDescriptorRepresentation).body
+
+ def aedv1 = new JsonSlurper().parseText(edv1).with {
+ it.put('version', tedv2.version)
+ it
+ }.with {
+ JsonOutput.toJson(it)
+ }
+
+ def request = new HttpEntity(aedv1, headers)
+ def response = this.restTemplate.exchange("/api/EntityDescriptor/${ed.resourceId}", HttpMethod.PUT, request, String)
+
+ then:
+ response.statusCodeValue != 400
+ noExceptionThrown()
+ }
+
private getAllEntityDescriptorVersions(String resourceId, responseType) {
this.restTemplate.getForEntity(resourceUriFor(ALL_VERSIONS_URI, resourceId), responseType)
}
diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties
index 559a6c082..909b2943a 100644
--- a/backend/src/main/resources/application.properties
+++ b/backend/src/main/resources/application.properties
@@ -20,6 +20,7 @@ spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.h2.console.enabled=true
# spring.jackson.default-property-inclusion=non_absent
+spring.jackson.default-property-inclusion=NON_NULL
# Database Configuration PostgreSQL
#spring.datasource.url=jdbc:postgresql://localhost:5432/shibui
diff --git a/backend/src/main/resources/i18n/messages.properties b/backend/src/main/resources/i18n/messages.properties
index f82153ce6..621dc4e63 100644
--- a/backend/src/main/resources/i18n/messages.properties
+++ b/backend/src/main/resources/i18n/messages.properties
@@ -55,6 +55,7 @@ action.xml=XML
action.manage=Manage
action.close=Close
action.back-to-top=Back to Top
+action.restore=Restore
value.enabled=Enabled
value.disabled=Disabled
@@ -416,6 +417,7 @@ label.check-to-select=Check to select
label.current=Current
label.restore=Restore
label.compare-selected=Compare Selected
+label.restore-version=Restore Version ({ date })
label.saved=Saved
label.by=By
@@ -471,6 +473,9 @@ message.database-constraint=There was a database constraint problem processing t
message.no-filters=No Filters
message.no-filters-added=No filters have been added to this Metadata Provider
+message.create-new-version-from-version=Create New Version from Previous Settings
+message.restoring-this-version-will-copy=Restoring this version will copy the Version ({ date }) configuration and create a new Version from the selected version settings. You can then edit the configuration before saving the new version.
+
tooltip.entity-id=Entity ID
tooltip.service-provider-name=Service Provider Name (Dashboard Display Only)
tooltip.force-authn=Disallows use (or reuse) of authentication results and login flows that don\u0027t provide a real-time proof of user presence in the login process
diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/BadJSONMetadataSourcesUiDefinitionControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/BadJSONMetadataSourcesUiDefinitionControllerIntegrationTests.groovy
index af758f131..b9789201f 100644
--- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/BadJSONMetadataSourcesUiDefinitionControllerIntegrationTests.groovy
+++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/BadJSONMetadataSourcesUiDefinitionControllerIntegrationTests.groovy
@@ -7,6 +7,7 @@ import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.boot.test.web.client.TestRestTemplate
import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Profile
import org.springframework.core.io.ResourceLoader
import org.springframework.test.context.ActiveProfiles
import spock.lang.Specification
@@ -23,7 +24,7 @@ import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResour
* @author Dmitriy Kopylenko
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
-@ActiveProfiles("no-auth")
+@ActiveProfiles(["no-auth", "badjson"])
class BadJSONMetadataSourcesUiDefinitionControllerIntegrationTests extends Specification {
@Autowired
@@ -42,6 +43,7 @@ class BadJSONMetadataSourcesUiDefinitionControllerIntegrationTests extends Speci
}
@TestConfiguration
+ @Profile('badjson')
static class Config {
@Bean
JsonSchemaResourceLocationRegistry jsonSchemaResourceLocationRegistry(ResourceLoader resourceLoader,
diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerIntegrationTests.groovy
index 8bf12484a..d8d9e7afb 100644
--- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerIntegrationTests.groovy
+++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerIntegrationTests.groovy
@@ -42,25 +42,16 @@ class EntitiesControllerIntegrationTests extends Specification {
def "GET /api/entities returns the proper json"() {
given:
def expectedBody = '''
- {
- "id":null,
- "serviceProviderName":null,
- "entityId":"http://test.scaldingspoon.org/test1",
- "organization":null,
- "contacts":null,
- "mdui":null,
+ {
+ "entityId":"http://test.scaldingspoon.org/test1",
"serviceProviderSsoDescriptor": {
"protocolSupportEnum":"SAML 2",
"nameIdFormats":["urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"]
- },
- "logoutEndpoints":null,
- "securityInfo":null,
+ },
"assertionConsumerServices":[
{"locationUrl":"https://test.scaldingspoon.org/test1/acs","binding":"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST","makeDefault":false}
],
- "serviceEnabled":false,
- "createdDate":null,
- "modifiedDate":null,
+ "serviceEnabled":false,
"relyingPartyOverrides":{},
"attributeRelease":["givenName","employeeNumber"]
}
diff --git a/ui/src/app/metadata/configuration/action/restore.action.ts b/ui/src/app/metadata/configuration/action/restore.action.ts
index 48861c35e..290367054 100644
--- a/ui/src/app/metadata/configuration/action/restore.action.ts
+++ b/ui/src/app/metadata/configuration/action/restore.action.ts
@@ -5,13 +5,39 @@ export enum RestoreActionTypes {
SELECT_VERSION_SUCCESS = '[Restore Version] Select Version Success',
SELECT_VERSION_ERROR = '[Restore Version] Select Version Error',
SELECT_VERSION_REQUEST = '[Restore Version] Select Version Request',
+
+ RESTORE_VERSION_REQUEST = '[Restore Version] Restore Version Request',
+ RESTORE_VERSION_SUCCESS = '[Restore Version] Restore Version Success',
+ RESTORE_VERSION_ERROR = '[Restore Version] Restore Version Error',
+
CLEAR_VERSION = '[Restore Version] Clear Versions'
}
+export interface VersionRequest {
+ type: string;
+ id: string;
+ version: string;
+}
+
+export class RestoreVersionRequest implements Action {
+ readonly type = RestoreActionTypes.RESTORE_VERSION_REQUEST;
+ constructor(public payload: VersionRequest) { }
+}
+
+export class RestoreVersionSuccess implements Action {
+ readonly type = RestoreActionTypes.RESTORE_VERSION_SUCCESS;
+ constructor(public payload: { id: string, type: string, model: Metadata }) { }
+}
+
+export class RestoreVersionError implements Action {
+ readonly type = RestoreActionTypes.RESTORE_VERSION_ERROR;
+ constructor(public payload: any) { }
+}
+
export class SelectVersionRestoreRequest implements Action {
readonly type = RestoreActionTypes.SELECT_VERSION_REQUEST;
- constructor(public payload: { type: string, id: string, version: string }) { }
+ constructor(public payload: VersionRequest) { }
}
export class SelectVersionRestoreSuccess implements Action {
@@ -33,4 +59,7 @@ export type RestoreActionsUnion =
| SelectVersionRestoreRequest
| SelectVersionRestoreError
| SelectVersionRestoreSuccess
+ | RestoreVersionRequest
+ | RestoreVersionSuccess
+ | RestoreVersionError
| ClearVersionRestore;
diff --git a/ui/src/app/metadata/configuration/component/history-list.component.html b/ui/src/app/metadata/configuration/component/history-list.component.html
index ff4599b91..168d0cf19 100644
--- a/ui/src/app/metadata/configuration/component/history-list.component.html
+++ b/ui/src/app/metadata/configuration/component/history-list.component.html
@@ -34,7 +34,7 @@
0">
- Restore
+ Restore
diff --git a/ui/src/app/metadata/configuration/configuration.module.ts b/ui/src/app/metadata/configuration/configuration.module.ts
index c55e24051..906bed24d 100644
--- a/ui/src/app/metadata/configuration/configuration.module.ts
+++ b/ui/src/app/metadata/configuration/configuration.module.ts
@@ -1,5 +1,5 @@
import { NgModule, ModuleWithProviders } from '@angular/core';
-import { CommonModule } from '@angular/common';
+import { CommonModule, DatePipe } from '@angular/common';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { RouterModule } from '@angular/router';
@@ -63,7 +63,9 @@ import { RestoreVersionEffects } from './effect/restore.effect';
SharedModule
],
exports: [],
- providers: []
+ providers: [
+ DatePipe
+ ]
})
export class MetadataConfigurationModule {
static forRoot(): ModuleWithProviders {
diff --git a/ui/src/app/metadata/configuration/container/configuration.component.spec.ts b/ui/src/app/metadata/configuration/container/configuration.component.spec.ts
index 71bb05ab0..0d2fca0d3 100644
--- a/ui/src/app/metadata/configuration/container/configuration.component.spec.ts
+++ b/ui/src/app/metadata/configuration/container/configuration.component.spec.ts
@@ -59,4 +59,12 @@ describe('Metadata Configuration Page Component', () => {
it('should load metadata objects', async(() => {
expect(app).toBeTruthy();
}));
+
+ describe('hasVersion function', () => {
+ it('should determine if a version is defined', () => {
+ expect(app.hasVersion([[{id: 'foo'}], { version: 'foo' }])).toBe('foo');
+ expect(app.hasVersion([[{ id: 'foo' }], {}])).toBe('foo');
+ expect(app.hasVersion([[], {}])).toBeNull();
+ });
+ });
});
diff --git a/ui/src/app/metadata/configuration/container/configuration.component.ts b/ui/src/app/metadata/configuration/container/configuration.component.ts
index d8c0234c6..295f90257 100644
--- a/ui/src/app/metadata/configuration/container/configuration.component.ts
+++ b/ui/src/app/metadata/configuration/container/configuration.component.ts
@@ -22,6 +22,8 @@ export class ConfigurationComponent implements OnDestroy {
name$: Observable;
type$: Observable;
+ hasVersion = ([collection, params]) => params.version || collection && collection.length > 0 ? collection[0].id : null;
+
constructor(
private store: Store,
private routerState: ActivatedRoute
@@ -37,7 +39,7 @@ export class ConfigurationComponent implements OnDestroy {
type,
version
}))
- ).subscribe(store);
+ ).subscribe(this.store);
this.routerState.params.pipe(
takeUntil(this.ngUnsubscribe),
@@ -50,10 +52,8 @@ export class ConfigurationComponent implements OnDestroy {
withLatestFrom(
this.routerState.queryParams
),
- map(([collection, params]) => params.version || collection && collection.length ? collection[0].id : null)
- ).subscribe(version => {
- this.store.dispatch(new SelectVersion(version));
- });
+ map(this.hasVersion)
+ ).subscribe(version => this.store.dispatch(new SelectVersion(version)));
this.name$ = this.store.select(fromReducer.getConfigurationModelName);
this.type$ = this.store.select(fromReducer.getConfigurationModelType);
diff --git a/ui/src/app/metadata/configuration/container/metadata-history.component.ts b/ui/src/app/metadata/configuration/container/metadata-history.component.ts
index 17f2c3caa..7d817b94f 100644
--- a/ui/src/app/metadata/configuration/container/metadata-history.component.ts
+++ b/ui/src/app/metadata/configuration/container/metadata-history.component.ts
@@ -49,6 +49,7 @@ export class MetadataHistoryComponent {
this.router.navigate(
[ '../', 'restore' ],
{
+ queryParams: { version: version.id },
relativeTo: this.route
}
);
diff --git a/ui/src/app/metadata/configuration/container/metadata-xml.component.spec.ts b/ui/src/app/metadata/configuration/container/metadata-xml.component.spec.ts
index c9129dfaa..5e5815c06 100644
--- a/ui/src/app/metadata/configuration/container/metadata-xml.component.spec.ts
+++ b/ui/src/app/metadata/configuration/container/metadata-xml.component.spec.ts
@@ -56,4 +56,11 @@ describe('Metadata Xml Page Component', () => {
expect(app).toBeTruthy();
expect(store.select).toHaveBeenCalledTimes(3);
}));
+
+ describe('preview method', () => {
+ it('should dispatch an action', () => {
+ app.preview();
+ expect(store.dispatch).toHaveBeenCalled();
+ });
+ });
});
diff --git a/ui/src/app/metadata/configuration/container/restore.component.html b/ui/src/app/metadata/configuration/container/restore.component.html
index c842aa47d..77fc2c7cb 100644
--- a/ui/src/app/metadata/configuration/container/restore.component.html
+++ b/ui/src/app/metadata/configuration/container/restore.component.html
@@ -1,12 +1,20 @@
-
- Restore Version
+
+
+ Restore Version ( date )
+
-
Create new version from {{ date | date:DATE_FORMAT }} settings
-
Restoring this version will copy the configuration from the selected version and create a new version from these settings. You can then edit the configuration before saving the new version.
-
Cancel Restore
+
+ Create New Version from Previous Settings
+
+
+ Restoring this version will copy the Version ( date ) configuration and create a new Version from the selected version settings. You can then edit the configuration before saving the new version.
+
+
Cancel
+
Restore
diff --git a/ui/src/app/metadata/configuration/container/restore.component.spec.ts b/ui/src/app/metadata/configuration/container/restore.component.spec.ts
new file mode 100644
index 000000000..e79cbcac9
--- /dev/null
+++ b/ui/src/app/metadata/configuration/container/restore.component.spec.ts
@@ -0,0 +1,76 @@
+import { Component, ViewChild, Input } from '@angular/core';
+import { TestBed, async, ComponentFixture } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+import { StoreModule, combineReducers, Store } from '@ngrx/store';
+import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
+
+import * as fromConfiguration from '../reducer';
+import * as fromProviders from '../../provider/reducer';
+import * as fromResolvers from '../../resolver/reducer';
+import { MockI18nModule } from '../../../../testing/i18n.stub';
+import { RestoreComponent } from './restore.component';
+import { of } from 'rxjs';
+import { DatePipe } from '@angular/common';
+
+@Component({
+ template: `
+
+ `
+})
+class TestHostComponent {
+ @ViewChild(RestoreComponent)
+ public componentUnderTest: RestoreComponent;
+}
+
+describe('Metadata Restore Page Component', () => {
+
+ let fixture: ComponentFixture;
+ let instance: TestHostComponent;
+ let app: RestoreComponent;
+ let store: Store;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ NgbDropdownModule,
+ StoreModule.forRoot({
+ 'metadata-configuration': combineReducers(fromConfiguration.reducers),
+ 'provider': combineReducers(fromProviders.reducers),
+ 'resolver': combineReducers(fromResolvers.reducers)
+ }),
+ MockI18nModule,
+ RouterTestingModule
+ ],
+ declarations: [
+ RestoreComponent,
+ TestHostComponent
+ ],
+ providers: [
+ DatePipe
+ ]
+ }).compileComponents();
+
+ store = TestBed.get(Store);
+ spyOn(store, 'dispatch');
+ spyOn(store, 'select').and.callFake(() => of(new Date().toDateString()));
+
+ fixture = TestBed.createComponent(TestHostComponent);
+ instance = fixture.componentInstance;
+ app = instance.componentUnderTest;
+ fixture.detectChanges();
+ }));
+
+ it('should load metadata objects', async(() => {
+ expect(app).toBeTruthy();
+ expect(store.select).toHaveBeenCalledTimes(4);
+ expect(store.dispatch).not.toHaveBeenCalled();
+ }));
+
+ describe('restore method', () => {
+ it('should emit a value from the restore subject', () => {
+ spyOn(app.subj, 'next').and.callThrough();
+ app.restore();
+ expect(app.subj.next).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/ui/src/app/metadata/configuration/container/restore.component.ts b/ui/src/app/metadata/configuration/container/restore.component.ts
index 53f3573ec..0b6faff29 100644
--- a/ui/src/app/metadata/configuration/container/restore.component.ts
+++ b/ui/src/app/metadata/configuration/container/restore.component.ts
@@ -1,10 +1,13 @@
import { Component, ChangeDetectionStrategy, OnDestroy } from '@angular/core';
import { ActivatedRoute, } from '@angular/router';
import { Store } from '@ngrx/store';
-import { Subject } from 'rxjs';
+import { Subject, Observable } from 'rxjs';
import * as fromConfiguration from '../reducer';
import { CONFIG_DATE_FORMAT } from '../configuration.values';
+import { RestoreVersionRequest, SelectVersionRestoreRequest } from '../action/restore.action';
+import { withLatestFrom, map, takeUntil } from 'rxjs/operators';
+import { DatePipe } from '@angular/common';
@Component({
selector: 'restore-component',
@@ -13,19 +16,47 @@ import { CONFIG_DATE_FORMAT } from '../configuration.values';
styleUrls: []
})
export class RestoreComponent implements OnDestroy {
- private ngUnsubscribe: Subject = new Subject();
- DATE_FORMAT = CONFIG_DATE_FORMAT;
+ readonly subj = new Subject();
+ restore$ = this.subj.asObservable();
- date = new Date();
+ date$: Observable;
+ kind$: Observable = this.store.select(fromConfiguration.getConfigurationModelKind);
+ id$: Observable = this.store.select(fromConfiguration.getConfigurationModelId);
constructor(
private store: Store,
- private routerState: ActivatedRoute
- ) {}
+ private datePipe: DatePipe,
+ private route: ActivatedRoute
+ ) {
+ this.restore$.pipe(
+ withLatestFrom(
+ this.store.select(fromConfiguration.getSelectedVersionId),
+ this.kind$,
+ this.id$
+ ),
+ map(([restore, version, type, id]) => new RestoreVersionRequest({ id, type, version }))
+ ).subscribe(this.store);
- ngOnDestroy() {
- this.ngUnsubscribe.next();
- this.ngUnsubscribe.complete();
+ this.date$ = this.store
+ .select(fromConfiguration.getConfigurationVersionDate)
+ .pipe(
+ map((date) => this.datePipe.transform(date, CONFIG_DATE_FORMAT))
+ );
+
+ this.route.queryParams.pipe(
+ takeUntil(this.subj),
+ map(params => params.version),
+ withLatestFrom(this.id$, this.kind$),
+ map(([version, id, type]) => new SelectVersionRestoreRequest({ version, id, type }))
+ ).subscribe(this.store);
+ }
+
+ restore() {
+ this.subj.next();
+ }
+
+ ngOnDestroy(): void {
+ this.subj.complete();
}
}
diff --git a/ui/src/app/metadata/configuration/effect/configuration.effect.ts b/ui/src/app/metadata/configuration/effect/configuration.effect.ts
index 924d61919..b9719469d 100644
--- a/ui/src/app/metadata/configuration/effect/configuration.effect.ts
+++ b/ui/src/app/metadata/configuration/effect/configuration.effect.ts
@@ -44,11 +44,11 @@ export class MetadataConfigurationEffects {
map(action => action.payload),
switchMap(payload =>
this.historyService.getVersion(payload.id, payload.type, payload.version).pipe(
- map((response: Metadata) =>
- (payload.type === 'resolver') ?
+ map((response: Metadata) => {
+ return (payload.type === 'resolver') ?
new SelectResolverSuccess(response as MetadataResolver) :
- new SelectProviderSuccess(response as MetadataProvider)
- )
+ new SelectProviderSuccess(response as MetadataProvider);
+ })
)
)
);
diff --git a/ui/src/app/metadata/configuration/effect/restore.effect.ts b/ui/src/app/metadata/configuration/effect/restore.effect.ts
index b7f928f88..df5272ced 100644
--- a/ui/src/app/metadata/configuration/effect/restore.effect.ts
+++ b/ui/src/app/metadata/configuration/effect/restore.effect.ts
@@ -6,29 +6,73 @@ import {
RestoreActionTypes,
SelectVersionRestoreRequest,
SelectVersionRestoreError,
- SelectVersionRestoreSuccess
+ SelectVersionRestoreSuccess,
+ RestoreVersionRequest,
+ RestoreVersionSuccess,
+ RestoreVersionError
} from '../action/restore.action';
import { MetadataHistoryService } from '../service/history.service';
import { of } from 'rxjs';
+import { Router, ActivatedRoute } from '@angular/router';
+
+import { AddNotification } from '../../../notification/action/notification.action';
+import { Notification, NotificationType } from '../../../notification/model/notification';
+import { removeNulls } from '../../../shared/util';
@Injectable()
export class RestoreVersionEffects {
@Effect()
- restoreVersionFromId$ = this.actions$.pipe(
+ selectVersionFromId$ = this.actions$.pipe(
ofType(RestoreActionTypes.SELECT_VERSION_REQUEST),
map(action => action.payload),
switchMap(({ type, id, version }) => {
- return this.historyService.getVersion(id, version, type).pipe(
+ return this.historyService.getVersion(id, type, version).pipe(
map(v => new SelectVersionRestoreSuccess(v)),
catchError(err => of(new SelectVersionRestoreError(err)))
);
})
);
+ @Effect()
+ restoreVersion$ = this.actions$.pipe(
+ ofType(RestoreActionTypes.RESTORE_VERSION_REQUEST),
+ map(action => action.payload),
+ switchMap(({ id, type, version }) =>
+ this.historyService.restoreVersion(id, type, version).pipe(
+ map(v => new RestoreVersionSuccess({ id, type, model: v })),
+ catchError(err => of(new RestoreVersionError(err)))
+ )
+ )
+ );
+
+ @Effect()
+ restoreVersionSuccessNotification$ = this.actions$.pipe(
+ ofType(RestoreActionTypes.RESTORE_VERSION_SUCCESS),
+ map(action => action.payload),
+ map((data) =>
+ new AddNotification(new Notification(
+ NotificationType.Success,
+ `Version Restored!`,
+ 5000
+ ))
+ )
+ );
+
+ @Effect({dispatch: false})
+ restoreVersionSuccessRedirect$ = this.actions$.pipe(
+ ofType(RestoreActionTypes.RESTORE_VERSION_SUCCESS),
+ map(action => action.payload),
+ switchMap((data) =>
+ this.router.navigate(['/metadata', data.type, data.id, 'configuration', 'options'])
+ )
+ );
+
constructor(
private historyService: MetadataHistoryService,
- private actions$: Actions
+ private actions$: Actions,
+ private router: Router,
+ private route: ActivatedRoute
) { }
}
diff --git a/ui/src/app/metadata/configuration/reducer/index.ts b/ui/src/app/metadata/configuration/reducer/index.ts
index 76140bfca..ab217de6d 100644
--- a/ui/src/app/metadata/configuration/reducer/index.ts
+++ b/ui/src/app/metadata/configuration/reducer/index.ts
@@ -183,3 +183,4 @@ export const getConfigurationModelType = createSelector(getConfigurationModel, g
export const getConfigurationHasXml = createSelector(getConfigurationXml, xml => !!xml);
export const getConfigurationFilters = createSelector(getConfigurationModel, model => model.metadataFilters);
+export const getConfigurationVersionDate = createSelector(getRestoreModel, version => version && version.modifiedDate);
diff --git a/ui/src/app/metadata/configuration/service/configuration.service.spec.ts b/ui/src/app/metadata/configuration/service/configuration.service.spec.ts
index 40bfb39ac..43fd99c05 100644
--- a/ui/src/app/metadata/configuration/service/configuration.service.spec.ts
+++ b/ui/src/app/metadata/configuration/service/configuration.service.spec.ts
@@ -7,10 +7,14 @@ import { MetadataSourceEditor } from '../../domain/model/wizards/metadata-source
import { ResolverService } from '../../domain/service/resolver.service';
import { of } from 'rxjs';
import { MetadataProviderService } from '../../domain/service/provider.service';
+import { Metadata } from '../../domain/domain.type';
+import { SCHEMA } from '../../../../testing/form-schema.stub';
+import { getConfigurationSectionsFn } from '../reducer';
describe(`Configuration Service`, () => {
let resolverService: any;
+ let providerService: any;
let mockService = {
find: () => of([])
@@ -36,11 +40,12 @@ describe(`Configuration Service`, () => {
});
resolverService = TestBed.get(ResolverService);
+ providerService = TestBed.get(MetadataProviderService);
});
describe('find method', () => {
- it(`should send an expected GET request`, async(inject([MetadataConfigurationService, HttpTestingController],
+ it(`should call the resolver service when type is resolver`, async(inject([MetadataConfigurationService, HttpTestingController],
(service: MetadataConfigurationService, backend: HttpTestingController) => {
spyOn(resolverService, 'find').and.callThrough();
const type = 'resolver';
@@ -49,6 +54,25 @@ describe(`Configuration Service`, () => {
expect(resolverService.find).toHaveBeenCalledWith(id);
}
)));
+ it(`should call the provider service when type is resolver`, async(inject([MetadataConfigurationService, HttpTestingController],
+ (service: MetadataConfigurationService, backend: HttpTestingController) => {
+ spyOn(providerService, 'find').and.callThrough();
+ const type = 'provider';
+ const id = 'foo';
+ service.find(id, type).subscribe();
+ expect(providerService.find).toHaveBeenCalledWith(id);
+ }
+ )));
+ it(`should throw an error when a type is not found`, async(inject([MetadataConfigurationService, HttpTestingController],
+ (service: MetadataConfigurationService, backend: HttpTestingController) => {
+ spyOn(providerService, 'find').and.callThrough();
+ const type = 'bar';
+ const id = 'foo';
+ service.find(id, type).subscribe(null, (err) => {
+ expect(err).toEqual(new Error('Type not supported'));
+ });
+ }
+ )));
});
describe('loadSchema method', () => {
@@ -79,4 +103,15 @@ describe(`Configuration Service`, () => {
}
)));
});
+
+ describe('getMetadataConfiguration method', () => {
+ it('should return the parsed configuration', async(inject([MetadataConfigurationService],
+ (service: MetadataConfigurationService) => {
+ const model = {} as Metadata;
+ const definition = {steps: []};
+ const expected = getConfigurationSectionsFn([model], definition, SCHEMA);
+ expect(service.getMetadataConfiguration(model, definition, SCHEMA)).toEqual(expected);
+ }
+ )));
+ });
});
diff --git a/ui/src/app/metadata/configuration/service/history.service.spec.ts b/ui/src/app/metadata/configuration/service/history.service.spec.ts
index 0e8bc77a4..0927f8e70 100644
--- a/ui/src/app/metadata/configuration/service/history.service.spec.ts
+++ b/ui/src/app/metadata/configuration/service/history.service.spec.ts
@@ -1,7 +1,10 @@
import { TestBed, async, inject } from '@angular/core/testing';
-import { HttpClientModule } from '@angular/common/http';
+import { HttpClientModule, HttpRequest } from '@angular/common/http';
import { HttpTestingController, HttpClientTestingModule } from '@angular/common/http/testing';
import { MetadataHistoryService } from './history.service';
+import { of } from 'rxjs';
+import { PATHS } from '../configuration.values';
+import { Metadata } from '../../domain/domain.type';
describe(`Attributes Service`, () => {
beforeEach(() => {
@@ -25,4 +28,73 @@ describe(`Attributes Service`, () => {
}
)));
});
+
+ describe('getVersions method', () => {
+ it(`should join a list of observables`, async(inject([MetadataHistoryService, HttpTestingController],
+ (service: MetadataHistoryService) => {
+ spyOn(service, 'getVersion').and.returnValue(of());
+ service.getVersions('foo', ['abc', 'def'], 'resolver').subscribe(history => {
+ expect(service.getVersion).toHaveBeenCalledTimes(2);
+ });
+ }
+ )));
+ });
+
+ describe('getVersion method', () => {
+ it(`should get the primary version of the resource`, async(inject([MetadataHistoryService, HttpTestingController],
+ (service: MetadataHistoryService, backend: HttpTestingController) => {
+ const resourceId = 'foo';
+ const type = 'resource';
+ service.getVersion(resourceId, type).subscribe();
+ backend.expectOne((req: HttpRequest) => {
+ return req.url === `/${service.base}/${PATHS[type]}/${resourceId}`
+ && req.method === 'GET';
+ }, `GET schema by path`);
+ }
+ )));
+ it(`should get the provided version of the resource`, async(inject([MetadataHistoryService, HttpTestingController],
+ (service: MetadataHistoryService, backend: HttpTestingController) => {
+ const resourceId = 'foo';
+ const type = 'resource';
+ const versionId = '1';
+ service.getVersion(resourceId, type, versionId).subscribe();
+ backend.expectOne((req: HttpRequest) => {
+ return req.url === `/${service.base}/${PATHS[type]}/${resourceId}/${service.path}/${versionId}`
+ && req.method === 'GET';
+ }, `GET schema by path`);
+ }
+ )));
+ });
+
+ describe('updateVersion method', () => {
+ it(`should send a put request`, async(inject([MetadataHistoryService, HttpTestingController],
+ (service: MetadataHistoryService, backend: HttpTestingController) => {
+ const resourceId = 'foo';
+ const type = 'resource';
+ const versionId = '1';
+ service.updateVersion(resourceId, type, {} as Metadata).subscribe();
+ backend.expectOne((req: HttpRequest) => {
+ return req.url === `/${service.base}/${PATHS[type]}/${resourceId}`
+ && req.method === 'PUT';
+ }, `PUT schema by path`);
+ }
+ )));
+ });
+
+ describe('restoreVersion method', () => {
+ it(`should send a put request`, async(inject([MetadataHistoryService, HttpTestingController],
+ (service: MetadataHistoryService, backend: HttpTestingController) => {
+ const resourceId = 'foo';
+ const type = 'resource';
+ const versionId = '1';
+ const response = {version: 'bar'} as Metadata;
+ spyOn(service, 'getVersions').and.returnValue(of([response, { ...response, version: 'foo' }]));
+ spyOn(service, 'updateVersion').and.returnValue(of(response));
+ service.restoreVersion(resourceId, type, versionId).subscribe(updated => {
+ expect(service.getVersions).toHaveBeenCalled();
+ expect(service.updateVersion).toHaveBeenCalled();
+ });
+ }
+ )));
+ });
});
diff --git a/ui/src/app/metadata/configuration/service/history.service.ts b/ui/src/app/metadata/configuration/service/history.service.ts
index 700e88e6a..6a5f79607 100644
--- a/ui/src/app/metadata/configuration/service/history.service.ts
+++ b/ui/src/app/metadata/configuration/service/history.service.ts
@@ -5,8 +5,10 @@ import { MetadataHistory } from '../model/history';
import { PATHS } from '../../configuration/configuration.values';
import { MetadataVersion } from '../model/version';
-import { map } from 'rxjs/operators';
+import { map, catchError, switchMap } from 'rxjs/operators';
import { Metadata } from '../../domain/domain.type';
+import { withLatestFrom } from 'rxjs-compat/operator/withLatestFrom';
+import { removeNulls } from '../../../shared/util';
@Injectable()
export class MetadataHistoryService {
@@ -39,4 +41,16 @@ export class MetadataHistoryService {
`/${this.base}/${PATHS[type]}/${resourceId}`;
return this.http.get(api);
}
+
+ updateVersion(resourceId: string, type: string, model: Metadata): Observable {
+ return this.http.put(`/${this.base}/${PATHS[type]}/${resourceId}`, model);
+ }
+
+ restoreVersion(resourceId: string, type: string, versionId: string): Observable {
+ return this.getVersions(resourceId, [null, versionId], type).pipe(
+ switchMap(([current, toRestore]) =>
+ this.updateVersion(resourceId, type, { ...removeNulls(toRestore), version: current.version })
+ )
+ );
+ }
}
diff --git a/ui/src/app/metadata/domain/model/wizards/metadata-source-base.ts b/ui/src/app/metadata/domain/model/wizards/metadata-source-base.ts
index aab844d5e..02b1a2625 100644
--- a/ui/src/app/metadata/domain/model/wizards/metadata-source-base.ts
+++ b/ui/src/app/metadata/domain/model/wizards/metadata-source-base.ts
@@ -86,6 +86,7 @@ export class MetadataSourceBase implements Wizard {
const checkOrg = (value, property, form) => {
const org = property.parent;
const orgValue = org.value || {};
+ console.log(orgValue);
const err = Object.keys(orgValue) && !value ? {
code: 'ORG_INCOMPLETE',
path: `#${property.path}`,
diff --git a/ui/src/app/metadata/filter/effect/collection.effect.ts b/ui/src/app/metadata/filter/effect/collection.effect.ts
index 58ecd41f7..dcf67b246 100644
--- a/ui/src/app/metadata/filter/effect/collection.effect.ts
+++ b/ui/src/app/metadata/filter/effect/collection.effect.ts
@@ -35,8 +35,7 @@ import { FilterCollectionActionTypes } from '../action/collection.action';
import * as fromFilter from '../reducer';
import * as fromProvider from '../../provider/reducer';
import { MetadataFilter } from '../../domain/model';
-import { removeNulls, array_move } from '../../../shared/util';
-import { EntityAttributesFilterEntity } from '../../domain/entity/filter/entity-attributes-filter';
+import { array_move } from '../../../shared/util';
import { MetadataFilterService } from '../../domain/service/filter.service';
import { SelectProviderRequest } from '../../provider/action/collection.action';
import { UpdateFilterChanges, ClearFilter } from '../action/filter.action';
diff --git a/ui/src/app/metadata/filter/model/entity-attributes-configuration.filter.ts b/ui/src/app/metadata/filter/model/entity-attributes-configuration.filter.ts
index 5333e08f1..06a6609b6 100644
--- a/ui/src/app/metadata/filter/model/entity-attributes-configuration.filter.ts
+++ b/ui/src/app/metadata/filter/model/entity-attributes-configuration.filter.ts
@@ -1,6 +1,5 @@
import { Wizard } from '../../../wizard/model';
import { MetadataFilter } from '../../domain/model';
-import { removeNulls } from '../../../shared/util';
import { EntityAttributesFilter } from './entity-attributes.filter';
export const EntityAttributesFilterConfiguration: Wizard = {
diff --git a/ui/src/app/schema-form/service/schema.service.ts b/ui/src/app/schema-form/service/schema.service.ts
index 617769de6..0e5905a8a 100644
--- a/ui/src/app/schema-form/service/schema.service.ts
+++ b/ui/src/app/schema-form/service/schema.service.ts
@@ -28,12 +28,13 @@ export class SchemaService {
const conditions = formProperty.parent.schema.anyOf || [];
const values = formProperty.parent.value;
const currentConditions = conditions.filter(condition =>
- Object
+ 'properties' in condition ? Object
.keys(condition.properties)
.some(
key => values.hasOwnProperty(key) && condition.properties[key].enum ?
condition.properties[key].enum[0] === values[key] : false
)
+ : false
);
currentConditions.forEach(el => {
requiredFields = el.required || [];