From f5c20ad493c4a6dd25b3c022282658359461f153 Mon Sep 17 00:00:00 2001 From: Shad Vider Date: Wed, 17 Mar 2021 12:08:55 -0700 Subject: [PATCH 001/151] added test for added test for 1772, and enabled previously ignored test --- .../EntityDescriptorControllerTests.groovy | 3 +- .../EntityDescriptorRepositoryTest.groovy | 9 +++ .../test/resources/metadata/SHIBUI-1772.xml | 81 +++++++++++++++++++ 3 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 backend/src/test/resources/metadata/SHIBUI-1772.xml diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy index ed721055a..4bf4fe7b2 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy @@ -707,7 +707,6 @@ class EntityDescriptorControllerTests extends Specification { result.andExpect(status().is(403)) } - @Ignore("until we handle the workaround for SHIBUI-1237") def "POST /EntityDescriptor handles XML happily"() { given: def username = 'admin' @@ -785,7 +784,7 @@ class EntityDescriptorControllerTests extends Specification { then: result.andExpect(status().isCreated()) - .andExpect(content().json(expectedJson, true)) + .andExpect(content().json(expectedJson, false)) } def "POST /EntityDescriptor returns error for duplicate entity id"() { diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepositoryTest.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepositoryTest.groovy index 9719ada72..86fe1b74e 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepositoryTest.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepositoryTest.groovy @@ -76,6 +76,15 @@ class EntityDescriptorRepositoryTest extends Specification { noExceptionThrown() } + def "SHIBUI-1772"() { + when: + def input = openSamlObjects.unmarshalFromXml(this.class.getResource('/metadata/SHIBUI-1772.xml').bytes) as EntityDescriptor + entityDescriptorRepository.save(input) + + then: + noExceptionThrown() + } + @TestConfiguration static class Config { @Bean diff --git a/backend/src/test/resources/metadata/SHIBUI-1772.xml b/backend/src/test/resources/metadata/SHIBUI-1772.xml new file mode 100644 index 000000000..e0961dc71 --- /dev/null +++ b/backend/src/test/resources/metadata/SHIBUI-1772.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 67af3237368e + + CN=67af3237368e + MIID6zCCAlOgAwIBAgIJALaLIs8AvRgDMA0GCSqGSIb3DQEBCwUAMBcxFTATBgNV + BAMTDDY3YWYzMjM3MzY4ZTAeFw0xODAxMDkyMDUxMzlaFw0yODAxMDcyMDUxMzla + MBcxFTATBgNVBAMTDDY3YWYzMjM3MzY4ZTCCAaIwDQYJKoZIhvcNAQEBBQADggGP + ADCCAYoCggGBAKes3NT2GqXPDrl8OwrDRARLoBxKJ8ALZ0Ipj1sbYcUEv8aZ4ElR + +G4xf/KL/nF77ctc9WzE/fUlFG55lVKF7l2heKenweXsmeInmOj0MqXpIIofF1G2 + Sh4xUGACQp0OT7ndNlk2+mHK9zuCNA5o3VrAhJuWAruL3nRk14+DE7UkqF+oBZaF + k5/U7W2sa6R1+UHXvSI823O5jAjhSSIfY/N2mME7QogU3P6ETa7BSRS3IvZ8oJvU + T2Ss2ai1qZoiR/0Exeyj5nc026XEAQ675WAcxpAfxu+uDVQ274nPrvOdqUtUeaiU + nc3kIdorXx/m8+WjYT4rGAXx2NbRRC/wRc7ilptLtL/5iHGJkIieFbzHWJXRRMzT + /wWuYM8lr6lqEiOW/2B6z6kQpuGZoinQYBrNU+xYelkfK+jc1IZgR5wOP2aVJ2Gj + DAarnNHkkd36sxOUKw+4f2R2K5obwzHqJl7jySW2p0KldjobQLt0M1DswK0U478R + qqItwPfsTxju0wIDAQABozowODAXBgNVHREEEDAOggw2N2FmMzIzNzM2OGUwHQYD + VR0OBBYEFAvZyUZ8LY3YacgpZSzgQMYSFmptMA0GCSqGSIb3DQEBCwUAA4IBgQCM + IBtz+5mfFtRwmcScYdUzIujCbGfnY6+YsjGMa0jDDfyS1kFfNrj9VRl6uuPO8gWu + BOxR9asm96FOC/tZOQ9SZgQN6ltQsfAMhl+cnI80Qwx8pUXt+iUtM+IgTUFqiv51 + Oy2GyFJGr1QIq4/iF8Q6k27ar73qVGFAYtGgOO5va4hKk6wPgiTHtIqgut6nbWD3 + ErCB28sMnNVYUVhQUxVf9c/9sXC8UIj9Ze0xrclhOeHwrwr9G5EZzyxK8VPFkv0Q + Xrz01BXy/DcbIkcuFFvXE+BMeaziT4FofvvPXWpM1i+FX0wlkCXLj4GNsTuVspfi + T5uUQUubf52kpO5tkPmF9JdM25qmVu5IgpqCMbm1mqrq7qAEFjzOtIp4f0EmHUSy + /BNdo6DT14p56KA0DyrkBCR1UUgzBJcFNJLBLbyEHnHL8fXDcIhjjKJETYoKdsHT + NEi6AB6UOCbYz//CwA96sW154Lu4CpMKrm5fZHLPQ+7UeE9UOQidzHLJ3Vb54BY= + + + + + + + + + + + + + + + + + + + + + + + + + From ba6ffc2dd98dda834022e98705330f5e240f2006 Mon Sep 17 00:00:00 2001 From: chasegawa Date: Thu, 18 Mar 2021 11:48:32 -0700 Subject: [PATCH 002/151] NOJIRA Adding Eclipse bits to ignore --- .gitignore | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gitignore b/.gitignore index 76178c9b0..cb8ad4931 100644 --- a/.gitignore +++ b/.gitignore @@ -400,3 +400,10 @@ rdurable build-no-tests beacon/spring/out +/.classpath + +# Eclipse junk +*.classpath +*.settings +*.project +*bin \ No newline at end of file From 34017c5b11c11acfa03613cebdc8805d451cb690 Mon Sep 17 00:00:00 2001 From: chasegawa Date: Thu, 18 Mar 2021 15:20:44 -0700 Subject: [PATCH 003/151] SHIBUI-1750 Updated controller for entity query to support the MDQ spec. Added the spring etags filter. --- .../ui/configuration/ETagsConfiguration.java | 17 ++++ .../ui/controller/EntitiesController.java | 28 ++++--- .../controller/EntitiesControllerTests.groovy | 83 +++++++++++++++++++ 3 files changed, 115 insertions(+), 13 deletions(-) create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/ETagsConfiguration.java diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/ETagsConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/ETagsConfiguration.java new file mode 100644 index 000000000..a409c8909 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/ETagsConfiguration.java @@ -0,0 +1,17 @@ +package edu.internet2.tier.shibboleth.admin.ui.configuration; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.ShallowEtagHeaderFilter; + +@Configuration +public class ETagsConfiguration { + @Bean + public FilterRegistrationBean shallowEtagHeaderFilter() { + FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>(new ShallowEtagHeaderFilter()); + filterRegistrationBean.addUrlPatterns("/api/entities/*", "/entities/*"); + filterRegistrationBean.setName("etagFilter"); + return filterRegistrationBean; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesController.java index 8a5172fb6..e96b7b5b0 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesController.java @@ -1,16 +1,14 @@ package edu.internet2.tier.shibboleth.admin.ui.controller; -import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; -import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; -import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService; -import net.shibboleth.utilities.java.support.resolver.CriteriaSet; -import net.shibboleth.utilities.java.support.resolver.ResolverException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; + +import javax.servlet.http.HttpServletRequest; + import org.opensaml.core.criterion.EntityIdCriterion; import org.opensaml.core.xml.io.MarshallingException; import org.opensaml.saml.metadata.resolver.MetadataResolver; import org.opensaml.saml.saml2.metadata.EntityDescriptor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; @@ -18,15 +16,19 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; -import javax.servlet.http.HttpServletRequest; -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; +import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService; +import lombok.extern.slf4j.Slf4j; +import net.shibboleth.utilities.java.support.resolver.CriteriaSet; +import net.shibboleth.utilities.java.support.resolver.ResolverException; @Controller -@RequestMapping(value = "/api/entities", method = RequestMethod.GET) +@RequestMapping(value = { "/entities", // per protocol - https://spaces.at.internet2.edu/display/MDQ/Metadata+Query+Protocol + "/api/entities" }, // existing - included to break no existing code + method = RequestMethod.GET) +@Slf4j public class EntitiesController { - private static final Logger logger = LoggerFactory.getLogger(EntitiesController.class); - @Autowired private MetadataResolver metadataResolver; diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerTests.groovy index ea265d46d..e906cb058 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerTests.groovy @@ -64,6 +64,14 @@ class EntitiesControllerTests extends Specification { result.andExpect(status().isNotFound()) } + def 'GET /entities/test'() { + when: + def result = mockMvc.perform(get("/entities/test")) + + then: + result.andExpect(status().isNotFound()) + } + def 'GET /api/entities/test XML'() { when: def result = mockMvc.perform(get("/api/entities/test").header('Accept', 'application/xml')) @@ -72,6 +80,15 @@ class EntitiesControllerTests extends Specification { result.andExpect(status().isNotFound()) } + def 'GET /entities/test XML'() { + when: + def result = mockMvc.perform(get("/entities/test").header('Accept', 'application/xml')) + + then: + result.andExpect(status().isNotFound()) + } + + def 'GET /api/entities/http%3A%2F%2Ftest.scaldingspoon.org%2Ftest1'() { given: def expectedBody = ''' @@ -108,6 +125,42 @@ class EntitiesControllerTests extends Specification { .andExpect(content().json(expectedBody, false)) } + def 'GET /entities/http%3A%2F%2Ftest.scaldingspoon.org%2Ftest1'() { + given: + def expectedBody = ''' + { + "id":null, + "serviceProviderName":null, + "entityId":"http://test.scaldingspoon.org/test1", + "organization":null, + "contacts":null, + "mdui":null, + "serviceProviderSsoDescriptor": { + "protocolSupportEnum":"SAML 2", + "nameIdFormats":["urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"] + }, + "logoutEndpoints":null, + "securityInfo":null, + "assertionConsumerServices":[ + {"locationUrl":"https://test.scaldingspoon.org/test1/acs","binding":"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST","makeDefault":false} + ], + "serviceEnabled":false, + "createdDate":null, + "modifiedDate":null, + "relyingPartyOverrides":{}, + "attributeRelease":["givenName","employeeNumber"] + } + ''' + when: + def result = mockMvc.perform(get('/entities/http%3A%2F%2Ftest.scaldingspoon.org%2Ftest1')) + + then: + def x = content() + result.andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(expectedBody, false)) + } + def 'GET /api/entities/http%3A%2F%2Ftest.scaldingspoon.org%2Ftest1 XML'() { given: def expectedBody = ''' @@ -137,4 +190,34 @@ class EntitiesControllerTests extends Specification { .andExpect(content().contentType('application/xml;charset=ISO-8859-1')) .andExpect(content().xml(expectedBody)) } + + def 'GET /entities/http%3A%2F%2Ftest.scaldingspoon.org%2Ftest1 XML'() { + given: + def expectedBody = ''' + + + + + internal + + + givenName + employeeNumber + + + + + urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified + + + +''' + when: + def result = mockMvc.perform(get('/entities/http%3A%2F%2Ftest.scaldingspoon.org%2Ftest1').header('Accept', 'application/xml')) + + then: + result.andExpect(status().isOk()) + .andExpect(content().contentType('application/xml;charset=ISO-8859-1')) + .andExpect(content().xml(expectedBody)) + } } From e74d6bf86686e1681887c6c14a1fd31d50138d8b Mon Sep 17 00:00:00 2001 From: Shad Vider Date: Wed, 24 Mar 2021 16:31:24 -0700 Subject: [PATCH 004/151] SHIBUI-1791: fixed error reporting failure when saving metadata --- .../support/RestControllersSupport.java | 22 ++++++++++++++++++- .../JsonSchemaValidationFailedException.java | 4 ++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/support/RestControllersSupport.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/support/RestControllersSupport.java index f4bc81df4..1642fe2c9 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/support/RestControllersSupport.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/support/RestControllersSupport.java @@ -1,10 +1,12 @@ package edu.internet2.tier.shibboleth.admin.ui.controller.support; import edu.internet2.tier.shibboleth.admin.ui.controller.ErrorResponse; +import edu.internet2.tier.shibboleth.admin.ui.domain.Attribute; import edu.internet2.tier.shibboleth.admin.ui.domain.exceptions.MetadataFileNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; import edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaValidationFailedException; import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; +import edu.internet2.tier.shibboleth.admin.util.MDDCConstants; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; @@ -13,6 +15,7 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.client.HttpClientErrorException; +import java.util.*; import static org.springframework.http.HttpStatus.BAD_REQUEST; import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; import static org.springframework.http.HttpStatus.NOT_FOUND; @@ -60,6 +63,23 @@ public final ResponseEntity metadataFileNotFoundHandler(MetadataF @ExceptionHandler(JsonSchemaValidationFailedException.class) public final ResponseEntity handleJsonSchemaValidationFailedException(JsonSchemaValidationFailedException ex) { - return ResponseEntity.status(BAD_REQUEST).body(new ErrorResponse("400", String.join("\n", ex.getErrors()))); + return ResponseEntity.status(BAD_REQUEST).body(new ErrorResponse("400", String.join("\n", flattenErrorsList(ex.getErrors())))); + } + + private List flattenErrorsList(List errors) { + List theseErrors = new ArrayList<>(); + processErrorsList(theseErrors, errors); + return theseErrors; + } + + private static void processErrorsList(List outputErrorList, Object errors){ + if(errors instanceof String){ + outputErrorList.add((String)errors); + } + else if(errors instanceof List){ + for(Object error2:(List)errors){ + processErrorsList(outputErrorList, error2); + } + } } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaValidationFailedException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaValidationFailedException.java index fac8d91fb..9ebc11713 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaValidationFailedException.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaValidationFailedException.java @@ -13,9 +13,9 @@ @Getter public class JsonSchemaValidationFailedException extends RuntimeException { - List errors; + List errors; JsonSchemaValidationFailedException(List errors) { - this.errors = (List) errors; + this.errors = errors; } } From 171b3243dbf82caf59fb7419867d1118399005d7 Mon Sep 17 00:00:00 2001 From: chasegawa Date: Wed, 31 Mar 2021 14:50:28 -0700 Subject: [PATCH 005/151] SHIBUI-1786 SHIBUI-1734 Created entity for custom entity attributes and JPA repo for operations --- .../admin/ui/domain/CustomAttribute.java | 44 +++++ .../repository/CustomAttributeRepository.java | 26 +++ .../CustomAttributeRepositoryTests.groovy | 156 ++++++++++++++++++ 3 files changed, 226 insertions(+) create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/CustomAttribute.java create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/CustomAttributeRepository.java create mode 100644 backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/CustomAttributeRepositoryTests.groovy diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/CustomAttribute.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/CustomAttribute.java new file mode 100644 index 000000000..9c1574be6 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/CustomAttribute.java @@ -0,0 +1,44 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import java.util.HashSet; +import java.util.Set; + +import javax.persistence.CollectionTable; +import javax.persistence.Column; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.JoinColumn; + +import org.hibernate.envers.Audited; + +import lombok.Data; + +@Entity(name = "custom_attribute") +@Audited +@Data +public class CustomAttribute { + @Id + @Column(nullable = false) + String name; + + @Column(name = "help_text", nullable = true) + String helpText; + + @Column(name = "attribute_type", nullable = false) + CustomAttributeType attributeType; + + @Column(name = "default_value", nullable = true) + String defaultValue; + + @ElementCollection + @CollectionTable(name = "custom_attr_values", joinColumns = @JoinColumn(name = "name")) + @Column(name = "value", nullable = false) + Set customAttrValues = new HashSet<>(); + + // @TODO: logic to ensure defaultValue matches an item from the list of values when SELECTION_LIST is the type ?? +} + +enum CustomAttributeType { + BOOLEAN, INTEGER, LONG, DOUBLE, DURATION, SELECTION_LIST, SPRING_BEAN_ID +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/CustomAttributeRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/CustomAttributeRepository.java new file mode 100644 index 000000000..edbc4cd9b --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/CustomAttributeRepository.java @@ -0,0 +1,26 @@ +package edu.internet2.tier.shibboleth.admin.ui.repository; + +import java.util.Date; +import java.util.List; + +import javax.transaction.Transactional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import edu.internet2.tier.shibboleth.admin.ui.domain.CustomAttribute; + +/** + * Repository to manage {@link CustomAttribute} instances. + */ +public interface CustomAttributeRepository extends JpaRepository { + + List findAll(); + + CustomAttribute findByName(String name); + + @SuppressWarnings("unchecked") + CustomAttribute save(CustomAttribute attribute); +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/CustomAttributeRepositoryTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/CustomAttributeRepositoryTests.groovy new file mode 100644 index 000000000..9aa14cab0 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/CustomAttributeRepositoryTests.groovy @@ -0,0 +1,156 @@ +package edu.internet2.tier.shibboleth.admin.ui.repository + +import javax.persistence.EntityManager + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.test.context.ContextConfiguration + +import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration +import edu.internet2.tier.shibboleth.admin.ui.domain.CustomAttribute +import spock.lang.Specification + +/** + * Tests to validate the repo and model for custom entity attributes + * @author chasegawa + */ +@DataJpaTest +@ContextConfiguration(classes=[InternationalizationConfiguration]) +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +class CustomAttributeRepositoryTests extends Specification { + + @Autowired + CustomAttributeRepository repo + + @Autowired + EntityManager entityManager + + def "basic CRUD operations validated"() { + given: + def setItems = new HashSet(["val1", "val2", "val3"]) + def ca = new CustomAttribute().with { + it.name = "ca-name" + it.attributeType = "SELECTION_LIST" + it.customAttrValues = setItems + it + } + + // Confirm empty state + when: + def atts = repo.findAll() + + then: + atts.size() == 0 + + // save check + when: + repo.save(ca) + entityManager.flush() + entityManager.clear() + + then: + // save check + def cas = repo.findAll() + cas.size() == 1 + def caFromDb1 = cas.get(0).asType(CustomAttribute) + caFromDb1.equals(ca) == true + + // fetch checks + repo.findByName("not a name") == null + repo.findByName("ca-name").equals(ca) + + // update check + caFromDb1.with { + it.helpText = "some new text that wasn't there before" + } + caFromDb1.equals(ca) == false + + when: + repo.save(caFromDb1) + entityManager.flush() + entityManager.clear() + + then: + def cas2 = repo.findAll() + cas2.size() == 1 + def caFromDb2 = cas2.get(0).asType(CustomAttribute) + caFromDb2.equals(ca) == false + caFromDb2.equals(caFromDb1) == true + + // delete tests + when: + def delByName = new CustomAttribute().with { + it.name = "ca-name" + it + } + repo.delete(delByName) + entityManager.flush() + entityManager.clear() + + then: + repo.findAll().size() == 0 + } + + def "attribute list tests"() { + given: + def setItems2 = new HashSet(["val2", "val1"]) + def setItems3 = new HashSet(["val1", "val2", "val3"]) + def setItems4 = new HashSet(["val1", "val2", "val3", "val4"]) + def ca2 = new CustomAttribute().with { + it.name = "ca-name" + it.attributeType = "SELECTION_LIST" + it.customAttrValues = setItems2 + it + } + def ca3 = new CustomAttribute().with { + it.name = "ca-name" + it.attributeType = "SELECTION_LIST" + it.customAttrValues = setItems3 + it + } + def ca4 = new CustomAttribute().with { + it.name = "ca-name" + it.attributeType = "SELECTION_LIST" + it.customAttrValues = setItems4 + it + } + + when: + repo.save(ca3) + entityManager.flush() + entityManager.clear() + + then: + def cas = repo.findAll() + cas.size() == 1 + def caFromDb = cas.get(0).asType(CustomAttribute) + caFromDb.equals(ca3) == true + + // now update the attribute list items + caFromDb.with { + it.customAttrValues = setItems4 + it + } + repo.save(caFromDb) + entityManager.flush() + entityManager.clear() + + def caFromDb4 = repo.findAll().get(0).asType(CustomAttribute) + caFromDb4.equals(ca4) == true + + // now remove items + caFromDb.with { + it.customAttrValues = setItems2 + it + } + repo.save(caFromDb) + entityManager.flush() + entityManager.clear() + + def caFromDb2 = repo.findAll().get(0).asType(CustomAttribute) + caFromDb2.equals(ca2) == true + } +} \ No newline at end of file From ca9e8b4ebbe1a91b4f453d72c2e566edfb69fd72 Mon Sep 17 00:00:00 2001 From: Jj! Date: Fri, 2 Apr 2021 09:44:27 -0500 Subject: [PATCH 006/151] [SHIBUI-1772] WIP update schema write test --- .../admin/ui/domain/DigestMethod.java | 4 +- .../EntityDescriptorRepresentation.java | 7 +- .../frontend/OrganizationRepresentation.java | 5 ++ .../resources/metadata-sources-ui-schema.json | 6 ++ .../service/AuxiliaryIntegrationTests.groovy | 59 ++++++++++++++ ...on => metadata-sources-ui-schema.json.old} | 0 .../test/resources/metadata/SHIBUI-1723-1.xml | 81 +++++++++++++++++++ 7 files changed, 159 insertions(+), 3 deletions(-) create mode 100644 backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/AuxiliaryIntegrationTests.groovy rename backend/src/test/resources/{metadata-sources-ui-schema.json => metadata-sources-ui-schema.json.old} (100%) create mode 100644 backend/src/test/resources/metadata/SHIBUI-1723-1.xml diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/DigestMethod.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/DigestMethod.java index bb0fe5359..199947f88 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/DigestMethod.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/DigestMethod.java @@ -19,11 +19,11 @@ public DigestMethod(String algorithm) { @Nullable @Override public String getAlgorithm() { - return null; + return this.algorithm; } @Override public void setAlgorithm(@Nullable String value) { - + this.algorithm = value; } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/EntityDescriptorRepresentation.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/EntityDescriptorRepresentation.java index 44f1463c5..2131696c4 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/EntityDescriptorRepresentation.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/EntityDescriptorRepresentation.java @@ -1,5 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.domain.frontend; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import javax.validation.constraints.NotNull; @@ -38,10 +39,12 @@ public EntityDescriptorRepresentation(String id, @NotNull private String entityId; - private OrganizationRepresentation organization; + //TODO: review requirement + private OrganizationRepresentation organization = new OrganizationRepresentation(); private List contacts; + @JsonInclude(JsonInclude.Include.NON_EMPTY) private MduiRepresentation mdui; private ServiceProviderSsoDescriptorRepresentation serviceProviderSsoDescriptor; @@ -58,8 +61,10 @@ public EntityDescriptorRepresentation(String id, private LocalDateTime modifiedDate; + @JsonInclude(JsonInclude.Include.NON_EMPTY) private Map relyingPartyOverrides; + @JsonInclude(JsonInclude.Include.NON_EMPTY) private List attributeRelease; private int version; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/OrganizationRepresentation.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/OrganizationRepresentation.java index 00d98797c..bbcf20a3b 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/OrganizationRepresentation.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/OrganizationRepresentation.java @@ -1,15 +1,20 @@ package edu.internet2.tier.shibboleth.admin.ui.domain.frontend; +import com.fasterxml.jackson.annotation.JsonInclude; + import java.io.Serializable; public class OrganizationRepresentation implements Serializable { private static final long serialVersionUID = 802722455433573538L; + @JsonInclude(JsonInclude.Include.NON_EMPTY) private String name; + @JsonInclude(JsonInclude.Include.NON_EMPTY) private String displayName; + @JsonInclude(JsonInclude.Include.NON_EMPTY) private String url; public String getName() { diff --git a/backend/src/main/resources/metadata-sources-ui-schema.json b/backend/src/main/resources/metadata-sources-ui-schema.json index 9a0b4fb9e..156dffce2 100644 --- a/backend/src/main/resources/metadata-sources-ui-schema.json +++ b/backend/src/main/resources/metadata-sources-ui-schema.json @@ -446,6 +446,12 @@ "urn:oasis:names:tc:SAML:2.0:bindings:SOAP" ], "description": "urn:oasis:names:tc:SAML:2.0:bindings:SOAP" + }, + { + "enum": [ + "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" + ], + "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" } ] } diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/AuxiliaryIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/AuxiliaryIntegrationTests.groovy new file mode 100644 index 000000000..02fde7269 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/AuxiliaryIntegrationTests.groovy @@ -0,0 +1,59 @@ +package edu.internet2.tier.shibboleth.admin.ui.service + +import com.fasterxml.jackson.databind.ObjectMapper +import edu.internet2.tier.shibboleth.admin.ui.configuration.JsonSchemaComponentsConfiguration +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor +import edu.internet2.tier.shibboleth.admin.ui.jsonschema.LowLevelJsonSchemaValidator +import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects +import org.springframework.core.io.DefaultResourceLoader +import org.springframework.core.io.ResourceLoader +import org.springframework.mock.http.MockHttpInputMessage +import spock.lang.Shared +import spock.lang.Specification + +import java.time.LocalDateTime + +class AuxiliaryIntegrationTests extends Specification { + @Shared + OpenSamlObjects openSamlObjects = new OpenSamlObjects().with { + it.init() + it + } + + @Shared + EntityDescriptorService entityDescriptorService + + @Shared + ObjectMapper objectMapper + + @Shared + ResourceLoader resourceLoader + + void setup() { + this.entityDescriptorService = new JPAEntityDescriptorServiceImpl(openSamlObjects, null, null) + this.objectMapper = new ObjectMapper() + this.resourceLoader = new DefaultResourceLoader() + } + + def "SHIBUI-1723: after enabling saved entity descriptor, it should still have valid xml"() { + given: + def entityDescriptor = openSamlObjects.unmarshalFromXml(this.class.getResource('/metadata/SHIBUI-1723-1.xml').bytes) as EntityDescriptor + def entityDescriptorRepresentation = this.entityDescriptorService.createRepresentationFromDescriptor(entityDescriptor).with { + it.serviceProviderName = 'testme' + it.contacts = [] + it.securityInfo.x509Certificates[0].name = 'testcert' + it.createdBy = 'root' + it.setCreatedDate(LocalDateTime.now()) + it.setModifiedDate(LocalDateTime.now()) + it + } + def json = this.objectMapper.writeValueAsString(entityDescriptorRepresentation) + def schemaUri = edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaLocationLookup.metadataSourcesSchema(new JsonSchemaComponentsConfiguration().jsonSchemaResourceLocationRegistry(this.resourceLoader, this.objectMapper)).uri + + when: + LowLevelJsonSchemaValidator.validatePayloadAgainstSchema(new MockHttpInputMessage(json.bytes), schemaUri) + + then: + noExceptionThrown() + } +} diff --git a/backend/src/test/resources/metadata-sources-ui-schema.json b/backend/src/test/resources/metadata-sources-ui-schema.json.old similarity index 100% rename from backend/src/test/resources/metadata-sources-ui-schema.json rename to backend/src/test/resources/metadata-sources-ui-schema.json.old diff --git a/backend/src/test/resources/metadata/SHIBUI-1723-1.xml b/backend/src/test/resources/metadata/SHIBUI-1723-1.xml new file mode 100644 index 000000000..98c1b5a8a --- /dev/null +++ b/backend/src/test/resources/metadata/SHIBUI-1723-1.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 67af3237368e + + CN=67af3237368e + MIID6zCCAlOgAwIBAgIJALaLIs8AvRgDMA0GCSqGSIb3DQEBCwUAMBcxFTATBgNV +BAMTDDY3YWYzMjM3MzY4ZTAeFw0xODAxMDkyMDUxMzlaFw0yODAxMDcyMDUxMzla +MBcxFTATBgNVBAMTDDY3YWYzMjM3MzY4ZTCCAaIwDQYJKoZIhvcNAQEBBQADggGP +ADCCAYoCggGBAKes3NT2GqXPDrl8OwrDRARLoBxKJ8ALZ0Ipj1sbYcUEv8aZ4ElR ++G4xf/KL/nF77ctc9WzE/fUlFG55lVKF7l2heKenweXsmeInmOj0MqXpIIofF1G2 +Sh4xUGACQp0OT7ndNlk2+mHK9zuCNA5o3VrAhJuWAruL3nRk14+DE7UkqF+oBZaF +k5/U7W2sa6R1+UHXvSI823O5jAjhSSIfY/N2mME7QogU3P6ETa7BSRS3IvZ8oJvU +T2Ss2ai1qZoiR/0Exeyj5nc026XEAQ675WAcxpAfxu+uDVQ274nPrvOdqUtUeaiU +nc3kIdorXx/m8+WjYT4rGAXx2NbRRC/wRc7ilptLtL/5iHGJkIieFbzHWJXRRMzT +/wWuYM8lr6lqEiOW/2B6z6kQpuGZoinQYBrNU+xYelkfK+jc1IZgR5wOP2aVJ2Gj +DAarnNHkkd36sxOUKw+4f2R2K5obwzHqJl7jySW2p0KldjobQLt0M1DswK0U478R +qqItwPfsTxju0wIDAQABozowODAXBgNVHREEEDAOggw2N2FmMzIzNzM2OGUwHQYD +VR0OBBYEFAvZyUZ8LY3YacgpZSzgQMYSFmptMA0GCSqGSIb3DQEBCwUAA4IBgQCM +IBtz+5mfFtRwmcScYdUzIujCbGfnY6+YsjGMa0jDDfyS1kFfNrj9VRl6uuPO8gWu +BOxR9asm96FOC/tZOQ9SZgQN6ltQsfAMhl+cnI80Qwx8pUXt+iUtM+IgTUFqiv51 +Oy2GyFJGr1QIq4/iF8Q6k27ar73qVGFAYtGgOO5va4hKk6wPgiTHtIqgut6nbWD3 +ErCB28sMnNVYUVhQUxVf9c/9sXC8UIj9Ze0xrclhOeHwrwr9G5EZzyxK8VPFkv0Q +Xrz01BXy/DcbIkcuFFvXE+BMeaziT4FofvvPXWpM1i+FX0wlkCXLj4GNsTuVspfi +T5uUQUubf52kpO5tkPmF9JdM25qmVu5IgpqCMbm1mqrq7qAEFjzOtIp4f0EmHUSy +/BNdo6DT14p56KA0DyrkBCR1UUgzBJcFNJLBLbyEHnHL8fXDcIhjjKJETYoKdsHT +NEi6AB6UOCbYz//CwA96sW154Lu4CpMKrm5fZHLPQ+7UeE9UOQidzHLJ3Vb54BY= + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 70159e38b1128004b6fa67e33a3e975167e2e921 Mon Sep 17 00:00:00 2001 From: Jj! Date: Fri, 2 Apr 2021 14:23:59 -0500 Subject: [PATCH 007/151] [SHIBUI-1772] new test --- .../admin/ui/domain/VersioningTests.groovy | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/VersioningTests.groovy diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/VersioningTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/VersioningTests.groovy new file mode 100644 index 000000000..649d8d5a4 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/VersioningTests.groovy @@ -0,0 +1,25 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain + +import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects +import spock.lang.Shared +import spock.lang.Specification + +class VersioningTests extends Specification{ + @Shared + OpenSamlObjects openSamlObjects + + def setup() { + this.openSamlObjects = new OpenSamlObjects() + this.openSamlObjects.init() + } + + def "test that two loaded entity descriptors gets the same version when load from the same file"() { + given: + def xmlBytes = this.class.getResource('/metadata/SHIBUI-1723-1.xml').bytes + def ed1 = openSamlObjects.unmarshalFromXml(xmlBytes) + def ed2 = openSamlObjects.unmarshalFromXml(xmlBytes) + + expect: + ed1.hashCode() == ed2.hashCode() + } +} From 9f8f007878d0d09039899ef7c57c598c266effae Mon Sep 17 00:00:00 2001 From: Jj! Date: Mon, 5 Apr 2021 10:16:40 -0500 Subject: [PATCH 008/151] [SHIBUI-1772] update test (missed field) hashcode enhancement for request initiator --- .../shibboleth/admin/ui/domain/RequestInitiator.java | 11 ++++++++++- .../shibboleth/admin/ui/domain/VersioningTests.groovy | 3 +++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RequestInitiator.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RequestInitiator.java index dbd667ff9..e4d87f302 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RequestInitiator.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RequestInitiator.java @@ -12,10 +12,12 @@ import javax.xml.namespace.QName; import java.util.HashMap; import java.util.Map; +import java.util.Set; @Entity -@EqualsAndHashCode(callSuper = true, exclude = {"storageAttributeMap"}) +@EqualsAndHashCode(callSuper = true, onlyExplicitlyIncluded = true) public class RequestInitiator extends AbstractElementExtensibleXMLObject implements org.opensaml.saml.ext.saml2mdreqinit.RequestInitiator { + @EqualsAndHashCode.Include private String binding; @Override public String getBinding() { @@ -27,6 +29,7 @@ public void setBinding(String binding) { this.binding = binding; } + @EqualsAndHashCode.Include private String location; @Override @@ -39,6 +42,7 @@ public void setLocation(String location) { this.location = location; } + @EqualsAndHashCode.Include private String responseLocation; @Override @@ -57,6 +61,11 @@ public void setResponseLocation(String location) { @Transient private AttributeMap attributeMap = new AttributeMap(this); + @EqualsAndHashCode.Include + private Set> attributeMapEntrySet() { + return this.attributeMap.entrySet(); + } + @PrePersist void prePersist() { this.storageAttributeMap = this.attributeMap; diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/VersioningTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/VersioningTests.groovy index 649d8d5a4..0e9ecaf10 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/VersioningTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/VersioningTests.groovy @@ -15,9 +15,12 @@ class VersioningTests extends Specification{ def "test that two loaded entity descriptors gets the same version when load from the same file"() { given: + def resourceId = "testme" def xmlBytes = this.class.getResource('/metadata/SHIBUI-1723-1.xml').bytes def ed1 = openSamlObjects.unmarshalFromXml(xmlBytes) + ed1.resourceId = resourceId def ed2 = openSamlObjects.unmarshalFromXml(xmlBytes) + ed2.resourceId = resourceId expect: ed1.hashCode() == ed2.hashCode() From b1d23f31a410847027acd5e861b30b205d8bc210 Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Fri, 9 Apr 2021 15:27:37 -0700 Subject: [PATCH 009/151] Updated to enable deletion --- .../action/metadata-collection.action.ts | 4 ++-- .../component/enable-metadata.component.ts | 2 +- .../effect/metadata-collection.effect.spec.ts | 3 +++ .../effect/metadata-collection.effect.ts | 13 +++++++++++- .../reducer/metadata-collection.reducer.ts | 2 +- .../app/core/service/can-deactivate.guard.ts | 2 +- .../component/metadata-header.component.html | 18 ++++++++++------- .../container/metadata-options.component.html | 8 ++++++-- .../container/metadata-options.component.ts | 20 ++++++++++++++++++- .../domain/service/draft.service.spec.ts | 5 ++--- .../metadata/domain/service/draft.service.ts | 8 +++++--- .../domain/service/resolver.service.spec.ts | 6 ++---- .../domain/service/resolver.service.ts | 4 ++-- .../component/resolvers-list.component.html | 19 +++++++++++------- ...dashboard-resolvers-list.component.spec.ts | 4 ++-- .../dashboard-resolvers-list.component.ts | 9 +++++++-- .../resolver/action/collection.action.ts | 4 ++-- .../resolver/effect/collection.effects.ts | 2 +- .../resolver/reducer/collection.reducer.ts | 2 +- 19 files changed, 92 insertions(+), 43 deletions(-) diff --git a/ui/src/app/admin/action/metadata-collection.action.ts b/ui/src/app/admin/action/metadata-collection.action.ts index 01795d49b..07565cb6a 100644 --- a/ui/src/app/admin/action/metadata-collection.action.ts +++ b/ui/src/app/admin/action/metadata-collection.action.ts @@ -62,13 +62,13 @@ export class UpdateMetadataConflict implements Action { export class RemoveMetadataRequest implements Action { readonly type = MetadataCollectionActionTypes.REMOVE_METADATA; - constructor(public payload: MetadataResolver) { } + constructor(public payload: string) { } } export class RemoveMetadataSuccess implements Action { readonly type = MetadataCollectionActionTypes.REMOVE_METADATA_SUCCESS; - constructor(public payload: MetadataResolver) { } + constructor(public payload: string) { } } export class RemoveMetadataFail implements Action { diff --git a/ui/src/app/admin/component/enable-metadata.component.ts b/ui/src/app/admin/component/enable-metadata.component.ts index 014e0dfa3..e6d49b958 100644 --- a/ui/src/app/admin/component/enable-metadata.component.ts +++ b/ui/src/app/admin/component/enable-metadata.component.ts @@ -50,7 +50,7 @@ export class EnableMetadataComponent { .result .then( success => { - this.store.dispatch(new RemoveMetadataRequest(entity)); + this.store.dispatch(new RemoveMetadataRequest(entity.id)); }, err => { console.log('Cancelled'); diff --git a/ui/src/app/admin/effect/metadata-collection.effect.spec.ts b/ui/src/app/admin/effect/metadata-collection.effect.spec.ts index d4d0bfcb4..5c76b04b7 100644 --- a/ui/src/app/admin/effect/metadata-collection.effect.spec.ts +++ b/ui/src/app/admin/effect/metadata-collection.effect.spec.ts @@ -10,6 +10,8 @@ import { Metadata } from '../../metadata/domain/domain.type'; import { MetadataResolver } from '../../metadata/domain/model'; import * as fromI18n from '../../i18n/reducer'; import { I18nService } from '../../i18n/service/i18n.service'; +import { Router } from '@angular/router'; +import { RouterStub } from '../../../testing/router.stub'; describe('Metadata Collection Effects', () => { let effects: MetadataCollectionEffects; @@ -55,6 +57,7 @@ describe('Metadata Collection Effects', () => { }, MetadataCollectionEffects, provideMockActions(() => actions), + { provide: Router, useClass: RouterStub } ], }); diff --git a/ui/src/app/admin/effect/metadata-collection.effect.ts b/ui/src/app/admin/effect/metadata-collection.effect.ts index 2005a91bd..db5f3e4e7 100644 --- a/ui/src/app/admin/effect/metadata-collection.effect.ts +++ b/ui/src/app/admin/effect/metadata-collection.effect.ts @@ -24,6 +24,7 @@ import { Notification, NotificationType } from '../../notification/model/notific import { I18nService } from '../../i18n/service/i18n.service'; import * as fromRoot from '../../app.reducer'; import * as fromI18n from '../../i18n/reducer'; +import { Router } from '@angular/router'; /* istanbul ignore next */ @@ -123,10 +124,20 @@ export class MetadataCollectionEffects { ) ); + @Effect({dispatch: false}) + redirectOnRemove$ = this.actions$.pipe( + ofType(MetadataCollectionActionTypes.REMOVE_METADATA_SUCCESS), + map(action => action.payload), + switchMap(entity => + this.router.navigate(['/']) + ) + ); + constructor( private descriptorService: ResolverService, private actions$: Actions, private store: Store, - private i18nService: I18nService + private i18nService: I18nService, + private router: Router ) { } } /* istanbul ignore next */ diff --git a/ui/src/app/admin/reducer/metadata-collection.reducer.ts b/ui/src/app/admin/reducer/metadata-collection.reducer.ts index dc4006841..b11dded01 100644 --- a/ui/src/app/admin/reducer/metadata-collection.reducer.ts +++ b/ui/src/app/admin/reducer/metadata-collection.reducer.ts @@ -39,7 +39,7 @@ export function reducer(state = initialState, action: MetadataCollectionActionsU } case MetadataCollectionActionTypes.REMOVE_METADATA_SUCCESS: { - return adapter.removeOne(action.payload.id, { + return adapter.removeOne(action.payload, { ...state }); } diff --git a/ui/src/app/core/service/can-deactivate.guard.ts b/ui/src/app/core/service/can-deactivate.guard.ts index 341db400c..c0c7363ec 100644 --- a/ui/src/app/core/service/can-deactivate.guard.ts +++ b/ui/src/app/core/service/can-deactivate.guard.ts @@ -22,6 +22,6 @@ export class CanDeactivateGuard implements CanDeactivate currentState: RouterStateSnapshot, nextState: RouterStateSnapshot ) { - return component.canDeactivate ? component.canDeactivate(currentRoute, currentState, nextState) : true; + return component && component.canDeactivate ? component.canDeactivate(currentRoute, currentState, nextState) : true; } } /* istanbul ignore next */ diff --git a/ui/src/app/metadata/configuration/component/metadata-header.component.html b/ui/src/app/metadata/configuration/component/metadata-header.component.html index d105088c7..942c04b5c 100644 --- a/ui/src/app/metadata/configuration/component/metadata-header.component.html +++ b/ui/src/app/metadata/configuration/component/metadata-header.component.html @@ -1,12 +1,16 @@
-
- Saved:  - {{ (version.modifiedDate) | date:DATE_FORMAT }} -
- By:  - {{ version.createdBy }} -
+
+
+ Saved:  + {{ (version.modifiedDate) | date:DATE_FORMAT }} +
+ By:  + {{ version.createdBy }} +
+ +
+

Enabled Disabled diff --git a/ui/src/app/metadata/configuration/container/metadata-options.component.html b/ui/src/app/metadata/configuration/container/metadata-options.component.html index 1e643f4c4..1f4f9f326 100644 --- a/ui/src/app/metadata/configuration/container/metadata-options.component.html +++ b/ui/src/app/metadata/configuration/container/metadata-options.component.html @@ -7,8 +7,12 @@

+ [isCurrent]="true"> + +
- { /* search goes here */} - - + + {(ordered, first, last, onOrderUp, onOrderDown) => + + {(searched) => } + + } +

diff --git a/ui/src/app/dashboard/container/SourcesTab.js b/ui/src/app/dashboard/container/SourcesTab.js index 3a57861f7..601421da5 100644 --- a/ui/src/app/dashboard/container/SourcesTab.js +++ b/ui/src/app/dashboard/container/SourcesTab.js @@ -5,6 +5,9 @@ import API_BASE_PATH from '../../App.constant'; import SourceList from '../../metadata/domain/source/component/SourceList'; import { useMetadataEntities } from '../../metadata/hooks/api'; +import { Search } from '../component/Search'; + +const searchProps = ['serviceProviderName', 'entityId', 'createdBy']; export function SourcesTab () { @@ -37,9 +40,9 @@ export function SourcesTab () {
- { /* search goes here */ } - - + + {(searched) => } +
diff --git a/ui/src/app/metadata/component/MetadataOptions.js b/ui/src/app/metadata/component/MetadataOptions.js index 18b1f354b..e59b1f8d1 100644 --- a/ui/src/app/metadata/component/MetadataOptions.js +++ b/ui/src/app/metadata/component/MetadataOptions.js @@ -37,7 +37,7 @@ export function MetadataOptions () { Version History {type === 'provider' && - diff --git a/ui/src/app/metadata/component/properties/ArrayProperty.js b/ui/src/app/metadata/component/properties/ArrayProperty.js index b655d784d..625d20654 100644 --- a/ui/src/app/metadata/component/properties/ArrayProperty.js +++ b/ui/src/app/metadata/component/properties/ArrayProperty.js @@ -71,7 +71,7 @@ export function ArrayProperty ({ property, columns, index, onPreview }) { {property.differences && Changed: } { property.name } {property.value.map((v, vidx) => - <> + {(!v || !v.length) &&

-

} {(v && v.length > 0) &&
    @@ -89,22 +89,22 @@ export function ArrayProperty ({ property, columns, index, onPreview }) { )}
} - +
)} : property.widget && property.widget.data ? <> {dataList.map((item, itemIdx) => -
- {item.differences && Changed: } - { item.label } - { property.value.map((v, vIdx) => -
- {v && v.indexOf(item.key) > -1 && true } - {(!v || !(v.indexOf(item.key) > -1)) && false } +
+ {item.differences && Changed: } + { item.label } + { property.value.map((v, vIdx) => +
+ {v && v.indexOf(item.key) > -1 && true } + {(!v || !(v.indexOf(item.key) > -1)) && false } +
+ )}
- )} -
)} : ''} diff --git a/ui/src/app/metadata/domain/provider/component/ProviderList.js b/ui/src/app/metadata/domain/provider/component/ProviderList.js index a771fac38..9c353e0de 100644 --- a/ui/src/app/metadata/domain/provider/component/ProviderList.js +++ b/ui/src/app/metadata/domain/provider/component/ProviderList.js @@ -1,16 +1,17 @@ import React from 'react'; import { Link } from 'react-router-dom'; -import { Badge, UncontrolledPopover, PopoverBody } from 'reactstrap'; - +import { Badge, } from 'reactstrap'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faChevronCircleDown, faChevronCircleUp } from '@fortawesome/free-solid-svg-icons'; import FormattedDate from '../../../../core/components/FormattedDate'; import Translate from '../../../../i18n/components/translate'; +import { Scroller } from '../../../../dashboard/component/Scroller'; -export default function ProviderList({ entities, onDelete }) { +export default function ProviderList({ entities, reorder = true, first, last, onOrderUp, onOrderDown }) { return ( -
+ + {(limited) =>
@@ -23,25 +24,36 @@ export default function ProviderList({ entities, onDelete }) { - {entities.map((provider, idx) => + {limited.map((provider, idx) =>
-
{ idx + 1 }
-
+ : +
+ } +   + - - { /* + -
{{ i + 1 }}
+ {/*
 
-
+ } +
); } diff --git a/ui/src/app/metadata/domain/source/component/SourceList.js b/ui/src/app/metadata/domain/source/component/SourceList.js index 482e21b0c..a8358c912 100644 --- a/ui/src/app/metadata/domain/source/component/SourceList.js +++ b/ui/src/app/metadata/domain/source/component/SourceList.js @@ -5,8 +5,13 @@ import { Badge, UncontrolledPopover, PopoverBody, Button, Modal, ModalHeader, Mo import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faTrash, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; + + import FormattedDate from '../../../../core/components/FormattedDate'; import Translate from '../../../../i18n/components/translate'; +import { Scroller } from '../../../../dashboard/component/Scroller'; + + export default function SourceList({ entities, onDelete }) { @@ -16,63 +21,68 @@ export default function SourceList({ entities, onDelete }) { const [deleting, setDeleting] = React.useState(null); - - const deleteSource = (id) => { onDelete(deleting); setDeleting(null); } return ( -
- - - - - - - - - - - - - {entities.map((source, idx) => - - - - - - - + <> + + {(limited) => +
+ +
TitleEntity IDAuthorCreated DateEnabled
- {source.serviceProviderName } - - {source.entityId} - - {source.createdBy } - - - - - - - { source.serviceEnabled && - - A metadata source must be disabled before it can be deleted. - - } -
+ + + + + + + + - ) } - -
TitleEntity IDAuthorCreated DateEnabled
- setDeleting(null)}> + + + {limited.map((source, idx) => + + + {source.serviceProviderName} + + + {source.entityId} + + + {source.createdBy} + + + + + + + + + + {source.serviceEnabled && + + A metadata source must be disabled before it can be deleted. + + } + + + )} + + +
+ } + + setDeleting(null)}> Delete Metadata Source? @@ -89,7 +99,7 @@ export default function SourceList({ entities, onDelete }) { -
+ ); } diff --git a/ui/src/app/metadata/hooks/api.js b/ui/src/app/metadata/hooks/api.js index 3402e7eb8..0b74f445c 100644 --- a/ui/src/app/metadata/hooks/api.js +++ b/ui/src/app/metadata/hooks/api.js @@ -2,17 +2,26 @@ import useFetch from 'use-http'; import API_BASE_PATH from '../../App.constant'; -const paths = { - source: 'EntityDescriptor', - provider: 'MetadataResolver' +const lists = { + source: 'EntityDescriptors', + provider: 'MetadataResolvers' }; +const details = { + source: 'EntityDescriptor', + provider: 'MetadataResolvers' +} + const schema = { source: 'MetadataSources' } export function getMetadataPath(type) { - return `/${paths[type]}`; + return `/${details[type]}`; +} + +export function getMetadataListPath(type) { + return `/${lists[type]}`; } export function getSchemaPath(type) { @@ -20,13 +29,16 @@ export function getSchemaPath(type) { } export function useMetadataEntities(type = 'source') { - return useFetch(`${API_BASE_PATH}${getMetadataPath(type)}s`); + return useFetch(`${API_BASE_PATH}${getMetadataListPath(type)}`); } export function useMetadataEntity(type = 'source') { return useFetch(`${API_BASE_PATH}${getMetadataPath(type)}`); } +export function useMetadataProviderOrder() { + return useFetch(`${API_BASE_PATH}/MetadataResolversPositionOrder`) +} export function useMetadataSchema() { return useFetch(`${API_BASE_PATH}/ui`); diff --git a/ui/src/setupProxy.js b/ui/src/setupProxy.js index 44319e180..abb8ec30d 100644 --- a/ui/src/setupProxy.js +++ b/ui/src/setupProxy.js @@ -6,8 +6,10 @@ module.exports = function (app) { '/api', createProxyMiddleware({ target: 'http://localhost:8080', - secure: false, - logLevel: "debug" + changeOrigin: true, + onProxyRes: function (proxyRes, req, res) { + proxyRes.headers['Access-Control-Allow-Origin'] = '*'; + } }) ); @@ -15,8 +17,7 @@ module.exports = function (app) { '/actuator', createProxyMiddleware({ target: 'http://localhost:8080', - secure: false, - logLevel: "debug" + changeOrigin: true }) ); @@ -24,8 +25,7 @@ module.exports = function (app) { '/login', createProxyMiddleware({ target: 'http://localhost:8080', - secure: false, - logLevel: "debug" + changeOrigin: true }) ); @@ -33,8 +33,7 @@ module.exports = function (app) { '/logout', createProxyMiddleware({ target: 'http://localhost:8080', - secure: false, - logLevel: "debug" + changeOrigin: true }) ); }; diff --git a/ui/src/theme/project/list.scss b/ui/src/theme/project/list.scss index 5acc06806..be94eddd0 100644 --- a/ui/src/theme/project/list.scss +++ b/ui/src/theme/project/list.scss @@ -22,3 +22,7 @@ padding: 0px; } } + +.infinite-scroll-component { + overflow: unset !important; +} \ No newline at end of file diff --git a/ui/yarn.lock b/ui/yarn.lock index ac9e26a66..5f9867d98 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -9087,6 +9087,13 @@ react-error-overlay@^6.0.9: resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a" integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew== +react-infinite-scroll-component@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz#7e511e7aa0f728ac3e51f64a38a6079ac522407f" + integrity sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ== + dependencies: + throttle-debounce "^2.1.0" + react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -10616,6 +10623,11 @@ throat@^5.0.0: resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b" integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA== +throttle-debounce@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-2.3.0.tgz#fd31865e66502071e411817e241465b3e9c372e2" + integrity sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ== + through2@^2.0.0: version "2.0.5" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" From fcc84851ed824a2b3b0240563d201988aefcf873 Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Tue, 27 Apr 2021 14:04:14 -0700 Subject: [PATCH 027/151] Implemented dashboard --- ui/src/app/admin/component/AccessRequest.js | 66 ++++++++++ ui/src/app/admin/component/UserMaintenance.js | 63 ++++++++++ ui/src/app/admin/container/SourcesActions.js | 21 ++++ ui/src/app/admin/container/UserActions.js | 14 +++ ui/src/app/admin/container/UserManagement.js | 118 ++++++------------ ui/src/app/core/components/AdminRoute.js | 25 ++++ ui/src/app/core/hooks/utils.js | 2 +- ui/src/app/core/user/UserContext.js | 7 +- ui/src/app/core/utility/get_cookie.js | 4 +- ui/src/app/dashboard/component/Ordered.js | 7 +- ui/src/app/dashboard/container/ActionsTab.js | 11 +- ui/src/app/dashboard/container/AdminTab.js | 39 ++---- ui/src/app/dashboard/container/Dashboard.js | 86 +++++++++---- .../app/dashboard/container/ProvidersTab.js | 50 +++++--- ui/src/app/dashboard/container/SourcesTab.js | 5 +- .../component/properties/ArrayProperty.js | 3 +- ui/src/app/metadata/editor/MetadataEditor.js | 2 - ui/src/app/metadata/hoc/MetadataSchema.js | 1 + ui/src/app/metadata/hoc/MetadataSelector.js | 1 + ui/src/app/metadata/hooks/api.js | 8 +- 20 files changed, 357 insertions(+), 176 deletions(-) create mode 100644 ui/src/app/admin/component/AccessRequest.js create mode 100644 ui/src/app/admin/component/UserMaintenance.js create mode 100644 ui/src/app/admin/container/SourcesActions.js create mode 100644 ui/src/app/admin/container/UserActions.js create mode 100644 ui/src/app/core/components/AdminRoute.js diff --git a/ui/src/app/admin/component/AccessRequest.js b/ui/src/app/admin/component/AccessRequest.js new file mode 100644 index 000000000..682c5b1d8 --- /dev/null +++ b/ui/src/app/admin/component/AccessRequest.js @@ -0,0 +1,66 @@ +import React from 'react'; +import Translate from '../../i18n/components/translate'; + +export function AccessRequest({ users, roles, onDeleteUser, onChangeUserRole }) { + + return ( + <> + {(!users || !users.length) ? + <> +
+
+

There are no new user requests at this time.

+
+
+ + : + users.map((user, i) => ( +
+
+
+
+
+
+ UserId +
+
{ user.username }
+
+ Email +
+
{ user.emailAddress }
+
+
+
+
+ Name +
+
{ user.firstName } { user.lastName }
+ +
+ +
+
+
+
+ +
+
+
+
+ ))} + + ); +} \ No newline at end of file diff --git a/ui/src/app/admin/component/UserMaintenance.js b/ui/src/app/admin/component/UserMaintenance.js new file mode 100644 index 000000000..efce47acb --- /dev/null +++ b/ui/src/app/admin/component/UserMaintenance.js @@ -0,0 +1,63 @@ +import React from 'react'; + +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faTrash } from '@fortawesome/free-solid-svg-icons'; + +import Translate from '../../i18n/components/translate'; +import { useCurrentUser } from '../../core/user/UserContext'; + +export default function UserMaintenance({ users, roles, onDeleteUser, onChangeUserRole }) { + + const currentUser = useCurrentUser(); + + return ( +
+ + + + + + + + + + + + {users.map((user, idx) => + + + + + + + + )} + +
UserIdNameEmailRoleDelete?
{user.username}{user.firstName} {user.lastName}{user.emailAddress} + + + + {currentUser.username !== user.username && + + } +
+
+ ); +} diff --git a/ui/src/app/admin/container/SourcesActions.js b/ui/src/app/admin/container/SourcesActions.js new file mode 100644 index 000000000..805ffbc66 --- /dev/null +++ b/ui/src/app/admin/container/SourcesActions.js @@ -0,0 +1,21 @@ +import React from 'react'; +import SourceList from '../../metadata/domain/source/component/SourceList'; +import { useMetadataEntity } from '../../metadata/hooks/api'; + +export function SourcesActions ({sources, reloadSources}) { + + const { del, response } = useMetadataEntity('source', { + cachePolicy: 'no-cache' + }); + + async function deleteSource(id) { + await del(`/${id}`); + if (response.ok) { + reloadSources(); + } + } + + return ( + + ); +} \ No newline at end of file diff --git a/ui/src/app/admin/container/UserActions.js b/ui/src/app/admin/container/UserActions.js new file mode 100644 index 000000000..dd05c9594 --- /dev/null +++ b/ui/src/app/admin/container/UserActions.js @@ -0,0 +1,14 @@ +import React from 'react'; +import { AccessRequest } from '../../admin/component/AccessRequest'; +import UserManagement from '../../admin/container/UserManagement'; + +export function UserActions({ users, reloadUsers }) { + return ( + + {(u, roles, onChangeUserRole, onDeleteUser) => + } + + ); +} + +export default UserActions; \ No newline at end of file diff --git a/ui/src/app/admin/container/UserManagement.js b/ui/src/app/admin/container/UserManagement.js index d06a6e0dd..518a77236 100644 --- a/ui/src/app/admin/container/UserManagement.js +++ b/ui/src/app/admin/container/UserManagement.js @@ -1,17 +1,47 @@ import React from 'react'; import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; +import useFetch from 'use-http'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faTrash, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; +import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; import Translate from '../../i18n/components/translate'; -import { useCurrentUser } from '../../core/user/UserContext'; +import API_BASE_PATH from '../../App.constant'; -export default function UserManagement({ users, roles, onDelete, onSetRole }) { +export default function UserManagement({ users, children, reload }) { - const setUserRole = (user, role) => onSetRole(user, role); + const [roles, setRoles] = React.useState([]); - const currentUser = useCurrentUser(); + const { get, patch, del, response } = useFetch(`${API_BASE_PATH}`, {}); + + async function loadRoles() { + const roles = await get('/supportedRoles') + if (response.ok) { + setRoles(roles); + } + } + + async function setUserRoleRequest(user, role) { + await patch(`/admin/users/${user.username}`, { + ...user, + role + }); + if (response.ok && reload) { + reload(); + } + } + + async function deleteUserRequest(id) { + await del(`/admin/users/${id}`); + if (response.ok && reload) { + reload(); + } + } + + /*eslint-disable react-hooks/exhaustive-deps*/ + React.useEffect(() => { + loadRoles(); + }, []); const [modal, setModal] = React.useState(false); @@ -20,57 +50,13 @@ export default function UserManagement({ users, roles, onDelete, onSetRole }) { const [deleting, setDeleting] = React.useState(null); const deleteUser = (id) => { - onDelete(deleting); + deleteUserRequest(deleting); setDeleting(null); } return ( -
- - - - - - - - - - - - {users.map((user, idx) => - - - - - - - - )} - -
UserIdNameEmailRoleDelete?
{ user.username }{ user.firstName } { user.lastName }{ user.emailAddress } - - - - {currentUser.username !== user.username && - - } -
+
+ {children(users, roles, setUserRoleRequest, (id) => setDeleting(id))} setDeleting(null)}> Delete User? @@ -91,31 +77,3 @@ export default function UserManagement({ users, roles, onDelete, onSetRole }) {
); } - -/* - - {{ user.username }} - {{ user.firstName }} {{ user.lastName }} - {{ user.emailAddress }} - - - - - - - - */ \ No newline at end of file diff --git a/ui/src/app/core/components/AdminRoute.js b/ui/src/app/core/components/AdminRoute.js new file mode 100644 index 000000000..1b8783ec8 --- /dev/null +++ b/ui/src/app/core/components/AdminRoute.js @@ -0,0 +1,25 @@ +import React from 'react'; +import { Redirect, Route } from 'react-router'; + +import { useIsAdmin } from '../user/UserContext'; + +export function AdminRoute({ children, ...rest }) { + const isAdmin = useIsAdmin(); + return ( + + isAdmin ? ( + children + ) : ( + + ) + } + /> + ); +} \ No newline at end of file diff --git a/ui/src/app/core/hooks/utils.js b/ui/src/app/core/hooks/utils.js index e8ee1f74a..c7ae9a0cf 100644 --- a/ui/src/app/core/hooks/utils.js +++ b/ui/src/app/core/hooks/utils.js @@ -1,6 +1,6 @@ function uuidv4() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { - var r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8); + var r = Math.random() * 16 | 0, v = c === 'x' ? r : ((r & 0x3) | 0x8); return v.toString(16); }); } diff --git a/ui/src/app/core/user/UserContext.js b/ui/src/app/core/user/UserContext.js index d99e52cc9..b1235877c 100644 --- a/ui/src/app/core/user/UserContext.js +++ b/ui/src/app/core/user/UserContext.js @@ -34,5 +34,10 @@ function useCurrentUser() { return context; } +function useIsAdmin() { + const user = useCurrentUser(); + return user.role === 'ROLE_ADMIN'; +} + -export { UserContext, UserProvider, Consumer as UserConsumer, useCurrentUser }; \ No newline at end of file +export { UserContext, UserProvider, Consumer as UserConsumer, useCurrentUser, useIsAdmin }; \ No newline at end of file diff --git a/ui/src/app/core/utility/get_cookie.js b/ui/src/app/core/utility/get_cookie.js index 97a5195ac..de9f5bae7 100644 --- a/ui/src/app/core/utility/get_cookie.js +++ b/ui/src/app/core/utility/get_cookie.js @@ -4,10 +4,10 @@ export function get_cookie (cname) { var ca = decodedCookie.split(';'); for (var i = 0; i < ca.length; i++) { var c = ca[i]; - while (c.charAt(0) == ' ') { + while (c.charAt(0) === ' ') { c = c.substring(1); } - if (c.indexOf(name) == 0) { + if (c.indexOf(name) === 0) { return c.substring(name.length, c.length); } } diff --git a/ui/src/app/dashboard/component/Ordered.js b/ui/src/app/dashboard/component/Ordered.js index 4f352710c..0fe8b1675 100644 --- a/ui/src/app/dashboard/component/Ordered.js +++ b/ui/src/app/dashboard/component/Ordered.js @@ -4,7 +4,6 @@ import first from 'lodash/first'; import last from 'lodash/last'; import API_BASE_PATH from '../../App.constant'; import { array_move } from '../../core/utility/array_move'; -import { pick } from 'lodash'; const orderPaths = { provider: `/MetadataResolversPositionOrder` @@ -42,7 +41,7 @@ export function Ordered ({type = 'provider', entities, children}) { const [lastId, setLastId] = React.useState(null); async function changeOrder(resourceIds) { - const update = await post(`${orderPaths[type]}`, { + await post(`${orderPaths[type]}`, { resourceIds }); if (response.ok) { @@ -64,7 +63,6 @@ export function Ordered ({type = 'provider', entities, children}) { async function loadOrder () { const o = await get(`${orderPaths[type]}`); - console.log(o) if (response.ok) { const ids = o.resourceIds; setOrder(ids); @@ -73,12 +71,11 @@ export function Ordered ({type = 'provider', entities, children}) { } } + /*eslint-disable react-hooks/exhaustive-deps*/ React.useEffect(() => loadOrder(),[]); React.useEffect(() => orderEntities(order, entities), [order, entities]); - React.useEffect(() => console.log(ordered.map(e => pick(e, ['resourceId']))), [ordered]); - return ( <> {children(ordered, firstId, lastId, onOrderUp, onOrderDown)} diff --git a/ui/src/app/dashboard/container/ActionsTab.js b/ui/src/app/dashboard/container/ActionsTab.js index c89e23585..76d1ff592 100644 --- a/ui/src/app/dashboard/container/ActionsTab.js +++ b/ui/src/app/dashboard/container/ActionsTab.js @@ -1,11 +1,10 @@ import React from 'react'; -import useFetch from 'use-http'; -import UserManagement from '../../admin/container/UserManagement'; -import API_BASE_PATH from '../../App.constant'; +import { SourcesActions } from '../../admin/container/SourcesActions'; +import UserActions from '../../admin/container/UserActions'; import Translate from '../../i18n/components/translate'; -export function ActionsTab() { +export function ActionsTab({ sources, users, reloadSources, reloadUsers }) { return ( <> @@ -19,7 +18,7 @@ export function ActionsTab() {
- {/**/} +
@@ -32,7 +31,7 @@ export function ActionsTab() { - {/**/} + diff --git a/ui/src/app/dashboard/container/AdminTab.js b/ui/src/app/dashboard/container/AdminTab.js index 2e04221c0..a458c810a 100644 --- a/ui/src/app/dashboard/container/AdminTab.js +++ b/ui/src/app/dashboard/container/AdminTab.js @@ -1,6 +1,7 @@ import React from 'react'; import useFetch from 'use-http'; import UserManagement from '../../admin/container/UserManagement'; +import UserMaintenance from '../../admin/component/UserMaintenance'; import API_BASE_PATH from '../../App.constant'; import Translate from '../../i18n/components/translate'; @@ -9,43 +10,20 @@ export function AdminTab () { const [users, setUsers] = React.useState([]); - const { get, patch, del, response } = useFetch(`${API_BASE_PATH}`, {}) + const { get, response } = useFetch(`${API_BASE_PATH}/admin/users`, { + cachePolicy: 'no-cache' + }, []); async function loadUsers() { - const users = await get('/admin/users') + const users = await get('') if (response.ok) { setUsers(users); } } - const [roles, setRoles] = React.useState([]); - - async function loadRoles() { - const roles = await get('/supportedRoles') - if (response.ok) { - setRoles(roles); - } - } - - async function setUserRole (user, role) { - const update = await patch(`/admin/users/${user.username}`, { - ...user, - role - }); - if (response.ok) { - loadUsers(); - } - } - - async function deleteUser(id) { - const removal = await del(`/admin/users/${id}`); - if (response.ok) { - loadUsers(); - } - } + /*eslint-disable react-hooks/exhaustive-deps*/ React.useEffect(() => { loadUsers(); - loadRoles(); }, []); @@ -60,7 +38,10 @@ export function AdminTab () {
- + + {(u, roles, onChangeUserRole, onDeleteUser) => + } +
diff --git a/ui/src/app/dashboard/container/Dashboard.js b/ui/src/app/dashboard/container/Dashboard.js index dc1274e98..97b070874 100644 --- a/ui/src/app/dashboard/container/Dashboard.js +++ b/ui/src/app/dashboard/container/Dashboard.js @@ -5,52 +5,96 @@ import { Switch, Route, Redirect, useRouteMatch } from 'react-router-dom'; import { NavLink } from 'react-router-dom'; import Translate from '../../i18n/components/translate'; +import { AdminRoute } from '../../core/components/AdminRoute'; import './Dashboard.scss'; import { SourcesTab } from './SourcesTab'; import { ProvidersTab } from './ProvidersTab'; import { AdminTab } from './AdminTab'; import { ActionsTab } from './ActionsTab'; +import { useIsAdmin } from '../../core/user/UserContext'; +import useFetch from 'use-http'; +import API_BASE_PATH from '../../App.constant'; +import { getMetadataPath } from '../../metadata/hooks/api'; export function Dashboard () { - const actions = 0; + const { path } = useRouteMatch(); - let { path } = useRouteMatch(); + const isAdmin = useIsAdmin(); + + const [actions, setActions] = React.useState(0); + const [users, setUsers] = React.useState([]); + const [sources, setSources] = React.useState([]); + + const { get, response } = useFetch(`${API_BASE_PATH}`, { + cachePolicy: 'no-cache' + }); + + async function loadUsers() { + const users = await get('/admin/users') + if (response.ok) { + setUsers(users.filter(u => u.role === 'ROLE_NONE')); + } + } + + async function loadSources() { + const s = await get(`/${getMetadataPath('source')}/disabledNonAdmin`); + if (response.ok) { + setSources(s); + } + } + + /*eslint-disable react-hooks/exhaustive-deps*/ + React.useEffect(() => { + loadSources(); + loadUsers(); + }, []); + + React.useEffect(() => { + setActions(users.length + sources.length); + }, [users, sources]); return (
+ - - - + + + + +
); diff --git a/ui/src/app/dashboard/container/ProvidersTab.js b/ui/src/app/dashboard/container/ProvidersTab.js index 54daf1786..27d89ece5 100644 --- a/ui/src/app/dashboard/container/ProvidersTab.js +++ b/ui/src/app/dashboard/container/ProvidersTab.js @@ -5,6 +5,8 @@ import Translate from '../../i18n/components/translate'; import ProviderList from '../../metadata/domain/provider/component/ProviderList'; import {Search} from '../component/Search'; import { Ordered } from '../component/Ordered'; +import { useIsAdmin } from '../../core/user/UserContext'; +import { Alert } from 'reactstrap'; const searchProps = ['name', '@type', 'createdBy']; @@ -21,31 +23,39 @@ export function ProvidersTab () { } } + /*eslint-disable react-hooks/exhaustive-deps*/ React.useEffect(() => { loadProviders() }, []); + const isAdmin = useIsAdmin(); + return (
-
- - Current Metadata Providers - -
-
- - {(ordered, first, last, onOrderUp, onOrderDown) => - - {(searched) => } - - } - -
+ {isAdmin ? + <> +
+ + Current Metadata Providers + +
+
+ + {(ordered, first, last, onOrderUp, onOrderDown) => + + {(searched) => } + + } + +
+ + : + Access Denied}
); diff --git a/ui/src/app/dashboard/container/SourcesTab.js b/ui/src/app/dashboard/container/SourcesTab.js index 601421da5..073aeaa86 100644 --- a/ui/src/app/dashboard/container/SourcesTab.js +++ b/ui/src/app/dashboard/container/SourcesTab.js @@ -1,7 +1,5 @@ import React from 'react'; -import useFetch from 'use-http'; import Translate from '../../i18n/components/translate'; -import API_BASE_PATH from '../../App.constant'; import SourceList from '../../metadata/domain/source/component/SourceList'; import { useMetadataEntities } from '../../metadata/hooks/api'; @@ -23,12 +21,13 @@ export function SourcesTab () { } async function deleteSource(id) { - const removal = await del(`/${id}`); + await del(`/${id}`); if (response.ok) { loadSources(); } } + /*eslint-disable react-hooks/exhaustive-deps*/ React.useEffect(() => { loadSources() }, []); return ( diff --git a/ui/src/app/metadata/component/properties/ArrayProperty.js b/ui/src/app/metadata/component/properties/ArrayProperty.js index 625d20654..979ed95f2 100644 --- a/ui/src/app/metadata/component/properties/ArrayProperty.js +++ b/ui/src/app/metadata/component/properties/ArrayProperty.js @@ -2,7 +2,6 @@ import { faEye } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import React from 'react'; import Translate from '../../../i18n/components/translate'; -import { ArrayValue } from './ArrayValue'; import { usePropertyWidth } from './hooks'; import { PropertyValue } from './PropertyValue'; @@ -10,7 +9,7 @@ import { PropertyValue } from './PropertyValue'; const isUri = (value) => { try { - let url = new URL(value); + new URL(value); } catch (err) { return false; } diff --git a/ui/src/app/metadata/editor/MetadataEditor.js b/ui/src/app/metadata/editor/MetadataEditor.js index 8e0e4aece..9dbec266c 100644 --- a/ui/src/app/metadata/editor/MetadataEditor.js +++ b/ui/src/app/metadata/editor/MetadataEditor.js @@ -1,7 +1,5 @@ import React from 'react'; -import Translate from '../../i18n/components/translate'; - import { MetadataObjectContext, MetadataTypeContext } from '../hoc/MetadataSelector'; export function MetadataEditor () { diff --git a/ui/src/app/metadata/hoc/MetadataSchema.js b/ui/src/app/metadata/hoc/MetadataSchema.js index 2db96155f..ad6574bfe 100644 --- a/ui/src/app/metadata/hoc/MetadataSchema.js +++ b/ui/src/app/metadata/hoc/MetadataSchema.js @@ -23,6 +23,7 @@ export function MetadataSchema({ children }) { } } + /*eslint-disable react-hooks/exhaustive-deps*/ React.useEffect(() => { loadSchema(definition) }, [definition]); return ( diff --git a/ui/src/app/metadata/hoc/MetadataSelector.js b/ui/src/app/metadata/hoc/MetadataSelector.js index f7cd954a1..be7ab62c6 100644 --- a/ui/src/app/metadata/hoc/MetadataSelector.js +++ b/ui/src/app/metadata/hoc/MetadataSelector.js @@ -21,6 +21,7 @@ export function MetadataSelector ({ children }) { } } + /*eslint-disable react-hooks/exhaustive-deps*/ React.useEffect(() => { loadMetadata(id) }, [id]); return ( diff --git a/ui/src/app/metadata/hooks/api.js b/ui/src/app/metadata/hooks/api.js index 0b74f445c..8b76c5233 100644 --- a/ui/src/app/metadata/hooks/api.js +++ b/ui/src/app/metadata/hooks/api.js @@ -28,12 +28,12 @@ export function getSchemaPath(type) { return `/${schema[type]}`; } -export function useMetadataEntities(type = 'source') { - return useFetch(`${API_BASE_PATH}${getMetadataListPath(type)}`); +export function useMetadataEntities(type = 'source', opts = {}) { + return useFetch(`${API_BASE_PATH}${getMetadataListPath(type)}`, opts); } -export function useMetadataEntity(type = 'source') { - return useFetch(`${API_BASE_PATH}${getMetadataPath(type)}`); +export function useMetadataEntity(type = 'source', opts = {}) { + return useFetch(`${API_BASE_PATH}${getMetadataPath(type)}`, opts); } export function useMetadataProviderOrder() { From 0ed524706cf689f562fabfc6383ec180375e9ce8 Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Tue, 27 Apr 2021 15:18:52 -0700 Subject: [PATCH 028/151] Added notifications --- ui/src/app/App.js | 45 +++++----- ui/src/app/admin/container/SourcesActions.js | 24 +++++- ui/src/app/admin/container/UserManagement.js | 9 ++ ui/src/app/core/hooks/utils.js | 9 +- ui/src/app/core/utility/uuid.js | 6 ++ .../domain/source/component/SourceList.js | 20 +++-- .../component/NotificationItem.js | 22 +++++ .../component/NotificationList.js | 18 ++++ ui/src/app/notifications/hoc/Notifications.js | 82 +++++++++++++++++++ ui/src/theme/project/index.scss | 7 +- ui/src/theme/project/notifications.scss | 5 ++ 11 files changed, 210 insertions(+), 37 deletions(-) create mode 100644 ui/src/app/core/utility/uuid.js create mode 100644 ui/src/app/notifications/component/NotificationItem.js create mode 100644 ui/src/app/notifications/component/NotificationList.js create mode 100644 ui/src/app/notifications/hoc/Notifications.js create mode 100644 ui/src/theme/project/notifications.scss diff --git a/ui/src/app/App.js b/ui/src/app/App.js index dcce907a2..85bae3dc5 100644 --- a/ui/src/app/App.js +++ b/ui/src/app/App.js @@ -16,6 +16,8 @@ import Dashboard from './dashboard/container/Dashboard'; import Header from './core/components/Header'; import { UserProvider } from './core/user/UserContext'; import { Metadata } from './metadata/Metadata'; +import { Notifications } from './notifications/hoc/Notifications'; +import { NotificationList } from './notifications/component/NotificationList'; function App() { @@ -29,27 +31,28 @@ function App() { }; return ( -
+
- - - - - -
-
- - - - - - - -
-
- - - + + + + +
+
+ + + + + + + + +
+
+ + + +
); @@ -58,8 +61,6 @@ function App() { /*
- -
*/ diff --git a/ui/src/app/admin/container/SourcesActions.js b/ui/src/app/admin/container/SourcesActions.js index 805ffbc66..c3a8cca27 100644 --- a/ui/src/app/admin/container/SourcesActions.js +++ b/ui/src/app/admin/container/SourcesActions.js @@ -2,20 +2,40 @@ import React from 'react'; import SourceList from '../../metadata/domain/source/component/SourceList'; import { useMetadataEntity } from '../../metadata/hooks/api'; +import { NotificationContext, createNotificationAction } from '../../notifications/hoc/Notifications'; + export function SourcesActions ({sources, reloadSources}) { - const { del, response } = useMetadataEntity('source', { + const { dispatch } = React.useContext(NotificationContext); + + const { put, del, response } = useMetadataEntity('source', { cachePolicy: 'no-cache' }); async function deleteSource(id) { await del(`/${id}`); if (response.ok) { + dispatch(createNotificationAction( + `Metadata Source has been removed.` + )); + reloadSources(); + } + } + + async function enableSource(source) { + await put(`/${source.id}`, { + ...source, + serviceEnabled: true + }); + if (response.ok) { + dispatch(createNotificationAction( + `Metadata Source has been enabled.` + )); reloadSources(); } } return ( - + ); } \ No newline at end of file diff --git a/ui/src/app/admin/container/UserManagement.js b/ui/src/app/admin/container/UserManagement.js index 518a77236..777cab129 100644 --- a/ui/src/app/admin/container/UserManagement.js +++ b/ui/src/app/admin/container/UserManagement.js @@ -7,11 +7,14 @@ import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; import Translate from '../../i18n/components/translate'; import API_BASE_PATH from '../../App.constant'; +import { NotificationContext, createNotificationAction} from '../../notifications/hoc/Notifications'; export default function UserManagement({ users, children, reload }) { const [roles, setRoles] = React.useState([]); + const { dispatch } = React.useContext(NotificationContext); + const { get, patch, del, response } = useFetch(`${API_BASE_PATH}`, {}); async function loadRoles() { @@ -27,6 +30,9 @@ export default function UserManagement({ users, children, reload }) { role }); if (response.ok && reload) { + dispatch(createNotificationAction( + `User update successful for ${user.username}.` + )); reload(); } } @@ -34,6 +40,9 @@ export default function UserManagement({ users, children, reload }) { async function deleteUserRequest(id) { await del(`/admin/users/${id}`); if (response.ok && reload) { + dispatch(createNotificationAction( + `User deleted.` + )); reload(); } } diff --git a/ui/src/app/core/hooks/utils.js b/ui/src/app/core/hooks/utils.js index c7ae9a0cf..8ba7e8020 100644 --- a/ui/src/app/core/hooks/utils.js +++ b/ui/src/app/core/hooks/utils.js @@ -1,10 +1,5 @@ -function uuidv4() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { - var r = Math.random() * 16 | 0, v = c === 'x' ? r : ((r & 0x3) | 0x8); - return v.toString(16); - }); -} +import {uuid} from '../utility/uuid'; export function useGuid() { - return uuidv4(); + return uuid(); } \ No newline at end of file diff --git a/ui/src/app/core/utility/uuid.js b/ui/src/app/core/utility/uuid.js new file mode 100644 index 000000000..b7cf7f739 --- /dev/null +++ b/ui/src/app/core/utility/uuid.js @@ -0,0 +1,6 @@ +export function uuid() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + var r = Math.random() * 16 | 0, v = c === 'x' ? r : ((r & 0x3) | 0x8); + return v.toString(16); + }); +} \ No newline at end of file diff --git a/ui/src/app/metadata/domain/source/component/SourceList.js b/ui/src/app/metadata/domain/source/component/SourceList.js index a8358c912..ae59abf06 100644 --- a/ui/src/app/metadata/domain/source/component/SourceList.js +++ b/ui/src/app/metadata/domain/source/component/SourceList.js @@ -3,7 +3,7 @@ import { Link } from 'react-router-dom'; import { Badge, UncontrolledPopover, PopoverBody, Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faTrash, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; +import { faTrash, faExclamationTriangle, faCheck } from '@fortawesome/free-solid-svg-icons'; @@ -13,7 +13,7 @@ import { Scroller } from '../../../../dashboard/component/Scroller'; -export default function SourceList({ entities, onDelete }) { +export default function SourceList({ entities, onDelete, onEnable }) { const [modal, setModal] = React.useState(false); @@ -57,9 +57,19 @@ export default function SourceList({ entities, onDelete }) { - - - + {onEnable ? + + : + + + + } + +
} + {loading &&
+ + Loading... +
} ); } \ No newline at end of file diff --git a/ui/src/app/metadata/component/MetadataOptions.js b/ui/src/app/metadata/component/MetadataOptions.js index e59b1f8d1..374713f12 100644 --- a/ui/src/app/metadata/component/MetadataOptions.js +++ b/ui/src/app/metadata/component/MetadataOptions.js @@ -11,6 +11,7 @@ import { MetadataConfiguration } from './MetadataConfiguration'; import { useMetadataConfiguration } from '../hooks/configuration'; +import { MetadataViewToggle } from './MetadataViewToggle'; export function MetadataOptions () { @@ -43,18 +44,7 @@ export function MetadataOptions () { } - {/**/} + diff --git a/ui/src/app/metadata/component/MetadataViewToggle.js b/ui/src/app/metadata/component/MetadataViewToggle.js new file mode 100644 index 000000000..59975b9fb --- /dev/null +++ b/ui/src/app/metadata/component/MetadataViewToggle.js @@ -0,0 +1,26 @@ +import React from 'react'; +import { NavLink } from 'react-router-dom'; + +import {Translate} from '../../i18n/components/translate'; +import { MetadataXmlContext } from '../hoc/MetadataXmlLoader'; + +export function MetadataViewToggle () { + const xml = React.useContext(MetadataXmlContext); + + return ( + <> + {xml ? +
+ + Toggle view: + Options + + + Toggle view: + XML + +
+ : ''} + + ); +} \ No newline at end of file diff --git a/ui/src/app/metadata/component/MetadataXml.js b/ui/src/app/metadata/component/MetadataXml.js new file mode 100644 index 000000000..10c740d8e --- /dev/null +++ b/ui/src/app/metadata/component/MetadataXml.js @@ -0,0 +1,45 @@ +import { faSave } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import React from 'react'; + +import { useParams } from 'react-router'; +import Translate from '../../i18n/components/translate'; +import { MetadataObjectContext } from '../hoc/MetadataSelector'; + +import { MetadataXmlContext } from '../hoc/MetadataXmlLoader'; +import { MetadataViewToggle } from './MetadataViewToggle'; +import { downloadAsXml } from '../../core/utility/download_as_xml'; + +export function MetadataXml () { + const xml = React.useContext(MetadataXmlContext); + const entity = React.useContext(MetadataObjectContext); + const { type } = useParams(); + + const download = () => downloadAsXml(entity, xml); + + return ( + <> +

+ Source Configuration +

+
+
+ +
+
+
+
{xml}
+
+ { xml } +
+
+ + +
+
+ + ); +} \ No newline at end of file diff --git a/ui/src/app/metadata/component/properties/PropertyValue.js b/ui/src/app/metadata/component/properties/PropertyValue.js index 14779b3e4..91b51d5cd 100644 --- a/ui/src/app/metadata/component/properties/PropertyValue.js +++ b/ui/src/app/metadata/component/properties/PropertyValue.js @@ -10,8 +10,6 @@ export function PropertyValue ({ name, value, columns }) { const id = useGuid(); - console.log(value) - return ( <> { name && value && value !== false ? diff --git a/ui/src/app/metadata/domain/index.js b/ui/src/app/metadata/domain/index.js index 01d2429aa..5cd284162 100644 --- a/ui/src/app/metadata/domain/index.js +++ b/ui/src/app/metadata/domain/index.js @@ -1,10 +1,13 @@ +import { MetadataProviderEditorTypes } from './provider'; import { SourceEditor } from "./source/SourceDefinition"; export const editors = { source: SourceEditor }; -export const ProviderEditorTypes = []; +export const ProviderEditorTypes = [ + ...MetadataProviderEditorTypes +]; export const FilterEditorTypes = []; diff --git a/ui/src/app/metadata/domain/provider/BaseProviderDefinition.js b/ui/src/app/metadata/domain/provider/BaseProviderDefinition.js new file mode 100644 index 000000000..082ab3203 --- /dev/null +++ b/ui/src/app/metadata/domain/provider/BaseProviderDefinition.js @@ -0,0 +1,26 @@ +import { metadataFilterProcessor } from './utility/providerFilterProcessor'; + +export const BaseProviderDefinition = { + schemaPreprocessor: metadataFilterProcessor, + parser: (changes) => (changes.metadataFilters ? ({ + ...changes, + metadataFilters: [ + ...Object.keys(changes.metadataFilters).reduce((collection, filterName) => ([ + ...collection, + { + ...changes.metadataFilters[filterName], + '@type': filterName + } + ]), []) + ] + }) : changes), + formatter: (changes) => (changes.metadataFilters ? ({ + ...changes, + metadataFilters: { + ...(changes.metadataFilters || []).reduce((collection, filter) => ({ + ...collection, + [filter['@type']]: filter + }), {}) + } + }) : changes), +} \ No newline at end of file diff --git a/ui/src/app/metadata/domain/provider/DynamicHttpMetadataProviderDefinition.js b/ui/src/app/metadata/domain/provider/DynamicHttpMetadataProviderDefinition.js new file mode 100644 index 000000000..e1f04b0a1 --- /dev/null +++ b/ui/src/app/metadata/domain/provider/DynamicHttpMetadataProviderDefinition.js @@ -0,0 +1,131 @@ +// import { metadataFilterProcessor } from './utility/providerFilterProcessor'; + +import { BaseProviderDefinition } from './BaseProviderDefinition'; +import API_BASE_PATH from "../../../App.constant"; + +export const DynamicHttpMetadataProviderWizard = { + ...BaseProviderDefinition, + label: 'DynamicHttpMetadataProvider', + type: 'DynamicHttpMetadataResolver', + schema: `${API_BASE_PATH}/ui/MetadataResolver/DynamicHttpMetadataResolver`, + steps: [ + { + id: 'common', + label: 'label.common-attributes', + index: 2, + initialValues: [], + fields: [ + 'xmlId', + 'requireValidMetadata', + 'failFastInitialization', + 'metadataRequestURLConstructionScheme' + ] + }, + { + id: 'dynamic', + label: 'label.dynamic-attributes', + index: 3, + initialValues: [], + fields: [ + 'dynamicMetadataResolverAttributes' + ] + }, + { + id: 'plugins', + label: 'label.metadata-filter-plugins', + index: 4, + initialValues: [], + fields: [ + 'metadataFilters' + ] + }, + { + id: 'summary', + label: 'label.finished', + index: 5, + initialValues: [], + fields: [ + 'enabled' + ] + } + ] +}; + + +export const DynamicHttpMetadataProviderEditor = { + ...DynamicHttpMetadataProviderWizard, + steps: [ + { + id: 'common', + label: 'label.common-attributes', + index: 1, + initialValues: [], + fields: [ + 'name', + '@type', + 'xmlId', + 'metadataRequestURLConstructionScheme', + 'enabled', + 'requireValidMetadata', + 'failFastInitialization' + ], + fieldsets: [ + { + type: 'section', + class: ['mb-3'], + fields: [ + 'name', + '@type' + ] + }, + { + type: 'group-lg', + class: ['col-12'], + fields: [ + 'xmlId', + 'metadataRequestURLConstructionScheme', + 'enabled', + 'requireValidMetadata', + 'failFastInitialization' + ] + } + ], + override: { + '@type': { + type: 'string', + readOnly: true, + widget: 'string', + oneOf: [{ enum: ['DynamicHttpMetadataResolver'], description: 'value.dynamic-http-metadata-provider' }] + } + } + }, + { + id: 'dynamic', + label: 'label.dynamic-attributes', + index: 3, + initialValues: [], + fields: [ + 'dynamicMetadataResolverAttributes' + ] + }, + { + id: 'plugins', + label: 'label.metadata-filter-plugins', + index: 4, + initialValues: [], + fields: [ + 'metadataFilters' + ] + }, + { + id: 'advanced', + label: 'label.http-settings-advanced', + index: 4, + initialValues: [], + locked: true, + fields: [ + 'httpMetadataResolverAttributes' + ] + } + ] +}; \ No newline at end of file diff --git a/ui/src/app/metadata/domain/provider/FileBackedHttpMetadataProviderDefinition.js b/ui/src/app/metadata/domain/provider/FileBackedHttpMetadataProviderDefinition.js new file mode 100644 index 000000000..37e4216b2 --- /dev/null +++ b/ui/src/app/metadata/domain/provider/FileBackedHttpMetadataProviderDefinition.js @@ -0,0 +1,114 @@ +import { BaseProviderDefinition } from "./BaseProviderDefinition"; + +export const FileBackedHttpMetadataProviderWizard = { + ...BaseProviderDefinition, + label: 'FileBackedHttpMetadataProvider', + type: 'FileBackedHttpMetadataResolver', + schema: '/assets/schema/provider/filebacked-http.schema.json', + steps: [ + { + id: 'common', + label: 'label.common-attributes', + index: 2, + initialValues: [], + fields: [ + 'xmlId', + 'metadataURL', + 'initializeFromBackupFile', + 'backingFile', + 'backupFileInitNextRefreshDelay', + 'requireValidMetadata', + 'failFastInitialization', + 'useDefaultPredicateRegistry', + 'satisfyAnyPredicates' + ] + }, + { + id: 'reloading', + label: 'label.reloading-attributes', + index: 3, + initialValues: [], + fields: [ + 'reloadableMetadataResolverAttributes' + ] + }, + { + id: 'plugins', + label: 'label.metadata-filter-plugins', + index: 4, + initialValues: [ + { key: 'metadataFilters', value: [] } + ], + fields: [ + 'metadataFilters' + ] + }, + { + id: 'summary', + label: 'label.finished', + index: 5, + initialValues: [], + fields: [ + 'enabled' + ] + } + ] +}; + + +export const FileBackedHttpMetadataProviderEditor = { + ...FileBackedHttpMetadataProviderWizard, + schema: 'assets/schema/provider/filebacked-http.schema.json', + steps: [ + { + id: 'common', + label: 'label.common-attributes', + index: 1, + initialValues: [], + fields: [ + 'name', + '@type', + 'enabled', + 'xmlId', + 'metadataURL', + 'initializeFromBackupFile', + 'backingFile', + 'backupFileInitNextRefreshDelay', + 'requireValidMetadata', + 'failFastInitialization', + 'useDefaultPredicateRegistry', + 'satisfyAnyPredicates' + ] + }, + { + id: 'reloading', + label: 'label.reloading-attributes', + index: 2, + initialValues: [], + fields: [ + 'reloadableMetadataResolverAttributes' + ] + }, + { + id: 'plugins', + label: 'label.metadata-filter-plugins', + index: 3, + initialValues: [ + { key: 'metadataFilters', value: [] } + ], + fields: [ + 'metadataFilters' + ] + }, + { + id: 'advanced', + label: 'label.advanced-settings', + index: 4, + initialValues: [], + locked: true, + fields: [ + 'httpMetadataResolverAttributes' + ] + } + ] +}; diff --git a/ui/src/app/metadata/domain/provider/FileSystemMetadataProviderDefinition.js b/ui/src/app/metadata/domain/provider/FileSystemMetadataProviderDefinition.js new file mode 100644 index 000000000..6a06e1d51 --- /dev/null +++ b/ui/src/app/metadata/domain/provider/FileSystemMetadataProviderDefinition.js @@ -0,0 +1,137 @@ +import API_BASE_PATH from "../../../App.constant"; +import { BaseProviderDefinition } from "./BaseProviderDefinition"; + +export const FileSystemMetadataProviderWizard = { + ...BaseProviderDefinition, + label: 'FilesystemMetadataProvider', + type: 'FilesystemMetadataResolver', + schema: `${API_BASE_PATH}/ui/MetadataResolver/FilesystemMetadataResolver`, + steps: [ + { + id: 'common', + label: 'label.common-attributes', + index: 2, + initialValues: [], + fields: [ + 'xmlId', + 'metadataFile', + 'doInitialization' + ], + fieldsets: [ + { + type: 'group-lg', + class: ['col-12'], + fields: [ + 'xmlId', + 'metadataFile', + 'doInitialization' + ] + } + ] + }, + { + id: 'dynamic', + label: 'label.dynamic-attributes', + index: 3, + initialValues: [], + fields: [ + 'reloadableMetadataResolverAttributes' + ], + fieldsets: [ + { + type: 'group-lg', + class: ['col-12'], + fields: [ + 'reloadableMetadataResolverAttributes' + ] + } + ] + }, + { + id: 'summary', + label: 'label.finished', + index: 4, + initialValues: [], + fields: [ + 'enabled' + ], + fieldsets: [ + { + type: 'group-lg', + class: ['col-12'], + fields: [ + 'enabled' + ] + } + ] + } + ] +}; + + +export const FileSystemMetadataProviderEditor = { + ...FileSystemMetadataProviderWizard, + steps: [ + { + id: 'common', + label: 'label.common-attributes', + index: 1, + initialValues: [], + fields: [ + 'name', + 'xmlId', + '@type', + 'metadataFile', + 'enabled', + 'doInitialization' + ], + override: { + '@type': { + type: 'string', + readOnly: true, + widget: 'string', + oneOf: [{ enum: ['FilesystemMetadataResolver'], + description: 'value.file-system-metadata-provider' }] + } + }, + fieldsets: [ + { + type: 'section', + class: ['mb-3'], + fields: [ + 'name', + '@type' + ] + }, + { + type: 'group-lg', + class: ['col-12'], + fields: [ + 'enabled', + 'xmlId', + 'metadataFile', + 'doInitialization' + ] + } + ] + }, + { + id: 'dynamic', + label: 'label.dynamic-attributes', + index: 2, + initialValues: [], + fields: [ + 'reloadableMetadataResolverAttributes' + ], + fieldsets: [ + { + type: 'group-lg', + class: ['col-12'], + fields: [ + 'reloadableMetadataResolverAttributes' + ] + } + ] + } + ] +}; diff --git a/ui/src/app/metadata/domain/provider/LocalDynamicMetadataProviderDefinition.js b/ui/src/app/metadata/domain/provider/LocalDynamicMetadataProviderDefinition.js new file mode 100644 index 000000000..0595d0bad --- /dev/null +++ b/ui/src/app/metadata/domain/provider/LocalDynamicMetadataProviderDefinition.js @@ -0,0 +1,123 @@ +import API_BASE_PATH from "../../../App.constant"; +import { BaseProviderDefinition } from "./BaseProviderDefinition"; + +export const LocalDynamicMetadataProviderWizard = { + ...BaseProviderDefinition, + label: 'LocalDynamicMetadataProvider', + type: 'LocalDynamicMetadataResolver', + schema: `${API_BASE_PATH}/ui/MetadataResolver/LocalDynamicMetadataResolver`, + steps: [ + { + id: 'common', + label: 'label.common-attributes', + index: 2, + initialValues: [], + fields: [ + 'xmlId', + 'sourceDirectory' + ], + fieldsets: [ + { + type: 'group-lg', + class: ['col-12'], + fields: [ + 'xmlId', + 'sourceDirectory' + ] + } + ] + }, + { + id: 'dynamic', + label: 'label.dynamic-attributes', + index: 3, + initialValues: [], + fields: [ + 'dynamicMetadataResolverAttributes' + ], + fieldsets: [ + { + type: 'group-lg', + class: ['col-12'], + fields: [ + 'dynamicMetadataResolverAttributes' + ] + } + ] + }, + { + id: 'summary', + label: 'label.finished', + index: 4, + initialValues: [], + fields: [ + 'enabled' + ], + fieldsets: [ + { + type: 'group-lg', + class: ['col-12'], + fields: [ + 'enabled' + ] + } + ] + } + ] +}; + + +export const LocalDynamicMetadataProviderEditor = { + ...LocalDynamicMetadataProviderWizard, + steps: [ + { + id: 'common', + label: 'label.common-attributes', + index: 1, + initialValues: [], + fields: [ + 'name', + '@type', + 'enabled', + 'xmlId', + 'sourceDirectory', + ], + override: { + '@type': { + type: 'string', + readOnly: true, + widget: 'string', + oneOf: [{ enum: ['LocalDynamicMetadataResolver'], description: 'value.local-dynamic-metadata-provider' }] + } + }, + fieldsets: [ + { + type: 'section', + class: ['mb-3'], + fields: [ + 'name', + '@type' + ] + }, + { + type: 'group-lg', + class: ['col-12'], + fields: [ + 'enabled', + 'xmlId', + 'sourceDirectory', + ] + } + ] + }, + { + id: 'dynamic', + label: 'label.dynamic-attributes', + index: 2, + initialValues: [], + fields: [ + 'dynamicMetadataResolverAttributes' + ] + } + ] +}; diff --git a/ui/src/app/metadata/domain/provider/index.js b/ui/src/app/metadata/domain/provider/index.js new file mode 100644 index 000000000..bc36234e5 --- /dev/null +++ b/ui/src/app/metadata/domain/provider/index.js @@ -0,0 +1,24 @@ +import { FileBackedHttpMetadataProviderWizard, FileBackedHttpMetadataProviderEditor } from './FileBackedHttpMetadataProviderDefinition'; +import { DynamicHttpMetadataProviderWizard, DynamicHttpMetadataProviderEditor } from './DynamicHttpMetadataProviderDefinition'; +import { LocalDynamicMetadataProviderWizard, LocalDynamicMetadataProviderEditor } from './LocalDynamicMetadataProviderDefinition'; +import { FileSystemMetadataProviderWizard, FileSystemMetadataProviderEditor } from './FileSystemMetadataProviderDefinition'; + +export const MetadataProviderWizardTypes = [ + FileBackedHttpMetadataProviderWizard, + DynamicHttpMetadataProviderWizard, + FileSystemMetadataProviderWizard, + LocalDynamicMetadataProviderWizard +]; + +export const MetadataProviderEditorTypes = [ + FileBackedHttpMetadataProviderEditor, + DynamicHttpMetadataProviderEditor, + LocalDynamicMetadataProviderEditor, + FileSystemMetadataProviderEditor +]; + +export const FilterableProviders = [ + FileBackedHttpMetadataProviderEditor.type, + DynamicHttpMetadataProviderEditor.type, + LocalDynamicMetadataProviderEditor.type +]; diff --git a/ui/src/app/metadata/domain/provider/utility/providerFilterProcessor.js b/ui/src/app/metadata/domain/provider/utility/providerFilterProcessor.js new file mode 100644 index 000000000..dc415bae1 --- /dev/null +++ b/ui/src/app/metadata/domain/provider/utility/providerFilterProcessor.js @@ -0,0 +1,23 @@ +export const metadataFilterProcessor = (schema) => { + if (!schema) { + return null; + } + if (!schema.properties || !schema.properties.metadataFilters) { + return schema; + } + const filters = schema.properties.metadataFilters; + const processed = ({ + ...schema, + properties: { + ...schema.properties, + metadataFilters: { + type: 'object', + properties: filters.items.reduce((collection, filterType) => ({ + ...collection, + [filterType.$id]: filterType + }), {}) + } + } + }); + return processed; +}; diff --git a/ui/src/app/metadata/domain/source/SourceDefinition.js b/ui/src/app/metadata/domain/source/SourceDefinition.js index bbf38b740..39d5ed28d 100644 --- a/ui/src/app/metadata/domain/source/SourceDefinition.js +++ b/ui/src/app/metadata/domain/source/SourceDefinition.js @@ -1,8 +1,10 @@ +import API_BASE_PATH from "../../../App.constant"; + export const SourceBase = { label: 'Metadata Source', type: '@MetadataProvider', steps: [], - schema: '', + schema: `${API_BASE_PATH}/ui/MetadataSources`, validatorParams: [/*getAllOtherIds*/], bindings: { @@ -119,7 +121,7 @@ export const SourceBase = { export const SourceEditor = { ...SourceBase, - schema: `/MetadataSources`, + schema: `${API_BASE_PATH}/ui/MetadataSources`, steps: [ { index: 1, diff --git a/ui/src/app/metadata/hoc/MetadataSchema.js b/ui/src/app/metadata/hoc/MetadataSchema.js index ad6574bfe..54b29c681 100644 --- a/ui/src/app/metadata/hoc/MetadataSchema.js +++ b/ui/src/app/metadata/hoc/MetadataSchema.js @@ -2,22 +2,27 @@ import React from 'react'; import { useParams } from 'react-router'; import { useMetadataSchema } from '../hooks/api'; import { getDefinition } from '../domain/index'; +import { MetadataObjectContext } from './MetadataSelector'; export const MetadataSchemaContext = React.createContext(); export const MetadataDefinitionContext = React.createContext(); export function MetadataSchema({ children }) { - let { type } = useParams(); + const metadata = React.useContext(MetadataObjectContext); - const definition = React.useMemo(() => getDefinition(type), [type]); + const { type } = useParams(); + + const definition = React.useMemo(() => getDefinition( + type === 'source' ? type : metadata['@type'] + ), [type, metadata]); const { get, response } = useMetadataSchema(); const [schema, setSchema] = React.useState(); async function loadSchema(d) { - const source = await get(`/${definition.schema}`) + const source = await get(`/${d.schema}`) if (response.ok) { setSchema(source); } diff --git a/ui/src/app/metadata/hoc/MetadataXmlLoader.js b/ui/src/app/metadata/hoc/MetadataXmlLoader.js new file mode 100644 index 000000000..5b5d1e4ab --- /dev/null +++ b/ui/src/app/metadata/hoc/MetadataXmlLoader.js @@ -0,0 +1,32 @@ +import React from 'react'; +import { useParams } from 'react-router'; +import { useMetadataEntityXml } from '../hooks/api'; + +export const MetadataXmlContext = React.createContext(); + +export function MetadataXmlLoader({ children }) { + + let { type, id } = useParams(); + + const { get, response } = useMetadataEntityXml(type); + + const [xml, setXml] = React.useState([]); + + async function loadMetadataXml(id) { + const data = await get(`/${id}`) + if (response.ok) { + setXml(data); + } + } + + /*eslint-disable react-hooks/exhaustive-deps*/ + React.useEffect(() => { loadMetadataXml(id) }, [id]); + + return ( + + {children} + + ); +} + +export default MetadataXmlLoader; \ No newline at end of file diff --git a/ui/src/app/metadata/hooks/api.js b/ui/src/app/metadata/hooks/api.js index 8b76c5233..ac2f66355 100644 --- a/ui/src/app/metadata/hooks/api.js +++ b/ui/src/app/metadata/hooks/api.js @@ -32,14 +32,35 @@ export function useMetadataEntities(type = 'source', opts = {}) { return useFetch(`${API_BASE_PATH}${getMetadataListPath(type)}`, opts); } -export function useMetadataEntity(type = 'source', opts = {}) { +export function useMetadataEntity(type = 'source', opts = { + cachePolicy: 'no-cache' +}) { + return useFetch(`${API_BASE_PATH}${getMetadataPath(type)}`, opts); +} + +export function useMetadataEntityXml(type = 'source', opts = { + interceptors: { + request: ({options}) => { + options.headers['Accept'] = 'application/xml'; + return options; + } + } +}) { return useFetch(`${API_BASE_PATH}${getMetadataPath(type)}`, opts); } export function useMetadataProviderOrder() { - return useFetch(`${API_BASE_PATH}/MetadataResolversPositionOrder`) + return useFetch(`${API_BASE_PATH}/MetadataResolversPositionOrder`); } export function useMetadataSchema() { - return useFetch(`${API_BASE_PATH}/ui`); + return useFetch(``); } + +export function useMetadataHistory(type, id, opts = {}, i) { + + return useFetch(`${API_BASE_PATH}${getMetadataPath(type)}/${id}/Versions`, opts, i); + + // EntityDescriptor/d07d6122-0dd2-433e-baec-b76413b4c842/Versions + // MetadataResolvers/4161d661-2be7-4110-9e91-539669a691e3/Versions +} \ No newline at end of file diff --git a/ui/src/app/metadata/hooks/configuration.js b/ui/src/app/metadata/hooks/configuration.js index e1eb41d93..d7a16edd4 100644 --- a/ui/src/app/metadata/hooks/configuration.js +++ b/ui/src/app/metadata/hooks/configuration.js @@ -7,7 +7,5 @@ export function useMetadataConfiguration(models) { const definition = React.useContext(MetadataDefinitionContext); const schema = React.useContext(MetadataSchemaContext); - console.log(definition, schema); - return getConfigurationSections(models, definition, schema); } \ No newline at end of file diff --git a/ui/yarn.lock b/ui/yarn.lock index 5f9867d98..bc509ce9c 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -4950,6 +4950,11 @@ file-loader@6.1.1: loader-utils "^2.0.0" schema-utils "^3.0.0" +file-saver@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38" + integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA== + file-uri-to-path@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" From cfa28e0426e7a202feec08012327b273987bae0b Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Wed, 28 Apr 2021 12:02:46 -0700 Subject: [PATCH 030/151] Added delete to sources detail --- ui/package.json | 1 + ui/src/app/dashboard/container/SourcesTab.js | 15 +- .../component/MetadataConfiguration.js | 33 --- .../app/metadata/component/MetadataHeader.js | 6 +- .../app/metadata/component/MetadataOptions.js | 71 +++++- .../component/properties/ArrayProperty.js | 18 +- .../component/properties/PropertyValue.js | 4 +- .../component/DeleteSourceConfirmation.js | 58 +++++ .../domain/source/component/SourceList.js | 223 ++++++------------ ui/src/app/metadata/hoc/MetadataXmlLoader.js | 8 +- ui/yarn.lock | 13 + 11 files changed, 234 insertions(+), 216 deletions(-) create mode 100644 ui/src/app/metadata/domain/source/component/DeleteSourceConfirmation.js diff --git a/ui/package.json b/ui/package.json index 0e4e626b2..eeb591172 100644 --- a/ui/package.json +++ b/ui/package.json @@ -20,6 +20,7 @@ "react-infinite-scroll-component": "^6.1.0", "react-router-dom": "^5.2.0", "react-scripts": "4.0.3", + "react-scroll": "^1.8.2", "reactstrap": "^8.9.0", "sass": "^1.32.11", "use-http": "^1.0.20", diff --git a/ui/src/app/dashboard/container/SourcesTab.js b/ui/src/app/dashboard/container/SourcesTab.js index 073aeaa86..94305795b 100644 --- a/ui/src/app/dashboard/container/SourcesTab.js +++ b/ui/src/app/dashboard/container/SourcesTab.js @@ -11,21 +11,18 @@ export function SourcesTab () { const [sources, setSources] = React.useState([]); - const { get, del, response } = useMetadataEntities('source'); + const { get, response } = useMetadataEntities('source', { + cachePolicy: 'no-cache' + }); async function loadSources() { - const sources = await get('/') + const sources = await get('/'); if (response.ok) { setSources(sources); } } - async function deleteSource(id) { - await del(`/${id}`); - if (response.ok) { - loadSources(); - } - } + const updateSources = () => loadSources(); /*eslint-disable react-hooks/exhaustive-deps*/ React.useEffect(() => { loadSources() }, []); @@ -40,7 +37,7 @@ export function SourcesTab () {
- {(searched) => } + {(searched) => }
diff --git a/ui/src/app/metadata/component/MetadataConfiguration.js b/ui/src/app/metadata/component/MetadataConfiguration.js index 4a4584ce2..1ccd73d73 100644 --- a/ui/src/app/metadata/component/MetadataConfiguration.js +++ b/ui/src/app/metadata/component/MetadataConfiguration.js @@ -35,36 +35,3 @@ export function MetadataConfiguration ({ configuration }) { ); } - -/* -
-
-
-

- - 0{{ i + 1 }} - - {{ section.label | translate }} -

-
- -
-
-
- -
- Option - - Value - {{ date | date:DATE_FORMAT }} - -
- -
-
-
-
*/ \ No newline at end of file diff --git a/ui/src/app/metadata/component/MetadataHeader.js b/ui/src/app/metadata/component/MetadataHeader.js index d9fcdc3da..d8a2baa85 100644 --- a/ui/src/app/metadata/component/MetadataHeader.js +++ b/ui/src/app/metadata/component/MetadataHeader.js @@ -3,9 +3,9 @@ import FormattedDate from '../../core/components/FormattedDate'; import Translate from '../../i18n/components/translate'; -export function MetadataHeader ({ model, current = true, enabled = true }) { +export function MetadataHeader ({ model, current = true, enabled = true, children, ...props }) { return ( -
+
@@ -17,7 +17,7 @@ export function MetadataHeader ({ model, current = true, enabled = true }) { By{model.createdBy }
- + {children}

diff --git a/ui/src/app/metadata/component/MetadataOptions.js b/ui/src/app/metadata/component/MetadataOptions.js index 374713f12..13ce53b00 100644 --- a/ui/src/app/metadata/component/MetadataOptions.js +++ b/ui/src/app/metadata/component/MetadataOptions.js @@ -1,7 +1,9 @@ -import { faArrowDown, faHistory } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import React from 'react'; -import { Link, useParams } from 'react-router-dom'; +import { faArrowDown, faHistory, faTrash } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Link, useHistory, useParams } from 'react-router-dom'; + +import { scroller } from 'react-scroll'; import Translate from '../../i18n/components/translate'; @@ -12,25 +14,48 @@ import { MetadataConfiguration } from './MetadataConfiguration'; import { useMetadataConfiguration } from '../hooks/configuration'; import { MetadataViewToggle } from './MetadataViewToggle'; +import { DeleteSourceConfirmation } from '../domain/source/component/DeleteSourceConfirmation'; export function MetadataOptions () { const metadata = React.useContext(MetadataObjectContext); + const history = useHistory(); const { type, id } = useParams(); const configuration = useMetadataConfiguration([metadata]); + const onScrollTo = (element, offset = 0) => { + scroller.scrollTo(element, { + duration: 500, + smooth: true, + offset + }); + }; + + const redirectOnDelete = () => history.push('/dashboard'); + return ( - <> -

+ + {(onDeleteSource) => + <> +
+ enabled={type === 'source' ? metadata.serviceEnabled : metadata.enabled} + model={metadata}> + {type === 'source' && + + } + - + } + ); } \ No newline at end of file diff --git a/ui/src/app/metadata/component/properties/ArrayProperty.js b/ui/src/app/metadata/component/properties/ArrayProperty.js index 979ed95f2..6fa440c2f 100644 --- a/ui/src/app/metadata/component/properties/ArrayProperty.js +++ b/ui/src/app/metadata/component/properties/ArrayProperty.js @@ -43,9 +43,9 @@ export function ArrayProperty ({ property, columns, index, onPreview }) {
{ property.name }
{range.map((i) => -
+
{Object.keys(property.items.properties).map((prop, n) => -
+
{property.differences && Changed: } {property.items.properties &&
@@ -53,7 +53,15 @@ export function ArrayProperty ({ property, columns, index, onPreview }) {
} { property.value.map((version, vIdx) => - + + {version && version[vIdx] && + + } + {(!version || !version[vIdx]) &&
+ - +
+ } +
)}
)} @@ -75,7 +83,7 @@ export function ArrayProperty ({ property, columns, index, onPreview }) { {(v && v.length > 0) &&
    {v.map((item, idx) => -
  • 1 ? 'py-2' : ''} ${'border-0'}`}> +
  • 1 ? 'py-2' : ''} ${'border-0'}`}> {onPreview && isUrl(item) && <>   } - { item } +
  • )}
diff --git a/ui/src/app/metadata/component/properties/PropertyValue.js b/ui/src/app/metadata/component/properties/PropertyValue.js index 91b51d5cd..6ca8e8f53 100644 --- a/ui/src/app/metadata/component/properties/PropertyValue.js +++ b/ui/src/app/metadata/component/properties/PropertyValue.js @@ -18,11 +18,11 @@ export function PropertyValue ({ name, value, columns }) { id={`Popover-${id}`} className="d-block text-truncate" role="definition" - style={{ width }}> + style={columns ? { width } : {}}> {value !== undefined ? value.toString() : (value === false) ? value.toString() : '-'} - {value} + {value.toString()} : '-'} diff --git a/ui/src/app/metadata/domain/source/component/DeleteSourceConfirmation.js b/ui/src/app/metadata/domain/source/component/DeleteSourceConfirmation.js new file mode 100644 index 000000000..b322d096e --- /dev/null +++ b/ui/src/app/metadata/domain/source/component/DeleteSourceConfirmation.js @@ -0,0 +1,58 @@ +import React from 'react'; + +import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; + +import { useMetadataEntity } from '../../../hooks/api'; +import Translate from '../../../../i18n/components/translate'; +import { noop } from 'lodash'; + +export function DeleteSourceConfirmation ({children}) { + + const { del, response } = useMetadataEntity('source'); + + const cb = React.useRef(null); + + const [deleting, setDeleting] = React.useState(null); + + const onConfirm = (id, cb = () => {}) => { + deleteSource(deleting); + setDeleting(null); + } + + const onDeleteSource = (id, onComplete) => { + setDeleting(id); + cb.current = onComplete; + }; + + async function deleteSource(id) { + await del(`/${id}`); + if (response.ok) { + cb.current && typeof cb.current === 'function' ? cb.current() : noop(); + } + } + + return ( + <> + {children(onDeleteSource)} + setDeleting(null)}> + setDeleting(null)}>Delete Metadata Source? + + +

+ You are deleting a metadata source. This cannot be undone. Continue? +

+
+ + {' '} + + +
+ + ); +} \ No newline at end of file diff --git a/ui/src/app/metadata/domain/source/component/SourceList.js b/ui/src/app/metadata/domain/source/component/SourceList.js index ae59abf06..3315ff74f 100644 --- a/ui/src/app/metadata/domain/source/component/SourceList.js +++ b/ui/src/app/metadata/domain/source/component/SourceList.js @@ -1,171 +1,88 @@ import React from 'react'; import { Link } from 'react-router-dom'; -import { Badge, UncontrolledPopover, PopoverBody, Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; +import { Badge, UncontrolledPopover, PopoverBody } from 'reactstrap'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faTrash, faExclamationTriangle, faCheck } from '@fortawesome/free-solid-svg-icons'; +import { faTrash, faCheck } from '@fortawesome/free-solid-svg-icons'; import FormattedDate from '../../../../core/components/FormattedDate'; import Translate from '../../../../i18n/components/translate'; import { Scroller } from '../../../../dashboard/component/Scroller'; +import { DeleteSourceConfirmation } from './DeleteSourceConfirmation'; export default function SourceList({ entities, onDelete, onEnable }) { - - const [modal, setModal] = React.useState(false); - - const toggle = () => setModal(!modal); - - const [deleting, setDeleting] = React.useState(null); - - const deleteSource = (id) => { - onDelete(deleting); - setDeleting(null); - } - return ( - <> - - {(limited) => -
- - - - - - - - - - - - - - {limited.map((source, idx) => - - - - - - - + + {(onDeleteSource) => + + {(limited) => +
+ +
TitleEntity IDAuthorCreated DateEnabled
- {source.serviceProviderName} - - {source.entityId} - - {source.createdBy} - - {onEnable ? - - : - - - - } - - - {source.serviceEnabled && - - A metadata source must be disabled before it can be deleted. - - } -
+ + + + + + + + - )} - -
TitleEntity IDAuthorCreated DateEnabled
-
- } -
- setDeleting(null)}> - Delete Metadata Source? - - -

- You are deleting a metadata source. This cannot be undone. Continue? -

-
- - {' '} - - -
- + + + {limited.map((source, idx) => + + + {source.serviceProviderName} + + + {source.entityId} + + + {source.createdBy} + + + + {onEnable ? + + : + + + + } + + + + {source.serviceEnabled && + + A metadata source must be disabled before it can be deleted. + + } + + + )} + + +
+ } + + } + ); } - -/* - - - {{ resolver.name }} - - - {{ resolver.name }} - - {{ resolver.getDisplayId() }} - {{ resolver.createdBy ? resolver.createdBy : '—' }} - - {{ resolver.getCreationDate() ? (resolver.getCreationDate() | customDate) : '—' }} - - - - - - Incomplete Form - - - - - - - - - {{ (resolver.enabled ? 'value.enabled' : 'value.disabled') | translate }} - - - - - - - - */ \ No newline at end of file diff --git a/ui/src/app/metadata/hoc/MetadataXmlLoader.js b/ui/src/app/metadata/hoc/MetadataXmlLoader.js index 5b5d1e4ab..ffc1c8bcf 100644 --- a/ui/src/app/metadata/hoc/MetadataXmlLoader.js +++ b/ui/src/app/metadata/hoc/MetadataXmlLoader.js @@ -10,7 +10,7 @@ export function MetadataXmlLoader({ children }) { const { get, response } = useMetadataEntityXml(type); - const [xml, setXml] = React.useState([]); + const [xml, setXml] = React.useState(); async function loadMetadataXml(id) { const data = await get(`/${id}`) @@ -20,7 +20,11 @@ export function MetadataXmlLoader({ children }) { } /*eslint-disable react-hooks/exhaustive-deps*/ - React.useEffect(() => { loadMetadataXml(id) }, [id]); + React.useEffect(() => { + if (type === 'source') { + loadMetadataXml(id) + } + }, [id]); return ( diff --git a/ui/yarn.lock b/ui/yarn.lock index bc509ce9c..06eb13aa0 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -7017,6 +7017,11 @@ lodash.templatesettings@^4.0.0: dependencies: lodash._reinterpolate "^3.0.0" +lodash.throttle@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" + integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ= + lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" @@ -9227,6 +9232,14 @@ react-scripts@4.0.3: optionalDependencies: fsevents "^2.1.3" +react-scroll@^1.8.2: + version "1.8.2" + resolved "https://registry.yarnpkg.com/react-scroll/-/react-scroll-1.8.2.tgz#68e35b74ae296c88e7863393c9fd49f05afa29f5" + integrity sha512-f2ZEG5fsPbPTySI9ekcFpETCcNlqbmwbQj9hhzYK8tkgv+PA8APatSt66o/q0KSkDZxyT98ONTtXp9x0lyowEw== + dependencies: + lodash.throttle "^4.1.1" + prop-types "^15.7.2" + react-transition-group@^2.3.1: version "2.9.0" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d" From 6f3ec2fa40f2baaeb2bc34571ed48c24306e4f2d Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Wed, 28 Apr 2021 15:00:25 -0700 Subject: [PATCH 031/151] Adding filter list --- ui/src/app/App.constant.js | 5 + ui/src/app/core/utility/is_valid_regex.js | 11 ++ ui/src/app/core/utility/remove_null.js | 23 ++++ ui/src/app/dashboard/component/Ordered.js | 10 +- .../app/dashboard/container/ProvidersTab.js | 2 +- ui/src/app/metadata/Metadata.js | 4 +- .../app/metadata/component/MetadataOptions.js | 11 +- .../component/properties/ObjectProperty.js | 6 +- .../component/properties/PropertyValue.js | 2 +- .../EntityAttributesFilterDefinition.js | 109 ++++++++++++++++++ .../domain/filter/NameIdFilterDefinition.js | 81 +++++++++++++ .../domain/filter/component/MetadataFilter.js | 0 .../MetadataFilterConfigurationList.js | 46 ++++++++ .../MetadataFilterConfigurationListItem.js | 67 +++++++++++ .../component/MetadataFilterEditorList.js | 88 ++++++++++++++ .../filter/component/MetadataFilters.js | 26 +++++ ui/src/app/metadata/domain/filter/index.js | 16 +++ ui/src/app/metadata/domain/index.js | 6 +- .../domain/provider/component/ProviderList.js | 76 ------------ ui/src/app/metadata/hoc/MetadataSchema.js | 9 +- ui/src/app/metadata/hoc/MetadataSelector.js | 2 +- ui/src/app/metadata/hooks/configuration.js | 5 +- ui/src/theme/project/filters.scss | 15 +++ ui/src/theme/project/index.scss | 5 +- 24 files changed, 518 insertions(+), 107 deletions(-) create mode 100644 ui/src/app/core/utility/is_valid_regex.js create mode 100644 ui/src/app/core/utility/remove_null.js create mode 100644 ui/src/app/metadata/domain/filter/EntityAttributesFilterDefinition.js create mode 100644 ui/src/app/metadata/domain/filter/NameIdFilterDefinition.js create mode 100644 ui/src/app/metadata/domain/filter/component/MetadataFilter.js create mode 100644 ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationList.js create mode 100644 ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationListItem.js create mode 100644 ui/src/app/metadata/domain/filter/component/MetadataFilterEditorList.js create mode 100644 ui/src/app/metadata/domain/filter/component/MetadataFilters.js create mode 100644 ui/src/app/metadata/domain/filter/index.js create mode 100644 ui/src/theme/project/filters.scss diff --git a/ui/src/app/App.constant.js b/ui/src/app/App.constant.js index 206ee74c7..c08fda543 100644 --- a/ui/src/app/App.constant.js +++ b/ui/src/app/App.constant.js @@ -1,2 +1,7 @@ export const API_BASE_PATH = 'api'; + +export const FILTER_PLUGIN_TYPES = ['RequiredValidUntil', 'SignatureValidation', 'EntityRoleWhiteList']; + + + export default API_BASE_PATH; diff --git a/ui/src/app/core/utility/is_valid_regex.js b/ui/src/app/core/utility/is_valid_regex.js new file mode 100644 index 000000000..3f18d0191 --- /dev/null +++ b/ui/src/app/core/utility/is_valid_regex.js @@ -0,0 +1,11 @@ +export function isValidRegex(pattern) { + if (!pattern) { + return false; + } + try { + new RegExp(pattern); + } catch (err) { + return false; + } + return true; +}; diff --git a/ui/src/app/core/utility/remove_null.js b/ui/src/app/core/utility/remove_null.js new file mode 100644 index 000000000..30f342172 --- /dev/null +++ b/ui/src/app/core/utility/remove_null.js @@ -0,0 +1,23 @@ +export function checkByType(value) { + switch (typeof value) { + case 'object': { + return Object.keys(value).filter(k => !!value[k]).length > 0; + } + default: { + return true; + } + } +} + +export function removeNull(attribute, discardObjects = false) { + if (!attribute) { return {}; } + let removed = Object.keys(attribute).reduce((coll, val, index) => { + if (attribute[val] !== null) { + if (!discardObjects || checkByType(attribute[val])) { + coll[val] = attribute[val]; + } + } + return coll; + }, {}); + return removed; +} \ No newline at end of file diff --git a/ui/src/app/dashboard/component/Ordered.js b/ui/src/app/dashboard/component/Ordered.js index 0fe8b1675..76d713202 100644 --- a/ui/src/app/dashboard/component/Ordered.js +++ b/ui/src/app/dashboard/component/Ordered.js @@ -5,10 +5,6 @@ import last from 'lodash/last'; import API_BASE_PATH from '../../App.constant'; import { array_move } from '../../core/utility/array_move'; -const orderPaths = { - provider: `/MetadataResolversPositionOrder` -}; - export const getId = (entity) => { return entity.resourceId ? entity.resourceId : entity.id; }; @@ -24,7 +20,7 @@ export const mergeOrderFn = (entities, order) => { return ordered; }; -export function Ordered ({type = 'provider', entities, children}) { +export function Ordered ({path = '/MetadataResolvers', entities, children}) { const orderEntities = (orderById, list) => { setOrdered(mergeOrderFn(list, orderById)); @@ -41,7 +37,7 @@ export function Ordered ({type = 'provider', entities, children}) { const [lastId, setLastId] = React.useState(null); async function changeOrder(resourceIds) { - await post(`${orderPaths[type]}`, { + await post(path, { resourceIds }); if (response.ok) { @@ -62,7 +58,7 @@ export function Ordered ({type = 'provider', entities, children}) { }; async function loadOrder () { - const o = await get(`${orderPaths[type]}`); + const o = await get(path); if (response.ok) { const ids = o.resourceIds; setOrder(ids); diff --git a/ui/src/app/dashboard/container/ProvidersTab.js b/ui/src/app/dashboard/container/ProvidersTab.js index 27d89ece5..7da1c306f 100644 --- a/ui/src/app/dashboard/container/ProvidersTab.js +++ b/ui/src/app/dashboard/container/ProvidersTab.js @@ -39,7 +39,7 @@ export function ProvidersTab () {
- + {(ordered, first, last, onOrderUp, onOrderDown) => {(searched) => + {(entity) => - + @@ -37,6 +38,7 @@ export function Metadata () { + } ); } \ No newline at end of file diff --git a/ui/src/app/metadata/component/MetadataOptions.js b/ui/src/app/metadata/component/MetadataOptions.js index 13ce53b00..439d449e5 100644 --- a/ui/src/app/metadata/component/MetadataOptions.js +++ b/ui/src/app/metadata/component/MetadataOptions.js @@ -15,6 +15,9 @@ import { MetadataConfiguration } from './MetadataConfiguration'; import { useMetadataConfiguration } from '../hooks/configuration'; import { MetadataViewToggle } from './MetadataViewToggle'; import { DeleteSourceConfirmation } from '../domain/source/component/DeleteSourceConfirmation'; +import { MetadataFilters } from '../domain/filter/component/MetadataFilters'; +import { MetadataFilterConfigurationList } from '../domain/filter/component/MetadataFilterConfigurationList'; +import { MetadataFilterTypes } from '../domain/filter'; export function MetadataOptions () { @@ -87,11 +90,9 @@ export function MetadataOptions () {
- {/**/} + + + }
diff --git a/ui/src/app/metadata/component/properties/ObjectProperty.js b/ui/src/app/metadata/component/properties/ObjectProperty.js index f9a03aaac..8494d24a7 100644 --- a/ui/src/app/metadata/component/properties/ObjectProperty.js +++ b/ui/src/app/metadata/component/properties/ObjectProperty.js @@ -5,15 +5,13 @@ import { ArrayProperty } from './ArrayProperty'; import Translate from '../../../i18n/components/translate'; export function ObjectProperty ({ property, columns, onPreview }) { - - const getProperty = (prop, idx) => { switch(prop.type) { case 'array': return case 'object': return - {prop.name &&
} + {prop.name &&
}
default: @@ -32,7 +30,7 @@ export function ObjectProperty ({ property, columns, onPreview }) { -
{{ prop.name | translate }}
+
{{ prop.name | translate }}
diff --git a/ui/src/app/metadata/component/properties/PropertyValue.js b/ui/src/app/metadata/component/properties/PropertyValue.js index 6ca8e8f53..566489d77 100644 --- a/ui/src/app/metadata/component/properties/PropertyValue.js +++ b/ui/src/app/metadata/component/properties/PropertyValue.js @@ -12,7 +12,7 @@ export function PropertyValue ({ name, value, columns }) { return ( <> - { name && value && value !== false ? + { name && value !== null && value !== undefined ? <> { + let errors; + // iterate all customer + Object.keys(value).forEach((key) => { + const item = value[key]; + const validatorKey = `/${key}`; + const validator = validators.hasOwnProperty(validatorKey) ? validators[validatorKey] : null; + const error = validator ? validator(item, { path: `/${key}` }, form_current) : null; + if (error && error.invalidate) { + errors = errors || []; + errors.push(error); + } + }); + return errors; + }, + '/name': (value, property, form) => { + const err = namesList.indexOf(value) > -1 ? { + code: 'INVALID_NAME', + path: `#${property.path}`, + message: 'message.name-must-be-unique', + params: [value], + invalidate: true + } : null; + return err; + }, + '/relyingPartyOverrides': (value, property, form) => { + if (!value.signAssertion && value.dontSignResponse) { + return { + code: 'INVALID_SIGNING', + path: `#${property.path}`, + message: 'message.invalid-signing', + params: [value], + invalidate: false + }; + } + return null; + }, + '/entityAttributesFilterTarget': (value, property, form) => { + if (!form || !form.value || !form.value.entityAttributesFilterTarget || + form.value.entityAttributesFilterTarget.entityAttributesFilterTargetType !== 'REGEX') { + return null; + } + return isValidRegex(value.value[0]) ? null : { + code: 'INVALID_REGEX', + path: `#${property.path}`, + message: 'message.invalid-regex-pattern', + params: [value.value[0]], + invalidate: true + }; + }, + }; + return validators; + }, + parser: (changes) => { + return { + ...changes, + relyingPartyOverrides: removeNull(changes) + }; + }, + formatter: (changes) => changes +}; + + +export const EntityAttributesFilterEditor= { + ...EntityAttributesFilterWizard, + steps: [ + { + id: 'common', + label: 'label.target', + index: 1, + fields: [ + 'name', + '@type', + 'resourceId', + 'filterEnabled', + 'entityAttributesFilterTarget' + ] + }, + { + id: 'options', + label: 'label.options', + index: 2, + initialValues: [], + fields: [ + 'relyingPartyOverrides' + ] + }, + { + id: 'attributes', + label: 'label.attributes', + index: 3, + fields: [ + 'attributeRelease' + ] + } + ] +}; \ No newline at end of file diff --git a/ui/src/app/metadata/domain/filter/NameIdFilterDefinition.js b/ui/src/app/metadata/domain/filter/NameIdFilterDefinition.js new file mode 100644 index 000000000..7ff51a827 --- /dev/null +++ b/ui/src/app/metadata/domain/filter/NameIdFilterDefinition.js @@ -0,0 +1,81 @@ +import API_BASE_PATH from "../../../App.constant"; +import { isValidRegex } from "../../../core/utility/is_valid_regex"; + +export const NameIDFilterWizard = { + label: 'NameIDFormat', + type: 'NameIDFormat', + schema: `${API_BASE_PATH}/ui/NameIdFormatFilter`, + steps: [], + //validatorParams: [getFilterNames], + getValidators(namesList) { + const validators = { + '/': (value, property, form_current) => { + let errors; + // iterate all customer + Object.keys(value).forEach((key) => { + const item = value[key]; + const validatorKey = `/${key}`; + const validator = validators.hasOwnProperty(validatorKey) ? validators[validatorKey] : null; + const error = validator ? validator(item, { path: `/${key}` }, form_current) : null; + if (error) { + errors = errors || []; + errors.push(error); + } + }); + return errors; + }, + '/name': (value, property, form) => { + const err = namesList.indexOf(value) > -1 ? { + code: 'INVALID_NAME', + path: `#${property.path}`, + message: 'message.name-must-be-unique', + params: [value] + } : null; + return err; + }, + '/nameIdFormatFilterTarget': (value, property, form) => { + if (!form || !form.value || !form.value.nameIdFormatFilterTarget || + form.value.nameIdFormatFilterTarget.nameIdFormatFilterTargetType !== 'REGEX') { + return null; + } + return isValidRegex(value.value[0]) ? null : { + code: 'INVALID_REGEX', + path: `#${property.path}`, + message: 'message.invalid-regex-pattern', + params: [value.value[0]] + }; + } + }; + return validators; + }, + parser: (changes) => changes, + formatter: (changes) => changes +}; + +export const NameIDFilterEditor = { + ...NameIDFilterWizard, + steps: [ + { + id: 'common', + label: 'label.target', + index: 1, + fields: [ + 'name', + 'filterEnabled', + '@type', + 'resourceId', + 'nameIdFormatFilterTarget' + ] + }, + { + id: 'options', + label: 'label.options', + index: 1, + initialValues: [], + fields: [ + 'removeExistingFormats', + 'formats' + ] + } + ] +}; \ No newline at end of file diff --git a/ui/src/app/metadata/domain/filter/component/MetadataFilter.js b/ui/src/app/metadata/domain/filter/component/MetadataFilter.js new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationList.js b/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationList.js new file mode 100644 index 000000000..86dae0368 --- /dev/null +++ b/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationList.js @@ -0,0 +1,46 @@ +import React from 'react'; + +import { Ordered } from '../../../../dashboard/component/Ordered'; +import { Translate } from '../../../../i18n/components/translate'; +import { MetadataFiltersContext } from './MetadataFilters'; + +import { MetadataFilterConfigurationListItem } from './MetadataFilterConfigurationListItem'; + +export function MetadataFilterConfigurationList ({provider}) { + const filters = React.useContext(MetadataFiltersContext); + const removeFilter = () => {} + const editable = true; + + return ( + + {(ordered, first, last, onOrderUp, onOrderDown) => + <> + {ordered.length > 0 && +
    + {ordered.map((filter, i) => +
  • + +
  • + )} +
+ } + { filters && filters.length < 1 && +
+

No Filters

+

No filters have been added to this Metadata Provider

+
+ } + + } +
+ ); +} \ No newline at end of file diff --git a/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationListItem.js b/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationListItem.js new file mode 100644 index 000000000..e3f863f40 --- /dev/null +++ b/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationListItem.js @@ -0,0 +1,67 @@ +import React from 'react'; + +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faArrowCircleDown, faArrowCircleUp } from '@fortawesome/free-solid-svg-icons'; + +import { Translate } from '../../../../i18n/components/translate'; + +export function MetadataFilterConfigurationListItem ({ filter, isLast, isFirst, onOrderUp, onOrderDown, editable, onRemove, index }) { + const [open, setOpen] = React.useState(false); + + return (<> +
+ { index + 1 } + {editable && +
+ + +
+ } + + { filter['@type'] } + + + + + +
+ + ); +} + +/* + +
+
+
+
+ +   + Edit + + +
+
+ + +
+*/ \ No newline at end of file diff --git a/ui/src/app/metadata/domain/filter/component/MetadataFilterEditorList.js b/ui/src/app/metadata/domain/filter/component/MetadataFilterEditorList.js new file mode 100644 index 000000000..de698b027 --- /dev/null +++ b/ui/src/app/metadata/domain/filter/component/MetadataFilterEditorList.js @@ -0,0 +1,88 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faArrowCircleDown, faArrowCircleUp } from '@fortawesome/free-solid-svg-icons'; + +import { Ordered } from '../../../../dashboard/component/Ordered'; +import { Translate } from '../../../../i18n/components/translate'; +import { MetadataFiltersContext } from './MetadataFilters'; + +export function MetadataFilterEditorList ({provider}) { + const filters = React.useContext(MetadataFiltersContext); + + const onToggleEnabled = () => {} + const removeFilter = () => {} + + const disabled = false; + + return ( + + {(ordered, first, last, onOrderUp, onOrderDown) => + + + + + + + + + + + + + + {ordered.map((filter, i) => + + + + + + + + + + )} + +
Filter NameFilter TypeEnabled?EditDelete
+
+ + +
+
{i + 1}{filter.name}{filter['@type']} +
+
+ onToggleEnabled(filter)} /> + +
+ {filter.disabled && } +
+
+ + + Edit + + + +
+ } +
+ ); +} \ No newline at end of file diff --git a/ui/src/app/metadata/domain/filter/component/MetadataFilters.js b/ui/src/app/metadata/domain/filter/component/MetadataFilters.js new file mode 100644 index 000000000..6969dfcb7 --- /dev/null +++ b/ui/src/app/metadata/domain/filter/component/MetadataFilters.js @@ -0,0 +1,26 @@ +import React from 'react'; +import { useMetadataEntities } from '../../../hooks/api'; + +export const MetadataFiltersContext = React.createContext(); + +export function MetadataFilters ({ providerId, types = [], children }) { + + const { get, response } = useMetadataEntities('provider'); + + const [filters, setFilters] = React.useState([]); + + async function loadFilters(id) { + const list = await get(`/${id}/Filters`); + if (response.ok) { + setFilters(list.filter(f => types.length > 1 ? types.indexOf(f['@type']) > -1 : true)); + } + } + + /*eslint-disable react-hooks/exhaustive-deps*/ + React.useEffect(() => { loadFilters(providerId) }, [providerId]); + + + return ( + {children} + ); +} \ No newline at end of file diff --git a/ui/src/app/metadata/domain/filter/index.js b/ui/src/app/metadata/domain/filter/index.js new file mode 100644 index 000000000..f51b8bc5a --- /dev/null +++ b/ui/src/app/metadata/domain/filter/index.js @@ -0,0 +1,16 @@ +import { EntityAttributesFilterWizard, EntityAttributesFilterEditor } from './EntityAttributesFilterDefinition'; +import { NameIDFilterWizard, NameIDFilterEditor } from './NameIdFilterDefinition'; + +export const MetadataFilterWizardTypes = { + EntityAttributes: EntityAttributesFilterWizard, + NameIDFormat: NameIDFilterWizard +}; + +export const MetadataFilterEditorTypes = [ + EntityAttributesFilterEditor, + NameIDFilterEditor +]; + +export const MetadataFilterTypes = [ + ...MetadataFilterEditorTypes.map((t) => t.type) +]; \ No newline at end of file diff --git a/ui/src/app/metadata/domain/index.js b/ui/src/app/metadata/domain/index.js index 5cd284162..6565c1e85 100644 --- a/ui/src/app/metadata/domain/index.js +++ b/ui/src/app/metadata/domain/index.js @@ -1,3 +1,4 @@ +import { MetadataFilterEditorTypes } from './filter'; import { MetadataProviderEditorTypes } from './provider'; import { SourceEditor } from "./source/SourceDefinition"; @@ -8,8 +9,9 @@ export const editors = { export const ProviderEditorTypes = [ ...MetadataProviderEditorTypes ]; -export const FilterEditorTypes = []; - +export const FilterEditorTypes = [ + ...MetadataFilterEditorTypes +]; export const getDefinition = (type) => ProviderEditorTypes.find(def => def.type === type) || diff --git a/ui/src/app/metadata/domain/provider/component/ProviderList.js b/ui/src/app/metadata/domain/provider/component/ProviderList.js index 9c353e0de..dc8171bc1 100644 --- a/ui/src/app/metadata/domain/provider/component/ProviderList.js +++ b/ui/src/app/metadata/domain/provider/component/ProviderList.js @@ -51,25 +51,6 @@ export default function ProviderList({ entities, reorder = true, first, last, on
- - - {/* -
-   - - -

- */ } {provider.name} @@ -91,60 +72,3 @@ export default function ProviderList({ entities, reorder = true, first, last, on ); } - -/* - - - {{ resolver.name }} - - - {{ resolver.name }} - - {{ resolver.getDisplayId() }} - {{ resolver.createdBy ? resolver.createdBy : '—' }} - - {{ resolver.getCreationDate() ? (resolver.getCreationDate() | customDate) : '—' }} - - - - - - Incomplete Form - - - - - - - - - {{ (resolver.enabled ? 'value.enabled' : 'value.disabled') | translate }} - - - - - - - - */ \ No newline at end of file diff --git a/ui/src/app/metadata/hoc/MetadataSchema.js b/ui/src/app/metadata/hoc/MetadataSchema.js index 54b29c681..2dcbb76ae 100644 --- a/ui/src/app/metadata/hoc/MetadataSchema.js +++ b/ui/src/app/metadata/hoc/MetadataSchema.js @@ -2,20 +2,17 @@ import React from 'react'; import { useParams } from 'react-router'; import { useMetadataSchema } from '../hooks/api'; import { getDefinition } from '../domain/index'; -import { MetadataObjectContext } from './MetadataSelector'; export const MetadataSchemaContext = React.createContext(); export const MetadataDefinitionContext = React.createContext(); -export function MetadataSchema({ children }) { - - const metadata = React.useContext(MetadataObjectContext); +export function MetadataSchema({ entity, children }) { const { type } = useParams(); const definition = React.useMemo(() => getDefinition( - type === 'source' ? type : metadata['@type'] - ), [type, metadata]); + type === 'source' ? type : entity['@type'] + ), [type, entity]); const { get, response } = useMetadataSchema(); diff --git a/ui/src/app/metadata/hoc/MetadataSelector.js b/ui/src/app/metadata/hoc/MetadataSelector.js index be7ab62c6..88ebc82a1 100644 --- a/ui/src/app/metadata/hoc/MetadataSelector.js +++ b/ui/src/app/metadata/hoc/MetadataSelector.js @@ -29,7 +29,7 @@ export function MetadataSelector ({ children }) { {type && {metadata && metadata.version && - {children} + {children(metadata)} } } diff --git a/ui/src/app/metadata/hooks/configuration.js b/ui/src/app/metadata/hooks/configuration.js index d7a16edd4..b65b72a0f 100644 --- a/ui/src/app/metadata/hooks/configuration.js +++ b/ui/src/app/metadata/hooks/configuration.js @@ -7,5 +7,8 @@ export function useMetadataConfiguration(models) { const definition = React.useContext(MetadataDefinitionContext); const schema = React.useContext(MetadataSchemaContext); - return getConfigurationSections(models, definition, schema); + const processed = definition.schemaPreprocessor ? + definition.schemaPreprocessor(schema) : schema; + + return getConfigurationSections(models, definition, processed); } \ No newline at end of file diff --git a/ui/src/theme/project/filters.scss b/ui/src/theme/project/filters.scss new file mode 100644 index 000000000..e38d11791 --- /dev/null +++ b/ui/src/theme/project/filters.scss @@ -0,0 +1,15 @@ +.filter-list.table { + .td-sm { + max-width: 100px; + } + .td-xs { + max-width: 20px; + } + .td-lg { + width: 30%; + } + + td { + vertical-align: middle; + } +} \ No newline at end of file diff --git a/ui/src/theme/project/index.scss b/ui/src/theme/project/index.scss index e1640fd10..2fcd0651a 100644 --- a/ui/src/theme/project/index.scss +++ b/ui/src/theme/project/index.scss @@ -7,9 +7,10 @@ @import './typography'; @import './list'; @import './tabs'; -@import './table.scss'; +@import './table'; @import './utility'; -@import './notifications.scss'; +@import './notifications'; +@import './filters'; html, body { height: 100%; From b407d4fb934a92580f2e21da5ac804bb8d71b9f2 Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Thu, 29 Apr 2021 09:04:55 -0700 Subject: [PATCH 032/151] Added filter list to configuration page --- ui/src/app/dashboard/component/Ordered.js | 12 +-- .../app/dashboard/container/ProvidersTab.js | 4 +- .../app/metadata/component/MetadataOptions.js | 12 +-- .../properties/FilterTargetProperty.js | 19 +++++ .../component/properties/ObjectProperty.js | 33 +++----- .../domain/filter/NameIdFilterDefinition.js | 2 +- .../MetadataFilterConfigurationListItem.js | 83 +++++++++++-------- ui/src/app/metadata/hooks/configuration.js | 11 ++- 8 files changed, 104 insertions(+), 72 deletions(-) create mode 100644 ui/src/app/metadata/component/properties/FilterTargetProperty.js diff --git a/ui/src/app/dashboard/component/Ordered.js b/ui/src/app/dashboard/component/Ordered.js index 76d713202..117e28eed 100644 --- a/ui/src/app/dashboard/component/Ordered.js +++ b/ui/src/app/dashboard/component/Ordered.js @@ -20,7 +20,7 @@ export const mergeOrderFn = (entities, order) => { return ordered; }; -export function Ordered ({path = '/MetadataResolvers', entities, children}) { +export function Ordered({ path = '/MetadataResolversPositionOrder', entities, children, prop = null}) { const orderEntities = (orderById, list) => { setOrdered(mergeOrderFn(list, orderById)); @@ -37,9 +37,11 @@ export function Ordered ({path = '/MetadataResolvers', entities, children}) { const [lastId, setLastId] = React.useState(null); async function changeOrder(resourceIds) { - await post(path, { - resourceIds - }); + await post(path, prop ? { + [prop]: resourceIds + } : [ + ...resourceIds + ]); if (response.ok) { loadOrder(); } @@ -60,7 +62,7 @@ export function Ordered ({path = '/MetadataResolvers', entities, children}) { async function loadOrder () { const o = await get(path); if (response.ok) { - const ids = o.resourceIds; + const ids = prop ? o.hasOwnProperty(prop) ? o[prop] : o : o; setOrder(ids); setFirstId(first(ids)); setLastId(last(ids)); diff --git a/ui/src/app/dashboard/container/ProvidersTab.js b/ui/src/app/dashboard/container/ProvidersTab.js index 7da1c306f..f5a2d719b 100644 --- a/ui/src/app/dashboard/container/ProvidersTab.js +++ b/ui/src/app/dashboard/container/ProvidersTab.js @@ -17,7 +17,7 @@ export function ProvidersTab () { const { get, response } = useMetadataEntities('provider'); async function loadProviders() { - const providers = await get('/') + const providers = await get('') if (response.ok) { setProviders(providers); } @@ -39,7 +39,7 @@ export function ProvidersTab () {
- + {(ordered, first, last, onOrderUp, onOrderDown) => {(searched) => { scroller.scrollTo(element, { @@ -85,7 +87,7 @@ export function MetadataOptions () {
-   +   Add Filter
@@ -97,7 +99,7 @@ export function MetadataOptions () { }
diff --git a/ui/src/app/metadata/component/properties/FilterTargetProperty.js b/ui/src/app/metadata/component/properties/FilterTargetProperty.js new file mode 100644 index 000000000..cc7490af8 --- /dev/null +++ b/ui/src/app/metadata/component/properties/FilterTargetProperty.js @@ -0,0 +1,19 @@ +import React from 'react'; +import { ArrayProperty } from './ArrayProperty'; +import { PrimitiveProperty } from './PrimitiveProperty'; + +export function FilterTargetProperty ({ property, columns, onPreview }) { + return ( + <> + {property.properties.map((prop, idx) => + + { prop.type === 'array' ? + + : + + } + + )} + + ); +} \ No newline at end of file diff --git a/ui/src/app/metadata/component/properties/ObjectProperty.js b/ui/src/app/metadata/component/properties/ObjectProperty.js index 8494d24a7..a04e082d0 100644 --- a/ui/src/app/metadata/component/properties/ObjectProperty.js +++ b/ui/src/app/metadata/component/properties/ObjectProperty.js @@ -3,6 +3,7 @@ import React from 'react'; import { PrimitiveProperty } from './PrimitiveProperty'; import { ArrayProperty } from './ArrayProperty'; import Translate from '../../../i18n/components/translate'; +import { FilterTargetProperty } from './FilterTargetProperty'; export function ObjectProperty ({ property, columns, onPreview }) { const getProperty = (prop, idx) => { @@ -10,9 +11,19 @@ export function ObjectProperty ({ property, columns, onPreview }) { case 'array': return case 'object': + if (prop.widget && prop.widget.id && prop.widget.id === 'filter-target') { + console.log(prop); + } return - {prop.name &&
} - + {prop.widget && prop.widget.id && prop.widget.id === 'filter-target' ? + + : + + {prop.name &&
} + +
+ } +
default: return @@ -25,21 +36,3 @@ export function ObjectProperty ({ property, columns, onPreview }) { ); } - -/* - - - -
{{ prop.name | translate }}
- -
- -
- - - -*/ \ No newline at end of file diff --git a/ui/src/app/metadata/domain/filter/NameIdFilterDefinition.js b/ui/src/app/metadata/domain/filter/NameIdFilterDefinition.js index 7ff51a827..3c61e490b 100644 --- a/ui/src/app/metadata/domain/filter/NameIdFilterDefinition.js +++ b/ui/src/app/metadata/domain/filter/NameIdFilterDefinition.js @@ -70,7 +70,7 @@ export const NameIDFilterEditor = { { id: 'options', label: 'label.options', - index: 1, + index: 2, initialValues: [], fields: [ 'removeExistingFormats', diff --git a/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationListItem.js b/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationListItem.js index e3f863f40..fe6938902 100644 --- a/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationListItem.js +++ b/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationListItem.js @@ -1,12 +1,35 @@ import React from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faArrowCircleDown, faArrowCircleUp } from '@fortawesome/free-solid-svg-icons'; +import { faArrowCircleDown, faArrowCircleUp, faChevronUp, faEdit, faTrash } from '@fortawesome/free-solid-svg-icons'; import { Translate } from '../../../../i18n/components/translate'; +import { Link } from 'react-router-dom'; +import { getDefinition } from '../../../domain/index'; +import { useMetadataSchema } from '../../../hooks/api'; +import { MetadataConfiguration } from '../../../component/MetadataConfiguration'; +import { useMetadataConfiguration } from '../../../hooks/configuration'; export function MetadataFilterConfigurationListItem ({ filter, isLast, isFirst, onOrderUp, onOrderDown, editable, onRemove, index }) { const [open, setOpen] = React.useState(false); + + const definition = React.useMemo(() => getDefinition(filter['@type'], ), [filter]); + + const { get, response } = useMetadataSchema(); + + const [schema, setSchema] = React.useState(); + + async function loadSchema(d) { + const source = await get(`/${d.schema}`) + if (response.ok) { + setSchema(source); + } + } + + /*eslint-disable react-hooks/exhaustive-deps*/ + React.useEffect(() => { loadSchema(definition) }, [definition]); + + const configuration = useMetadataConfiguration([filter], schema, definition); return (<>
@@ -31,37 +54,31 @@ export function MetadataFilterConfigurationListItem ({ filter, isLast, isFirst,
- - ); -} - -/* - -
-
-
-
- -   - Edit - - +
+
+ } + {configuration && } +
- - - - -*/ \ No newline at end of file + } + ); +} \ No newline at end of file diff --git a/ui/src/app/metadata/hooks/configuration.js b/ui/src/app/metadata/hooks/configuration.js index b65b72a0f..6efd0ef38 100644 --- a/ui/src/app/metadata/hooks/configuration.js +++ b/ui/src/app/metadata/hooks/configuration.js @@ -1,11 +1,10 @@ -import React from 'react'; - -import { MetadataDefinitionContext, MetadataSchemaContext } from '../hoc/MetadataSchema'; import { getConfigurationSections } from './schema'; -export function useMetadataConfiguration(models) { - const definition = React.useContext(MetadataDefinitionContext); - const schema = React.useContext(MetadataSchemaContext); +export function useMetadataConfiguration(models, schema, definition) { + + if (!models || !schema || !definition) { + return {}; + } const processed = definition.schemaPreprocessor ? definition.schemaPreprocessor(schema) : schema; From 476b2e66dc5f9ef33a36f579b4afef8ecfabf1c7 Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Thu, 29 Apr 2021 09:07:49 -0700 Subject: [PATCH 033/151] Fixed messages and schema --- .../main/resources/entity-attributes-filters-ui-schema.json | 1 + backend/src/main/resources/i18n/messages.properties | 5 ++++- backend/src/main/resources/nameid-filter.schema.json | 3 ++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/backend/src/main/resources/entity-attributes-filters-ui-schema.json b/backend/src/main/resources/entity-attributes-filters-ui-schema.json index fa64d5abb..d5968834e 100644 --- a/backend/src/main/resources/entity-attributes-filters-ui-schema.json +++ b/backend/src/main/resources/entity-attributes-filters-ui-schema.json @@ -174,6 +174,7 @@ } }, "attributeRelease": { + "title": "label.attribute-release", "type": "array", "description": "Attribute release table - select the attributes you want to release (default unchecked)", "widget": { diff --git a/backend/src/main/resources/i18n/messages.properties b/backend/src/main/resources/i18n/messages.properties index 0032a25ed..1a74a40eb 100644 --- a/backend/src/main/resources/i18n/messages.properties +++ b/backend/src/main/resources/i18n/messages.properties @@ -118,6 +118,9 @@ brand.and=and heading.shibboleth=Shibboleth +label.source-configuration=Metadata Source Configuration +label.provider-configuration=Metadata Provider Configuration + label.metadata-source=Metadata Source label.metadata-sources=Metadata Sources label.metadata-provider=Metadata Provider @@ -410,7 +413,7 @@ label.author=Author label.creation-date=Creation Date label.order=Order label.provider-type=Provider Type - +label.version-history=Version History label.metadata-resolver-history=Metadata resolver history label.metadata-version-history=Metadata Version History label.select-version=Select Version diff --git a/backend/src/main/resources/nameid-filter.schema.json b/backend/src/main/resources/nameid-filter.schema.json index 0416092d0..b033a13b9 100644 --- a/backend/src/main/resources/nameid-filter.schema.json +++ b/backend/src/main/resources/nameid-filter.schema.json @@ -49,7 +49,7 @@ }, "properties": { "nameIdFormatFilterTargetType": { - "title": "", + "title": "label.filter-target-type", "type": "string", "default": "ENTITY", "oneOf": [ @@ -74,6 +74,7 @@ ] }, "value": { + "title": "label.filter-target-value", "type": "array", "buttons": [ { From e42c9d2dffba263a06eca050fd623af6f9a43e89 Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Thu, 29 Apr 2021 15:22:33 -0700 Subject: [PATCH 034/151] Implemented version view --- ui/package.json | 2 + ui/src/app/App.js | 29 ++--- ui/src/app/core/components/FormattedDate.js | 4 +- ui/src/app/dashboard/container/ActionsTab.js | 42 ------- ui/src/app/dashboard/container/AdminTab.js | 51 -------- ui/src/app/dashboard/container/Dashboard.js | 108 ----------------- ui/src/app/dashboard/container/Dashboard.scss | 2 - .../app/dashboard/container/ProvidersTab.js | 62 ---------- ui/src/app/dashboard/container/SourcesTab.js | 46 -------- ui/src/app/metadata/Metadata.js | 18 ++- .../component/MetadataConfiguration.js | 36 +++--- .../app/metadata/component/MetadataHistory.js | 85 -------------- .../app/metadata/component/MetadataOptions.js | 109 ------------------ ui/src/app/metadata/component/MetadataXml.js | 45 -------- .../component/properties/PropertyValue.js | 2 +- .../MetadataFilterConfigurationList.js | 3 +- .../MetadataFilterConfigurationListItem.js | 4 +- .../filter/component/MetadataFilters.js | 14 ++- .../domain/provider/component/ProviderList.js | 12 +- ui/src/app/metadata/hooks/configuration.js | 21 +++- ui/yarn.lock | 37 ++++++ 21 files changed, 129 insertions(+), 603 deletions(-) delete mode 100644 ui/src/app/dashboard/container/ActionsTab.js delete mode 100644 ui/src/app/dashboard/container/AdminTab.js delete mode 100644 ui/src/app/dashboard/container/Dashboard.js delete mode 100644 ui/src/app/dashboard/container/Dashboard.scss delete mode 100644 ui/src/app/dashboard/container/ProvidersTab.js delete mode 100644 ui/src/app/dashboard/container/SourcesTab.js delete mode 100644 ui/src/app/metadata/component/MetadataHistory.js delete mode 100644 ui/src/app/metadata/component/MetadataOptions.js delete mode 100644 ui/src/app/metadata/component/MetadataXml.js diff --git a/ui/package.json b/ui/package.json index eeb591172..2c8a54b12 100644 --- a/ui/package.json +++ b/ui/package.json @@ -15,6 +15,7 @@ "http-proxy-middleware": "^1.2.0", "lodash": "^4.17.21", "prop-types": "^15.7.2", + "query-string": "^7.0.0", "react": "^17.0.2", "react-dom": "^17.0.2", "react-infinite-scroll-component": "^6.1.0", @@ -24,6 +25,7 @@ "reactstrap": "^8.9.0", "sass": "^1.32.11", "use-http": "^1.0.20", + "use-query-params": "^1.2.2", "web-vitals": "^1.0.1" }, "scripts": { diff --git a/ui/src/app/App.js b/ui/src/app/App.js index 2e53c4e3d..547f0a45b 100644 --- a/ui/src/app/App.js +++ b/ui/src/app/App.js @@ -5,6 +5,7 @@ import { Redirect, Route } from "react-router-dom"; +import { QueryParamProvider } from 'use-query-params'; import { Provider as HttpProvider } from 'use-http'; import './App.scss'; @@ -12,7 +13,7 @@ import { I18nProvider } from './i18n/context/I18n.provider'; import Footer from './core/components/Footer'; import { get_cookie } from './core/utility/get_cookie'; -import Dashboard from './dashboard/container/Dashboard'; +import Dashboard from './dashboard/view/Dashboard'; import Header from './core/components/Header'; import { UserProvider } from './core/user/UserContext'; import { Metadata } from './metadata/Metadata'; @@ -38,18 +39,20 @@ function App() { -
-
- - - - - - - - -
-