From c820640ab884b2011dfbb886ff41d68717344763 Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Thu, 4 Oct 2018 07:38:14 -0700 Subject: [PATCH 01/33] SHIBUI-913 Added md-source json-schema --- .../assets/schema/source/metadata-source.json | 439 ++++++++++++++++++ 1 file changed, 439 insertions(+) create mode 100644 ui/src/assets/schema/source/metadata-source.json diff --git a/ui/src/assets/schema/source/metadata-source.json b/ui/src/assets/schema/source/metadata-source.json new file mode 100644 index 000000000..a92e5611c --- /dev/null +++ b/ui/src/assets/schema/source/metadata-source.json @@ -0,0 +1,439 @@ +{ + "entityId": { + "title": "label.entity-id", + "description": "tooltip.entity-id", + "type": "string" + }, + "serviceProviderName": { + "title": "label.service-provider-name", + "description": "tooltip.service-provider-name", + "type": "string" + }, + "serviceEnabled": { + "title": "label.enable-this-service-opon-saving", + "description": "tooltip.enable-this-service-upon-saving", + "type": "boolean" + }, + "organization": { + "type": "object", + "properties": { + "name": { + "title": "label.organization-name", + "description": "tooltip.organization-name", + "type": "string" + }, + "displayName": { + "title": "label.organization-display-name", + "description": "tooltip.organization-display-name", + "type": "string" + }, + "url": { + "title": "label.organization-display-name", + "description": "tooltip.organization-display-name", + "type": "string" + } + }, + "dependencies": { + "name": [ + "displayName", + "url" + ], + "displayName": [ + "name", + "url" + ], + "url": [ + "name", + "displayName" + ] + } + }, + "contacts": { + "title": "label.contact-information", + "description": "tooltip.contact-information", + "type": "array", + "items": { + "$ref": "#/definitions/Contact" + } + }, + "mdui": { + "type": "object", + "properties": { + "displayName": { + "title": "label.display-name", + "description": "tooltip.mdui-display-name", + "type": "string" + }, + "informationUrl": { + "title": "label.information-url", + "description": "tooltip.mdui-information-url", + "type": "string" + }, + "privacyStatementUrl": { + "title": "label.privacy-statement-url", + "description": "tooltip.mdui-privacy-statement-url", + "type": "string" + }, + "description": { + "title": "label.description", + "description": "tooltip.mdui-description", + "type": "string" + }, + "logoUrl": { + "title": "label.logo-url", + "description": "tooltip.mdui-logo-url", + "type": "string" + }, + "logoHeight": { + "title": "label.logo-height", + "description": "tooltip.mdui-logo-height", + "min": 0, + "type": "integer" + }, + "logoWidth": { + "title": "label.logo-width", + "description": "tooltip.mdui-logo-width", + "min": 0, + "type": "integer" + } + } + }, + "securityInfo": { + "type": "object", + "properties": { + "x509CertificateAvailable": { + "title": "label.is-there-a-x509-certificate", + "description": "tooltip.is-there-a-x509-certificate", + "type": "boolean", + "default": false + }, + "authenticationRequestsSigned": { + "title": "label.authentication-requests-signed", + "description": "tooltip.authentication-requests-signed", + "type": "boolean", + "default": false + }, + "wantAssertionsSigned": { + "title": "label.want-assertions-signed", + "description": "tooltip.want-assertions-signed", + "type": "boolean", + "default": false + }, + "x509Certificates": { + "title": "label.x509-certificates", + "type": "array", + "items": { + "$ref": "#/definitions/Certificate" + } + } + } + }, + "assertionConsumerServices": { + "title": "label.assertion-consumer-service-endpoints", + "description": "", + "type": "array", + "items": { + "$ref": "#/definitions/AssertionConsumerService" + } + }, + "serviceProviderSsoDescriptor": { + "type": "object", + "properties": { + "protocolSupportEnum": { + "title": "label.protocol-support-enumeration", + "description": "tooltip.protocol-support-enumeration", + "type": "string", + "placeholder": "label.select-protocol", + "oneOf": [ + { + "enum": [ + "SAML 2" + ], + "description": "SAML 2" + }, + { + "enum": [ + "SAML 1.1" + ], + "description": "SAML 1.1" + } + ] + } + }, + "nameIdFormats": { + "$ref": "#/definitions/NameIdFormatList" + } + }, + "logoutEndpoints": { + "title": "label.logout-endpoints", + "description": "tooltip.logout-endpoints", + "type": "array", + "items": { + "$ref": "#/definitions/LogoutEndpoint" + } + }, + "relyingPartyOverrides": { + "type": "object", + "properties": { + "signAssertion": { + "title": "label.sign-the-assertion", + "description": "tooltip.sign-assertion", + "type": "boolean", + "default": false + }, + "dontSignResponse": { + "title": "label.dont-sign-the-response", + "description": "tooltip.dont-sign-response", + "type": "boolean", + "default": false + }, + "turnOffEncryption": { + "title": "label.turn-off-encryption-of-response", + "description": "tooltip.turn-off-encryption", + "type": "boolean", + "default": false + }, + "useSha": { + "title": "label.use-sha1-signing-algorithm", + "description": "tooltip.usa-sha-algorithm", + "type": "boolean", + "default": false + }, + "ignoreAuthenticationMethod": { + "title": "label.ignore-any-sp-requested-authentication-method", + "description": "tooltip.ignore-auth-method", + "type": "boolean", + "default": false + }, + "forceAuthn": { + "title": "label.force-authn", + "description": "tooltip.force-authn", + "type": "boolean", + "default": false + }, + "omitNotBefore": { + "title": "label.omit-not-before-condition", + "type": "boolean", + "description": "tooltip.omit-not-before-condition", + "default": false + }, + "responderId": { + "title": "label.responder-id", + "description": "tooltip.responder-id", + "type": "string" + }, + "nameIdFormats": { + "$ref": "#/definitions/NameIdFormatList" + }, + "authenticationMethods": { + "$ref": "#/definitions/AuthenticationMethodList" + } + } + }, + "attributeRelease": { + "type": "array", + "description": "Attribute release table - select the attributes you want to release (default unchecked)", + "widget": { + "id": "checklist", + "dataUrl": "/customAttributes" + }, + "items": { + "type": "string" + } + }, + "definitions": { + "Contact": { + "type": "object", + "properties": { + "name": { + "title": "label.contact-name", + "description": "tooltip.contact-name", + "type": "string" + }, + "type": { + "title": "label.contact-type", + "description": "tooltip.contact-type", + "type": "string", + "oneOf": [ + { + "enum": [ + "support" + ], + "description": "value.support" + }, + { + "enum": [ + "technical" + ], + "description": "value.technical" + }, + { + "enum": [ + "administrative" + ], + "description": "value.administrative" + }, + { + "enum": [ + "other" + ], + "description": "value.other" + } + ] + }, + "emailAddress": { + "title": "label.contact-email-address", + "description": "tooltip.contact-email", + "type": "string", + "pattern": "^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$" + } + } + }, + "Certificate": { + "name": { + "title": "label.certificate-name-display-only", + "description": "tooltip.certificate-name", + "type": "string" + }, + "type": { + "title": "label.type", + "description": "tooltip.certificate-type", + "type": "string", + "oneOf": [ + { + "enum": [ + "signing" + ], + "description": "value.signing" + }, + { + "enum": [ + "encryption" + ], + "description": "value.encryption" + }, + { + "enum": [ + "both" + ], + "description": "value.both" + } + ], + "default": "both" + }, + "value": { + "title": "label.certificate", + "description": "tooltip.certificate", + "type": "string" + } + }, + "AssertionConsumerService": { + "type": "object", + "properties": { + "locationUrl": { + "title": "label.assertion-consumer-services-location", + "description": "tooltip.assertion-consumer-service-location", + "type": "string", + "widget": { + "id": "string", + "help": "message.valid-url" + } + }, + "binding": { + "title": "label.assertion-consumer-service-location-binding", + "description": "tooltip.assertion-consumer-service-location-binding", + "type": "string", + "oneOf": [ + { + "enum": [ + "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + ], + "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + }, + { + "enum": [ + "urn:oasis:names:tc:SAML:1.0:profiles:browser-post" + ], + "description": "urn:oasis:names:tc:SAML:1.0:profiles:browser-post" + } + ] + }, + "makeDefault": { + "title": "label.mark-as-default", + "description": "tooltip.mark-as-default", + "type": "boolean" + } + } + }, + "NameIdFormatList": { + "title": "label.nameid-format-to-send", + "placeholder": "label.nameid-format", + "description": "tooltip.nameid-format", + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "widget": "datalist", + "data": [ + "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", + "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", + "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", + "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" + ] + }, + "default": null + }, + "AuthenticationMethodList": { + "title": "label.authentication-methods-to-use", + "description": "tooltip.authentication-methods-to-use", + "type": "array", + "placeholder": "label.authentication-method", + "uniqueItems": true, + "items": { + "type": "string", + "title": "label.authentication-method", + "widget": { + "id": "datalist", + "data": [ + "https://refeds.org/profile/mfa", + "urn:oasis:names:tc:SAML:2.0:ac:classes:TimeSyncToken", + "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport" + ] + } + }, + "default": null + }, + "LogoutEndpoint": { + "title": "label.new-endpoint", + "description": "tooltip.new-endpoint", + "type": "object", + "properties": { + "url": { + "title": "label.url", + "description": "tooltip.url", + "type": "string" + }, + "bindingType": { + "title": "label.binding-type", + "description": "tooltip.binding-type", + "type": "string", + "oneOf": [ + { + "enum": [ + "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + ], + "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + }, + { + "enum": [ + "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + ], + "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + } + ] + + } + } + } + } +} \ No newline at end of file From 853c1bd51af5955695b3cbbdb258df946e4530ef Mon Sep 17 00:00:00 2001 From: Dmitriy Kopylenko Date: Thu, 4 Oct 2018 16:28:17 -0400 Subject: [PATCH 02/33] WIP1 --- ...tadataSourcesUiDefinitionController.groovy | 36 ++ .../src/main/resources/application.properties | 3 + .../resources/metadata-sources-ui-schema.json | 439 ++++++++++++++++++ 3 files changed, 478 insertions(+) create mode 100644 backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionController.groovy create mode 100644 backend/src/main/resources/metadata-sources-ui-schema.json diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionController.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionController.groovy new file mode 100644 index 000000000..481d95ac5 --- /dev/null +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionController.groovy @@ -0,0 +1,36 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller + +import groovy.json.JsonSlurper +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.core.io.ResourceLoader +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController + +/** + * Controller implementing REST resource responsible for exposing structure definition for metadata sources user + * interface in terms of JSON schema. + * + * @author Dmitriy Kopylenko + */ +@RestController('/api/ui/metadataSources') +@ConfigurationProperties('shibui') +class MetadataSourcesUiDefinitionController { + + //Configured via @ConfigurationProperties with 'shibui.metadata-sources-ui-schema-location' property and default + //value set here if that property is not set + String metadataSourcesUiSchemaLocation = 'classpath:metadata-sources-ui-schema.json' + + URL jsonSchemaUrl + + MetadataSourcesUiDefinitionController(ResourceLoader resourceLoader) { + jsonSchemaUrl = resourceLoader.getResource(metadataSourcesUiSchemaLocation).getURL() + } + + @GetMapping + ResponseEntity getUiDefinitionJsonSchema() { + //JsonSlurper is not threadsafe, but cheap to init. New instance per-thread is the canonical usage + def json = new JsonSlurper().parse(this.jsonSchemaUrl) + ResponseEntity.ok(json) + } +} diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 634d32c5b..4d5baf934 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -49,6 +49,9 @@ spring.profiles.active=default #shibui.default-password= +shibui.metadata-sources-ui-schema-location=classpath:metadata-sources-ui-schema-location.json + #Actuator endpoints (info) # Un-comment to get full git details exposed like author, abbreviated SHA-1, commit message #management.info.git.mode=full + diff --git a/backend/src/main/resources/metadata-sources-ui-schema.json b/backend/src/main/resources/metadata-sources-ui-schema.json new file mode 100644 index 000000000..92211e558 --- /dev/null +++ b/backend/src/main/resources/metadata-sources-ui-schema.json @@ -0,0 +1,439 @@ +{ + "entityId": { + "title": "label.entity-id", + "description": "tooltip.entity-id", + "type": "string" + }, + "serviceProviderName": { + "title": "label.service-provider-name", + "description": "tooltip.service-provider-name", + "type": "string" + }, + "serviceEnabled": { + "title": "label.enable-this-service-upon-saving", + "description": "tooltip.enable-this-service-upon-saving", + "type": "boolean" + }, + "organization": { + "type": "object", + "properties": { + "name": { + "title": "label.organization-name", + "description": "tooltip.organization-name", + "type": "string" + }, + "displayName": { + "title": "label.organization-display-name", + "description": "tooltip.organization-display-name", + "type": "string" + }, + "url": { + "title": "label.organization-display-name", + "description": "tooltip.organization-display-name", + "type": "string" + } + }, + "dependencies": { + "name": [ + "displayName", + "url" + ], + "displayName": [ + "name", + "url" + ], + "url": [ + "name", + "displayName" + ] + } + }, + "contacts": { + "title": "label.contact-information", + "description": "tooltip.contact-information", + "type": "array", + "items": { + "$ref": "#/definitions/Contact" + } + }, + "mdui": { + "type": "object", + "properties": { + "displayName": { + "title": "label.display-name", + "description": "tooltip.mdui-display-name", + "type": "string" + }, + "informationUrl": { + "title": "label.information-url", + "description": "tooltip.mdui-information-url", + "type": "string" + }, + "privacyStatementUrl": { + "title": "label.privacy-statement-url", + "description": "tooltip.mdui-privacy-statement-url", + "type": "string" + }, + "description": { + "title": "label.description", + "description": "tooltip.mdui-description", + "type": "string" + }, + "logoUrl": { + "title": "label.logo-url", + "description": "tooltip.mdui-logo-url", + "type": "string" + }, + "logoHeight": { + "title": "label.logo-height", + "description": "tooltip.mdui-logo-height", + "min": 0, + "type": "integer" + }, + "logoWidth": { + "title": "label.logo-width", + "description": "tooltip.mdui-logo-width", + "min": 0, + "type": "integer" + } + } + }, + "securityInfo": { + "type": "object", + "properties": { + "x509CertificateAvailable": { + "title": "label.is-there-a-x509-certificate", + "description": "tooltip.is-there-a-x509-certificate", + "type": "boolean", + "default": false + }, + "authenticationRequestsSigned": { + "title": "label.authentication-requests-signed", + "description": "tooltip.authentication-requests-signed", + "type": "boolean", + "default": false + }, + "wantAssertionsSigned": { + "title": "label.want-assertions-signed", + "description": "tooltip.want-assertions-signed", + "type": "boolean", + "default": false + }, + "x509Certificates": { + "title": "label.x509-certificates", + "type": "array", + "items": { + "$ref": "#/definitions/Certificate" + } + } + } + }, + "assertionConsumerServices": { + "title": "label.assertion-consumer-service-endpoints", + "description": "", + "type": "array", + "items": { + "$ref": "#/definitions/AssertionConsumerService" + } + }, + "serviceProviderSsoDescriptor": { + "type": "object", + "properties": { + "protocolSupportEnum": { + "title": "label.protocol-support-enumeration", + "description": "tooltip.protocol-support-enumeration", + "type": "string", + "placeholder": "label.select-protocol", + "oneOf": [ + { + "enum": [ + "SAML 2" + ], + "description": "SAML 2" + }, + { + "enum": [ + "SAML 1.1" + ], + "description": "SAML 1.1" + } + ] + } + }, + "nameIdFormats": { + "$ref": "#/definitions/NameIdFormatList" + } + }, + "logoutEndpoints": { + "title": "label.logout-endpoints", + "description": "tooltip.logout-endpoints", + "type": "array", + "items": { + "$ref": "#/definitions/LogoutEndpoint" + } + }, + "relyingPartyOverrides": { + "type": "object", + "properties": { + "signAssertion": { + "title": "label.sign-the-assertion", + "description": "tooltip.sign-assertion", + "type": "boolean", + "default": false + }, + "dontSignResponse": { + "title": "label.dont-sign-the-response", + "description": "tooltip.dont-sign-response", + "type": "boolean", + "default": false + }, + "turnOffEncryption": { + "title": "label.turn-off-encryption-of-response", + "description": "tooltip.turn-off-encryption", + "type": "boolean", + "default": false + }, + "useSha": { + "title": "label.use-sha1-signing-algorithm", + "description": "tooltip.usa-sha-algorithm", + "type": "boolean", + "default": false + }, + "ignoreAuthenticationMethod": { + "title": "label.ignore-any-sp-requested-authentication-method", + "description": "tooltip.ignore-auth-method", + "type": "boolean", + "default": false + }, + "forceAuthn": { + "title": "label.force-authn", + "description": "tooltip.force-authn", + "type": "boolean", + "default": false + }, + "omitNotBefore": { + "title": "label.omit-not-before-condition", + "type": "boolean", + "description": "tooltip.omit-not-before-condition", + "default": false + }, + "responderId": { + "title": "label.responder-id", + "description": "tooltip.responder-id", + "type": "string" + }, + "nameIdFormats": { + "$ref": "#/definitions/NameIdFormatList" + }, + "authenticationMethods": { + "$ref": "#/definitions/AuthenticationMethodList" + } + } + }, + "attributeRelease": { + "type": "array", + "description": "Attribute release table - select the attributes you want to release (default unchecked)", + "widget": { + "id": "checklist", + "dataUrl": "/customAttributes" + }, + "items": { + "type": "string" + } + }, + "definitions": { + "Contact": { + "type": "object", + "properties": { + "name": { + "title": "label.contact-name", + "description": "tooltip.contact-name", + "type": "string" + }, + "type": { + "title": "label.contact-type", + "description": "tooltip.contact-type", + "type": "string", + "oneOf": [ + { + "enum": [ + "support" + ], + "description": "value.support" + }, + { + "enum": [ + "technical" + ], + "description": "value.technical" + }, + { + "enum": [ + "administrative" + ], + "description": "value.administrative" + }, + { + "enum": [ + "other" + ], + "description": "value.other" + } + ] + }, + "emailAddress": { + "title": "label.contact-email-address", + "description": "tooltip.contact-email", + "type": "string", + "pattern": "^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$" + } + } + }, + "Certificate": { + "name": { + "title": "label.certificate-name-display-only", + "description": "tooltip.certificate-name", + "type": "string" + }, + "type": { + "title": "label.type", + "description": "tooltip.certificate-type", + "type": "string", + "oneOf": [ + { + "enum": [ + "signing" + ], + "description": "value.signing" + }, + { + "enum": [ + "encryption" + ], + "description": "value.encryption" + }, + { + "enum": [ + "both" + ], + "description": "value.both" + } + ], + "default": "both" + }, + "value": { + "title": "label.certificate", + "description": "tooltip.certificate", + "type": "string" + } + }, + "AssertionConsumerService": { + "type": "object", + "properties": { + "locationUrl": { + "title": "label.assertion-consumer-services-location", + "description": "tooltip.assertion-consumer-service-location", + "type": "string", + "widget": { + "id": "string", + "help": "message.valid-url" + } + }, + "binding": { + "title": "label.assertion-consumer-service-location-binding", + "description": "tooltip.assertion-consumer-service-location-binding", + "type": "string", + "oneOf": [ + { + "enum": [ + "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + ], + "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + }, + { + "enum": [ + "urn:oasis:names:tc:SAML:1.0:profiles:browser-post" + ], + "description": "urn:oasis:names:tc:SAML:1.0:profiles:browser-post" + } + ] + }, + "makeDefault": { + "title": "label.mark-as-default", + "description": "tooltip.mark-as-default", + "type": "boolean" + } + } + }, + "NameIdFormatList": { + "title": "label.nameid-format-to-send", + "placeholder": "label.nameid-format", + "description": "tooltip.nameid-format", + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "widget": "datalist", + "data": [ + "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", + "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", + "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", + "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" + ] + }, + "default": null + }, + "AuthenticationMethodList": { + "title": "label.authentication-methods-to-use", + "description": "tooltip.authentication-methods-to-use", + "type": "array", + "placeholder": "label.authentication-method", + "uniqueItems": true, + "items": { + "type": "string", + "title": "label.authentication-method", + "widget": { + "id": "datalist", + "data": [ + "https://refeds.org/profile/mfa", + "urn:oasis:names:tc:SAML:2.0:ac:classes:TimeSyncToken", + "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport" + ] + } + }, + "default": null + }, + "LogoutEndpoint": { + "title": "label.new-endpoint", + "description": "tooltip.new-endpoint", + "type": "object", + "properties": { + "url": { + "title": "label.url", + "description": "tooltip.url", + "type": "string" + }, + "bindingType": { + "title": "label.binding-type", + "description": "tooltip.binding-type", + "type": "string", + "oneOf": [ + { + "enum": [ + "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + ], + "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + }, + { + "enum": [ + "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + ], + "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + } + ] + + } + } + } + } +} \ No newline at end of file From 3e1a499cec737c5a3c784b19e24e5fdd4fd1bb47 Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Thu, 4 Oct 2018 13:33:10 -0700 Subject: [PATCH 03/33] SHIBUI-914 Updating sources to use json-schema --- ui/src/app/app.module.ts | 3 + .../model/wizards/metadata-source-wizard.ts | 104 +++++ .../model/entity-attributes.filter.spec.ts | 4 +- .../filter/model/entity-attributes.filter.ts | 6 +- ui/src/app/metadata/metadata.module.ts | 7 +- .../metadata/provider/action/editor.action.ts | 43 +- .../provider-wizard-summary.component.ts | 2 +- .../container/provider-edit-step.component.ts | 9 +- .../container/provider-edit.component.ts | 4 +- .../provider-wizard-step.component.ts | 8 +- .../container/provider-wizard.component.ts | 3 +- .../metadata/provider/effect/editor.effect.ts | 16 +- .../provider/model/base.provider.form.spec.ts | 4 +- .../provider/model/base.provider.form.ts | 44 +- .../file-backed-http.provider.form.spec.ts | 4 +- ui/src/app/metadata/provider/model/index.ts | 4 +- .../app/metadata/provider/provider.module.ts | 9 +- .../provider/reducer/editor.reducer.ts | 48 +- ui/src/app/metadata/provider/reducer/index.ts | 29 -- .../metadata/resolver/action/editor.action.ts | 44 -- .../metadata/resolver/action/entity.action.ts | 46 ++ .../component/wizard-nav.component.ts | 8 +- .../container/copy-resolver.component.ts | 1 - .../resolver/container/draft.component.ts | 5 +- .../resolver/container/editor.component.ts | 16 +- ...nt.html => resolver-wizard.component.html} | 4 +- ...nt.scss => resolver-wizard.component.scss} | 0 .../container/resolver-wizard.component.ts | 119 +++++ .../resolver/container/wizard.component.ts | 155 ------ .../{editor.effect.ts => entity.effect.ts} | 14 +- .../metadata/resolver/effect/wizard.effect.ts | 10 +- .../resolver/reducer/editor.reducer.ts | 67 --- ...reducer.spec.ts => entity.reducer.spec.ts} | 96 ++-- .../resolver/reducer/entity.reducer.ts | 56 +++ ui/src/app/metadata/resolver/reducer/index.ts | 28 +- .../app/metadata/resolver/resolver.module.ts | 13 +- .../app/metadata/resolver/resolver.routing.ts | 4 +- .../metadata/resolver/wizard-definition.ts | 5 + ui/src/app/wizard/action/wizard.action.ts | 42 +- ui/src/app/wizard/model/form-definition.ts | 6 +- ui/src/app/wizard/model/wizard.ts | 7 + ui/src/app/wizard/reducer/index.ts | 29 ++ ui/src/app/wizard/reducer/wizard.reducer.ts | 51 +- .../assets/schema/source/metadata-source.json | 442 ++++++++++++++++++ 44 files changed, 1054 insertions(+), 565 deletions(-) create mode 100644 ui/src/app/metadata/domain/model/wizards/metadata-source-wizard.ts delete mode 100644 ui/src/app/metadata/resolver/action/editor.action.ts create mode 100644 ui/src/app/metadata/resolver/action/entity.action.ts rename ui/src/app/metadata/resolver/container/{wizard.component.html => resolver-wizard.component.html} (96%) rename ui/src/app/metadata/resolver/container/{wizard.component.scss => resolver-wizard.component.scss} (100%) create mode 100644 ui/src/app/metadata/resolver/container/resolver-wizard.component.ts delete mode 100644 ui/src/app/metadata/resolver/container/wizard.component.ts rename ui/src/app/metadata/resolver/effect/{editor.effect.ts => entity.effect.ts} (87%) delete mode 100644 ui/src/app/metadata/resolver/reducer/editor.reducer.ts rename ui/src/app/metadata/resolver/reducer/{editor.reducer.spec.ts => entity.reducer.spec.ts} (52%) create mode 100644 ui/src/app/metadata/resolver/reducer/entity.reducer.ts create mode 100644 ui/src/app/metadata/resolver/wizard-definition.ts create mode 100644 ui/src/assets/schema/source/metadata-source.json diff --git a/ui/src/app/app.module.ts b/ui/src/app/app.module.ts index 6d54d350a..63830bd95 100644 --- a/ui/src/app/app.module.ts +++ b/ui/src/app/app.module.ts @@ -26,6 +26,8 @@ import { FormModule } from './schema-form/schema-form.module'; import { environment } from '../environments/environment.prod'; import { getCurrentLocale } from './shared/util'; import { I18nModule } from './i18n/i18n.module'; +import { WidgetRegistry } from 'ngx-schema-form'; +import { CustomWidgetRegistry } from './schema-form/registry'; @NgModule({ declarations: [ @@ -59,6 +61,7 @@ import { I18nModule } from './i18n/i18n.module'; ], providers: [ NavigatorService, + { provide: WidgetRegistry, useClass: CustomWidgetRegistry }, { provide: RouterStateSerializer, useClass: CustomRouterStateSerializer }, { provide: HTTP_INTERCEPTORS, diff --git a/ui/src/app/metadata/domain/model/wizards/metadata-source-wizard.ts b/ui/src/app/metadata/domain/model/wizards/metadata-source-wizard.ts new file mode 100644 index 000000000..90fcb9d0c --- /dev/null +++ b/ui/src/app/metadata/domain/model/wizards/metadata-source-wizard.ts @@ -0,0 +1,104 @@ +import { Wizard, WizardStep } from '../../../../wizard/model'; +import { MetadataResolver } from '../metadata-resolver'; + +export class MetadataSourceWizard implements Wizard { + label = 'Metadata Source'; + type = '@MetadataProvider'; + steps: WizardStep[] = [ + { + index: 2, + id: 'org-info', + label: 'label.org-info', + schema: 'assets/schema/source/metadata-source.json', + fields: [ + 'organization', + 'contacts' + ] + }, + { + index: 3, + id: 'metadata-ui', + label: 'label.metadata-ui', + schema: 'assets/schema/source/metadata-source.json', + fields: [ + 'mdui' + ] + }, + { + index: 4, + id: 'descriptor-info', + label: 'label.descriptor-info', + schema: 'assets/schema/source/metadata-source.json', + fields: [ + 'serviceProviderSsoDescriptor' + ] + }, + { + index: 5, + id: 'logout-endpoints', + label: 'label.logout-endpoints', + schema: 'assets/schema/source/metadata-source.json', + fields: [ + 'logoutEndpoints' + ] + }, + { + index: 6, + id: 'key-info', + label: 'label.key-info', + schema: 'assets/schema/source/metadata-source.json', + fields: [ + 'securityInfo' + ] + }, + { + index: 7, + id: 'assertion', + label: 'label.assertion', + schema: 'assets/schema/source/metadata-source.json', + fields: [ + 'assertionConsumerServices' + ] + }, + { + index: 8, + id: 'relying-party', + label: 'label.relying-party', + schema: 'assets/schema/source/metadata-source.json', + fields: [ + 'relyingPartyOverrides' + ] + }, + { + index: 9, + id: 'attribute', + label: 'label.attribute-release', + schema: 'assets/schema/source/metadata-source.json', + fields: [ + 'attributeRelease' + ] + }, + { + index: 10, + id: 'finish', + label: 'label.finished', + schema: 'assets/schema/source/metadata-source.json', + fields: [ + 'serviceEnabled' + ], + summary: true + } + ]; + + parser (changes: Partial, schema?: any): any { + return changes; + } + + formatter (changes: Partial < MetadataResolver >, schema ?: any): any { + return changes; + } + + getValidators(...args: any[]): { [key: string]: any } { + return {}; + } +} diff --git a/ui/src/app/metadata/filter/model/entity-attributes.filter.spec.ts b/ui/src/app/metadata/filter/model/entity-attributes.filter.spec.ts index 35ebaff95..ba27a427d 100644 --- a/ui/src/app/metadata/filter/model/entity-attributes.filter.spec.ts +++ b/ui/src/app/metadata/filter/model/entity-attributes.filter.spec.ts @@ -7,8 +7,8 @@ describe('Entity Attributes filter form', () => { describe('transformer', () => { it('should not modify the object', () => { - expect(EntityAttributesFilter.translate.formatter({})).toEqual({}); - expect(EntityAttributesFilter.translate.parser({})).toEqual({}); + expect(EntityAttributesFilter.formatter({})).toEqual({}); + expect(EntityAttributesFilter.parser({})).toEqual({}); }); }); }); diff --git a/ui/src/app/metadata/filter/model/entity-attributes.filter.ts b/ui/src/app/metadata/filter/model/entity-attributes.filter.ts index bf2cec9b4..fc358dece 100644 --- a/ui/src/app/metadata/filter/model/entity-attributes.filter.ts +++ b/ui/src/app/metadata/filter/model/entity-attributes.filter.ts @@ -9,8 +9,6 @@ export const EntityAttributesFilter: FormDefinition = { const validators = {}; return validators; }, - translate: { - parser: (changes: any): MetadataFilter => changes, - formatter: (changes: MetadataFilter): any => changes - } + parser: (changes: any): MetadataFilter => changes, + formatter: (changes: MetadataFilter): any => changes }; diff --git a/ui/src/app/metadata/metadata.module.ts b/ui/src/app/metadata/metadata.module.ts index 9c88a04df..946c89423 100644 --- a/ui/src/app/metadata/metadata.module.ts +++ b/ui/src/app/metadata/metadata.module.ts @@ -1,6 +1,5 @@ import { NgModule } from '@angular/core'; - import { ResolverModule } from './resolver/resolver.module'; import { FilterModule } from './filter/filter.module'; import { DomainModule } from './domain/domain.module'; @@ -9,6 +8,8 @@ import { ManagerModule } from './manager/manager.module'; import { MetadataRoutingModule } from './metadata.routing'; import { ProviderModule } from './provider/provider.module'; import { I18nModule } from '../i18n/i18n.module'; +import { CustomWidgetRegistry } from '../schema-form/registry'; +import { WidgetRegistry } from 'ngx-schema-form'; @NgModule({ @@ -21,7 +22,9 @@ import { I18nModule } from '../i18n/i18n.module'; MetadataRoutingModule, I18nModule ], - providers: [], + providers: [ + { provide: WidgetRegistry, useClass: CustomWidgetRegistry } + ], declarations: [ MetadataPageComponent ] diff --git a/ui/src/app/metadata/provider/action/editor.action.ts b/ui/src/app/metadata/provider/action/editor.action.ts index 3c82c0c19..37a99c98b 100644 --- a/ui/src/app/metadata/provider/action/editor.action.ts +++ b/ui/src/app/metadata/provider/action/editor.action.ts @@ -2,16 +2,8 @@ import { Action } from '@ngrx/store'; export enum EditorActionTypes { UPDATE_STATUS = '[Provider Editor] Update Status', - LOAD_SCHEMA_REQUEST = '[Provider Editor] Load Schema Request', - LOAD_SCHEMA_SUCCESS = '[Provider Editor] Load Schema Success', - LOAD_SCHEMA_FAIL = '[Provider Editor] Load Schema Fail', - SELECT_PROVIDER_TYPE = '[Provider Editor] Select Provider Type', - - CLEAR = '[Provider Editor] Clear', - - LOCK = '[Provider Editor] Lock', - UNLOCK = '[Provider Editor] Unlock' + CLEAR = '[Provider Editor] Clear' } export class UpdateStatus implements Action { @@ -20,24 +12,6 @@ export class UpdateStatus implements Action { constructor(public payload: { [key: string]: string }) { } } -export class LoadSchemaRequest implements Action { - readonly type = EditorActionTypes.LOAD_SCHEMA_REQUEST; - - constructor(public payload: string) { } -} - -export class LoadSchemaSuccess implements Action { - readonly type = EditorActionTypes.LOAD_SCHEMA_SUCCESS; - - constructor(public payload: any) { } -} - -export class LoadSchemaFail implements Action { - readonly type = EditorActionTypes.LOAD_SCHEMA_FAIL; - - constructor(public payload: Error) { } -} - export class SelectProviderType implements Action { readonly type = EditorActionTypes.SELECT_PROVIDER_TYPE; @@ -48,20 +22,7 @@ export class ClearEditor implements Action { readonly type = EditorActionTypes.CLEAR; } -export class LockEditor implements Action { - readonly type = EditorActionTypes.LOCK; -} - -export class UnlockEditor implements Action { - readonly type = EditorActionTypes.UNLOCK; -} - export type EditorActionUnion = | UpdateStatus - | LoadSchemaRequest - | LoadSchemaSuccess - | LoadSchemaFail | SelectProviderType - | ClearEditor - | LockEditor - | UnlockEditor; + | ClearEditor; 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 index c74c4935b..8d350d163 100644 --- a/ui/src/app/metadata/provider/component/provider-wizard-summary.component.ts +++ b/ui/src/app/metadata/provider/component/provider-wizard-summary.component.ts @@ -58,7 +58,7 @@ export class ProviderWizardSummaryComponent implements OnChanges { id: step.id, index: step.index, label: step.label, - properties: getStepProperties(schemas[step.id], def.translate.formatter(model)) + properties: getStepProperties(schemas[step.id], def.formatter(model)) }) ); diff --git a/ui/src/app/metadata/provider/container/provider-edit-step.component.ts b/ui/src/app/metadata/provider/container/provider-edit-step.component.ts index 6ffc7e13c..6ba2346bb 100644 --- a/ui/src/app/metadata/provider/container/provider-edit-step.component.ts +++ b/ui/src/app/metadata/provider/container/provider-edit-step.component.ts @@ -3,9 +3,10 @@ import { Observable, Subject } from 'rxjs'; import { Store } from '@ngrx/store'; import * as fromProvider from '../reducer'; -import { UpdateStatus, LockEditor, UnlockEditor } from '../action/editor.action'; +import { UpdateStatus } from '../action/editor.action'; import { Wizard, WizardStep } from '../../../wizard/model'; import { MetadataProvider } from '../../domain/model'; +import { LockEditor, UnlockEditor } from '../../../wizard/action/wizard.action'; import * as fromWizard from '../../../wizard/reducer'; import { withLatestFrom, map, skipWhile, distinctUntilChanged, startWith, combineLatest } from 'rxjs/operators'; @@ -46,7 +47,7 @@ export class ProviderEditStepComponent implements OnDestroy { this.changes$ = this.store.select(fromProvider.getEntityChanges); this.provider$ = this.store.select(fromProvider.getSelectedProvider); this.step$ = this.store.select(fromWizard.getCurrent); - this.schema$ = this.store.select(fromProvider.getSchema); + this.schema$ = this.store.select(fromWizard.getParsedSchema); this.step$.subscribe(s => { if (s && s.locked) { @@ -86,14 +87,14 @@ export class ProviderEditStepComponent implements OnDestroy { definition })), skipWhile(({ model, definition }) => !definition || !model), - map(({ model, definition }) => definition.translate.formatter(model)) + map(({ model, definition }) => definition.formatter(model)) ); this.valueChangeEmitted$.pipe( map(changes => changes.value), withLatestFrom(this.definition$), skipWhile(([ changes, definition ]) => !definition || !changes), - map(([ changes, definition ]) => definition.translate.parser(changes)) + map(([ changes, definition ]) => definition.parser(changes)) ) .subscribe(changes => this.store.dispatch(new UpdateProvider(changes))); diff --git a/ui/src/app/metadata/provider/container/provider-edit.component.ts b/ui/src/app/metadata/provider/container/provider-edit.component.ts index a2c2e0b86..a1de135b6 100644 --- a/ui/src/app/metadata/provider/container/provider-edit.component.ts +++ b/ui/src/app/metadata/provider/container/provider-edit.component.ts @@ -5,8 +5,8 @@ import { skipWhile, map, combineLatest } from 'rxjs/operators'; import { Store } from '@ngrx/store'; import * as fromWizard from '../../../wizard/reducer'; import * as fromProvider from '../reducer'; -import { ClearWizard, SetDefinition, SetIndex } from '../../../wizard/action/wizard.action'; -import { ClearEditor, LoadSchemaRequest } from '../action/editor.action'; +import { ClearWizard, SetDefinition, SetIndex, LoadSchemaRequest } from '../../../wizard/action/wizard.action'; +import { ClearEditor } from '../action/editor.action'; import { MetadataProvider } from '../../domain/model'; import { ClearProvider } from '../action/entity.action'; import { Wizard } from '../../../wizard/model'; 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 index 0b9a7469e..db8e461ee 100644 --- a/ui/src/app/metadata/provider/container/provider-wizard-step.component.ts +++ b/ui/src/app/metadata/provider/container/provider-wizard-step.component.ts @@ -42,10 +42,12 @@ export class ProviderWizardStepComponent implements OnDestroy { constructor( private store: Store, ) { - this.schema$ = this.store.select(fromProvider.getSchema); + this.schema$ = this.store.select(fromWizard.getParsedSchema); this.definition$ = this.store.select(fromWizard.getWizardDefinition); this.changes$ = this.store.select(fromProvider.getEntityChanges); + this.schema$.subscribe(s => console.log(s)); + this.validators$ = this.definition$.pipe( withLatestFrom( this.store.select(fromProvider.getProviderNames), @@ -71,14 +73,14 @@ export class ProviderWizardStepComponent implements OnDestroy { definition })), skipWhile(({ model, definition }) => !definition || !model), - map(({ model, definition }) => definition.translate.formatter(model)) + map(({ model, definition }) => definition.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)) + map(({ changes, definition }) => definition.parser(changes)) ) .subscribe(changes => this.store.dispatch(new UpdateProvider(changes))); 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 8a4e481f5..24140a0a2 100644 --- a/ui/src/app/metadata/provider/container/provider-wizard.component.ts +++ b/ui/src/app/metadata/provider/container/provider-wizard.component.ts @@ -5,7 +5,8 @@ import { Store } from '@ngrx/store'; import * as fromProvider from '../reducer'; import * as fromWizard from '../../../wizard/reducer'; import { SetIndex, SetDisabled, ClearWizard, SetDefinition } from '../../../wizard/action/wizard.action'; -import { LoadSchemaRequest, ClearEditor } from '../action/editor.action'; +import { ClearEditor } from '../action/editor.action'; +import { LoadSchemaRequest } from '../../../wizard/action/wizard.action'; import { startWith } from 'rxjs/operators'; import { Wizard, WizardStep } from '../../../wizard/model'; import { MetadataProvider } from '../../domain/model'; diff --git a/ui/src/app/metadata/provider/effect/editor.effect.ts b/ui/src/app/metadata/provider/effect/editor.effect.ts index 3146ff91d..f9ac13baf 100644 --- a/ui/src/app/metadata/provider/effect/editor.effect.ts +++ b/ui/src/app/metadata/provider/effect/editor.effect.ts @@ -3,14 +3,18 @@ import { Effect, Actions, ofType } from '@ngrx/effects'; import { SchemaService } from '../../../schema-form/service/schema.service'; import { - LoadSchemaRequest, - LoadSchemaSuccess, - LoadSchemaFail, EditorActionTypes } from '../action/editor.action'; import { map, switchMap, catchError, withLatestFrom, debounceTime } from 'rxjs/operators'; import { of } from 'rxjs'; -import { SetDefinition, WizardActionTypes, AddSchema } from '../../../wizard/action/wizard.action'; +import { + LoadSchemaRequest, + LoadSchemaSuccess, + LoadSchemaFail, + SetDefinition, + WizardActionTypes, + AddSchema +} from '../../../wizard/action/wizard.action'; import { ResetChanges } from '../action/entity.action'; import * as fromWizard from '../../../wizard/reducer'; @@ -21,7 +25,7 @@ export class EditorEffects { @Effect() $loadSchemaRequest = this.actions$.pipe( - ofType(EditorActionTypes.LOAD_SCHEMA_REQUEST), + ofType(WizardActionTypes.LOAD_SCHEMA_REQUEST), map(action => action.payload), debounceTime(100), switchMap((schemaPath: string) => @@ -36,7 +40,7 @@ export class EditorEffects { @Effect() $loadSchemaSuccess = this.actions$.pipe( - ofType(EditorActionTypes.LOAD_SCHEMA_SUCCESS), + ofType(WizardActionTypes.LOAD_SCHEMA_SUCCESS), map(action => action.payload), withLatestFrom(this.store.select(fromWizard.getWizardIndex)), map(([schema, id]) => new AddSchema({ id, schema })) diff --git a/ui/src/app/metadata/provider/model/base.provider.form.spec.ts b/ui/src/app/metadata/provider/model/base.provider.form.spec.ts index 307368ef2..7e1aeff61 100644 --- a/ui/src/app/metadata/provider/model/base.provider.form.spec.ts +++ b/ui/src/app/metadata/provider/model/base.provider.form.spec.ts @@ -2,8 +2,8 @@ import { BaseMetadataProviderEditor } from './base.provider.form'; describe('BaseMetadataProviderForm', () => { - const parser = BaseMetadataProviderEditor.translate.parser; - const formatter = BaseMetadataProviderEditor.translate.formatter; + const parser = BaseMetadataProviderEditor.parser; + const formatter = BaseMetadataProviderEditor.formatter; const requiredValidUntilFilter = { maxValidityInterval: 1, diff --git a/ui/src/app/metadata/provider/model/base.provider.form.ts b/ui/src/app/metadata/provider/model/base.provider.form.ts index 33ff3aa3a..459eaa956 100644 --- a/ui/src/app/metadata/provider/model/base.provider.form.ts +++ b/ui/src/app/metadata/provider/model/base.provider.form.ts @@ -42,28 +42,26 @@ export const BaseMetadataProviderEditor: Wizard = { }; return validators; }, - translate: { - parser: (changes: any): BaseMetadataProvider => changes.metadataFilters ? ({ - ...changes, - metadataFilters: [ - ...Object.keys(changes.metadataFilters).reduce((collection, filterName) => ([ - ...collection, - { - ...changes.metadataFilters[filterName], - '@type': filterName - } - ]), []) - ] - }) : changes, - formatter: (changes: BaseMetadataProvider): any => changes.metadataFilters ? ({ - ...changes, - metadataFilters: { - ...(changes.metadataFilters || []).reduce((collection, filter) => ({ - ...collection, - [filter['@type']]: filter - }), {}) - } - }) : changes - }, + parser: (changes: any): BaseMetadataProvider => (changes.metadataFilters ? ({ + ...changes, + metadataFilters: [ + ...Object.keys(changes.metadataFilters).reduce((collection, filterName) => ([ + ...collection, + { + ...changes.metadataFilters[filterName], + '@type': filterName + } + ]), []) + ] + }) : changes), + formatter: (changes: BaseMetadataProvider): any => (changes.metadataFilters ? ({ + ...changes, + metadataFilters: { + ...(changes.metadataFilters || []).reduce((collection, filter) => ({ + ...collection, + [filter['@type']]: filter + }), {}) + } + }) : changes), steps: [] }; 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 index 4bba62c7d..492bc8f3d 100644 --- 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 @@ -3,8 +3,8 @@ import { FileBackedHttpMetadataProvider } from '../../domain/model/providers'; describe('FileBackedHttpMetadataProviderWizard', () => { - const parser = FileBackedHttpMetadataProviderWizard.translate.parser; - const formatter = FileBackedHttpMetadataProviderWizard.translate.formatter; + const parser = FileBackedHttpMetadataProviderWizard.parser; + const formatter = FileBackedHttpMetadataProviderWizard.formatter; const requiredValidUntilFilter = { maxValidityInterval: 1, diff --git a/ui/src/app/metadata/provider/model/index.ts b/ui/src/app/metadata/provider/model/index.ts index a6c438df4..cfb64a08d 100644 --- a/ui/src/app/metadata/provider/model/index.ts +++ b/ui/src/app/metadata/provider/model/index.ts @@ -1,14 +1,12 @@ import { FileBackedHttpMetadataProviderWizard } from './file-backed-http.provider.form'; import { FileBackedHttpMetadataProviderEditor } from './file-backed-http.provider.form'; -import { BaseMetadataProviderEditor } from './base.provider.form'; export const MetadataProviderWizardTypes = [ FileBackedHttpMetadataProviderWizard ]; export const MetadataProviderEditorTypes = [ - FileBackedHttpMetadataProviderEditor, - BaseMetadataProviderEditor + FileBackedHttpMetadataProviderEditor ]; export * from './file-backed-http.provider.form'; diff --git a/ui/src/app/metadata/provider/provider.module.ts b/ui/src/app/metadata/provider/provider.module.ts index 219f3d19c..4ce52c7db 100644 --- a/ui/src/app/metadata/provider/provider.module.ts +++ b/ui/src/app/metadata/provider/provider.module.ts @@ -15,9 +15,7 @@ import { WizardModule } from '../../wizard/wizard.module'; import * as fromProvider from './reducer'; import { EditorEffects } from './effect/editor.effect'; -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'; @@ -32,6 +30,8 @@ import { UnsavedProviderComponent } from './component/unsaved-provider.dialog'; import { ContentionModule } from '../../contention/contention.module'; import { DeleteFilterComponent } from './component/delete-filter.component'; import { I18nModule } from '../../i18n/i18n.module'; +import { WidgetRegistry } from 'ngx-schema-form'; +import { CustomWidgetRegistry } from '../../schema-form/registry'; @NgModule({ declarations: [ @@ -70,10 +70,7 @@ import { I18nModule } from '../../i18n/i18n.module'; export class ProviderModule { static forRoot(): ModuleWithProviders { return { - ngModule: RootProviderModule, - providers: [ - { provide: WidgetRegistry, useClass: CustomWidgetRegistry } - ] + ngModule: RootProviderModule }; } } diff --git a/ui/src/app/metadata/provider/reducer/editor.reducer.ts b/ui/src/app/metadata/provider/reducer/editor.reducer.ts index 32c96c86c..0d56550b7 100644 --- a/ui/src/app/metadata/provider/reducer/editor.reducer.ts +++ b/ui/src/app/metadata/provider/reducer/editor.reducer.ts @@ -2,20 +2,12 @@ import { EditorActionTypes, EditorActionUnion } from '../action/editor.action'; export interface EditorState { status: { [key: string]: string }; - schemaPath: string; - loading: boolean; - schema: any; type: string; - locked: boolean; } export const initialState: EditorState = { status: {}, - schemaPath: null, - loading: false, - schema: null, - type: null, - locked: false + type: null }; export function reducer(state = initialState, action: EditorActionUnion): EditorState { @@ -40,49 +32,13 @@ export function reducer(state = initialState, action: EditorActionUnion): Editor } }; } - case EditorActionTypes.LOAD_SCHEMA_REQUEST: { - return { - ...state, - loading: true, - schemaPath: action.payload - }; - } - case EditorActionTypes.LOAD_SCHEMA_SUCCESS: { - return { - ...state, - loading: false, - schema: action.payload - }; - } - case EditorActionTypes.LOAD_SCHEMA_FAIL: { - return { - ...state, - loading: false, - schema: initialState.schema - }; - } - - case EditorActionTypes.LOCK: { - return { - ...state, - locked: true - }; - } - - case EditorActionTypes.UNLOCK: { - return { - ...state, - locked: false - }; - } default: { return state; } } } -export const getSchema = (state: EditorState) => state.schema; -export const getLocked = (state: EditorState) => state.locked; + export const isEditorValid = (state: EditorState) => !Object.keys(state.status).some(key => state.status[key] === ('INVALID')); diff --git a/ui/src/app/metadata/provider/reducer/index.ts b/ui/src/app/metadata/provider/reducer/index.ts index f9def08d4..c05a233e3 100644 --- a/ui/src/app/metadata/provider/reducer/index.ts +++ b/ui/src/app/metadata/provider/reducer/index.ts @@ -39,35 +39,6 @@ export const getCollectionState = createSelector(getProviderState, getCollection Editor State */ -export function getSchemaParseFn(schema, locked): any { - if (!schema) { - return null; - } - return { - ...schema, - properties: Object.keys(schema.properties).reduce((prev, current) => { - return { - ...prev, - [current]: { - ...schema.properties[current], - readOnly: locked, - ...(schema.properties[current].hasOwnProperty('properties') ? - getSchemaParseFn(schema.properties[current], locked) : - {} - ) - } - }; - }, {}) - }; -} - -export const getSchemaLockedFn = (step, locked) => step ? step.locked ? locked : false : false; -export const getLockedStatus = createSelector(getEditorState, fromEditor.getLocked); -export const getLocked = createSelector(fromWizard.getCurrent, getLockedStatus, getSchemaLockedFn); - -export const getSchemaObject = createSelector(getEditorState, fromEditor.getSchema); -export const getSchema = createSelector(getSchemaObject, getLocked, getSchemaParseFn); - export const getEditorIsValid = createSelector(getEditorState, fromEditor.isEditorValid); export const getFormStatus = createSelector(getEditorState, fromEditor.getFormStatus); diff --git a/ui/src/app/metadata/resolver/action/editor.action.ts b/ui/src/app/metadata/resolver/action/editor.action.ts deleted file mode 100644 index 80da7774e..000000000 --- a/ui/src/app/metadata/resolver/action/editor.action.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Action } from '@ngrx/store'; -import { MetadataResolver } from '../../domain/model'; - -export const UPDATE_STATUS = '[Editor] Update Status'; -export const UPDATE_SAVED = '[Editor] Update Saved'; -export const UPDATE_CHANGES = '[Editor] Update Changes'; -export const CANCEL_CHANGES = '[Editor] Cancel Changes'; -export const SAVE_CHANGES = '[Editor] Save Changes'; -export const RESET_CHANGES = '[Editor] Reset Changes'; - -export class UpdateStatus implements Action { - readonly type = UPDATE_STATUS; - - constructor(public payload: { [key: string]: string }) { } -} - -export class UpdateChanges implements Action { - readonly type = UPDATE_CHANGES; - - constructor(public payload: MetadataResolver) { } -} - -export class CancelChanges implements Action { - readonly type = CANCEL_CHANGES; -} - -export class SaveChanges implements Action { - readonly type = SAVE_CHANGES; - - constructor(public payload: MetadataResolver) { } -} - -export class ResetChanges implements Action { - readonly type = RESET_CHANGES; - - constructor() { } -} - -export type Actions = - | UpdateStatus - | UpdateChanges - | CancelChanges - | SaveChanges - | ResetChanges; diff --git a/ui/src/app/metadata/resolver/action/entity.action.ts b/ui/src/app/metadata/resolver/action/entity.action.ts new file mode 100644 index 000000000..490cd6a03 --- /dev/null +++ b/ui/src/app/metadata/resolver/action/entity.action.ts @@ -0,0 +1,46 @@ +import { Action } from '@ngrx/store'; +import { MetadataResolver } from '../../domain/model'; + +export enum ResolverEntityActionTypes { + UPDATE_STATUS = '[Resolver Entity] Update Status', + UPDATE_SAVING = '[Resolver Entity] Update Saving', + UPDATE_CHANGES = '[Resolver Entity] Update Changes', + CLEAR = '[Resolver Entity] Clear', + CANCEL = '[Resolver Entity] Cancel' +} + +export class UpdateStatus implements Action { + readonly type = ResolverEntityActionTypes.UPDATE_STATUS; + + constructor(public payload: { [key: string]: string }) { } +} + +export class UpdateChanges implements Action { + readonly type = ResolverEntityActionTypes.UPDATE_CHANGES; + + constructor(public payload: MetadataResolver) { } +} + +export class UpdateSaving implements Action { + readonly type = ResolverEntityActionTypes.UPDATE_SAVING; + + constructor(public payload: boolean) { } +} + +export class Clear implements Action { + readonly type = ResolverEntityActionTypes.CLEAR; + + constructor() { } +} + +export class Cancel implements Action { + readonly type = ResolverEntityActionTypes.CANCEL; + + constructor() { } +} + +export type ResolverEntityActionUnion = + | UpdateStatus + | UpdateChanges + | UpdateSaving + | Clear; diff --git a/ui/src/app/metadata/resolver/component/wizard-nav.component.ts b/ui/src/app/metadata/resolver/component/wizard-nav.component.ts index c55a30191..e743c887e 100644 --- a/ui/src/app/metadata/resolver/component/wizard-nav.component.ts +++ b/ui/src/app/metadata/resolver/component/wizard-nav.component.ts @@ -2,7 +2,7 @@ import { Component, Input, Output, EventEmitter, OnChanges } from '@angular/core import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { Store } from '@ngrx/store'; -import * as fromEditor from '../reducer'; +import * as fromResolver from '../reducer'; import { WIZARD as WizardDef, EditorFlowDefinition } from '../editor-definition.const'; @Component({ @@ -27,7 +27,7 @@ export class WizardNavComponent implements OnChanges { wizard: EditorFlowDefinition[] = WizardDef; constructor( - private store: Store + private store: Store ) {} ngOnChanges(): void { @@ -38,8 +38,8 @@ export class WizardNavComponent implements OnChanges { this.isFirstPage = WizardDef[0].index === this.index; this.wizard = WizardDef; - this.wizardIsValid$ = this.store.select(fromEditor.getEditorIsValid); - this.wizardIsSaving$ = this.store.select(fromEditor.getEditorIsSaving); + this.wizardIsValid$ = this.store.select(fromResolver.getEntityIsValid); + this.wizardIsSaving$ = this.store.select(fromResolver.getEntityIsSaving); this.wizardIsInvalid$ = this.wizardIsValid$.pipe(map(valid => !valid)); } } /* istanbul ignore next */ diff --git a/ui/src/app/metadata/resolver/container/copy-resolver.component.ts b/ui/src/app/metadata/resolver/container/copy-resolver.component.ts index 217fb4226..c07f88c35 100644 --- a/ui/src/app/metadata/resolver/container/copy-resolver.component.ts +++ b/ui/src/app/metadata/resolver/container/copy-resolver.component.ts @@ -16,7 +16,6 @@ import { SearchIds } from '../action/search.action'; import * as fromProvider from '../reducer'; import { CreateResolverCopyRequest, UpdateResolverCopySections } from '../action/copy.action'; - @Component({ selector: 'copy-resolver-form', templateUrl: './copy-resolver.component.html' diff --git a/ui/src/app/metadata/resolver/container/draft.component.ts b/ui/src/app/metadata/resolver/container/draft.component.ts index b1838f18e..e8c865c53 100644 --- a/ui/src/app/metadata/resolver/container/draft.component.ts +++ b/ui/src/app/metadata/resolver/container/draft.component.ts @@ -1,7 +1,6 @@ -import { Component, Output, Input, EventEmitter, OnDestroy, ChangeDetectionStrategy } from '@angular/core'; -import { FormBuilder, FormGroup, FormControl, Validators } from '@angular/forms'; +import { Component, OnDestroy } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { Observable, Subscription } from 'rxjs'; +import { Subscription } from 'rxjs'; import { distinctUntilChanged, map } from 'rxjs/operators'; import { Store } from '@ngrx/store'; diff --git a/ui/src/app/metadata/resolver/container/editor.component.ts b/ui/src/app/metadata/resolver/container/editor.component.ts index 0071a97dc..312860f51 100644 --- a/ui/src/app/metadata/resolver/container/editor.component.ts +++ b/ui/src/app/metadata/resolver/container/editor.component.ts @@ -20,7 +20,7 @@ import * as fromResolver from '../reducer'; import { UpdateResolverRequest } from '../action/collection.action'; import { ProviderStatusEmitter, ProviderValueEmitter } from '../../domain/service/provider-change-emitter.service'; -import { UpdateStatus, UpdateChanges, CancelChanges } from '../action/editor.action'; +import { UpdateStatus, UpdateChanges, Cancel } from '../action/entity.action'; import { EDITOR as EditorDef, EditorFlowDefinition } from '../editor-definition.const'; import { UnsavedDialogComponent } from '../component/unsaved-dialog.component'; @@ -69,7 +69,7 @@ export class EditorComponent implements OnInit, OnDestroy { private modalService: NgbModal ) { this.resolver$ = this.store.select(fromResolver.getSelectedResolver); - this.changes$ = this.store.select(fromResolver.getEditorChanges); + this.changes$ = this.store.select(fromResolver.getEntityChanges); this.latest$ = this.resolver$.pipe( combineLatest(this.changes$, (base, changes) => Object.assign({}, base, changes)) @@ -79,11 +79,11 @@ export class EditorComponent implements OnInit, OnDestroy { this.editorIndex$ = this.route.params.pipe(map(params => Number(params.index))); this.currentPage$ = this.editorIndex$.pipe(map(index => EditorDef.find(r => r.index === index))); this.editor = EditorDef; - this.store.select(fromResolver.getEditorIsSaving).pipe( + this.store.select(fromResolver.getEntityIsSaving).pipe( takeUntil(this.ngUnsubscribe) ).subscribe(saving => this.saving = saving); - this.wizardIsValid$ = this.store.select(fromResolver.getEditorIsValid); + this.wizardIsValid$ = this.store.select(fromResolver.getEntityIsValid); this.wizardIsInvalid$ = this.wizardIsValid$.pipe(map(valid => !valid)); this.ids$ = this.store @@ -105,7 +105,7 @@ export class EditorComponent implements OnInit, OnDestroy { } cancel(): void { - this.store.dispatch(new CancelChanges()); + this.store.dispatch(new Cancel()); } go(event, index: number): void { @@ -147,7 +147,7 @@ export class EditorComponent implements OnInit, OnDestroy { skipWhile(() => this.saving) ).subscribe(latest => this.latest = latest); - this.invalidForms$ = this.store.select(fromResolver.getInvalidEditorForms); + this.invalidForms$ = this.store.select(fromResolver.getInvalidEntityForms); this.invalidForms$.pipe( distinctUntilChanged(), @@ -176,12 +176,12 @@ export class EditorComponent implements OnInit, OnDestroy { let modal = this.modalService.open(UnsavedDialogComponent); modal.componentInstance.resolver = this.latest; modal.componentInstance.message = 'editor'; - modal.componentInstance.action = new CancelChanges(); + modal.componentInstance.action = new Cancel(); modal.result.then( () => this.router.navigate([nextState.url]), () => console.warn('denied') ); } - return this.store.select(fromResolver.getEditorIsSaved); + return this.store.select(fromResolver.getEntityIsSaved); } } diff --git a/ui/src/app/metadata/resolver/container/wizard.component.html b/ui/src/app/metadata/resolver/container/resolver-wizard.component.html similarity index 96% rename from ui/src/app/metadata/resolver/container/wizard.component.html rename to ui/src/app/metadata/resolver/container/resolver-wizard.component.html index 6487cd6fc..b8a398c95 100644 --- a/ui/src/app/metadata/resolver/container/wizard.component.html +++ b/ui/src/app/metadata/resolver/container/resolver-wizard.component.html @@ -7,11 +7,11 @@ Add a new metadata source – {{ (currentPage$ | async).label | translate }} ({{ (resolver$ | async).serviceProviderName }}) - + diff --git a/ui/src/app/metadata/resolver/container/wizard.component.scss b/ui/src/app/metadata/resolver/container/resolver-wizard.component.scss similarity index 100% rename from ui/src/app/metadata/resolver/container/wizard.component.scss rename to ui/src/app/metadata/resolver/container/resolver-wizard.component.scss diff --git a/ui/src/app/metadata/resolver/container/resolver-wizard.component.ts b/ui/src/app/metadata/resolver/container/resolver-wizard.component.ts new file mode 100644 index 000000000..92b5738f6 --- /dev/null +++ b/ui/src/app/metadata/resolver/container/resolver-wizard.component.ts @@ -0,0 +1,119 @@ +import { + Component, + OnDestroy, + Inject +} from '@angular/core'; +import { + ActivatedRoute, + Router, + ActivatedRouteSnapshot, + RouterStateSnapshot +} from '@angular/router'; +import { Observable, Subject, of } from 'rxjs'; +import { skipWhile } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; + +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import { MetadataResolver } from '../../domain/model/metadata-resolver'; +import * as fromCollections from '../reducer'; +import * as draftActions from '../action/draft.action'; +import { AddResolverRequest } from '../action/collection.action'; +import * as fromResolver from '../reducer'; + +import { UpdateChanges } from '../action/entity.action'; +import { CanComponentDeactivate } from '../../../core/service/can-deactivate.guard'; + +import { UnsavedDialogComponent } from '../component/unsaved-dialog.component'; +import { METADATA_SOURCE_WIZARD } from '../wizard-definition'; +import { Wizard } from '../../../wizard/model'; +import { SetDefinition, SetIndex } from '../../../wizard/action/wizard.action'; + +import * as fromWizard from '../../../wizard/reducer'; +import { LoadSchemaRequest } from '../../../wizard/action/wizard.action'; + +@Component({ + selector: 'resolver-wizard-page', + templateUrl: './resolver-wizard.component.html', + styleUrls: ['./resolver-wizard.component.scss'] +}) +export class ResolverWizardComponent implements OnDestroy, CanComponentDeactivate { + + private ngUnsubscribe: Subject = new Subject(); + + resolver$: Observable; + resolver: MetadataResolver; + providerName$: Observable; + changes$: Observable; + changes: MetadataResolver; + latest: MetadataResolver; + + wizardIndex$: Observable; + wizardIndex: number; + currentPage$: Observable; + wizard$: Observable; + saved$: Observable; + saving: boolean; + + constructor( + private store: Store, + private route: ActivatedRoute, + private router: Router, + private modalService: NgbModal, + @Inject(METADATA_SOURCE_WIZARD) private sourceWizard: Wizard + ) { + this.store + .select(fromWizard.getCurrentWizardSchema) + .pipe( + skipWhile(s => !s) + ) + .subscribe(s => { + if (s) { + this.store.dispatch(new LoadSchemaRequest(s)); + } + }); + + this.store.dispatch(new SetDefinition(this.sourceWizard)); + this.store.dispatch(new SetIndex(this.sourceWizard.steps[0].id)); + + this.store.select(fromWizard.getParsedSchema).subscribe(s => console.log(s)); + } + + save(): void { + this.store.dispatch(new AddResolverRequest(this.latest)); + } + + next(index: number): void { + this.go(index.toString()); + } + + previous(index: number): void { + this.go(index.toString()); + } + + go(index: string): void { + this.store.dispatch(new SetIndex(index)); + } + + ngOnDestroy(): void { + this.ngUnsubscribe.next(); + this.ngUnsubscribe.complete(); + } + + canDeactivate( + currentRoute: ActivatedRouteSnapshot, + currentState: RouterStateSnapshot, + nextState: RouterStateSnapshot + ): Observable { + if (nextState.url.match('wizard')) { return of(true); } + if (Object.keys(this.changes).length > 0) { + let modal = this.modalService.open(UnsavedDialogComponent); + modal.componentInstance.action = new UpdateChanges(this.latest); + modal.result.then( + () => this.router.navigate([nextState.url]), + () => console.warn('denied') + ); + } + return this.store.select(fromResolver.getEntityIsSaved); + } +} /* istanbul ignore next */ diff --git a/ui/src/app/metadata/resolver/container/wizard.component.ts b/ui/src/app/metadata/resolver/container/wizard.component.ts deleted file mode 100644 index e8efa06c3..000000000 --- a/ui/src/app/metadata/resolver/container/wizard.component.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { - Component, - OnInit, - OnDestroy -} from '@angular/core'; -import { - ActivatedRoute, - Router, - ActivatedRouteSnapshot, - RouterStateSnapshot -} from '@angular/router'; -import { Observable, Subject, of } from 'rxjs'; -import { map, takeUntil, skipWhile, combineLatest } from 'rxjs/operators'; -import { Store } from '@ngrx/store'; - -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; - -import { MetadataResolver } from '../../domain/model/metadata-resolver'; -import * as fromCollections from '../reducer'; -import * as draftActions from '../action/draft.action'; -import { AddResolverRequest } from '../action/collection.action'; -import * as fromEditor from '../reducer'; - -import { ProviderStatusEmitter, ProviderValueEmitter } from '../../domain/service/provider-change-emitter.service'; -import { UpdateStatus, UpdateChanges, SaveChanges } from '../action/editor.action'; -import { WIZARD as WizardDef, EditorFlowDefinition } from '../editor-definition.const'; -import { CanComponentDeactivate } from '../../../core/service/can-deactivate.guard'; - -import { UnsavedDialogComponent } from '../component/unsaved-dialog.component'; - - -@Component({ - selector: 'wizard-page', - templateUrl: './wizard.component.html', - styleUrls: ['./wizard.component.scss'] -}) -export class WizardComponent implements OnInit, OnDestroy, CanComponentDeactivate { - - private ngUnsubscribe: Subject = new Subject(); - - resolver$: Observable; - resolver: MetadataResolver; - providerName$: Observable; - changes$: Observable; - changes: MetadataResolver; - latest: MetadataResolver; - - wizardIndex$: Observable; - wizardIndex: number; - currentPage$: Observable; - wizard$: Observable; - wizard: EditorFlowDefinition[]; - saved$: Observable; - saving: boolean; - - constructor( - private store: Store, - private route: ActivatedRoute, - private router: Router, - private statusEmitter: ProviderStatusEmitter, - private valueEmitter: ProviderValueEmitter, - private modalService: NgbModal - ) { - this.resolver$ = this.store.select(fromCollections.getSelectedDraft); - this.providerName$ = this.resolver$.pipe( - map(p => p.serviceProviderName) - ); - this.changes$ = this.store.select(fromEditor.getEditorChanges); - - this.wizardIndex$ = this.route.params.pipe(map(params => Number(params.index))); - this.currentPage$ = this.wizardIndex$.pipe(map(index => WizardDef.find(r => r.index === index))); - this.wizard = WizardDef; - - this.wizardIndex$.pipe(takeUntil(this.ngUnsubscribe)).subscribe(i => this.wizardIndex = i); - - this.saved$ = this.store.select(fromEditor.getEditorIsSaved); - - this.store.select(fromEditor.getEditorIsSaving).pipe(takeUntil(this.ngUnsubscribe)).subscribe(saving => this.saving = saving); - } - - save(): void { - this.store.dispatch(new AddResolverRequest(this.latest)); - } - - next(index: number): void { - this.go(index); - } - - previous(index: number): void { - this.go(index); - } - - go(index: number): void { - this.store.dispatch(new draftActions.UpdateDraftRequest(this.latest)); - this.router.navigate(['../', index], { relativeTo: this.route }); - } - - ngOnInit(): void { - this.subscribe(); - } - - subscribe(): void { - this.resolver$.pipe( - takeUntil(this.ngUnsubscribe), - skipWhile(() => this.saving) - ).subscribe(resolver => this.resolver = resolver); - this.changes$.pipe( - takeUntil(this.ngUnsubscribe), - skipWhile(() => this.saving) - ).subscribe(changes => this.changes = changes); - this.changes$.pipe( - takeUntil(this.ngUnsubscribe), - skipWhile(() => this.saving), - combineLatest(this.resolver$, (changes, base) => ({ ...base, ...changes })) - ).subscribe(latest => this.latest = latest); - - this.valueEmitter - .changeEmitted$ - .pipe( - takeUntil(this.ngUnsubscribe), - skipWhile(() => this.saving) - ).subscribe(changes => this.store.dispatch(new UpdateChanges(changes))); - this.statusEmitter - .changeEmitted$ - .pipe( - takeUntil(this.ngUnsubscribe), - skipWhile(() => this.saving), - combineLatest(this.currentPage$, (status: string, page: any) => { - return { [page.path]: status }; - }) - ).subscribe(status => this.store.dispatch(new UpdateStatus(status))); - } - - ngOnDestroy(): void { - this.ngUnsubscribe.next(); - this.ngUnsubscribe.complete(); - } - - canDeactivate( - currentRoute: ActivatedRouteSnapshot, - currentState: RouterStateSnapshot, - nextState: RouterStateSnapshot - ): Observable { - if (nextState.url.match('wizard')) { return of(true); } - if (Object.keys(this.changes).length > 0) { - let modal = this.modalService.open(UnsavedDialogComponent); - modal.componentInstance.action = new SaveChanges(this.latest); - modal.result.then( - () => this.router.navigate([nextState.url]), - () => console.warn('denied') - ); - } - return this.store.select(fromEditor.getEditorIsSaved); - } -} /* istanbul ignore next */ diff --git a/ui/src/app/metadata/resolver/effect/editor.effect.ts b/ui/src/app/metadata/resolver/effect/entity.effect.ts similarity index 87% rename from ui/src/app/metadata/resolver/effect/editor.effect.ts rename to ui/src/app/metadata/resolver/effect/entity.effect.ts index d194d05a7..1c487dec2 100644 --- a/ui/src/app/metadata/resolver/effect/editor.effect.ts +++ b/ui/src/app/metadata/resolver/effect/entity.effect.ts @@ -6,7 +6,11 @@ import { switchMap, map, withLatestFrom, tap } from 'rxjs/operators'; import * as fromResolver from '../reducer'; import * as fromRoot from '../../../app.reducer'; -import * as editor from '../action/editor.action'; +import { + ResolverEntityActionTypes, + Clear, + Cancel +} from '../action/entity.action'; import * as provider from '../action/collection.action'; import { ShowContentionAction } from '../../../contention/action/contention.action'; @@ -18,11 +22,11 @@ import { Store } from '@ngrx/store'; import { ContentionService } from '../../../contention/service/contention.service'; @Injectable() -export class EditorEffects { +export class EntityEffects { @Effect() cancelChanges$ = this.actions$.pipe( - ofType(editor.CANCEL_CHANGES), + ofType(ResolverEntityActionTypes.CANCEL), map(() => new provider.LoadResolverRequest()), tap(() => this.router.navigate(['metadata'])) ); @@ -31,7 +35,7 @@ export class EditorEffects { updateResolverSuccessRedirect$ = this.actions$.pipe( ofType(ResolverCollectionActionTypes.UPDATE_RESOLVER_SUCCESS), map(action => action.payload), - map(p => new editor.ResetChanges()) + map(p => new Clear()) ); @Effect() @@ -43,7 +47,7 @@ export class EditorEffects { return this.service.find(filter.id).pipe( map(data => new ShowContentionAction(this.contentionService.getContention(current, filter, data, { resolve: (obj) => this.store.dispatch(new provider.UpdateResolverRequest(obj)), - reject: (obj) => this.store.dispatch(new editor.CancelChanges()) + reject: (obj) => this.store.dispatch(new Cancel()) }))) ); }) diff --git a/ui/src/app/metadata/resolver/effect/wizard.effect.ts b/ui/src/app/metadata/resolver/effect/wizard.effect.ts index 3296d1f25..3916e349a 100644 --- a/ui/src/app/metadata/resolver/effect/wizard.effect.ts +++ b/ui/src/app/metadata/resolver/effect/wizard.effect.ts @@ -3,7 +3,11 @@ import { Effect, Actions, ofType } from '@ngrx/effects'; import { map, switchMap } from 'rxjs/operators'; -import * as editorActions from '../action/editor.action'; +import { + UpdateChanges, + Clear, + ResolverEntityActionTypes +} from '../action/entity.action'; import * as provider from '../action/collection.action'; import { ResolverCollectionActionTypes } from '../action/collection.action'; import { EntityDraftService } from '../../domain/service/draft.service'; @@ -13,7 +17,7 @@ export class WizardEffects { @Effect({ dispatch: false }) updateResolver$ = this.actions$.pipe( - ofType(editorActions.SAVE_CHANGES), + ofType(ResolverEntityActionTypes.UPDATE_CHANGES), map(action => action.payload), switchMap(provider => this.draftService.update(provider)) ); @@ -22,7 +26,7 @@ export class WizardEffects { addResolverSuccessDiscard$ = this.actions$.pipe( ofType(ResolverCollectionActionTypes.ADD_RESOLVER_SUCCESS), map(action => action.payload), - map(provider => new editorActions.ResetChanges()) + map(provider => new Clear()) ); constructor( diff --git a/ui/src/app/metadata/resolver/reducer/editor.reducer.ts b/ui/src/app/metadata/resolver/reducer/editor.reducer.ts deleted file mode 100644 index 290ce15fd..000000000 --- a/ui/src/app/metadata/resolver/reducer/editor.reducer.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { MetadataResolver } from '../../domain/model'; -import * as editor from '../action/editor.action'; -import { ResolverCollectionActionTypes, ResolverCollectionActionsUnion } from '../action/collection.action'; - -export interface EditorState { - saving: boolean; - formStatus: { [key: string]: string }; - changes: MetadataResolver; -} - -export const initialState: EditorState = { - saving: false, - formStatus: {}, - changes: {} as MetadataResolver -}; - -export function reducer(state = initialState, action: editor.Actions | ResolverCollectionActionsUnion): EditorState { - switch (action.type) { - case ResolverCollectionActionTypes.ADD_RESOLVER: { - return { - ...state, - saving: true, - }; - } - case ResolverCollectionActionTypes.ADD_RESOLVER_FAIL: { - return { - ...state, - saving: false - }; - } - case ResolverCollectionActionTypes.ADD_RESOLVER_SUCCESS: { - return { - ...state, - changes: { ...initialState.changes }, - saving: false - }; - } - case editor.UPDATE_STATUS: { - return Object.assign({}, state, { - formStatus: { ...state.formStatus, ...action.payload } - }); - } - case editor.UPDATE_CHANGES: { - return Object.assign({}, state, { - changes: { ...state.changes, ...action.payload } - }); - } - case editor.CANCEL_CHANGES: - case editor.SAVE_CHANGES: - case editor.RESET_CHANGES: - return Object.assign({}, state, { - changes: { ...initialState.changes } - }); - default: { - return state; - } - } -} - -export const isEditorValid = (state: EditorState) => - !Object.keys(state.formStatus).some(key => state.formStatus[key] === ('INVALID')); -export const isEditorSaved = (state: EditorState) => !Object.keys(state.changes).length; -export const getChanges = (state: EditorState) => state.changes; -export const isEditorSaving = (state: EditorState) => state.saving; -export const getFormStatus = (state: EditorState) => state.formStatus; -export const getInvalidForms = (state: EditorState) => - Object.keys(state.formStatus).filter(key => state.formStatus[key] === 'INVALID'); diff --git a/ui/src/app/metadata/resolver/reducer/editor.reducer.spec.ts b/ui/src/app/metadata/resolver/reducer/entity.reducer.spec.ts similarity index 52% rename from ui/src/app/metadata/resolver/reducer/editor.reducer.spec.ts rename to ui/src/app/metadata/resolver/reducer/entity.reducer.spec.ts index 78e272848..deaa49327 100644 --- a/ui/src/app/metadata/resolver/reducer/editor.reducer.spec.ts +++ b/ui/src/app/metadata/resolver/reducer/entity.reducer.spec.ts @@ -1,13 +1,16 @@ -import { reducer } from './editor.reducer'; -import * as fromEditor from './editor.reducer'; -import * as actions from '../action/editor.action'; -import * as collectionActions from '../action/collection.action'; +import { reducer } from './entity.reducer'; +import * as fromEntity from './entity.reducer'; +import { + UpdateChanges, + UpdateStatus, + Clear +} from '../action/entity.action'; import { MetadataResolver } from '../../domain/model'; describe('Editor Reducer', () => { - const initialState: fromEditor.EditorState = { + const initialState: fromEntity.EditorState = { saving: false, - formStatus: {}, + status: {}, changes: {} as MetadataResolver }; @@ -23,43 +26,10 @@ describe('Editor Reducer', () => { }); }); - describe('Editor Add Resolver', () => { - it('should update the status when a provider is saved', () => { - const action = new collectionActions.AddResolverRequest(changes); - const result = reducer(initialState, action); - expect(result).toEqual( - Object.assign({}, initialState, { - saving: true - }) - ); - }); - - it('should update the status on success', () => { - const action = new collectionActions.AddResolverSuccess(changes); - const result = reducer({...initialState, changes: {...changes, organization: {name: 'foo'}}}, action); - expect(result).toEqual( - Object.assign({}, initialState, { - saving: false, - changes: initialState.changes - }) - ); - }); - - it('should update the status on success', () => { - const action = new collectionActions.AddResolverFail(changes); - const result = reducer(initialState, action); - expect(result).toEqual( - Object.assign({}, initialState, { - saving: false - }) - ); - }); - }); - - describe('Editor Update Status', () => { + describe('Entity Update Status', () => { it('should update the status of the provided form', () => { const status = { organization: 'VALID' }; - const action = new actions.UpdateStatus(status); + const action = new UpdateStatus(status); const result = reducer(initialState, action); expect(result).toEqual( Object.assign({}, initialState, { @@ -71,7 +41,7 @@ describe('Editor Reducer', () => { describe('Editor Update Changes', () => { it('should add changes of the provided form', () => { - const action = new actions.UpdateChanges(changes); + const action = new UpdateChanges(changes); const result = reducer(initialState, action); expect(result).toEqual( Object.assign({}, initialState, { @@ -81,9 +51,9 @@ describe('Editor Reducer', () => { }); }); - describe('Editor Reset', () => { + describe('Editor Clear', () => { it('should remove changes', () => { - const action = new actions.ResetChanges(); + const action = new Clear(); const result = reducer(initialState, action); expect(result).toEqual( Object.assign({}, initialState, { @@ -95,19 +65,7 @@ describe('Editor Reducer', () => { describe('Editor Save', () => { it('should remove changes', () => { - const action = new actions.SaveChanges(changes); - const result = reducer(initialState, action); - expect(result).toEqual( - Object.assign({}, initialState, { - changes: initialState.changes - }) - ); - }); - }); - - describe('Editor Cancel', () => { - it('should remove changes', () => { - const action = new actions.CancelChanges(); + const action = new UpdateChanges(changes); const result = reducer(initialState, action); expect(result).toEqual( Object.assign({}, initialState, { @@ -119,10 +77,10 @@ describe('Editor Reducer', () => { describe('Selectors', () => { it('should aggregate the status', () => { - expect(fromEditor.isEditorValid({ + expect(fromEntity.isEditorValid({ saving: false, changes: {} as MetadataResolver, - formStatus: { + status: { organization: 'INVALID', foo: 'VALID' } @@ -130,38 +88,38 @@ describe('Editor Reducer', () => { }); it('should calculate a saved status based on changes', () => { - expect(fromEditor.isEditorSaved({ + expect(fromEntity.isEditorSaving({ saving: false, changes: {} as MetadataResolver, - formStatus: {} + status: {} })).toBe(true); - expect(fromEditor.isEditorSaved({ + expect(fromEntity.isEditorSaving({ saving: false, changes: {organization: {}, entityId: 'bar'} as MetadataResolver, - formStatus: {} + status: {} })).toBe(false); }); it('should return current changes', () => { - expect(fromEditor.getChanges({ + expect(fromEntity.getChanges({ saving: false, changes: {} as MetadataResolver, - formStatus: {} + status: {} })).toEqual({} as MetadataResolver); }); it('should return `saving` status', () => { - expect(fromEditor.isEditorSaving({ + expect(fromEntity.isEditorSaving({ saving: false, changes: {} as MetadataResolver, - formStatus: {} + status: {} })).toBe(false); - expect(fromEditor.isEditorSaving({ + expect(fromEntity.isEditorSaving({ saving: true, changes: {} as MetadataResolver, - formStatus: {} + status: {} })).toBe(true); }); }); diff --git a/ui/src/app/metadata/resolver/reducer/entity.reducer.ts b/ui/src/app/metadata/resolver/reducer/entity.reducer.ts new file mode 100644 index 000000000..819eacd11 --- /dev/null +++ b/ui/src/app/metadata/resolver/reducer/entity.reducer.ts @@ -0,0 +1,56 @@ +import { MetadataResolver } from '../../domain/model'; +import * as entity from '../action/entity.action'; +import { ResolverEntityActionTypes, ResolverEntityActionUnion } from '../action/entity.action'; + +export interface EntityState { + saving: boolean; + status: { [key: string]: string }; + changes: MetadataResolver; +} + +export const initialState: EntityState = { + saving: false, + status: {}, + changes: {} as MetadataResolver +}; + +export function reducer(state = initialState, action: ResolverEntityActionUnion): EntityState { + switch (action.type) { + case ResolverEntityActionTypes.UPDATE_CHANGES: { + return { + ...state, + changes: { ...state.changes, ...action.payload } + }; + } + case ResolverEntityActionTypes.UPDATE_STATUS: { + return { + ...state, + status: { ...state.status, ...action.payload } + }; + } + case ResolverEntityActionTypes.UPDATE_SAVING: { + return { + ...state, + saving: action.payload + }; + } + case ResolverEntityActionTypes.CLEAR: + return { + ...state, + changes: { ...initialState.changes } + }; + default: { + return state; + } + } +} + +export const isEntitySaved = (state: EntityState) => !Object.keys(state.changes).length; +export const getChanges = (state: EntityState) => state.changes; +export const isEntitySaving = (state: EntityState) => state.saving; +export const getFormStatus = (state: EntityState) => state.status; + +export const isEntityValid = (state: EntityState) => + !Object.keys(state.status).some(key => state.status[key] === ('INVALID')); +export const getInvalidForms = (state: EntityState) => + Object.keys(state.status).filter(key => state.status[key] === 'INVALID'); diff --git a/ui/src/app/metadata/resolver/reducer/index.ts b/ui/src/app/metadata/resolver/reducer/index.ts index ca47726ba..bf38715d9 100644 --- a/ui/src/app/metadata/resolver/reducer/index.ts +++ b/ui/src/app/metadata/resolver/reducer/index.ts @@ -1,6 +1,6 @@ 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 fromSearch from './search.reducer'; import * as fromCopy from './copy.reducer'; import * as fromDraft from './draft.reducer'; @@ -9,7 +9,7 @@ import * as fromCollection from './collection.reducer'; import { combineAllFn, getEntityIdsFn, getInCollectionFn, doesExistFn } from '../../domain/domain.util'; export interface ResolverState { - editor: fromEditor.EditorState; + entity: fromEntity.EntityState; copy: fromCopy.CopyState; search: fromSearch.SearchState; draft: fromDraft.DraftState; @@ -18,10 +18,10 @@ export interface ResolverState { export const reducers = { copy: fromCopy.reducer, - search: fromSearch.reducer, - editor: fromEditor.reducer, + entity: fromEntity.reducer, collection: fromCollection.reducer, - draft: fromDraft.reducer + draft: fromDraft.reducer, + search: fromSearch.reducer }; export interface State extends fromRoot.State { @@ -32,26 +32,26 @@ export const getResolverState = createFeatureSelector('resolver') export const getCollectionStateFn = (state: ResolverState) => state.collection; export const getDraftStateFn = (state: ResolverState) => state.draft; -export const getEditorStateFn = (state: ResolverState) => state.editor; +export const getEntityStateFn = (state: ResolverState) => state.entity; export const getCopyStateFn = (state: ResolverState) => state.copy; export const getSearchStateFn = (state: ResolverState) => state.search; export const getCollectionState = createSelector(getResolverState, getCollectionStateFn); export const getDraftState = createSelector(getResolverState, getDraftStateFn); -export const getEditorState = createSelector(getResolverState, getEditorStateFn); +export const getEntityState = createSelector(getResolverState, getEntityStateFn); export const getCopyState = createSelector(getResolverState, getCopyStateFn); export const getSearchState = createSelector(getResolverState, getSearchStateFn); /* -Editor State +Entity State */ -export const getEditorIsValid = createSelector(getEditorState, fromEditor.isEditorValid); -export const getEditorIsSaved = createSelector(getEditorState, fromEditor.isEditorSaved); -export const getEditorChanges = createSelector(getEditorState, fromEditor.getChanges); -export const getEditorIsSaving = createSelector(getEditorState, fromEditor.isEditorSaving); -export const getFormStatus = createSelector(getEditorState, fromEditor.getFormStatus); -export const getInvalidEditorForms = createSelector(getEditorState, fromEditor.getInvalidForms); +export const getEntityIsValid = createSelector(getEntityState, fromEntity.isEntityValid); +export const getEntityIsSaved = createSelector(getEntityState, fromEntity.isEntitySaved); +export const getEntityChanges = createSelector(getEntityState, fromEntity.getChanges); +export const getEntityIsSaving = createSelector(getEntityState, fromEntity.isEntitySaving); +export const getFormStatus = createSelector(getEntityState, fromEntity.getFormStatus); +export const getInvalidEntityForms = createSelector(getEntityState, fromEntity.getInvalidForms); /* Copy State diff --git a/ui/src/app/metadata/resolver/resolver.module.ts b/ui/src/app/metadata/resolver/resolver.module.ts index f086c6f44..356fae5b1 100644 --- a/ui/src/app/metadata/resolver/resolver.module.ts +++ b/ui/src/app/metadata/resolver/resolver.module.ts @@ -23,14 +23,16 @@ import { CopyResolverEffects } from './effect/copy.effect'; import { DomainModule } from '../domain/domain.module'; import { DraftComponent } from './container/draft.component'; import { EditorComponent } from './container/editor.component'; -import { WizardComponent } from './container/wizard.component'; +import { ResolverWizardComponent } from './container/resolver-wizard.component'; import { WizardNavComponent } from './component/wizard-nav.component'; import { ResolverCollectionEffects } from './effect/collection.effects'; import { DraftCollectionEffects } from './effect/draft-collection.effects'; import { WizardEffects } from './effect/wizard.effect'; -import { EditorEffects } from './effect/editor.effect'; import { UnsavedDialogComponent } from './component/unsaved-dialog.component'; import { I18nModule } from '../../i18n/i18n.module'; +import { MetadataSourceWizard } from '../domain/model/wizards/metadata-source-wizard'; +import { METADATA_SOURCE_WIZARD } from './wizard-definition'; +import { EntityEffects } from './effect/entity.effect'; @NgModule({ declarations: [ @@ -42,7 +44,7 @@ import { I18nModule } from '../../i18n/i18n.module'; ResolverComponent, DraftComponent, EditorComponent, - WizardComponent, + ResolverWizardComponent, WizardNavComponent, UnsavedDialogComponent ], @@ -88,8 +90,11 @@ export class ResolverModule { ResolverCollectionEffects, DraftCollectionEffects, WizardEffects, - EditorEffects + EntityEffects ]) ], + providers: [ + { provide: METADATA_SOURCE_WIZARD, useClass: MetadataSourceWizard } + ] }) export class RootResolverModule { } diff --git a/ui/src/app/metadata/resolver/resolver.routing.ts b/ui/src/app/metadata/resolver/resolver.routing.ts index 9aa4ab730..3e8cc3b47 100644 --- a/ui/src/app/metadata/resolver/resolver.routing.ts +++ b/ui/src/app/metadata/resolver/resolver.routing.ts @@ -3,7 +3,7 @@ import { Routes } from '@angular/router'; import { ResolverComponent } from './container/resolver.component'; import { EditorComponent } from './container/editor.component'; import { DraftComponent } from './container/draft.component'; -import { WizardComponent } from './container/wizard.component'; +import { ResolverWizardComponent } from './container/resolver-wizard.component'; import { BlankResolverComponent } from './container/blank-resolver.component'; import { NewResolverComponent } from './container/new-resolver.component'; @@ -66,7 +66,7 @@ export const ResolverRoutes: Routes = [ { path: 'wizard', redirectTo: 'wizard/2' }, { path: 'wizard/:index', - component: WizardComponent, + component: ResolverWizardComponent, canDeactivate: [CanDeactivateGuard] } ] diff --git a/ui/src/app/metadata/resolver/wizard-definition.ts b/ui/src/app/metadata/resolver/wizard-definition.ts new file mode 100644 index 000000000..74e15df28 --- /dev/null +++ b/ui/src/app/metadata/resolver/wizard-definition.ts @@ -0,0 +1,5 @@ +import { InjectionToken } from '@angular/core'; +import { Wizard } from '../../wizard/model'; +import { MetadataResolver } from '../domain/model'; + +export const METADATA_SOURCE_WIZARD = new InjectionToken>('METADATA_SOURCE_WIZARD'); diff --git a/ui/src/app/wizard/action/wizard.action.ts b/ui/src/app/wizard/action/wizard.action.ts index ecb507cb4..83a5f08a5 100644 --- a/ui/src/app/wizard/action/wizard.action.ts +++ b/ui/src/app/wizard/action/wizard.action.ts @@ -12,7 +12,14 @@ export enum WizardActionTypes { NEXT = '[Wizard] Next Page', PREVIOUS = '[Wizard] Previous Page', - CLEAR = '[Wizard] Clear' + CLEAR = '[Wizard] Clear', + + LOAD_SCHEMA_REQUEST = '[Wizard] Load Schema Request', + LOAD_SCHEMA_SUCCESS = '[Wizard] Load Schema Success', + LOAD_SCHEMA_FAIL = '[Wizard] Load Schema Fail', + + LOCK = '[Wizard] Lock', + UNLOCK = '[Wizard] Unlock' } export class SetIndex implements Action { @@ -61,6 +68,32 @@ export class ClearWizard implements Action { readonly type = WizardActionTypes.CLEAR; } +export class LoadSchemaRequest implements Action { + readonly type = WizardActionTypes.LOAD_SCHEMA_REQUEST; + + constructor(public payload: string) { } +} + +export class LoadSchemaSuccess implements Action { + readonly type = WizardActionTypes.LOAD_SCHEMA_SUCCESS; + + constructor(public payload: any) { } +} + +export class LoadSchemaFail implements Action { + readonly type = WizardActionTypes.LOAD_SCHEMA_FAIL; + + constructor(public payload: Error) { } +} + +export class LockEditor implements Action { + readonly type = WizardActionTypes.LOCK; +} + +export class UnlockEditor implements Action { + readonly type = WizardActionTypes.UNLOCK; +} + export type WizardActionUnion = | SetIndex | SetDefinition @@ -69,4 +102,9 @@ export type WizardActionUnion = | Next | Previous | ClearWizard - | AddSchema; + | AddSchema + | LoadSchemaRequest + | LoadSchemaSuccess + | LoadSchemaFail + | LockEditor + | UnlockEditor; diff --git a/ui/src/app/wizard/model/form-definition.ts b/ui/src/app/wizard/model/form-definition.ts index 9cd42184b..f14fd2f23 100644 --- a/ui/src/app/wizard/model/form-definition.ts +++ b/ui/src/app/wizard/model/form-definition.ts @@ -2,9 +2,7 @@ export interface FormDefinition { label: string; type: string; schema?: string; - translate: { - parser(changes: Partial, schema?: any), - formatter(changes: Partial, schema?: any) - }; + parser(changes: Partial, schema?: any); + formatter(changes: Partial, schema?: any); getValidators?(...args: any[]): { [key: string]: any }; } diff --git a/ui/src/app/wizard/model/wizard.ts b/ui/src/app/wizard/model/wizard.ts index 71389c7c0..b07e98da4 100644 --- a/ui/src/app/wizard/model/wizard.ts +++ b/ui/src/app/wizard/model/wizard.ts @@ -11,6 +11,13 @@ export interface WizardStep { schema?: string; index: number; locked?: boolean; + fields?: (string | WizardFieldset)[]; + summary?: boolean; +} + +export interface WizardFieldset { + type: string; + fields: string[]; } export interface WizardValue { diff --git a/ui/src/app/wizard/reducer/index.ts b/ui/src/app/wizard/reducer/index.ts index a1a1e1c9b..e2c15147b 100644 --- a/ui/src/app/wizard/reducer/index.ts +++ b/ui/src/app/wizard/reducer/index.ts @@ -15,6 +15,28 @@ export interface State extends fromRoot.State { 'wizard': WizardState; } +export function getSchemaParseFn(schema, locked): any { + if (!schema) { + return null; + } + return { + ...schema, + properties: Object.keys(schema.properties).reduce((prev, current) => { + return { + ...prev, + [current]: { + ...schema.properties[current], + readOnly: locked, + ...(schema.properties[current].hasOwnProperty('properties') ? + getSchemaParseFn(schema.properties[current], locked) : + {} + ) + } + }; + }, {}) + }; +} + export const getWizardState = createFeatureSelector('wizard'); export const getWizardStateFn = (state: WizardState) => state.wizard; export const getState = createSelector(getWizardState, getWizardStateFn); @@ -67,3 +89,10 @@ export const getLast = createSelector(getWizardIndex, getWizardDefinition, getLa export const getModel = createSelector(getCurrent, getModelFn); export const getRoutes = createSelector(getWizardDefinition, d => d ? d.steps.map(step => ({ path: step.id, label: step.label })) : [] ); + +export const getLockedStatus = createSelector(getState, fromWizard.getLocked); +export const getSchemaLockedFn = (step, locked) => step ? step.locked ? locked : false : false; +export const getLocked = createSelector(getCurrent, getLockedStatus, getSchemaLockedFn); + +export const getSchemaObject = createSelector(getState, fromWizard.getSchema); +export const getParsedSchema = createSelector(getSchemaObject, getLocked, getSchemaParseFn); diff --git a/ui/src/app/wizard/reducer/wizard.reducer.ts b/ui/src/app/wizard/reducer/wizard.reducer.ts index 9ecae96ce..5b9e390b5 100644 --- a/ui/src/app/wizard/reducer/wizard.reducer.ts +++ b/ui/src/app/wizard/reducer/wizard.reducer.ts @@ -6,17 +6,63 @@ export interface State { disabled: boolean; definition: Wizard; schemaCollection: { [id: string]: any }; + + schemaPath: string; + loading: boolean; + schema: any; + locked: boolean; } export const initialState: State = { index: null, disabled: false, definition: null, - schemaCollection: {} + schemaCollection: {}, + + schemaPath: null, + loading: false, + schema: null, + locked: false }; export function reducer(state = initialState, action: WizardActionUnion): State { switch (action.type) { + case WizardActionTypes.LOAD_SCHEMA_REQUEST: { + return { + ...state, + loading: true, + schemaPath: action.payload + }; + } + case WizardActionTypes.LOAD_SCHEMA_SUCCESS: { + return { + ...state, + loading: false, + schema: action.payload + }; + } + case WizardActionTypes.LOAD_SCHEMA_FAIL: { + return { + ...state, + loading: false, + schema: initialState.schema + }; + } + + case WizardActionTypes.LOCK: { + return { + ...state, + locked: true + }; + } + + case WizardActionTypes.UNLOCK: { + return { + ...state, + locked: false + }; + } + case WizardActionTypes.ADD_SCHEMA: { return { ...state, @@ -64,6 +110,9 @@ export function reducer(state = initialState, action: WizardActionUnion): State } } +export const getSchema = (state: State) => state.schema; +export const getLocked = (state: State) => state.locked; + export const getIndex = (state: State) => state.index; export const getDisabled = (state: State) => state.disabled; export const getDefinition = (state: State) => state.definition; diff --git a/ui/src/assets/schema/source/metadata-source.json b/ui/src/assets/schema/source/metadata-source.json new file mode 100644 index 000000000..5766e298f --- /dev/null +++ b/ui/src/assets/schema/source/metadata-source.json @@ -0,0 +1,442 @@ +{ + "type": "object", + "properties": { + "entityId": { + "title": "label.entity-id", + "description": "tooltip.entity-id", + "type": "string" + }, + "serviceProviderName": { + "title": "label.service-provider-name", + "description": "tooltip.service-provider-name", + "type": "string" + }, + "serviceEnabled": { + "title": "label.enable-this-service-opon-saving", + "description": "tooltip.enable-this-service-upon-saving", + "type": "boolean" + }, + "organization": { + "type": "object", + "properties": { + "name": { + "title": "label.organization-name", + "description": "tooltip.organization-name", + "type": "string" + }, + "displayName": { + "title": "label.organization-display-name", + "description": "tooltip.organization-display-name", + "type": "string" + }, + "url": { + "title": "label.organization-display-name", + "description": "tooltip.organization-display-name", + "type": "string" + } + }, + "dependencies": { + "name": [ + "displayName", + "url" + ], + "displayName": [ + "name", + "url" + ], + "url": [ + "name", + "displayName" + ] + } + }, + "contacts": { + "title": "label.contact-information", + "description": "tooltip.contact-information", + "type": "array", + "items": { + "$ref": "#/definitions/Contact" + } + }, + "mdui": { + "type": "object", + "properties": { + "displayName": { + "title": "label.display-name", + "description": "tooltip.mdui-display-name", + "type": "string" + }, + "informationUrl": { + "title": "label.information-url", + "description": "tooltip.mdui-information-url", + "type": "string" + }, + "privacyStatementUrl": { + "title": "label.privacy-statement-url", + "description": "tooltip.mdui-privacy-statement-url", + "type": "string" + }, + "description": { + "title": "label.description", + "description": "tooltip.mdui-description", + "type": "string" + }, + "logoUrl": { + "title": "label.logo-url", + "description": "tooltip.mdui-logo-url", + "type": "string" + }, + "logoHeight": { + "title": "label.logo-height", + "description": "tooltip.mdui-logo-height", + "min": 0, + "type": "integer" + }, + "logoWidth": { + "title": "label.logo-width", + "description": "tooltip.mdui-logo-width", + "min": 0, + "type": "integer" + } + } + }, + "securityInfo": { + "type": "object", + "properties": { + "x509CertificateAvailable": { + "title": "label.is-there-a-x509-certificate", + "description": "tooltip.is-there-a-x509-certificate", + "type": "boolean", + "default": false + }, + "authenticationRequestsSigned": { + "title": "label.authentication-requests-signed", + "description": "tooltip.authentication-requests-signed", + "type": "boolean", + "default": false + }, + "wantAssertionsSigned": { + "title": "label.want-assertions-signed", + "description": "tooltip.want-assertions-signed", + "type": "boolean", + "default": false + }, + "x509Certificates": { + "title": "label.x509-certificates", + "type": "array", + "items": { + "$ref": "#/definitions/Certificate" + } + } + } + }, + "assertionConsumerServices": { + "title": "label.assertion-consumer-service-endpoints", + "description": "", + "type": "array", + "items": { + "$ref": "#/definitions/AssertionConsumerService" + } + }, + "serviceProviderSsoDescriptor": { + "type": "object", + "properties": { + "protocolSupportEnum": { + "title": "label.protocol-support-enumeration", + "description": "tooltip.protocol-support-enumeration", + "type": "string", + "placeholder": "label.select-protocol", + "oneOf": [ + { + "enum": [ + "SAML 2" + ], + "description": "SAML 2" + }, + { + "enum": [ + "SAML 1.1" + ], + "description": "SAML 1.1" + } + ] + } + }, + "nameIdFormats": { + "$ref": "#/definitions/NameIdFormatList" + } + }, + "logoutEndpoints": { + "title": "label.logout-endpoints", + "description": "tooltip.logout-endpoints", + "type": "array", + "items": { + "$ref": "#/definitions/LogoutEndpoint" + } + }, + "relyingPartyOverrides": { + "type": "object", + "properties": { + "signAssertion": { + "title": "label.sign-the-assertion", + "description": "tooltip.sign-assertion", + "type": "boolean", + "default": false + }, + "dontSignResponse": { + "title": "label.dont-sign-the-response", + "description": "tooltip.dont-sign-response", + "type": "boolean", + "default": false + }, + "turnOffEncryption": { + "title": "label.turn-off-encryption-of-response", + "description": "tooltip.turn-off-encryption", + "type": "boolean", + "default": false + }, + "useSha": { + "title": "label.use-sha1-signing-algorithm", + "description": "tooltip.usa-sha-algorithm", + "type": "boolean", + "default": false + }, + "ignoreAuthenticationMethod": { + "title": "label.ignore-any-sp-requested-authentication-method", + "description": "tooltip.ignore-auth-method", + "type": "boolean", + "default": false + }, + "forceAuthn": { + "title": "label.force-authn", + "description": "tooltip.force-authn", + "type": "boolean", + "default": false + }, + "omitNotBefore": { + "title": "label.omit-not-before-condition", + "type": "boolean", + "description": "tooltip.omit-not-before-condition", + "default": false + }, + "responderId": { + "title": "label.responder-id", + "description": "tooltip.responder-id", + "type": "string" + }, + "nameIdFormats": { + "$ref": "#/definitions/NameIdFormatList" + }, + "authenticationMethods": { + "$ref": "#/definitions/AuthenticationMethodList" + } + } + }, + "attributeRelease": { + "type": "array", + "description": "Attribute release table - select the attributes you want to release (default unchecked)", + "widget": { + "id": "checklist", + "dataUrl": "/customAttributes" + }, + "items": { + "type": "string" + } + } + }, + "definitions": { + "Contact": { + "type": "object", + "properties": { + "name": { + "title": "label.contact-name", + "description": "tooltip.contact-name", + "type": "string" + }, + "type": { + "title": "label.contact-type", + "description": "tooltip.contact-type", + "type": "string", + "oneOf": [ + { + "enum": [ + "support" + ], + "description": "value.support" + }, + { + "enum": [ + "technical" + ], + "description": "value.technical" + }, + { + "enum": [ + "administrative" + ], + "description": "value.administrative" + }, + { + "enum": [ + "other" + ], + "description": "value.other" + } + ] + }, + "emailAddress": { + "title": "label.contact-email-address", + "description": "tooltip.contact-email", + "type": "string", + "pattern": "^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$" + } + } + }, + "Certificate": { + "name": { + "title": "label.certificate-name-display-only", + "description": "tooltip.certificate-name", + "type": "string" + }, + "type": { + "title": "label.type", + "description": "tooltip.certificate-type", + "type": "string", + "oneOf": [ + { + "enum": [ + "signing" + ], + "description": "value.signing" + }, + { + "enum": [ + "encryption" + ], + "description": "value.encryption" + }, + { + "enum": [ + "both" + ], + "description": "value.both" + } + ], + "default": "both" + }, + "value": { + "title": "label.certificate", + "description": "tooltip.certificate", + "type": "string" + } + }, + "AssertionConsumerService": { + "type": "object", + "properties": { + "locationUrl": { + "title": "label.assertion-consumer-services-location", + "description": "tooltip.assertion-consumer-service-location", + "type": "string", + "widget": { + "id": "string", + "help": "message.valid-url" + } + }, + "binding": { + "title": "label.assertion-consumer-service-location-binding", + "description": "tooltip.assertion-consumer-service-location-binding", + "type": "string", + "oneOf": [ + { + "enum": [ + "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + ], + "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + }, + { + "enum": [ + "urn:oasis:names:tc:SAML:1.0:profiles:browser-post" + ], + "description": "urn:oasis:names:tc:SAML:1.0:profiles:browser-post" + } + ] + }, + "makeDefault": { + "title": "label.mark-as-default", + "description": "tooltip.mark-as-default", + "type": "boolean" + } + } + }, + "NameIdFormatList": { + "title": "label.nameid-format-to-send", + "placeholder": "label.nameid-format", + "description": "tooltip.nameid-format", + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "widget": "datalist", + "data": [ + "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", + "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", + "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", + "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" + ] + }, + "default": null + }, + "AuthenticationMethodList": { + "title": "label.authentication-methods-to-use", + "description": "tooltip.authentication-methods-to-use", + "type": "array", + "placeholder": "label.authentication-method", + "uniqueItems": true, + "items": { + "type": "string", + "title": "label.authentication-method", + "widget": { + "id": "datalist", + "data": [ + "https://refeds.org/profile/mfa", + "urn:oasis:names:tc:SAML:2.0:ac:classes:TimeSyncToken", + "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport" + ] + } + }, + "default": null + }, + "LogoutEndpoint": { + "title": "label.new-endpoint", + "description": "tooltip.new-endpoint", + "type": "object", + "properties": { + "url": { + "title": "label.url", + "description": "tooltip.url", + "type": "string" + }, + "bindingType": { + "title": "label.binding-type", + "description": "tooltip.binding-type", + "type": "string", + "oneOf": [ + { + "enum": [ + "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + ], + "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + }, + { + "enum": [ + "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + ], + "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + } + ] + + } + } + } + } +} \ No newline at end of file From 6ab5c0d85a2b81cbf99fc187a658ce22a1834c49 Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Thu, 4 Oct 2018 14:19:12 -0700 Subject: [PATCH 04/33] SHIBUI-914 Implemented dynamic wizard for sources --- .../model/wizards/metadata-source-wizard.ts | 10 +++ .../container/provider-wizard.component.ts | 3 + .../app/metadata/provider/provider.module.ts | 2 - .../resolver-wizard-step.component.html | 8 ++ .../resolver-wizard-step.component.ts | 89 +++++++++++++++++++ .../container/resolver-wizard.component.html | 46 ++-------- .../container/resolver-wizard.component.ts | 16 +++- .../app/metadata/resolver/resolver.module.ts | 12 ++- .../app/metadata/resolver/resolver.routing.ts | 11 ++- ui/src/app/wizard/reducer/index.spec.ts | 4 +- ui/src/app/wizard/reducer/index.ts | 20 ++++- 11 files changed, 168 insertions(+), 53 deletions(-) create mode 100644 ui/src/app/metadata/resolver/container/resolver-wizard-step.component.html create mode 100644 ui/src/app/metadata/resolver/container/resolver-wizard-step.component.ts diff --git a/ui/src/app/metadata/domain/model/wizards/metadata-source-wizard.ts b/ui/src/app/metadata/domain/model/wizards/metadata-source-wizard.ts index 90fcb9d0c..7624065f2 100644 --- a/ui/src/app/metadata/domain/model/wizards/metadata-source-wizard.ts +++ b/ui/src/app/metadata/domain/model/wizards/metadata-source-wizard.ts @@ -5,6 +5,16 @@ export class MetadataSourceWizard implements Wizard { label = 'Metadata Source'; type = '@MetadataProvider'; steps: WizardStep[] = [ + { + index: 1, + id: 'common', + label: 'label.resolver-common-attributes', + schema: 'assets/schema/source/metadata-source.json', + fields: [ + 'serviceProviderName', + 'entityId' + ] + }, { index: 2, id: 'org-info', 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 24140a0a2..73d092b3e 100644 --- a/ui/src/app/metadata/provider/container/provider-wizard.component.ts +++ b/ui/src/app/metadata/provider/container/provider-wizard.component.ts @@ -38,6 +38,9 @@ export class ProviderWizardComponent implements OnDestroy { constructor( private store: Store ) { + + + this.store .select(fromWizard.getCurrentWizardSchema) .subscribe(s => { diff --git a/ui/src/app/metadata/provider/provider.module.ts b/ui/src/app/metadata/provider/provider.module.ts index 4ce52c7db..550b5c198 100644 --- a/ui/src/app/metadata/provider/provider.module.ts +++ b/ui/src/app/metadata/provider/provider.module.ts @@ -30,8 +30,6 @@ import { UnsavedProviderComponent } from './component/unsaved-provider.dialog'; import { ContentionModule } from '../../contention/contention.module'; import { DeleteFilterComponent } from './component/delete-filter.component'; import { I18nModule } from '../../i18n/i18n.module'; -import { WidgetRegistry } from 'ngx-schema-form'; -import { CustomWidgetRegistry } from '../../schema-form/registry'; @NgModule({ declarations: [ diff --git a/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.html b/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.html new file mode 100644 index 000000000..5d07730fd --- /dev/null +++ b/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.html @@ -0,0 +1,8 @@ + + + \ No newline at end of file diff --git a/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.ts b/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.ts new file mode 100644 index 000000000..5cd58b332 --- /dev/null +++ b/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.ts @@ -0,0 +1,89 @@ +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 fromResolver from '../reducer'; +import * as fromWizard from '../../../wizard/reducer'; + +import { SetDefinition } from '../../../wizard/action/wizard.action'; +import { UpdateStatus, UpdateChanges } from '../action/entity.action'; +import { Wizard } from '../../../wizard/model'; +import { MetadataResolver } from '../../domain/model'; +import { pick } from '../../../shared/util'; + +@Component({ + selector: 'resolver-wizard-step', + templateUrl: './resolver-wizard-step.component.html', + styleUrls: [] +}) + +export class ResolverWizardStepComponent 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$: Observable<{ [key: string]: any }>; + + constructor( + private store: Store, + ) { + this.schema$ = this.store.select(fromWizard.getSchema); + this.definition$ = this.store.select(fromWizard.getWizardDefinition); + this.changes$ = this.store.select(fromResolver.getEntityChanges); + + this.validators$ = this.definition$.pipe( + map((def) => def.getValidators()) + ); + + 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.formatter(model)) + ); + + this.valueChangeEmitted$.pipe( + withLatestFrom(this.definition$), + skipWhile(([ changes, definition ]) => !definition || !changes), + map(([ changes, definition ]) => definition.parser(changes)) + ) + .subscribe(changes => this.store.dispatch(new UpdateChanges(changes))); + + this.statusChangeEmitted$.pipe(distinctUntilChanged()).subscribe(errors => this.updateStatus(errors)); + + this.store.select(fromWizard.getWizardIndex).subscribe(i => this.currentPage = i); + } + + 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/resolver/container/resolver-wizard.component.html b/ui/src/app/metadata/resolver/container/resolver-wizard.component.html index b8a398c95..1e043a147 100644 --- a/ui/src/app/metadata/resolver/container/resolver-wizard.component.html +++ b/ui/src/app/metadata/resolver/container/resolver-wizard.component.html @@ -1,42 +1,10 @@ -
-
-
-
-
- - - Add a new metadata source - – {{ (currentPage$ | async).label | translate }} ({{ (resolver$ | async).serviceProviderName }}) - - -
-
-
-
- + +
+
+ +
diff --git a/ui/src/app/metadata/resolver/container/resolver-wizard.component.ts b/ui/src/app/metadata/resolver/container/resolver-wizard.component.ts index 92b5738f6..3b82f735d 100644 --- a/ui/src/app/metadata/resolver/container/resolver-wizard.component.ts +++ b/ui/src/app/metadata/resolver/container/resolver-wizard.component.ts @@ -74,9 +74,16 @@ export class ResolverWizardComponent implements OnDestroy, CanComponentDeactivat }); this.store.dispatch(new SetDefinition(this.sourceWizard)); - this.store.dispatch(new SetIndex(this.sourceWizard.steps[0].id)); - this.store.select(fromWizard.getParsedSchema).subscribe(s => console.log(s)); + this.route.params.subscribe(params => { + if (params.index) { + this.store.dispatch(new SetIndex(params.index)); + } else { + this.store.dispatch(new SetIndex(this.sourceWizard.steps[0].id)); + } + }); + + console.log('hi'); } save(): void { @@ -105,6 +112,8 @@ export class ResolverWizardComponent implements OnDestroy, CanComponentDeactivat currentState: RouterStateSnapshot, nextState: RouterStateSnapshot ): Observable { + return of(true); + /* if (nextState.url.match('wizard')) { return of(true); } if (Object.keys(this.changes).length > 0) { let modal = this.modalService.open(UnsavedDialogComponent); @@ -115,5 +124,6 @@ export class ResolverWizardComponent implements OnDestroy, CanComponentDeactivat ); } return this.store.select(fromResolver.getEntityIsSaved); + */ } -} /* istanbul ignore next */ +} diff --git a/ui/src/app/metadata/resolver/resolver.module.ts b/ui/src/app/metadata/resolver/resolver.module.ts index 356fae5b1..468c02e46 100644 --- a/ui/src/app/metadata/resolver/resolver.module.ts +++ b/ui/src/app/metadata/resolver/resolver.module.ts @@ -33,6 +33,9 @@ import { I18nModule } from '../../i18n/i18n.module'; import { MetadataSourceWizard } from '../domain/model/wizards/metadata-source-wizard'; import { METADATA_SOURCE_WIZARD } from './wizard-definition'; import { EntityEffects } from './effect/entity.effect'; +import { ResolverWizardStepComponent } from './container/resolver-wizard-step.component'; +import { WizardModule } from '../../wizard/wizard.module'; +import { FormModule } from '../../schema-form/schema-form.module'; @NgModule({ declarations: [ @@ -44,9 +47,10 @@ import { EntityEffects } from './effect/entity.effect'; ResolverComponent, DraftComponent, EditorComponent, - ResolverWizardComponent, WizardNavComponent, - UnsavedDialogComponent + UnsavedDialogComponent, + ResolverWizardComponent, + ResolverWizardStepComponent ], entryComponents: [ UnsavedDialogComponent @@ -61,7 +65,9 @@ import { EntityEffects } from './effect/entity.effect'; FormsModule, ProviderEditorFormModule, NgbDropdownModule, - I18nModule + I18nModule, + WizardModule, + FormModule ], exports: [ ProviderEditorFormModule, diff --git a/ui/src/app/metadata/resolver/resolver.routing.ts b/ui/src/app/metadata/resolver/resolver.routing.ts index 3e8cc3b47..ca1911706 100644 --- a/ui/src/app/metadata/resolver/resolver.routing.ts +++ b/ui/src/app/metadata/resolver/resolver.routing.ts @@ -13,6 +13,7 @@ import { ConfirmCopyComponent } from './container/confirm-copy.component'; import { CopyIsSetGuard } from './guard/copy-isset.guard'; import { CanDeactivateGuard } from '../../core/service/can-deactivate.guard'; +import { ResolverWizardStepComponent } from './container/resolver-wizard-step.component'; export const ResolverRoutes: Routes = [ { @@ -25,8 +26,14 @@ export const ResolverRoutes: Routes = [ { path: '', redirectTo: 'blank', pathMatch: 'prefix' }, { path: 'blank', - component: BlankResolverComponent, - canDeactivate: [] + component: ResolverWizardComponent, + canDeactivate: [], + children: [ + { + path: '', + component: ResolverWizardStepComponent + } + ] }, { path: 'upload', diff --git a/ui/src/app/wizard/reducer/index.spec.ts b/ui/src/app/wizard/reducer/index.spec.ts index 4bf95f7ae..ec2a2a848 100644 --- a/ui/src/app/wizard/reducer/index.spec.ts +++ b/ui/src/app/wizard/reducer/index.spec.ts @@ -5,12 +5,12 @@ describe('wizard index selectors', () => { describe('getSchema method', () => { it('should return the schema by index name', () => { expect( - selectors.getSchema('common', FileBackedHttpMetadataProviderWizard) + selectors.getSchemaPath('common', FileBackedHttpMetadataProviderWizard) ).toBe(FileBackedHttpMetadataProviderWizard.steps[0].schema); }); it('should return nothing if no schema is found', () => { expect( - selectors.getSchema('common', null) + selectors.getSchemaPath('common', null) ).toBeFalsy(); }); }); diff --git a/ui/src/app/wizard/reducer/index.ts b/ui/src/app/wizard/reducer/index.ts index e2c15147b..8787cad71 100644 --- a/ui/src/app/wizard/reducer/index.ts +++ b/ui/src/app/wizard/reducer/index.ts @@ -46,13 +46,27 @@ export const getWizardIsDisabled = createSelector(getState, fromWizard.getDisabl export const getWizardDefinition = createSelector(getState, fromWizard.getDefinition); export const getSchemaCollection = createSelector(getState, fromWizard.getCollection); -export const getSchema = (index: string, wizard: Wizard) => { +export const getSchemaPath = (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 getSplitSchema = (schema: any, step: WizardStep) => { + if (!schema || !step.fields || !step.fields.length) { + return schema; + } + const keys = Object.keys(schema.properties).filter(key => step.fields.indexOf(key) > -1); + + return { + ...schema, + properties: { + ...keys.reduce( (properties, key) => ({ ...properties, [key]: schema.properties[key] }) , {}) + } + }; +}; + +export const getCurrentWizardSchema = createSelector(getWizardIndex, getWizardDefinition, getSchemaPath); export const getPreviousFn = (index: string, wizard: Wizard) => { if (!wizard) { return null; } @@ -96,3 +110,5 @@ export const getLocked = createSelector(getCurrent, getLockedStatus, getSchemaLo export const getSchemaObject = createSelector(getState, fromWizard.getSchema); export const getParsedSchema = createSelector(getSchemaObject, getLocked, getSchemaParseFn); + +export const getSchema = createSelector(getParsedSchema, getCurrent, getSplitSchema); From d578ed685232ca594f322e2296f512d680fc0dca Mon Sep 17 00:00:00 2001 From: Dmitriy Kopylenko Date: Fri, 5 Oct 2018 13:58:38 -0400 Subject: [PATCH 05/33] Updated JSON schema --- .../resources/metadata-sources-ui-schema.json | 461 +++++++++--------- 1 file changed, 232 insertions(+), 229 deletions(-) diff --git a/backend/src/main/resources/metadata-sources-ui-schema.json b/backend/src/main/resources/metadata-sources-ui-schema.json index 92211e558..11c5595ac 100644 --- a/backend/src/main/resources/metadata-sources-ui-schema.json +++ b/backend/src/main/resources/metadata-sources-ui-schema.json @@ -1,244 +1,247 @@ { - "entityId": { - "title": "label.entity-id", - "description": "tooltip.entity-id", - "type": "string" - }, - "serviceProviderName": { - "title": "label.service-provider-name", - "description": "tooltip.service-provider-name", - "type": "string" - }, - "serviceEnabled": { - "title": "label.enable-this-service-upon-saving", - "description": "tooltip.enable-this-service-upon-saving", - "type": "boolean" - }, - "organization": { - "type": "object", - "properties": { - "name": { - "title": "label.organization-name", - "description": "tooltip.organization-name", - "type": "string" - }, - "displayName": { - "title": "label.organization-display-name", - "description": "tooltip.organization-display-name", - "type": "string" + "type": "object", + "properties": { + "entityId": { + "title": "label.entity-id", + "description": "tooltip.entity-id", + "type": "string" + }, + "serviceProviderName": { + "title": "label.service-provider-name", + "description": "tooltip.service-provider-name", + "type": "string" + }, + "serviceEnabled": { + "title": "label.enable-this-service-opon-saving", + "description": "tooltip.enable-this-service-upon-saving", + "type": "boolean" + }, + "organization": { + "type": "object", + "properties": { + "name": { + "title": "label.organization-name", + "description": "tooltip.organization-name", + "type": "string" + }, + "displayName": { + "title": "label.organization-display-name", + "description": "tooltip.organization-display-name", + "type": "string" + }, + "url": { + "title": "label.organization-display-name", + "description": "tooltip.organization-display-name", + "type": "string" + } }, - "url": { - "title": "label.organization-display-name", - "description": "tooltip.organization-display-name", - "type": "string" + "dependencies": { + "name": [ + "displayName", + "url" + ], + "displayName": [ + "name", + "url" + ], + "url": [ + "name", + "displayName" + ] } }, - "dependencies": { - "name": [ - "displayName", - "url" - ], - "displayName": [ - "name", - "url" - ], - "url": [ - "name", - "displayName" - ] - } - }, - "contacts": { - "title": "label.contact-information", - "description": "tooltip.contact-information", - "type": "array", - "items": { - "$ref": "#/definitions/Contact" - } - }, - "mdui": { - "type": "object", - "properties": { - "displayName": { - "title": "label.display-name", - "description": "tooltip.mdui-display-name", - "type": "string" - }, - "informationUrl": { - "title": "label.information-url", - "description": "tooltip.mdui-information-url", - "type": "string" - }, - "privacyStatementUrl": { - "title": "label.privacy-statement-url", - "description": "tooltip.mdui-privacy-statement-url", - "type": "string" - }, - "description": { - "title": "label.description", - "description": "tooltip.mdui-description", - "type": "string" - }, - "logoUrl": { - "title": "label.logo-url", - "description": "tooltip.mdui-logo-url", - "type": "string" - }, - "logoHeight": { - "title": "label.logo-height", - "description": "tooltip.mdui-logo-height", - "min": 0, - "type": "integer" - }, - "logoWidth": { - "title": "label.logo-width", - "description": "tooltip.mdui-logo-width", - "min": 0, - "type": "integer" + "contacts": { + "title": "label.contact-information", + "description": "tooltip.contact-information", + "type": "array", + "items": { + "$ref": "#/definitions/Contact" } - } - }, - "securityInfo": { - "type": "object", - "properties": { - "x509CertificateAvailable": { - "title": "label.is-there-a-x509-certificate", - "description": "tooltip.is-there-a-x509-certificate", - "type": "boolean", - "default": false - }, - "authenticationRequestsSigned": { - "title": "label.authentication-requests-signed", - "description": "tooltip.authentication-requests-signed", - "type": "boolean", - "default": false - }, - "wantAssertionsSigned": { - "title": "label.want-assertions-signed", - "description": "tooltip.want-assertions-signed", - "type": "boolean", - "default": false - }, - "x509Certificates": { - "title": "label.x509-certificates", - "type": "array", - "items": { - "$ref": "#/definitions/Certificate" + }, + "mdui": { + "type": "object", + "properties": { + "displayName": { + "title": "label.display-name", + "description": "tooltip.mdui-display-name", + "type": "string" + }, + "informationUrl": { + "title": "label.information-url", + "description": "tooltip.mdui-information-url", + "type": "string" + }, + "privacyStatementUrl": { + "title": "label.privacy-statement-url", + "description": "tooltip.mdui-privacy-statement-url", + "type": "string" + }, + "description": { + "title": "label.description", + "description": "tooltip.mdui-description", + "type": "string" + }, + "logoUrl": { + "title": "label.logo-url", + "description": "tooltip.mdui-logo-url", + "type": "string" + }, + "logoHeight": { + "title": "label.logo-height", + "description": "tooltip.mdui-logo-height", + "min": 0, + "type": "integer" + }, + "logoWidth": { + "title": "label.logo-width", + "description": "tooltip.mdui-logo-width", + "min": 0, + "type": "integer" } } - } - }, - "assertionConsumerServices": { - "title": "label.assertion-consumer-service-endpoints", - "description": "", - "type": "array", - "items": { - "$ref": "#/definitions/AssertionConsumerService" - } - }, - "serviceProviderSsoDescriptor": { - "type": "object", - "properties": { - "protocolSupportEnum": { - "title": "label.protocol-support-enumeration", - "description": "tooltip.protocol-support-enumeration", - "type": "string", - "placeholder": "label.select-protocol", - "oneOf": [ - { - "enum": [ - "SAML 2" - ], - "description": "SAML 2" - }, - { - "enum": [ - "SAML 1.1" - ], - "description": "SAML 1.1" + }, + "securityInfo": { + "type": "object", + "properties": { + "x509CertificateAvailable": { + "title": "label.is-there-a-x509-certificate", + "description": "tooltip.is-there-a-x509-certificate", + "type": "boolean", + "default": false + }, + "authenticationRequestsSigned": { + "title": "label.authentication-requests-signed", + "description": "tooltip.authentication-requests-signed", + "type": "boolean", + "default": false + }, + "wantAssertionsSigned": { + "title": "label.want-assertions-signed", + "description": "tooltip.want-assertions-signed", + "type": "boolean", + "default": false + }, + "x509Certificates": { + "title": "label.x509-certificates", + "type": "array", + "items": { + "$ref": "#/definitions/Certificate" } - ] + } } }, - "nameIdFormats": { - "$ref": "#/definitions/NameIdFormatList" - } - }, - "logoutEndpoints": { - "title": "label.logout-endpoints", - "description": "tooltip.logout-endpoints", - "type": "array", - "items": { - "$ref": "#/definitions/LogoutEndpoint" - } - }, - "relyingPartyOverrides": { - "type": "object", - "properties": { - "signAssertion": { - "title": "label.sign-the-assertion", - "description": "tooltip.sign-assertion", - "type": "boolean", - "default": false - }, - "dontSignResponse": { - "title": "label.dont-sign-the-response", - "description": "tooltip.dont-sign-response", - "type": "boolean", - "default": false - }, - "turnOffEncryption": { - "title": "label.turn-off-encryption-of-response", - "description": "tooltip.turn-off-encryption", - "type": "boolean", - "default": false - }, - "useSha": { - "title": "label.use-sha1-signing-algorithm", - "description": "tooltip.usa-sha-algorithm", - "type": "boolean", - "default": false - }, - "ignoreAuthenticationMethod": { - "title": "label.ignore-any-sp-requested-authentication-method", - "description": "tooltip.ignore-auth-method", - "type": "boolean", - "default": false - }, - "forceAuthn": { - "title": "label.force-authn", - "description": "tooltip.force-authn", - "type": "boolean", - "default": false - }, - "omitNotBefore": { - "title": "label.omit-not-before-condition", - "type": "boolean", - "description": "tooltip.omit-not-before-condition", - "default": false - }, - "responderId": { - "title": "label.responder-id", - "description": "tooltip.responder-id", - "type": "string" + "assertionConsumerServices": { + "title": "label.assertion-consumer-service-endpoints", + "description": "", + "type": "array", + "items": { + "$ref": "#/definitions/AssertionConsumerService" + } + }, + "serviceProviderSsoDescriptor": { + "type": "object", + "properties": { + "protocolSupportEnum": { + "title": "label.protocol-support-enumeration", + "description": "tooltip.protocol-support-enumeration", + "type": "string", + "placeholder": "label.select-protocol", + "oneOf": [ + { + "enum": [ + "SAML 2" + ], + "description": "SAML 2" + }, + { + "enum": [ + "SAML 1.1" + ], + "description": "SAML 1.1" + } + ] + } }, "nameIdFormats": { "$ref": "#/definitions/NameIdFormatList" - }, - "authenticationMethods": { - "$ref": "#/definitions/AuthenticationMethodList" } - } - }, - "attributeRelease": { - "type": "array", - "description": "Attribute release table - select the attributes you want to release (default unchecked)", - "widget": { - "id": "checklist", - "dataUrl": "/customAttributes" }, - "items": { - "type": "string" + "logoutEndpoints": { + "title": "label.logout-endpoints", + "description": "tooltip.logout-endpoints", + "type": "array", + "items": { + "$ref": "#/definitions/LogoutEndpoint" + } + }, + "relyingPartyOverrides": { + "type": "object", + "properties": { + "signAssertion": { + "title": "label.sign-the-assertion", + "description": "tooltip.sign-assertion", + "type": "boolean", + "default": false + }, + "dontSignResponse": { + "title": "label.dont-sign-the-response", + "description": "tooltip.dont-sign-response", + "type": "boolean", + "default": false + }, + "turnOffEncryption": { + "title": "label.turn-off-encryption-of-response", + "description": "tooltip.turn-off-encryption", + "type": "boolean", + "default": false + }, + "useSha": { + "title": "label.use-sha1-signing-algorithm", + "description": "tooltip.usa-sha-algorithm", + "type": "boolean", + "default": false + }, + "ignoreAuthenticationMethod": { + "title": "label.ignore-any-sp-requested-authentication-method", + "description": "tooltip.ignore-auth-method", + "type": "boolean", + "default": false + }, + "forceAuthn": { + "title": "label.force-authn", + "description": "tooltip.force-authn", + "type": "boolean", + "default": false + }, + "omitNotBefore": { + "title": "label.omit-not-before-condition", + "type": "boolean", + "description": "tooltip.omit-not-before-condition", + "default": false + }, + "responderId": { + "title": "label.responder-id", + "description": "tooltip.responder-id", + "type": "string" + }, + "nameIdFormats": { + "$ref": "#/definitions/NameIdFormatList" + }, + "authenticationMethods": { + "$ref": "#/definitions/AuthenticationMethodList" + } + } + }, + "attributeRelease": { + "type": "array", + "description": "Attribute release table - select the attributes you want to release (default unchecked)", + "widget": { + "id": "checklist", + "dataUrl": "/customAttributes" + }, + "items": { + "type": "string" + } } }, "definitions": { @@ -259,7 +262,7 @@ "enum": [ "support" ], - "description": "value.support" + "description": "value.support" }, { "enum": [ @@ -431,9 +434,9 @@ "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" } ] - + } } } } -} \ No newline at end of file +} From a9f5cf9ab7ad43c080acb6561bcad5efd30e2bb7 Mon Sep 17 00:00:00 2001 From: Dmitriy Kopylenko Date: Fri, 5 Oct 2018 18:51:09 -0400 Subject: [PATCH 06/33] Work in progress --- ...tadataSourcesUiDefinitionController.groovy | 17 +- .../src/main/resources/application.properties | 2 +- ...efinitionControllerIntegrationTests.groovy | 51 ++ .../metadata-sources-ui-schema_BAD.json | 442 ++++++++++++++++++ 4 files changed, 506 insertions(+), 6 deletions(-) create mode 100644 backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionControllerIntegrationTests.groovy create mode 100644 backend/src/test/resources/metadata-sources-ui-schema_BAD.json diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionController.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionController.groovy index 481d95ac5..6156b97cd 100644 --- a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionController.groovy +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionController.groovy @@ -1,31 +1,33 @@ package edu.internet2.tier.shibboleth.admin.ui.controller import groovy.json.JsonSlurper +import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.core.io.ResourceLoader import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController +import javax.annotation.PostConstruct + /** * Controller implementing REST resource responsible for exposing structure definition for metadata sources user * interface in terms of JSON schema. * * @author Dmitriy Kopylenko */ -@RestController('/api/ui/metadataSources') +@RestController('/api/ui/MetadataSources') @ConfigurationProperties('shibui') class MetadataSourcesUiDefinitionController { //Configured via @ConfigurationProperties with 'shibui.metadata-sources-ui-schema-location' property and default //value set here if that property is not set - String metadataSourcesUiSchemaLocation = 'classpath:metadata-sources-ui-schema.json' + String metadataSourcesUiSchemaLocation //= 'classpath:metadata-sources-ui-schema.json' URL jsonSchemaUrl - MetadataSourcesUiDefinitionController(ResourceLoader resourceLoader) { - jsonSchemaUrl = resourceLoader.getResource(metadataSourcesUiSchemaLocation).getURL() - } + @Autowired + ResourceLoader resourceLoader @GetMapping ResponseEntity getUiDefinitionJsonSchema() { @@ -33,4 +35,9 @@ class MetadataSourcesUiDefinitionController { def json = new JsonSlurper().parse(this.jsonSchemaUrl) ResponseEntity.ok(json) } + + @PostConstruct + def init() { + jsonSchemaUrl = this.resourceLoader.getResource(this.metadataSourcesUiSchemaLocation).getURL() + } } diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 4d5baf934..0386e64b1 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -49,7 +49,7 @@ spring.profiles.active=default #shibui.default-password= -shibui.metadata-sources-ui-schema-location=classpath:metadata-sources-ui-schema-location.json +shibui.metadata-sources-ui-schema-location=classpath:metadata-sources-ui-schema.json #Actuator endpoints (info) # Un-comment to get full git details exposed like author, abbreviated SHA-1, commit message diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionControllerIntegrationTests.groovy new file mode 100644 index 000000000..643df654d --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionControllerIntegrationTests.groovy @@ -0,0 +1,51 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller + +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.core.env.ConfigurableEnvironment +import org.springframework.test.context.ActiveProfiles +import spock.lang.Specification + +/** + * @author Dmitriy Kopylenko + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("no-auth") +class MetadataSourcesUiDefinitionControllerIntegrationTests extends Specification { + + @Autowired + private TestRestTemplate restTemplate + + @Autowired + ConfigurableEnvironment environment + + @Autowired + MetadataSourcesUiDefinitionController controllerUnderTest + + static RESOURCE_URI = '/api/ui/MetadataSources' + + def "GET Metadata Sources UI definition schema"() { + when: 'GET request is made for metadata source UI definition schema' + + def result = this.restTemplate.getForEntity(RESOURCE_URI, Object) + + then: "Request completed successfully" + result.statusCodeValue == 200 + result.body.properties.entityId.title == 'label.entity-id' + } + + def "GET Malformed Metadata Sources UI definition schema"() { + when: 'GET request is made for malformed metadata source UI definition schema' + configureMalformedJsonInput() + def result = this.restTemplate.getForEntity(RESOURCE_URI, Object) + + then: "Request results in HTTP 400" + result.statusCodeValue == 200 + } + + private configureMalformedJsonInput() { + controllerUnderTest.metadataSourcesUiSchemaLocation = 'classpath:metadata-sources-ui-schema_BAD.json' + controllerUnderTest.init() + } +} \ No newline at end of file diff --git a/backend/src/test/resources/metadata-sources-ui-schema_BAD.json b/backend/src/test/resources/metadata-sources-ui-schema_BAD.json new file mode 100644 index 000000000..ee055fdf4 --- /dev/null +++ b/backend/src/test/resources/metadata-sources-ui-schema_BAD.json @@ -0,0 +1,442 @@ +{ + "type": "object" + "properties": { + "entityId": { + "title": "label.entity-id", + "description": "tooltip.entity-id", + "type": "string" + }, + "serviceProviderName": { + "title": "label.service-provider-name", + "description": "tooltip.service-provider-name", + "type": "string" + }, + "serviceEnabled": { + "title": "label.enable-this-service-upon-saving", + "description": "tooltip.enable-this-service-upon-saving", + "type": "boolean" + }, + "organization": { + "type": "object", + "properties": { + "name": { + "title": "label.organization-name", + "description": "tooltip.organization-name", + "type": "string" + }, + "displayName": { + "title": "label.organization-display-name", + "description": "tooltip.organization-display-name", + "type": "string" + }, + "url": { + "title": "label.organization-display-name", + "description": "tooltip.organization-display-name", + "type": "string" + } + }, + "dependencies": { + "name": [ + "displayName", + "url" + ], + "displayName": [ + "name", + "url" + ], + "url": [ + "name", + "displayName" + ] + } + }, + "contacts": { + "title": "label.contact-information", + "description": "tooltip.contact-information", + "type": "array", + "items": { + "$ref": "#/definitions/Contact" + } + }, + "mdui": { + "type": "object", + "properties": { + "displayName": { + "title": "label.display-name", + "description": "tooltip.mdui-display-name", + "type": "string" + }, + "informationUrl": { + "title": "label.information-url", + "description": "tooltip.mdui-information-url", + "type": "string" + }, + "privacyStatementUrl": { + "title": "label.privacy-statement-url", + "description": "tooltip.mdui-privacy-statement-url", + "type": "string" + }, + "description": { + "title": "label.description", + "description": "tooltip.mdui-description", + "type": "string" + }, + "logoUrl": { + "title": "label.logo-url", + "description": "tooltip.mdui-logo-url", + "type": "string" + }, + "logoHeight": { + "title": "label.logo-height", + "description": "tooltip.mdui-logo-height", + "min": 0, + "type": "integer" + }, + "logoWidth": { + "title": "label.logo-width", + "description": "tooltip.mdui-logo-width", + "min": 0, + "type": "integer" + } + } + }, + "securityInfo": { + "type": "object", + "properties": { + "x509CertificateAvailable": { + "title": "label.is-there-a-x509-certificate", + "description": "tooltip.is-there-a-x509-certificate", + "type": "boolean", + "default": false + }, + "authenticationRequestsSigned": { + "title": "label.authentication-requests-signed", + "description": "tooltip.authentication-requests-signed", + "type": "boolean", + "default": false + }, + "wantAssertionsSigned": { + "title": "label.want-assertions-signed", + "description": "tooltip.want-assertions-signed", + "type": "boolean", + "default": false + }, + "x509Certificates": { + "title": "label.x509-certificates", + "type": "array", + "items": { + "$ref": "#/definitions/Certificate" + } + } + } + }, + "assertionConsumerServices": { + "title": "label.assertion-consumer-service-endpoints", + "description": "", + "type": "array", + "items": { + "$ref": "#/definitions/AssertionConsumerService" + } + }, + "serviceProviderSsoDescriptor": { + "type": "object", + "properties": { + "protocolSupportEnum": { + "title": "label.protocol-support-enumeration", + "description": "tooltip.protocol-support-enumeration", + "type": "string", + "placeholder": "label.select-protocol", + "oneOf": [ + { + "enum": [ + "SAML 2" + ], + "description": "SAML 2" + }, + { + "enum": [ + "SAML 1.1" + ], + "description": "SAML 1.1" + } + ] + } + }, + "nameIdFormats": { + "$ref": "#/definitions/NameIdFormatList" + } + }, + "logoutEndpoints": { + "title": "label.logout-endpoints", + "description": "tooltip.logout-endpoints", + "type": "array", + "items": { + "$ref": "#/definitions/LogoutEndpoint" + } + }, + "relyingPartyOverrides": { + "type": "object", + "properties": { + "signAssertion": { + "title": "label.sign-the-assertion", + "description": "tooltip.sign-assertion", + "type": "boolean", + "default": false + }, + "dontSignResponse": { + "title": "label.dont-sign-the-response", + "description": "tooltip.dont-sign-response", + "type": "boolean", + "default": false + }, + "turnOffEncryption": { + "title": "label.turn-off-encryption-of-response", + "description": "tooltip.turn-off-encryption", + "type": "boolean", + "default": false + }, + "useSha": { + "title": "label.use-sha1-signing-algorithm", + "description": "tooltip.usa-sha-algorithm", + "type": "boolean", + "default": false + }, + "ignoreAuthenticationMethod": { + "title": "label.ignore-any-sp-requested-authentication-method", + "description": "tooltip.ignore-auth-method", + "type": "boolean", + "default": false + }, + "forceAuthn": { + "title": "label.force-authn", + "description": "tooltip.force-authn", + "type": "boolean", + "default": false + }, + "omitNotBefore": { + "title": "label.omit-not-before-condition", + "type": "boolean", + "description": "tooltip.omit-not-before-condition", + "default": false + }, + "responderId": { + "title": "label.responder-id", + "description": "tooltip.responder-id", + "type": "string" + }, + "nameIdFormats": { + "$ref": "#/definitions/NameIdFormatList" + }, + "authenticationMethods": { + "$ref": "#/definitions/AuthenticationMethodList" + } + } + }, + "attributeRelease": { + "type": "array", + "description": "Attribute release table - select the attributes you want to release (default unchecked)", + "widget": { + "id": "checklist", + "dataUrl": "/customAttributes" + }, + "items": { + "type": "string" + } + } + }, + "definitions": { + "Contact": { + "type": "object", + "properties": { + "name": { + "title": "label.contact-name", + "description": "tooltip.contact-name", + "type": "string" + }, + "type": { + "title": "label.contact-type", + "description": "tooltip.contact-type", + "type": "string", + "oneOf": [ + { + "enum": [ + "support" + ], + "description": "value.support" + }, + { + "enum": [ + "technical" + ], + "description": "value.technical" + }, + { + "enum": [ + "administrative" + ], + "description": "value.administrative" + }, + { + "enum": [ + "other" + ], + "description": "value.other" + } + ] + }, + "emailAddress": { + "title": "label.contact-email-address", + "description": "tooltip.contact-email", + "type": "string", + "pattern": "^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$" + } + } + }, + "Certificate": { + "name": { + "title": "label.certificate-name-display-only", + "description": "tooltip.certificate-name", + "type": "string" + }, + "type": { + "title": "label.type", + "description": "tooltip.certificate-type", + "type": "string", + "oneOf": [ + { + "enum": [ + "signing" + ], + "description": "value.signing" + }, + { + "enum": [ + "encryption" + ], + "description": "value.encryption" + }, + { + "enum": [ + "both" + ], + "description": "value.both" + } + ], + "default": "both" + }, + "value": { + "title": "label.certificate", + "description": "tooltip.certificate", + "type": "string" + } + }, + "AssertionConsumerService": { + "type": "object", + "properties": { + "locationUrl": { + "title": "label.assertion-consumer-services-location", + "description": "tooltip.assertion-consumer-service-location", + "type": "string", + "widget": { + "id": "string", + "help": "message.valid-url" + } + }, + "binding": { + "title": "label.assertion-consumer-service-location-binding", + "description": "tooltip.assertion-consumer-service-location-binding", + "type": "string", + "oneOf": [ + { + "enum": [ + "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + ], + "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + }, + { + "enum": [ + "urn:oasis:names:tc:SAML:1.0:profiles:browser-post" + ], + "description": "urn:oasis:names:tc:SAML:1.0:profiles:browser-post" + } + ] + }, + "makeDefault": { + "title": "label.mark-as-default", + "description": "tooltip.mark-as-default", + "type": "boolean" + } + } + }, + "NameIdFormatList": { + "title": "label.nameid-format-to-send", + "placeholder": "label.nameid-format", + "description": "tooltip.nameid-format", + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "widget": "datalist", + "data": [ + "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", + "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", + "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", + "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" + ] + }, + "default": null + }, + "AuthenticationMethodList": { + "title": "label.authentication-methods-to-use", + "description": "tooltip.authentication-methods-to-use", + "type": "array", + "placeholder": "label.authentication-method", + "uniqueItems": true, + "items": { + "type": "string", + "title": "label.authentication-method", + "widget": { + "id": "datalist", + "data": [ + "https://refeds.org/profile/mfa", + "urn:oasis:names:tc:SAML:2.0:ac:classes:TimeSyncToken", + "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport" + ] + } + }, + "default": null + }, + "LogoutEndpoint": { + "title": "label.new-endpoint", + "description": "tooltip.new-endpoint", + "type": "object", + "properties": { + "url": { + "title": "label.url", + "description": "tooltip.url", + "type": "string" + }, + "bindingType": { + "title": "label.binding-type", + "description": "tooltip.binding-type", + "type": "string", + "oneOf": [ + { + "enum": [ + "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + ], + "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + }, + { + "enum": [ + "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + ], + "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + } + ] + + } + } + } + } +} From 08cf1023818db8adbfefccc678dd3172deb8ba36 Mon Sep 17 00:00:00 2001 From: Dmitriy Kopylenko Date: Fri, 5 Oct 2018 18:52:57 -0400 Subject: [PATCH 07/33] Work in progress --- ...etadataSourcesUiDefinitionControllerIntegrationTests.groovy | 3 --- 1 file changed, 3 deletions(-) diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionControllerIntegrationTests.groovy index 643df654d..abc416b78 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionControllerIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionControllerIntegrationTests.groovy @@ -17,9 +17,6 @@ class MetadataSourcesUiDefinitionControllerIntegrationTests extends Specificatio @Autowired private TestRestTemplate restTemplate - @Autowired - ConfigurableEnvironment environment - @Autowired MetadataSourcesUiDefinitionController controllerUnderTest From 12a91edc2da8d49a57f0fcb6e007b08303e09e63 Mon Sep 17 00:00:00 2001 From: Dmitriy Kopylenko Date: Mon, 8 Oct 2018 09:19:33 -0400 Subject: [PATCH 08/33] WIP --- .../resources/metadata-sources-ui-schema.json | 6 +- .../resources/metadata-sources-ui-schema.json | 442 ++++++++++++++++++ ...metadata-sources-ui-schema_MALFORMED.json} | 6 +- 3 files changed, 448 insertions(+), 6 deletions(-) create mode 100644 backend/src/test/resources/metadata-sources-ui-schema.json rename backend/src/test/resources/{metadata-sources-ui-schema_BAD.json => metadata-sources-ui-schema_MALFORMED.json} (99%) diff --git a/backend/src/main/resources/metadata-sources-ui-schema.json b/backend/src/main/resources/metadata-sources-ui-schema.json index 11c5595ac..5766e298f 100644 --- a/backend/src/main/resources/metadata-sources-ui-schema.json +++ b/backend/src/main/resources/metadata-sources-ui-schema.json @@ -262,7 +262,7 @@ "enum": [ "support" ], - "description": "value.support" + "description": "value.support" }, { "enum": [ @@ -434,9 +434,9 @@ "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" } ] - + } } } } -} +} \ No newline at end of file diff --git a/backend/src/test/resources/metadata-sources-ui-schema.json b/backend/src/test/resources/metadata-sources-ui-schema.json new file mode 100644 index 000000000..5766e298f --- /dev/null +++ b/backend/src/test/resources/metadata-sources-ui-schema.json @@ -0,0 +1,442 @@ +{ + "type": "object", + "properties": { + "entityId": { + "title": "label.entity-id", + "description": "tooltip.entity-id", + "type": "string" + }, + "serviceProviderName": { + "title": "label.service-provider-name", + "description": "tooltip.service-provider-name", + "type": "string" + }, + "serviceEnabled": { + "title": "label.enable-this-service-opon-saving", + "description": "tooltip.enable-this-service-upon-saving", + "type": "boolean" + }, + "organization": { + "type": "object", + "properties": { + "name": { + "title": "label.organization-name", + "description": "tooltip.organization-name", + "type": "string" + }, + "displayName": { + "title": "label.organization-display-name", + "description": "tooltip.organization-display-name", + "type": "string" + }, + "url": { + "title": "label.organization-display-name", + "description": "tooltip.organization-display-name", + "type": "string" + } + }, + "dependencies": { + "name": [ + "displayName", + "url" + ], + "displayName": [ + "name", + "url" + ], + "url": [ + "name", + "displayName" + ] + } + }, + "contacts": { + "title": "label.contact-information", + "description": "tooltip.contact-information", + "type": "array", + "items": { + "$ref": "#/definitions/Contact" + } + }, + "mdui": { + "type": "object", + "properties": { + "displayName": { + "title": "label.display-name", + "description": "tooltip.mdui-display-name", + "type": "string" + }, + "informationUrl": { + "title": "label.information-url", + "description": "tooltip.mdui-information-url", + "type": "string" + }, + "privacyStatementUrl": { + "title": "label.privacy-statement-url", + "description": "tooltip.mdui-privacy-statement-url", + "type": "string" + }, + "description": { + "title": "label.description", + "description": "tooltip.mdui-description", + "type": "string" + }, + "logoUrl": { + "title": "label.logo-url", + "description": "tooltip.mdui-logo-url", + "type": "string" + }, + "logoHeight": { + "title": "label.logo-height", + "description": "tooltip.mdui-logo-height", + "min": 0, + "type": "integer" + }, + "logoWidth": { + "title": "label.logo-width", + "description": "tooltip.mdui-logo-width", + "min": 0, + "type": "integer" + } + } + }, + "securityInfo": { + "type": "object", + "properties": { + "x509CertificateAvailable": { + "title": "label.is-there-a-x509-certificate", + "description": "tooltip.is-there-a-x509-certificate", + "type": "boolean", + "default": false + }, + "authenticationRequestsSigned": { + "title": "label.authentication-requests-signed", + "description": "tooltip.authentication-requests-signed", + "type": "boolean", + "default": false + }, + "wantAssertionsSigned": { + "title": "label.want-assertions-signed", + "description": "tooltip.want-assertions-signed", + "type": "boolean", + "default": false + }, + "x509Certificates": { + "title": "label.x509-certificates", + "type": "array", + "items": { + "$ref": "#/definitions/Certificate" + } + } + } + }, + "assertionConsumerServices": { + "title": "label.assertion-consumer-service-endpoints", + "description": "", + "type": "array", + "items": { + "$ref": "#/definitions/AssertionConsumerService" + } + }, + "serviceProviderSsoDescriptor": { + "type": "object", + "properties": { + "protocolSupportEnum": { + "title": "label.protocol-support-enumeration", + "description": "tooltip.protocol-support-enumeration", + "type": "string", + "placeholder": "label.select-protocol", + "oneOf": [ + { + "enum": [ + "SAML 2" + ], + "description": "SAML 2" + }, + { + "enum": [ + "SAML 1.1" + ], + "description": "SAML 1.1" + } + ] + } + }, + "nameIdFormats": { + "$ref": "#/definitions/NameIdFormatList" + } + }, + "logoutEndpoints": { + "title": "label.logout-endpoints", + "description": "tooltip.logout-endpoints", + "type": "array", + "items": { + "$ref": "#/definitions/LogoutEndpoint" + } + }, + "relyingPartyOverrides": { + "type": "object", + "properties": { + "signAssertion": { + "title": "label.sign-the-assertion", + "description": "tooltip.sign-assertion", + "type": "boolean", + "default": false + }, + "dontSignResponse": { + "title": "label.dont-sign-the-response", + "description": "tooltip.dont-sign-response", + "type": "boolean", + "default": false + }, + "turnOffEncryption": { + "title": "label.turn-off-encryption-of-response", + "description": "tooltip.turn-off-encryption", + "type": "boolean", + "default": false + }, + "useSha": { + "title": "label.use-sha1-signing-algorithm", + "description": "tooltip.usa-sha-algorithm", + "type": "boolean", + "default": false + }, + "ignoreAuthenticationMethod": { + "title": "label.ignore-any-sp-requested-authentication-method", + "description": "tooltip.ignore-auth-method", + "type": "boolean", + "default": false + }, + "forceAuthn": { + "title": "label.force-authn", + "description": "tooltip.force-authn", + "type": "boolean", + "default": false + }, + "omitNotBefore": { + "title": "label.omit-not-before-condition", + "type": "boolean", + "description": "tooltip.omit-not-before-condition", + "default": false + }, + "responderId": { + "title": "label.responder-id", + "description": "tooltip.responder-id", + "type": "string" + }, + "nameIdFormats": { + "$ref": "#/definitions/NameIdFormatList" + }, + "authenticationMethods": { + "$ref": "#/definitions/AuthenticationMethodList" + } + } + }, + "attributeRelease": { + "type": "array", + "description": "Attribute release table - select the attributes you want to release (default unchecked)", + "widget": { + "id": "checklist", + "dataUrl": "/customAttributes" + }, + "items": { + "type": "string" + } + } + }, + "definitions": { + "Contact": { + "type": "object", + "properties": { + "name": { + "title": "label.contact-name", + "description": "tooltip.contact-name", + "type": "string" + }, + "type": { + "title": "label.contact-type", + "description": "tooltip.contact-type", + "type": "string", + "oneOf": [ + { + "enum": [ + "support" + ], + "description": "value.support" + }, + { + "enum": [ + "technical" + ], + "description": "value.technical" + }, + { + "enum": [ + "administrative" + ], + "description": "value.administrative" + }, + { + "enum": [ + "other" + ], + "description": "value.other" + } + ] + }, + "emailAddress": { + "title": "label.contact-email-address", + "description": "tooltip.contact-email", + "type": "string", + "pattern": "^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$" + } + } + }, + "Certificate": { + "name": { + "title": "label.certificate-name-display-only", + "description": "tooltip.certificate-name", + "type": "string" + }, + "type": { + "title": "label.type", + "description": "tooltip.certificate-type", + "type": "string", + "oneOf": [ + { + "enum": [ + "signing" + ], + "description": "value.signing" + }, + { + "enum": [ + "encryption" + ], + "description": "value.encryption" + }, + { + "enum": [ + "both" + ], + "description": "value.both" + } + ], + "default": "both" + }, + "value": { + "title": "label.certificate", + "description": "tooltip.certificate", + "type": "string" + } + }, + "AssertionConsumerService": { + "type": "object", + "properties": { + "locationUrl": { + "title": "label.assertion-consumer-services-location", + "description": "tooltip.assertion-consumer-service-location", + "type": "string", + "widget": { + "id": "string", + "help": "message.valid-url" + } + }, + "binding": { + "title": "label.assertion-consumer-service-location-binding", + "description": "tooltip.assertion-consumer-service-location-binding", + "type": "string", + "oneOf": [ + { + "enum": [ + "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + ], + "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + }, + { + "enum": [ + "urn:oasis:names:tc:SAML:1.0:profiles:browser-post" + ], + "description": "urn:oasis:names:tc:SAML:1.0:profiles:browser-post" + } + ] + }, + "makeDefault": { + "title": "label.mark-as-default", + "description": "tooltip.mark-as-default", + "type": "boolean" + } + } + }, + "NameIdFormatList": { + "title": "label.nameid-format-to-send", + "placeholder": "label.nameid-format", + "description": "tooltip.nameid-format", + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "widget": "datalist", + "data": [ + "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", + "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", + "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", + "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" + ] + }, + "default": null + }, + "AuthenticationMethodList": { + "title": "label.authentication-methods-to-use", + "description": "tooltip.authentication-methods-to-use", + "type": "array", + "placeholder": "label.authentication-method", + "uniqueItems": true, + "items": { + "type": "string", + "title": "label.authentication-method", + "widget": { + "id": "datalist", + "data": [ + "https://refeds.org/profile/mfa", + "urn:oasis:names:tc:SAML:2.0:ac:classes:TimeSyncToken", + "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport" + ] + } + }, + "default": null + }, + "LogoutEndpoint": { + "title": "label.new-endpoint", + "description": "tooltip.new-endpoint", + "type": "object", + "properties": { + "url": { + "title": "label.url", + "description": "tooltip.url", + "type": "string" + }, + "bindingType": { + "title": "label.binding-type", + "description": "tooltip.binding-type", + "type": "string", + "oneOf": [ + { + "enum": [ + "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + ], + "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + }, + { + "enum": [ + "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + ], + "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + } + ] + + } + } + } + } +} \ No newline at end of file diff --git a/backend/src/test/resources/metadata-sources-ui-schema_BAD.json b/backend/src/test/resources/metadata-sources-ui-schema_MALFORMED.json similarity index 99% rename from backend/src/test/resources/metadata-sources-ui-schema_BAD.json rename to backend/src/test/resources/metadata-sources-ui-schema_MALFORMED.json index ee055fdf4..6d4d76a24 100644 --- a/backend/src/test/resources/metadata-sources-ui-schema_BAD.json +++ b/backend/src/test/resources/metadata-sources-ui-schema_MALFORMED.json @@ -1,5 +1,5 @@ -{ - "type": "object" + + "type": "object", "properties": { "entityId": { "title": "label.entity-id", @@ -12,7 +12,7 @@ "type": "string" }, "serviceEnabled": { - "title": "label.enable-this-service-upon-saving", + "title": "label.enable-this-service-opon-saving", "description": "tooltip.enable-this-service-upon-saving", "type": "boolean" }, From 47582984d8b4b111f6ffbc605b100658b36d61df Mon Sep 17 00:00:00 2001 From: Dmitriy Kopylenko Date: Mon, 8 Oct 2018 10:37:15 -0400 Subject: [PATCH 09/33] WIP2 --- ...tadataSourcesUiDefinitionController.groovy | 23 ++++++++++++++----- ...efinitionControllerIntegrationTests.groovy | 9 ++++---- .../metadata-sources-ui-schema_MALFORMED.json | 4 ++-- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionController.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionController.groovy index 6156b97cd..0471e40dd 100644 --- a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionController.groovy +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionController.groovy @@ -1,6 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.controller -import groovy.json.JsonSlurper +import com.fasterxml.jackson.databind.ObjectMapper import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.core.io.ResourceLoader @@ -10,6 +10,8 @@ import org.springframework.web.bind.annotation.RestController import javax.annotation.PostConstruct +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR + /** * Controller implementing REST resource responsible for exposing structure definition for metadata sources user * interface in terms of JSON schema. @@ -21,19 +23,28 @@ import javax.annotation.PostConstruct class MetadataSourcesUiDefinitionController { //Configured via @ConfigurationProperties with 'shibui.metadata-sources-ui-schema-location' property and default - //value set here if that property is not set - String metadataSourcesUiSchemaLocation //= 'classpath:metadata-sources-ui-schema.json' + //value set here if that property is not explicitly set in application.properties + String metadataSourcesUiSchemaLocation = 'classpath:metadata-sources-ui-schema.json' URL jsonSchemaUrl @Autowired ResourceLoader resourceLoader + @Autowired + ObjectMapper jacksonObjectMapper + @GetMapping ResponseEntity getUiDefinitionJsonSchema() { - //JsonSlurper is not threadsafe, but cheap to init. New instance per-thread is the canonical usage - def json = new JsonSlurper().parse(this.jsonSchemaUrl) - ResponseEntity.ok(json) + try { + def parsedJson = jacksonObjectMapper.readValue(this.jsonSchemaUrl, Map) + return ResponseEntity.ok(parsedJson) + } + catch (Exception e) { + return ResponseEntity.status(INTERNAL_SERVER_ERROR) + .body([jsonParseError : e.getMessage(), + sourceUiSchemaDefinitionFile: this.jsonSchemaUrl]) + } } @PostConstruct diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionControllerIntegrationTests.groovy index abc416b78..9d804ad57 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionControllerIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionControllerIntegrationTests.groovy @@ -3,7 +3,6 @@ package edu.internet2.tier.shibboleth.admin.ui.controller 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.core.env.ConfigurableEnvironment import org.springframework.test.context.ActiveProfiles import spock.lang.Specification @@ -37,12 +36,14 @@ class MetadataSourcesUiDefinitionControllerIntegrationTests extends Specificatio configureMalformedJsonInput() def result = this.restTemplate.getForEntity(RESOURCE_URI, Object) - then: "Request results in HTTP 400" - result.statusCodeValue == 200 + then: "Request results in HTTP 500" + result.statusCodeValue == 500 + result.body.jsonParseError + result.body.sourceUiSchemaDefinitionFile } private configureMalformedJsonInput() { - controllerUnderTest.metadataSourcesUiSchemaLocation = 'classpath:metadata-sources-ui-schema_BAD.json' + controllerUnderTest.metadataSourcesUiSchemaLocation = 'classpath:metadata-sources-ui-schema_MALFORMED.json' controllerUnderTest.init() } } \ No newline at end of file diff --git a/backend/src/test/resources/metadata-sources-ui-schema_MALFORMED.json b/backend/src/test/resources/metadata-sources-ui-schema_MALFORMED.json index 6d4d76a24..7ca5220f0 100644 --- a/backend/src/test/resources/metadata-sources-ui-schema_MALFORMED.json +++ b/backend/src/test/resources/metadata-sources-ui-schema_MALFORMED.json @@ -1,5 +1,5 @@ - - "type": "object", +{ + "type": "object" "properties": { "entityId": { "title": "label.entity-id", From 145f18d6a69f99e47ad1af7ea5b73587d9144836 Mon Sep 17 00:00:00 2001 From: Bill Smith Date: Mon, 8 Oct 2018 16:51:28 -0700 Subject: [PATCH 10/33] [SHIBUI-905][SHIBUI-916] Added custom attributes to json schema generation. --- .../MetadataSourcesUiDefinitionController.groovy | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionController.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionController.groovy index 0471e40dd..9d790f362 100644 --- a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionController.groovy +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionController.groovy @@ -1,6 +1,8 @@ package edu.internet2.tier.shibboleth.admin.ui.controller import com.fasterxml.jackson.databind.ObjectMapper +import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomAttributesConfiguration +import groovy.json.JsonOutput import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.core.io.ResourceLoader @@ -34,10 +36,22 @@ class MetadataSourcesUiDefinitionController { @Autowired ObjectMapper jacksonObjectMapper + @Autowired + CustomAttributesConfiguration customAttributesConfiguration + @GetMapping ResponseEntity getUiDefinitionJsonSchema() { try { def parsedJson = jacksonObjectMapper.readValue(this.jsonSchemaUrl, Map) + def widget = parsedJson["properties"]["attributeRelease"]["widget"] + def data = [] + customAttributesConfiguration.getAttributes().each { + def attribute = [:] + attribute["key"] = it["name"] + attribute["label"] = it["displayName"] + data << attribute + } + widget["data"] = data return ResponseEntity.ok(parsedJson) } catch (Exception e) { From b700f07e48dcc547c9156ccc7931f528af4146a1 Mon Sep 17 00:00:00 2001 From: Dmitriy Kopylenko Date: Tue, 9 Oct 2018 10:15:43 -0400 Subject: [PATCH 11/33] Fail fast during app start up --- .../MetadataSourcesUiDefinitionController.groovy | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionController.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionController.groovy index 9d790f362..74f5e6d8c 100644 --- a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionController.groovy +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionController.groovy @@ -2,7 +2,8 @@ package edu.internet2.tier.shibboleth.admin.ui.controller import com.fasterxml.jackson.databind.ObjectMapper import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomAttributesConfiguration -import groovy.json.JsonOutput + +import org.springframework.beans.factory.BeanInitializationException import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.core.io.ResourceLoader @@ -19,6 +20,7 @@ import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR * interface in terms of JSON schema. * * @author Dmitriy Kopylenko + * @author Bill Smith (wsmith@unicon.net) */ @RestController('/api/ui/MetadataSources') @ConfigurationProperties('shibui') @@ -64,5 +66,17 @@ class MetadataSourcesUiDefinitionController { @PostConstruct def init() { jsonSchemaUrl = this.resourceLoader.getResource(this.metadataSourcesUiSchemaLocation).getURL() + //Detect malformed JSON schema early, during application start up and fail fast with useful exception message + try { + this.jacksonObjectMapper.readValue(this.jsonSchemaUrl, Map) + } + catch (Exception e) { + def msg = """ + An error is detected during JSON parsing => [${e.message}] + ********************************************************** + Offending resource => [${this.jsonSchemaUrl}] + """ + throw new BeanInitializationException(msg.toString(), e) + } } } From 3e95694b5463669912535197b31cdbf8edba3f64 Mon Sep 17 00:00:00 2001 From: Dmitriy Kopylenko Date: Tue, 9 Oct 2018 11:24:35 -0400 Subject: [PATCH 12/33] Refactor tests --- ...efinitionControllerIntegrationTests.groovy | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionControllerIntegrationTests.groovy index 9d804ad57..2695ac803 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionControllerIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionControllerIntegrationTests.groovy @@ -1,5 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.controller +import org.springframework.beans.factory.BeanInitializationException import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.web.client.TestRestTemplate @@ -23,7 +24,6 @@ class MetadataSourcesUiDefinitionControllerIntegrationTests extends Specificatio def "GET Metadata Sources UI definition schema"() { when: 'GET request is made for metadata source UI definition schema' - def result = this.restTemplate.getForEntity(RESOURCE_URI, Object) then: "Request completed successfully" @@ -33,7 +33,7 @@ class MetadataSourcesUiDefinitionControllerIntegrationTests extends Specificatio def "GET Malformed Metadata Sources UI definition schema"() { when: 'GET request is made for malformed metadata source UI definition schema' - configureMalformedJsonInput() + configureMalformedJsonInput(simulateApplicationStartup { false }) def result = this.restTemplate.getForEntity(RESOURCE_URI, Object) then: "Request results in HTTP 500" @@ -42,8 +42,32 @@ class MetadataSourcesUiDefinitionControllerIntegrationTests extends Specificatio result.body.sourceUiSchemaDefinitionFile } - private configureMalformedJsonInput() { + def "Malformed Metadata Sources UI definition schema is detected during application start up"() { + when: 'Application is starting up and malformed JSON schema is detected' + configureMalformedJsonInput(simulateApplicationStartup { true }) + + then: + def ex = thrown(BeanInitializationException) + ex.message.contains('An error is detected during JSON parsing =>') + ex.message.contains('Offending resource =>') + + } + + private configureMalformedJsonInput(boolean simulateApplicationStartup) { controllerUnderTest.metadataSourcesUiSchemaLocation = 'classpath:metadata-sources-ui-schema_MALFORMED.json' - controllerUnderTest.init() + try { + controllerUnderTest.init() + } + catch (Exception e) { + if (simulateApplicationStartup) { + throw e + } + } + + } + + //Just for the nicer, readable, DSL-like + private static boolean simulateApplicationStartup(Closure booleanFlagSupplier) { + booleanFlagSupplier() } } \ No newline at end of file From 6fd5c07c97ca3f22ca974ed58fb0ab2ac71d17e2 Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Tue, 9 Oct 2018 10:43:54 -0700 Subject: [PATCH 13/33] SHIBUI-914 Fixed issue with navigation, fixed schema --- .../model/wizards/metadata-source-wizard.ts | 31 +++ .../container/provider-wizard.component.ts | 3 - .../container/confirm-copy.component.html | 2 +- .../container/new-resolver.component.html | 92 ++++---- .../container/new-resolver.component.ts | 16 +- .../resolver-wizard-step.component.ts | 9 +- .../container/resolver-wizard.component.html | 15 +- .../container/resolver-wizard.component.ts | 36 +-- .../metadata/resolver/effect/wizard.effect.ts | 52 ++++- .../resolver/reducer/draft.reducer.ts | 5 - .../app/schema-form/service/schema.service.ts | 2 +- .../widget/array/array.component.html | 2 +- .../boolean-radio.component.html | 2 +- .../widget/object/object.component.html | 28 ++- ui/src/app/wizard/model/wizard.ts | 3 +- ui/src/app/wizard/reducer/index.ts | 14 +- ui/src/app/wizard/reducer/wizard.reducer.ts | 5 + .../assets/schema/source/metadata-source.json | 214 ++++++++++++++---- 18 files changed, 389 insertions(+), 142 deletions(-) diff --git a/ui/src/app/metadata/domain/model/wizards/metadata-source-wizard.ts b/ui/src/app/metadata/domain/model/wizards/metadata-source-wizard.ts index 7624065f2..7917d7283 100644 --- a/ui/src/app/metadata/domain/model/wizards/metadata-source-wizard.ts +++ b/ui/src/app/metadata/domain/model/wizards/metadata-source-wizard.ts @@ -13,6 +13,15 @@ export class MetadataSourceWizard implements Wizard { fields: [ 'serviceProviderName', 'entityId' + ], + fieldsets: [ + { + type: 'section', + fields: [ + 'serviceProviderName', + 'entityId' + ] + } ] }, { @@ -23,6 +32,20 @@ export class MetadataSourceWizard implements Wizard { fields: [ 'organization', 'contacts' + ], + fieldsets: [ + { + type: 'group', + fields: [ + 'organization' + ] + }, + { + type: 'group', + fields: [ + 'contacts' + ] + } ] }, { @@ -50,6 +73,14 @@ export class MetadataSourceWizard implements Wizard { schema: 'assets/schema/source/metadata-source.json', fields: [ 'logoutEndpoints' + ], + fieldsets: [ + { + type: 'group', + fields: [ + 'logoutEndpoints' + ] + } ] }, { 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 73d092b3e..24140a0a2 100644 --- a/ui/src/app/metadata/provider/container/provider-wizard.component.ts +++ b/ui/src/app/metadata/provider/container/provider-wizard.component.ts @@ -38,9 +38,6 @@ export class ProviderWizardComponent implements OnDestroy { constructor( private store: Store ) { - - - this.store .select(fromWizard.getCurrentWizardSchema) .subscribe(s => { diff --git a/ui/src/app/metadata/resolver/container/confirm-copy.component.html b/ui/src/app/metadata/resolver/container/confirm-copy.component.html index c9df7b998..1259dc568 100644 --- a/ui/src/app/metadata/resolver/container/confirm-copy.component.html +++ b/ui/src/app/metadata/resolver/container/confirm-copy.component.html @@ -10,7 +10,7 @@
-

How are you adding the metadata information?

-
-
-
-
-
- -
-
-  or  -
-
- -
-
-  or  -
-
- + +

How are you adding the metadata information?

+
+
+
+
+
+ +
+
+  or  +
+
+ +
+
+  or  +
+
+ +
+
-
-
+
diff --git a/ui/src/app/metadata/resolver/container/new-resolver.component.ts b/ui/src/app/metadata/resolver/container/new-resolver.component.ts index f7f71726c..71eee6e07 100644 --- a/ui/src/app/metadata/resolver/container/new-resolver.component.ts +++ b/ui/src/app/metadata/resolver/container/new-resolver.component.ts @@ -1,5 +1,7 @@ import { Component } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs'; +import { map, withLatestFrom } from 'rxjs/operators'; @Component({ selector: 'new-resolver-page', @@ -7,5 +9,15 @@ import { ActivatedRoute } from '@angular/router'; styleUrls: ['./new-resolver.component.scss'] }) export class NewResolverComponent { - constructor() {} -} /* istanbul ignore next */ + + canSetNewType$: Observable; + + constructor( + private route: ActivatedRoute + ) { + this.canSetNewType$ = this.route.queryParams.pipe( + withLatestFrom(this.route.url), + map(([params, url]) => this.route.snapshot.firstChild.routeConfig.path !== 'blank' || params.index === 'common') + ); + } +} diff --git a/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.ts b/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.ts index 5cd58b332..b9be4d33b 100644 --- a/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.ts +++ b/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.ts @@ -44,6 +44,8 @@ export class ResolverWizardStepComponent implements OnDestroy { this.definition$ = this.store.select(fromWizard.getWizardDefinition); this.changes$ = this.store.select(fromResolver.getEntityChanges); + // this.schema$.subscribe(s => console.log(s)); + this.validators$ = this.definition$.pipe( map((def) => def.getValidators()) ); @@ -68,9 +70,12 @@ export class ResolverWizardStepComponent implements OnDestroy { this.valueChangeEmitted$.pipe( withLatestFrom(this.definition$), skipWhile(([ changes, definition ]) => !definition || !changes), - map(([ changes, definition ]) => definition.parser(changes)) + map(([ changes, definition ]) => definition.parser(changes.value)) ) - .subscribe(changes => this.store.dispatch(new UpdateChanges(changes))); + .subscribe(changes => { + // console.log(changes); + this.store.dispatch(new UpdateChanges(changes)); + }); this.statusChangeEmitted$.pipe(distinctUntilChanged()).subscribe(errors => this.updateStatus(errors)); diff --git a/ui/src/app/metadata/resolver/container/resolver-wizard.component.html b/ui/src/app/metadata/resolver/container/resolver-wizard.component.html index 1e043a147..f5d57a27d 100644 --- a/ui/src/app/metadata/resolver/container/resolver-wizard.component.html +++ b/ui/src/app/metadata/resolver/container/resolver-wizard.component.html @@ -1,10 +1,5 @@ -
- -
-
- -
-
+ +
+
+ +
\ No newline at end of file diff --git a/ui/src/app/metadata/resolver/container/resolver-wizard.component.ts b/ui/src/app/metadata/resolver/container/resolver-wizard.component.ts index 3b82f735d..b2f4f77d3 100644 --- a/ui/src/app/metadata/resolver/container/resolver-wizard.component.ts +++ b/ui/src/app/metadata/resolver/container/resolver-wizard.component.ts @@ -17,17 +17,13 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { MetadataResolver } from '../../domain/model/metadata-resolver'; import * as fromCollections from '../reducer'; -import * as draftActions from '../action/draft.action'; import { AddResolverRequest } from '../action/collection.action'; import * as fromResolver from '../reducer'; -import { UpdateChanges } from '../action/entity.action'; import { CanComponentDeactivate } from '../../../core/service/can-deactivate.guard'; - -import { UnsavedDialogComponent } from '../component/unsaved-dialog.component'; import { METADATA_SOURCE_WIZARD } from '../wizard-definition'; -import { Wizard } from '../../../wizard/model'; -import { SetDefinition, SetIndex } from '../../../wizard/action/wizard.action'; +import { Wizard, WizardStep } from '../../../wizard/model'; +import { SetDefinition, SetIndex, SetDisabled, ClearWizard } from '../../../wizard/action/wizard.action'; import * as fromWizard from '../../../wizard/reducer'; import { LoadSchemaRequest } from '../../../wizard/action/wizard.action'; @@ -41,6 +37,10 @@ export class ResolverWizardComponent implements OnDestroy, CanComponentDeactivat private ngUnsubscribe: Subject = new Subject(); + nextStep: WizardStep; + previousStep: WizardStep; + currentPage: string; + resolver$: Observable; resolver: MetadataResolver; providerName$: Observable; @@ -75,27 +75,32 @@ export class ResolverWizardComponent implements OnDestroy, CanComponentDeactivat this.store.dispatch(new SetDefinition(this.sourceWizard)); - this.route.params.subscribe(params => { + 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.changes$ = this.store.select(fromResolver.getEntityChanges); + + this.route.queryParams.subscribe(params => { if (params.index) { this.store.dispatch(new SetIndex(params.index)); } else { this.store.dispatch(new SetIndex(this.sourceWizard.steps[0].id)); } }); - - console.log('hi'); } - save(): void { - this.store.dispatch(new AddResolverRequest(this.latest)); + next(): void { + this.go(this.nextStep.id); } - next(index: number): void { - this.go(index.toString()); + previous(): void { + this.go(this.previousStep.id); } - previous(index: number): void { - this.go(index.toString()); + save(): void { + this.store.dispatch(new SetDisabled(true)); + this.store.dispatch(new AddResolverRequest(this.resolver)); } go(index: string): void { @@ -105,6 +110,7 @@ export class ResolverWizardComponent implements OnDestroy, CanComponentDeactivat ngOnDestroy(): void { this.ngUnsubscribe.next(); this.ngUnsubscribe.complete(); + this.store.dispatch(new ClearWizard()); } canDeactivate( diff --git a/ui/src/app/metadata/resolver/effect/wizard.effect.ts b/ui/src/app/metadata/resolver/effect/wizard.effect.ts index 3916e349a..5e6c8d6b1 100644 --- a/ui/src/app/metadata/resolver/effect/wizard.effect.ts +++ b/ui/src/app/metadata/resolver/effect/wizard.effect.ts @@ -1,16 +1,25 @@ import { Injectable } from '@angular/core'; import { Effect, Actions, ofType } from '@ngrx/effects'; - -import { map, switchMap } from 'rxjs/operators'; +import { ActivatedRoute, Router } from '@angular/router'; +import { map, switchMap, tap, withLatestFrom } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; import { UpdateChanges, Clear, ResolverEntityActionTypes } from '../action/entity.action'; -import * as provider from '../action/collection.action'; -import { ResolverCollectionActionTypes } from '../action/collection.action'; +import { + ResolverCollectionActionTypes, + AddResolverSuccess +} from '../action/collection.action'; + +import * as fromResolver from '../reducer'; + import { EntityDraftService } from '../../domain/service/draft.service'; +import { SetIndex, WizardActionTypes } from '../../../wizard/action/wizard.action'; + + @Injectable() export class WizardEffects { @@ -24,13 +33,44 @@ export class WizardEffects { @Effect() addResolverSuccessDiscard$ = this.actions$.pipe( - ofType(ResolverCollectionActionTypes.ADD_RESOLVER_SUCCESS), + ofType(ResolverCollectionActionTypes.ADD_RESOLVER_SUCCESS), map(action => action.payload), map(provider => new Clear()) ); + @Effect({ dispatch: false }) + updateIndexInUrl$ = this.actions$.pipe( + ofType(WizardActionTypes.SET_INDEX), + map(action => action.payload), + tap(index => { + const params = { ...this.activatedRoute.snapshot.queryParams, index }; + this.router.navigate([], { + relativeTo: this.activatedRoute, + queryParams: params + }); + }) + ); + + @Effect({ dispatch: false }) + updateEntityIdInUrl$ = this.actions$.pipe( + ofType(ResolverEntityActionTypes.UPDATE_CHANGES), + map(action => action.payload), + withLatestFrom(this.store.select(fromResolver.getEntityChanges)), + map(([id, changes]) => changes.entityId ), + tap(entityId => { + const params = { ...this.activatedRoute.snapshot.queryParams, entityId }; + this.router.navigate([], { + relativeTo: this.activatedRoute, + queryParams: params + }); + }) + ); + constructor( + private store: Store, private actions$: Actions, - private draftService: EntityDraftService + private draftService: EntityDraftService, + private activatedRoute: ActivatedRoute, + private router: Router ) { } } diff --git a/ui/src/app/metadata/resolver/reducer/draft.reducer.ts b/ui/src/app/metadata/resolver/reducer/draft.reducer.ts index 987883aaf..e642d6b8a 100644 --- a/ui/src/app/metadata/resolver/reducer/draft.reducer.ts +++ b/ui/src/app/metadata/resolver/reducer/draft.reducer.ts @@ -7,12 +7,7 @@ export interface DraftState extends EntityState { selectedDraftId: string | null; } -export function sortByName(a: MetadataResolver, b: MetadataResolver): number { - return a.serviceProviderName.localeCompare(b.serviceProviderName); -} - export const adapter: EntityAdapter = createEntityAdapter({ - sortComparer: sortByName, selectId: (model: MetadataResolver) => model.entityId }); diff --git a/ui/src/app/schema-form/service/schema.service.ts b/ui/src/app/schema-form/service/schema.service.ts index c9c9ea4ca..2114ff283 100644 --- a/ui/src/app/schema-form/service/schema.service.ts +++ b/ui/src/app/schema-form/service/schema.service.ts @@ -15,7 +15,7 @@ export class SchemaService { isRequired(formProperty: any): boolean { let required = false; - if (!formProperty) { + if (!formProperty || !formProperty.parent) { return false; } let requiredFields = formProperty.parent.schema.required || []; diff --git a/ui/src/app/schema-form/widget/array/array.component.html b/ui/src/app/schema-form/widget/array/array.component.html index 9392fe327..ebdf58ab0 100644 --- a/ui/src/app/schema-form/widget/array/array.component.html +++ b/ui/src/app/schema-form/widget/array/array.component.html @@ -3,7 +3,7 @@ -   +   
diff --git a/ui/src/app/schema-form/widget/object/object.component.html b/ui/src/app/schema-form/widget/object/object.component.html index e26b1bc89..8d9a8535f 100644 --- a/ui/src/app/schema-form/widget/object/object.component.html +++ b/ui/src/app/schema-form/widget/object/object.component.html @@ -1,7 +1,23 @@ -
- {{fieldset.title}} -
{{fieldset.description}}
-
- +
+
+ +
+ + {{ fieldset.title }} + +

{{ fieldset.description }}

+
+
+ +
+
+
+
-
\ No newline at end of file +
\ No newline at end of file diff --git a/ui/src/app/wizard/model/wizard.ts b/ui/src/app/wizard/model/wizard.ts index b07e98da4..1a0fa48bc 100644 --- a/ui/src/app/wizard/model/wizard.ts +++ b/ui/src/app/wizard/model/wizard.ts @@ -11,7 +11,8 @@ export interface WizardStep { schema?: string; index: number; locked?: boolean; - fields?: (string | WizardFieldset)[]; + fields?: string[]; + fieldsets?: WizardFieldset[]; summary?: boolean; } diff --git a/ui/src/app/wizard/reducer/index.ts b/ui/src/app/wizard/reducer/index.ts index 8787cad71..6961e5be9 100644 --- a/ui/src/app/wizard/reducer/index.ts +++ b/ui/src/app/wizard/reducer/index.ts @@ -2,6 +2,7 @@ import * as fromRoot from '../../app.reducer'; import * as fromWizard from './wizard.reducer'; import { createFeatureSelector, createSelector } from '@ngrx/store'; import { Wizard, WizardStep } from '../model'; +import { diff } from 'deep-object-diff'; export interface WizardState { wizard: fromWizard.State; @@ -57,13 +58,22 @@ export const getSplitSchema = (schema: any, step: WizardStep) => { return schema; } const keys = Object.keys(schema.properties).filter(key => step.fields.indexOf(key) > -1); - - return { + const required = (schema.required || []).filter(val => keys.indexOf(val) > -1); + let s: any = { ...schema, properties: { ...keys.reduce( (properties, key) => ({ ...properties, [key]: schema.properties[key] }) , {}) } }; + + if (required && required.length) { + s.required = required; + } + if (step.fieldsets) { + s.fieldsets = step.fieldsets; + } + + return s; }; export const getCurrentWizardSchema = createSelector(getWizardIndex, getWizardDefinition, getSchemaPath); diff --git a/ui/src/app/wizard/reducer/wizard.reducer.ts b/ui/src/app/wizard/reducer/wizard.reducer.ts index 5b9e390b5..603fbaeda 100644 --- a/ui/src/app/wizard/reducer/wizard.reducer.ts +++ b/ui/src/app/wizard/reducer/wizard.reducer.ts @@ -104,6 +104,11 @@ export function reducer(state = initialState, action: WizardActionUnion): State } }; } + case WizardActionTypes.CLEAR: { + return { + ...initialState + }; + } default: { return state; } diff --git a/ui/src/assets/schema/source/metadata-source.json b/ui/src/assets/schema/source/metadata-source.json index 98335ffa2..f8aa3c9a0 100644 --- a/ui/src/assets/schema/source/metadata-source.json +++ b/ui/src/assets/schema/source/metadata-source.json @@ -1,5 +1,9 @@ { "type": "object", + "required": [ + "serviceProviderName", + "entityId" + ], "properties": { "entityId": { "title": "label.entity-id", @@ -12,7 +16,7 @@ "type": "string" }, "serviceEnabled": { - "title": "label.enable-this-service-opon-saving", + "title": "label.enable-this-service-upon-saving", "description": "tooltip.enable-this-service-upon-saving", "type": "boolean" }, @@ -30,8 +34,8 @@ "type": "string" }, "url": { - "title": "label.organization-display-name", - "description": "tooltip.organization-display-name", + "title": "label.organization-url", + "description": "tooltip.organization-url", "type": "string" } }, @@ -60,6 +64,28 @@ }, "mdui": { "type": "object", + "widget": { + "id": "fieldset" + }, + "fieldsets": [ + { + "type": "group", + "fields": [ + "displayName", + "informationUrl", + "description" + ] + }, + { + "type": "group", + "fields": [ + "privacyStatementUrl", + "logoUrl", + "logoWidth", + "logoHeight" + ] + } + ], "properties": { "displayName": { "title": "label.display-name", @@ -79,7 +105,10 @@ "description": { "title": "label.description", "description": "tooltip.mdui-description", - "type": "string" + "type": "string", + "widget": { + "id": "textarea" + } }, "logoUrl": { "title": "label.logo-url", @@ -102,23 +131,93 @@ }, "securityInfo": { "type": "object", + "widget": { + "id": "fieldset" + }, + "fieldsets": [ + { + "type": "group", + "fields": [ + "x509CertificateAvailable", + "authenticationRequestsSigned", + "wantAssertionsSigned" + ] + }, + { + "type": "group", + "fields": [ + "x509Certificates" + ] + } + ], "properties": { "x509CertificateAvailable": { "title": "label.is-there-a-x509-certificate", "description": "tooltip.is-there-a-x509-certificate", "type": "boolean", + "widget": { + "id": "boolean-radio" + }, + "oneOf": [ + { + "enum": [ + true + ], + "description": "value.true" + }, + { + "enum": [ + false + ], + "description": "value.false" + } + ], "default": false }, "authenticationRequestsSigned": { "title": "label.authentication-requests-signed", "description": "tooltip.authentication-requests-signed", "type": "boolean", + "widget": { + "id": "boolean-radio" + }, + "oneOf": [ + { + "enum": [ + true + ], + "description": "value.true" + }, + { + "enum": [ + false + ], + "description": "value.false" + } + ], "default": false }, "wantAssertionsSigned": { "title": "label.want-assertions-signed", "description": "tooltip.want-assertions-signed", "type": "boolean", + "widget": { + "id": "boolean-radio" + }, + "oneOf": [ + { + "enum": [ + true + ], + "description": "value.true" + }, + { + "enum": [ + false + ], + "description": "value.false" + } + ], "default": false }, "x509Certificates": { @@ -140,12 +239,27 @@ }, "serviceProviderSsoDescriptor": { "type": "object", + "widget": { + "id": "fieldset" + }, + "fieldsets": [ + { + "type": "group", + "fields": [ + "protocolSupportEnum", + "nameIdFormats" + ] + } + ], "properties": { "protocolSupportEnum": { "title": "label.protocol-support-enumeration", "description": "tooltip.protocol-support-enumeration", "type": "string", "placeholder": "label.select-protocol", + "widget": { + "id": "select" + }, "oneOf": [ { "enum": [ @@ -160,10 +274,10 @@ "description": "SAML 1.1" } ] + }, + "nameIdFormats": { + "$ref": "#/definitions/NameIdFormatList" } - }, - "nameIdFormats": { - "$ref": "#/definitions/NameIdFormatList" } }, "logoutEndpoints": { @@ -293,41 +407,47 @@ } }, "Certificate": { - "name": { - "title": "label.certificate-name-display-only", - "description": "tooltip.certificate-name", - "type": "string" - }, - "type": { - "title": "label.type", - "description": "tooltip.certificate-type", - "type": "string", - "oneOf": [ - { - "enum": [ - "signing" - ], - "description": "value.signing" - }, - { - "enum": [ - "encryption" - ], - "description": "value.encryption" - }, - { - "enum": [ - "both" - ], - "description": "value.both" - } - ], - "default": "both" + "type": "object", + "widget": { + "id": "fieldset" }, - "value": { - "title": "label.certificate", - "description": "tooltip.certificate", - "type": "string" + "properties": { + "name": { + "title": "label.certificate-name-display-only", + "description": "tooltip.certificate-name", + "type": "string" + }, + "type": { + "title": "label.type", + "description": "tooltip.certificate-type", + "type": "string", + "oneOf": [ + { + "enum": [ + "signing" + ], + "description": "value.signing" + }, + { + "enum": [ + "encryption" + ], + "description": "value.encryption" + }, + { + "enum": [ + "both" + ], + "description": "value.both" + } + ], + "default": "both" + }, + "value": { + "title": "label.certificate", + "description": "tooltip.certificate", + "type": "string" + } } }, "AssertionConsumerService": { @@ -410,6 +530,18 @@ "title": "label.new-endpoint", "description": "tooltip.new-endpoint", "type": "object", + "widget": { + "id": "fieldset" + }, + "fieldsets": [ + { + "type": "section", + "fields": [ + "url", + "bindingType" + ] + } + ], "properties": { "url": { "title": "label.url", From 2c95d4d6985f93d4b7296c6bb91d9b2563106e4a Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Thu, 11 Oct 2018 10:51:10 -0700 Subject: [PATCH 14/33] SHIBUI-914 Fixing validation --- ui/package-lock.json | 6 +- ui/package.json | 2 +- .../model/wizards/metadata-source-wizard.ts | 3 +- .../dashboard-resolvers-list.component.ts | 10 ++- .../container/new-resolver.component.ts | 17 +++- .../resolver-wizard-step.component.ts | 7 +- .../container/resolver-wizard.component.html | 4 +- .../container/resolver-wizard.component.ts | 14 +++- .../metadata/resolver/effect/wizard.effect.ts | 6 +- .../widget/button/icon-button.component.ts | 2 +- ui/src/app/wizard/reducer/index.ts | 3 +- .../filebacked-http-common.editor.schema.json | 1 + .../assets/schema/source/metadata-source.json | 81 ++++++++++++++----- 13 files changed, 119 insertions(+), 37 deletions(-) diff --git a/ui/package-lock.json b/ui/package-lock.json index 4850aa82f..89be49e79 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -7956,9 +7956,9 @@ "dev": true }, "ngx-schema-form": { - "version": "2.0.0-beta.3", - "resolved": "https://registry.npmjs.org/ngx-schema-form/-/ngx-schema-form-2.0.0-beta.3.tgz", - "integrity": "sha512-t545QEm16G4kdHFq4VOfmn0ICVwE6v+WAeQ5Bw1ur21U6U6n5q1srImQtefmGdqx69kl1sNwLJLF7Zm8UhJZCg==", + "version": "2.2.0-beta.1", + "resolved": "https://registry.npmjs.org/ngx-schema-form/-/ngx-schema-form-2.2.0-beta.1.tgz", + "integrity": "sha512-tNJR/rFEU2zHAOKOl47ujqM/7y08x8slPYyozgUgKt6aJa5JATqzf2Ee0WAF0fYrJzN4diF1fpNWHYqoA3qDGg==", "requires": { "tslib": "1.9.3" }, diff --git a/ui/package.json b/ui/package.json index b02a3f394..9bf38918e 100644 --- a/ui/package.json +++ b/ui/package.json @@ -34,7 +34,7 @@ "deep-object-diff": "^1.1.0", "file-saver": "^1.3.3", "font-awesome": "^4.7.0", - "ngx-schema-form": "^2.0.0-beta.3", + "ngx-schema-form": "^2.2.0-beta.1", "rxjs": "^6.1.0", "rxjs-compat": "^6.1.0", "xml-formatter": "^1.0.1", diff --git a/ui/src/app/metadata/domain/model/wizards/metadata-source-wizard.ts b/ui/src/app/metadata/domain/model/wizards/metadata-source-wizard.ts index 7917d7283..82a7a2763 100644 --- a/ui/src/app/metadata/domain/model/wizards/metadata-source-wizard.ts +++ b/ui/src/app/metadata/domain/model/wizards/metadata-source-wizard.ts @@ -140,6 +140,7 @@ export class MetadataSourceWizard implements Wizard { } getValidators(...args: any[]): { [key: string]: any } { - return {}; + const validators = {}; + return validators; } } diff --git a/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.ts b/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.ts index b4b5af807..d32b1742c 100644 --- a/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.ts +++ b/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.ts @@ -71,7 +71,15 @@ export class DashboardResolversListComponent implements OnInit { } edit(entity: MetadataEntity): void { - this.router.navigate(['metadata', 'resolver', entity.getId(), entity.isDraft() ? 'wizard' : 'edit']); + if (entity.isDraft()) { + this.router.navigate(['metadata', 'resolver', 'new', 'blank'], { + queryParams: { + entityId: entity.getId() + } + }); + } else { + this.router.navigate(['metadata', 'resolver', entity.getId(), 'edit']); + } } toggleEntity(entity: MetadataEntity): void { diff --git a/ui/src/app/metadata/resolver/container/new-resolver.component.ts b/ui/src/app/metadata/resolver/container/new-resolver.component.ts index 71eee6e07..ce42ea511 100644 --- a/ui/src/app/metadata/resolver/container/new-resolver.component.ts +++ b/ui/src/app/metadata/resolver/container/new-resolver.component.ts @@ -1,7 +1,10 @@ import { Component } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { Observable } from 'rxjs'; -import { map, withLatestFrom } from 'rxjs/operators'; +import { Observable, Subscription } from 'rxjs'; +import { map, withLatestFrom, distinctUntilChanged } from 'rxjs/operators'; +import { SelectDraft } from '../action/draft.action'; +import { Store } from '@ngrx/store'; +import * as fromCollection from '../reducer'; @Component({ selector: 'new-resolver-page', @@ -10,14 +13,22 @@ import { map, withLatestFrom } from 'rxjs/operators'; }) export class NewResolverComponent { + actionsSubscription: Subscription; + canSetNewType$: Observable; constructor( - private route: ActivatedRoute + private route: ActivatedRoute, + private store: Store ) { this.canSetNewType$ = this.route.queryParams.pipe( withLatestFrom(this.route.url), map(([params, url]) => this.route.snapshot.firstChild.routeConfig.path !== 'blank' || params.index === 'common') ); + + this.actionsSubscription = this.route.queryParams.pipe( + distinctUntilChanged(), + map(params => new SelectDraft(params.entityId)) + ).subscribe(store); } } diff --git a/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.ts b/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.ts index b9be4d33b..c916cb689 100644 --- a/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.ts +++ b/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.ts @@ -6,11 +6,9 @@ import { Store } from '@ngrx/store'; import * as fromResolver from '../reducer'; import * as fromWizard from '../../../wizard/reducer'; -import { SetDefinition } from '../../../wizard/action/wizard.action'; import { UpdateStatus, UpdateChanges } from '../action/entity.action'; import { Wizard } from '../../../wizard/model'; import { MetadataResolver } from '../../domain/model'; -import { pick } from '../../../shared/util'; @Component({ selector: 'resolver-wizard-step', @@ -70,10 +68,11 @@ export class ResolverWizardStepComponent implements OnDestroy { this.valueChangeEmitted$.pipe( withLatestFrom(this.definition$), skipWhile(([ changes, definition ]) => !definition || !changes), - map(([ changes, definition ]) => definition.parser(changes.value)) + map(([ changes, definition ]) => definition.parser(changes.value)), + withLatestFrom(this.store.select(fromResolver.getSelectedDraft)), + map(([changes, original]) => ({ ...changes })) ) .subscribe(changes => { - // console.log(changes); this.store.dispatch(new UpdateChanges(changes)); }); diff --git a/ui/src/app/metadata/resolver/container/resolver-wizard.component.html b/ui/src/app/metadata/resolver/container/resolver-wizard.component.html index f5d57a27d..273a327e6 100644 --- a/ui/src/app/metadata/resolver/container/resolver-wizard.component.html +++ b/ui/src/app/metadata/resolver/container/resolver-wizard.component.html @@ -2,4 +2,6 @@
-
\ No newline at end of file +
+
{{ changes$ | async | json }}
+
{{ schema$ | async | json }}
\ No newline at end of file diff --git a/ui/src/app/metadata/resolver/container/resolver-wizard.component.ts b/ui/src/app/metadata/resolver/container/resolver-wizard.component.ts index b2f4f77d3..62d57798f 100644 --- a/ui/src/app/metadata/resolver/container/resolver-wizard.component.ts +++ b/ui/src/app/metadata/resolver/container/resolver-wizard.component.ts @@ -10,7 +10,7 @@ import { RouterStateSnapshot } from '@angular/router'; import { Observable, Subject, of } from 'rxjs'; -import { skipWhile } from 'rxjs/operators'; +import { skipWhile, startWith } from 'rxjs/operators'; import { Store } from '@ngrx/store'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; @@ -55,6 +55,9 @@ export class ResolverWizardComponent implements OnDestroy, CanComponentDeactivat saved$: Observable; saving: boolean; + valid$: Observable; + schema$: Observable; + constructor( private store: Store, private route: ActivatedRoute, @@ -73,6 +76,14 @@ export class ResolverWizardComponent implements OnDestroy, CanComponentDeactivat } }); + this.valid$ = this.store.select(fromResolver.getEntityIsValid); + + this.valid$ + .pipe(startWith(false)) + .subscribe((valid) => { + this.store.dispatch(new SetDisabled(!valid)); + }); + this.store.dispatch(new SetDefinition(this.sourceWizard)); this.store.select(fromWizard.getNext).subscribe(n => this.nextStep = n); @@ -80,6 +91,7 @@ export class ResolverWizardComponent implements OnDestroy, CanComponentDeactivat this.store.select(fromWizard.getWizardIndex).subscribe(i => this.currentPage = i); this.changes$ = this.store.select(fromResolver.getEntityChanges); + this.schema$ = this.store.select(fromWizard.getSchema); this.route.queryParams.subscribe(params => { if (params.index) { diff --git a/ui/src/app/metadata/resolver/effect/wizard.effect.ts b/ui/src/app/metadata/resolver/effect/wizard.effect.ts index 5e6c8d6b1..f53e0fa70 100644 --- a/ui/src/app/metadata/resolver/effect/wizard.effect.ts +++ b/ui/src/app/metadata/resolver/effect/wizard.effect.ts @@ -46,7 +46,8 @@ export class WizardEffects { const params = { ...this.activatedRoute.snapshot.queryParams, index }; this.router.navigate([], { relativeTo: this.activatedRoute, - queryParams: params + queryParams: params, + queryParamsHandling: 'preserve' }); }) ); @@ -61,7 +62,8 @@ export class WizardEffects { const params = { ...this.activatedRoute.snapshot.queryParams, entityId }; this.router.navigate([], { relativeTo: this.activatedRoute, - queryParams: params + queryParams: params, + queryParamsHandling: 'preserve' }); }) ); diff --git a/ui/src/app/schema-form/widget/button/icon-button.component.ts b/ui/src/app/schema-form/widget/button/icon-button.component.ts index 652490804..01efc1f74 100644 --- a/ui/src/app/schema-form/widget/button/icon-button.component.ts +++ b/ui/src/app/schema-form/widget/button/icon-button.component.ts @@ -2,7 +2,7 @@ import { Component, AfterViewInit, ChangeDetectorRef } from '@angular/core'; import { ButtonWidget } from 'ngx-schema-form'; -import { ɵb as ActionRegistry } from 'ngx-schema-form'; +import { ActionRegistry } from 'ngx-schema-form'; import { interval } from 'rxjs'; @Component({ diff --git a/ui/src/app/wizard/reducer/index.ts b/ui/src/app/wizard/reducer/index.ts index 6961e5be9..e3defa126 100644 --- a/ui/src/app/wizard/reducer/index.ts +++ b/ui/src/app/wizard/reducer/index.ts @@ -60,7 +60,8 @@ export const getSplitSchema = (schema: any, step: WizardStep) => { const keys = Object.keys(schema.properties).filter(key => step.fields.indexOf(key) > -1); const required = (schema.required || []).filter(val => keys.indexOf(val) > -1); let s: any = { - ...schema, + type: schema.type, + definitions: schema.definitions, properties: { ...keys.reduce( (properties, key) => ({ ...properties, [key]: schema.properties[key] }) , {}) } diff --git a/ui/src/assets/schema/provider/filebacked-http-common.editor.schema.json b/ui/src/assets/schema/provider/filebacked-http-common.editor.schema.json index 69adf788d..9e1d37308 100644 --- a/ui/src/assets/schema/provider/filebacked-http-common.editor.schema.json +++ b/ui/src/assets/schema/provider/filebacked-http-common.editor.schema.json @@ -50,6 +50,7 @@ ] }, { + "type": "group-lg", "fields": [ "xmlId", "metadataURL", diff --git a/ui/src/assets/schema/source/metadata-source.json b/ui/src/assets/schema/source/metadata-source.json index f8aa3c9a0..4784deeea 100644 --- a/ui/src/assets/schema/source/metadata-source.json +++ b/ui/src/assets/schema/source/metadata-source.json @@ -8,17 +8,22 @@ "entityId": { "title": "label.entity-id", "description": "tooltip.entity-id", - "type": "string" + "type": "string", + "minLength": 1, + "maxLength": 255 }, "serviceProviderName": { "title": "label.service-provider-name", "description": "tooltip.service-provider-name", - "type": "string" + "type": "string", + "minLength": 1, + "maxLength": 255 }, "serviceEnabled": { "title": "label.enable-this-service-upon-saving", "description": "tooltip.enable-this-service-upon-saving", - "type": "boolean" + "type": "boolean", + "default": false }, "organization": { "type": "object", @@ -119,13 +124,15 @@ "title": "label.logo-height", "description": "tooltip.mdui-logo-height", "min": 0, - "type": "integer" + "type": "integer", + "default": 0 }, "logoWidth": { "title": "label.logo-width", "description": "tooltip.mdui-logo-width", "min": 0, - "type": "integer" + "type": "integer", + "default": 0 } } }, @@ -251,6 +258,9 @@ ] } ], + "required": [ + "nameIdFormats" + ], "properties": { "protocolSupportEnum": { "title": "label.protocol-support-enumeration", @@ -361,16 +371,25 @@ "definitions": { "Contact": { "type": "object", + "required": [ + "name", + "type", + "emailAddress" + ], "properties": { "name": { "title": "label.contact-name", "description": "tooltip.contact-name", - "type": "string" + "type": "string", + "minLength": 1, + "maxLength": 255 }, "type": { "title": "label.contact-type", "description": "tooltip.contact-type", "type": "string", + "widget": "select", + "minLength": 1, "oneOf": [ { "enum": [ @@ -402,7 +421,9 @@ "title": "label.contact-email-address", "description": "tooltip.contact-email", "type": "string", - "pattern": "^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$" + "pattern": "^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$", + "minLength": 1, + "maxLength": 255 } } }, @@ -411,16 +432,24 @@ "widget": { "id": "fieldset" }, + "required": [ + "name", + "type", + "value" + ], "properties": { "name": { "title": "label.certificate-name-display-only", "description": "tooltip.certificate-name", - "type": "string" + "type": "string", + "minLength": 1, + "maxLength": 255 }, "type": { "title": "label.type", "description": "tooltip.certificate-type", "type": "string", + "widget": "radio", "oneOf": [ { "enum": [ @@ -446,7 +475,8 @@ "value": { "title": "label.certificate", "description": "tooltip.certificate", - "type": "string" + "type": "string", + "minLength": 1 } } }, @@ -460,7 +490,9 @@ "widget": { "id": "string", "help": "message.valid-url" - } + }, + "minLength": 1, + "maxLength": 255 }, "binding": { "title": "label.assertion-consumer-service-location-binding", @@ -496,13 +528,17 @@ "uniqueItems": true, "items": { "type": "string", - "widget": "datalist", - "data": [ - "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", - "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", - "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", - "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" - ] + "minLength": 1, + "maxLength": 255, + "widget": { + "id": "datalist", + "data": [ + "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", + "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", + "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", + "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" + ] + } }, "default": null }, @@ -515,6 +551,8 @@ "items": { "type": "string", "title": "label.authentication-method", + "minLength": 1, + "maxLength": 255, "widget": { "id": "datalist", "data": [ @@ -542,16 +580,23 @@ ] } ], + "required": [ + "url", + "bindingType" + ], "properties": { "url": { "title": "label.url", "description": "tooltip.url", - "type": "string" + "type": "string", + "minLength": 1, + "maxLength": 255 }, "bindingType": { "title": "label.binding-type", "description": "tooltip.binding-type", "type": "string", + "widget": "select", "oneOf": [ { "enum": [ From 95e93e57936e0df9c617e6ebc5ec4ff11b351f2a Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Fri, 12 Oct 2018 07:23:16 -0700 Subject: [PATCH 15/33] SHIBUI-914 Implemented localstorage drafts --- .../dashboard-resolvers-list.component.ts | 2 +- .../resolver-wizard-step.component.ts | 6 ++-- .../container/resolver-wizard.component.html | 4 +-- .../container/resolver-wizard.component.ts | 28 ++++++++++----- .../effect/draft-collection.effects.ts | 1 + .../metadata/resolver/effect/wizard.effect.ts | 35 +++++++------------ .../app/metadata/resolver/resolver.routing.ts | 4 +-- 7 files changed, 41 insertions(+), 39 deletions(-) diff --git a/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.ts b/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.ts index d32b1742c..b3d331387 100644 --- a/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.ts +++ b/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.ts @@ -72,7 +72,7 @@ export class DashboardResolversListComponent implements OnInit { edit(entity: MetadataEntity): void { if (entity.isDraft()) { - this.router.navigate(['metadata', 'resolver', 'new', 'blank'], { + this.router.navigate(['metadata', 'resolver', 'new'], { queryParams: { entityId: entity.getId() } diff --git a/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.ts b/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.ts index c916cb689..5e09b5215 100644 --- a/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.ts +++ b/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.ts @@ -51,12 +51,14 @@ export class ResolverWizardStepComponent implements OnDestroy { this.model$ = this.schema$.pipe( withLatestFrom( this.store.select(fromWizard.getModel), + this.store.select(fromResolver.getSelectedDraft), this.changes$, this.definition$ ), - map(([schema, model, changes, definition]) => ({ + map(([schema, model, selected, changes, definition]) => ({ model: { ...model, + ...selected, ...changes }, definition @@ -70,7 +72,7 @@ export class ResolverWizardStepComponent implements OnDestroy { skipWhile(([ changes, definition ]) => !definition || !changes), map(([ changes, definition ]) => definition.parser(changes.value)), withLatestFrom(this.store.select(fromResolver.getSelectedDraft)), - map(([changes, original]) => ({ ...changes })) + map(([changes, original]) => ({ ...original, ...changes })) ) .subscribe(changes => { this.store.dispatch(new UpdateChanges(changes)); diff --git a/ui/src/app/metadata/resolver/container/resolver-wizard.component.html b/ui/src/app/metadata/resolver/container/resolver-wizard.component.html index 273a327e6..f5d57a27d 100644 --- a/ui/src/app/metadata/resolver/container/resolver-wizard.component.html +++ b/ui/src/app/metadata/resolver/container/resolver-wizard.component.html @@ -2,6 +2,4 @@
-
-
{{ changes$ | async | json }}
-
{{ schema$ | async | json }}
\ No newline at end of file + \ No newline at end of file diff --git a/ui/src/app/metadata/resolver/container/resolver-wizard.component.ts b/ui/src/app/metadata/resolver/container/resolver-wizard.component.ts index 62d57798f..0a62978ab 100644 --- a/ui/src/app/metadata/resolver/container/resolver-wizard.component.ts +++ b/ui/src/app/metadata/resolver/container/resolver-wizard.component.ts @@ -10,7 +10,7 @@ import { RouterStateSnapshot } from '@angular/router'; import { Observable, Subject, of } from 'rxjs'; -import { skipWhile, startWith } from 'rxjs/operators'; +import { skipWhile, startWith, distinctUntilChanged, map, defaultIfEmpty } from 'rxjs/operators'; import { Store } from '@ngrx/store'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; @@ -93,13 +93,14 @@ export class ResolverWizardComponent implements OnDestroy, CanComponentDeactivat this.changes$ = this.store.select(fromResolver.getEntityChanges); this.schema$ = this.store.select(fromWizard.getSchema); - this.route.queryParams.subscribe(params => { - if (params.index) { - this.store.dispatch(new SetIndex(params.index)); - } else { - this.store.dispatch(new SetIndex(this.sourceWizard.steps[0].id)); - } - }); + this.route.params + .pipe( + map(params => params.index), + distinctUntilChanged() + ) + .subscribe(index => { + this.store.dispatch(new SetIndex(index)); + }); } next(): void { @@ -116,7 +117,16 @@ export class ResolverWizardComponent implements OnDestroy, CanComponentDeactivat } go(index: string): void { - this.store.dispatch(new SetIndex(index)); + this.router.navigate( + [ + '../', + index + ], + { + relativeTo: this.route, + queryParamsHandling: 'preserve' + } + ); } ngOnDestroy(): void { diff --git a/ui/src/app/metadata/resolver/effect/draft-collection.effects.ts b/ui/src/app/metadata/resolver/effect/draft-collection.effects.ts index 49bc7eb5e..73daf4de8 100644 --- a/ui/src/app/metadata/resolver/effect/draft-collection.effects.ts +++ b/ui/src/app/metadata/resolver/effect/draft-collection.effects.ts @@ -66,6 +66,7 @@ export class DraftCollectionEffects { ofType(DraftActionTypes.UPDATE_DRAFT_REQUEST), map(getPayload), switchMap(provider => { + console.log(provider); return this.draftService .update(provider) .pipe( diff --git a/ui/src/app/metadata/resolver/effect/wizard.effect.ts b/ui/src/app/metadata/resolver/effect/wizard.effect.ts index f53e0fa70..3aa75c3f9 100644 --- a/ui/src/app/metadata/resolver/effect/wizard.effect.ts +++ b/ui/src/app/metadata/resolver/effect/wizard.effect.ts @@ -18,17 +18,18 @@ import * as fromResolver from '../reducer'; import { EntityDraftService } from '../../domain/service/draft.service'; import { SetIndex, WizardActionTypes } from '../../../wizard/action/wizard.action'; +import { UpdateDraftRequest } from '../action/draft.action'; @Injectable() export class WizardEffects { - @Effect({ dispatch: false }) + @Effect() updateResolver$ = this.actions$.pipe( ofType(ResolverEntityActionTypes.UPDATE_CHANGES), map(action => action.payload), - switchMap(provider => this.draftService.update(provider)) + map(provider => new UpdateDraftRequest(provider)) ); @Effect() @@ -38,32 +39,22 @@ export class WizardEffects { map(provider => new Clear()) ); - @Effect({ dispatch: false }) - updateIndexInUrl$ = this.actions$.pipe( - ofType(WizardActionTypes.SET_INDEX), - map(action => action.payload), - tap(index => { - const params = { ...this.activatedRoute.snapshot.queryParams, index }; - this.router.navigate([], { - relativeTo: this.activatedRoute, - queryParams: params, - queryParamsHandling: 'preserve' - }); - }) - ); - @Effect({ dispatch: false }) updateEntityIdInUrl$ = this.actions$.pipe( ofType(ResolverEntityActionTypes.UPDATE_CHANGES), map(action => action.payload), - withLatestFrom(this.store.select(fromResolver.getEntityChanges)), - map(([id, changes]) => changes.entityId ), - tap(entityId => { - const params = { ...this.activatedRoute.snapshot.queryParams, entityId }; + withLatestFrom( + this.store.select(fromResolver.getEntityChanges), + this.activatedRoute.queryParams + ), + tap(([id, changes, params]) => { this.router.navigate([], { relativeTo: this.activatedRoute, - queryParams: params, - queryParamsHandling: 'preserve' + queryParams: { + ...params, + entityId: changes.entityId + }, + queryParamsHandling: 'merge' }); }) ); diff --git a/ui/src/app/metadata/resolver/resolver.routing.ts b/ui/src/app/metadata/resolver/resolver.routing.ts index ca1911706..885851148 100644 --- a/ui/src/app/metadata/resolver/resolver.routing.ts +++ b/ui/src/app/metadata/resolver/resolver.routing.ts @@ -23,9 +23,9 @@ export const ResolverRoutes: Routes = [ path: 'new', component: NewResolverComponent, children: [ - { path: '', redirectTo: 'blank', pathMatch: 'prefix' }, + { path: '', redirectTo: 'blank/common', pathMatch: 'prefix' }, { - path: 'blank', + path: 'blank/:index', component: ResolverWizardComponent, canDeactivate: [], children: [ From 262276dad1e15ffb532636b385bca9d41d81abc1 Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Fri, 12 Oct 2018 13:22:58 -0700 Subject: [PATCH 16/33] Fixed draft saving and creation --- .../resources/metadata-sources-ui-schema.json | 294 ++++++++++++++---- .../file-backed-http-metadata-resolver.ts | 6 +- .../domain/model/metadata-resolver.ts | 8 +- .../model/wizards/metadata-source-wizard.ts | 20 +- .../metadata/domain/service/draft.service.ts | 27 +- .../domain/service/resolver.service.ts | 3 +- .../dashboard-resolvers-list.component.ts | 2 +- .../metadata/resolver/action/draft.action.ts | 24 +- .../resolver/container/draft.component.ts | 4 +- .../container/new-resolver.component.ts | 6 +- .../resolver-wizard-step.component.ts | 4 +- .../container/resolver-wizard.component.ts | 1 - .../effect/draft-collection.effects.ts | 42 ++- .../metadata/resolver/effect/wizard.effect.ts | 16 +- .../resolver/reducer/draft.reducer.ts | 6 +- 15 files changed, 335 insertions(+), 128 deletions(-) diff --git a/backend/src/main/resources/metadata-sources-ui-schema.json b/backend/src/main/resources/metadata-sources-ui-schema.json index 5766e298f..4784deeea 100644 --- a/backend/src/main/resources/metadata-sources-ui-schema.json +++ b/backend/src/main/resources/metadata-sources-ui-schema.json @@ -1,20 +1,29 @@ { "type": "object", + "required": [ + "serviceProviderName", + "entityId" + ], "properties": { "entityId": { "title": "label.entity-id", "description": "tooltip.entity-id", - "type": "string" + "type": "string", + "minLength": 1, + "maxLength": 255 }, "serviceProviderName": { "title": "label.service-provider-name", "description": "tooltip.service-provider-name", - "type": "string" + "type": "string", + "minLength": 1, + "maxLength": 255 }, "serviceEnabled": { - "title": "label.enable-this-service-opon-saving", + "title": "label.enable-this-service-upon-saving", "description": "tooltip.enable-this-service-upon-saving", - "type": "boolean" + "type": "boolean", + "default": false }, "organization": { "type": "object", @@ -30,8 +39,8 @@ "type": "string" }, "url": { - "title": "label.organization-display-name", - "description": "tooltip.organization-display-name", + "title": "label.organization-url", + "description": "tooltip.organization-url", "type": "string" } }, @@ -60,6 +69,28 @@ }, "mdui": { "type": "object", + "widget": { + "id": "fieldset" + }, + "fieldsets": [ + { + "type": "group", + "fields": [ + "displayName", + "informationUrl", + "description" + ] + }, + { + "type": "group", + "fields": [ + "privacyStatementUrl", + "logoUrl", + "logoWidth", + "logoHeight" + ] + } + ], "properties": { "displayName": { "title": "label.display-name", @@ -79,7 +110,10 @@ "description": { "title": "label.description", "description": "tooltip.mdui-description", - "type": "string" + "type": "string", + "widget": { + "id": "textarea" + } }, "logoUrl": { "title": "label.logo-url", @@ -90,35 +124,107 @@ "title": "label.logo-height", "description": "tooltip.mdui-logo-height", "min": 0, - "type": "integer" + "type": "integer", + "default": 0 }, "logoWidth": { "title": "label.logo-width", "description": "tooltip.mdui-logo-width", "min": 0, - "type": "integer" + "type": "integer", + "default": 0 } } }, "securityInfo": { "type": "object", + "widget": { + "id": "fieldset" + }, + "fieldsets": [ + { + "type": "group", + "fields": [ + "x509CertificateAvailable", + "authenticationRequestsSigned", + "wantAssertionsSigned" + ] + }, + { + "type": "group", + "fields": [ + "x509Certificates" + ] + } + ], "properties": { "x509CertificateAvailable": { "title": "label.is-there-a-x509-certificate", "description": "tooltip.is-there-a-x509-certificate", "type": "boolean", + "widget": { + "id": "boolean-radio" + }, + "oneOf": [ + { + "enum": [ + true + ], + "description": "value.true" + }, + { + "enum": [ + false + ], + "description": "value.false" + } + ], "default": false }, "authenticationRequestsSigned": { "title": "label.authentication-requests-signed", "description": "tooltip.authentication-requests-signed", "type": "boolean", + "widget": { + "id": "boolean-radio" + }, + "oneOf": [ + { + "enum": [ + true + ], + "description": "value.true" + }, + { + "enum": [ + false + ], + "description": "value.false" + } + ], "default": false }, "wantAssertionsSigned": { "title": "label.want-assertions-signed", "description": "tooltip.want-assertions-signed", "type": "boolean", + "widget": { + "id": "boolean-radio" + }, + "oneOf": [ + { + "enum": [ + true + ], + "description": "value.true" + }, + { + "enum": [ + false + ], + "description": "value.false" + } + ], "default": false }, "x509Certificates": { @@ -140,12 +246,30 @@ }, "serviceProviderSsoDescriptor": { "type": "object", + "widget": { + "id": "fieldset" + }, + "fieldsets": [ + { + "type": "group", + "fields": [ + "protocolSupportEnum", + "nameIdFormats" + ] + } + ], + "required": [ + "nameIdFormats" + ], "properties": { "protocolSupportEnum": { "title": "label.protocol-support-enumeration", "description": "tooltip.protocol-support-enumeration", "type": "string", "placeholder": "label.select-protocol", + "widget": { + "id": "select" + }, "oneOf": [ { "enum": [ @@ -160,10 +284,10 @@ "description": "SAML 1.1" } ] + }, + "nameIdFormats": { + "$ref": "#/definitions/NameIdFormatList" } - }, - "nameIdFormats": { - "$ref": "#/definitions/NameIdFormatList" } }, "logoutEndpoints": { @@ -247,22 +371,31 @@ "definitions": { "Contact": { "type": "object", + "required": [ + "name", + "type", + "emailAddress" + ], "properties": { "name": { "title": "label.contact-name", "description": "tooltip.contact-name", - "type": "string" + "type": "string", + "minLength": 1, + "maxLength": 255 }, "type": { "title": "label.contact-type", "description": "tooltip.contact-type", "type": "string", + "widget": "select", + "minLength": 1, "oneOf": [ { "enum": [ "support" ], - "description": "value.support" + "description": "value.support" }, { "enum": [ @@ -288,46 +421,63 @@ "title": "label.contact-email-address", "description": "tooltip.contact-email", "type": "string", - "pattern": "^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$" + "pattern": "^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$", + "minLength": 1, + "maxLength": 255 } } }, "Certificate": { - "name": { - "title": "label.certificate-name-display-only", - "description": "tooltip.certificate-name", - "type": "string" - }, - "type": { - "title": "label.type", - "description": "tooltip.certificate-type", - "type": "string", - "oneOf": [ - { - "enum": [ - "signing" - ], - "description": "value.signing" - }, - { - "enum": [ - "encryption" - ], - "description": "value.encryption" - }, - { - "enum": [ - "both" - ], - "description": "value.both" - } - ], - "default": "both" + "type": "object", + "widget": { + "id": "fieldset" }, - "value": { - "title": "label.certificate", - "description": "tooltip.certificate", - "type": "string" + "required": [ + "name", + "type", + "value" + ], + "properties": { + "name": { + "title": "label.certificate-name-display-only", + "description": "tooltip.certificate-name", + "type": "string", + "minLength": 1, + "maxLength": 255 + }, + "type": { + "title": "label.type", + "description": "tooltip.certificate-type", + "type": "string", + "widget": "radio", + "oneOf": [ + { + "enum": [ + "signing" + ], + "description": "value.signing" + }, + { + "enum": [ + "encryption" + ], + "description": "value.encryption" + }, + { + "enum": [ + "both" + ], + "description": "value.both" + } + ], + "default": "both" + }, + "value": { + "title": "label.certificate", + "description": "tooltip.certificate", + "type": "string", + "minLength": 1 + } } }, "AssertionConsumerService": { @@ -340,7 +490,9 @@ "widget": { "id": "string", "help": "message.valid-url" - } + }, + "minLength": 1, + "maxLength": 255 }, "binding": { "title": "label.assertion-consumer-service-location-binding", @@ -376,13 +528,17 @@ "uniqueItems": true, "items": { "type": "string", - "widget": "datalist", - "data": [ - "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", - "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", - "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", - "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" - ] + "minLength": 1, + "maxLength": 255, + "widget": { + "id": "datalist", + "data": [ + "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", + "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", + "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", + "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" + ] + } }, "default": null }, @@ -395,6 +551,8 @@ "items": { "type": "string", "title": "label.authentication-method", + "minLength": 1, + "maxLength": 255, "widget": { "id": "datalist", "data": [ @@ -410,16 +568,35 @@ "title": "label.new-endpoint", "description": "tooltip.new-endpoint", "type": "object", + "widget": { + "id": "fieldset" + }, + "fieldsets": [ + { + "type": "section", + "fields": [ + "url", + "bindingType" + ] + } + ], + "required": [ + "url", + "bindingType" + ], "properties": { "url": { "title": "label.url", "description": "tooltip.url", - "type": "string" + "type": "string", + "minLength": 1, + "maxLength": 255 }, "bindingType": { "title": "label.binding-type", "description": "tooltip.binding-type", "type": "string", + "widget": "select", "oneOf": [ { "enum": [ @@ -434,7 +611,6 @@ "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" } ] - } } } diff --git a/ui/src/app/metadata/domain/entity/resolver/file-backed-http-metadata-resolver.ts b/ui/src/app/metadata/domain/entity/resolver/file-backed-http-metadata-resolver.ts index 982d86d35..9d71ee2b5 100644 --- a/ui/src/app/metadata/domain/entity/resolver/file-backed-http-metadata-resolver.ts +++ b/ui/src/app/metadata/domain/entity/resolver/file-backed-http-metadata-resolver.ts @@ -14,7 +14,7 @@ import { MetadataTypes } from '../../domain.type'; import { MetadataEntity } from '../../model/metadata-entity'; export class FileBackedHttpMetadataResolver implements MetadataResolver, MetadataEntity { - id = ''; + resourceId = ''; createdDate?: string; modifiedDate?: string; version: string; @@ -53,7 +53,7 @@ export class FileBackedHttpMetadataResolver implements MetadataResolver, Metadat } getId(): string { - return this.id ? this.id : this.entityId; + return this.resourceId; } getDisplayId(): string { @@ -61,7 +61,7 @@ export class FileBackedHttpMetadataResolver implements MetadataResolver, Metadat } isDraft(): boolean { - return this.id ? false : true; + return this.createdDate ? false : true; } getCreationDate(): Date { diff --git a/ui/src/app/metadata/domain/model/metadata-resolver.ts b/ui/src/app/metadata/domain/model/metadata-resolver.ts index 699ab615f..55776ea03 100644 --- a/ui/src/app/metadata/domain/model/metadata-resolver.ts +++ b/ui/src/app/metadata/domain/model/metadata-resolver.ts @@ -11,8 +11,8 @@ import { } from '../model'; export interface MetadataResolver extends MetadataBase { - resourceId?: string; - entityId: string; + resourceId: string; + entityId?: string; serviceProviderName: string; organization?: Organization; contacts?: Contact[]; @@ -22,6 +22,6 @@ export interface MetadataResolver extends MetadataBase { serviceProviderSsoDescriptor?: IdpSsoDescriptor; logoutEndpoints?: LogoutEndpoint[]; serviceEnabled?: boolean; - relyingPartyOverrides: RelyingPartyOverrides; - attributeRelease: string[]; + relyingPartyOverrides?: RelyingPartyOverrides; + attributeRelease?: string[]; } diff --git a/ui/src/app/metadata/domain/model/wizards/metadata-source-wizard.ts b/ui/src/app/metadata/domain/model/wizards/metadata-source-wizard.ts index 82a7a2763..32e938008 100644 --- a/ui/src/app/metadata/domain/model/wizards/metadata-source-wizard.ts +++ b/ui/src/app/metadata/domain/model/wizards/metadata-source-wizard.ts @@ -9,7 +9,7 @@ export class MetadataSourceWizard implements Wizard { index: 1, id: 'common', label: 'label.resolver-common-attributes', - schema: 'assets/schema/source/metadata-source.json', + schema: '/api/ui/MetadataSources', fields: [ 'serviceProviderName', 'entityId' @@ -28,7 +28,7 @@ export class MetadataSourceWizard implements Wizard { index: 2, id: 'org-info', label: 'label.org-info', - schema: 'assets/schema/source/metadata-source.json', + schema: '/api/ui/MetadataSources', fields: [ 'organization', 'contacts' @@ -52,7 +52,7 @@ export class MetadataSourceWizard implements Wizard { index: 3, id: 'metadata-ui', label: 'label.metadata-ui', - schema: 'assets/schema/source/metadata-source.json', + schema: '/api/ui/MetadataSources', fields: [ 'mdui' ] @@ -61,7 +61,7 @@ export class MetadataSourceWizard implements Wizard { index: 4, id: 'descriptor-info', label: 'label.descriptor-info', - schema: 'assets/schema/source/metadata-source.json', + schema: '/api/ui/MetadataSources', fields: [ 'serviceProviderSsoDescriptor' ] @@ -70,7 +70,7 @@ export class MetadataSourceWizard implements Wizard { index: 5, id: 'logout-endpoints', label: 'label.logout-endpoints', - schema: 'assets/schema/source/metadata-source.json', + schema: '/api/ui/MetadataSources', fields: [ 'logoutEndpoints' ], @@ -87,7 +87,7 @@ export class MetadataSourceWizard implements Wizard { index: 6, id: 'key-info', label: 'label.key-info', - schema: 'assets/schema/source/metadata-source.json', + schema: '/api/ui/MetadataSources', fields: [ 'securityInfo' ] @@ -96,7 +96,7 @@ export class MetadataSourceWizard implements Wizard { index: 7, id: 'assertion', label: 'label.assertion', - schema: 'assets/schema/source/metadata-source.json', + schema: '/api/ui/MetadataSources', fields: [ 'assertionConsumerServices' ] @@ -105,7 +105,7 @@ export class MetadataSourceWizard implements Wizard { index: 8, id: 'relying-party', label: 'label.relying-party', - schema: 'assets/schema/source/metadata-source.json', + schema: '/api/ui/MetadataSources', fields: [ 'relyingPartyOverrides' ] @@ -114,7 +114,7 @@ export class MetadataSourceWizard implements Wizard { index: 9, id: 'attribute', label: 'label.attribute-release', - schema: 'assets/schema/source/metadata-source.json', + schema: '/api/ui/MetadataSources', fields: [ 'attributeRelease' ] @@ -123,7 +123,7 @@ export class MetadataSourceWizard implements Wizard { index: 10, id: 'finish', label: 'label.finished', - schema: 'assets/schema/source/metadata-source.json', + schema: '/api/ui/MetadataSources', fields: [ 'serviceEnabled' ], diff --git a/ui/src/app/metadata/domain/service/draft.service.ts b/ui/src/app/metadata/domain/service/draft.service.ts index a31f8c798..76dcb9f85 100644 --- a/ui/src/app/metadata/domain/service/draft.service.ts +++ b/ui/src/app/metadata/domain/service/draft.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; -import { Observable, of } from 'rxjs'; +import { Observable, of, throwError } from 'rxjs'; import { switchMap } from 'rxjs/operators'; import { MetadataResolver } from '../../domain/model'; @@ -18,12 +18,13 @@ export class EntityDraftService { return of(this.storage.query()); } - find(entityId: string): Observable { + find(id: string, attr: string = 'resourceId'): Observable { + if (!id) { + return throwError(404); + } return this.query().pipe( switchMap( - list => of( - list.find(entity => entity.entityId === entityId) - ) + list => of(list.find(entity => entity[attr] === id)) ) ); } @@ -34,15 +35,19 @@ export class EntityDraftService { } remove(provider: MetadataResolver): Observable { - this.storage.removeByAttr(provider.entityId, 'entityId'); + this.storage.removeByAttr(provider.resourceId, 'resourceId'); return of(provider); } update(provider: MetadataResolver): Observable { - let stored = this.storage.findByAttr(provider.id, 'entityId'); - stored = Object.assign({}, stored, provider); - this.storage.removeByAttr(provider.entityId, 'entityId'); - this.storage.add(stored); - return of(stored); + let stored = this.storage.findByAttr(provider.resourceId, 'resourceId'); + if (stored) { + stored = { ...stored, ...provider }; + this.storage.removeByAttr(provider.resourceId, 'resourceId'); + this.storage.add(stored); + return of(stored); + } else { + return throwError(404); + } } } /* istanbul ignore next */ diff --git a/ui/src/app/metadata/domain/service/resolver.service.ts b/ui/src/app/metadata/domain/service/resolver.service.ts index c3486de76..f0c9f4b3c 100644 --- a/ui/src/app/metadata/domain/service/resolver.service.ts +++ b/ui/src/app/metadata/domain/service/resolver.service.ts @@ -33,7 +33,8 @@ export class ResolverService { } save(provider: MetadataResolver): Observable { - return this.http.post(`${this.base}${this.endpoint}`, provider); + const { id, ...p } = provider; + return this.http.post(`${this.base}${this.endpoint}`, p); } remove(provider: MetadataResolver): Observable { diff --git a/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.ts b/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.ts index b3d331387..8f8caca24 100644 --- a/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.ts +++ b/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.ts @@ -74,7 +74,7 @@ export class DashboardResolversListComponent implements OnInit { if (entity.isDraft()) { this.router.navigate(['metadata', 'resolver', 'new'], { queryParams: { - entityId: entity.getId() + id: entity.getId() } }); } else { diff --git a/ui/src/app/metadata/resolver/action/draft.action.ts b/ui/src/app/metadata/resolver/action/draft.action.ts index f4bba2744..5826b343c 100644 --- a/ui/src/app/metadata/resolver/action/draft.action.ts +++ b/ui/src/app/metadata/resolver/action/draft.action.ts @@ -4,7 +4,9 @@ import { MetadataResolver } from '../../domain/model'; export enum DraftActionTypes { FIND = '[Metadata Draft] Find', - SELECT = '[Metadata Draft] Select', + SELECT_REQUEST = '[Metadata Draft] Select Request', + SELECT_SUCCESS = '[Metadata Draft] Select Success', + SELECT_ERROR = '[Metadata Draft] Select Error', UPDATE_DRAFT_REQUEST = '[Metadata Draft] Update Request', UPDATE_DRAFT_SUCCESS = '[Metadata Draft] Update Success', UPDATE_DRAFT_FAIL = '[Metadata Draft] Update Fail', @@ -19,19 +21,24 @@ export enum DraftActionTypes { REMOVE_DRAFT_FAIL = '[Metadata Draft Collection] Remove Draft Fail' } - -export class FindDraft implements Action { - readonly type = DraftActionTypes.FIND; +export class SelectDraftRequest implements Action { + readonly type = DraftActionTypes.SELECT_REQUEST; constructor(public payload: string) { } } -export class SelectDraft implements Action { - readonly type = DraftActionTypes.SELECT; +export class SelectDraftSuccess implements Action { + readonly type = DraftActionTypes.SELECT_SUCCESS; constructor(public payload: string) { } } +export class SelectDraftError implements Action { + readonly type = DraftActionTypes.SELECT_ERROR; + + constructor() { } +} + export class UpdateDraftRequest implements Action { readonly type = DraftActionTypes.UPDATE_DRAFT_REQUEST; @@ -114,8 +121,9 @@ export type DraftActionsUnion = | RemoveDraftRequest | RemoveDraftSuccess | RemoveDraftFail - | FindDraft - | SelectDraft + | SelectDraftRequest + | SelectDraftSuccess + | SelectDraftError | UpdateDraftRequest | UpdateDraftSuccess | UpdateDraftFail; diff --git a/ui/src/app/metadata/resolver/container/draft.component.ts b/ui/src/app/metadata/resolver/container/draft.component.ts index e8c865c53..687ad2260 100644 --- a/ui/src/app/metadata/resolver/container/draft.component.ts +++ b/ui/src/app/metadata/resolver/container/draft.component.ts @@ -5,7 +5,7 @@ import { distinctUntilChanged, map } from 'rxjs/operators'; import { Store } from '@ngrx/store'; import { NgbPopoverConfig } from '@ng-bootstrap/ng-bootstrap'; -import { SelectDraft } from '../action/draft.action'; +import { SelectDraftRequest } from '../action/draft.action'; import * as fromCollection from '../reducer'; @Component({ @@ -23,7 +23,7 @@ export class DraftComponent implements OnDestroy { ) { this.actionsSubscription = route.params.pipe( distinctUntilChanged(), - map(params => new SelectDraft(params.entityId)) + map(params => new SelectDraftRequest(params.entityId)) ).subscribe(store); } diff --git a/ui/src/app/metadata/resolver/container/new-resolver.component.ts b/ui/src/app/metadata/resolver/container/new-resolver.component.ts index ce42ea511..cf654069c 100644 --- a/ui/src/app/metadata/resolver/container/new-resolver.component.ts +++ b/ui/src/app/metadata/resolver/container/new-resolver.component.ts @@ -2,7 +2,7 @@ import { Component } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable, Subscription } from 'rxjs'; import { map, withLatestFrom, distinctUntilChanged } from 'rxjs/operators'; -import { SelectDraft } from '../action/draft.action'; +import { SelectDraftRequest } from '../action/draft.action'; import { Store } from '@ngrx/store'; import * as fromCollection from '../reducer'; @@ -28,7 +28,7 @@ export class NewResolverComponent { this.actionsSubscription = this.route.queryParams.pipe( distinctUntilChanged(), - map(params => new SelectDraft(params.entityId)) - ).subscribe(store); + map(params => new SelectDraftRequest(params.id)) + ).subscribe(this.store); } } diff --git a/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.ts b/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.ts index 5e09b5215..6b37348aa 100644 --- a/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.ts +++ b/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.ts @@ -75,7 +75,9 @@ export class ResolverWizardStepComponent implements OnDestroy { map(([changes, original]) => ({ ...original, ...changes })) ) .subscribe(changes => { - this.store.dispatch(new UpdateChanges(changes)); + if (changes.resourceId) { + this.store.dispatch(new UpdateChanges(changes)); + } }); this.statusChangeEmitted$.pipe(distinctUntilChanged()).subscribe(errors => this.updateStatus(errors)); diff --git a/ui/src/app/metadata/resolver/container/resolver-wizard.component.ts b/ui/src/app/metadata/resolver/container/resolver-wizard.component.ts index 0a62978ab..1f98d87ed 100644 --- a/ui/src/app/metadata/resolver/container/resolver-wizard.component.ts +++ b/ui/src/app/metadata/resolver/container/resolver-wizard.component.ts @@ -62,7 +62,6 @@ export class ResolverWizardComponent implements OnDestroy, CanComponentDeactivat private store: Store, private route: ActivatedRoute, private router: Router, - private modalService: NgbModal, @Inject(METADATA_SOURCE_WIZARD) private sourceWizard: Wizard ) { this.store diff --git a/ui/src/app/metadata/resolver/effect/draft-collection.effects.ts b/ui/src/app/metadata/resolver/effect/draft-collection.effects.ts index 73daf4de8..8b73d331e 100644 --- a/ui/src/app/metadata/resolver/effect/draft-collection.effects.ts +++ b/ui/src/app/metadata/resolver/effect/draft-collection.effects.ts @@ -5,7 +5,12 @@ import { Router } from '@angular/router'; import { of } from 'rxjs'; import { switchMap, map, catchError, tap } from 'rxjs/operators'; -import { DraftActionTypes } from '../action/draft.action'; +import { + DraftActionTypes, + SelectDraftRequest, + SelectDraftError, + SelectDraftSuccess +} from '../action/draft.action'; import * as actions from '../action/draft.action'; import { EntityDraftService } from '../../domain/service/draft.service'; @@ -66,7 +71,6 @@ export class DraftCollectionEffects { ofType(DraftActionTypes.UPDATE_DRAFT_REQUEST), map(getPayload), switchMap(provider => { - console.log(provider); return this.draftService .update(provider) .pipe( @@ -80,29 +84,49 @@ export class DraftCollectionEffects { @Effect() selectDraft$ = this.actions$.pipe( - ofType(DraftActionTypes.SELECT), + ofType(DraftActionTypes.SELECT_REQUEST), map(getPayload), switchMap(id => this.draftService .find(id) .pipe( - map(p => new actions.FindDraft(p.entityId)) + map(p => new SelectDraftSuccess(p.resourceId)), + catchError(e => of(new SelectDraftError())) ) ) ); @Effect() - removeDraft$ = this.actions$.pipe( - ofType(DraftActionTypes.REMOVE_DRAFT), + selectDraftReload$ = this.actions$.pipe( + ofType(DraftActionTypes.SELECT_REQUEST), map(getPayload), - switchMap(provider => + map(id => new actions.LoadDraftRequest()) + ); + + @Effect() + selectDraftError$ = this.actions$.pipe( + ofType(DraftActionTypes.SELECT_ERROR), + map(getPayload), + switchMap(id => this.draftService - .remove(provider) + .save({ resourceId: `r-${ Date.now() }`, serviceProviderName: '' }) .pipe( - map(p => new actions.RemoveDraftSuccess(p)) + map(p => new SelectDraftRequest(p.resourceId)), + catchError(e => of(new SelectDraftError())) ) ) ); + + @Effect() + removeDraft$ = this.actions$.pipe( + ofType(DraftActionTypes.REMOVE_DRAFT), + map(getPayload), + switchMap(provider => this.draftService.find(provider.entityId, 'entityId').pipe( + switchMap(selected => this.draftService.remove(selected)), + map(p => new actions.RemoveDraftSuccess(p)) + ) + ) + ); @Effect() removeDraftSuccessReload$ = this.actions$.pipe( ofType(DraftActionTypes.REMOVE_DRAFT), diff --git a/ui/src/app/metadata/resolver/effect/wizard.effect.ts b/ui/src/app/metadata/resolver/effect/wizard.effect.ts index 3aa75c3f9..e8ece35c7 100644 --- a/ui/src/app/metadata/resolver/effect/wizard.effect.ts +++ b/ui/src/app/metadata/resolver/effect/wizard.effect.ts @@ -17,8 +17,7 @@ import { import * as fromResolver from '../reducer'; import { EntityDraftService } from '../../domain/service/draft.service'; -import { SetIndex, WizardActionTypes } from '../../../wizard/action/wizard.action'; -import { UpdateDraftRequest } from '../action/draft.action'; +import { UpdateDraftRequest, SelectDraftSuccess, DraftActionTypes } from '../action/draft.action'; @@ -41,19 +40,12 @@ export class WizardEffects { @Effect({ dispatch: false }) updateEntityIdInUrl$ = this.actions$.pipe( - ofType(ResolverEntityActionTypes.UPDATE_CHANGES), + ofType(DraftActionTypes.SELECT_SUCCESS), map(action => action.payload), - withLatestFrom( - this.store.select(fromResolver.getEntityChanges), - this.activatedRoute.queryParams - ), - tap(([id, changes, params]) => { + tap((id) => { this.router.navigate([], { relativeTo: this.activatedRoute, - queryParams: { - ...params, - entityId: changes.entityId - }, + queryParams: { id }, queryParamsHandling: 'merge' }); }) diff --git a/ui/src/app/metadata/resolver/reducer/draft.reducer.ts b/ui/src/app/metadata/resolver/reducer/draft.reducer.ts index e642d6b8a..d49cabf38 100644 --- a/ui/src/app/metadata/resolver/reducer/draft.reducer.ts +++ b/ui/src/app/metadata/resolver/reducer/draft.reducer.ts @@ -8,7 +8,7 @@ export interface DraftState extends EntityState { } export const adapter: EntityAdapter = createEntityAdapter({ - selectId: (model: MetadataResolver) => model.entityId + selectId: (model: MetadataResolver) => model.resourceId }); export const initialState: DraftState = adapter.getInitialState({ @@ -29,10 +29,10 @@ export function reducer(state = initialState, action: DraftActionsUnion): DraftS } case DraftActionTypes.REMOVE_DRAFT_SUCCESS: { - return adapter.removeOne(action.payload.entityId, state); + return adapter.removeOne(action.payload.resourceId, state); } - case DraftActionTypes.SELECT: { + case DraftActionTypes.SELECT_SUCCESS: { return { ...state, selectedDraftId: action.payload, From 7e3f1ec53cee5d620647125f35b33ae64520c4c1 Mon Sep 17 00:00:00 2001 From: Dmitriy Kopylenko Date: Tue, 16 Oct 2018 10:26:21 -0400 Subject: [PATCH 17/33] Refactor schema location --- ...tadataSourcesUiDefinitionController.groovy | 32 +------- ...hemaValidationComponentsConfiguration.java | 19 +++++ ...dataSourcesJsonSchemaResourceLocation.java | 76 +++++++++++++++++++ ...efinitionControllerIntegrationTests.groovy | 8 +- 4 files changed, 105 insertions(+), 30 deletions(-) create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/JsonSchemaValidationComponentsConfiguration.java create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/MetadataSourcesJsonSchemaResourceLocation.java diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionController.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionController.groovy index 74f5e6d8c..9d54e5ac0 100644 --- a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionController.groovy +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionController.groovy @@ -2,7 +2,7 @@ package edu.internet2.tier.shibboleth.admin.ui.controller import com.fasterxml.jackson.databind.ObjectMapper import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomAttributesConfiguration - +import edu.internet2.tier.shibboleth.admin.ui.jsonschema.MetadataSourcesJsonSchemaResourceLocation import org.springframework.beans.factory.BeanInitializationException import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.context.properties.ConfigurationProperties @@ -23,17 +23,10 @@ import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR * @author Bill Smith (wsmith@unicon.net) */ @RestController('/api/ui/MetadataSources') -@ConfigurationProperties('shibui') class MetadataSourcesUiDefinitionController { - //Configured via @ConfigurationProperties with 'shibui.metadata-sources-ui-schema-location' property and default - //value set here if that property is not explicitly set in application.properties - String metadataSourcesUiSchemaLocation = 'classpath:metadata-sources-ui-schema.json' - - URL jsonSchemaUrl - @Autowired - ResourceLoader resourceLoader + MetadataSourcesJsonSchemaResourceLocation jsonSchemaLocation @Autowired ObjectMapper jacksonObjectMapper @@ -44,7 +37,7 @@ class MetadataSourcesUiDefinitionController { @GetMapping ResponseEntity getUiDefinitionJsonSchema() { try { - def parsedJson = jacksonObjectMapper.readValue(this.jsonSchemaUrl, Map) + def parsedJson = jacksonObjectMapper.readValue(this.jsonSchemaLocation.url, Map) def widget = parsedJson["properties"]["attributeRelease"]["widget"] def data = [] customAttributesConfiguration.getAttributes().each { @@ -59,24 +52,7 @@ class MetadataSourcesUiDefinitionController { catch (Exception e) { return ResponseEntity.status(INTERNAL_SERVER_ERROR) .body([jsonParseError : e.getMessage(), - sourceUiSchemaDefinitionFile: this.jsonSchemaUrl]) - } - } - - @PostConstruct - def init() { - jsonSchemaUrl = this.resourceLoader.getResource(this.metadataSourcesUiSchemaLocation).getURL() - //Detect malformed JSON schema early, during application start up and fail fast with useful exception message - try { - this.jacksonObjectMapper.readValue(this.jsonSchemaUrl, Map) - } - catch (Exception e) { - def msg = """ - An error is detected during JSON parsing => [${e.message}] - ********************************************************** - Offending resource => [${this.jsonSchemaUrl}] - """ - throw new BeanInitializationException(msg.toString(), e) + sourceUiSchemaDefinitionFile: this.jsonSchemaLocation.url]) } } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/JsonSchemaValidationComponentsConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/JsonSchemaValidationComponentsConfiguration.java new file mode 100644 index 000000000..7e983edeb --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/JsonSchemaValidationComponentsConfiguration.java @@ -0,0 +1,19 @@ +package edu.internet2.tier.shibboleth.admin.ui.configuration; + +import com.fasterxml.jackson.databind.ObjectMapper; +import edu.internet2.tier.shibboleth.admin.ui.jsonschema.MetadataSourcesJsonSchemaResourceLocation; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ResourceLoader; + +/** + * @author Dmitriy Kopylenko + */ +@Configuration +public class JsonSchemaValidationComponentsConfiguration { + + @Bean + public MetadataSourcesJsonSchemaResourceLocation metadataSourcesJsonSchemaResourceLocation(ResourceLoader resourceLoader, ObjectMapper jacksonMapper) { + return new MetadataSourcesJsonSchemaResourceLocation(resourceLoader, jacksonMapper); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/MetadataSourcesJsonSchemaResourceLocation.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/MetadataSourcesJsonSchemaResourceLocation.java new file mode 100644 index 000000000..d02b09ff0 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/MetadataSourcesJsonSchemaResourceLocation.java @@ -0,0 +1,76 @@ +package edu.internet2.tier.shibboleth.admin.ui.jsonschema; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.core.io.ResourceLoader; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Map; + +/** + * Encapsulates metadata sources JSON schema location. + * + * @author Dmitriy Kopylenko + */ +@ConfigurationProperties("shibui") +public class MetadataSourcesJsonSchemaResourceLocation { + + //Configured via @ConfigurationProperties with 'shibui.metadata-sources-ui-schema-location' property and default + //value set here if that property is not explicitly set in application.properties + private String metadataSourcesUiSchemaLocation = "classpath:metadata-sources-ui-schema.json"; + + private URL jsonSchemaUrl; + + private ResourceLoader resourceLoader; + + private ObjectMapper jacksonMapper; + + + + public MetadataSourcesJsonSchemaResourceLocation(ResourceLoader resourceLoader, ObjectMapper jacksonMapper) { + this.resourceLoader = resourceLoader; + this.jacksonMapper = jacksonMapper; + } + + public void setMetadataSourcesUiSchemaLocation(String metadataSourcesUiSchemaLocation) { + this.metadataSourcesUiSchemaLocation = metadataSourcesUiSchemaLocation; + } + + public URL getUrl() { + return this.jsonSchemaUrl; + } + + public URI getUri() { + try { + return this.jsonSchemaUrl.toURI(); + } + catch (URISyntaxException ex) { + throw new RuntimeException(ex); + } + } + + @PostConstruct + public void init() { + try { + this.jsonSchemaUrl = this.resourceLoader.getResource(this.metadataSourcesUiSchemaLocation).getURL(); + //Detect malformed JSON schema early, during application start up and fail fast with useful exception message + this.jacksonMapper.readValue(this.jsonSchemaUrl, Map.class); + } + catch (Exception ex) { + StringBuilder msg = + new StringBuilder(String.format("An error is detected during JSON parsing => [%s]", ex.getMessage())); + msg.append(String.format("Offending resource => [%s]", this.metadataSourcesUiSchemaLocation)); + + throw new BeanInitializationException(msg.toString(), ex); + } + } +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionControllerIntegrationTests.groovy index 2695ac803..5773cd12b 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionControllerIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionControllerIntegrationTests.groovy @@ -1,5 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.controller +import edu.internet2.tier.shibboleth.admin.ui.jsonschema.MetadataSourcesJsonSchemaResourceLocation import org.springframework.beans.factory.BeanInitializationException import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest @@ -20,6 +21,9 @@ class MetadataSourcesUiDefinitionControllerIntegrationTests extends Specificatio @Autowired MetadataSourcesUiDefinitionController controllerUnderTest + @Autowired + MetadataSourcesJsonSchemaResourceLocation schemaLocation + static RESOURCE_URI = '/api/ui/MetadataSources' def "GET Metadata Sources UI definition schema"() { @@ -54,9 +58,9 @@ class MetadataSourcesUiDefinitionControllerIntegrationTests extends Specificatio } private configureMalformedJsonInput(boolean simulateApplicationStartup) { - controllerUnderTest.metadataSourcesUiSchemaLocation = 'classpath:metadata-sources-ui-schema_MALFORMED.json' + schemaLocation.metadataSourcesUiSchemaLocation = 'classpath:metadata-sources-ui-schema_MALFORMED.json' try { - controllerUnderTest.init() + schemaLocation.init() } catch (Exception e) { if (simulateApplicationStartup) { From 70f51b893904f8ed664e252db3dfaecdb46185f6 Mon Sep 17 00:00:00 2001 From: Dmitriy Kopylenko Date: Tue, 16 Oct 2018 10:43:21 -0400 Subject: [PATCH 18/33] Polishing --- ...etadataSourcesUiDefinitionControllerIntegrationTests.groovy | 3 --- 1 file changed, 3 deletions(-) diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionControllerIntegrationTests.groovy index 5773cd12b..240a08b77 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionControllerIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionControllerIntegrationTests.groovy @@ -18,9 +18,6 @@ class MetadataSourcesUiDefinitionControllerIntegrationTests extends Specificatio @Autowired private TestRestTemplate restTemplate - @Autowired - MetadataSourcesUiDefinitionController controllerUnderTest - @Autowired MetadataSourcesJsonSchemaResourceLocation schemaLocation From 12b959ee638dc1f17a8f8f3895657c4bc009ae99 Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Tue, 16 Oct 2018 11:16:55 -0700 Subject: [PATCH 19/33] SHIBUI-914 Fixed issues with summary page, fixed issues with bindings --- .../main/resources/i18n/messages.properties | 2 + .../resources/i18n/messages_en.properties | 2 + package-lock.json | 3 + .../component/summary-property.component.html | 54 +++++++++ .../summary-property.component.spec.ts | 0 .../component/summary-property.component.ts | 4 + .../component/wizard-summary.component.html} | 0 .../wizard-summary.component.spec.ts} | 0 .../component/wizard-summary.component.ts | 113 ++++++++++++++++++ ui/src/app/metadata/domain/domain.module.ts | 10 +- .../{provider => domain}/model/property.ts | 1 + .../model/wizards/metadata-source-wizard.ts | 47 ++++++-- .../provider-wizard-summary.component.ts | 83 ------------- .../component/summary-property.component.html | 18 --- .../container/provider-wizard.component.html | 4 +- .../app/metadata/provider/provider.module.ts | 8 +- .../container/new-resolver.component.html | 2 +- .../container/new-resolver.component.ts | 15 ++- .../resolver-wizard-step.component.html | 1 + .../resolver-wizard-step.component.ts | 48 +++++++- .../container/resolver-wizard.component.html | 7 +- .../container/resolver-wizard.component.ts | 34 +++++- .../resolver/effect/collection.effects.ts | 11 +- .../effect/draft-collection.effects.ts | 15 ++- ui/src/app/schema-form/registry.ts | 5 +- ui/src/app/schema-form/schema-form.module.ts | 2 + .../widget/array/array.component.html | 49 ++++++-- .../widget/array/array.component.ts | 6 +- .../widget/object/object.component.html | 3 +- .../widget/radio/radio.component.html | 11 ++ .../widget/radio/radio.component.ts | 8 ++ ui/src/app/wizard/model/wizard.ts | 1 + .../assets/schema/source/metadata-source.json | 26 ++-- ui/src/theme/list.scss | 10 +- 34 files changed, 434 insertions(+), 169 deletions(-) create mode 100644 package-lock.json create mode 100644 ui/src/app/metadata/domain/component/summary-property.component.html rename ui/src/app/metadata/{provider => domain}/component/summary-property.component.spec.ts (100%) rename ui/src/app/metadata/{provider => domain}/component/summary-property.component.ts (77%) rename ui/src/app/metadata/{provider/component/provider-wizard-summary.component.html => domain/component/wizard-summary.component.html} (100%) rename ui/src/app/metadata/{provider/component/provider-wizard-summary.component.spec.ts => domain/component/wizard-summary.component.spec.ts} (100%) create mode 100644 ui/src/app/metadata/domain/component/wizard-summary.component.ts rename ui/src/app/metadata/{provider => domain}/model/property.ts (84%) delete mode 100644 ui/src/app/metadata/provider/component/provider-wizard-summary.component.ts delete mode 100644 ui/src/app/metadata/provider/component/summary-property.component.html create mode 100644 ui/src/app/schema-form/widget/radio/radio.component.html create mode 100644 ui/src/app/schema-form/widget/radio/radio.component.ts diff --git a/backend/src/main/resources/i18n/messages.properties b/backend/src/main/resources/i18n/messages.properties index 72d6e706d..9af594612 100644 --- a/backend/src/main/resources/i18n/messages.properties +++ b/backend/src/main/resources/i18n/messages.properties @@ -253,6 +253,8 @@ label.make-default=Make Default label.metadata-provider-name-dashboard-display-only=Metadata Provider Name (Dashboard Display Only) label.default-authentication-methods=Default Authentication Method(s) +label.new-of-type=New { type } + label.filter-name=Filter Name (Dashboard Display Only) label.metadata-filter-name=Metadata Filter Name (Dashboard Display Only) label.filter-enable=Enable this Filter? diff --git a/backend/src/main/resources/i18n/messages_en.properties b/backend/src/main/resources/i18n/messages_en.properties index 6d64dd72a..9ce0bb57e 100644 --- a/backend/src/main/resources/i18n/messages_en.properties +++ b/backend/src/main/resources/i18n/messages_en.properties @@ -235,6 +235,8 @@ label.make-default=Make Default label.metadata-provider-name-dashboard-display-only=Metadata Provider Name (Dashboard Display Only) label.default-authentication-methods=Default Authentication Method(s) +label.new-of-type=New { type } + label.filter-name=Filter Name (Dashboard Display Only) label.metadata-filter-name=Metadata Filter Name (Dashboard Display Only) label.filter-enable=Enable this Filter? diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..48e341a09 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3 @@ +{ + "lockfileVersion": 1 +} diff --git a/ui/src/app/metadata/domain/component/summary-property.component.html b/ui/src/app/metadata/domain/component/summary-property.component.html new file mode 100644 index 000000000..399b0d7fc --- /dev/null +++ b/ui/src/app/metadata/domain/component/summary-property.component.html @@ -0,0 +1,54 @@ +
+ + + + {{ property.name }} + {{ property.value || property.value === false ? property.value : '-' }} + + + + + {{ property.name }} + + +

+ + + + + + + + + + + +
+ +
+ {{ value[prop] }} +
+
+ +

+
    +
  • + {{ item }} +
  • +
+
+
+ + + {{ property.name }} + + + + + +
+
diff --git a/ui/src/app/metadata/provider/component/summary-property.component.spec.ts b/ui/src/app/metadata/domain/component/summary-property.component.spec.ts similarity index 100% rename from ui/src/app/metadata/provider/component/summary-property.component.spec.ts rename to ui/src/app/metadata/domain/component/summary-property.component.spec.ts diff --git a/ui/src/app/metadata/provider/component/summary-property.component.ts b/ui/src/app/metadata/domain/component/summary-property.component.ts similarity index 77% rename from ui/src/app/metadata/provider/component/summary-property.component.ts rename to ui/src/app/metadata/domain/component/summary-property.component.ts index 6dbd0c716..359bd223d 100644 --- a/ui/src/app/metadata/provider/component/summary-property.component.ts +++ b/ui/src/app/metadata/domain/component/summary-property.component.ts @@ -9,5 +9,9 @@ import { Property } from '../model/property'; export class SummaryPropertyComponent { @Input() property: Property; + + getKeys(schema): string[] { + return Object.keys(schema.properties); + } } diff --git a/ui/src/app/metadata/provider/component/provider-wizard-summary.component.html b/ui/src/app/metadata/domain/component/wizard-summary.component.html similarity index 100% rename from ui/src/app/metadata/provider/component/provider-wizard-summary.component.html rename to ui/src/app/metadata/domain/component/wizard-summary.component.html diff --git a/ui/src/app/metadata/provider/component/provider-wizard-summary.component.spec.ts b/ui/src/app/metadata/domain/component/wizard-summary.component.spec.ts similarity index 100% rename from ui/src/app/metadata/provider/component/provider-wizard-summary.component.spec.ts rename to ui/src/app/metadata/domain/component/wizard-summary.component.spec.ts diff --git a/ui/src/app/metadata/domain/component/wizard-summary.component.ts b/ui/src/app/metadata/domain/component/wizard-summary.component.ts new file mode 100644 index 000000000..3d2b14912 --- /dev/null +++ b/ui/src/app/metadata/domain/component/wizard-summary.component.ts @@ -0,0 +1,113 @@ +import { Component, Input, SimpleChanges, OnChanges, Output, EventEmitter } from '@angular/core'; + +import { Wizard, WizardStep } from '../../../wizard/model'; +import { MetadataProvider, MetadataResolver } from '../../domain/model'; +import { Property } from '../model/property'; +import { getSplitSchema } from '../../../wizard/reducer'; + +interface Section { + id: string; + index: number; + label: string; + properties: Property[]; +} + +export function getDefinition(path: string, definitions): any { + let def = path.split('/').pop(); + return definitions[def]; +} + +export function getPropertyItemSchema(items: any, definitions: any): any { + if (!items) { return null; } + return items.$ref ? getDefinition(items.$ref, definitions) : items; +} + +export function getStepProperty(property, model, definitions): Property { + property = property.$ref ? getDefinition(property.$ref, definitions) : property; + return { + name: property.title, + value: model, + type: property.type, + items: getPropertyItemSchema(property.items, definitions), + properties: getStepProperties( + property, + model, + definitions + ) + }; +} + + +export function getStepProperties(schema: any, model: any, definitions: any = {}): Property[] { + if (!schema || !schema.properties) { return []; } + return Object + .keys(schema.properties) + .map(property => { + return getStepProperty( + schema.properties[property], + model[property], + definitions + ); + }); +} + +@Component({ + selector: 'wizard-summary', + templateUrl: './wizard-summary.component.html', + styleUrls: [] +}) + +export class WizardSummaryComponent implements OnChanges { + @Input() summary: { definition: Wizard, schema: { [id: string]: any }, model: any }; + + @Output() onPageSelect: EventEmitter = new EventEmitter(); + + sections: Section[]; + columns: Array
[]; + steps: WizardStep[]; + + constructor() {} + + 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) => { + return ({ + id: step.id, + index: step.index, + label: step.label, + properties: getStepProperties( + schemas[step.id] || getSplitSchema(schemas['summary'], step), + def.formatter(model), + schemas.definitions || schemas.hasOwnProperty('summary') ? schemas['summary'].definitions : {} + ) + }); + } + ); + + 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/domain/domain.module.ts b/ui/src/app/metadata/domain/domain.module.ts index a9120f45d..359f101fb 100644 --- a/ui/src/app/metadata/domain/domain.module.ts +++ b/ui/src/app/metadata/domain/domain.module.ts @@ -15,9 +15,14 @@ import { PreviewDialogComponent } from './component/preview-dialog.component'; import { MetadataFilterService } from './service/filter.service'; import { AttributesService } from './service/attributes.service'; import { I18nModule } from '../../i18n/i18n.module'; +import { WizardSummaryComponent } from './component/wizard-summary.component'; +import { SummaryPropertyComponent } from './component/summary-property.component'; +import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'; export const COMPONENTS = [ - PreviewDialogComponent + PreviewDialogComponent, + WizardSummaryComponent, + SummaryPropertyComponent ]; export const DECLARATIONS = [ @@ -30,7 +35,8 @@ export const DECLARATIONS = [ imports: [ HttpModule, CommonModule, - I18nModule + I18nModule, + NgbPopoverModule ], exports: DECLARATIONS, providers: [] diff --git a/ui/src/app/metadata/provider/model/property.ts b/ui/src/app/metadata/domain/model/property.ts similarity index 84% rename from ui/src/app/metadata/provider/model/property.ts rename to ui/src/app/metadata/domain/model/property.ts index 031dce75c..3bb96b6b8 100644 --- a/ui/src/app/metadata/provider/model/property.ts +++ b/ui/src/app/metadata/domain/model/property.ts @@ -2,5 +2,6 @@ export interface Property { type: string; name: string; value: string[]; + items: Property; properties: Property[]; } diff --git a/ui/src/app/metadata/domain/model/wizards/metadata-source-wizard.ts b/ui/src/app/metadata/domain/model/wizards/metadata-source-wizard.ts index 32e938008..82c929cea 100644 --- a/ui/src/app/metadata/domain/model/wizards/metadata-source-wizard.ts +++ b/ui/src/app/metadata/domain/model/wizards/metadata-source-wizard.ts @@ -9,7 +9,7 @@ export class MetadataSourceWizard implements Wizard { index: 1, id: 'common', label: 'label.resolver-common-attributes', - schema: '/api/ui/MetadataSources', + schema: 'assets/schema/source/metadata-source.json', fields: [ 'serviceProviderName', 'entityId' @@ -17,6 +17,7 @@ export class MetadataSourceWizard implements Wizard { fieldsets: [ { type: 'section', + class: ['col-6'], fields: [ 'serviceProviderName', 'entityId' @@ -28,7 +29,7 @@ export class MetadataSourceWizard implements Wizard { index: 2, id: 'org-info', label: 'label.org-info', - schema: '/api/ui/MetadataSources', + schema: 'assets/schema/source/metadata-source.json', fields: [ 'organization', 'contacts' @@ -52,7 +53,7 @@ export class MetadataSourceWizard implements Wizard { index: 3, id: 'metadata-ui', label: 'label.metadata-ui', - schema: '/api/ui/MetadataSources', + schema: 'assets/schema/source/metadata-source.json', fields: [ 'mdui' ] @@ -61,7 +62,7 @@ export class MetadataSourceWizard implements Wizard { index: 4, id: 'descriptor-info', label: 'label.descriptor-info', - schema: '/api/ui/MetadataSources', + schema: 'assets/schema/source/metadata-source.json', fields: [ 'serviceProviderSsoDescriptor' ] @@ -70,7 +71,7 @@ export class MetadataSourceWizard implements Wizard { index: 5, id: 'logout-endpoints', label: 'label.logout-endpoints', - schema: '/api/ui/MetadataSources', + schema: 'assets/schema/source/metadata-source.json', fields: [ 'logoutEndpoints' ], @@ -87,7 +88,7 @@ export class MetadataSourceWizard implements Wizard { index: 6, id: 'key-info', label: 'label.key-info', - schema: '/api/ui/MetadataSources', + schema: 'assets/schema/source/metadata-source.json', fields: [ 'securityInfo' ] @@ -96,34 +97,58 @@ export class MetadataSourceWizard implements Wizard { index: 7, id: 'assertion', label: 'label.assertion', - schema: '/api/ui/MetadataSources', + schema: 'assets/schema/source/metadata-source.json', fields: [ 'assertionConsumerServices' + ], + fieldsets: [ + { + type: 'group', + fields: [ + 'assertionConsumerServices' + ] + } ] }, { index: 8, id: 'relying-party', label: 'label.relying-party', - schema: '/api/ui/MetadataSources', + schema: 'assets/schema/source/metadata-source.json', fields: [ 'relyingPartyOverrides' + ], + fieldsets: [ + { + type: 'group', + fields: [ + 'relyingPartyOverrides' + ] + } ] }, { index: 9, id: 'attribute', label: 'label.attribute-release', - schema: '/api/ui/MetadataSources', + schema: 'assets/schema/source/metadata-source.json', fields: [ 'attributeRelease' + ], + fieldsets: [ + { + type: 'group', + fields: [ + 'attributeRelease' + ] + } ] }, { index: 10, - id: 'finish', + id: 'summary', label: 'label.finished', - schema: '/api/ui/MetadataSources', + schema: 'assets/schema/source/metadata-source.json', fields: [ 'serviceEnabled' ], 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 deleted file mode 100644 index 8d350d163..000000000 --- a/ui/src/app/metadata/provider/component/provider-wizard-summary.component.ts +++ /dev/null @@ -1,83 +0,0 @@ -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[]; -} - -export 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.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 deleted file mode 100644 index a4b4b57b5..000000000 --- a/ui/src/app/metadata/provider/component/summary-property.component.html +++ /dev/null @@ -1,18 +0,0 @@ -
- - - - {{ property.name }} - {{ property.value || property.value === false ? property.value : '-' }} - - - - - {{ property.name }} - - - - - - -
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 650a2ed82..b1027711b 100644 --- a/ui/src/app/metadata/provider/container/provider-wizard.component.html +++ b/ui/src/app/metadata/provider/container/provider-wizard.component.html @@ -18,11 +18,11 @@ - - +
diff --git a/ui/src/app/metadata/provider/provider.module.ts b/ui/src/app/metadata/provider/provider.module.ts index 550b5c198..1aaedf997 100644 --- a/ui/src/app/metadata/provider/provider.module.ts +++ b/ui/src/app/metadata/provider/provider.module.ts @@ -9,14 +9,12 @@ import { NgbDropdownModule, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; import { ProviderWizardComponent } from './container/provider-wizard.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 { EditorEffects } from './effect/editor.effect'; import { FormModule } from '../../schema-form/schema-form.module'; -import { SummaryPropertyComponent } from './component/summary-property.component'; import { CollectionEffects } from './effect/collection.effect'; import { SharedModule } from '../../shared/shared.module'; import { ProviderEditComponent } from './container/provider-edit.component'; @@ -30,18 +28,17 @@ import { UnsavedProviderComponent } from './component/unsaved-provider.dialog'; import { ContentionModule } from '../../contention/contention.module'; import { DeleteFilterComponent } from './component/delete-filter.component'; import { I18nModule } from '../../i18n/i18n.module'; +import { DomainModule } from '../domain/domain.module'; @NgModule({ declarations: [ ProviderComponent, ProviderWizardComponent, ProviderWizardStepComponent, - ProviderWizardSummaryComponent, ProviderEditComponent, ProviderEditStepComponent, ProviderSelectComponent, ProviderFilterListComponent, - SummaryPropertyComponent, ProviderEditorNavComponent, UnsavedProviderComponent, DeleteFilterComponent @@ -61,7 +58,8 @@ import { I18nModule } from '../../i18n/i18n.module'; ContentionModule, NgbDropdownModule, NgbModalModule, - I18nModule + I18nModule, + DomainModule ], exports: [] }) diff --git a/ui/src/app/metadata/resolver/container/new-resolver.component.html b/ui/src/app/metadata/resolver/container/new-resolver.component.html index 6773f845b..924321a93 100644 --- a/ui/src/app/metadata/resolver/container/new-resolver.component.html +++ b/ui/src/app/metadata/resolver/container/new-resolver.component.html @@ -33,7 +33,7 @@

How are you ad class="btn btn-lg btn-block btn-secondary" aria-label="Create metadata source using the wizard" role="button" - routerLink="blank" + [routerLink]="['./']" routerLinkActive="btn-info"> Create diff --git a/ui/src/app/metadata/resolver/container/new-resolver.component.ts b/ui/src/app/metadata/resolver/container/new-resolver.component.ts index cf654069c..ab4f09103 100644 --- a/ui/src/app/metadata/resolver/container/new-resolver.component.ts +++ b/ui/src/app/metadata/resolver/container/new-resolver.component.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { Observable, Subscription } from 'rxjs'; -import { map, withLatestFrom, distinctUntilChanged } from 'rxjs/operators'; +import { map, startWith, distinctUntilChanged, debounceTime } from 'rxjs/operators'; import { SelectDraftRequest } from '../action/draft.action'; import { Store } from '@ngrx/store'; import * as fromCollection from '../reducer'; @@ -19,11 +19,16 @@ export class NewResolverComponent { constructor( private route: ActivatedRoute, + private router: Router, private store: Store ) { - this.canSetNewType$ = this.route.queryParams.pipe( - withLatestFrom(this.route.url), - map(([params, url]) => this.route.snapshot.firstChild.routeConfig.path !== 'blank' || params.index === 'common') + this.canSetNewType$ = this.router.events.pipe( + startWith(this.route), + debounceTime(10), + map(url => { + let child = this.route.snapshot.firstChild; + return child.routeConfig.path.match('blank').length === 0 || child.params.index === 'common'; + }) ); this.actionsSubscription = this.route.queryParams.pipe( diff --git a/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.html b/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.html index 5d07730fd..f69fbfdf5 100644 --- a/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.html +++ b/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.html @@ -2,6 +2,7 @@ diff --git a/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.ts b/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.ts index 6b37348aa..478d569f4 100644 --- a/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.ts +++ b/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.ts @@ -9,6 +9,9 @@ import * as fromWizard from '../../../wizard/reducer'; import { UpdateStatus, UpdateChanges } from '../action/entity.action'; import { Wizard } from '../../../wizard/model'; import { MetadataResolver } from '../../domain/model'; +import { FormProperty } from 'ngx-schema-form/lib/model/formproperty'; +import { ArrayProperty } from 'ngx-schema-form/lib/model/arrayproperty'; +import { ObjectProperty } from 'ngx-schema-form/lib/model/objectproperty'; @Component({ selector: 'resolver-wizard-step', @@ -35,6 +38,8 @@ export class ResolverWizardStepComponent implements OnDestroy { validators$: Observable<{ [key: string]: any }>; + bindings: any; + constructor( private store: Store, ) { @@ -42,8 +47,6 @@ export class ResolverWizardStepComponent implements OnDestroy { this.definition$ = this.store.select(fromWizard.getWizardDefinition); this.changes$ = this.store.select(fromResolver.getEntityChanges); - // this.schema$.subscribe(s => console.log(s)); - this.validators$ = this.definition$.pipe( map((def) => def.getValidators()) ); @@ -83,6 +86,47 @@ export class ResolverWizardStepComponent implements OnDestroy { this.statusChangeEmitted$.pipe(distinctUntilChanged()).subscribe(errors => this.updateStatus(errors)); this.store.select(fromWizard.getWizardIndex).subscribe(i => this.currentPage = i); + + this.bindings = { + '/securityInfo/x509CertificateAvailable': [ + { + 'input': (event, property: FormProperty) => { + let available = !property.value, + parent = property.parent, + certs = parent.getProperty('x509Certificates'); + if (available && !certs.value.length) { + certs.setValue([ + { + name: '', + type: 'both', + value: '' + } + ], true); + } + + if (!available && certs.value.length > 0) { + certs.setValue([], true); + } + } + } + ], + '/assertionConsumerServices/*/makeDefault': [ + { + 'input': (event, property: FormProperty) => { + let parent = property.parent.parent as ArrayProperty; + let props = parent.properties as ObjectProperty[]; + props.forEach(prop => { + if (prop !== property) { + prop.setValue({ + ...prop.value, + makeDefault: false + }, false); + } + }); + } + } + ] + }; } updateStatus(errors: any): void { diff --git a/ui/src/app/metadata/resolver/container/resolver-wizard.component.html b/ui/src/app/metadata/resolver/container/resolver-wizard.component.html index f5d57a27d..1163fe889 100644 --- a/ui/src/app/metadata/resolver/container/resolver-wizard.component.html +++ b/ui/src/app/metadata/resolver/container/resolver-wizard.component.html @@ -2,4 +2,9 @@
-
\ No newline at end of file + + + \ No newline at end of file diff --git a/ui/src/app/metadata/resolver/container/resolver-wizard.component.ts b/ui/src/app/metadata/resolver/container/resolver-wizard.component.ts index 1f98d87ed..ab205e613 100644 --- a/ui/src/app/metadata/resolver/container/resolver-wizard.component.ts +++ b/ui/src/app/metadata/resolver/container/resolver-wizard.component.ts @@ -9,8 +9,8 @@ import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; -import { Observable, Subject, of } from 'rxjs'; -import { skipWhile, startWith, distinctUntilChanged, map, defaultIfEmpty } from 'rxjs/operators'; +import { Observable, Subject, of, combineLatest as combine } from 'rxjs'; +import { skipWhile, startWith, distinctUntilChanged, map, takeUntil, combineLatest } from 'rxjs/operators'; import { Store } from '@ngrx/store'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; @@ -58,6 +58,8 @@ export class ResolverWizardComponent implements OnDestroy, CanComponentDeactivat valid$: Observable; schema$: Observable; + summary$: Observable<{ definition: Wizard, schema: { [id: string]: any }, model: any }>; + constructor( private store: Store, private route: ActivatedRoute, @@ -92,6 +94,8 @@ export class ResolverWizardComponent implements OnDestroy, CanComponentDeactivat this.changes$ = this.store.select(fromResolver.getEntityChanges); this.schema$ = this.store.select(fromWizard.getSchema); + this.resolver$ = this.store.select(fromCollections.getSelectedDraft); + this.route.params .pipe( map(params => params.index), @@ -100,6 +104,26 @@ export class ResolverWizardComponent implements OnDestroy, CanComponentDeactivat .subscribe(index => { this.store.dispatch(new SetIndex(index)); }); + + this.changes$.pipe( + takeUntil(this.ngUnsubscribe), + skipWhile(() => this.saving), + combineLatest(this.resolver$, (changes, base) => ({ ...base, ...changes })) + ).subscribe(latest => this.latest = latest); + + this.summary$ = combine( + this.store.select(fromWizard.getWizardDefinition), + this.store.select(fromWizard.getSchemaCollection), + this.store.select(fromResolver.getEntityChanges) + ).pipe( + map(([definition, schema, model]) => ( + { + definition, + schema, + model + } + )) + ); } next(): void { @@ -112,7 +136,7 @@ export class ResolverWizardComponent implements OnDestroy, CanComponentDeactivat save(): void { this.store.dispatch(new SetDisabled(true)); - this.store.dispatch(new AddResolverRequest(this.resolver)); + this.store.dispatch(new AddResolverRequest(this.latest)); } go(index: string): void { @@ -128,6 +152,10 @@ export class ResolverWizardComponent implements OnDestroy, CanComponentDeactivat ); } + gotoPage(page: string): void { + this.store.dispatch(new SetIndex(page)); + } + ngOnDestroy(): void { this.ngUnsubscribe.next(); this.ngUnsubscribe.complete(); diff --git a/ui/src/app/metadata/resolver/effect/collection.effects.ts b/ui/src/app/metadata/resolver/effect/collection.effects.ts index cb237aace..bf85c44f1 100644 --- a/ui/src/app/metadata/resolver/effect/collection.effects.ts +++ b/ui/src/app/metadata/resolver/effect/collection.effects.ts @@ -78,10 +78,13 @@ export class ResolverCollectionEffects { addResolverRequest$ = this.actions$.pipe( ofType(ResolverCollectionActionTypes.ADD_RESOLVER), map(action => action.payload), - map(provider => ({ - ...provider, - relyingPartyOverrides: removeNulls(provider.relyingPartyOverrides) - })), + map(provider => { + console.log(provider); + return ({ + ...provider, + relyingPartyOverrides: removeNulls(provider.relyingPartyOverrides) + }); + }), switchMap(provider => this.descriptorService .save(provider) diff --git a/ui/src/app/metadata/resolver/effect/draft-collection.effects.ts b/ui/src/app/metadata/resolver/effect/draft-collection.effects.ts index 8b73d331e..e731d4070 100644 --- a/ui/src/app/metadata/resolver/effect/draft-collection.effects.ts +++ b/ui/src/app/metadata/resolver/effect/draft-collection.effects.ts @@ -13,6 +13,10 @@ import { } from '../action/draft.action'; import * as actions from '../action/draft.action'; import { EntityDraftService } from '../../domain/service/draft.service'; +import * as fromResolver from '../reducer'; +import { Store } from '@ngrx/store'; +import { Clear } from '../action/entity.action'; +import { ClearWizard } from '../../../wizard/action/wizard.action'; export const getPayload = (action: any) => action.payload; @@ -75,7 +79,7 @@ export class DraftCollectionEffects { .update(provider) .pipe( map(p => new actions.UpdateDraftSuccess({ - id: p.entityId, + id: p.resourceId, changes: p })) ); @@ -114,7 +118,11 @@ export class DraftCollectionEffects { map(p => new SelectDraftRequest(p.resourceId)), catchError(e => of(new SelectDraftError())) ) - ) + ), + tap(() => { + this.store.dispatch(new ClearWizard()); + this.store.dispatch(new Clear()); + }) ); @Effect() @@ -137,6 +145,7 @@ export class DraftCollectionEffects { constructor( private draftService: EntityDraftService, private actions$: Actions, - private router: Router + private router: Router, + private store: Store ) { } } /* istanbul ignore next */ diff --git a/ui/src/app/schema-form/registry.ts b/ui/src/app/schema-form/registry.ts index b081e2cfa..f372e88f6 100644 --- a/ui/src/app/schema-form/registry.ts +++ b/ui/src/app/schema-form/registry.ts @@ -18,6 +18,7 @@ import { FilterTargetComponent } from './widget/filter-target/filter-target.comp import { ChecklistComponent } from './widget/check/checklist.component'; import { IconButtonComponent } from './widget/button/icon-button.component'; import { CustomObjectWidget } from './widget/object/object.component'; +import { CustomRadioComponent } from './widget/radio/radio.component'; export class CustomWidgetRegistry extends WidgetRegistry { constructor() { @@ -58,12 +59,12 @@ export class CustomWidgetRegistry extends WidgetRegistry { this.register('icon-button', IconButtonComponent); + this.register('radio', CustomRadioComponent); + /* NGX-Form */ this.register('range', RangeWidget); this.register('file', FileWidget); - this.register('radio', RadioWidget); - this.register('button', ButtonWidget); this.setDefaultWidget(CustomStringComponent); diff --git a/ui/src/app/schema-form/schema-form.module.ts b/ui/src/app/schema-form/schema-form.module.ts index ea7e4289a..8927b9876 100644 --- a/ui/src/app/schema-form/schema-form.module.ts +++ b/ui/src/app/schema-form/schema-form.module.ts @@ -20,6 +20,7 @@ import { ChecklistComponent } from './widget/check/checklist.component'; import { IconButtonComponent } from './widget/button/icon-button.component'; import { I18nModule } from '../i18n/i18n.module'; import { CustomObjectWidget } from './widget/object/object.component'; +import { CustomRadioComponent } from './widget/radio/radio.component'; export const COMPONENTS = [ BooleanRadioComponent, @@ -34,6 +35,7 @@ export const COMPONENTS = [ FilterTargetComponent, ChecklistComponent, IconButtonComponent, + CustomRadioComponent, CustomObjectWidget ]; diff --git a/ui/src/app/schema-form/widget/array/array.component.html b/ui/src/app/schema-form/widget/array/array.component.html index ebdf58ab0..4588dfb4b 100644 --- a/ui/src/app/schema-form/widget/array/array.component.html +++ b/ui/src/app/schema-form/widget/array/array.component.html @@ -13,17 +13,46 @@ -
-
- -
- +
    +
  • +
    + + + +
    + +
    +
    + +
    +
    + + +   + + + +
    +
    +
    + +
    +
    +
    +
    -
-
+ + {{ schema.description }} diff --git a/ui/src/app/schema-form/widget/array/array.component.ts b/ui/src/app/schema-form/widget/array/array.component.ts index 52987cda7..53dfb1798 100644 --- a/ui/src/app/schema-form/widget/array/array.component.ts +++ b/ui/src/app/schema-form/widget/array/array.component.ts @@ -6,4 +6,8 @@ import { ArrayWidget } from 'ngx-schema-form'; selector: 'array-component', templateUrl: `./array.component.html` }) -export class CustomArrayComponent extends ArrayWidget {} +export class CustomArrayComponent extends ArrayWidget { + getListType(property: any): string { + return property.properties.length ? property.properties[0].type : null; + } +} diff --git a/ui/src/app/schema-form/widget/object/object.component.html b/ui/src/app/schema-form/widget/object/object.component.html index 8d9a8535f..83a1f90c9 100644 --- a/ui/src/app/schema-form/widget/object/object.component.html +++ b/ui/src/app/schema-form/widget/object/object.component.html @@ -5,7 +5,8 @@ class="col" [class.d-none]="fieldset.type === 'hidden'" [class.col-lg-6]="fieldset.type === 'group'" - [class.col-lg-12]="fieldset.type === 'group-lg'"> + [class.col-lg-12]="fieldset.type === 'group-lg'" + [ngClass]="fieldset.class"> {{ fieldset.title }} diff --git a/ui/src/app/schema-form/widget/radio/radio.component.html b/ui/src/app/schema-form/widget/radio/radio.component.html new file mode 100644 index 000000000..0f2d396cf --- /dev/null +++ b/ui/src/app/schema-form/widget/radio/radio.component.html @@ -0,0 +1,11 @@ +
+ + {{schema.description}} + +
+ + +
+
+ +
\ No newline at end of file diff --git a/ui/src/app/schema-form/widget/radio/radio.component.ts b/ui/src/app/schema-form/widget/radio/radio.component.ts new file mode 100644 index 000000000..23faf2cc0 --- /dev/null +++ b/ui/src/app/schema-form/widget/radio/radio.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; +import { RadioWidget } from 'ngx-schema-form'; + +@Component({ + selector: 'custom-radio-widget', + templateUrl: `./radio.component.html` +}) +export class CustomRadioComponent extends RadioWidget {} diff --git a/ui/src/app/wizard/model/wizard.ts b/ui/src/app/wizard/model/wizard.ts index 1a0fa48bc..35ae220c6 100644 --- a/ui/src/app/wizard/model/wizard.ts +++ b/ui/src/app/wizard/model/wizard.ts @@ -18,6 +18,7 @@ export interface WizardStep { export interface WizardFieldset { type: string; + class?: string[]; fields: string[]; } diff --git a/ui/src/assets/schema/source/metadata-source.json b/ui/src/assets/schema/source/metadata-source.json index 4784deeea..582af2b42 100644 --- a/ui/src/assets/schema/source/metadata-source.json +++ b/ui/src/assets/schema/source/metadata-source.json @@ -343,16 +343,16 @@ "description": "tooltip.omit-not-before-condition", "default": false }, - "responderId": { - "title": "label.responder-id", - "description": "tooltip.responder-id", - "type": "string" - }, "nameIdFormats": { "$ref": "#/definitions/NameIdFormatList" }, "authenticationMethods": { "$ref": "#/definitions/AuthenticationMethodList" + }, + "responderId": { + "title": "label.responder-id", + "description": "tooltip.responder-id", + "type": "string" } } }, @@ -429,9 +429,6 @@ }, "Certificate": { "type": "object", - "widget": { - "id": "fieldset" - }, "required": [ "name", "type", @@ -449,7 +446,10 @@ "title": "label.type", "description": "tooltip.certificate-type", "type": "string", - "widget": "radio", + "widget": { + "id": "radio", + "class": "form-check-inline" + }, "oneOf": [ { "enum": [ @@ -498,6 +498,7 @@ "title": "label.assertion-consumer-service-location-binding", "description": "tooltip.assertion-consumer-service-location-binding", "type": "string", + "widget": "select", "oneOf": [ { "enum": [ @@ -516,7 +517,8 @@ "makeDefault": { "title": "label.mark-as-default", "description": "tooltip.mark-as-default", - "type": "boolean" + "type": "boolean", + "default": false } } }, @@ -568,12 +570,8 @@ "title": "label.new-endpoint", "description": "tooltip.new-endpoint", "type": "object", - "widget": { - "id": "fieldset" - }, "fieldsets": [ { - "type": "section", "fields": [ "url", "bindingType" diff --git a/ui/src/theme/list.scss b/ui/src/theme/list.scss index 2fa09b033..5acc06806 100644 --- a/ui/src/theme/list.scss +++ b/ui/src/theme/list.scss @@ -13,4 +13,12 @@ min-height: 30px; } } -} \ No newline at end of file +} + +.no-style { + list-style: none; + padding: 0px; + & > li { + padding: 0px; + } +} From ab461393137e2919723fdeb5958daa4a9f8b1232 Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Tue, 16 Oct 2018 13:24:38 -0700 Subject: [PATCH 20/33] SHIBUI-914 Updated editor to use schema --- .../component/editor-nav.component.html} | 18 +- .../component/editor-nav.component.spec.ts} | 0 .../component/editor-nav.component.ts} | 15 +- .../component/unsaved-entity.dialog.html} | 0 .../component/unsaved-entity.dialog.spec.ts} | 0 .../component/unsaved-entity.dialog.ts} | 9 +- .../component/wizard-summary.component.ts | 2 +- ui/src/app/metadata/domain/domain.module.ts | 15 +- ui/src/app/metadata/domain/domain.util.ts | 4 +- .../file-backed-http-metadata-resolver.ts | 4 +- .../domain/model/metadata-resolver.ts | 2 +- .../model/wizards/metadata-source-editor.ts | 147 ++++++++++++++ .../model/wizards/metadata-source-wizard.ts | 1 + .../metadata/domain/service/draft.service.ts | 8 +- .../container/provider-edit.component.html | 9 +- .../container/provider-edit.component.ts | 9 +- .../provider-filter-list.component.html | 8 +- .../provider-filter-list.component.ts | 2 +- .../app/metadata/provider/provider.module.ts | 5 - .../resolver/container/draft.component.html | 1 - .../resolver/container/draft.component.ts | 33 ---- .../resolver/container/editor.component.html | 103 ---------- .../resolver/container/editor.component.ts | 187 ------------------ .../resolver-edit-step.component.html | 8 + .../container/resolver-edit-step.component.ts | 117 +++++++++++ .../container/resolver-edit.component.html | 62 ++++++ .../container/resolver-edit.component.ts | 125 ++++++++++++ .../container/resolver-select.component.html | 3 + .../container/resolver-select.component.ts | 52 +++++ .../resolver-wizard-step.component.ts | 2 +- .../container/resolver.component.html | 1 - .../container/resolver.component.scss | 0 .../container/resolver.component.spec.ts | 49 ----- .../resolver/container/resolver.component.ts | 34 ---- .../effect/draft-collection.effects.ts | 8 +- .../resolver/reducer/draft.reducer.ts | 4 +- .../app/metadata/resolver/resolver.module.ts | 18 +- .../app/metadata/resolver/resolver.routing.ts | 35 ++-- .../metadata/resolver/wizard-definition.ts | 1 + .../widget/object/object.component.html | 2 +- 40 files changed, 597 insertions(+), 506 deletions(-) rename ui/src/app/metadata/{provider/component/provider-editor-nav.component.html => domain/component/editor-nav.component.html} (78%) rename ui/src/app/metadata/{provider/component/provider-editor-nav.component.spec.ts => domain/component/editor-nav.component.spec.ts} (100%) rename ui/src/app/metadata/{provider/component/provider-editor-nav.component.ts => domain/component/editor-nav.component.ts} (73%) rename ui/src/app/metadata/{provider/component/unsaved-provider.dialog.html => domain/component/unsaved-entity.dialog.html} (100%) rename ui/src/app/metadata/{provider/component/unsaved-provider.dialog.spec.ts => domain/component/unsaved-entity.dialog.spec.ts} (100%) rename ui/src/app/metadata/{provider/component/unsaved-provider.dialog.ts => domain/component/unsaved-entity.dialog.ts} (67%) create mode 100644 ui/src/app/metadata/domain/model/wizards/metadata-source-editor.ts delete mode 100644 ui/src/app/metadata/resolver/container/draft.component.html delete mode 100644 ui/src/app/metadata/resolver/container/draft.component.ts delete mode 100644 ui/src/app/metadata/resolver/container/editor.component.html delete mode 100644 ui/src/app/metadata/resolver/container/editor.component.ts create mode 100644 ui/src/app/metadata/resolver/container/resolver-edit-step.component.html create mode 100644 ui/src/app/metadata/resolver/container/resolver-edit-step.component.ts create mode 100644 ui/src/app/metadata/resolver/container/resolver-edit.component.html create mode 100644 ui/src/app/metadata/resolver/container/resolver-edit.component.ts create mode 100644 ui/src/app/metadata/resolver/container/resolver-select.component.html create mode 100644 ui/src/app/metadata/resolver/container/resolver-select.component.ts delete mode 100644 ui/src/app/metadata/resolver/container/resolver.component.html delete mode 100644 ui/src/app/metadata/resolver/container/resolver.component.scss delete mode 100644 ui/src/app/metadata/resolver/container/resolver.component.spec.ts delete mode 100644 ui/src/app/metadata/resolver/container/resolver.component.ts diff --git a/ui/src/app/metadata/provider/component/provider-editor-nav.component.html b/ui/src/app/metadata/domain/component/editor-nav.component.html similarity index 78% rename from ui/src/app/metadata/provider/component/provider-editor-nav.component.html rename to ui/src/app/metadata/domain/component/editor-nav.component.html index 8fbbccefb..354dd0582 100644 --- a/ui/src/app/metadata/provider/component/provider-editor-nav.component.html +++ b/ui/src/app/metadata/domain/component/editor-nav.component.html @@ -20,18 +20,10 @@ [attr.aria-label]="route.label" role="button"> - + - - Filter List - +
@@ -45,13 +37,13 @@ [attr.aria-label]="route.label">
-