diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy index b60c9b0c8..f63dbad31 100644 --- a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy @@ -43,6 +43,9 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { @Autowired private OpenSamlObjects openSamlObjects + @Autowired + private MetadataResolversPositionOrderContainerService resolversPositionOrderContainerService + // TODO: enhance @Override void reloadFilters(String metadataResolverName) { @@ -98,8 +101,11 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { 'xsi:type': 'ChainingMetadataProvider', 'xsi:schemaLocation': 'urn:mace:shibboleth:2.0:metadata http://shibboleth.net/schema/idp/shibboleth-metadata.xsd urn:mace:shibboleth:2.0:resource http://shibboleth.net/schema/idp/shibboleth-resource.xsd urn:mace:shibboleth:2.0:security http://shibboleth.net/schema/idp/shibboleth-security.xsd urn:oasis:names:tc:SAML:2.0:metadata http://docs.oasis-open.org/security/saml/v2.0/saml-schema-metadata-2.0.xsd urn:oasis:names:tc:SAML:2.0:assertion http://docs.oasis-open.org/security/saml/v2.0/saml-schema-assertion-2.0.xsd' ) { - metadataResolverRepository.findAll().each { edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver mr -> - //TODO: We cannot/do not currently have the code to marshall the internal incommon chaining resolver + + + resolversPositionOrderContainerService.allMetadataResolversInDefinedOrderOrUnordered.each { + edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver mr -> + //TODO: We do not currently marshall the internal incommon chaining resolver (with BaseMetadataResolver type) if ((mr.type != 'BaseMetadataResolver') && (mr.enabled)) { constructXmlNodeForResolver(mr, delegate) { MetadataFilter( diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java index 0e4daa059..5b9d330bf 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java @@ -1,8 +1,11 @@ package edu.internet2.tier.shibboleth.admin.ui.configuration; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityIdsSearchResultRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolversPositionOrderContainer; import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository; +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolversPositionOrderContainerRepository; import edu.internet2.tier.shibboleth.admin.ui.scheduled.EntityDescriptorFilesScheduledTasks; import edu.internet2.tier.shibboleth.admin.ui.service.*; import edu.internet2.tier.shibboleth.admin.util.AttributeUtility; @@ -167,4 +170,14 @@ public void addInterceptors(InterceptorRegistry registry) { } }; } + + @Bean + public MetadataResolversPositionOrderContainerService + metadataResolversPositionOrderContainerService(MetadataResolversPositionOrderContainerRepository + positionOrderContainerRepository, + MetadataResolverRepository resolverRepository) { + + return new DefaultMetadataResolversPositionOrderContainerService(positionOrderContainerRepository, resolverRepository); + + } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java index ac1339201..5adffec76 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java @@ -6,6 +6,7 @@ import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; import edu.internet2.tier.shibboleth.admin.ui.service.IndexWriterService; import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService; +import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolversPositionOrderContainerService; import lombok.extern.slf4j.Slf4j; import net.shibboleth.utilities.java.support.component.ComponentInitializationException; import org.apache.lucene.document.Document; @@ -36,6 +37,7 @@ import java.io.IOException; import java.io.StringWriter; import java.net.URI; +import java.util.List; import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolverValidator.ValidationResult; @@ -53,6 +55,9 @@ public class MetadataResolversController { @Autowired MetadataResolverService metadataResolverService; + @Autowired + MetadataResolversPositionOrderContainerService positionOrderContainerService; + @Autowired IndexWriterService indexWriterService; @@ -64,7 +69,7 @@ public ResponseEntity unableToParseJson(Exception ex) { @GetMapping("/MetadataResolvers") @Transactional(readOnly = true) public ResponseEntity getAll() { - Iterable resolvers = resolverRepository.findAll(); + List resolvers = positionOrderContainerService.getAllMetadataResolversInDefinedOrderOrUnordered(); resolvers.forEach(MetadataResolver::updateVersion); return ResponseEntity.ok(resolvers); } @@ -108,6 +113,7 @@ public ResponseEntity create(@RequestBody MetadataResolver newResolver) { newResolver.convertFiltersFromTransientRepresentationIfNecessary(); MetadataResolver persistedResolver = resolverRepository.save(newResolver); + positionOrderContainerService.appendPositionOrderForNew(persistedResolver); persistedResolver.updateVersion(); persistedResolver.convertFiltersIntoTransientRepresentationIfNecessary(); diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversPositionOrderController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversPositionOrderController.java new file mode 100644 index 000000000..ab78e4a52 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversPositionOrderController.java @@ -0,0 +1,34 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller; + + +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolversPositionOrderContainer; +import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolversPositionOrderContainerService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author Dmitriy Kopylenko + */ +@RestController +@RequestMapping("/api/MetadataResolversPositionOrder") +public class MetadataResolversPositionOrderController { + + @Autowired + MetadataResolversPositionOrderContainerService positionOrderContainerService; + + @PostMapping + public ResponseEntity createOrUpdate(@RequestBody MetadataResolversPositionOrderContainer metadataResolversPositionOrderContainer) { + positionOrderContainerService.addOrUpdatePositionOrderContainer(metadataResolversPositionOrderContainer); + return ResponseEntity.noContent().build(); + } + + @GetMapping + public ResponseEntity getPositionOrderContainer() { + return ResponseEntity.ok(positionOrderContainerService.retrieveExistingOrEmpty()); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilterTarget.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilterTarget.java index 68cab1d4c..5effdae3d 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilterTarget.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilterTarget.java @@ -16,7 +16,7 @@ @EqualsAndHashCode(callSuper = true) public class EntityAttributesFilterTarget extends AbstractAuditable { public enum EntityAttributesFilterTargetType { - ENTITY, CONDITION_SCRIPT, CONDITION_REF + ENTITY, CONDITION_SCRIPT, CONDITION_REF, REGEX } private static Logger LOGGER = LoggerFactory.getLogger(EntityAttributesFilterTarget.class); diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolversOrderContainer.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolversOrderContainer.java new file mode 100644 index 000000000..5b5d0d87f --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolversOrderContainer.java @@ -0,0 +1,14 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers; + +/** + * This is a persistent entity abstraction encapsulating a collection of metadata resolver ids + * for the purpose of maintaining an order of all persistent metadata resolvers which becomes significant during + * generation of XML metadata for the resolvers. + * + * Maintaining this separate entity enables UI layer for example to explicitly manipulate ordering e.g. use REST + * API to reorder resolvers, etc. + * + * @author Dmitriy + */ +public class MetadataResolversOrderContainer { +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolversPositionOrderContainer.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolversPositionOrderContainer.java new file mode 100644 index 000000000..883069531 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolversPositionOrderContainer.java @@ -0,0 +1,50 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import javax.persistence.CollectionTable; +import javax.persistence.Column; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.OrderColumn; +import java.util.ArrayList; +import java.util.List; + +/** + * This is a persistent entity abstraction encapsulating a collection of metadata resolver ids + * for the purpose of maintaining an order of all persistent metadata resolvers which becomes significant during + * generation of XML metadata for the resolvers. + * + * Maintaining this separate entity enables UI layer for example to explicitly manipulate ordering e.g. use REST + * API to reorder resolvers, etc. + * + * @author Dmitriy Kopylenko + */ +@Entity +@EqualsAndHashCode +@NoArgsConstructor +@Getter +@Setter +@ToString +public class MetadataResolversPositionOrderContainer { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + @JsonIgnore + protected Long id; + + @ElementCollection + @CollectionTable(name="METADATA_RESOLVER_POSITION_ORDER", joinColumns=@JoinColumn(name="METADATA_RESOLVER_POSITION_ORDER_CONTAINER_ID")) + @Column(name="METADATA_RESOLVER_RESOURCE_ID") + @OrderColumn + private List resourceIds = new ArrayList<>(); +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/DynamicHttpMetadataResolverRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/DynamicHttpMetadataResolverRepository.java deleted file mode 100644 index c4a08804f..000000000 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/DynamicHttpMetadataResolverRepository.java +++ /dev/null @@ -1,18 +0,0 @@ -package edu.internet2.tier.shibboleth.admin.ui.repository; - -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicHttpMetadataResolver; -import org.springframework.data.repository.CrudRepository; - -/** - * Spring Data CRUD repository for instances of {@link DynamicHttpMetadataResolver}s. - * - * @author Bill Smith (wsmith@unicon.net) - */ -public interface DynamicHttpMetadataResolverRepository extends CrudRepository { - - DynamicHttpMetadataResolver findByName(String name); - - boolean deleteByResourceId(String resourceId); - - DynamicHttpMetadataResolver findByResourceId(String resourceId); -} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityRoleWhiteListFilterRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityRoleWhiteListFilterRepository.java deleted file mode 100644 index afa830773..000000000 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityRoleWhiteListFilterRepository.java +++ /dev/null @@ -1,16 +0,0 @@ -package edu.internet2.tier.shibboleth.admin.ui.repository; - -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityRoleWhiteListFilter; -import org.springframework.data.repository.CrudRepository; - -/** - * Spring Data CRUD repository for instances of {@link EntityRoleWhiteListFilter}s. - */ -public interface EntityRoleWhiteListFilterRepository extends CrudRepository { - - EntityRoleWhiteListFilter findByName(String name); - - EntityRoleWhiteListFilter findByResourceId(String resourceId); - - boolean deleteByResourceId(String resourceId); -} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/FileBackedHttpMetadataResolverRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/FileBackedHttpMetadataResolverRepository.java deleted file mode 100644 index 97d069173..000000000 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/FileBackedHttpMetadataResolverRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package edu.internet2.tier.shibboleth.admin.ui.repository; - -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver; -import org.springframework.data.repository.CrudRepository; - -/** - * Spring Data CRUD repository for instances of {@link FileBackedHttpMetadataResolver}s. - */ -public interface FileBackedHttpMetadataResolverRepository extends CrudRepository { - - FileBackedHttpMetadataResolver findByName(String name); - boolean deleteByResourceId(String resourceId); - FileBackedHttpMetadataResolver findByResourceId(String resourceId); -} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/LocalDynamicMetadataResolverRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/LocalDynamicMetadataResolverRepository.java deleted file mode 100644 index 323b470d5..000000000 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/LocalDynamicMetadataResolverRepository.java +++ /dev/null @@ -1,16 +0,0 @@ -package edu.internet2.tier.shibboleth.admin.ui.repository; - -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.LocalDynamicMetadataResolver; -import org.springframework.data.repository.CrudRepository; - -/** - * Spring Data CRUD repository for instances of {@link LocalDynamicMetadataResolver}s. - */ -public interface LocalDynamicMetadataResolverRepository extends CrudRepository { - - LocalDynamicMetadataResolver findByName(String name); - - boolean deleteByResourceId(String resourceId); - - LocalDynamicMetadataResolver findByResourceId(String resourceId); -} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolversPositionOrderContainerRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolversPositionOrderContainerRepository.java new file mode 100644 index 000000000..6afe35ded --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolversPositionOrderContainerRepository.java @@ -0,0 +1,13 @@ +package edu.internet2.tier.shibboleth.admin.ui.repository; + +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolversPositionOrderContainer; +import org.springframework.data.repository.CrudRepository; + +/** + * Spring Data Repository API for persistence operations on instances of {@link MetadataResolversPositionOrderContainer}. + * + * @author Dmitriy Kopylenko + */ +public interface MetadataResolversPositionOrderContainerRepository + extends CrudRepository { +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/RoleDescriptorResolverRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/RoleDescriptorResolverRepository.java deleted file mode 100644 index c2cbef436..000000000 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/RoleDescriptorResolverRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package edu.internet2.tier.shibboleth.admin.ui.repository; - -import edu.internet2.tier.shibboleth.admin.ui.domain.RoleDescriptorResolver; -import org.springframework.data.repository.CrudRepository; - - -/** - * Repository to manage {@link RoleDescriptorResolver} instances. - */ -public interface RoleDescriptorResolverRepository extends CrudRepository { - -} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/SignatureValidationFilterRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/SignatureValidationFilterRepository.java deleted file mode 100644 index 7b1c70e0f..000000000 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/SignatureValidationFilterRepository.java +++ /dev/null @@ -1,16 +0,0 @@ -package edu.internet2.tier.shibboleth.admin.ui.repository; - -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.SignatureValidationFilter; -import org.springframework.data.repository.CrudRepository; - -/** - * Spring Data CRUD repository for instances of {@link SignatureValidationFilter}s. - */ -public interface SignatureValidationFilterRepository extends CrudRepository { - - SignatureValidationFilter findByName(String name); - - SignatureValidationFilter findByResourceId(String resourceId); - - boolean deleteByResourceId(String resourceId); -} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/DefaultMetadataResolversPositionOrderContainerService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/DefaultMetadataResolversPositionOrderContainerService.java new file mode 100644 index 000000000..76ac86cea --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/DefaultMetadataResolversPositionOrderContainerService.java @@ -0,0 +1,77 @@ +package edu.internet2.tier.shibboleth.admin.ui.service; + +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolversPositionOrderContainer; +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolversPositionOrderContainerRepository; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Iterator; +import java.util.List; +import java.util.Optional; + +import static com.google.common.collect.FluentIterable.from; +import static java.util.stream.Collectors.toList; + + +/** + * Default implementation of {@link MetadataResolversPositionOrderContainer}. + * + * @author Dmitriy Kopylenko + */ +public class DefaultMetadataResolversPositionOrderContainerService implements MetadataResolversPositionOrderContainerService { + + private MetadataResolversPositionOrderContainerRepository positionOrderContainerRepository; + + private MetadataResolverRepository metadataResolverRepository; + + public DefaultMetadataResolversPositionOrderContainerService(MetadataResolversPositionOrderContainerRepository positionOrderRepository, + MetadataResolverRepository metadataResolverRepository) { + this.positionOrderContainerRepository = positionOrderRepository; + this.metadataResolverRepository = metadataResolverRepository; + } + + @Override + @Transactional + public void addOrUpdatePositionOrderContainer(MetadataResolversPositionOrderContainer metadataResolversPositionOrderContainer) { + MetadataResolversPositionOrderContainer existingPositionOrder = getPositionOrderContainerIfExists().orElse(null); + if (existingPositionOrder != null) { + existingPositionOrder.setResourceIds(metadataResolversPositionOrderContainer.getResourceIds()); + positionOrderContainerRepository.save(existingPositionOrder); + return; + } + positionOrderContainerRepository.save(metadataResolversPositionOrderContainer); + } + + @Override + @Transactional(readOnly = true) + public List getAllMetadataResolversInDefinedOrderOrUnordered() { + Optional orderContainer = getPositionOrderContainerIfExists(); + if(orderContainer.isPresent()) { + return orderContainer.get().getResourceIds() + .stream() + .map(metadataResolverRepository::findByResourceId) + .collect(toList()); + } + + return from(metadataResolverRepository.findAll()).toList(); + } + + @Override + public MetadataResolversPositionOrderContainer retrieveExistingOrEmpty() { + return getPositionOrderContainerIfExists().orElseGet(MetadataResolversPositionOrderContainer::new); + } + + @Override + @Transactional + public void appendPositionOrderForNew(MetadataResolver metadataResolver) { + MetadataResolversPositionOrderContainer positionOrderContainer = retrieveExistingOrEmpty(); + positionOrderContainer.getResourceIds().add(metadataResolver.getResourceId()); + positionOrderContainerRepository.save(positionOrderContainer); + } + + private Optional getPositionOrderContainerIfExists() { + Iterator iter = positionOrderContainerRepository.findAll().iterator(); + return iter.hasNext() ? Optional.of(iter.next()) : Optional.empty(); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolversPositionOrderContainerService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolversPositionOrderContainerService.java new file mode 100644 index 000000000..7de6490af --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolversPositionOrderContainerService.java @@ -0,0 +1,23 @@ +package edu.internet2.tier.shibboleth.admin.ui.service; + +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolversPositionOrderContainer; + +import java.util.List; + +/** + * Service interface for manipulation of instances of {@link MetadataResolversPositionOrderContainer} and + * to abstract away MetadataResolvers ordering logic. + * + * @author Dmitriy Kopylenko + */ +public interface MetadataResolversPositionOrderContainerService { + + MetadataResolversPositionOrderContainer retrieveExistingOrEmpty(); + + void addOrUpdatePositionOrderContainer(MetadataResolversPositionOrderContainer metadataResolversPositionOrderContainer); + + List getAllMetadataResolversInDefinedOrderOrUnordered(); + + void appendPositionOrderForNew(MetadataResolver metadataResolver); +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/FileBackedHttpMetadataResolverRepositoryTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/FileBackedHttpMetadataResolverRepositoryTests.groovy deleted file mode 100644 index 46f582501..000000000 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/FileBackedHttpMetadataResolverRepositoryTests.groovy +++ /dev/null @@ -1,130 +0,0 @@ -package edu.internet2.tier.shibboleth.admin.ui.repository - -import com.fasterxml.jackson.databind.ObjectMapper -import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilterTarget -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.HttpMetadataResolverAttributes -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ReloadableMetadataResolverAttributes -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.annotation.DirtiesContext -import org.springframework.test.context.ContextConfiguration -import spock.lang.Specification - -import javax.persistence.EntityManager - -import static edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilterTarget.EntityAttributesFilterTargetType.ENTITY -import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.HttpMetadataResolverAttributes.HttpCachingType.memory - -@DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration]) -@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) -@EntityScan("edu.internet2.tier.shibboleth.admin.ui") -@DirtiesContext(methodMode = DirtiesContext.MethodMode.BEFORE_METHOD) -class FileBackedHttpMetadataResolverRepositoryTests extends Specification { - - @Autowired - MetadataResolverRepository repositoryUnderTest - - @Autowired - EntityManager entityManager - - def "file backed http metadata resolver instances persist OK"() { - when: - def mdr = new FileBackedHttpMetadataResolver().with { - it.name = "FileBackedHttpMetadata" - - it.httpMetadataResolverAttributes = new HttpMetadataResolverAttributes().with { - it.connectionRequestTimeout = "PT05" - it.disregardTLSCertificate = true - it.httpCaching = memory - it - } - it.reloadableMetadataResolverAttributes = new ReloadableMetadataResolverAttributes().with { - it.indexesRef = "indexesSpringBeanId" - it - } - - it.metadataFilters.add(new EntityAttributesFilter().with { - it.entityAttributesFilterTarget = new EntityAttributesFilterTarget().with { - it.entityAttributesFilterTargetType = ENTITY - it.setValue(["hola"]) - it - } - it - }) - it - } - repositoryUnderTest.save(mdr) - - then: - repositoryUnderTest.findAll().size() > 0 - FileBackedHttpMetadataResolver item = repositoryUnderTest.findByName("FileBackedHttpMetadata") - - item.name == "FileBackedHttpMetadata" - item.metadataFilters.size() == 1 - item.metadataFilters[0].entityAttributesFilterTarget.entityAttributesFilterTargetType == ENTITY - item.metadataFilters[0].entityAttributesFilterTarget.value.size() == 1 - item.metadataFilters[0].entityAttributesFilterTarget.value.get(0) == "hola" - item.httpMetadataResolverAttributes.connectionRequestTimeout == "PT05" - item.httpMetadataResolverAttributes.disregardTLSCertificate - item.httpMetadataResolverAttributes.httpCaching == memory - item.reloadableMetadataResolverAttributes.indexesRef == "indexesSpringBeanId" - } - - def "FileBackedHttpMetadataResolver hashcode works as desired"() { - given: - // TODO: There is weirdness here if reloadableMetadataResolverAttributes is empty. - // I suspect similar weirdness if httpMetadataResolverAttributes is an empty object, too. - def resolverJson = '''{ - "name": "name", - "@type": "FileBackedHttpMetadataResolver", - "requireValidMetadata": true, - "failFastInitialization": true, - "sortKey": 7, - "criterionPredicateRegistryRef": "criterionPredicateRegistryRef", - "useDefaultPredicateRegistry": true, - "satisfyAnyPredicates": true, - "metadataFilters": [], - "reloadableMetadataResolverAttributes": { - "parserPoolRef": "parserPoolRef" - }, - "httpMetadataResolverAttributes": { - "httpClientRef": "httpClientRef", - "connectionRequestTimeout": "connectionRequestTimeout", - "connectionTimeout": "connectionTimeout", - "socketTimeout": "socketTimeout", - "disregardTLSCertificate": true, - "tlsTrustEngineRef": "tlsTrustEngineRef", - "httpClientSecurityParametersRef": "httpClientSecurityParametersRef", - "proxyHost": "proxyHost", - "proxyPort": "proxyPort", - "proxyUser": "proxyUser", - "proxyPassword": "proxyPassword", - "httpCaching": "none", - "httpCacheDirectory": "httpCacheDirectory", - "httpMaxCacheEntries": 1, - "httpMaxCacheEntrySize": 2 - } -}''' - - when: - def resolver = new ObjectMapper().readValue(resolverJson.bytes, FileBackedHttpMetadataResolver) - def persistedResolver = repositoryUnderTest.save(resolver) - entityManager.flush() - - then: - def item1 = repositoryUnderTest.findByName(persistedResolver.name) - entityManager.clear() - def item2 = repositoryUnderTest.findByName(persistedResolver.name) - - item1.hashCode() == item2.hashCode() - } -} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/LocalDynamicMetadataResolverRepositoryTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/LocalDynamicMetadataResolverRepositoryTests.groovy deleted file mode 100644 index 63ae7018c..000000000 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/LocalDynamicMetadataResolverRepositoryTests.groovy +++ /dev/null @@ -1,78 +0,0 @@ -package edu.internet2.tier.shibboleth.admin.ui.repository - -import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilterTarget -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicMetadataResolverAttributes -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.LocalDynamicMetadataResolver -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 spock.lang.Specification - -import javax.persistence.EntityManager - -import static edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilterTarget.EntityAttributesFilterTargetType.ENTITY - -@DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration]) -@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) -@EntityScan("edu.internet2.tier.shibboleth.admin.ui") -class LocalDynamicMetadataResolverRepositoryTests extends Specification { - - @Autowired - LocalDynamicMetadataResolverRepository repositoryUnderTest - - @Autowired - EntityManager entityManager - - def "local dynamic metadata resolver instances persist OK"() { - when: - def mdr = new LocalDynamicMetadataResolver().with { - it.name = 'LocalDynamicMetadataResolver' - it.sourceDirectory = '/etc/shibui' - it.sourceKeyGeneratorRef = 'sourceKeyGeneratorBean' - it.sourceManagerRef = 'sourceManagerBean' - it.dynamicMetadataResolverAttributes = new DynamicMetadataResolverAttributes().with { - it.cleanupTaskInterval = 'PT5H' - it.maxCacheDuration = 'PT8H' - it.initializeFromPersistentCacheInBackground = true - it - } - - it.metadataFilters.add(new EntityAttributesFilter().with { - it.entityAttributesFilterTarget = new EntityAttributesFilterTarget().with { - it.entityAttributesFilterTargetType = ENTITY - it.setValue(['hola']) - it - } - it - }) - - it - } - repositoryUnderTest.save(mdr) - - then: - repositoryUnderTest.findAll().size() > 0 - def item = repositoryUnderTest.findByName("LocalDynamicMetadataResolver") - - item.name == "LocalDynamicMetadataResolver" - item.metadataFilters.size() == 1 - item.metadataFilters[0].entityAttributesFilterTarget.entityAttributesFilterTargetType == ENTITY - item.metadataFilters[0].entityAttributesFilterTarget.value.size() == 1 - item.metadataFilters[0].entityAttributesFilterTarget.value.get(0) == "hola" - item.sourceDirectory == '/etc/shibui' - item.sourceKeyGeneratorRef == 'sourceKeyGeneratorBean' - item.sourceManagerRef == 'sourceManagerBean' - item.dynamicMetadataResolverAttributes.cleanupTaskInterval == 'PT5H' - item.dynamicMetadataResolverAttributes.maxCacheDuration == 'PT8H' - item.dynamicMetadataResolverAttributes.initializeFromPersistentCacheInBackground - } - -} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolversPositionOrderContainerServiceTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolversPositionOrderContainerServiceTests.groovy new file mode 100644 index 000000000..de4af7e8e --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolversPositionOrderContainerServiceTests.groovy @@ -0,0 +1,53 @@ +package edu.internet2.tier.shibboleth.admin.ui.service + +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolversPositionOrderContainer +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolversPositionOrderContainerRepository +import spock.lang.Specification +import spock.lang.Subject + +/** + * @author Dmitriy Kopylenko + */ +class MetadataResolversPositionOrderContainerServiceTests extends Specification { + + def "no order container has been provided and saved, so using unordered persisted resolvers"() { + given: + def resolverRepo = Mock(MetadataResolverRepository) + resolverRepo.findAll() >> [new MetadataResolver(resourceId: 'second'), new MetadataResolver(resourceId: 'first')] + def positionOrderRepo = Mock(MetadataResolversPositionOrderContainerRepository) + positionOrderRepo.findAll() >> [] + @Subject + def positionContainerSvc = new DefaultMetadataResolversPositionOrderContainerService(positionOrderRepo, resolverRepo) + + when: + def unorderedResolvers = positionContainerSvc.getAllMetadataResolversInDefinedOrderOrUnordered() + + then: + unorderedResolvers[0].resourceId == 'second' + unorderedResolvers[1].resourceId == 'first' + + } + + def "an order container has been provided and saved, so using resolvers with order defined in that position order container"() { + given: + def resolverRepo = Mock(MetadataResolverRepository) + resolverRepo.findAll() >> [new MetadataResolver(resourceId: 'second'), new MetadataResolver(resourceId: 'first')] + resolverRepo.findByResourceId('first') >> new MetadataResolver(resourceId: 'first') + resolverRepo.findByResourceId('second') >> new MetadataResolver(resourceId: 'second') + def positionOrderRepo = Mock(MetadataResolversPositionOrderContainerRepository) + positionOrderRepo.findAll() >> [new MetadataResolversPositionOrderContainer(resourceIds: ['first', 'second'])] + positionOrderRepo.findAll() >> [] + @Subject + def positionContainerSvc = new DefaultMetadataResolversPositionOrderContainerService(positionOrderRepo, resolverRepo) + + when: + def orderedResolvers = positionContainerSvc.getAllMetadataResolversInDefinedOrderOrUnordered() + + then: + orderedResolvers[0].resourceId == 'first' + orderedResolvers[1].resourceId == 'second' + + } +} diff --git a/ui/package-lock.json b/ui/package-lock.json index 0fd8aeb12..1228abd2d 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -1366,6 +1366,11 @@ "resolved": "https://registry.npmjs.org/@ngrx/store/-/store-5.2.0.tgz", "integrity": "sha1-Yn7XTJzZVGKTBIXZEqVXEXsjkD4=" }, + "@ngrx/store-devtools": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@ngrx/store-devtools/-/store-devtools-6.1.0.tgz", + "integrity": "sha512-Uc0g/NCbJIbzvIMuCy3skiZVD5hoIrOAAvaninXkVHt7bXpbsSAdvJlmnozuGQqTbC0UQhYRwAR7InRSrzIbMQ==" + }, "@ngtools/webpack": { "version": "6.0.0-rc.11", "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-6.0.0-rc.11.tgz", diff --git a/ui/package.json b/ui/package.json index c0199ee2d..6dc630ba9 100644 --- a/ui/package.json +++ b/ui/package.json @@ -31,6 +31,7 @@ "@ngrx/entity": "^5.2.0", "@ngrx/router-store": "^5.2.0", "@ngrx/store": "^5.2.0", + "@ngrx/store-devtools": "^6.1.0", "bootstrap": "4.1.1", "core-js": "^2.4.1", "deep-object-diff": "^1.1.0", diff --git a/ui/src/app/app.module.ts b/ui/src/app/app.module.ts index e51a919f2..aea2a20f6 100644 --- a/ui/src/app/app.module.ts +++ b/ui/src/app/app.module.ts @@ -2,6 +2,7 @@ import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { StoreModule } from '@ngrx/store'; import { EffectsModule } from '@ngrx/effects'; +import { StoreDevtoolsModule } from '@ngrx/store-devtools'; import { StoreRouterConnectingModule, RouterStateSerializer } from '@ngrx/router-store'; import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; @@ -22,6 +23,7 @@ import { ContentionModule } from './contention/contention.module'; import { SharedModule } from './shared/shared.module'; import { WizardModule } from './wizard/wizard.module'; import { FormModule } from './schema-form/schema-form.module'; +import { environment } from '../environments/environment.prod'; @NgModule({ declarations: [ @@ -31,6 +33,10 @@ import { FormModule } from './schema-form/schema-form.module'; StoreModule.forRoot(reducers, { metaReducers }), + StoreDevtoolsModule.instrument({ + maxAge: 25, // Retains last 25 states + logOnly: environment.production, // Restrict extension to log-only mode + }), EffectsModule.forRoot([]), BrowserModule, CoreModule.forRoot(), diff --git a/ui/src/app/metadata/domain/component/preview-dialog.component.spec.ts b/ui/src/app/metadata/domain/component/preview-dialog.component.spec.ts new file mode 100644 index 000000000..d08b873ba --- /dev/null +++ b/ui/src/app/metadata/domain/component/preview-dialog.component.spec.ts @@ -0,0 +1,46 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { ViewChild, Component } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { PreviewDialogComponent } from './preview-dialog.component'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { NgbActiveModalStub } from '../../../../testing/modal.stub'; + +@Component({ + template: `` +}) +class TestHostComponent { + @ViewChild(PreviewDialogComponent) + public formUnderTest: PreviewDialogComponent; +} + + +describe('Advanced Info Form Component', () => { + let fixture: ComponentFixture; + let instance: TestHostComponent; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + { provide: NgbActiveModal, useClass: NgbActiveModalStub } + ], + imports: [ + NoopAnimationsModule, + ReactiveFormsModule + ], + declarations: [ + PreviewDialogComponent, + TestHostComponent + ], + }); + + fixture = TestBed.createComponent(TestHostComponent); + instance = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should compile', () => { + expect(fixture).toBeDefined(); + }); +}); diff --git a/ui/src/app/metadata/domain/domain.type.ts b/ui/src/app/metadata/domain/domain.type.ts index 3cda36b83..e62766ed8 100644 --- a/ui/src/app/metadata/domain/domain.type.ts +++ b/ui/src/app/metadata/domain/domain.type.ts @@ -5,7 +5,7 @@ import { } from './model'; import { - EntityAttributesFilter, + EntityAttributesFilterEntity, FileBackedHttpMetadataResolver } from './entity'; import { @@ -13,7 +13,7 @@ import { } from './model/providers'; export type Filter = - | EntityAttributesFilter; + | EntityAttributesFilterEntity; export type Resolver = | FileBackedHttpMetadataResolver; diff --git a/ui/src/app/metadata/domain/effect/entity.effect.spec.ts b/ui/src/app/metadata/domain/effect/entity.effect.spec.ts index d42817b6e..9039a4cd9 100644 --- a/ui/src/app/metadata/domain/effect/entity.effect.spec.ts +++ b/ui/src/app/metadata/domain/effect/entity.effect.spec.ts @@ -8,7 +8,7 @@ import { EntityIdService } from '../service/entity-id.service'; import { ResolverService } from '../service/resolver.service'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModalStub } from '../../../../testing/modal.stub'; -import { EntityAttributesFilter } from '../entity'; +import { EntityAttributesFilterEntity, FileBackedHttpMetadataResolver } from '../entity'; describe('Entity Effects', () => { let effects: EntityEffects; @@ -45,13 +45,22 @@ describe('Entity Effects', () => { }); describe('openModal', () => { - it('should open a modal window', fakeAsync(() => { + it('should open a modal window for a filter', fakeAsync(() => { spyOn(modal, 'open').and.returnValue({componentInstance: {}}); spyOn(idService, 'preview').and.returnValue(of('')); - effects.openModal(new EntityAttributesFilter()); + effects.openModal(new EntityAttributesFilterEntity()); expect(idService.preview).toHaveBeenCalled(); tick(10); expect(modal.open).toHaveBeenCalled(); })); + + it('should open a modal window for a provider', fakeAsync(() => { + spyOn(modal, 'open').and.returnValue({ componentInstance: {} }); + spyOn(providerService, 'preview').and.returnValue(of('')); + effects.openModal(new FileBackedHttpMetadataResolver()); + expect(providerService.preview).toHaveBeenCalled(); + tick(10); + expect(modal.open).toHaveBeenCalled(); + })); }); }); diff --git a/ui/src/app/metadata/domain/entity/filter/entity-attributes-filter.spec.ts b/ui/src/app/metadata/domain/entity/filter/entity-attributes-filter.spec.ts new file mode 100644 index 000000000..22eae4cda --- /dev/null +++ b/ui/src/app/metadata/domain/entity/filter/entity-attributes-filter.spec.ts @@ -0,0 +1,21 @@ +import { EntityAttributesFilterEntity } from './entity-attributes-filter'; + +describe('EntityAttributesFilter Entity', () => { + let entity: EntityAttributesFilterEntity; + beforeEach(() => { + entity = new EntityAttributesFilterEntity({ + resourceId: 'foo', + filterEnabled: false + }); + }); + + it('should be an instance', () => { + expect(entity).toBeDefined(); + expect(entity.resourceId).toBe('foo'); + expect(entity.enabled).toBe(entity.filterEnabled); + expect(entity.id).toBe(entity.resourceId); + expect(entity.getId()).toBe(entity.entityId); + expect(entity.getDisplayId()).toBe(entity.entityId); + expect(entity.isDraft()).toBe(false); + }); +}); diff --git a/ui/src/app/metadata/domain/entity/filter/entity-attributes-filter.ts b/ui/src/app/metadata/domain/entity/filter/entity-attributes-filter.ts index 82b5ae9d6..304e6055e 100644 --- a/ui/src/app/metadata/domain/entity/filter/entity-attributes-filter.ts +++ b/ui/src/app/metadata/domain/entity/filter/entity-attributes-filter.ts @@ -2,7 +2,7 @@ import { MetadataFilter, MetadataEntity, RelyingPartyOverrides } from '../../mod import { MetadataTypes } from '../../domain.type'; import { FilterTarget } from '../../model'; -export class EntityAttributesFilter implements MetadataFilter, MetadataEntity { +export class EntityAttributesFilterEntity implements MetadataFilter, MetadataEntity { createdDate?: string; modifiedDate?: string; version: string; @@ -25,7 +25,7 @@ export class EntityAttributesFilter implements MetadataFilter, MetadataEntity { value: [''] }; - constructor(obj?: Partial) { + constructor(obj?: Partial) { Object.assign(this, { ...obj }); } @@ -73,7 +73,7 @@ export class EntityAttributesFilter implements MetadataFilter, MetadataEntity { return { attributeRelease: this.attributeRelease, relyingPartyOverrides: this.relyingPartyOverrides, - entityAttributesFilterTarget: { ...this.entityAttributesFilterTarget }, + entityAttributesFilterTarget: this.entityAttributesFilterTarget, filterEnabled: this.filterEnabled, name: this.name, '@type': 'EntityAttributes' diff --git a/ui/src/app/metadata/domain/model/metadata-filter.ts b/ui/src/app/metadata/domain/model/metadata-filter.ts index 093fc4cf6..5cb50e2ea 100644 --- a/ui/src/app/metadata/domain/model/metadata-filter.ts +++ b/ui/src/app/metadata/domain/model/metadata-filter.ts @@ -1,11 +1,10 @@ import { MetadataBase } from './metadata-base'; export interface MetadataFilter extends MetadataBase { - entityId: string; name: string; filterEnabled?: boolean; type: string; resourceId: string; - serialize(): any; + [key: string]: any; } diff --git a/ui/src/app/metadata/domain/model/metadata-order.ts b/ui/src/app/metadata/domain/model/metadata-order.ts new file mode 100644 index 000000000..b365af3b0 --- /dev/null +++ b/ui/src/app/metadata/domain/model/metadata-order.ts @@ -0,0 +1,3 @@ +export interface ProviderOrder { + resourceIds: string[]; +} diff --git a/ui/src/app/metadata/domain/model/metadata-provider.ts b/ui/src/app/metadata/domain/model/metadata-provider.ts index 4dc283368..f068ec718 100644 --- a/ui/src/app/metadata/domain/model/metadata-provider.ts +++ b/ui/src/app/metadata/domain/model/metadata-provider.ts @@ -1,10 +1,14 @@ import { MetadataBase, } from '../model'; +import { MetadataFilter } from './metadata-filter'; export interface MetadataProvider extends MetadataBase { name: string; '@type': string; enabled: boolean; resourceId: string; + xmlId: string; + sortKey: number; + metadataFilters: MetadataFilter[]; } diff --git a/ui/src/app/metadata/domain/service/filter.service.spec.ts b/ui/src/app/metadata/domain/service/filter.service.spec.ts index 1410f6723..218054885 100644 --- a/ui/src/app/metadata/domain/service/filter.service.spec.ts +++ b/ui/src/app/metadata/domain/service/filter.service.spec.ts @@ -2,10 +2,12 @@ import { TestBed, async, inject } from '@angular/core/testing'; import { HttpTestingController, HttpClientTestingModule } from '@angular/common/http/testing'; import { HttpClientModule, HttpRequest } from '@angular/common/http'; import { MetadataFilterService } from './filter.service'; -import { EntityAttributesFilter } from '../entity'; +import { EntityAttributesFilterEntity } from '../entity'; describe(`Metadata Filter Service`, () => { + const provider = 'foo'; + beforeEach(() => { TestBed.configureTestingModule({ imports: [ @@ -21,53 +23,53 @@ describe(`Metadata Filter Service`, () => { describe('query method', () => { it(`should send an expected GET[] request`, async(inject([MetadataFilterService, HttpTestingController], (service: MetadataFilterService, backend: HttpTestingController) => { - service.query().subscribe(); + service.query(provider).subscribe(); backend.expectOne((req: HttpRequest) => { - return req.url === `${service.base}${service.endpoint}` + return req.url === `${service.base}${service.endpoint}/${provider}/Filters` && req.method === 'GET'; - }, `GET MetadataResolvers collection`); + }, `GET MetadataFilter collection`); } ))); }); describe('find method', () => { it(`should send an expected GET request`, async(inject([MetadataFilterService, HttpTestingController], (service: MetadataFilterService, backend: HttpTestingController) => { - const id = 'foo'; - service.find(id).subscribe(); + const id = 'bar'; + service.find(provider, id).subscribe(); backend.expectOne((req: HttpRequest) => { - return req.url === `${service.base}${service.endpoint}/${id}` + return req.url === `${service.base}${service.endpoint}/${provider}/Filters/${id}` && req.method === 'GET'; - }, `GET MetadataResolvers collection`); + }, `GET MetadataFilter`); } ))); }); describe('update method', () => { it(`should send an expected PUT request`, async(inject([MetadataFilterService, HttpTestingController], (service: MetadataFilterService, backend: HttpTestingController) => { - const id = 'foo'; - const filter = new EntityAttributesFilter({ id }); - service.update(filter).subscribe(); + const id = 'bar'; + const filter = new EntityAttributesFilterEntity({ resourceId: id }); + service.update(provider, filter).subscribe(); backend.expectOne((req: HttpRequest) => { - return req.url === `${service.base}${service.endpoint}/${id}` + return req.url === `${service.base}${service.endpoint}/${provider}/Filters/${ id }` && req.method === 'PUT'; - }, `PUT (update) MetadataResolvers collection`); + }, `PUT (update) MetadataFilter`); } ))); }); describe('save method', () => { it(`should send an expected POST request`, async(inject([MetadataFilterService, HttpTestingController], (service: MetadataFilterService, backend: HttpTestingController) => { - const id = 'foo'; - const filter = new EntityAttributesFilter({ id }); - service.save(filter).subscribe(); + const id = 'bar'; + const filter = new EntityAttributesFilterEntity({ resourceId: id }); + service.save(provider, filter).subscribe(); backend.expectOne((req: HttpRequest) => { - return req.url === `${service.base}${service.endpoint}` + return req.url === `${service.base}${service.endpoint}/${provider}/Filters` && req.method === 'POST'; - }, `POST MetadataResolvers collection`); + }, `POST MetadataFilter`); } ))); }); diff --git a/ui/src/app/metadata/domain/service/filter.service.ts b/ui/src/app/metadata/domain/service/filter.service.ts index b96907eca..7334ee301 100644 --- a/ui/src/app/metadata/domain/service/filter.service.ts +++ b/ui/src/app/metadata/domain/service/filter.service.ts @@ -13,20 +13,21 @@ export class MetadataFilterService { constructor( private http: HttpClient ) { } - query(): Observable { - return this.http.get(`${this.base}${this.endpoint}`, {}); + query(providerId: string): Observable { + return this.http.get(`${this.base}${this.endpoint}/${providerId}/Filters`); } - find(id: string): Observable { + find(providerId: string, filterId: string): Observable { // console.log(id); - return this.http.get(`${this.base}${this.endpoint}/${id}`); + return this.http.get(`${this.base}${this.endpoint}/${providerId}/Filters/${ filterId }`); } - update(filter: MetadataFilter): Observable { - return this.http.put(`${this.base}${this.endpoint}/${filter.id}`, filter); + update(providerId: string, filter: MetadataFilter): Observable { + return this.http.put(`${this.base}${this.endpoint}/${providerId}/Filters/${ filter.resourceId }`, filter); } - save(filter: MetadataFilter): Observable { - return this.http.post(`${this.base}${this.endpoint}`, filter); + save(providerId: string, filter: MetadataFilter): Observable { + console.log(providerId, filter); + return this.http.post(`${this.base}${this.endpoint}/${providerId}/Filters`, filter); } } diff --git a/ui/src/app/metadata/domain/service/provider.service.ts b/ui/src/app/metadata/domain/service/provider.service.ts index d82ee3d4b..ede4bb172 100644 --- a/ui/src/app/metadata/domain/service/provider.service.ts +++ b/ui/src/app/metadata/domain/service/provider.service.ts @@ -1,25 +1,30 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; import { MetadataProvider } from '../../domain/model'; import { FileBackedHttpMetadataProvider } from '../model/providers'; +import { ProviderOrder } from '../model/metadata-order'; + @Injectable() export class MetadataProviderService { readonly endpoint = '/MetadataResolvers'; + readonly order = '/MetadataResolversPositionOrder'; readonly base = '/api'; constructor( private http: HttpClient ) {} query(): Observable { - return this.http.get(`${this.base}${this.endpoint}`, {}); + return this.http.get(`${this.base}${this.endpoint}`).pipe( + map(providers => providers.filter(p => p['@type'] !== 'BaseMetadataResolver')) + ); } find(id: string): Observable { - // console.log(id); return this.http.get(`${this.base}${this.endpoint}/${id}`); } @@ -28,7 +33,14 @@ export class MetadataProviderService { } save(provider: MetadataProvider): Observable { - const { metadataFilters, id, ...pruned } = provider as FileBackedHttpMetadataProvider; - return this.http.post(`${this.base}${this.endpoint}`, pruned); + return this.http.post(`${this.base}${this.endpoint}`, provider); + } + + getOrder(): Observable { + return this.http.get(`${this.base}${this.order}`); + } + + setOrder(order: ProviderOrder): Observable { + return this.http.post(`${this.base}${this.order}`, order); } } diff --git a/ui/src/app/metadata/filter/action/collection.action.ts b/ui/src/app/metadata/filter/action/collection.action.ts index 9ed93aaff..c93c563d8 100644 --- a/ui/src/app/metadata/filter/action/collection.action.ts +++ b/ui/src/app/metadata/filter/action/collection.action.ts @@ -3,37 +3,29 @@ import { MetadataFilter } from '../../domain/model/metadata-filter'; import { Update } from '@ngrx/entity'; export enum FilterCollectionActionTypes { - FIND = '[Metadata Filter] Find', - SELECT_FILTER = '[Metadata Filter] Select Request', - SELECT_FILTER_SUCCESS = '[Metadata Filter] Select Success', - SELECT_FILTER_FAIL = '[Metadata Filter] Select Fail', + SELECT_FILTER_REQUEST = '[Metadata Filter Collection] Select Filter Request', + SELECT_FILTER_SUCCESS = '[Metadata Filter Collection] Select Filter Success', + SELECT_FILTER_FAIL = '[Metadata Filter Collection] Select Filter Fail', - UPDATE_FILTER_REQUEST = '[Metadata Filter] Update Request', - UPDATE_FILTER_SUCCESS = '[Metadata Filter] Update Success', - UPDATE_FILTER_FAIL = '[Metadata Filter] Update Fail', + UPDATE_FILTER_REQUEST = '[Metadata Filter Collection] Update Filter Request', + UPDATE_FILTER_SUCCESS = '[Metadata Filter Collection] Update Filter Success', + UPDATE_FILTER_FAIL = '[Metadata Filter Collection] Update Filter Fail', - LOAD_FILTER_REQUEST = '[Metadata Filter Collection] Filter REQUEST', - LOAD_FILTER_SUCCESS = '[Metadata Filter Collection] Filter SUCCESS', - LOAD_FILTER_ERROR = '[Metadata Filter Collection] Filter ERROR', + LOAD_FILTER_REQUEST = '[Metadata Filter Collection] Load Filter Request', + LOAD_FILTER_SUCCESS = '[Metadata Filter Collection] Load Filter Success', + LOAD_FILTER_ERROR = '[Metadata Filter Collection] Load Filter Error', - ADD_FILTER = '[Metadata Filter Collection] Add Filter', + ADD_FILTER_REQUEST = '[Metadata Filter Collection] Add Filter Request', ADD_FILTER_SUCCESS = '[Metadata Filter Collection] Add Filter Success', ADD_FILTER_FAIL = '[Metadata Filter Collection] Add Filter Fail', - REMOVE_FILTER = '[Metadata Filter Collection] Remove Filter', + REMOVE_FILTER_REQUEST = '[Metadata Filter Collection] Remove Filter Request', REMOVE_FILTER_SUCCESS = '[Metadata Filter Collection] Remove Filter Success', REMOVE_FILTER_FAIL = '[Metadata Filter Collection] Remove Filter Fail' } - -export class FindFilter implements Action { - readonly type = FilterCollectionActionTypes.FIND; - - constructor(public payload: string) { } -} - export class SelectFilter implements Action { - readonly type = FilterCollectionActionTypes.SELECT_FILTER; + readonly type = FilterCollectionActionTypes.SELECT_FILTER_REQUEST; constructor(public payload: string) { } } @@ -53,7 +45,7 @@ export class SelectFilterFail implements Action { export class LoadFilterRequest implements Action { readonly type = FilterCollectionActionTypes.LOAD_FILTER_REQUEST; - constructor() { } + constructor(public payload: string) { } } export class LoadFilterSuccess implements Action { @@ -87,7 +79,7 @@ export class UpdateFilterFail implements Action { } export class AddFilterRequest implements Action { - readonly type = FilterCollectionActionTypes.ADD_FILTER; + readonly type = FilterCollectionActionTypes.ADD_FILTER_REQUEST; constructor(public payload: MetadataFilter) { } } @@ -105,7 +97,7 @@ export class AddFilterFail implements Action { } export class RemoveFilterRequest implements Action { - readonly type = FilterCollectionActionTypes.REMOVE_FILTER; + readonly type = FilterCollectionActionTypes.REMOVE_FILTER_REQUEST; constructor(public payload: MetadataFilter) { } } @@ -132,7 +124,6 @@ export type FilterCollectionActionsUnion = | RemoveFilterRequest | RemoveFilterSuccess | RemoveFilterFail - | FindFilter | SelectFilter | SelectFilterSuccess | SelectFilterFail diff --git a/ui/src/app/metadata/filter/container/edit-filter.component.html b/ui/src/app/metadata/filter/container/edit-filter.component.html index 2a2267d26..cbdf2df18 100644 --- a/ui/src/app/metadata/filter/container/edit-filter.component.html +++ b/ui/src/app/metadata/filter/container/edit-filter.component.html @@ -1,184 +1,36 @@
-
+
- Edit Filter - {{ (filter$ | async).name }} +   Edit EntityAttributesFilter
-
-
-
-
-
-
-
- - - Filter Name - - -
- - - - Filter Name is required - - -
-
-
- - - Search Entity ID - - -
- -
-
- - - - Minimum 4 characters. - - - - Entity ID is required - Entity ID not found - - -
-
- - -
-
-
-
-
-
- -   - -   - -
-
-
-
-
-
-
-
- - - Entity Preview - - -
-
-
Display Name
-
{{ (preview$ | async).displayName }}
-
Description
-
{{ (preview$ | async).description || '—' }}
-
-
-
-
-
-
-
-
-
- - -
- Enable this filter upon saving popover - -
-
-
-
-
- -
-
- -
-
-
-
+
+
+ +   + +
+
-
\ No newline at end of file + diff --git a/ui/src/app/metadata/filter/container/edit-filter.component.spec.ts b/ui/src/app/metadata/filter/container/edit-filter.component.spec.ts index e69de29bb..8f632b0d8 100644 --- a/ui/src/app/metadata/filter/container/edit-filter.component.spec.ts +++ b/ui/src/app/metadata/filter/container/edit-filter.component.spec.ts @@ -0,0 +1,99 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { ReactiveFormsModule, FormBuilder } from '@angular/forms'; +import { StoreModule, Store, combineReducers } from '@ngrx/store'; +import { NewFilterComponent } from './new-filter.component'; +import * as fromFilter from '../reducer'; +import { ProviderEditorFormModule } from '../../domain/component'; +import { ProviderStatusEmitter, ProviderValueEmitter } from '../../domain/service/provider-change-emitter.service'; +import { NgbPopoverModule, NgbPopoverConfig } from '@ng-bootstrap/ng-bootstrap'; +import { NavigatorService } from '../../../core/service/navigator.service'; +import { SharedModule } from '../../../shared/shared.module'; +import { EditFilterComponent } from './edit-filter.component'; +import { SchemaFormModule, WidgetRegistry, DefaultWidgetRegistry } from 'ngx-schema-form'; +import { SchemaService } from '../../../schema-form/service/schema.service'; +import { HttpClientModule } from '@angular/common/http'; + +describe('New Metadata Filter Page', () => { + let fixture: ComponentFixture; + let store: Store; + let instance: EditFilterComponent; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + ProviderStatusEmitter, + ProviderValueEmitter, + FormBuilder, + NgbPopoverConfig, + NavigatorService, + SchemaService, + { provide: WidgetRegistry, useClass: DefaultWidgetRegistry } + ], + imports: [ + StoreModule.forRoot({ + 'filter': combineReducers(fromFilter.reducers), + }), + ReactiveFormsModule, + ProviderEditorFormModule, + NgbPopoverModule, + SharedModule, + HttpClientModule, + SchemaFormModule.forRoot() + ], + declarations: [ + EditFilterComponent + ], + }); + + fixture = TestBed.createComponent(EditFilterComponent); + instance = fixture.componentInstance; + store = TestBed.get(Store); + + spyOn(store, 'dispatch').and.callThrough(); + }); + + it('should compile', () => { + fixture.detectChanges(); + + expect(fixture).toBeDefined(); + }); + + describe('cancel method', () => { + it('should dispatch a cancel changes action', () => { + fixture.detectChanges(); + instance.cancel(); + expect(store.dispatch).toHaveBeenCalled(); + }); + }); + + describe('preview method', () => { + it('should dispatch a cancel changes action', () => { + fixture.detectChanges(); + instance.cancel(); + expect(store.dispatch).toHaveBeenCalled(); + }); + }); + + describe('status emitter', () => { + it('should set the isValid property to true', () => { + fixture.detectChanges(); + instance.statusChangeSubject.next({value: []}); + fixture.detectChanges(); + expect(instance.isValid).toBe(true); + }); + + it('should set the isValid property to true if value is undefined', () => { + fixture.detectChanges(); + instance.statusChangeSubject.next({value: null}); + fixture.detectChanges(); + expect(instance.isValid).toBe(true); + }); + + it('should set the isValid property to false', () => { + fixture.detectChanges(); + instance.statusChangeSubject.next({ value: [{control: 'foo'}] }); + fixture.detectChanges(); + expect(instance.isValid).toBe(false); + }); + }); +}); diff --git a/ui/src/app/metadata/filter/container/edit-filter.component.ts b/ui/src/app/metadata/filter/container/edit-filter.component.ts index 41d10a3e9..50184abf8 100644 --- a/ui/src/app/metadata/filter/container/edit-filter.component.ts +++ b/ui/src/app/metadata/filter/container/edit-filter.component.ts @@ -1,176 +1,67 @@ -import { Component, OnInit, OnDestroy, SimpleChanges, ViewChild, ElementRef } from '@angular/core'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Component } from '@angular/core'; import { Store } from '@ngrx/store'; -import { Observable, Subject } from 'rxjs'; -import { withLatestFrom, distinctUntilChanged, takeUntil, startWith, filter } from 'rxjs/operators'; +import { Subject, Observable, of } from 'rxjs'; -import * as fromRoot from '../../../app.reducer'; import * as fromFilter from '../reducer'; - -import { ProviderValueEmitter } from '../../domain/service/provider-change-emitter.service'; -import { CancelCreateFilter, SelectId, UpdateFilterChanges } from '../action/filter.action'; -import { UpdateFilterRequest } from '../../filter/action/collection.action'; -import { MetadataFilter } from '../../domain/model/metadata-filter'; -import { EntityValidators } from '../../domain/service/entity-validators.service'; -import { QueryEntityIds, ViewMoreIds, ClearSearch } from '../action/search.action'; -import { AutoCompleteComponent } from '../../../shared/autocomplete/autocomplete.component'; -import { MDUI } from '../../domain/model'; +import { MetadataFilterTypes, EntityAttributesFilter } from '../model'; +import { FormDefinition } from '../../../wizard/model'; +import { MetadataFilter, MetadataEntity } from '../../domain/model'; +import { SchemaService } from '../../../schema-form/service/schema.service'; +import { UpdateFilterRequest } from '../action/collection.action'; +import { CancelCreateFilter, UpdateFilterChanges } from '../action/filter.action'; import { PreviewEntity } from '../../domain/action/entity.action'; -import { MetadataEntity } from '../../domain/model'; -import { EntityAttributesFilter } from '../../domain/entity/filter/entity-attributes-filter'; +import { EntityAttributesFilterEntity } from '../../domain/entity'; @Component({ selector: 'edit-filter-page', templateUrl: './edit-filter.component.html' }) -export class EditFilterComponent implements OnInit, OnDestroy { - - @ViewChild(AutoCompleteComponent) entityInput: AutoCompleteComponent; +export class EditFilterComponent { - private ngUnsubscribe: Subject = new Subject(); + valueChangeSubject = new Subject>(); + private valueChangeEmitted$ = this.valueChangeSubject.asObservable(); - changeId = false; + statusChangeSubject = new Subject<{ value: any[] }>(); + private statusChangeEmitted$ = this.statusChangeSubject.asObservable(); - changes$: Observable; - changes: MetadataFilter; + definition: FormDefinition; + schema$: Observable; - ids: string[]; - entityIds$: Observable; - showMore$: Observable; - selected$: Observable; - filter$: Observable; - loading$: Observable; - processing$: Observable; - preview$: Observable; + model$: Observable; isSaving$: Observable; - - form: FormGroup = this.fb.group({ - entityId: [ - '', - [Validators.required] - ], - name: ['', [Validators.required]], - filterEnabled: [false] - }); - filter: MetadataFilter; - filterEntity: EntityAttributesFilter; - - isValid = false; + isValid: boolean; constructor( - private store: Store, - private valueEmitter: ProviderValueEmitter, - private fb: FormBuilder + private store: Store, + private schemaService: SchemaService ) { - this.changes$ = this.store.select(fromFilter.getFilter); - this.changes$ - .pipe( - distinctUntilChanged() - ) - .subscribe(c => this.changes = new EntityAttributesFilter(c)); - - this.showMore$ = this.store.select(fromFilter.getViewingMore); - this.selected$ = this.store.select(fromFilter.getSelected); - this.filter$ = this.store.select(fromFilter.getSelectedFilter); - this.entityIds$ = this.store.select(fromFilter.getEntityCollection); - this.loading$ = this.store.select(fromFilter.getIsLoading); - this.processing$ = this.loading$.pipe(withLatestFrom(this.showMore$, (l, s) => !s && l)); - this.preview$ = this.store.select(fromFilter.getPreview); - this.isSaving$ = this.store.select(fromFilter.getSaving); + this.definition = MetadataFilterTypes.EntityAttributesFilter; - this.entityIds$.subscribe(ids => this.ids = ids); + this.schema$ = this.schemaService.get(this.definition.schema); + this.isSaving$ = this.store.select(fromFilter.getCollectionSaving); + this.model$ = this.store.select(fromFilter.getSelectedFilter); - this.filter$ - .pipe( - filter(f => !!f) - ) - .subscribe(filter => { - this.filterEntity = new EntityAttributesFilter(filter); - let { entityId, name, filterEnabled } = this.filterEntity; - this.form.patchValue({ - entityId, - name, - filterEnabled - }); - this.filter = filter; - this.store.dispatch(new SelectId(entityId)); + this.valueChangeEmitted$.subscribe(changes => this.store.dispatch(new UpdateFilterChanges(changes.value))); + this.statusChangeEmitted$.subscribe(valid => { + this.isValid = valid.value ? valid.value.length === 0 : true; + }); - this.form.get('entityId').disable(); - }); + this.store + .select(fromFilter.getFilter) + .subscribe(filter => this.filter = filter); } - ngOnInit(): void { - let id = this.form.get('entityId'); - id.valueChanges.pipe(distinctUntilChanged()) - .subscribe(query => this.searchEntityIds(query)); - - this.form.valueChanges.pipe( - takeUntil(this.ngUnsubscribe), - startWith(this.form.value) - ).subscribe(changes => this.store.dispatch(new UpdateFilterChanges(changes))); - - this.valueEmitter.changeEmitted$.pipe( - takeUntil(this.ngUnsubscribe), - startWith(this.form.value) - ).subscribe(changes => this.store.dispatch(new UpdateFilterChanges(changes))); - - id.valueChanges - .pipe(distinctUntilChanged()) - .subscribe(entityId => id.valid ? this.store.dispatch(new SelectId(entityId)) : null); - - this.selected$ - .pipe(distinctUntilChanged()) - .subscribe(entityId => id.setValue(entityId)); - } - - ngOnDestroy(): void { - this.ngUnsubscribe.next(); - this.ngUnsubscribe.complete(); - } - - onChangeId(): void { - const input = this.form.get('entityId'); - input.enable(); - input.setAsyncValidators(EntityValidators.existsInCollection(this.store.select(fromFilter.getEntityCollection))); - input.reset(''); - this.entityInput.inputField.nativeElement.focus(); - } - - onCancelChangeId(): void { - const input = this.form.get('entityId'); - input.disable(); - input.clearAsyncValidators(); - input.setValue(this.filterEntity.entityId); - } - - searchEntityIds(term: string): void { - if (term && term.length >= 4 && this.ids.indexOf(term) < 0) { - this.store.dispatch(new QueryEntityIds({ - term, - limit: 10 - })); - } - } - - onViewMore(query: string): void { - this.store.dispatch(new ViewMoreIds(query)); - } - - onStatusChange(status): void { - this.isValid = status === 'VALID'; - } - - save($event): void { - $event.preventDefault(); - this.store.dispatch(new UpdateFilterRequest({...this.filter, ...this.changes.serialize()})); + save(): void { + this.store.dispatch(new UpdateFilterRequest(this.filter)); } cancel(): void { this.store.dispatch(new CancelCreateFilter()); } - preview(entity: MetadataEntity): void { - this.store.dispatch(new PreviewEntity(new EntityAttributesFilter(entity))); + preview(entity: MetadataFilter): void { + this.store.dispatch(new PreviewEntity(new EntityAttributesFilterEntity(entity))); } } + diff --git a/ui/src/app/metadata/filter/container/filter.component.html b/ui/src/app/metadata/filter/container/filter.component.html index 90c6b6463..3793ba3bf 100644 --- a/ui/src/app/metadata/filter/container/filter.component.html +++ b/ui/src/app/metadata/filter/container/filter.component.html @@ -1 +1,3 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/ui/src/app/metadata/filter/container/new-filter.component.html b/ui/src/app/metadata/filter/container/new-filter.component.html index 0985f9764..22c07b036 100644 --- a/ui/src/app/metadata/filter/container/new-filter.component.html +++ b/ui/src/app/metadata/filter/container/new-filter.component.html @@ -5,146 +5,32 @@
- new EntityAttributesFilter +   New Filter - EntityAttributes
-
-
-
-
-
-
-
- - - Filter Name - - -
- - - - Filter Name is required - - -
-
-
- - - Search Entity ID - - -
- - - - - Minimum 4 characters. - - - - Entity ID is required - Entity ID not found - - -
-
-
-
- -   - -
-
-
-
-
-
-
-
- - - Entity Preview - - -
-
-
Display Name
-
{{ (preview$ | async).displayName }}
-
Description
-
{{ (preview$ | async).description || '—' }}
-
-
-
-
-
-
-
-
-
- - -
- Enable this filter upon saving popover - -
-
-
-
-
- -
-
- -
-
-
-
+
+
+ +   + +
+
diff --git a/ui/src/app/metadata/filter/container/new-filter.component.spec.ts b/ui/src/app/metadata/filter/container/new-filter.component.spec.ts index e954c1cbf..cf96683ac 100644 --- a/ui/src/app/metadata/filter/container/new-filter.component.spec.ts +++ b/ui/src/app/metadata/filter/container/new-filter.component.spec.ts @@ -8,6 +8,9 @@ import { ProviderStatusEmitter, ProviderValueEmitter } from '../../domain/servic import { NgbPopoverModule, NgbPopoverConfig } from '@ng-bootstrap/ng-bootstrap'; import { NavigatorService } from '../../../core/service/navigator.service'; import { SharedModule } from '../../../shared/shared.module'; +import { SchemaFormModule, WidgetRegistry, DefaultWidgetRegistry } from 'ngx-schema-form'; +import { SchemaService } from '../../../schema-form/service/schema.service'; +import { HttpClientModule } from '@angular/common/http'; describe('New Metadata Filter Page', () => { let fixture: ComponentFixture; @@ -21,7 +24,9 @@ describe('New Metadata Filter Page', () => { ProviderValueEmitter, FormBuilder, NgbPopoverConfig, - NavigatorService + NavigatorService, + SchemaService, + { provide: WidgetRegistry, useClass: DefaultWidgetRegistry } ], imports: [ StoreModule.forRoot({ @@ -30,7 +35,9 @@ describe('New Metadata Filter Page', () => { ReactiveFormsModule, ProviderEditorFormModule, NgbPopoverModule, - SharedModule + SharedModule, + HttpClientModule, + SchemaFormModule.forRoot() ], declarations: [ NewFilterComponent @@ -57,4 +64,27 @@ describe('New Metadata Filter Page', () => { expect(store.dispatch).toHaveBeenCalled(); }); }); + + describe('status emitter', () => { + it('should set the isValid property to true', () => { + fixture.detectChanges(); + instance.statusChangeSubject.next({ value: [] }); + fixture.detectChanges(); + expect(instance.isValid).toBe(true); + }); + + it('should set the isValid property to true if value is undefined', () => { + fixture.detectChanges(); + instance.statusChangeSubject.next({ value: null }); + fixture.detectChanges(); + expect(instance.isValid).toBe(true); + }); + + it('should set the isValid property to false', () => { + fixture.detectChanges(); + instance.statusChangeSubject.next({ value: [{ control: 'foo' }] }); + fixture.detectChanges(); + expect(instance.isValid).toBe(false); + }); + }); }); diff --git a/ui/src/app/metadata/filter/container/new-filter.component.ts b/ui/src/app/metadata/filter/container/new-filter.component.ts index 840d2d9a4..542126491 100644 --- a/ui/src/app/metadata/filter/container/new-filter.component.ts +++ b/ui/src/app/metadata/filter/container/new-filter.component.ts @@ -1,106 +1,63 @@ -import { Component, OnInit, OnDestroy } from '@angular/core'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Component, OnDestroy, OnInit } from '@angular/core'; import { Store } from '@ngrx/store'; -import { Observable, Subject } from 'rxjs'; -import { withLatestFrom, distinctUntilChanged, startWith, takeUntil } from 'rxjs/operators'; +import { Subject, Observable, of } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; import * as fromFilter from '../reducer'; -import { ProviderValueEmitter } from '../../domain/service/provider-change-emitter.service'; -import { CancelCreateFilter, SelectId, UpdateFilterChanges } from '../action/filter.action'; +import { MetadataFilterTypes } from '../model'; +import { FormDefinition } from '../../../wizard/model'; +import { MetadataFilter } from '../../domain/model'; +import { SchemaService } from '../../../schema-form/service/schema.service'; import { AddFilterRequest } from '../action/collection.action'; -import { MetadataFilter } from '../../domain/model/metadata-filter'; -import { EntityValidators } from '../../domain/service/entity-validators.service'; -import { QueryEntityIds, ViewMoreIds, ClearSearch } from '../action/search.action'; -import { MDUI } from '../../domain/model'; -import { EntityAttributesFilter } from '../../domain/entity/filter/entity-attributes-filter'; +import { CancelCreateFilter, UpdateFilterChanges } from '../action/filter.action'; @Component({ selector: 'new-filter-page', templateUrl: './new-filter.component.html' }) -export class NewFilterComponent implements OnInit, OnDestroy { +export class NewFilterComponent implements OnDestroy, OnInit { private ngUnsubscribe: Subject = new Subject(); - changes$: Observable; - changes: MetadataFilter; - filter: EntityAttributesFilter = new EntityAttributesFilter({ - entityId: '', - name: '', - relyingPartyOverrides: { - signAssertion: false, - dontSignResponse: false, - turnOffEncryption: false, - useSha: false, - ignoreAuthenticationMethod: false, - omitNotBefore: false, - responderId: '', - nameIdFormats: [], - authenticationMethods: [] - }, - attributeRelease: [] - }); + valueChangeSubject = new Subject>(); + private valueChangeEmitted$ = this.valueChangeSubject.asObservable(); - ids: string[]; - entityIds$: Observable; - showMore$: Observable; - selected$: Observable; - loading$: Observable; - processing$: Observable; - preview$: Observable; - isSaving$: Observable; + statusChangeSubject = new Subject<{ value: any[] }>(); + private statusChangeEmitted$ = this.statusChangeSubject.asObservable(); - form: FormGroup = this.fb.group({ - entityId: ['', [Validators.required], [ - EntityValidators.existsInCollection(this.store.select(fromFilter.getEntityCollection)) - ]], - name: ['', [Validators.required]], - filterEnabled: [false] - }); + definition: FormDefinition; + schema$: Observable; - isValid = false; + model$: Observable; + isSaving$: Observable; + filter: MetadataFilter; + isValid: boolean; constructor( private store: Store, - private valueEmitter: ProviderValueEmitter, - private fb: FormBuilder + private schemaService: SchemaService ) { - this.store.dispatch(new ClearSearch()); - this.changes$ = this.store.select(fromFilter.getFilter); - this.changes$.subscribe(c => this.changes = new EntityAttributesFilter(c)); - - this.showMore$ = this.store.select(fromFilter.getViewingMore); - this.selected$ = this.store.select(fromFilter.getSelected); - this.entityIds$ = this.store.select(fromFilter.getEntityCollection); - this.loading$ = this.store.select(fromFilter.getIsLoading); - this.processing$ = this.loading$.pipe(withLatestFrom(this.showMore$, (l, s) => !s && l)); - this.preview$ = this.store.select(fromFilter.getPreview); - this.isSaving$ = this.store.select(fromFilter.getSaving); + this.definition = MetadataFilterTypes.EntityAttributesFilter; - this.entityIds$.subscribe(ids => this.ids = ids); + this.schema$ = this.schemaService.get(this.definition.schema); + this.isSaving$ = this.store.select(fromFilter.getCollectionSaving); + this.model$ = of({}); } ngOnInit(): void { - let id = this.form.get('entityId'); - id.valueChanges.pipe(distinctUntilChanged()) - .subscribe(query => this.searchEntityIds(query)); - - this.form.valueChanges.pipe( - takeUntil(this.ngUnsubscribe), - startWith(this.form.value) - ).subscribe(changes => this.store.dispatch(new UpdateFilterChanges(changes))); - this.valueEmitter - .changeEmitted$ - .pipe( - takeUntil(this.ngUnsubscribe), - startWith(this.form.value) - ) - .subscribe(changes => this.store.dispatch(new UpdateFilterChanges(changes))); - - id.valueChanges.pipe(distinctUntilChanged()) - .subscribe(entityId => id.valid ? this.store.dispatch(new SelectId(entityId)) : null); - this.selected$.pipe(distinctUntilChanged()) - .subscribe(entityId => id.setValue(entityId)); + this.valueChangeEmitted$ + .pipe(takeUntil(this.ngUnsubscribe)) + .subscribe(changes => this.store.dispatch(new UpdateFilterChanges(changes.value))); + this.statusChangeEmitted$ + .pipe(takeUntil(this.ngUnsubscribe)) + .subscribe(valid => { + this.isValid = valid.value ? valid.value.length === 0 : true; + }); + + this.store + .select(fromFilter.getFilter) + .pipe(takeUntil(this.ngUnsubscribe)) + .subscribe(filter => this.filter = filter); } ngOnDestroy(): void { @@ -108,25 +65,8 @@ export class NewFilterComponent implements OnInit, OnDestroy { this.ngUnsubscribe.complete(); } - searchEntityIds(term: string): void { - if (term && term.length >= 4 && this.ids.indexOf(term) < 0) { - this.store.dispatch(new QueryEntityIds({ - term, - limit: 10 - })); - } - } - - onViewMore(query: string): void { - this.store.dispatch(new ViewMoreIds(query)); - } - - onStatusChange(status): void { - this.isValid = status === 'VALID'; - } - save(): void { - this.store.dispatch(new AddFilterRequest(this.changes.serialize())); + this.store.dispatch(new AddFilterRequest(this.filter)); } cancel(): void { diff --git a/ui/src/app/metadata/filter/effect/collection.effect.ts b/ui/src/app/metadata/filter/effect/collection.effect.ts index 5422f2d99..cc41754c2 100644 --- a/ui/src/app/metadata/filter/effect/collection.effect.ts +++ b/ui/src/app/metadata/filter/effect/collection.effect.ts @@ -4,14 +4,15 @@ import { Store } from '@ngrx/store'; import { Router } from '@angular/router'; import { of } from 'rxjs'; -import { switchMap, map, catchError, tap } from 'rxjs/operators'; +import { switchMap, map, catchError, tap, combineLatest, skipWhile, debounceTime, withLatestFrom } from 'rxjs/operators'; import * as actions from '../action/collection.action'; import { FilterCollectionActionTypes } from '../action/collection.action'; import * as fromFilter from '../reducer'; +import * as fromProvider from '../../provider/reducer'; import { MetadataFilter } from '../../domain/model'; import { removeNulls } from '../../../shared/util'; -import { EntityAttributesFilter } from '../../domain/entity/filter/entity-attributes-filter'; +import { EntityAttributesFilterEntity } from '../../domain/entity/filter/entity-attributes-filter'; import { MetadataFilterService } from '../../domain/service/filter.service'; /* istanbul ignore next */ @@ -21,9 +22,11 @@ export class FilterCollectionEffects { @Effect() loadFilters$ = this.actions$.pipe( ofType(FilterCollectionActionTypes.LOAD_FILTER_REQUEST), - switchMap(() => + map(action => action.payload), + skipWhile(providerId => !providerId), + switchMap(providerId => this.filterService - .query() + .query(providerId) .pipe( map(filters => new actions.LoadFilterSuccess(filters)), catchError(error => of(new actions.LoadFilterError(error))) @@ -32,11 +35,12 @@ export class FilterCollectionEffects { ); @Effect() selectFilterRequest$ = this.actions$.pipe( - ofType(FilterCollectionActionTypes.SELECT_FILTER), + ofType(FilterCollectionActionTypes.SELECT_FILTER_REQUEST), map(action => action.payload), - switchMap(id => { + withLatestFrom(this.store.select(fromProvider.getSelectedProviderId).pipe(skipWhile(id => !id))), + switchMap(([filterId, providerId]) => { return this.filterService - .find(id) + .find(providerId, filterId) .pipe( map(p => new actions.SelectFilterSuccess(p)), catchError(error => of(new actions.SelectFilterFail(error))) @@ -47,49 +51,45 @@ export class FilterCollectionEffects { @Effect() addFilter$ = this.actions$.pipe( - ofType(FilterCollectionActionTypes.ADD_FILTER), + ofType(FilterCollectionActionTypes.ADD_FILTER_REQUEST), map(action => action.payload), map(filter => { return { ...filter, - relyingPartyOverrides: removeNulls(new EntityAttributesFilter(filter).relyingPartyOverrides) + relyingPartyOverrides: removeNulls(new EntityAttributesFilterEntity(filter).relyingPartyOverrides) }; }), - switchMap(unsaved => - this.filterService - .save(unsaved as MetadataFilter) + withLatestFrom(this.store.select(fromProvider.getSelectedProviderId).pipe(skipWhile(id => !id))), + switchMap(([unsaved, providerId]) => { + return this.filterService + .save(providerId, unsaved as MetadataFilter) .pipe( map(saved => new actions.AddFilterSuccess(saved)), catchError(error => of(new actions.AddFilterFail(error))) - ) - ) + ); + }) ); @Effect({ dispatch: false }) addFilterSuccessRedirect$ = this.actions$.pipe( ofType(FilterCollectionActionTypes.ADD_FILTER_SUCCESS), map(action => action.payload), - tap(filter => this.router.navigate(['/dashboard'])) - ); - - @Effect() - addFilterSuccessReload$ = this.actions$.pipe( - ofType(FilterCollectionActionTypes.ADD_FILTER_SUCCESS), - map(action => action.payload), - map(filter => new actions.LoadFilterRequest()) + withLatestFrom(this.store.select(fromProvider.getSelectedProviderId).pipe(skipWhile(id => !id))), + tap(([filter, provider]) => this.router.navigate(['/', 'metadata', 'provider', provider, 'filters'])) ); @Effect() updateFilter$ = this.actions$.pipe( ofType(FilterCollectionActionTypes.UPDATE_FILTER_REQUEST), map(action => action.payload), - switchMap(filter => { + withLatestFrom(this.store.select(fromProvider.getSelectedProviderId).pipe(skipWhile(id => !id))), + switchMap(([filter, providerId]) => { delete filter.modifiedDate; delete filter.createdDate; return this.filterService - .update(filter) + .update(providerId, filter) .pipe( map(p => new actions.UpdateFilterSuccess({ - id: p.id, + id: p.resourceId, changes: p })), catchError(err => of(new actions.UpdateFilterFail(filter))) @@ -100,13 +100,8 @@ export class FilterCollectionEffects { updateFilterSuccessRedirect$ = this.actions$.pipe( ofType(FilterCollectionActionTypes.UPDATE_FILTER_SUCCESS), map(action => action.payload), - tap(filter => this.router.navigate(['/dashboard'])) - ); - @Effect() - updateFilterSuccessReload$ = this.actions$.pipe( - ofType(FilterCollectionActionTypes.UPDATE_FILTER_SUCCESS), - map(action => action.payload), - map(filter => new actions.LoadFilterRequest()) + withLatestFrom(this.store.select(fromProvider.getSelectedProviderId).pipe(skipWhile(id => !id))), + tap(([filter, provider]) => this.router.navigate(['/', 'metadata', 'provider', provider, 'filters'])) ); constructor( diff --git a/ui/src/app/metadata/filter/effect/filter.effect.ts b/ui/src/app/metadata/filter/effect/filter.effect.ts index 87b5a7f1c..0c0ce98fe 100644 --- a/ui/src/app/metadata/filter/effect/filter.effect.ts +++ b/ui/src/app/metadata/filter/effect/filter.effect.ts @@ -2,18 +2,17 @@ import { Injectable } from '@angular/core'; import { Effect, Actions, ofType } from '@ngrx/effects'; import { Store } from '@ngrx/store'; import { of } from 'rxjs'; -import { map, switchMap, catchError, withLatestFrom, tap } from 'rxjs/operators'; +import { map, switchMap, catchError, withLatestFrom, tap, combineLatest, skipWhile } from 'rxjs/operators'; import { Router } from '@angular/router'; import * as fromFilter from '../reducer'; +import * as fromProvider from '../../provider/reducer'; import * as fromRoot from '../../../app.reducer'; import { FilterCollectionActionTypes, UpdateFilterFail, - UpdateFilterRequest, - AddFilterSuccess, - LoadFilterRequest + UpdateFilterRequest } from '../action/collection.action'; import { SelectId, @@ -59,16 +58,10 @@ export class FilterEffects { ); @Effect({ dispatch: false }) - saveFilterSuccess$ = this.actions$.pipe( - ofType(FilterCollectionActionTypes.ADD_FILTER_SUCCESS), - switchMap(() => this.router.navigate(['/dashboard'])) - ); - - @Effect() cancelChanges$ = this.actions$.pipe( ofType(FilterActionTypes.CANCEL_CREATE_FILTER), - map(() => new LoadFilterRequest()), - tap(() => this.router.navigate(['/dashboard'])) + combineLatest(this.store.select(fromProvider.getSelectedProviderId).pipe(skipWhile(id => !id))), + tap(([filter, provider]) => this.router.navigate(['/', 'metadata', 'provider', provider, 'filters'])) ); constructor( diff --git a/ui/src/app/metadata/filter/filter.module.ts b/ui/src/app/metadata/filter/filter.module.ts index 22a7ed992..f4f86b486 100644 --- a/ui/src/app/metadata/filter/filter.module.ts +++ b/ui/src/app/metadata/filter/filter.module.ts @@ -19,27 +19,8 @@ import { SearchIdEffects } from './effect/search.effect'; import { FilterExistsGuard } from './guard/filter-exists.guard'; import { DomainModule } from '../domain/domain.module'; import { ModuleWithProviders } from '@angular/compiler/src/core'; - - -export const routes: Routes = [ - { - path: 'new', - component: NewFilterComponent, - canActivate: [] - }, - { - path: ':id', - component: FilterComponent, - canActivate: [FilterExistsGuard], - children: [ - { - path: 'edit', - component: EditFilterComponent, - canDeactivate: [] - } - ] - } -]; +import { FilterCollectionEffects } from './effect/collection.effect'; +import { FormModule } from '../../schema-form/schema-form.module'; @NgModule({ declarations: [ @@ -60,7 +41,8 @@ export const routes: Routes = [ SharedModule, DomainModule, HttpClientModule, - RouterModule + RouterModule, + FormModule ] }) export class FilterModule { @@ -77,9 +59,8 @@ export class FilterModule { @NgModule({ imports: [ FilterModule, - RouterModule.forChild(routes), StoreModule.forFeature('filter', reducers), - EffectsModule.forFeature([FilterEffects, SearchIdEffects]), + EffectsModule.forFeature([FilterEffects, SearchIdEffects, FilterCollectionEffects]), ], }) export class RootFilterModule { } diff --git a/ui/src/app/metadata/filter/model/entity-attributes.filter.spec.ts b/ui/src/app/metadata/filter/model/entity-attributes.filter.spec.ts new file mode 100644 index 000000000..35ebaff95 --- /dev/null +++ b/ui/src/app/metadata/filter/model/entity-attributes.filter.spec.ts @@ -0,0 +1,14 @@ +import { EntityAttributesFilter } from './entity-attributes.filter'; + +describe('Entity Attributes filter form', () => { + it('should return an empty object for validators', () => { + expect(EntityAttributesFilter.getValidators()).toEqual({}); + }); + + describe('transformer', () => { + it('should not modify the object', () => { + expect(EntityAttributesFilter.translate.formatter({})).toEqual({}); + expect(EntityAttributesFilter.translate.parser({})).toEqual({}); + }); + }); +}); diff --git a/ui/src/app/metadata/filter/model/entity-attributes.filter.ts b/ui/src/app/metadata/filter/model/entity-attributes.filter.ts new file mode 100644 index 000000000..bf2cec9b4 --- /dev/null +++ b/ui/src/app/metadata/filter/model/entity-attributes.filter.ts @@ -0,0 +1,16 @@ +import { FormDefinition } from '../../../wizard/model'; +import { MetadataFilter } from '../../domain/model'; + +export const EntityAttributesFilter: FormDefinition = { + label: 'EntityAttributes', + type: 'EntityAttributes', + schema: 'assets/schema/filter/entity-attributes.schema.json', + getValidators(): any { + const validators = {}; + return validators; + }, + translate: { + parser: (changes: any): MetadataFilter => changes, + formatter: (changes: MetadataFilter): any => changes + } +}; diff --git a/ui/src/app/metadata/filter/model/index.ts b/ui/src/app/metadata/filter/model/index.ts new file mode 100644 index 000000000..c77b3a90f --- /dev/null +++ b/ui/src/app/metadata/filter/model/index.ts @@ -0,0 +1,7 @@ +import { EntityAttributesFilter } from './entity-attributes.filter'; + +export const MetadataFilterTypes = { + EntityAttributesFilter +}; + +export * from './entity-attributes.filter'; diff --git a/ui/src/app/metadata/filter/reducer/collection.reducer.spec.ts b/ui/src/app/metadata/filter/reducer/collection.reducer.spec.ts index 0a5d9f74f..dfe5fc107 100644 --- a/ui/src/app/metadata/filter/reducer/collection.reducer.spec.ts +++ b/ui/src/app/metadata/filter/reducer/collection.reducer.spec.ts @@ -1,19 +1,18 @@ -import { reducer } from './collection.reducer'; +import { reducer, initialState as snapshot } from './collection.reducer'; import * as fromFilter from './collection.reducer'; import { FilterCollectionActionTypes, LoadFilterSuccess, UpdateFilterSuccess, - SelectFilter + SelectFilter, + SelectFilterSuccess, + AddFilterRequest, + UpdateFilterRequest, + AddFilterSuccess, + AddFilterFail, + UpdateFilterFail } from '../action/collection.action'; -import { EntityAttributesFilter } from '../../domain/entity/filter/entity-attributes-filter'; - -const snapshot: fromFilter.CollectionState = { - ids: [], - entities: {}, - selectedFilterId: null, - loaded: false -}; +import { EntityAttributesFilterEntity } from '../../domain/entity/filter/entity-attributes-filter'; describe('Filter Reducer', () => { describe('undefined action', () => { @@ -24,7 +23,7 @@ describe('Filter Reducer', () => { }); }); - describe(`${FilterCollectionActionTypes.SELECT_FILTER}`, () => { + describe(`${FilterCollectionActionTypes.SELECT_FILTER_REQUEST}`, () => { it('should set the selected id in the store', () => { const selectedFilterId = 'foo'; const action = new SelectFilter(selectedFilterId); @@ -37,8 +36,8 @@ describe('Filter Reducer', () => { it('should add the loaded filters to the collection', () => { spyOn(fromFilter.adapter, 'addAll').and.callThrough(); const filters = [ - new EntityAttributesFilter({ id: 'foo', createdDate: new Date().toLocaleDateString() }), - new EntityAttributesFilter({ id: 'bar', createdDate: new Date().toLocaleDateString() }) + new EntityAttributesFilterEntity({ resourceId: 'foo', createdDate: new Date().toLocaleDateString() }), + new EntityAttributesFilterEntity({ resourceId: 'bar', createdDate: new Date().toLocaleDateString() }) ]; const action = new LoadFilterSuccess(filters); const result = reducer(snapshot, action); @@ -46,12 +45,60 @@ describe('Filter Reducer', () => { }); }); + describe(`${FilterCollectionActionTypes.SELECT_FILTER_SUCCESS}`, () => { + it('should add the loaded filter to the collection', () => { + spyOn(fromFilter.adapter, 'addOne').and.callThrough(); + const filter = new EntityAttributesFilterEntity({ resourceId: 'foo', createdDate: new Date().toLocaleDateString() }); + const action = new SelectFilterSuccess(filter); + const result = reducer(snapshot, action); + expect(fromFilter.adapter.addOne).toHaveBeenCalled(); + }); + }); + + describe(`${FilterCollectionActionTypes.ADD_FILTER_REQUEST}`, () => { + it('should set saving to true', () => { + const filter = new EntityAttributesFilterEntity({ resourceId: 'foo', createdDate: new Date().toLocaleDateString() }); + const action = new AddFilterRequest(filter); + expect(reducer(snapshot, action).saving).toBe(true); + }); + }); + describe(`${FilterCollectionActionTypes.UPDATE_FILTER_REQUEST}`, () => { + it('should set saving to true', () => { + const filter = new EntityAttributesFilterEntity({ resourceId: 'foo', createdDate: new Date().toLocaleDateString() }); + const action = new UpdateFilterRequest(filter); + expect(reducer(snapshot, action).saving).toBe(true); + }); + }); + + describe(`${FilterCollectionActionTypes.ADD_FILTER_SUCCESS}`, () => { + it('should set saving to false', () => { + const filter = new EntityAttributesFilterEntity({ resourceId: 'foo', createdDate: new Date().toLocaleDateString() }); + const action = new AddFilterSuccess(filter); + expect(reducer(snapshot, action).saving).toBe(false); + }); + }); + + describe(`${FilterCollectionActionTypes.ADD_FILTER_FAIL}`, () => { + it('should set saving to false', () => { + const action = new AddFilterFail(new Error('error')); + expect(reducer(snapshot, action).saving).toBe(false); + }); + }); + + describe(`${FilterCollectionActionTypes.UPDATE_FILTER_FAIL}`, () => { + it('should set saving to false', () => { + const filter = new EntityAttributesFilterEntity({ resourceId: 'foo', createdDate: new Date().toLocaleDateString() }); + const action = new UpdateFilterFail(filter); + expect(reducer(snapshot, action).saving).toBe(false); + }); + }); + describe(`${FilterCollectionActionTypes.UPDATE_FILTER_SUCCESS}`, () => { - it('should add the loaded filters to the collection', () => { + it('should update the filter in the collection', () => { spyOn(fromFilter.adapter, 'updateOne').and.callThrough(); const update = { id: 'foo', - changes: new EntityAttributesFilter({ id: 'foo', name: 'bar', createdDate: new Date().toLocaleDateString() }), + changes: new EntityAttributesFilterEntity({ resourceId: 'foo', name: 'bar', createdDate: new Date().toLocaleDateString() }), }; const action = new UpdateFilterSuccess(update); const result = reducer(snapshot, action); diff --git a/ui/src/app/metadata/filter/reducer/collection.reducer.ts b/ui/src/app/metadata/filter/reducer/collection.reducer.ts index 2eeff6ff1..35074b696 100644 --- a/ui/src/app/metadata/filter/reducer/collection.reducer.ts +++ b/ui/src/app/metadata/filter/reducer/collection.reducer.ts @@ -5,6 +5,7 @@ import { MetadataFilter } from '../../domain/model'; export interface CollectionState extends EntityState { selectedFilterId: string | null; loaded: boolean; + saving: boolean; } export function sortByDate(a: MetadataFilter, b: MetadataFilter): number { @@ -18,7 +19,8 @@ export const adapter: EntityAdapter = createEntityAdapter state.selectedFilterId; export const getIsLoaded = (state: CollectionState) => state.loaded; +export const getIsSaving = (state: CollectionState) => state.saving; export const { selectIds: selectFilterIds, selectEntities: selectFilterEntities, diff --git a/ui/src/app/metadata/filter/reducer/filter.reducer.spec.ts b/ui/src/app/metadata/filter/reducer/filter.reducer.spec.ts index f4f4cbe82..ff832e198 100644 --- a/ui/src/app/metadata/filter/reducer/filter.reducer.spec.ts +++ b/ui/src/app/metadata/filter/reducer/filter.reducer.spec.ts @@ -1,4 +1,4 @@ -import { reducer } from './filter.reducer'; +import { reducer, initialState as snapshot } from './filter.reducer'; import * as fromFilter from './filter.reducer'; import { SelectId, LoadEntityPreviewSuccess, UpdateFilterChanges, FilterActionTypes, CancelCreateFilter } from '../action/filter.action'; import { SearchActionTypes } from '../action/search.action'; @@ -16,15 +16,7 @@ import { } from '../action/collection.action'; import { MDUI } from '../../domain/model'; import { MetadataFilter } from '../../domain/model'; -import { EntityAttributesFilter } from '../../domain/entity/filter/entity-attributes-filter'; - - -const snapshot: fromFilter.FilterState = { - selected: null, - changes: null, - preview: null, - saving: false -}; +import { EntityAttributesFilterEntity } from '../../domain/entity/filter/entity-attributes-filter'; const mdui: MDUI = { displayName: 'foo', @@ -70,28 +62,15 @@ describe('Filter Reducer', () => { }); }); - describe(`${FilterCollectionActionTypes.ADD_FILTER} action`, () => { - it('should set saving to true', () => { - const result = reducer(snapshot, new AddFilterRequest(new EntityAttributesFilter())); - expect(result.saving).toBe(true); - }); - }); - describe(`${FilterCollectionActionTypes.UPDATE_FILTER_REQUEST} action`, () => { - it('should set saving to true', () => { - const result = reducer(snapshot, new UpdateFilterRequest(new EntityAttributesFilter())); - expect(result.saving).toBe(true); - }); - }); - describe(`${FilterCollectionActionTypes.ADD_FILTER_SUCCESS} action`, () => { it('should set saving to true', () => { - const result = reducer(snapshot, new AddFilterSuccess(new EntityAttributesFilter())); + const result = reducer(snapshot, new AddFilterSuccess(new EntityAttributesFilterEntity())); expect(result).toEqual(fromFilter.initialState); }); }); describe(`${FilterCollectionActionTypes.UPDATE_FILTER_SUCCESS} action`, () => { it('should set saving to true', () => { - const update = {id: 'foo', changes: new EntityAttributesFilter({id: 'foo'})}; + const update = {id: 'foo', changes: new EntityAttributesFilterEntity({id: 'foo'})}; const result = reducer(snapshot, new UpdateFilterSuccess(update)); expect(result).toEqual(fromFilter.initialState); }); diff --git a/ui/src/app/metadata/filter/reducer/filter.reducer.ts b/ui/src/app/metadata/filter/reducer/filter.reducer.ts index 3bce96ae3..653d53740 100644 --- a/ui/src/app/metadata/filter/reducer/filter.reducer.ts +++ b/ui/src/app/metadata/filter/reducer/filter.reducer.ts @@ -13,14 +13,12 @@ export interface FilterState { selected: string | null; changes: MetadataFilter | null; preview: MDUI | null; - saving: boolean; } export const initialState: FilterState = { selected: null, changes: null, - preview: null, - saving: false + preview: null }; export function reducer(state = initialState, action: FilterActionsUnion | SearchActionsUnion | FilterCollectionActionsUnion): FilterState { @@ -46,23 +44,6 @@ export function reducer(state = initialState, action: FilterActionsUnion | Searc } }; } - case FilterCollectionActionTypes.ADD_FILTER: - case FilterCollectionActionTypes.UPDATE_FILTER_REQUEST: { - return { - ...state, - saving: true - }; - } - case FilterCollectionActionTypes.ADD_FILTER_FAIL: - case FilterCollectionActionTypes.UPDATE_FILTER_FAIL: { - return { - ...state, - saving: false - }; - } - case FilterCollectionActionTypes.ADD_FILTER_SUCCESS: - case FilterCollectionActionTypes.UPDATE_FILTER_SUCCESS: - case SearchActionTypes.CLEAR_SEARCH: case FilterActionTypes.CANCEL_CREATE_FILTER: { return { ...initialState @@ -77,4 +58,3 @@ export function reducer(state = initialState, action: FilterActionsUnion | Searc export const getSelected = (state: FilterState) => state.selected; export const getFilterChanges = (state: FilterState) => state.changes; export const getPreview = (state: FilterState) => state.preview; -export const getSaving = (state: FilterState) => state.saving; diff --git a/ui/src/app/metadata/filter/reducer/index.spec.ts b/ui/src/app/metadata/filter/reducer/index.spec.ts new file mode 100644 index 000000000..3d30cf891 --- /dev/null +++ b/ui/src/app/metadata/filter/reducer/index.spec.ts @@ -0,0 +1,21 @@ +import * as selectors from './index'; + +describe('filter selectors', () => { + describe('mergeFn', () => { + it('should return merged objects', () => { + expect(selectors.mergeFn({foo: 'bar' }, { resourceId: 'baz' })).toEqual({foo: 'bar', resourceId: 'baz'}); + }); + }); + + describe('filterTypeFn', () => { + it('should return filtered objects', () => { + const filters = [{ '@type': 'EntityAttributes' }]; + expect(selectors.filterTypeFn(filters)).toEqual(filters); + }); + + it('should return filtered objects', () => { + const filters = [{ '@type': 'EntityAttributes' }, { '@type': 'EntityRoleWhiteList' }]; + expect(selectors.filterTypeFn(filters).length).toBe(1); + }); + }); +}); diff --git a/ui/src/app/metadata/filter/reducer/index.ts b/ui/src/app/metadata/filter/reducer/index.ts index 5ae10da51..5d9a4ecbe 100644 --- a/ui/src/app/metadata/filter/reducer/index.ts +++ b/ui/src/app/metadata/filter/reducer/index.ts @@ -32,7 +32,6 @@ export const getFilterFromState = createSelector(getFilterState, getFiltersFromS export const getSelected = createSelector(getFilterFromState, fromFilter.getSelected); export const getFilter = createSelector(getFilterFromState, fromFilter.getFilterChanges); export const getPreview = createSelector(getFilterFromState, fromFilter.getPreview); -export const getSaving = createSelector(getFilterFromState, fromFilter.getSaving); /* * Select pieces of Search Collection @@ -50,8 +49,23 @@ export const getViewingMore = createSelector(getSearchFromState, fromSearch.getV */ export const getCollectionState = createSelector(getFilterState, getCollectionFromStateFn); export const getAllFilters = createSelector(getCollectionState, fromCollection.selectAllFilters); +export const getCollectionSaving = createSelector(getCollectionState, fromCollection.getIsSaving); + +export const notAddtlFilters = ['RequiredValidUntil', 'SignatureValidation', 'EntityRoleWhiteList']; +export const filterTypeFn = filters => filters.filter(f => notAddtlFilters.indexOf(f['@type']) === -1); + +export const getAdditionalFilters = createSelector(getAllFilters, filterTypeFn); + export const getFilterEntities = createSelector(getCollectionState, fromCollection.selectFilterEntities); export const getSelectedFilterId = createSelector(getCollectionState, fromCollection.getSelectedFilterId); export const getSelectedFilter = createSelector(getFilterEntities, getSelectedFilterId, utils.getInCollectionFn); export const getFilterIds = createSelector(getCollectionState, fromCollection.selectFilterIds); export const getFilterCollectionIsLoaded = createSelector(getCollectionState, fromCollection.getIsLoaded); + +/* + * Combine pieces of State +*/ + +export const mergeFn = (changes, filter) => ({ ...filter, ...changes }); + +export const getFilterWithChanges = createSelector(getFilter, getSelectedFilter, mergeFn); diff --git a/ui/src/app/metadata/manager/component/entity-item.component.spec.ts b/ui/src/app/metadata/manager/component/entity-item.component.spec.ts index b71ce9f57..1de355104 100644 --- a/ui/src/app/metadata/manager/component/entity-item.component.spec.ts +++ b/ui/src/app/metadata/manager/component/entity-item.component.spec.ts @@ -7,8 +7,6 @@ describe('Resolver List item', () => { let fixture: ComponentFixture; let instance: EntityItemComponent; - let resolver = new FileBackedHttpMetadataResolver({ entityId: 'foo', serviceProviderName: 'bar' }); - beforeEach(() => { TestBed.configureTestingModule({ providers: [], @@ -20,7 +18,6 @@ describe('Resolver List item', () => { fixture = TestBed.createComponent(EntityItemComponent); instance = fixture.componentInstance; - instance.entity = resolver; }); it('should compile', () => { diff --git a/ui/src/app/metadata/manager/component/entity-item.component.ts b/ui/src/app/metadata/manager/component/entity-item.component.ts index b5aed3756..b2453c474 100644 --- a/ui/src/app/metadata/manager/component/entity-item.component.ts +++ b/ui/src/app/metadata/manager/component/entity-item.component.ts @@ -6,18 +6,14 @@ import { MetadataTypes } from '../../domain/domain.type'; @Component({ selector: 'entity-item', changeDetection: ChangeDetectionStrategy.OnPush, - templateUrl: './entity-item.component.html', - styleUrls: ['./entity-item.component.scss'] + template: '', + styleUrls: [] }) export class EntityItemComponent { - types = MetadataTypes; - - @Input() entity: MetadataEntity; @Input() isOpen: boolean; - @Output() select = new EventEmitter(); - @Output() toggle = new EventEmitter(); - @Output() preview = new EventEmitter(); - @Output() delete = new EventEmitter(); - -} /* istanbul ignore next */ + @Output() select = new EventEmitter(); + @Output() toggle = new EventEmitter(); + @Output() preview = new EventEmitter(); + @Output() delete = new EventEmitter(); +} diff --git a/ui/src/app/metadata/manager/component/provider-item.component.html b/ui/src/app/metadata/manager/component/provider-item.component.html new file mode 100644 index 000000000..1ca8f575b --- /dev/null +++ b/ui/src/app/metadata/manager/component/provider-item.component.html @@ -0,0 +1,78 @@ +
+
+
+
+

{{ index }}

+ + +
+
+
+ +   + + + +
+
+ {{ provider.name }} + + {{ provider['@type'] }} +
+
+
+ + +
+
+
+
+
+
+
+
+
+ Metadata Provider Name: +
+
{{ provider.name }}
+
+ Created Date: +
+
{{ provider.createdDate | date:'medium' }}
+
+
+
+ Metadata Provider Type: +
+
{{ provider['@type'] }}
+
+ Metadata Provider Status: +
+
+ + Enabled + Disabled +
+
+
+
+
+
+
+ + diff --git a/ui/src/app/metadata/manager/component/provider-item.component.scss b/ui/src/app/metadata/manager/component/provider-item.component.scss new file mode 100644 index 000000000..2ab451b75 --- /dev/null +++ b/ui/src/app/metadata/manager/component/provider-item.component.scss @@ -0,0 +1,44 @@ +@import '../../../../theme/palette'; + +.card { + background-color: rgba($light-grey, 0.5); + &:focus { + outline: -webkit-focus-ring-color auto 5px; + } + .btn-link { + &:focus { + outline: -webkit-focus-ring-color auto 5px; + } + } + + .label-icon { + display: inline-block; + min-width: 64px; + text-align: center; + } + + .reorder-card { + width: 65px; + margin-left: 20px; + background-color: $white; + border: 1px solid $gray-400; + border-radius: 4px; + + p { + margin-bottom: 0; + padding-top: 3px; + font-size: 1.40rem; + line-height: 1; + } + + .btn { + padding: 2px 5px; + } + } + + .btn-filter { + .label { + font-size: .90rem; + } + } +} diff --git a/ui/src/app/metadata/manager/component/provider-item.component.spec.ts b/ui/src/app/metadata/manager/component/provider-item.component.spec.ts new file mode 100644 index 000000000..23ad6bc62 --- /dev/null +++ b/ui/src/app/metadata/manager/component/provider-item.component.spec.ts @@ -0,0 +1,69 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { ProviderItemComponent } from './provider-item.component'; +import { ViewChild, Component } from '@angular/core'; +import { MetadataProvider } from '../../domain/model'; + +@Component({ + template: ` + + + ` +}) +class TestHostComponent { + @ViewChild(ProviderItemComponent) + public componentUnderTest: ProviderItemComponent; + + private _provider; + private _open; + + public set isOpen(open: boolean) { + this._open = open; + } + + public get isOpen(): boolean { + return this._open; + } + + public set provider(provider: MetadataProvider) { + this._provider = provider; + } + + public get provider(): MetadataProvider { + return this._provider; + } +} + +describe('Provider List item', () => { + let fixture: ComponentFixture; + let instance: TestHostComponent; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [], + imports: [ + NoopAnimationsModule + ], + declarations: [ + ProviderItemComponent, + TestHostComponent + ], + }); + + fixture = TestBed.createComponent(TestHostComponent); + instance = fixture.componentInstance; + + instance.provider = { + resourceId: 'foo', + metadataFilters: [] + }; + }); + + it('should compile', () => { + fixture.detectChanges(); + + expect(fixture).toBeDefined(); + }); +}); diff --git a/ui/src/app/metadata/manager/component/provider-item.component.ts b/ui/src/app/metadata/manager/component/provider-item.component.ts new file mode 100644 index 000000000..ad8fa572b --- /dev/null +++ b/ui/src/app/metadata/manager/component/provider-item.component.ts @@ -0,0 +1,23 @@ +import { Component, Input, ChangeDetectionStrategy, Output, EventEmitter } from '@angular/core'; + +import { MetadataProvider } from '../../domain/model'; +import { EntityItemComponent } from './entity-item.component'; + +@Component({ + selector: 'provider-item', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './provider-item.component.html', + styleUrls: ['./provider-item.component.scss'] +}) + +export class ProviderItemComponent extends EntityItemComponent { + @Input() provider: MetadataProvider; + @Input() index: number; + @Input() first: boolean; + @Input() last: boolean; + + @Output() viewFilters: EventEmitter = new EventEmitter(); + + @Output() changeOrderUp: EventEmitter = new EventEmitter(); + @Output() changeOrderDown: EventEmitter = new EventEmitter(); +} diff --git a/ui/src/app/metadata/manager/component/entity-item.component.html b/ui/src/app/metadata/manager/component/resolver-item.component.html similarity index 100% rename from ui/src/app/metadata/manager/component/entity-item.component.html rename to ui/src/app/metadata/manager/component/resolver-item.component.html diff --git a/ui/src/app/metadata/manager/component/entity-item.component.scss b/ui/src/app/metadata/manager/component/resolver-item.component.scss similarity index 100% rename from ui/src/app/metadata/manager/component/entity-item.component.scss rename to ui/src/app/metadata/manager/component/resolver-item.component.scss diff --git a/ui/src/app/metadata/manager/component/resolver-item.component.spec.ts b/ui/src/app/metadata/manager/component/resolver-item.component.spec.ts new file mode 100644 index 000000000..ac0f2bf84 --- /dev/null +++ b/ui/src/app/metadata/manager/component/resolver-item.component.spec.ts @@ -0,0 +1,34 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { EntityItemComponent } from './entity-item.component'; +import { FileBackedHttpMetadataResolver } from '../../domain/entity'; +import { ResolverItemComponent } from './resolver-item.component'; + +describe('Resolver List item', () => { + let fixture: ComponentFixture; + let instance: ResolverItemComponent; + + let resolver = new FileBackedHttpMetadataResolver({ entityId: 'foo', serviceProviderName: 'bar' }); + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [], + imports: [ + NoopAnimationsModule + ], + declarations: [ + ResolverItemComponent + ], + }); + + fixture = TestBed.createComponent(ResolverItemComponent); + instance = fixture.componentInstance; + instance.entity = resolver; + }); + + it('should compile', () => { + fixture.detectChanges(); + + expect(fixture).toBeDefined(); + }); +}); diff --git a/ui/src/app/metadata/manager/component/resolver-item.component.ts b/ui/src/app/metadata/manager/component/resolver-item.component.ts new file mode 100644 index 000000000..cb2f31adb --- /dev/null +++ b/ui/src/app/metadata/manager/component/resolver-item.component.ts @@ -0,0 +1,14 @@ +import { Component, ChangeDetectionStrategy, Input } from '@angular/core'; + +import { MetadataEntity } from '../../domain/model'; +import { EntityItemComponent } from './entity-item.component'; + +@Component({ + selector: 'resolver-item', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './resolver-item.component.html', + styleUrls: ['./resolver-item.component.scss'] +}) +export class ResolverItemComponent extends EntityItemComponent { + @Input() entity: MetadataEntity; +} diff --git a/ui/src/app/metadata/manager/container/dashboard-providers-list.component.html b/ui/src/app/metadata/manager/container/dashboard-providers-list.component.html index 5690a336e..467538039 100644 --- a/ui/src/app/metadata/manager/container/dashboard-providers-list.component.html +++ b/ui/src/app/metadata/manager/container/dashboard-providers-list.component.html @@ -8,13 +8,23 @@ - - - diff --git a/ui/src/app/metadata/manager/container/dashboard-providers-list.component.spec.ts b/ui/src/app/metadata/manager/container/dashboard-providers-list.component.spec.ts new file mode 100644 index 000000000..b185dca02 --- /dev/null +++ b/ui/src/app/metadata/manager/container/dashboard-providers-list.component.spec.ts @@ -0,0 +1,83 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { Router } from '@angular/router'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { StoreModule, Store, combineReducers } from '@ngrx/store'; +import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap/pagination/pagination.module'; +import { NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap/modal/modal.module'; +import * as fromDashboard from '../reducer'; +import { ProviderSearchComponent } from '../component/provider-search.component'; +import { DeleteDialogComponent } from '../component/delete-dialog.component'; +import { RouterStub } from '../../../../testing/router.stub'; +import { NgbModalStub } from '../../../../testing/modal.stub'; +import { DashboardProvidersListComponent } from './dashboard-providers-list.component'; +import { MetadataProvider } from '../../domain/model'; +import { ProviderItemComponent } from '../component/provider-item.component'; +import { FileBackedHttpMetadataResolver } from '../../domain/entity'; + + +describe('Dashboard Providers List Page', () => { + let fixture: ComponentFixture; + let store: Store; + let router: Router; + let modal: NgbModal; + let instance: DashboardProvidersListComponent; + + let provider = { + resourceId: 'foo', + name: 'bar' + }; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + { provide: Router, useClass: RouterStub }, + { provide: NgbModal, useClass: NgbModalStub } + ], + imports: [ + NoopAnimationsModule, + StoreModule.forRoot({ + manager: combineReducers(fromDashboard.reducers), + }), + ReactiveFormsModule, + NgbPaginationModule, + NgbModalModule + ], + declarations: [ + DashboardProvidersListComponent, + ProviderSearchComponent, + ProviderItemComponent, + DeleteDialogComponent + ], + }); + + fixture = TestBed.createComponent(DashboardProvidersListComponent); + instance = fixture.componentInstance; + store = TestBed.get(Store); + router = TestBed.get(Router); + modal = TestBed.get(NgbModal); + + spyOn(store, 'dispatch').and.callThrough(); + }); + + it('should compile', () => { + fixture.detectChanges(); + + expect(fixture).toBeDefined(); + }); + + describe('toggleProvider method', () => { + it('should fire a redux action', () => { + instance.toggleEntity(provider); + expect(store.dispatch).toHaveBeenCalled(); + }); + }); + + describe('edit method', () => { + it('should route to the edit page', () => { + spyOn(router, 'navigate'); + instance.edit(provider); + expect(router.navigate).toHaveBeenCalledWith(['metadata', 'provider', provider.resourceId, 'edit']); + }); + }); +}); diff --git a/ui/src/app/metadata/manager/container/dashboard-providers-list.component.ts b/ui/src/app/metadata/manager/container/dashboard-providers-list.component.ts index fd43e20c6..88ec0b19e 100644 --- a/ui/src/app/metadata/manager/container/dashboard-providers-list.component.ts +++ b/ui/src/app/metadata/manager/container/dashboard-providers-list.component.ts @@ -1,21 +1,56 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; import { MetadataProvider } from '../../domain/model'; import { Observable } from '../../../../../node_modules/rxjs'; import { Store } from '@ngrx/store'; -import { ProviderState, getAllProviders } from '../../provider/reducer'; + +import { ProviderState, getOrderedProviders } from '../../provider/reducer'; +import { getOpenProviders } from '../reducer'; +import { ToggleEntityDisplay } from '../action/manager.action'; +import { map } from 'rxjs/operators'; +import { ChangeOrderUp, ChangeOrderDown } from '../../provider/action/collection.action'; @Component({ selector: 'dashboard-providers-list', templateUrl: './dashboard-providers-list.component.html' }) -export class DashboardProvidersListComponent { +export class DashboardProvidersListComponent implements OnInit { providers$: Observable; + providersOpen$: Observable<{ [key: string]: boolean }>; constructor( - private store: Store - ) { - this.providers$ = this.store.select(getAllProviders); + private store: Store, + private router: Router + ) { } + + ngOnInit(): void { + this.providers$ = this.store.select(getOrderedProviders); + this.providersOpen$ = this.store.select(getOpenProviders); + } + + view(id: string, page: string): void { + this.router.navigate(['metadata', 'provider', id, page]); + } + + edit(provider: MetadataProvider): void { + this.view(provider.resourceId, 'edit'); + } + + gotoFilters(provider: MetadataProvider): void { + this.view(provider.resourceId, 'filters'); + } + + toggleEntity(provider: MetadataProvider): void { + this.store.dispatch(new ToggleEntityDisplay(provider.resourceId)); + } + + updateOrderUp(provider: MetadataProvider): void { + this.store.dispatch(new ChangeOrderUp(provider.resourceId)); + } + + updateOrderDown(provider: MetadataProvider): void { + this.store.dispatch(new ChangeOrderDown(provider.resourceId)); } } diff --git a/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.html b/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.html index 647b976a0..d771864ca 100644 --- a/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.html +++ b/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.html @@ -16,17 +16,17 @@
    -
  • - - + +
diff --git a/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.spec.ts b/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.spec.ts index 9a9a8150d..ca8d2b6ff 100644 --- a/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.spec.ts +++ b/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.spec.ts @@ -13,6 +13,7 @@ import { RouterStub } from '../../../../testing/router.stub'; import { NgbModalStub } from '../../../../testing/modal.stub'; import { FileBackedHttpMetadataResolver } from '../../domain/entity'; import { DashboardResolversListComponent } from './dashboard-resolvers-list.component'; +import { ResolverItemComponent } from '../component/resolver-item.component'; describe('Dashboard Resolvers List Page', () => { @@ -50,7 +51,7 @@ describe('Dashboard Resolvers List Page', () => { declarations: [ DashboardResolversListComponent, ProviderSearchComponent, - EntityItemComponent, + ResolverItemComponent, DeleteDialogComponent ], }); diff --git a/ui/src/app/metadata/manager/container/manager.component.html b/ui/src/app/metadata/manager/container/manager.component.html index c77eb319f..42a490c44 100644 --- a/ui/src/app/metadata/manager/container/manager.component.html +++ b/ui/src/app/metadata/manager/container/manager.component.html @@ -1,10 +1,10 @@
diff --git a/ui/src/app/metadata/manager/container/manager.component.spec.ts b/ui/src/app/metadata/manager/container/manager.component.spec.ts new file mode 100644 index 000000000..be02fd913 --- /dev/null +++ b/ui/src/app/metadata/manager/container/manager.component.spec.ts @@ -0,0 +1,35 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { ManagerComponent } from './manager.component'; +import { RouterModule, Router } from '@angular/router'; +import { RouterStub, RouterLinkStubDirective, RouterOutletStubComponent } from '../../../../testing/router.stub'; + +describe('Metadata Manager Parent Page', () => { + let fixture: ComponentFixture; + let instance: ManagerComponent; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + { provide: Router, useClass: RouterStub } + ], + imports: [ + NoopAnimationsModule, + ], + declarations: [ + ManagerComponent, + RouterLinkStubDirective, + RouterOutletStubComponent + ], + }); + + fixture = TestBed.createComponent(ManagerComponent); + instance = fixture.componentInstance; + }); + + it('should compile', () => { + fixture.detectChanges(); + + expect(fixture).toBeDefined(); + }); +}); diff --git a/ui/src/app/metadata/manager/effect/search.effects.ts b/ui/src/app/metadata/manager/effect/search.effects.ts index fa3386def..e1da7c539 100644 --- a/ui/src/app/metadata/manager/effect/search.effects.ts +++ b/ui/src/app/metadata/manager/effect/search.effects.ts @@ -13,12 +13,6 @@ import { FileBackedHttpMetadataResolver } from '../../domain/entity'; @Injectable() export class SearchEffects { - @Effect() - filter$ = this.actions$.pipe( - ofType(entitySearch.ENTITY_FILTER), - switchMap(() => this.performSearch()) - ); - @Effect() search$ = this.actions$.pipe( ofType(entitySearch.ENTITY_SEARCH), @@ -45,9 +39,12 @@ export class SearchEffects { ), combineLatest( this.store.select(fromManager.getSearchQuery), - (entities, term) => entities.filter( - e => this.matcher(e.name, term) || this.matcher(e.entityId, term) - ) + (entities, term) => { + const filtered = entities.filter( + e => this.matcher(e.name, term) || this.matcher(e.entityId, term) + ); + return filtered; + } ), map(entities => new entitySearch.SearchCompleteAction(entities)) ); diff --git a/ui/src/app/metadata/manager/manager.module.ts b/ui/src/app/metadata/manager/manager.module.ts index ed29e36c9..0f00569d1 100644 --- a/ui/src/app/metadata/manager/manager.module.ts +++ b/ui/src/app/metadata/manager/manager.module.ts @@ -9,6 +9,8 @@ import { EffectsModule } from '@ngrx/effects'; import { ManagerComponent } from './container/manager.component'; import { EntityItemComponent } from './component/entity-item.component'; +import { ProviderItemComponent } from './component/provider-item.component'; +import { ResolverItemComponent } from './component/resolver-item.component'; import { ProviderSearchComponent } from './component/provider-search.component'; import { DashboardResolversListComponent } from './container/dashboard-resolvers-list.component'; import { DashboardProvidersListComponent } from './container/dashboard-providers-list.component'; @@ -22,6 +24,8 @@ import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; declarations: [ ManagerComponent, EntityItemComponent, + ResolverItemComponent, + ProviderItemComponent, ProviderSearchComponent, DeleteDialogComponent, DashboardResolversListComponent, diff --git a/ui/src/app/metadata/manager/reducer/index.ts b/ui/src/app/metadata/manager/reducer/index.ts index d35d2b66e..2585fb182 100644 --- a/ui/src/app/metadata/manager/reducer/index.ts +++ b/ui/src/app/metadata/manager/reducer/index.ts @@ -2,6 +2,8 @@ import { createSelector, createFeatureSelector } from '@ngrx/store'; import * as fromRoot from '../../../core/reducer'; import * as fromDashboard from './manager.reducer'; import * as fromSearch from './search.reducer'; +import { MetadataEntity } from '../../domain/model'; +import { Metadata } from '../../domain/domain.type'; export interface DashboardState { manager: fromDashboard.State; @@ -33,10 +35,6 @@ export const getSearchQuery = createSelector( getSearchState, fromSearch.getQuery ); -export const getFilterType = createSelector( - getSearchState, - fromSearch.getFilter -); export const getSearchLoading = createSelector( getSearchState, fromSearch.getLoading diff --git a/ui/src/app/metadata/manager/reducer/search.reducer.ts b/ui/src/app/metadata/manager/reducer/search.reducer.ts index 2ab931fc0..17e700abd 100644 --- a/ui/src/app/metadata/manager/reducer/search.reducer.ts +++ b/ui/src/app/metadata/manager/reducer/search.reducer.ts @@ -5,14 +5,12 @@ export interface SearchState { entities: MetadataEntity[]; loading: boolean; query: string; - kind: string; } const initialState: SearchState = { entities: [], loading: false, - query: '', - kind: 'all' + query: '' }; export function reducer(state = initialState, action: searchActions.Actions): SearchState { @@ -25,20 +23,11 @@ export function reducer(state = initialState, action: searchActions.Actions): Se }; } - case searchActions.ENTITY_FILTER: { - return { - ...state, - kind: action.payload, - loading: true - }; - } - case searchActions.ENTITY_SEARCH_COMPLETE: { return { entities: action.payload, loading: false, - query: state.query, - kind: state.kind + query: state.query }; } @@ -53,5 +42,3 @@ export const getEntities = (state: SearchState) => state.entities; export const getQuery = (state: SearchState) => state.query; export const getLoading = (state: SearchState) => state.loading; - -export const getFilter = (state: SearchState) => state.kind; diff --git a/ui/src/app/metadata/metadata.component.spec.ts b/ui/src/app/metadata/metadata.component.spec.ts index dffc99361..89b2fc4ac 100644 --- a/ui/src/app/metadata/metadata.component.spec.ts +++ b/ui/src/app/metadata/metadata.component.spec.ts @@ -50,6 +50,6 @@ describe('Metadata Root Component', () => { it('should load metadata objects', async(() => { expect(app).toBeTruthy(); - expect(store.dispatch).toHaveBeenCalledTimes(4); + expect(store.dispatch).toHaveBeenCalledTimes(3); })); }); diff --git a/ui/src/app/metadata/metadata.component.ts b/ui/src/app/metadata/metadata.component.ts index 62170d286..df271e2fd 100644 --- a/ui/src/app/metadata/metadata.component.ts +++ b/ui/src/app/metadata/metadata.component.ts @@ -2,7 +2,6 @@ import { Component, ChangeDetectionStrategy } from '@angular/core'; import { Store } from '@ngrx/store'; import { LoadResolverRequest } from './resolver/action/collection.action'; -import { LoadFilterRequest } from './filter/action/collection.action'; import { LoadDraftRequest } from './resolver/action/draft.action'; import * as fromRoot from '../app.reducer'; import { LoadProviderRequest } from './provider/action/collection.action'; @@ -19,7 +18,6 @@ export class MetadataPageComponent { private store: Store ) { this.store.dispatch(new LoadResolverRequest()); - this.store.dispatch(new LoadFilterRequest()); this.store.dispatch(new LoadDraftRequest()); this.store.dispatch(new LoadProviderRequest()); } diff --git a/ui/src/app/metadata/provider/action/collection.action.ts b/ui/src/app/metadata/provider/action/collection.action.ts index 7eaf7d1df..637c58115 100644 --- a/ui/src/app/metadata/provider/action/collection.action.ts +++ b/ui/src/app/metadata/provider/action/collection.action.ts @@ -1,6 +1,7 @@ import { Action } from '@ngrx/store'; import { MetadataProvider } from '../../domain/model/metadata-provider'; import { Update } from '@ngrx/entity'; +import { ProviderOrder } from '../../domain/model/metadata-order'; export enum ProviderCollectionActionTypes { UPDATE_PROVIDER_REQUEST = '[Metadata Provider] Update Request', @@ -21,7 +22,18 @@ export enum ProviderCollectionActionTypes { REMOVE_PROVIDER_REQUEST = '[Metadata Provider Collection] Remove Provider Request', REMOVE_PROVIDER_SUCCESS = '[Metadata Provider Collection] Remove Provider Success', - REMOVE_PROVIDER_FAIL = '[Metadata Provider Collection] Remove Provider Fail' + REMOVE_PROVIDER_FAIL = '[Metadata Provider Collection] Remove Provider Fail', + + SET_ORDER_PROVIDER_REQUEST = '[Metadata Provider Collection] Set Order Provider Request', + SET_ORDER_PROVIDER_SUCCESS = '[Metadata Provider Collection] Set Order Remove Provider Success', + SET_ORDER_PROVIDER_FAIL = '[Metadata Provider Collection] Set Order Remove Provider Fail', + + GET_ORDER_PROVIDER_REQUEST = '[Metadata Provider Collection] Get Order Remove Provider Request', + GET_ORDER_PROVIDER_SUCCESS = '[Metadata Provider Collection] Get Order Remove Provider Success', + GET_ORDER_PROVIDER_FAIL = '[Metadata Provider Collection] Get Order Remove Provider Fail', + + CHANGE_PROVIDER_ORDER_UP = '[Metadata Provider Collection] Change Order Up', + CHANGE_PROVIDER_ORDER_DOWN = '[Metadata Provider Collection] Change Order Down', } export class LoadProviderRequest implements Action { @@ -45,7 +57,7 @@ export class LoadProviderError implements Action { export class SelectProviderRequest implements Action { readonly type = ProviderCollectionActionTypes.SELECT_PROVIDER_REQUEST; - constructor(public payload: any) { } + constructor(public payload: string) { } } export class SelectProviderSuccess implements Action { @@ -114,6 +126,54 @@ export class RemoveProviderFail implements Action { constructor(public payload: MetadataProvider) { } } +export class SetOrderProviderRequest implements Action { + readonly type = ProviderCollectionActionTypes.SET_ORDER_PROVIDER_REQUEST; + + constructor(public payload: ProviderOrder) { } +} + +export class SetOrderProviderSuccess implements Action { + readonly type = ProviderCollectionActionTypes.SET_ORDER_PROVIDER_SUCCESS; + + constructor() { } +} + +export class SetOrderProviderFail implements Action { + readonly type = ProviderCollectionActionTypes.SET_ORDER_PROVIDER_FAIL; + + constructor(public payload: Error) { } +} + +export class GetOrderProviderRequest implements Action { + readonly type = ProviderCollectionActionTypes.GET_ORDER_PROVIDER_REQUEST; + + constructor() { } +} + +export class GetOrderProviderSuccess implements Action { + readonly type = ProviderCollectionActionTypes.GET_ORDER_PROVIDER_SUCCESS; + + constructor(public payload: ProviderOrder) { } +} + +export class GetOrderProviderFail implements Action { + readonly type = ProviderCollectionActionTypes.GET_ORDER_PROVIDER_FAIL; + + constructor(public payload: Error) { } +} + +export class ChangeOrderUp implements Action { + readonly type = ProviderCollectionActionTypes.CHANGE_PROVIDER_ORDER_UP; + + constructor(public payload: string) { } +} + +export class ChangeOrderDown implements Action { + readonly type = ProviderCollectionActionTypes.CHANGE_PROVIDER_ORDER_DOWN; + + constructor(public payload: string) { } +} + export type ProviderCollectionActionsUnion = | LoadProviderRequest | LoadProviderSuccess @@ -129,4 +189,12 @@ export type ProviderCollectionActionsUnion = | RemoveProviderFail | UpdateProviderRequest | UpdateProviderSuccess - | UpdateProviderFail; + | UpdateProviderFail + | SetOrderProviderRequest + | SetOrderProviderSuccess + | SetOrderProviderFail + | GetOrderProviderRequest + | GetOrderProviderSuccess + | GetOrderProviderFail + | ChangeOrderUp + | ChangeOrderDown; diff --git a/ui/src/app/metadata/provider/action/editor.action.ts b/ui/src/app/metadata/provider/action/editor.action.ts index 85ea00edd..3c82c0c19 100644 --- a/ui/src/app/metadata/provider/action/editor.action.ts +++ b/ui/src/app/metadata/provider/action/editor.action.ts @@ -8,7 +8,10 @@ export enum EditorActionTypes { SELECT_PROVIDER_TYPE = '[Provider Editor] Select Provider Type', - CLEAR = '[Provider Editor] Clear' + CLEAR = '[Provider Editor] Clear', + + LOCK = '[Provider Editor] Lock', + UNLOCK = '[Provider Editor] Unlock' } export class UpdateStatus implements Action { @@ -45,10 +48,20 @@ export class ClearEditor implements Action { readonly type = EditorActionTypes.CLEAR; } +export class LockEditor implements Action { + readonly type = EditorActionTypes.LOCK; +} + +export class UnlockEditor implements Action { + readonly type = EditorActionTypes.UNLOCK; +} + export type EditorActionUnion = | UpdateStatus | LoadSchemaRequest | LoadSchemaSuccess | LoadSchemaFail | SelectProviderType - | ClearEditor; + | ClearEditor + | LockEditor + | UnlockEditor; diff --git a/ui/src/app/metadata/provider/component/provider-editor-nav.component.html b/ui/src/app/metadata/provider/component/provider-editor-nav.component.html new file mode 100644 index 000000000..3ce1eef57 --- /dev/null +++ b/ui/src/app/metadata/provider/component/provider-editor-nav.component.html @@ -0,0 +1,61 @@ + + + + +
+ +
+
diff --git a/ui/src/app/metadata/provider/component/provider-editor-nav.component.ts b/ui/src/app/metadata/provider/component/provider-editor-nav.component.ts new file mode 100644 index 000000000..fd244a18f --- /dev/null +++ b/ui/src/app/metadata/provider/component/provider-editor-nav.component.ts @@ -0,0 +1,50 @@ +import { Component, Input, Output, EventEmitter } from '@angular/core'; +import { Store } from '@ngrx/store'; + +import { Observable } from 'rxjs'; +import { skipWhile, combineLatest, map } from 'rxjs/operators'; + +import * as fromProvider from '../reducer'; +import { WizardStep, Wizard } from '../../../wizard/model'; +import * as fromWizard from '../../../wizard/reducer'; +import { MetadataProvider } from '../../domain/model'; + +export enum NAV_FORMATS { + DROPDOWN = 'NAV_DROPDOWN', + TABS = 'NAV_TABS' +} + +@Component({ + selector: 'provider-editor-nav', + templateUrl: './provider-editor-nav.component.html', + styleUrls: [] +}) + +export class ProviderEditorNavComponent { + @Input() format: string; + + @Output() onPageSelect: EventEmitter = new EventEmitter(); + + formats = NAV_FORMATS; + + currentPage$: Observable; + index$: Observable; + invalidForms$: Observable; + routes$: Observable<{ path: string, label: string }[]>; + + constructor( + private store: Store + ) { + this.index$ = this.store.select(fromWizard.getWizardIndex).pipe(skipWhile(i => !i)); + this.routes$ = this.store.select(fromWizard.getRoutes); + this.currentPage$ = this.store.select(fromWizard.getCurrent).pipe( + map(p => p ? p.id : 'filters') + ); + this.invalidForms$ = this.store.select(fromProvider.getInvalidEditorForms); + } + + gotoPage(page: string = ''): void { + this.onPageSelect.emit(page); + } +} + diff --git a/ui/src/app/metadata/provider/component/summary-property.component.html b/ui/src/app/metadata/provider/component/summary-property.component.html index 8f4781e47..664dcb82a 100644 --- a/ui/src/app/metadata/provider/component/summary-property.component.html +++ b/ui/src/app/metadata/provider/component/summary-property.component.html @@ -3,7 +3,7 @@ {{ property.name }} - {{ property.value || '-' }} + {{ property.value || property.value === false ? property.value : '-' }} diff --git a/ui/src/app/metadata/provider/component/unsaved-provider.dialog.html b/ui/src/app/metadata/provider/component/unsaved-provider.dialog.html new file mode 100644 index 000000000..f556f49cd --- /dev/null +++ b/ui/src/app/metadata/provider/component/unsaved-provider.dialog.html @@ -0,0 +1,16 @@ + +
+ + + +
+
\ No newline at end of file diff --git a/ui/src/app/metadata/provider/component/unsaved-provider.dialog.spec.ts b/ui/src/app/metadata/provider/component/unsaved-provider.dialog.spec.ts new file mode 100644 index 000000000..e9f0e4554 --- /dev/null +++ b/ui/src/app/metadata/provider/component/unsaved-provider.dialog.spec.ts @@ -0,0 +1,46 @@ +import { Component, ViewChild } from '@angular/core'; +import { TestBed, async, ComponentFixture} from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { SharedModule } from '../../../shared/shared.module'; +import { UnsavedProviderComponent } from './unsaved-provider.dialog'; +import { NgbActiveModalStub } from '../../../../testing/modal.stub'; + +@Component({ + template: ` + + ` +}) +class TestHostComponent { + @ViewChild(UnsavedProviderComponent) + public componentUnderTest: UnsavedProviderComponent; +} + +describe('Unsaved Provider Dialog Component', () => { + + let fixture: ComponentFixture; + let instance: TestHostComponent; + let cmp: UnsavedProviderComponent; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [], + declarations: [ + UnsavedProviderComponent, + TestHostComponent + ], + providers: [ + { provide: NgbActiveModal, useClass: NgbActiveModalStub } + ] + }).compileComponents(); + + fixture = TestBed.createComponent(TestHostComponent); + instance = fixture.componentInstance; + cmp = instance.componentUnderTest; + fixture.detectChanges(); + })); + + it('should instantiate the component', async(() => { + expect(cmp).toBeTruthy(); + })); +}); diff --git a/ui/src/app/metadata/provider/component/unsaved-provider.dialog.ts b/ui/src/app/metadata/provider/component/unsaved-provider.dialog.ts new file mode 100644 index 000000000..4b8d12da3 --- /dev/null +++ b/ui/src/app/metadata/provider/component/unsaved-provider.dialog.ts @@ -0,0 +1,27 @@ +import { Component, Input } from '@angular/core'; + +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { Store, Action } from '@ngrx/store'; +import { Subject } from 'rxjs/Subject'; + +import * as fromEditor from '../reducer'; + +@Component({ + selector: 'unsaved-provider', + templateUrl: './unsaved-provider.dialog.html' +}) +export class UnsavedProviderComponent { + readonly subject: Subject = new Subject(); + + constructor( + public activeModal: NgbActiveModal + ) { } + + close(): void { + this.activeModal.close(); + } + + dismiss(): void { + this.activeModal.dismiss(); + } +} diff --git a/ui/src/app/metadata/provider/container/provider-edit-step.component.html b/ui/src/app/metadata/provider/container/provider-edit-step.component.html index 5d07730fd..bab3e5b90 100644 --- a/ui/src/app/metadata/provider/container/provider-edit-step.component.html +++ b/ui/src/app/metadata/provider/container/provider-edit-step.component.html @@ -1,4 +1,11 @@ +
+ + + {{ lock.value ? 'Locked' : 'Unlocked' }} + + For Advanced Knowledge Only +
{ app.currentPage = 'common'; app.updateStatus({value: 'common'}); app.updateStatus({value: 'foo'}); - expect(store.dispatch).toHaveBeenCalledTimes(2); + expect(store.dispatch).toHaveBeenCalledTimes(3); }); }); diff --git a/ui/src/app/metadata/provider/container/provider-edit-step.component.ts b/ui/src/app/metadata/provider/container/provider-edit-step.component.ts index 63256e8a6..6ffc7e13c 100644 --- a/ui/src/app/metadata/provider/container/provider-edit-step.component.ts +++ b/ui/src/app/metadata/provider/container/provider-edit-step.component.ts @@ -3,20 +3,20 @@ import { Observable, Subject } from 'rxjs'; import { Store } from '@ngrx/store'; import * as fromProvider from '../reducer'; -import { UpdateStatus } from '../action/editor.action'; -import { Wizard } from '../../../wizard/model'; +import { UpdateStatus, LockEditor, UnlockEditor } from '../action/editor.action'; +import { Wizard, WizardStep } from '../../../wizard/model'; import { MetadataProvider } from '../../domain/model'; import * as fromWizard from '../../../wizard/reducer'; -import { withLatestFrom, map, skipWhile, distinctUntilChanged } from 'rxjs/operators'; +import { withLatestFrom, map, skipWhile, distinctUntilChanged, startWith, combineLatest } from 'rxjs/operators'; import { UpdateProvider } from '../action/entity.action'; +import { FormControl } from '@angular/forms'; @Component({ selector: 'provider-edit-step', templateUrl: './provider-edit-step.component.html', styleUrls: [] }) - export class ProviderEditStepComponent implements OnDestroy { valueChangeSubject = new Subject>(); private valueChangeEmitted$ = this.valueChangeSubject.asObservable(); @@ -33,20 +33,41 @@ export class ProviderEditStepComponent implements OnDestroy { model$: Observable; definition$: Observable>; changes$: Observable; + step$: Observable; validators$: Observable<{ [key: string]: any }>; + lock: FormControl = new FormControl(true); + constructor( private store: Store ) { - this.schema$ = this.store.select(fromProvider.getSchema); this.definition$ = this.store.select(fromWizard.getWizardDefinition); this.changes$ = this.store.select(fromProvider.getEntityChanges); this.provider$ = this.store.select(fromProvider.getSelectedProvider); + this.step$ = this.store.select(fromWizard.getCurrent); + this.schema$ = this.store.select(fromProvider.getSchema); - this.validators$ = this.store.select(fromProvider.getProviderNames).pipe( - withLatestFrom(this.definition$, this.provider$), - map(([names, def, provider]) => def.getValidators(names.filter(n => n !== provider.name))) + this.step$.subscribe(s => { + if (s && s.locked) { + this.store.dispatch(new LockEditor()); + } else { + this.store.dispatch(new UnlockEditor()); + } + }); + + this.lock.valueChanges.subscribe(locked => this.store.dispatch(locked ? new LockEditor() : new UnlockEditor())); + + this.validators$ = this.definition$.pipe( + withLatestFrom( + this.store.select(fromProvider.getProviderNames), + this.store.select(fromProvider.getProviderXmlIds), + this.provider$ + ), + map(([def, names, ids, provider]) => def.getValidators( + names.filter(n => n !== provider.name), + ids.filter(id => id !== provider.xmlId) + )) ); this.model$ = this.schema$.pipe( @@ -76,7 +97,11 @@ export class ProviderEditStepComponent implements OnDestroy { ) .subscribe(changes => this.store.dispatch(new UpdateProvider(changes))); - this.statusChangeEmitted$.pipe(distinctUntilChanged()).subscribe(errors => this.updateStatus(errors)); + this.statusChangeEmitted$ + .pipe(distinctUntilChanged()) + .subscribe(errors => { + this.updateStatus(errors); + }); this.store.select(fromWizard.getWizardIndex).subscribe(i => this.currentPage = i); } diff --git a/ui/src/app/metadata/provider/container/provider-edit.component.html b/ui/src/app/metadata/provider/container/provider-edit.component.html index 98c8b0f64..85fc2bfc9 100644 --- a/ui/src/app/metadata/provider/container/provider-edit.component.html +++ b/ui/src/app/metadata/provider/container/provider-edit.component.html @@ -10,39 +10,22 @@
- + +
  @@ -63,21 +46,10 @@
- + +
diff --git a/ui/src/app/metadata/provider/container/provider-edit.component.spec.ts b/ui/src/app/metadata/provider/container/provider-edit.component.spec.ts index 096a269c7..05cb75832 100644 --- a/ui/src/app/metadata/provider/container/provider-edit.component.spec.ts +++ b/ui/src/app/metadata/provider/container/provider-edit.component.spec.ts @@ -1,16 +1,20 @@ import { Component, ViewChild } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute, Router, ActivatedRouteSnapshot } from '@angular/router'; import { APP_BASE_HREF } from '@angular/common'; -import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { TestBed, async, ComponentFixture, fakeAsync } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { StoreModule, Store, combineReducers } from '@ngrx/store'; -import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; +import { NgbDropdownModule, NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { ProviderEditComponent } from './provider-edit.component'; import * as fromRoot from '../reducer'; import * as fromWizard from '../../../wizard/reducer'; import { SharedModule } from '../../../shared/shared.module'; import { ActivatedRouteStub } from '../../../../testing/activated-route.stub'; import { FileBackedHttpMetadataProviderEditor } from '../model'; +import { ProviderEditorNavComponent } from '../component/provider-editor-nav.component'; +import { NgbModalStub } from '../../../../testing/modal.stub'; +import { MetadataProvider } from '../../domain/model'; +import { of } from 'rxjs'; @Component({ template: ` @@ -31,6 +35,7 @@ describe('Provider Edit Component', () => { let router: Router; let activatedRoute: ActivatedRouteStub = new ActivatedRouteStub(); let child: ActivatedRouteStub = new ActivatedRouteStub(); + let modal: NgbModal; child.testParamMap = { form: 'common' }; activatedRoute.firstChild = child; @@ -55,9 +60,11 @@ describe('Provider Edit Component', () => { ], declarations: [ ProviderEditComponent, - TestHostComponent + TestHostComponent, + ProviderEditorNavComponent ], providers: [ + { provide: NgbModal, useClass: NgbModalStub }, { provide: ActivatedRoute, useValue: activatedRoute }, { provide: APP_BASE_HREF, useValue: '/' } ] @@ -65,6 +72,7 @@ describe('Provider Edit Component', () => { store = TestBed.get(Store); router = TestBed.get(Router); + modal = TestBed.get(NgbModal); spyOn(store, 'dispatch'); fixture = TestBed.createComponent(TestHostComponent); @@ -79,13 +87,7 @@ describe('Provider Edit Component', () => { describe('setIndex method', () => { it('should interrupt event default and dispatch an event', () => { - const ev = { - preventDefault: jasmine.createSpy('preventDefault'), - stopPropagation: jasmine.createSpy('stopPropagation') - }; - app.setIndex(ev, 'common'); - expect(ev.preventDefault).toHaveBeenCalled(); - expect(ev.stopPropagation).toHaveBeenCalled(); + app.setIndex('common'); expect(store.dispatch).toHaveBeenCalled(); }); }); @@ -108,8 +110,46 @@ describe('Provider Edit Component', () => { describe('cancel method', () => { it('should route to the metadata manager', () => { spyOn(router, 'navigate'); + spyOn(app, 'clear'); app.cancel(); expect(router.navigate).toHaveBeenCalled(); + expect(app.clear).toHaveBeenCalled(); + }); + }); + + describe('clear method', () => { + it('should dispatch actions to clear the reducer state', () => { + app.clear(); + expect(store.dispatch).toHaveBeenCalled(); + }); + }); + + describe('canDeactivate method', () => { + it('should check if the current route is another edit page', (done) => { + let route = new ActivatedRouteStub(), + snapshot = route.snapshot; + let result = app.canDeactivate(null, { url: 'edit', root: null }, { url: 'edit', root: null }); + result.subscribe(can => { + expect(can).toBe(true); + done(); + }); + fixture.detectChanges(); + }); + + it('should open a modal', (done) => { + app.latest = { name: 'bar' }; + spyOn(store, 'select').and.returnValue(of(false)); + spyOn(modal, 'open').and.returnValue({ result: Promise.resolve('closed') }); + fixture.detectChanges(); + let route = new ActivatedRouteStub(), + snapshot = route.snapshot; + let result = app.canDeactivate(null, { url: 'edit', root: null }, { url: 'foo', root: null }); + result.subscribe(can => { + expect(can).toBe(false); + expect(modal.open).toHaveBeenCalled(); + done(); + }); + fixture.detectChanges(); }); }); }); diff --git a/ui/src/app/metadata/provider/container/provider-edit.component.ts b/ui/src/app/metadata/provider/container/provider-edit.component.ts index 79f5cb2a6..a2c2e0b86 100644 --- a/ui/src/app/metadata/provider/container/provider-edit.component.ts +++ b/ui/src/app/metadata/provider/container/provider-edit.component.ts @@ -1,6 +1,6 @@ import { Component, OnDestroy } from '@angular/core'; -import { Router, ActivatedRoute } from '@angular/router'; -import { Observable } from 'rxjs'; +import { Router, ActivatedRoute, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; +import { Observable, of } from 'rxjs'; import { skipWhile, map, combineLatest } from 'rxjs/operators'; import { Store } from '@ngrx/store'; import * as fromWizard from '../../../wizard/reducer'; @@ -9,9 +9,14 @@ import { ClearWizard, SetDefinition, SetIndex } from '../../../wizard/action/wiz import { ClearEditor, LoadSchemaRequest } from '../action/editor.action'; import { MetadataProvider } from '../../domain/model'; import { ClearProvider } from '../action/entity.action'; -import { MetadataProviderEditorTypes } from '../model'; -import { Wizard, WizardStep } from '../../../wizard/model'; +import { Wizard } from '../../../wizard/model'; import { UpdateProviderRequest } from '../action/collection.action'; +import { NAV_FORMATS } from '../component/provider-editor-nav.component'; +import { NgbModal } from '../../../../../node_modules/@ng-bootstrap/ng-bootstrap'; +import { UnsavedDialogComponent } from '../../resolver/component/unsaved-dialog.component'; +import { UnsavedProviderComponent } from '../component/unsaved-provider.dialog'; +import { CanComponentDeactivate } from '../../../core/service/can-deactivate.guard'; +import { DifferentialService } from '../../../core/service/differential.service'; @Component({ selector: 'provider-edit', @@ -19,24 +24,28 @@ import { UpdateProviderRequest } from '../action/collection.action'; styleUrls: [] }) -export class ProviderEditComponent implements OnDestroy { +export class ProviderEditComponent implements OnDestroy, CanComponentDeactivate { provider$: Observable; definition$: Observable>; index$: Observable; - invalidForms$: Observable; - currentPage$: Observable; valid$: Observable; isInvalid$: Observable; status$: Observable; + isSaving$: Observable; latest: MetadataProvider; + provider: MetadataProvider; + + formats = NAV_FORMATS; constructor( private store: Store, private router: Router, - private route: ActivatedRoute + private route: ActivatedRoute, + private modalService: NgbModal, + private diffService: DifferentialService ) { this.provider$ = this.store.select(fromProvider.getSelectedProvider).pipe(skipWhile(d => !d)); this.definition$ = this.store.select(fromWizard.getWizardDefinition).pipe(skipWhile(d => !d)); @@ -44,9 +53,10 @@ export class ProviderEditComponent implements OnDestroy { this.valid$ = this.store.select(fromProvider.getEditorIsValid); this.isInvalid$ = this.valid$.pipe(map(v => !v)); this.status$ = this.store.select(fromProvider.getInvalidEditorForms); + this.isSaving$ = this.store.select(fromProvider.getEntityIsSaving); let startIndex$ = this.route.firstChild ? - this.route.firstChild.params.pipe(map(p => p.form || 'filter-list')) : + this.route.firstChild.params.pipe(map(p => p.form || 'filters')) : this.definition$.pipe(map(d => d.steps[0].id)); startIndex$ @@ -54,13 +64,6 @@ export class ProviderEditComponent implements OnDestroy { this.store.dispatch(new SetIndex(index)); }); - this.provider$ - .subscribe(provider => { - this.store.dispatch(new SetDefinition({ - ...MetadataProviderEditorTypes.find(def => def.type === provider['@type']) - })); - }); - this.index$.subscribe(id => this.go(id)); this.store @@ -72,25 +75,23 @@ export class ProviderEditComponent implements OnDestroy { } }); + this.provider$.subscribe(p => this.provider = p); this.store.select(fromProvider.getEntityChanges).subscribe(changes => this.latest = changes); - - this.invalidForms$ = this.store.select(fromProvider.getInvalidEditorForms); - this.currentPage$ = this.index$.pipe( - combineLatest(this.definition$, (index, definition) => (definition.steps.find(r => r.id === index))) - ); } go(id: string): void { this.router.navigate(['./', id], { relativeTo: this.route }); } - setIndex($event: Event, id: string): void { - $event.preventDefault(); - $event.stopPropagation(); + setIndex(id: string): void { this.store.dispatch(new SetIndex(id)); } ngOnDestroy() { + this.clear(); + } + + clear(): void { this.store.dispatch(new ClearProvider()); this.store.dispatch(new ClearWizard()); this.store.dispatch(new ClearEditor()); @@ -101,7 +102,31 @@ export class ProviderEditComponent implements OnDestroy { } cancel(): void { + this.clear(); this.router.navigate(['metadata', 'manager', 'providers']); } + + canDeactivate( + currentRoute: ActivatedRouteSnapshot, + currentState: RouterStateSnapshot, + nextState: RouterStateSnapshot + ): Observable { + if (nextState.url.match('edit')) { + return of(true); + } + const diff = this.diffService.updatedDiff(this.provider, this.latest); + if (diff && Object.keys(diff).length > 0) { + let modal = this.modalService.open(UnsavedProviderComponent); + modal.result.then( + () => { + this.clear(); + this.router.navigate([nextState.url]); + }, + () => console.warn('denied') + ); + return this.store.select(fromProvider.getEntityIsSaved); + } + return of(true); + } } diff --git a/ui/src/app/metadata/provider/container/provider-filter-list.component.html b/ui/src/app/metadata/provider/container/provider-filter-list.component.html index a1c8e0b1d..53e99847c 100644 --- a/ui/src/app/metadata/provider/container/provider-filter-list.component.html +++ b/ui/src/app/metadata/provider/container/provider-filter-list.component.html @@ -1,5 +1,96 @@ -
-
- Filter list. -
-
\ No newline at end of file +
+
+
+
+ + Edit Metadata Provider - {{ (provider$ | async).name }} +
+
+
+
+
+
+ + +
+ +
+
+
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
Filter NameFilter TypeEnabled?Edit
{{ i + 1 }}{{ filter.name }}{{ filter['@type'] }} + + + + + + Edit + +
+
+
+
+
diff --git a/ui/src/app/metadata/provider/container/provider-filter-list.component.scss b/ui/src/app/metadata/provider/container/provider-filter-list.component.scss new file mode 100644 index 000000000..e1a2e17b9 --- /dev/null +++ b/ui/src/app/metadata/provider/container/provider-filter-list.component.scss @@ -0,0 +1,17 @@ +:host { + .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/app/metadata/provider/container/provider-filter-list.component.spec.ts b/ui/src/app/metadata/provider/container/provider-filter-list.component.spec.ts index dd8c2ef95..f24673419 100644 --- a/ui/src/app/metadata/provider/container/provider-filter-list.component.spec.ts +++ b/ui/src/app/metadata/provider/container/provider-filter-list.component.spec.ts @@ -6,6 +6,9 @@ import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; import { ProviderFilterListComponent } from './provider-filter-list.component'; import * as fromRoot from '../reducer'; import * as fromWizard from '../../../wizard/reducer'; +import { ProviderEditorNavComponent } from '../component/provider-editor-nav.component'; +import { I18nTextComponent } from '../../../shared/component/i18n-text.component'; +import { ValidFormIconComponent } from '../../../shared/component/valid-form-icon.component'; @Component({ template: ` @@ -36,6 +39,9 @@ describe('Provider Filter List Component', () => { ], declarations: [ ProviderFilterListComponent, + ProviderEditorNavComponent, + I18nTextComponent, + ValidFormIconComponent, TestHostComponent ], providers: [] diff --git a/ui/src/app/metadata/provider/container/provider-filter-list.component.ts b/ui/src/app/metadata/provider/container/provider-filter-list.component.ts index a35bb9fc3..92c346505 100644 --- a/ui/src/app/metadata/provider/container/provider-filter-list.component.ts +++ b/ui/src/app/metadata/provider/container/provider-filter-list.component.ts @@ -1,14 +1,51 @@ -import { Component } from '@angular/core'; +import { Component, OnInit, OnDestroy } from '@angular/core'; import { Store } from '@ngrx/store'; +import { Observable, Subject } from 'rxjs'; +import { skipWhile, distinctUntilChanged, takeUntil, map } from 'rxjs/operators'; import * as fromProvider from '../reducer'; +import * as fromFilter from '../../filter/reducer'; +import { MetadataFilter, MetadataProvider } from '../../domain/model'; +import { NAV_FORMATS } from '../component/provider-editor-nav.component'; +import { SetIndex } from '../../../wizard/action/wizard.action'; +import { UpdateFilterRequest, LoadFilterRequest } from '../../filter/action/collection.action'; @Component({ selector: 'provider-filter-list', templateUrl: './provider-filter-list.component.html', - styleUrls: [] + styleUrls: ['./provider-filter-list.component.scss'] }) -export class ProviderFilterListComponent { +export class ProviderFilterListComponent implements OnDestroy { + + private ngUnsubscribe: Subject = new Subject(); + + filters$: Observable; + provider$: Observable; + isSaving$: Observable; + + formats = NAV_FORMATS; + constructor( private store: Store - ) { } + ) { + this.filters$ = this.store.select(fromFilter.getAdditionalFilters); + this.provider$ = this.store.select(fromProvider.getSelectedProvider).pipe(skipWhile(p => !p)); + this.provider$ + .pipe(takeUntil(this.ngUnsubscribe)) + .subscribe(p => { + this.store.dispatch(new LoadFilterRequest(p.resourceId)); + }); + + this.store.dispatch(new SetIndex('filters')); + + this.isSaving$ = this.store.select(fromFilter.getCollectionSaving); + } + + toggleEnabled(filter: MetadataFilter): void { + this.store.dispatch(new UpdateFilterRequest({ ...filter, filterEnabled: !filter.filterEnabled })); + } + + ngOnDestroy(): void { + this.ngUnsubscribe.next(); + this.ngUnsubscribe.complete(); + } } diff --git a/ui/src/app/metadata/provider/container/provider-select.component.ts b/ui/src/app/metadata/provider/container/provider-select.component.ts index dc0bcaa6b..a540df912 100644 --- a/ui/src/app/metadata/provider/container/provider-select.component.ts +++ b/ui/src/app/metadata/provider/container/provider-select.component.ts @@ -1,11 +1,14 @@ import { Component, OnDestroy } from '@angular/core'; -import { Subscription } from 'rxjs'; +import { Subscription, Observable } from 'rxjs'; import { Store } from '@ngrx/store'; import { ActivatedRoute } from '@angular/router'; -import { map, distinctUntilChanged } from 'rxjs/operators'; +import { map, distinctUntilChanged, skipWhile } from 'rxjs/operators'; import { SelectProviderRequest } from '../action/collection.action'; import * as fromProviders from '../reducer'; +import { MetadataProvider } from '../../domain/model'; +import { SetDefinition } from '../../../wizard/action/wizard.action'; +import { MetadataProviderEditorTypes } from '../model'; @Component({ selector: 'provider-select', @@ -16,14 +19,24 @@ import * as fromProviders from '../reducer'; export class ProviderSelectComponent implements OnDestroy { actionsSubscription: Subscription; + provider$: Observable; + constructor( - store: Store, - route: ActivatedRoute + private store: Store, + private route: ActivatedRoute ) { - this.actionsSubscription = route.params.pipe( - distinctUntilChanged(), + this.actionsSubscription = this.route.params.pipe( map(params => new SelectProviderRequest(params.providerId)) ).subscribe(store); + + this.provider$ = this.store.select(fromProviders.getSelectedProvider).pipe(skipWhile(p => !p)); + + this.provider$ + .subscribe(provider => { + this.store.dispatch(new SetDefinition({ + ...MetadataProviderEditorTypes.find(def => def.type === provider['@type']) + })); + }); } ngOnDestroy() { diff --git a/ui/src/app/metadata/provider/container/provider-wizard-step.component.ts b/ui/src/app/metadata/provider/container/provider-wizard-step.component.ts index dce4187d7..0b9a7469e 100644 --- a/ui/src/app/metadata/provider/container/provider-wizard-step.component.ts +++ b/ui/src/app/metadata/provider/container/provider-wizard-step.component.ts @@ -46,9 +46,15 @@ export class ProviderWizardStepComponent implements OnDestroy { this.definition$ = this.store.select(fromWizard.getWizardDefinition); this.changes$ = this.store.select(fromProvider.getEntityChanges); - this.validators$ = this.store.select(fromProvider.getProviderNames).pipe( - withLatestFrom(this.definition$), - map(([names, def]) => def.getValidators(names)) + this.validators$ = this.definition$.pipe( + withLatestFrom( + this.store.select(fromProvider.getProviderNames), + this.store.select(fromProvider.getProviderXmlIds), + ), + map(([def, names, ids]) => def.getValidators( + names, + ids + )) ); this.model$ = this.schema$.pipe( diff --git a/ui/src/app/metadata/provider/container/provider-wizard.component.ts b/ui/src/app/metadata/provider/container/provider-wizard.component.ts index 014061305..8a4e481f5 100644 --- a/ui/src/app/metadata/provider/container/provider-wizard.component.ts +++ b/ui/src/app/metadata/provider/container/provider-wizard.component.ts @@ -10,7 +10,6 @@ import { startWith } from 'rxjs/operators'; import { Wizard, WizardStep } from '../../../wizard/model'; import { MetadataProvider } from '../../domain/model'; import { ClearProvider } from '../action/entity.action'; -import { Router, ActivatedRoute } from '@angular/router'; import { map } from 'rxjs/operators'; import { AddProviderRequest } from '../action/collection.action'; import { MetadataProviderWizard } from '../model'; @@ -36,9 +35,7 @@ export class ProviderWizardComponent implements OnDestroy { provider: MetadataProvider; constructor( - private store: Store, - private router: Router, - private route: ActivatedRoute + private store: Store ) { this.store .select(fromWizard.getCurrentWizardSchema) @@ -89,6 +86,7 @@ export class ProviderWizardComponent implements OnDestroy { } save(): void { + this.store.dispatch(new SetDisabled(true)); this.store.dispatch(new AddProviderRequest(this.provider)); } diff --git a/ui/src/app/metadata/provider/effect/collection.effect.ts b/ui/src/app/metadata/provider/effect/collection.effect.ts index d4eb5ff0f..55528b395 100644 --- a/ui/src/app/metadata/provider/effect/collection.effect.ts +++ b/ui/src/app/metadata/provider/effect/collection.effect.ts @@ -4,7 +4,7 @@ import { Store } from '@ngrx/store'; import { Router } from '@angular/router'; import { of } from 'rxjs'; -import { map, catchError, switchMap, tap, withLatestFrom } from 'rxjs/operators'; +import { map, catchError, switchMap, tap, withLatestFrom, debounceTime } from 'rxjs/operators'; import { ProviderCollectionActionsUnion, ProviderCollectionActionTypes, @@ -19,10 +19,21 @@ import { SelectProviderError, UpdateProviderRequest, UpdateProviderSuccess, - UpdateProviderFail + UpdateProviderFail, + GetOrderProviderRequest, + GetOrderProviderSuccess, + GetOrderProviderFail, + SetOrderProviderRequest, + SetOrderProviderSuccess, + SetOrderProviderFail, + ChangeOrderUp, + ChangeOrderDown } from '../action/collection.action'; import { MetadataProviderService } from '../../domain/service/provider.service'; import * as fromProvider from '../reducer'; +import * as fromRoot from '../../../app.reducer'; +import { AddFilterSuccess, FilterCollectionActionTypes } from '../../filter/action/collection.action'; +import { debounce } from '../../../../../node_modules/rxjs-compat/operator/debounce'; /* istanbul ignore next */ @@ -46,6 +57,7 @@ export class CollectionEffects { selectProviders$ = this.actions$.pipe( ofType(ProviderCollectionActionTypes.SELECT_PROVIDER_REQUEST), map(action => action.payload), + debounceTime(500), switchMap(id => this.providerService .find(id) @@ -108,16 +120,96 @@ export class CollectionEffects { ); @Effect() - addResolverSuccessReload$ = this.actions$.pipe( + addProviderSuccessReload$ = this.actions$.pipe( ofType(ProviderCollectionActionTypes.ADD_PROVIDER_SUCCESS), map(action => action.payload), map(provider => new LoadProviderRequest()) ); + @Effect() + getOrderWithLoad$ = this.actions$.pipe( + ofType(ProviderCollectionActionTypes.LOAD_PROVIDER_SUCCESS), + map(() => new GetOrderProviderRequest()) + ); + + @Effect() + getProviderOrder$ = this.actions$.pipe( + ofType(ProviderCollectionActionTypes.GET_ORDER_PROVIDER_REQUEST), + switchMap(() => + this.providerService.getOrder().pipe( + map(order => new GetOrderProviderSuccess(order)), + catchError(err => of(new GetOrderProviderFail(err))) + ) + ) + ); + + @Effect() + reloadProviderOrderAfterChange$ = this.actions$.pipe( + ofType(ProviderCollectionActionTypes.SET_ORDER_PROVIDER_SUCCESS), + map(() => new GetOrderProviderRequest()) + ); + + @Effect() + setProviderOrder$ = this.actions$.pipe( + ofType(ProviderCollectionActionTypes.SET_ORDER_PROVIDER_REQUEST), + map(action => action.payload), + switchMap(order => + this.providerService.setOrder(order).pipe( + map(() => new SetOrderProviderSuccess()), + catchError(err => of(new SetOrderProviderFail(err))) + ) + ) + ); + + @Effect() + changeOrderUp$ = this.actions$.pipe( + ofType(ProviderCollectionActionTypes.CHANGE_PROVIDER_ORDER_UP), + map(action => action.payload), + withLatestFrom(this.store.select(fromProvider.getProviderOrder)), + map(([id, orderSet]) => { + const order = orderSet.resourceIds; + const index = order.indexOf(id); + if (index > 0) { + const newOrder = this.array_move(order, index, index - 1); + return new SetOrderProviderRequest({ resourceIds: newOrder }); + } else { + return new SetOrderProviderFail(new Error(`could not change order: ${ id }`)); + } + }) + ); + + @Effect() + changeOrderDown$ = this.actions$.pipe( + ofType(ProviderCollectionActionTypes.CHANGE_PROVIDER_ORDER_DOWN), + map(action => action.payload), + withLatestFrom(this.store.select(fromProvider.getProviderOrder)), + map(([id, orderSet]) => { + const order = orderSet.resourceIds; + const index = order.indexOf(id); + if (index < order.length - 1) { + const newOrder = this.array_move(order, index, index + 1); + return new SetOrderProviderRequest({ resourceIds: newOrder }); + } else { + return new SetOrderProviderFail(new Error(`could not change order: ${id}`)); + } + }) + ); + + array_move(arr, old_index, new_index): any[] { + if (new_index >= arr.length) { + let k = new_index - arr.length + 1; + while (k--) { + arr.push(undefined); + } + } + arr.splice(new_index, 0, arr.splice(old_index, 1)[0]); + return arr; + } + constructor( private actions$: Actions, private router: Router, - private store: Store, + private store: Store, private providerService: MetadataProviderService ) { } -} /* istanbul ignore next */ +} diff --git a/ui/src/app/metadata/provider/model/file-backed-http.provider.form.ts b/ui/src/app/metadata/provider/model/file-backed-http.provider.form.ts index 5af8642bb..17238df3b 100644 --- a/ui/src/app/metadata/provider/model/file-backed-http.provider.form.ts +++ b/ui/src/app/metadata/provider/model/file-backed-http.provider.form.ts @@ -6,6 +6,19 @@ export const FileBackedHttpMetadataProviderWizard: Wizard { + const err = xmlIdList.indexOf(value) > -1 ? { + code: 'INVALID_ID', + path: `#${property.path}`, + message: 'ID must be unique.', + params: [value] + } : null; + return err; + }; + return validators; + }, steps: [ { id: 'common', @@ -22,7 +35,7 @@ export const FileBackedHttpMetadataProviderWizard: Wizard = { ...FileBackedHttpMetadataProviderWizard, steps: [ - { - id: 'filter-list', - label: 'Filter List', - index: 0 - }, { id: 'common', label: 'Common Attributes', @@ -64,7 +72,7 @@ export const FileBackedHttpMetadataProviderEditor: Wizard { describe('undefined action', () => { it('should return the default state', () => { @@ -26,8 +19,26 @@ describe('Provider Collection Reducer', () => { it('should add the loaded providers to the collection', () => { spyOn(fromProvider.adapter, 'addAll').and.callThrough(); const providers = [ - { resourceId: 'foo', name: 'foo', '@type': 'foo', enabled: true, createdDate: new Date().toLocaleDateString() }, - { resourceId: 'bar', name: 'bar', '@type': 'bar', enabled: false, createdDate: new Date().toLocaleDateString() } + { + resourceId: 'foo', + name: 'name', + '@type': 'foo', + enabled: true, + createdDate: new Date().toLocaleDateString(), + sortKey: 1, + xmlId: 'foo', + metadataFilters: [] + }, + { + resourceId: 'bar', + name: 'bar', + '@type': 'bar', + enabled: false, + createdDate: new Date().toLocaleDateString(), + sortKey: 2, + xmlId: 'bar', + metadataFilters: [] + } ]; const action = new LoadProviderSuccess(providers); const result = reducer(snapshot, action); diff --git a/ui/src/app/metadata/provider/reducer/collection.reducer.ts b/ui/src/app/metadata/provider/reducer/collection.reducer.ts index 8cc0c6f0c..04dfb0297 100644 --- a/ui/src/app/metadata/provider/reducer/collection.reducer.ts +++ b/ui/src/app/metadata/provider/reducer/collection.reducer.ts @@ -1,24 +1,22 @@ import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity'; import { ProviderCollectionActionTypes, ProviderCollectionActionsUnion } from '../action/collection.action'; import { MetadataProvider } from '../../domain/model'; +import { ProviderOrder } from '../../domain/model/metadata-order'; export interface CollectionState extends EntityState { selectedProviderId: string | null; loaded: boolean; -} - -export function sortByDate(a: MetadataProvider, b: MetadataProvider): number { - return a.createdDate.localeCompare(b.createdDate); + order: ProviderOrder; } export const adapter: EntityAdapter = createEntityAdapter({ - sortComparer: sortByDate, selectId: (model: MetadataProvider) => model.resourceId }); export const initialState: CollectionState = adapter.getInitialState({ selectedProviderId: null, - loaded: false + loaded: false, + order: { resourceIds: [] } }); export function reducer(state = initialState, action: ProviderCollectionActionsUnion): CollectionState { @@ -26,7 +24,7 @@ export function reducer(state = initialState, action: ProviderCollectionActionsU case ProviderCollectionActionTypes.SELECT_PROVIDER_SUCCESS: { return adapter.upsertOne(action.payload, { ...state, - selectedProviderId: action.payload.id.toString() + selectedProviderId: action.payload.id as string }); } @@ -42,6 +40,13 @@ export function reducer(state = initialState, action: ProviderCollectionActionsU return adapter.updateOne(action.payload, state); } + case ProviderCollectionActionTypes.GET_ORDER_PROVIDER_SUCCESS: { + return { + ...state, + order: action.payload + }; + } + default: { return state; } @@ -56,3 +61,5 @@ export const { selectAll: selectAllProviders, selectTotal: selectProviderTotal } = adapter.getSelectors(); + +export const getProviderOrder = (state: CollectionState) => state.order; diff --git a/ui/src/app/metadata/provider/reducer/editor.reducer.spec.ts b/ui/src/app/metadata/provider/reducer/editor.reducer.spec.ts index d1b58da34..9ab661f87 100644 --- a/ui/src/app/metadata/provider/reducer/editor.reducer.spec.ts +++ b/ui/src/app/metadata/provider/reducer/editor.reducer.spec.ts @@ -1,5 +1,14 @@ import { reducer, initialState as snapshot } from './editor.reducer'; -import { EditorActionTypes, ClearEditor } from '../action/editor.action'; +import { + EditorActionTypes, + ClearEditor, + LockEditor, + LoadSchemaRequest, + LoadSchemaFail, + LoadSchemaSuccess, + UnlockEditor, + SelectProviderType +} from '../action/editor.action'; describe('Provider Editor Reducer', () => { describe('undefined action', () => { @@ -15,4 +24,40 @@ describe('Provider Editor Reducer', () => { expect(reducer(snapshot, new ClearEditor())).toEqual(snapshot); }); }); + + describe(`${EditorActionTypes.LOCK}`, () => { + it('should reset to initial state', () => { + expect(reducer(snapshot, new LockEditor())).toEqual({ ...snapshot, locked: true }); + }); + }); + + describe(`${EditorActionTypes.UNLOCK}`, () => { + it('should reset to initial state', () => { + expect(reducer(snapshot, new UnlockEditor())).toEqual({ ...snapshot, locked: false }); + }); + }); + + describe(`${EditorActionTypes.LOAD_SCHEMA_REQUEST}`, () => { + it('should reset to initial state', () => { + expect(reducer(snapshot, new LoadSchemaRequest('foo'))).toEqual({ ...snapshot, schemaPath: 'foo', loading: true }); + }); + }); + + describe(`${EditorActionTypes.LOAD_SCHEMA_FAIL}`, () => { + it('should reset to initial state', () => { + expect(reducer(snapshot, new LoadSchemaFail(new Error('fail')))).toEqual({ ...snapshot }); + }); + }); + + describe(`${EditorActionTypes.LOAD_SCHEMA_REQUEST}`, () => { + it('should reset to initial state', () => { + expect(reducer(snapshot, new LoadSchemaSuccess({}))).toEqual({ ...snapshot, schema: {} }); + }); + }); + + describe(`${EditorActionTypes.SELECT_PROVIDER_TYPE}`, () => { + it('should reset to initial state', () => { + expect(reducer(snapshot, new SelectProviderType('foo'))).toEqual({ ...snapshot, type: 'foo' }); + }); + }); }); diff --git a/ui/src/app/metadata/provider/reducer/editor.reducer.ts b/ui/src/app/metadata/provider/reducer/editor.reducer.ts index 609f707f0..32c96c86c 100644 --- a/ui/src/app/metadata/provider/reducer/editor.reducer.ts +++ b/ui/src/app/metadata/provider/reducer/editor.reducer.ts @@ -6,6 +6,7 @@ export interface EditorState { loading: boolean; schema: any; type: string; + locked: boolean; } export const initialState: EditorState = { @@ -13,7 +14,8 @@ export const initialState: EditorState = { schemaPath: null, loading: false, schema: null, - type: null + type: null, + locked: false }; export function reducer(state = initialState, action: EditorActionUnion): EditorState { @@ -59,6 +61,20 @@ export function reducer(state = initialState, action: EditorActionUnion): Editor schema: initialState.schema }; } + + case EditorActionTypes.LOCK: { + return { + ...state, + locked: true + }; + } + + case EditorActionTypes.UNLOCK: { + return { + ...state, + locked: false + }; + } default: { return state; } @@ -66,6 +82,7 @@ export function reducer(state = initialState, action: EditorActionUnion): Editor } export const getSchema = (state: EditorState) => state.schema; +export const getLocked = (state: EditorState) => state.locked; export const isEditorValid = (state: EditorState) => !Object.keys(state.status).some(key => state.status[key] === ('INVALID')); diff --git a/ui/src/app/metadata/provider/reducer/entity.reducer.spec.ts b/ui/src/app/metadata/provider/reducer/entity.reducer.spec.ts index 3079a99cd..2f1bcff62 100644 --- a/ui/src/app/metadata/provider/reducer/entity.reducer.spec.ts +++ b/ui/src/app/metadata/provider/reducer/entity.reducer.spec.ts @@ -1,5 +1,15 @@ -import { reducer, initialState as snapshot } from './entity.reducer'; +import { reducer, initialState as snapshot, isEntitySaved } from './entity.reducer'; import { EntityActionTypes, ClearProvider } from '../action/entity.action'; +import { MetadataProvider } from '../../domain/model'; +import { + ProviderCollectionActionTypes, + UpdateProviderRequest, + AddProviderRequest, + UpdateProviderSuccess, + UpdateProviderFail, + AddProviderFail, + AddProviderSuccess +} from '../action/collection.action'; describe('Provider Editor Reducer', () => { describe('undefined action', () => { @@ -15,4 +25,58 @@ describe('Provider Editor Reducer', () => { expect(reducer(snapshot, new ClearProvider())).toEqual(snapshot); }); }); + + describe(`${ProviderCollectionActionTypes.UPDATE_PROVIDER_REQUEST}`, () => { + it('should set to `saving`', () => { + expect(reducer(snapshot, new UpdateProviderRequest({})).saving).toBe(true); + }); + }); + + describe(`${ProviderCollectionActionTypes.ADD_PROVIDER_REQUEST}`, () => { + it('should set to `saving`', () => { + expect(reducer(snapshot, new AddProviderRequest({})).saving).toBe(true); + }); + }); + + describe(`${ProviderCollectionActionTypes.UPDATE_PROVIDER_SUCCESS}`, () => { + it('should set to not `saving`', () => { + expect(reducer(snapshot, new UpdateProviderSuccess({id: 'foo', changes: {} })).saving).toBe(false); + }); + }); + + describe(`${ProviderCollectionActionTypes.ADD_PROVIDER_FAIL}`, () => { + it('should set to not `saving`', () => { + expect(reducer(snapshot, new AddProviderFail({})).saving).toBe(false); + }); + }); + + describe(`${ProviderCollectionActionTypes.ADD_PROVIDER_SUCCESS}`, () => { + it('should set to not `saving`', () => { + expect(reducer(snapshot, new AddProviderSuccess({})).saving).toBe(false); + }); + }); + + describe(`${ProviderCollectionActionTypes.UPDATE_PROVIDER_FAIL}`, () => { + it('should set to not `saving`', () => { + expect(reducer(snapshot, new UpdateProviderFail({})).saving).toBe(false); + }); + }); + + describe(`isEntitySaved method`, () => { + it('should return false if there are changes', () => { + expect(isEntitySaved({ + ...snapshot, + changes: { + name: 'bar' + } + })).toBe(false); + }); + + it('should return true if there are no changes', () => { + expect(isEntitySaved({ + ...snapshot, + changes: {} + })).toBe(true); + }); + }); }); diff --git a/ui/src/app/metadata/provider/reducer/entity.reducer.ts b/ui/src/app/metadata/provider/reducer/entity.reducer.ts index d5b63eca1..faa90afd5 100644 --- a/ui/src/app/metadata/provider/reducer/entity.reducer.ts +++ b/ui/src/app/metadata/provider/reducer/entity.reducer.ts @@ -1,5 +1,6 @@ import { MetadataProvider } from '../../domain/model'; import { EntityActionTypes, EntityActionUnion } from '../action/entity.action'; +import { ProviderCollectionActionsUnion, ProviderCollectionActionTypes } from '../action/collection.action'; export interface EntityState { saving: boolean; @@ -11,8 +12,25 @@ export const initialState: EntityState = { changes: null }; -export function reducer(state = initialState, action: EntityActionUnion): EntityState { +export function reducer(state = initialState, action: EntityActionUnion | ProviderCollectionActionsUnion): EntityState { switch (action.type) { + case ProviderCollectionActionTypes.ADD_PROVIDER_REQUEST: + case ProviderCollectionActionTypes.UPDATE_PROVIDER_REQUEST: { + return { + ...state, + saving: true + }; + } + + case ProviderCollectionActionTypes.UPDATE_PROVIDER_FAIL: + case ProviderCollectionActionTypes.UPDATE_PROVIDER_SUCCESS: + case ProviderCollectionActionTypes.ADD_PROVIDER_FAIL: + case ProviderCollectionActionTypes.ADD_PROVIDER_SUCCESS: { + return { + ...state, + saving: false + }; + } case EntityActionTypes.CLEAR_PROVIDER: { return { ...initialState @@ -41,7 +59,7 @@ export function reducer(state = initialState, action: EntityActionUnion): Entity } } -export const isEntitySaved = (state: EntityState) => !Object.keys(state.changes).length && !state.saving; +export const isEntitySaved = (state: EntityState) => state.changes ? !Object.keys(state.changes).length && !state.saving : true; export const getEntityChanges = (state: EntityState) => state.changes; export const isEditorSaving = (state: EntityState) => state.saving; export const getUpdatedEntity = (state: EntityState) => state.changes; diff --git a/ui/src/app/metadata/provider/reducer/index.spec.ts b/ui/src/app/metadata/provider/reducer/index.spec.ts new file mode 100644 index 000000000..f595dea38 --- /dev/null +++ b/ui/src/app/metadata/provider/reducer/index.spec.ts @@ -0,0 +1,96 @@ +import * as fromProvider from './'; +import { MetadataProvider } from '../../domain/model'; + +describe(`provider reducer/selector functions`, () => { + + describe('getSchemaParseFn', () => { + const schema = { + properties: { + foo: { + type: 'string' + } + } + }; + const schema2 = { + properties: { + foo: { + type: 'object', + properties: { + bar: { + type: 'string' + } + } + } + } + }; + it('should lock all properties', () => { + expect(fromProvider.getSchemaParseFn(schema, true)).toEqual({ + ...schema, + properties: { + ...schema.properties, + foo: { + ...schema.properties.foo, + readOnly: true + } + } + }); + }); + + it('should unlock all properties', () => { + expect(fromProvider.getSchemaParseFn(schema, false)).toEqual({ + ...schema, + properties: { + ...schema.properties, + foo: { + type: 'string', + readOnly: false + } + } + }); + }); + + it('should lock child properties properties', () => { + expect(fromProvider.getSchemaParseFn(schema2, true)).toEqual({ + ...schema, + properties: { + ...schema2.properties, + foo: { + ...schema2.properties.foo, + readOnly: true, + properties: { + bar: { + ...schema2.properties.foo.properties.bar, + readOnly: true + } + } + } + } + }); + }); + }); + + describe('getSchemaLockedFn', () => { + it('should return true if the step is locked', () => { + expect(fromProvider.getSchemaLockedFn({ locked: true }, false)).toEqual(false); + }); + }); + + describe('mergeProviderOrderFn', () => { + const providers = [ + { resourceId: 'foo', name: 'foo', '@type': 'foo', enabled: true, xmlId: 'id', sortKey: 1, metadataFilters: [] }, + { resourceId: 'bar', name: 'bar', '@type': 'bar', enabled: false, xmlId: 'id2', sortKey: 2, metadataFilters: [] }, + { resourceId: 'baz', name: 'baz', '@type': 'baz', enabled: false, xmlId: 'id3', sortKey: 3, metadataFilters: [] } + ]; + it('1 should sort the list accordingly', () => { + let order = {resourceIds: ['bar', 'foo', 'baz']}, + ordered = fromProvider.mergeProviderOrderFn([...providers], order); + expect(ordered.indexOf(providers[0])).toBe(1); + }); + + it('2 should sort the list accordingly', () => { + let order = { resourceIds: ['foo', 'bar', 'baz'] }, + ordered = fromProvider.mergeProviderOrderFn(providers, order); + expect(ordered.indexOf(providers[0])).toBe(0); + }); + }); +}); diff --git a/ui/src/app/metadata/provider/reducer/index.ts b/ui/src/app/metadata/provider/reducer/index.ts index ee98fec25..1fefd6406 100644 --- a/ui/src/app/metadata/provider/reducer/index.ts +++ b/ui/src/app/metadata/provider/reducer/index.ts @@ -4,7 +4,12 @@ import * as fromEditor from './editor.reducer'; import * as fromEntity from './entity.reducer'; import * as fromCollection from './collection.reducer'; import * as utils from '../../domain/domain.util'; + +import * as fromWizard from '../../../wizard/reducer'; + import { MetadataProvider } from '../../domain/model'; +import { WizardStep } from '../../../wizard/model'; +import { ProviderOrder } from '../../domain/model/metadata-order'; export interface ProviderState { editor: fromEditor.EditorState; @@ -36,13 +41,41 @@ export const getCollectionState = createSelector(getProviderState, getCollection Editor State */ -export const getSchema = createSelector(getEditorState, fromEditor.getSchema); +export function getSchemaParseFn(schema, locked): any { + if (!schema) { + return null; + } + return { + ...schema, + properties: Object.keys(schema.properties).reduce((prev, current) => { + return { + ...prev, + [current]: { + ...schema.properties[current], + readOnly: locked, + ...(schema.properties[current].hasOwnProperty('properties') ? + getSchemaParseFn(schema.properties[current], locked) : + {} + ) + } + }; + }, {}) + }; +} + +export const getSchemaLockedFn = (step, locked) => step ? step.locked ? locked : false : false; +export const getLockedStatus = createSelector(getEditorState, fromEditor.getLocked); +export const getLocked = createSelector(fromWizard.getCurrent, getLockedStatus, getSchemaLockedFn); + +export const getSchemaObject = createSelector(getEditorState, fromEditor.getSchema); +export const getSchema = createSelector(getSchemaObject, getLocked, getSchemaParseFn); export const getEditorIsValid = createSelector(getEditorState, fromEditor.isEditorValid); export const getFormStatus = createSelector(getEditorState, fromEditor.getFormStatus); export const getInvalidEditorForms = createSelector(getEditorState, fromEditor.getInvalidForms); + /* Entity State */ @@ -55,6 +88,7 @@ export const getUpdatedEntity = createSelector(getEntityState, fromEntity.getUpd /* * Select pieces of Provider Collection */ +export const getProviderOrder = createSelector(getCollectionState, fromCollection.getProviderOrder); export const getAllProviders = createSelector(getCollectionState, fromCollection.selectAllProviders); export const getProviderEntities = createSelector(getCollectionState, fromCollection.selectProviderEntities); export const getSelectedProviderId = createSelector(getCollectionState, fromCollection.getSelectedProviderId); @@ -63,3 +97,18 @@ export const getProviderIds = createSelector(getCollectionState, fromCollection. export const getProviderCollectionIsLoaded = createSelector(getCollectionState, fromCollection.getIsLoaded); export const getProviderNames = createSelector(getAllProviders, (providers: MetadataProvider[]) => providers.map(p => p.name)); + +export const getProviderFilters = createSelector(getSelectedProvider, provider => provider.metadataFilters); + +export const getProviderXmlIds = createSelector(getAllProviders, (providers: MetadataProvider[]) => providers.map(p => p.xmlId)); + +export const mergeProviderOrderFn = (providers: MetadataProvider[], order: ProviderOrder): MetadataProvider[] => { + return [...providers.sort( + (a: MetadataProvider, b: MetadataProvider) => { + const aIndex = order.resourceIds.indexOf(a.resourceId); + const bIndex = order.resourceIds.indexOf(b.resourceId); + return aIndex > bIndex ? 1 : bIndex > aIndex ? -1 : 0; + } + )]; +}; +export const getOrderedProviders = createSelector(getAllProviders, getProviderOrder, mergeProviderOrderFn); diff --git a/ui/src/app/metadata/resolver/component/wizard-nav.component.html b/ui/src/app/metadata/resolver/component/wizard-nav.component.html index cba134ecf..ca6c3cccc 100644 --- a/ui/src/app/metadata/resolver/component/wizard-nav.component.html +++ b/ui/src/app/metadata/resolver/component/wizard-nav.component.html @@ -11,12 +11,18 @@ - +
diff --git a/ui/src/app/schema-form/widget/boolean-radio/boolean-radio.component.ts b/ui/src/app/schema-form/widget/boolean-radio/boolean-radio.component.ts index bd5ffa7df..d4ad09d6f 100644 --- a/ui/src/app/schema-form/widget/boolean-radio/boolean-radio.component.ts +++ b/ui/src/app/schema-form/widget/boolean-radio/boolean-radio.component.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, AfterViewInit } from '@angular/core'; import { ControlWidget } from 'ngx-schema-form'; @Component({ @@ -6,6 +6,13 @@ import { ControlWidget } from 'ngx-schema-form'; templateUrl: './boolean-radio.component.html', styleUrls: ['./boolean-radio.component.scss'] }) -export class BooleanRadioComponent extends ControlWidget { - +export class BooleanRadioComponent extends ControlWidget implements AfterViewInit { + ngAfterViewInit(): void { + super.ngAfterViewInit(); + if (this.schema.readOnly) { + this.control.disable(); + } else { + this.control.enable(); + } + } } diff --git a/ui/src/app/schema-form/widget/check/checkbox.component.html b/ui/src/app/schema-form/widget/check/checkbox.component.html index d47aa2d4c..db1385630 100644 --- a/ui/src/app/schema-form/widget/check/checkbox.component.html +++ b/ui/src/app/schema-form/widget/check/checkbox.component.html @@ -7,7 +7,7 @@ [attr.name]="name" [indeterminate]="control.value !== false && control.value !== true ? true :null" type="checkbox" - [attr.disabled]="schema.readOnly" + [attr.disabled]="schema.readOnly?true:null" id="{{ name }}" [disableValidation]="true"> {{ schema.description }} diff --git a/ui/src/app/schema-form/widget/datalist/datalist.component.ts b/ui/src/app/schema-form/widget/datalist/datalist.component.ts index ab6041500..3393bd2c5 100644 --- a/ui/src/app/schema-form/widget/datalist/datalist.component.ts +++ b/ui/src/app/schema-form/widget/datalist/datalist.component.ts @@ -1,4 +1,4 @@ -import { Component, OnChanges } from '@angular/core'; +import { Component, AfterViewInit } from '@angular/core'; import { ControlWidget } from 'ngx-schema-form'; import { SchemaService } from '../../service/schema.service'; @@ -7,13 +7,22 @@ import { SchemaService } from '../../service/schema.service'; selector: 'datalist-component', templateUrl: `./datalist.component.html` }) -export class DatalistComponent extends ControlWidget { +export class DatalistComponent extends ControlWidget implements AfterViewInit { constructor( private widgetService: SchemaService ) { super(); } + ngAfterViewInit(): void { + super.ngAfterViewInit(); + if (this.schema.readOnly) { + this.control.disable(); + } else { + this.control.enable(); + } + } + get required(): boolean { return this.widgetService.isRequired(this.formProperty); } diff --git a/ui/src/app/schema-form/widget/fieldset/fieldset.component.html b/ui/src/app/schema-form/widget/fieldset/fieldset.component.html index 26c480dfe..fc880ad04 100644 --- a/ui/src/app/schema-form/widget/fieldset/fieldset.component.html +++ b/ui/src/app/schema-form/widget/fieldset/fieldset.component.html @@ -1,9 +1,21 @@ -
- {{ fieldset.title }} -

{{ fieldset.description }}

-
-
- -
+
+
+ +
+ {{ fieldset.title }} +

{{ fieldset.description }}

+
+
+ +
+
+
+
-
\ No newline at end of file +
\ No newline at end of file diff --git a/ui/src/app/schema-form/widget/filter-target/filter-target.component.html b/ui/src/app/schema-form/widget/filter-target/filter-target.component.html new file mode 100644 index 000000000..482803777 --- /dev/null +++ b/ui/src/app/schema-form/widget/filter-target/filter-target.component.html @@ -0,0 +1,74 @@ +
+
+
+
+
+ + +
+   + +
+
+
+
+ + + + You must add at least one entity id target. + + +

+ + Required for Scripts +   + +
+ + + + Required for Regex +   + + +
+
+ +
+
+
+
+
    +
  • + {{ id }} + +
  • +
+
+
+
+
\ No newline at end of file diff --git a/ui/src/app/schema-form/widget/filter-target/filter-target.component.scss b/ui/src/app/schema-form/widget/filter-target/filter-target.component.scss new file mode 100644 index 000000000..13926cdb3 --- /dev/null +++ b/ui/src/app/schema-form/widget/filter-target/filter-target.component.scss @@ -0,0 +1,3 @@ +:host { + +} \ No newline at end of file diff --git a/ui/src/app/schema-form/widget/filter-target/filter-target.component.ts b/ui/src/app/schema-form/widget/filter-target/filter-target.component.ts new file mode 100644 index 000000000..ab810dc32 --- /dev/null +++ b/ui/src/app/schema-form/widget/filter-target/filter-target.component.ts @@ -0,0 +1,117 @@ +import { Component, OnDestroy, AfterViewInit } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; +import { ObjectWidget } from 'ngx-schema-form'; +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; +import { distinctUntilChanged, skipWhile } from 'rxjs/operators'; + +import * as fromRoot from '../../../app.reducer'; +import * as fromFilters from '../../../metadata/filter/reducer'; + +import { QueryEntityIds, ClearSearch } from '../../../metadata/filter/action/search.action'; +import { EntityValidators } from '../../../metadata/domain/service/entity-validators.service'; + +/* istanbul ignore next */ +@Component({ + selector: 'filter-target', + templateUrl: `./filter-target.component.html`, + styleUrls: ['./filter-target.component.scss'] +}) +export class FilterTargetComponent extends ObjectWidget implements OnDestroy, AfterViewInit { + + ids$: Observable; + ids: string[]; + + search: FormControl = new FormControl( + '', + [], + [EntityValidators.existsInCollection(this.store.select(fromFilters.getEntityCollection))] + ); + + script: FormControl = new FormControl( + '', + [Validators.required] + ); + + constructor( + private store: Store + ) { + super(); + this.ids$ = this.store.select(fromFilters.getEntityCollection); + this.ids$.subscribe(ids => this.ids = ids); + + this.search + .valueChanges + .pipe(distinctUntilChanged()) + .subscribe(query => this.searchEntityIds(query)); + + this.script + .valueChanges + .pipe( + distinctUntilChanged(), + skipWhile(() => this.entityAttributesFilterTargetType === 'ENTITY') + ) + .subscribe(script => { + this.setTargetValue([script]); + }); + } + + ngAfterViewInit(): void { + super.ngAfterViewInit(); + this.script.setValue(this.targets[0]); + } + + searchEntityIds(term: string): void { + if (term && term.length >= 4 && this.ids.indexOf(term) < 0) { + this.store.dispatch(new QueryEntityIds({ + term, + limit: 10 + })); + } + } + + get targets(): string[] { + return this.formProperty.getProperty('value').value; + } + + get entityAttributesFilterTargetType(): string { + return this.formProperty.getProperty('entityAttributesFilterTargetType').value; + } + + get displayType(): string { + return this.typeOptions.find(opt => opt.value === this.entityAttributesFilterTargetType).description; + } + + get typeOptions(): any[] { + return this.formProperty + .getProperty('entityAttributesFilterTargetType') + .schema + .oneOf + .map(option => ({ ...option, value: option.enum[0] })); + } + + select(value: string): void { + this.formProperty.getProperty('entityAttributesFilterTargetType').setValue(value); + this.setTargetValue([]); + this.script.reset(); + this.search.reset(); + } + + removeId(id: string): void { + let rest = this.targets.filter(target => target !== id); + this.setTargetValue(rest); + } + + setTargetValue(value: string[]): void { + this.formProperty.getProperty('value').setValue(value); + } + + onSelectValue(value: string): void { + this.setTargetValue([...this.formProperty.getProperty('value').value, value]); + this.search.reset(null); + } + + ngOnDestroy(): void { + this.store.dispatch(new ClearSearch()); + } +} diff --git a/ui/src/app/schema-form/widget/select/select.component.html b/ui/src/app/schema-form/widget/select/select.component.html index 4a60d6425..174f6798d 100644 --- a/ui/src/app/schema-form/widget/select/select.component.html +++ b/ui/src/app/schema-form/widget/select/select.component.html @@ -18,7 +18,6 @@