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 c630752f8..18a8a4953 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,23 +1,36 @@ 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.ui.service.DefaultMetadataResolversPositionOrderContainerService; +import edu.internet2.tier.shibboleth.admin.ui.service.DirectoryService; +import edu.internet2.tier.shibboleth.admin.ui.service.DirectoryServiceImpl; +import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService; +import edu.internet2.tier.shibboleth.admin.ui.service.EntityIdsSearchService; +import edu.internet2.tier.shibboleth.admin.ui.service.EntityService; +import edu.internet2.tier.shibboleth.admin.ui.service.FilterService; +import edu.internet2.tier.shibboleth.admin.ui.service.FilterTargetService; +import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityDescriptorServiceImpl; +import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityServiceImpl; +import edu.internet2.tier.shibboleth.admin.ui.service.JPAFilterServiceImpl; +import edu.internet2.tier.shibboleth.admin.ui.service.JPAFilterTargetServiceImpl; +import edu.internet2.tier.shibboleth.admin.ui.service.JPAMetadataResolverServiceImpl; +import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService; +import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolversPositionOrderContainerService; import edu.internet2.tier.shibboleth.admin.util.AttributeUtility; +import edu.internet2.tier.shibboleth.admin.util.LuceneUtility; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.document.Document; -import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexReader; import org.apache.lucene.queryparser.classic.ParseException; import org.apache.lucene.queryparser.classic.QueryParser; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.ScoreDoc; import org.apache.lucene.search.TopDocs; -import org.apache.lucene.store.Directory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -83,7 +96,7 @@ public AttributeUtility attributeUtility() { Analyzer fullTokenAnalyzer; @Autowired - Directory directory; + DirectoryService directoryService; @Autowired LocaleResolver localeResolver; @@ -91,6 +104,9 @@ public AttributeUtility attributeUtility() { @Autowired ResourceBundleMessageSource messageSource; + @Autowired + LuceneUtility luceneUtility; + @Bean public EntityDescriptorFilesScheduledTasks entityDescriptorFilesScheduledTasks(EntityDescriptorRepository entityDescriptorRepository) { return new EntityDescriptorFilesScheduledTasks(this.metadataDir, entityDescriptorRepository, openSamlObjects()); @@ -98,10 +114,11 @@ public EntityDescriptorFilesScheduledTasks entityDescriptorFilesScheduledTasks(E @Bean public EntityIdsSearchService entityIdsSearchService() { - return (term, limit) -> { + return (resourceId, term, limit) -> { List entityIds = new ArrayList<>(); try { - IndexSearcher searcher = new IndexSearcher(DirectoryReader.open(directory)); + IndexReader indexReader = luceneUtility.getIndexReader(resourceId); + IndexSearcher searcher = new IndexSearcher(indexReader); QueryParser parser = new QueryParser("content", fullTokenAnalyzer); TopDocs topDocs = searcher.search(parser.parse(term.trim()), limit); for (ScoreDoc scoreDoc : topDocs.scoreDocs) { @@ -175,4 +192,14 @@ public void addInterceptors(InterceptorRegistry registry) { return new DefaultMetadataResolversPositionOrderContainerService(positionOrderContainerRepository, resolverRepository); } + + @Bean + public DirectoryService directoryService() { + return new DirectoryServiceImpl(); + } + + @Bean + public LuceneUtility luceneUtility() { + return new LuceneUtility(); + } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverConfiguration.java index 1968841c1..cdcd54f02 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverConfiguration.java @@ -2,6 +2,7 @@ import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; +import edu.internet2.tier.shibboleth.admin.ui.service.IndexWriterService; import net.shibboleth.utilities.java.support.component.ComponentInitializationException; import net.shibboleth.utilities.java.support.resolver.ResolverException; import org.apache.http.HttpResponse; @@ -38,7 +39,7 @@ public class MetadataResolverConfiguration { OpenSamlObjects openSamlObjects; @Autowired - IndexWriter indexWriter; + IndexWriterService indexWriterService; @Autowired MetadataResolverRepository metadataResolverRepository; @@ -50,13 +51,22 @@ public MetadataResolver metadataResolver() throws ResolverException, ComponentIn List resolvers = new ArrayList<>(); + String incommonMRId = "incommonmd"; // TODO: remove this later when we allow for creation of arbitrary metadata resolvers FileBackedHTTPMetadataResolver incommonMR = new FileBackedHTTPMetadataResolver(HttpClients.createMinimal(), "http://md.incommon.org/InCommon/InCommon-metadata.xml", "/tmp/incommonmd.xml"){ @Override protected void initMetadataResolver() throws ComponentInitializationException { super.initMetadataResolver(); + IndexWriter indexWriter; + try { + indexWriter = indexWriterService.getIndexWriter(incommonMRId); + } catch (IOException e) { + throw new ComponentInitializationException(e); + } + for (String entityId: this.getBackingStore().getIndexedDescriptors().keySet()) { + Document document = new Document(); document.add(new StringField("id", entityId, Field.Store.YES)); document.add(new TextField("content", entityId, Field.Store.YES)); // TODO: change entityId to be content of entity descriptor block @@ -86,7 +96,7 @@ protected void processConditionalRetrievalHeaders(HttpResponse response) { // let's do nothing 'cause we want to allow a refresh } }; - incommonMR.setId("incommonmd"); + incommonMR.setId(incommonMRId); incommonMR.setParserPool(openSamlObjects.getParserPool()); incommonMR.setMetadataFilter(new MetadataFilterChain()); incommonMR.initialize(); diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/SearchConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/SearchConfiguration.java index ac5d1efb7..fab39d551 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/SearchConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/SearchConfiguration.java @@ -1,5 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.configuration; +import edu.internet2.tier.shibboleth.admin.ui.service.DirectoryService; +import edu.internet2.tier.shibboleth.admin.ui.service.IndexWriterService; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.LowerCaseFilter; import org.apache.lucene.analysis.StopFilter; @@ -10,18 +12,18 @@ import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.store.Directory; -import org.apache.lucene.store.RAMDirectory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; @Configuration public class SearchConfiguration { - @Bean - Directory directory() { - return new RAMDirectory(); - } + @Autowired + DirectoryService directoryService; @Bean Analyzer analyzer() { @@ -54,10 +56,23 @@ protected TokenStreamComponents createComponents(String fieldName) { }; } - @Bean - IndexWriter indexWriter() throws IOException { + private IndexWriter createIndexWriter(Directory directory) throws IOException { IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer()); indexWriterConfig.setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND); - return new IndexWriter(directory(), indexWriterConfig); + return new IndexWriter(directory, indexWriterConfig); + } + + @Bean + public IndexWriterService indexWriterService() { + Map indexWriterMap = new HashMap<>(); + + return resourceId -> { + IndexWriter indexWriter = indexWriterMap.get(resourceId); + if (indexWriter == null) { + indexWriter = createIndexWriter(directoryService.getDirectory(resourceId)); + indexWriterMap.put(resourceId, indexWriter); + } + return indexWriter; + }; } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityIdsSearchController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityIdsSearchController.java index 643e626f4..034556c92 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityIdsSearchController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityIdsSearchController.java @@ -19,9 +19,11 @@ public EntityIdsSearchController(EntityIdsSearchService entityIdsSearchService) } @GetMapping - ResponseEntity search(@RequestParam String term, @RequestParam(required = false) Integer limit) { + ResponseEntity search(@RequestParam(required = false) String resourceId, + @RequestParam String term, + @RequestParam(required = false) Integer limit) { //Zero indicates no-limit final int resultLimit = (limit != null ? limit : 10); - return ResponseEntity.ok(this.entityIdsSearchService.findBySearchTermAndOptionalLimit(term, resultLimit)); + return ResponseEntity.ok(this.entityIdsSearchService.findBySearchTermAndOptionalLimit(resourceId, term, resultLimit)); } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersPositionOrderController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersPositionOrderController.java new file mode 100644 index 000000000..4c7d70ec0 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersPositionOrderController.java @@ -0,0 +1,83 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller; + +import edu.internet2.tier.shibboleth.admin.ui.controller.support.RestControllersSupport; +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +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; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import static java.util.stream.Collectors.toList; + +/** + * @author Dmitriy Kopylenko + */ +@RestController +@RequestMapping("/api/MetadataResolvers/{metadataResolverId}/FiltersPositionOrder") +public class MetadataFiltersPositionOrderController { + + @Autowired + MetadataResolverRepository metadataResolverRepository; + + @Autowired + RestControllersSupport restControllersSupport; + + @PostMapping + @Transactional + public ResponseEntity updateFiltersPositionOrder(@PathVariable String metadataResolverId, + @RequestBody List filtersResourceIds) { + + MetadataResolver resolver = restControllersSupport.findResolverOrThrowHttp404(metadataResolverId); + List currentFilters = resolver.getMetadataFilters(); + + //Check for bad data upfront. We could avoid this check and take wrong size and/or filter ids and blindly pass to sort below. + //In that case, the sort operation will silently NOT do anything and leave original filters order, + //but we will not be able to indicate to calling clients HTTP 400 in that case. + if ((filtersResourceIds.size() != currentFilters.size()) || + (!currentFilters.stream() + .map(MetadataFilter::getResourceId) + .collect(toList()) + .containsAll(filtersResourceIds))) { + + return ResponseEntity + .badRequest() + .body("Number of filters to reorder or filters resource ids do not match current filters"); + } + + //This is needed in order to set reference to persistent filters collection to be able to merge the persistent collection + //Otherwise if we manipulate the original collection directly and try to save, we'll get RDBMS constraint violation exception + List reOrderedFilters = new ArrayList<>(currentFilters); + + //Main re-ordering operation + reOrderedFilters.sort(Comparator.comparingInt(f -> filtersResourceIds.indexOf(f.getResourceId()))); + + //re-set the reference and save to DB + resolver.setMetadataFilters(reOrderedFilters); + metadataResolverRepository.save(resolver); + + return ResponseEntity.noContent().build(); + } + + @GetMapping + @Transactional(readOnly = true) + public ResponseEntity getFiltersPositionOrder(@PathVariable String metadataResolverId) { + MetadataResolver resolver = restControllersSupport.findResolverOrThrowHttp404(metadataResolverId); + List resourceIds = resolver.getMetadataFilters().stream() + .map(MetadataFilter::getResourceId) + .collect(toList()); + + return ResponseEntity.ok(resourceIds); + } +} 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 be6ab51a4..b6e1f0538 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 @@ -4,9 +4,15 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolverValidationService; import edu.internet2.tier.shibboleth.admin.ui.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; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.StringField; +import org.apache.lucene.index.IndexWriter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -52,6 +58,9 @@ public class MetadataResolversController { @Autowired MetadataResolversPositionOrderContainerService positionOrderContainerService; + @Autowired + IndexWriterService indexWriterService; + @ExceptionHandler({InvalidTypeIdException.class, IOException.class, HttpMessageNotReadableException.class}) public ResponseEntity unableToParseJson(Exception ex) { return ResponseEntity.badRequest().body(new ErrorResponse(HttpStatus.BAD_REQUEST.toString(), ex.getMessage())); diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/support/RestControllersSupport.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/support/RestControllersSupport.java new file mode 100644 index 000000000..1605b86dd --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/support/RestControllersSupport.java @@ -0,0 +1,41 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller.support; + +import com.google.common.collect.ImmutableMap; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.client.HttpClientErrorException; + +import static org.springframework.http.HttpStatus.NOT_FOUND; + +/** + * Common functionality for REST controllers. + * + * @author Dmitriy Kopylenko + */ +@RestControllerAdvice +public class RestControllersSupport { + + @Autowired + MetadataResolverRepository resolverRepository; + + public MetadataResolver findResolverOrThrowHttp404(String resolverResourceId) { + MetadataResolver resolver = resolverRepository.findByResourceId(resolverResourceId); + if(resolver == null) { + throw new HttpClientErrorException(NOT_FOUND, "Metadata resolver is not found"); + } + return resolver; + } + + + @ExceptionHandler + public ResponseEntity notFoundHandler(HttpClientErrorException ex) { + if(ex.getStatusCode() == NOT_FOUND) { + return ResponseEntity.status(NOT_FOUND).body(ex.getStatusText()); + } + throw ex; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/DirectoryService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/DirectoryService.java new file mode 100644 index 000000000..a3cf9fe2f --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/DirectoryService.java @@ -0,0 +1,19 @@ +package edu.internet2.tier.shibboleth.admin.ui.service; + +import org.apache.lucene.store.Directory; + +import java.util.List; + +/** + * API component responsible for entity ids search. + */ +public interface DirectoryService { + /** + * Return a Directory for a given resource id. If one is not found, it will be created. + * @param resourceId the resource to get the Directory for + * @return Directory + */ + Directory getDirectory(String resourceId); + + List getDirectories(); +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/DirectoryServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/DirectoryServiceImpl.java new file mode 100644 index 000000000..553af3094 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/DirectoryServiceImpl.java @@ -0,0 +1,30 @@ +package edu.internet2.tier.shibboleth.admin.ui.service; + +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.RAMDirectory; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +public class DirectoryServiceImpl implements DirectoryService { + private Map directoryMap = new HashMap<>(); + + @Override + public Directory getDirectory(String resourceId) { + Directory directory = directoryMap.get(resourceId); + if (directory == null) { + directory = new RAMDirectory(); + directoryMap.put(resourceId, directory); + } + return directory; + } + + @Override + public List getDirectories() { + return (List) directoryMap.values(); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityIdsSearchService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityIdsSearchService.java index 81b8a4b1c..6639d5b1a 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityIdsSearchService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityIdsSearchService.java @@ -15,9 +15,10 @@ public interface EntityIdsSearchService { /** * Find a list of entity ids + * @param resourceId the id of the resource to search within * @param searchTerm for the query * @param limit optional limit of query results to return. Zero or less value indicates no limit. * @return EntityIdsSearchResultRepresentation */ - EntityIdsSearchResultRepresentation findBySearchTermAndOptionalLimit(String searchTerm, int limit); + EntityIdsSearchResultRepresentation findBySearchTermAndOptionalLimit(String resourceId, String searchTerm, int limit); } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/IndexWriterService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/IndexWriterService.java new file mode 100644 index 000000000..1e314ca4b --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/IndexWriterService.java @@ -0,0 +1,18 @@ +package edu.internet2.tier.shibboleth.admin.ui.service; + +import org.apache.lucene.index.IndexWriter; + +import java.io.IOException; + +/** + * API component responsible for entity ids search. + */ +@FunctionalInterface +public interface IndexWriterService { + /** + * Return a (possibly cached) index writer for a given resource id. + * @param resourceId the resource to create the IndexWriter for + * @return IndexWriter + */ + IndexWriter getIndexWriter(String resourceId) throws IOException; +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/LuceneUtility.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/LuceneUtility.java new file mode 100644 index 000000000..9b53ebdbb --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/LuceneUtility.java @@ -0,0 +1,45 @@ +package edu.internet2.tier.shibboleth.admin.util; + +import edu.internet2.tier.shibboleth.admin.ui.service.DirectoryService; +import org.apache.commons.lang.StringUtils; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.MultiReader; +import org.apache.lucene.store.Directory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +public class LuceneUtility { + private static final Logger logger = LoggerFactory.getLogger(LuceneUtility.class); + + @Autowired + private DirectoryService directoryService; + + public IndexReader getIndexReader(String resourceId) throws IOException { + IndexReader indexReader; + if (StringUtils.isBlank(resourceId)) { + List directories = directoryService.getDirectories(); + List indexReaderList = new ArrayList<>(); + directories.forEach(it -> { + try { + indexReaderList.add(DirectoryReader.open(it)); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + }); + IndexReader[] indexReaders = (IndexReader[]) indexReaderList.toArray(); + indexReader = new MultiReader(indexReaders, true); + } else { + indexReader = DirectoryReader.open(directoryService.getDirectory(resourceId)); + } + return indexReader; + } +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestConfiguration.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestConfiguration.groovy index 73cf58307..2cb3c7d9d 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestConfiguration.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestConfiguration.groovy @@ -2,6 +2,7 @@ package edu.internet2.tier.shibboleth.admin.ui.configuration import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository +import edu.internet2.tier.shibboleth.admin.ui.service.IndexWriterService import net.shibboleth.ext.spring.resource.ResourceHelper import net.shibboleth.utilities.java.support.component.ComponentInitializationException import org.apache.lucene.document.Document @@ -14,20 +15,22 @@ import org.opensaml.saml.metadata.resolver.MetadataResolver import org.opensaml.saml.metadata.resolver.impl.ResourceBackedMetadataResolver import org.slf4j.Logger import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.core.io.ClassPathResource @Configuration class TestConfiguration { + @Autowired + IndexWriterService indexWriterService + final OpenSamlObjects openSamlObjects - final IndexWriter indexWriter final MetadataResolverRepository metadataResolverRepository final Logger logger = LoggerFactory.getLogger(TestConfiguration.class); - TestConfiguration(final OpenSamlObjects openSamlObjects, final IndexWriter indexWriter, final MetadataResolverRepository metadataResolverRepository) { + TestConfiguration(final OpenSamlObjects openSamlObjects, final MetadataResolverRepository metadataResolverRepository) { this.openSamlObjects =openSamlObjects - this.indexWriter = indexWriter this.metadataResolverRepository = metadataResolverRepository } @@ -35,6 +38,9 @@ class TestConfiguration { MetadataResolver metadataResolver() { ChainingMetadataResolver metadataResolver = new ChainingMetadataResolver() metadataResolver.setId("chain") + String resolverId = "test" + + IndexWriter indexWriter = indexWriterService.getIndexWriter(resolverId) def shortIncommon = new ResourceBackedMetadataResolver(ResourceHelper.of(new ClassPathResource('/metadata/incommon-short.xml'))){ @Override @@ -58,7 +64,7 @@ class TestConfiguration { } } }.with { - it.id = 'test' + it.id = resolverId TestConfiguration p = owner it.parserPool = p.openSamlObjects.parserPool it.initialize() diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityIdsSearchControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityIdsSearchControllerTests.groovy index a24ed433f..77ea5ddd0 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityIdsSearchControllerTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityIdsSearchControllerTests.groovy @@ -25,11 +25,14 @@ class EntityIdsSearchControllerTests extends Specification { def mockMvc = MockMvcBuilders.standaloneSetup(controller).build() + static final String RESOURCE_ID = "resourceId" + static final String RESOURCE_ID_VALUE = "test" + static final String TERM = "term" + static final String LIMIT = "limit" + def "GET /api/EntityIds/search with unicon and limit 5"() { given: - def term = "term" def termValue = "unicon" - def limit = "limit" def limitValue = "5" def expectedEntityIdsFromSearchService = new EntityIdsSearchResultRepresentation(["http://unicon.instructure.com/saml2", "https://idp.unicon.net/idp/shibboleth"]) def expectedHttpResponseStatus = status().isOk() @@ -45,12 +48,13 @@ class EntityIdsSearchControllerTests extends Specification { when: def result = mockMvc.perform(get('/api/EntityIds/search') - .param(term, termValue) - .param(limit, limitValue)) + .param(RESOURCE_ID, RESOURCE_ID_VALUE) + .param(TERM, termValue) + .param(LIMIT, limitValue)) then: result.andExpect(expectedHttpResponseStatus) - 1 * entityIdsSearchService.findBySearchTermAndOptionalLimit(termValue, Integer.valueOf(limitValue)) >> expectedEntityIdsFromSearchService + 1 * entityIdsSearchService.findBySearchTermAndOptionalLimit(RESOURCE_ID_VALUE, termValue, Integer.valueOf(limitValue)) >> expectedEntityIdsFromSearchService result.andExpect(expectedHttpResponseStatus) .andExpect(content().contentType(expectedResponseContentType)) .andExpect(content().json(expectedResponseBody, true)) @@ -58,9 +62,7 @@ class EntityIdsSearchControllerTests extends Specification { def "GET /api/EntityIds/search with unicon and limit 1"() { given: - def term = "term" def termValue = "unicon" - def limit = "limit" def limitValue = "1" def expectedEntityIdsFromSearchService = new EntityIdsSearchResultRepresentation(["http://unicon.instructure.com/saml2"]) def expectedHttpResponseStatus = status().isOk() @@ -75,12 +77,13 @@ class EntityIdsSearchControllerTests extends Specification { when: def result = mockMvc.perform(get('/api/EntityIds/search') - .param(term, termValue) - .param(limit, limitValue)) + .param(RESOURCE_ID, RESOURCE_ID_VALUE) + .param(TERM, termValue) + .param(LIMIT, limitValue)) then: result.andExpect(expectedHttpResponseStatus) - 1 * entityIdsSearchService.findBySearchTermAndOptionalLimit(termValue, Integer.valueOf(limitValue)) >> expectedEntityIdsFromSearchService + 1 * entityIdsSearchService.findBySearchTermAndOptionalLimit(RESOURCE_ID_VALUE, termValue, Integer.valueOf(limitValue)) >> expectedEntityIdsFromSearchService result.andExpect(expectedHttpResponseStatus) .andExpect(content().contentType(expectedResponseContentType)) .andExpect(content().json(expectedResponseBody, true)) @@ -88,9 +91,7 @@ class EntityIdsSearchControllerTests extends Specification { def "GET /api/EntityIds/search with shib and no limit"() { given: - def term = "term" def termValue = "shib" - def limit = "limit" def limitValue = NO_LIMIT def expectedEntityIdsFromSearchService = new EntityIdsSearchResultRepresentation(["https://shib.ucanr.org/shibboleth", "https://shibboleth2sp.tf.semcs.net/shibboleth", @@ -123,11 +124,12 @@ class EntityIdsSearchControllerTests extends Specification { when: def result = mockMvc.perform(get('/api/EntityIds/search') - .param(term, termValue)) + .param(RESOURCE_ID, RESOURCE_ID_VALUE) + .param(TERM, termValue)) then: result.andExpect(expectedHttpResponseStatus) - 1 * entityIdsSearchService.findBySearchTermAndOptionalLimit(termValue, Integer.valueOf(limitValue)) >> expectedEntityIdsFromSearchService + 1 * entityIdsSearchService.findBySearchTermAndOptionalLimit(RESOURCE_ID_VALUE, termValue, Integer.valueOf(limitValue)) >> expectedEntityIdsFromSearchService result.andExpect(expectedHttpResponseStatus) .andExpect(content().contentType(expectedResponseContentType)) .andExpect(content().json(expectedResponseBody, true)) @@ -135,9 +137,7 @@ class EntityIdsSearchControllerTests extends Specification { def "GET /api/EntityIds/search with empty term and limit 5"() { given: - def term = "term" def termValue = "" - def limit = "limit" def limitValue = "5" def expectedEntityIdsFromSearchService = new EntityIdsSearchResultRepresentation([]) def expectedHttpResponseStatus = status().isOk() @@ -150,12 +150,13 @@ class EntityIdsSearchControllerTests extends Specification { when: def result = mockMvc.perform(get('/api/EntityIds/search') - .param(term, termValue) - .param(limit, limitValue)) + .param(RESOURCE_ID, RESOURCE_ID_VALUE) + .param(TERM, termValue) + .param(LIMIT, limitValue)) then: result.andExpect(expectedHttpResponseStatus) - 1 * entityIdsSearchService.findBySearchTermAndOptionalLimit(termValue, Integer.valueOf(limitValue)) >> expectedEntityIdsFromSearchService + 1 * entityIdsSearchService.findBySearchTermAndOptionalLimit(RESOURCE_ID_VALUE, termValue, Integer.valueOf(limitValue)) >> expectedEntityIdsFromSearchService result.andExpect(expectedHttpResponseStatus) .andExpect(content().contentType(expectedResponseContentType)) .andExpect(content().json(expectedResponseBody, true)) @@ -163,9 +164,7 @@ class EntityIdsSearchControllerTests extends Specification { def "GET /api/EntityIds/search with empty term and no limit"() { given: - def term = "term" def termValue = "" - def limit = "limit" def limitValue = NO_LIMIT def expectedEntityIdsFromSearchService = new EntityIdsSearchResultRepresentation([]) def expectedHttpResponseStatus = status().isOk() @@ -178,12 +177,13 @@ class EntityIdsSearchControllerTests extends Specification { when: def result = mockMvc.perform(get('/api/EntityIds/search') - .param(term, termValue) - .param(limit, limitValue)) + .param(RESOURCE_ID, RESOURCE_ID_VALUE) + .param(TERM, termValue) + .param(LIMIT, limitValue)) then: result.andExpect(expectedHttpResponseStatus) - 1 * entityIdsSearchService.findBySearchTermAndOptionalLimit(termValue, Integer.valueOf(limitValue)) >> expectedEntityIdsFromSearchService + 1 * entityIdsSearchService.findBySearchTermAndOptionalLimit(RESOURCE_ID_VALUE, termValue, Integer.valueOf(limitValue)) >> expectedEntityIdsFromSearchService result.andExpect(expectedHttpResponseStatus) .andExpect(content().contentType(expectedResponseContentType)) .andExpect(content().json(expectedResponseBody, true)) diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersPositionOrderControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersPositionOrderControllerIntegrationTests.groovy new file mode 100644 index 000000000..4d163a660 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersPositionOrderControllerIntegrationTests.groovy @@ -0,0 +1,151 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller + +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository +import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator +import edu.internet2.tier.shibboleth.admin.util.AttributeUtility + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.web.client.TestRestTemplate +import org.springframework.test.context.ActiveProfiles + +import spock.lang.Specification + + +/** + * @author Dmitriy Kopylenko + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("no-auth") +class MetadataFiltersPositionOrderControllerIntegrationTests extends Specification { + + @Autowired + private TestRestTemplate restTemplate + + @Autowired + MetadataResolverRepository metadataResolverRepository + + @Autowired + AttributeUtility attributeUtility + + TestObjectGenerator generator + + static BASE_URI = '/api/MetadataResolvers' + + static RESOURCE_URI = "$BASE_URI/%s/FiltersPositionOrder" + + def setup() { + generator = new TestObjectGenerator(attributeUtility) + } + + def cleanup() { + metadataResolverRepository.deleteAll() + } + + def "GET Filter Position Order for non-existent resolver"() { + when: 'GET request is made with resolver resource id NOT matching any existing filter' + def result = getFiltersPositionOrderFor('non-existent-resolver-id', String) + + then: "Request completed successfully" + result.statusCodeValue == 404 + } + + def "GET Default Filter Position Order for filters originally attached to a resolver"() { + given: 'MetadataResolver with 2 filters in data store' + def resolver = createResolverWithTwoFilters() + + when: 'GET request is made to retrieve position order of filters' + def result = getFiltersPositionOrderFor(resolver.resourceId, List) + + then: 'Original filters order is preserved' + result.statusCodeValue == 200 + result.body == [resolver.firstFilterResourceId, resolver.secondFilterResourceId] + } + + def "Reorder filters and verify the position within resolver persisted accordingly"() { + given: 'MetadataResolver with 2 filters in data store' + def resolver = createResolverWithTwoFilters() + def reOrderedFiltersPosition = [resolver.secondFilterResourceId, resolver.firstFilterResourceId] + + when: 'POST is made to re-order filters position' + def reorderPOSTResult = reorderFilters(resolver.resourceId, reOrderedFiltersPosition) + + then: 'Request completed successfully' + reorderPOSTResult.statusCodeValue == 204 + + and: 'GET request is made to retrieve position order of filters' + def positionOrderResult = getFiltersPositionOrderFor(resolver.resourceId, List) + + then: + positionOrderResult.body == reOrderedFiltersPosition + + and: "Request is made to retrieve the resolver with affected filters" + def resolverResult = getResolver(resolver.resourceId) + + then: + resolverResult.statusCodeValue == 200 + resolverResult.body.metadataFilters.collect {it.resourceId} == reOrderedFiltersPosition + } + + def "Reorder filters with bad data"() { + given: 'MetadataResolver with 2 filters in data store' + def resolver = createResolverWithTwoFilters() + def originalFiltersPosition = [resolver.firstFilterResourceId, resolver.secondFilterResourceId] + //Only one filter in order position data, while there are two filters + def reOrderedFiltersPosition = [resolver.secondFilterResourceId] + + when: 'POST is made to re-order filters position with invalid number of filters to re-order' + def reorderPOSTResult = reorderFilters(resolver.resourceId, reOrderedFiltersPosition) + + then: 'Request completed successfully with 400' + reorderPOSTResult.statusCodeValue == 400 + + and: 'GET request is made to retrieve position order of filters' + def positionOrderResult = getFiltersPositionOrderFor(resolver.resourceId, List) + + then: 'Original filters position order is retrieved' + positionOrderResult.body == originalFiltersPosition + + and: "Request is made to retrieve the resolver with original filters" + def resolverResult = getResolver(resolver.resourceId) + + then: + resolverResult.statusCodeValue == 200 + resolverResult.body.metadataFilters.collect {it.resourceId} == originalFiltersPosition + + and: 'POST is made to re-order filters position with invalid resource ids' + def reorderPOSTResult_2 = reorderFilters(resolver.resourceId, [resolver.secondFilterResourceId, 'invalid-id']) + + then: 'Request completed successfully with 400' + reorderPOSTResult_2.statusCodeValue == 400 + } + + private createResolverWithTwoFilters() { + def resolver = generator.buildRandomMetadataResolverOfType('FileBacked') + resolver.metadataFilters = [generator.signatureValidationFilter(), generator.entityRoleWhitelistFilter()] + def resolverResourceId = resolver.resourceId + def firstFilterResourceId = resolver.metadataFilters[0].resourceId + def secondFilterResourceId = resolver.metadataFilters[1].resourceId + metadataResolverRepository.save(resolver) + + [resourceId : resolverResourceId, + firstFilterResourceId : firstFilterResourceId, + secondFilterResourceId: secondFilterResourceId] + } + + private getFiltersPositionOrderFor(String resourceId, responseType) { + this.restTemplate.getForEntity(resourceUriFor(resourceId), responseType) + } + + private reorderFilters(String resourceId, List filterIdsPositionOrderList) { + this.restTemplate.postForEntity(resourceUriFor(resourceId), filterIdsPositionOrderList, null) + } + + private getResolver(String resolverResourceId) { + this.restTemplate.getForEntity("$BASE_URI/$resolverResourceId", Object) + } + + private static resourceUriFor(String resolverResourceId) { + String.format(RESOURCE_URI, resolverResourceId) + } +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/EntityIdsSearchServiceTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/EntityIdsSearchServiceTests.groovy index 7b0cc9f4b..a36e7c4ae 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/EntityIdsSearchServiceTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/EntityIdsSearchServiceTests.groovy @@ -23,6 +23,8 @@ class EntityIdsSearchServiceTests extends Specification { @Autowired EntityIdsSearchService entityIdsSearchService + def test = "test" + def "searching for carmen produces one result"() { setup: def searchTerm = "carmen" @@ -31,7 +33,7 @@ class EntityIdsSearchServiceTests extends Specification { def expectedResultItem = "https://carmenwiki.osu.edu/shibboleth" when: - def actualResults = entityIdsSearchService.findBySearchTermAndOptionalLimit(searchTerm, searchLimit) + def actualResults = entityIdsSearchService.findBySearchTermAndOptionalLimit(test, searchTerm, searchLimit) then: expectedResultSize == actualResults.entityIds.size() @@ -46,7 +48,7 @@ class EntityIdsSearchServiceTests extends Specification { def expectedResults = Arrays.asList(["http://unicon.instructure.com/saml2", "https://idp.unicon.net/idp/shibboleth"]) when: - def actualResults = entityIdsSearchService.findBySearchTermAndOptionalLimit(searchTerm, searchLimit) + def actualResults = entityIdsSearchService.findBySearchTermAndOptionalLimit(test, searchTerm, searchLimit) then: expectedResultSize == actualResults.entityIds.size() @@ -60,7 +62,7 @@ class EntityIdsSearchServiceTests extends Specification { def expectedResultSize = 0 when: - def actualResults = entityIdsSearchService.findBySearchTermAndOptionalLimit(searchTerm, searchLimit) + def actualResults = entityIdsSearchService.findBySearchTermAndOptionalLimit(test, searchTerm, searchLimit) then: expectedResultSize == actualResults.entityIds.size() @@ -74,7 +76,7 @@ class EntityIdsSearchServiceTests extends Specification { def expectedResults = Arrays.asList(["http://unicon.instructure.com/saml2"]) when: - def actualResults = entityIdsSearchService.findBySearchTermAndOptionalLimit(searchTerm, searchLimit) + def actualResults = entityIdsSearchService.findBySearchTermAndOptionalLimit(test, searchTerm, searchLimit) then: expectedResultSize == actualResults.entityIds.size() @@ -87,7 +89,7 @@ class EntityIdsSearchServiceTests extends Specification { def searchLimit = 0 when: - entityIdsSearchService.findBySearchTermAndOptionalLimit(searchTerm, searchLimit) + entityIdsSearchService.findBySearchTermAndOptionalLimit(test, searchTerm, searchLimit) then: thrown IllegalArgumentException diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/IndexWriterServiceTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/IndexWriterServiceTests.groovy new file mode 100644 index 000000000..65ec74bc1 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/IndexWriterServiceTests.groovy @@ -0,0 +1,37 @@ +package edu.internet2.tier.shibboleth.admin.ui.service + +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 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 + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +@DataJpaTest +@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration]) +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +class IndexWriterServiceTests extends Specification { + + @Autowired + IndexWriterService service + + def "retrieving index writer for the same resource id multiple times results in the same index writer being returned"() { + given: + def resourceId = "12345" + + when: + def firstIndexWriter = service.getIndexWriter(resourceId) // causes new index writer to be created and added to map + def secondIndexWriter = service.getIndexWriter(resourceId) // retrieves the same index writer from above + + then: + firstIndexWriter == secondIndexWriter + } +} diff --git a/ui/src/app/contention/reducer/contention.reducer.spec.ts b/ui/src/app/contention/reducer/contention.reducer.spec.ts index e69de29bb..a96438428 100644 --- a/ui/src/app/contention/reducer/contention.reducer.spec.ts +++ b/ui/src/app/contention/reducer/contention.reducer.spec.ts @@ -0,0 +1,59 @@ +import { reducer, initialState as snapshot, getContention } from './contention.reducer'; +import { ContentionActionTypes, ShowContentionAction, ResolveContentionAction, CancelContentionAction } from '../action/contention.action'; + +describe('Contention Reducer', () => { + + const contention = { + base: {}, + ours: {}, + theirs: {}, + + rejectionObject: {}, + resolutionObject: {}, + + ourChanges: {}, + theirChanges: {}, + + handlers: { + resolve: (value: {}) => ({}), + reject: (value: {}) => ({}) + } + }; + + const populated = { ...snapshot, contention: { ...contention } }; + + const resolution = { value: contention.ours, handlers: contention.handlers }; + + describe('undefined action', () => { + it('should return the default state', () => { + const result = reducer(snapshot, {} as any); + + expect(result).toEqual(snapshot); + }); + }); + + describe(`${ContentionActionTypes.SHOW_CONTENTION}`, () => { + it('should reset to initial state', () => { + expect(reducer(snapshot, new ShowContentionAction(contention))).toEqual(populated); + }); + }); + + describe(`${ContentionActionTypes.RESOLVE_CONTENTION}`, () => { + it('should reset to initial state', () => { + expect(reducer(snapshot, new ResolveContentionAction(resolution))).toEqual(snapshot); + }); + }); + + describe(`${ContentionActionTypes.CANCEL_CONTENTION}`, () => { + it('should reset to initial state', () => { + expect(reducer(snapshot, new CancelContentionAction(resolution))).toEqual(snapshot); + }); + }); + + describe(`getContention method`, () => { + it('should return the contention object from the state', () => { + expect(getContention(snapshot)).toBe(snapshot.contention); + expect(getContention(populated)).toEqual(contention); + }); + }); +}); diff --git a/ui/src/app/core/model/query.ts b/ui/src/app/core/model/query.ts index 5c2cb11a3..d7d85a841 100644 --- a/ui/src/app/core/model/query.ts +++ b/ui/src/app/core/model/query.ts @@ -1,5 +1,6 @@ export interface QueryParams { term: string; + resourceId?: string; limit?: number; offset?: number; } diff --git a/ui/src/app/metadata/domain/domain.util.spec.ts b/ui/src/app/metadata/domain/domain.util.spec.ts index 548379455..f5e775495 100644 --- a/ui/src/app/metadata/domain/domain.util.spec.ts +++ b/ui/src/app/metadata/domain/domain.util.spec.ts @@ -1,4 +1,5 @@ import * as util from './domain.util'; +import { MetadataProvider } from './model'; describe('Domain Utility methods', () => { @@ -33,4 +34,23 @@ describe('Domain Utility methods', () => { expect(util.getEntityIdsFn(entities)).toEqual(['foo', 'bar']); }); }); + + 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 = ['bar', 'foo', 'baz'], + ordered = util.mergeOrderFn([...providers], order); + expect(ordered.indexOf(providers[0])).toBe(1); + }); + + it('2 should sort the list accordingly', () => { + let order = ['foo', 'bar', 'baz'], + ordered = util.mergeOrderFn(providers, order); + expect(ordered.indexOf(providers[0])).toBe(0); + }); + }); }); diff --git a/ui/src/app/metadata/domain/domain.util.ts b/ui/src/app/metadata/domain/domain.util.ts index 599ce04b7..2840e9cbc 100644 --- a/ui/src/app/metadata/domain/domain.util.ts +++ b/ui/src/app/metadata/domain/domain.util.ts @@ -1,3 +1,5 @@ +import { Metadata } from './domain.type'; + /* * Utility functions */ @@ -8,3 +10,13 @@ export const getInCollectionFn = (entities, selectedId) => { return selectedId && entities[selectedId]; }; export const getEntityIdsFn = list => list.map(entity => entity.entityId); + +export const mergeOrderFn = (entities: Metadata[], order: string[]): Metadata[] => { + return [...entities.sort( + (a: Metadata, b: Metadata) => { + const aIndex = order.indexOf(a.resourceId); + const bIndex = order.indexOf(b.resourceId); + return aIndex > bIndex ? 1 : bIndex > aIndex ? -1 : 0; + } + )]; +}; diff --git a/ui/src/app/metadata/domain/model/metadata-order.ts b/ui/src/app/metadata/domain/model/metadata-order.ts deleted file mode 100644 index b365af3b0..000000000 --- a/ui/src/app/metadata/domain/model/metadata-order.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface ProviderOrder { - resourceIds: string[]; -} diff --git a/ui/src/app/metadata/domain/model/metadata-resolver.ts b/ui/src/app/metadata/domain/model/metadata-resolver.ts index 431d9d448..699ab615f 100644 --- a/ui/src/app/metadata/domain/model/metadata-resolver.ts +++ b/ui/src/app/metadata/domain/model/metadata-resolver.ts @@ -11,6 +11,7 @@ import { } from '../model'; export interface MetadataResolver extends MetadataBase { + resourceId?: string; entityId: string; serviceProviderName: string; organization?: Organization; diff --git a/ui/src/app/metadata/domain/service/filter.service.ts b/ui/src/app/metadata/domain/service/filter.service.ts index 92dabd797..bb610bf01 100644 --- a/ui/src/app/metadata/domain/service/filter.service.ts +++ b/ui/src/app/metadata/domain/service/filter.service.ts @@ -8,28 +8,38 @@ import { MetadataFilter } from '../../domain/model'; export class MetadataFilterService { readonly endpoint = '/MetadataResolvers'; + readonly order = 'FiltersPositionOrder'; readonly base = '/api'; + readonly path = 'Filters'; constructor( private http: HttpClient ) { } query(providerId: string): Observable { - return this.http.get(`${this.base}${this.endpoint}/${providerId}/Filters`); + return this.http.get(`${this.base}${this.endpoint}/${providerId}/${this.path}`); } find(providerId: string, filterId: string): Observable { - return this.http.get(`${this.base}${this.endpoint}/${providerId}/Filters/${ filterId }`); + return this.http.get(`${this.base}${this.endpoint}/${providerId}/${this.path}/${filterId}`); } update(providerId: string, filter: MetadataFilter): Observable { - return this.http.put(`${this.base}${this.endpoint}/${providerId}/Filters/${ filter.resourceId }`, filter); + return this.http.put(`${this.base}${this.endpoint}/${providerId}/${this.path}/${ filter.resourceId }`, filter); } save(providerId: string, filter: MetadataFilter): Observable { - return this.http.post(`${this.base}${this.endpoint}/${providerId}/Filters`, filter); + return this.http.post(`${this.base}${this.endpoint}/${providerId}/${this.path}`, filter); + } + + getOrder(providerId: string): Observable { + return this.http.get(`${this.base}${this.endpoint}/${providerId}/${this.order}`); + } + + setOrder(providerId: string, order: string[]): Observable { + return this.http.post(`${this.base}${this.endpoint}/${providerId}/${this.order}`, order); } remove(providerId: string, filterId: string): Observable { - return this.http.delete(`${this.base}${this.endpoint}/${providerId}/Filters/${ filterId }`); + return this.http.delete(`${this.base}${this.endpoint}/${providerId}/${this.path}/${filterId}`); } } diff --git a/ui/src/app/metadata/domain/service/provider.service.ts b/ui/src/app/metadata/domain/service/provider.service.ts index ede4bb172..dca9bb1c3 100644 --- a/ui/src/app/metadata/domain/service/provider.service.ts +++ b/ui/src/app/metadata/domain/service/provider.service.ts @@ -4,8 +4,6 @@ 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() @@ -36,11 +34,15 @@ export class MetadataProviderService { return this.http.post(`${this.base}${this.endpoint}`, provider); } - getOrder(): Observable { - return this.http.get(`${this.base}${this.order}`); + getOrder(): Observable { + return this.http.get<{ [resourceIds: string]: string[] }>(`${this.base}${this.order}`).pipe( + map( + (order: {[resourceIds: string]: string[]}) => order.resourceIds + ) + ); } - setOrder(order: ProviderOrder): Observable { - return this.http.post(`${this.base}${this.order}`, order); + setOrder(order: string[]): Observable { + return this.http.post(`${this.base}${this.order}`, { resourceIds: order }); } } diff --git a/ui/src/app/metadata/filter/action/collection.action.ts b/ui/src/app/metadata/filter/action/collection.action.ts index e2b38793d..938b38ce4 100644 --- a/ui/src/app/metadata/filter/action/collection.action.ts +++ b/ui/src/app/metadata/filter/action/collection.action.ts @@ -1,7 +1,8 @@ import { Action } from '@ngrx/store'; -import { MetadataFilter } from '../../domain/model/metadata-filter'; import { Update } from '@ngrx/entity'; +import { MetadataFilter } from '../../domain/model/metadata-filter'; + export enum FilterCollectionActionTypes { SELECT_FILTER_REQUEST = '[Metadata Filter Collection] Select Filter Request', SELECT_FILTER_SUCCESS = '[Metadata Filter Collection] Select Filter Success', @@ -21,7 +22,21 @@ export enum FilterCollectionActionTypes { 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' + REMOVE_FILTER_FAIL = '[Metadata Filter Collection] Remove Filter Fail', + + CLEAR_FILTERS = '[Metadata Filter Collection] Clear Filters', + + SET_ORDER_FILTER_REQUEST = '[Metadata Filter Collection] Set Order Filter Request', + SET_ORDER_FILTER_SUCCESS = '[Metadata Filter Collection] Set Order Filter Success', + SET_ORDER_FILTER_FAIL = '[Metadata Filter Collection] Set Order Filter Fail', + + GET_ORDER_FILTER_REQUEST = '[Metadata Filter Collection] Get Order Filter Request', + GET_ORDER_FILTER_SUCCESS = '[Metadata Filter Collection] Get Order Filter Success', + GET_ORDER_FILTER_FAIL = '[Metadata Filter Collection] Get Order Filter Fail', + + CHANGE_FILTER_ORDER_UP = '[Metadata Filter Collection] Change Order Up', + CHANGE_FILTER_ORDER_DOWN = '[Metadata Filter Collection] Change Order Down' + } export class SelectFilter implements Action { @@ -114,6 +129,58 @@ export class RemoveFilterFail implements Action { constructor(public error: Error) { } } +export class ClearFilters implements Action { + readonly type = FilterCollectionActionTypes.CLEAR_FILTERS; +} + +export class SetOrderFilterRequest implements Action { + readonly type = FilterCollectionActionTypes.SET_ORDER_FILTER_REQUEST; + + constructor(public payload: string[]) { } +} + +export class SetOrderFilterSuccess implements Action { + readonly type = FilterCollectionActionTypes.SET_ORDER_FILTER_SUCCESS; + + constructor() { } +} + +export class SetOrderFilterFail implements Action { + readonly type = FilterCollectionActionTypes.SET_ORDER_FILTER_FAIL; + + constructor(public payload: Error) { } +} + +export class GetOrderFilterRequest implements Action { + readonly type = FilterCollectionActionTypes.GET_ORDER_FILTER_REQUEST; + + constructor() { } +} + +export class GetOrderFilterSuccess implements Action { + readonly type = FilterCollectionActionTypes.GET_ORDER_FILTER_SUCCESS; + + constructor(public payload: string[]) { } +} + +export class GetOrderFilterFail implements Action { + readonly type = FilterCollectionActionTypes.GET_ORDER_FILTER_FAIL; + + constructor(public payload: Error) { } +} + +export class ChangeFilterOrderUp implements Action { + readonly type = FilterCollectionActionTypes.CHANGE_FILTER_ORDER_UP; + + constructor(public payload: string) { } +} + +export class ChangeFilterOrderDown implements Action { + readonly type = FilterCollectionActionTypes.CHANGE_FILTER_ORDER_DOWN; + + constructor(public payload: string) { } +} + export type FilterCollectionActionsUnion = | LoadFilterRequest | LoadFilterSuccess @@ -129,4 +196,13 @@ export type FilterCollectionActionsUnion = | SelectFilterFail | UpdateFilterRequest | UpdateFilterSuccess - | UpdateFilterFail; + | UpdateFilterFail + | ClearFilters + | ChangeFilterOrderDown + | ChangeFilterOrderUp + | GetOrderFilterRequest + | GetOrderFilterSuccess + | GetOrderFilterFail + | SetOrderFilterRequest + | SetOrderFilterSuccess + | SetOrderFilterFail; diff --git a/ui/src/app/metadata/filter/effect/collection.effect.ts b/ui/src/app/metadata/filter/effect/collection.effect.ts index 779bb088f..e9673464d 100644 --- a/ui/src/app/metadata/filter/effect/collection.effect.ts +++ b/ui/src/app/metadata/filter/effect/collection.effect.ts @@ -6,12 +6,36 @@ import { Router } from '@angular/router'; import { of } from 'rxjs'; import { switchMap, map, catchError, tap, combineLatest, skipWhile, debounceTime, withLatestFrom } from 'rxjs/operators'; -import * as actions from '../action/collection.action'; +import { + LoadFilterRequest, + LoadFilterSuccess, + LoadFilterError, + UpdateFilterRequest, + UpdateFilterSuccess, + UpdateFilterFail, + SelectFilter, + SelectFilterSuccess, + SelectFilterFail, + AddFilterRequest, + AddFilterSuccess, + AddFilterFail, + GetOrderFilterRequest, + GetOrderFilterSuccess, + GetOrderFilterFail, + SetOrderFilterRequest, + SetOrderFilterSuccess, + SetOrderFilterFail, + ChangeFilterOrderUp, + ChangeFilterOrderDown, + RemoveFilterRequest, + RemoveFilterSuccess, + RemoveFilterFail +} 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 { removeNulls, array_move } from '../../../shared/util'; import { EntityAttributesFilterEntity } from '../../domain/entity/filter/entity-attributes-filter'; import { MetadataFilterService } from '../../domain/service/filter.service'; import { SelectProviderRequest } from '../../provider/action/collection.action'; @@ -22,29 +46,29 @@ export class FilterCollectionEffects { @Effect() loadFilters$ = this.actions$.pipe( - ofType(FilterCollectionActionTypes.LOAD_FILTER_REQUEST), + ofType(FilterCollectionActionTypes.LOAD_FILTER_REQUEST), map(action => action.payload), skipWhile(providerId => !providerId), switchMap(providerId => this.filterService .query(providerId) .pipe( - map(filters => new actions.LoadFilterSuccess(filters)), - catchError(error => of(new actions.LoadFilterError(error))) + map(filters => new LoadFilterSuccess(filters)), + catchError(error => of(new LoadFilterError(error))) ) ) ); @Effect() selectFilterRequest$ = this.actions$.pipe( - ofType(FilterCollectionActionTypes.SELECT_FILTER_REQUEST), + ofType(FilterCollectionActionTypes.SELECT_FILTER_REQUEST), map(action => action.payload), withLatestFrom(this.store.select(fromProvider.getSelectedProviderId).pipe(skipWhile(id => !id))), switchMap(([filterId, providerId]) => { return this.filterService .find(providerId, filterId) .pipe( - map(p => new actions.SelectFilterSuccess(p)), - catchError(error => of(new actions.SelectFilterFail(error))) + map(p => new SelectFilterSuccess(p)), + catchError(error => of(new SelectFilterFail(error))) ); } ) @@ -52,7 +76,7 @@ export class FilterCollectionEffects { @Effect() addFilter$ = this.actions$.pipe( - ofType(FilterCollectionActionTypes.ADD_FILTER_REQUEST), + ofType(FilterCollectionActionTypes.ADD_FILTER_REQUEST), map(action => action.payload), map(filter => { return { @@ -65,14 +89,14 @@ export class FilterCollectionEffects { return this.filterService .save(providerId, unsaved as MetadataFilter) .pipe( - map(saved => new actions.AddFilterSuccess(saved)), - catchError(error => of(new actions.AddFilterFail(error))) + map(saved => new AddFilterSuccess(saved)), + catchError(error => of(new AddFilterFail(error))) ); }) ); @Effect({ dispatch: false }) addFilterSuccessRedirect$ = this.actions$.pipe( - ofType(FilterCollectionActionTypes.ADD_FILTER_SUCCESS), + ofType(FilterCollectionActionTypes.ADD_FILTER_SUCCESS), map(action => action.payload), withLatestFrom(this.store.select(fromProvider.getSelectedProviderId).pipe(skipWhile(id => !id))), tap(([filter, provider]) => this.router.navigate(['/', 'metadata', 'provider', provider, 'filters'])) @@ -80,7 +104,7 @@ export class FilterCollectionEffects { @Effect() updateFilter$ = this.actions$.pipe( - ofType(FilterCollectionActionTypes.UPDATE_FILTER_REQUEST), + ofType(FilterCollectionActionTypes.UPDATE_FILTER_REQUEST), map(action => action.payload), withLatestFrom(this.store.select(fromProvider.getSelectedProviderId).pipe(skipWhile(id => !id))), switchMap(([filter, providerId]) => { @@ -89,49 +113,127 @@ export class FilterCollectionEffects { return this.filterService .update(providerId, filter) .pipe( - map(p => new actions.UpdateFilterSuccess({ + map(p => new UpdateFilterSuccess({ id: p.resourceId, changes: p })), - catchError(err => of(new actions.UpdateFilterFail(filter))) + catchError(err => of(new UpdateFilterFail(filter))) ); }) ); @Effect() updateFilterSuccessReloadProvider$ = this.actions$.pipe( - ofType(FilterCollectionActionTypes.UPDATE_FILTER_SUCCESS), - map(action => action.payload), + ofType( + FilterCollectionActionTypes.UPDATE_FILTER_SUCCESS, + FilterCollectionActionTypes.SET_ORDER_FILTER_SUCCESS, + FilterCollectionActionTypes.REMOVE_FILTER_SUCCESS + ), withLatestFrom(this.store.select(fromProvider.getSelectedProviderId).pipe(skipWhile(id => !id))), - map(([filter, providerId]) => new SelectProviderRequest(providerId)) + map(([action, providerId]) => new SelectProviderRequest(providerId)) ); @Effect({ dispatch: false }) updateFilterSuccessRedirect$ = this.actions$.pipe( - ofType(FilterCollectionActionTypes.UPDATE_FILTER_SUCCESS), + ofType(FilterCollectionActionTypes.UPDATE_FILTER_SUCCESS), map(action => action.payload), withLatestFrom(this.store.select(fromProvider.getSelectedProviderId).pipe(skipWhile(id => !id))), tap(([filter, provider]) => this.router.navigate(['/', 'metadata', 'provider', provider, 'filters'])) ); + @Effect() + getOrderWithLoad$ = this.actions$.pipe( + ofType(FilterCollectionActionTypes.LOAD_FILTER_SUCCESS), + map(() => new GetOrderFilterRequest()) + ); + + @Effect() + getOrder$ = this.actions$.pipe( + ofType(FilterCollectionActionTypes.GET_ORDER_FILTER_REQUEST), + withLatestFrom(this.store.select(fromProvider.getSelectedProviderId).pipe(skipWhile(id => !id))), + switchMap(([action, providerId]) => + this.filterService.getOrder(providerId).pipe( + map(order => new GetOrderFilterSuccess(order)), + catchError(err => of(new GetOrderFilterFail(err))) + ) + ) + ); + + /* + @Effect() + reloadOrderAfterChange$ = this.actions$.pipe( + ofType(FilterCollectionActionTypes.SET_ORDER_FILTER_SUCCESS), + map(() => new GetOrderFilterRequest()) + ); + */ + + @Effect() + setOrder$ = this.actions$.pipe( + ofType(FilterCollectionActionTypes.SET_ORDER_FILTER_REQUEST), + map(action => action.payload), + withLatestFrom( + this.store.select(fromProvider.getSelectedProviderId), + this.store.select(fromFilter.getPluginFilterOrder) + ), + switchMap(([order, providerId, pluginOrder]) => + this.filterService.setOrder(providerId, [...pluginOrder, ...order]).pipe( + map(() => new SetOrderFilterSuccess()), + catchError(err => of(new SetOrderFilterFail(err))) + ) + ) + ); + + @Effect() + changeOrderUp$ = this.actions$.pipe( + ofType(FilterCollectionActionTypes.CHANGE_FILTER_ORDER_UP), + map(action => action.payload), + withLatestFrom(this.store.select(fromFilter.getAdditionalFilterOrder)), + map(([id, order]) => { + const index = order.indexOf(id); + console.log(id, order); + if (index > 0) { + const newOrder = array_move(order, index, index - 1); + return new SetOrderFilterRequest(newOrder); + } else { + return new SetOrderFilterFail(new Error(`could not change order: ${id}`)); + } + }) + ); + + @Effect() + changeOrderDown$ = this.actions$.pipe( + ofType(FilterCollectionActionTypes.CHANGE_FILTER_ORDER_DOWN), + map(action => action.payload), + withLatestFrom(this.store.select(fromFilter.getAdditionalFilterOrder)), + map(([id, order]) => { + const index = order.indexOf(id); + if (index < order.length - 1) { + const newOrder = array_move(order, index, index + 1); + return new SetOrderFilterRequest(newOrder); + } else { + return new SetOrderFilterFail(new Error(`could not change order: ${id}`)); + } + }) + ); + @Effect() removeFilterRequest$ = this.actions$.pipe( - ofType(FilterCollectionActionTypes.REMOVE_FILTER_REQUEST), + ofType(FilterCollectionActionTypes.REMOVE_FILTER_REQUEST), map(action => action.payload), withLatestFrom(this.store.select(fromProvider.getSelectedProviderId).pipe(skipWhile(id => !id))), switchMap(([filterId, providerId]) => this.filterService.remove(providerId, filterId).pipe( - map(removed => new actions.RemoveFilterSuccess(removed)), - catchError(err => of(new actions.RemoveFilterFail(err))) + map(removed => new RemoveFilterSuccess(removed)), + catchError(err => of(new RemoveFilterFail(err))) ) ) ); @Effect() removeFilterSuccess$ = this.actions$.pipe( - ofType(FilterCollectionActionTypes.REMOVE_FILTER_SUCCESS), + ofType(FilterCollectionActionTypes.REMOVE_FILTER_SUCCESS), map(action => action.payload), withLatestFrom(this.store.select(fromProvider.getSelectedProviderId).pipe(skipWhile(id => !id))), - map(([filter, providerId]) => new actions.LoadFilterRequest(providerId)) + map(([filter, providerId]) => new LoadFilterRequest(providerId)) ); constructor( diff --git a/ui/src/app/metadata/filter/effect/search.effect.ts b/ui/src/app/metadata/filter/effect/search.effect.ts index 7642728e5..53db753e8 100644 --- a/ui/src/app/metadata/filter/effect/search.effect.ts +++ b/ui/src/app/metadata/filter/effect/search.effect.ts @@ -1,9 +1,11 @@ import { Injectable } from '@angular/core'; import { Effect, Actions, ofType } from '@ngrx/effects'; +import { Store } from '@ngrx/store'; import { of } from 'rxjs'; -import { catchError, map, debounceTime, switchMap } from 'rxjs/operators'; +import { catchError, map, debounceTime, switchMap, withLatestFrom } from 'rxjs/operators'; import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { State } from '../../../app.reducer'; import { SearchActionTypes, @@ -18,6 +20,7 @@ import { SearchDialogComponent } from '../component/search-dialog.component'; import { EntityIdService } from '../../domain/service/entity-id.service'; import { fromPromise } from 'rxjs/internal-compatibility'; import { SelectId } from '../action/filter.action'; +import * as fromProvider from '../../provider/reducer'; @Injectable() @@ -29,6 +32,8 @@ export class SearchIdEffects { ofType(SearchActionTypes.QUERY_ENTITY_IDS), map(action => action.payload), debounceTime(this.dbounce), + withLatestFrom(this.store.select(fromProvider.getSelectedProviderId)), + map(([query, resourceId]) => ({ ...query, resourceId })), switchMap(query => this.idService.query(query).pipe( map(ids => new LoadEntityIdsSuccess(ids)), @@ -55,6 +60,7 @@ export class SearchIdEffects { constructor( private actions$: Actions, private modalService: NgbModal, - private idService: EntityIdService + private idService: EntityIdService, + private store: Store ) { } } 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 8ff807401..d044e66df 100644 --- a/ui/src/app/metadata/filter/reducer/collection.reducer.spec.ts +++ b/ui/src/app/metadata/filter/reducer/collection.reducer.spec.ts @@ -11,7 +11,9 @@ import { AddFilterSuccess, AddFilterFail, UpdateFilterFail, - RemoveFilterFail + RemoveFilterFail, + RemoveFilterRequest, + RemoveFilterSuccess } from '../action/collection.action'; import { EntityAttributesFilterEntity } from '../../domain/entity/filter/entity-attributes-filter'; @@ -101,6 +103,20 @@ describe('Filter Reducer', () => { }); }); + describe(`${FilterCollectionActionTypes.REMOVE_FILTER_REQUEST}`, () => { + it('should set saving to false', () => { + const action = new RemoveFilterRequest('foo'); + expect(reducer(snapshot, action).saving).toBe(true); + }); + }); + + describe(`${FilterCollectionActionTypes.REMOVE_FILTER_SUCCESS}`, () => { + it('should set saving to false', () => { + const action = new RemoveFilterSuccess('foo'); + expect(reducer(snapshot, action).saving).toBe(false); + }); + }); + describe(`${FilterCollectionActionTypes.UPDATE_FILTER_SUCCESS}`, () => { it('should update the filter in the collection', () => { spyOn(fromFilter.adapter, 'updateOne').and.callThrough(); @@ -113,4 +129,30 @@ describe('Filter Reducer', () => { expect(fromFilter.adapter.updateOne).toHaveBeenCalled(); }); }); + + describe('selector methods', () => { + describe('getSelectedFilterId', () => { + it('should return the state selectedFilterId', () => { + expect(fromFilter.getSelectedFilterId(snapshot)).toBe(snapshot.selectedFilterId); + }); + }); + + describe('getIsLoaded', () => { + it('should return the state loaded', () => { + expect(fromFilter.getIsLoaded(snapshot)).toBe(snapshot.loaded); + }); + }); + + describe('getError', () => { + it('should return the state saving', () => { + expect(fromFilter.getIsSaving(snapshot)).toBe(snapshot.saving); + }); + }); + + describe('getOrder', () => { + it('should return the state order', () => { + expect(fromFilter.getOrder(snapshot)).toBe(snapshot.order); + }); + }); + }); }); diff --git a/ui/src/app/metadata/filter/reducer/collection.reducer.ts b/ui/src/app/metadata/filter/reducer/collection.reducer.ts index 1f3788c54..2db7e232e 100644 --- a/ui/src/app/metadata/filter/reducer/collection.reducer.ts +++ b/ui/src/app/metadata/filter/reducer/collection.reducer.ts @@ -6,6 +6,7 @@ export interface CollectionState extends EntityState { selectedFilterId: string | null; loaded: boolean; saving: boolean; + order: string[]; } export function sortByDate(a: MetadataFilter, b: MetadataFilter): number { @@ -20,7 +21,8 @@ export const adapter: EntityAdapter = createEntityAdapter state.selectedFilterId; export const getIsLoaded = (state: CollectionState) => state.loaded; export const getIsSaving = (state: CollectionState) => state.saving; +export const getOrder = (state: CollectionState) => state.order; 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 ff832e198..bd7e71514 100644 --- a/ui/src/app/metadata/filter/reducer/filter.reducer.spec.ts +++ b/ui/src/app/metadata/filter/reducer/filter.reducer.spec.ts @@ -87,4 +87,24 @@ describe('Filter Reducer', () => { expect(result).toEqual(fromFilter.initialState); }); }); + + describe('selector methods', () => { + describe('getSelected', () => { + it('should return the state selected', () => { + expect(fromFilter.getSelected(snapshot)).toBe(snapshot.selected); + }); + }); + + describe('getChanges', () => { + it('should return the state changes', () => { + expect(fromFilter.getFilterChanges(snapshot)).toBe(snapshot.changes); + }); + }); + + describe('getPreview', () => { + it('should return the state preview', () => { + expect(fromFilter.getPreview(snapshot)).toBe(snapshot.preview); + }); + }); + }); }); diff --git a/ui/src/app/metadata/filter/reducer/index.spec.ts b/ui/src/app/metadata/filter/reducer/index.spec.ts index 3d30cf891..0a136de68 100644 --- a/ui/src/app/metadata/filter/reducer/index.spec.ts +++ b/ui/src/app/metadata/filter/reducer/index.spec.ts @@ -7,6 +7,40 @@ describe('filter selectors', () => { }); }); + describe('isAdditionalFilter', () => { + 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); + }); + }); + + describe('isFilterPlugin', () => { + it('should return false for entity attributes type', () => { + expect(selectors.isFilterPlugin('EntityAttributes')).toBe(false); + }); + selectors.filterPluginTypes.forEach(type => { + it(`should return false for ${ type } type`, () => { + expect(selectors.isFilterPlugin(type)).toBe(true); + }); + }); + }); + + describe('isAdditionalFilter', () => { + it('should return false for entity attributes type', () => { + expect(selectors.isAdditionalFilter('EntityAttributes')).toBe(true); + }); + selectors.filterPluginTypes.forEach(type => { + it(`should return false for ${ type } type`, () => { + expect(selectors.isAdditionalFilter(type)).toBe(false); + }); + }); + }); + describe('filterTypeFn', () => { it('should return filtered objects', () => { const filters = [{ '@type': 'EntityAttributes' }]; @@ -18,4 +52,56 @@ describe('filter selectors', () => { expect(selectors.filterTypeFn(filters).length).toBe(1); }); }); + + describe('filterOrderFn', () => { + it('should return the ordered & filtered entities when they are not filter plugins', () => { + const filters = {'foo': { '@type': 'EntityAttributes' }}; + const order = ['foo']; + expect(selectors.filterOrderFn(filters, order)).toEqual(order); + }); + + it('should return entities that exist in the collection of additional filters', () => { + const filters = { 'foo': { '@type': 'EntityAttributes' } }; + const order = ['bar']; + expect(selectors.filterOrderFn(filters, order)).toEqual([]); + }); + + it('should return only additional filters', () => { + const filters = { 'foo': { '@type': 'EntityRoleWhiteList' } }; + const order = ['foo']; + expect(selectors.filterOrderFn(filters, order)).toEqual([]); + }); + + it('should return filtered objects', () => { + const filters = { 'foo': { '@type': 'EntityAttributes' }, 'bar': { '@type': 'EntityRoleWhiteList' }}; + const order = ['foo', 'bar']; + expect(selectors.filterOrderFn(filters, order).length).toBe(1); + }); + }); + + describe('pluginOrderFn', () => { + it('should return the ordered & filtered entities when they are filter plugins', () => { + const filters = { 'foo': { '@type': 'EntityRoleWhiteList' } }; + const order = ['foo']; + expect(selectors.pluginOrderFn(filters, order)).toEqual(order); + }); + + it('should return entities that exist in the collection of filter plugins', () => { + const filters = { 'foo': { '@type': 'EntityAttributes' } }; + const order = ['bar']; + expect(selectors.pluginOrderFn(filters, order)).toEqual([]); + }); + + it('should return only filter plugins', () => { + const filters = { 'foo': { '@type': 'EntityAttributes' } }; + const order = ['foo']; + expect(selectors.pluginOrderFn(filters, order)).toEqual([]); + }); + + it('should return filtered objects', () => { + const filters = { 'foo': { '@type': 'EntityAttributes' }, 'bar': { '@type': 'EntityRoleWhiteList' } }; + const order = ['foo', 'bar']; + expect(selectors.pluginOrderFn(filters, order).length).toBe(1); + }); + }); }); diff --git a/ui/src/app/metadata/filter/reducer/index.ts b/ui/src/app/metadata/filter/reducer/index.ts index 60cbe4b34..caacf0254 100644 --- a/ui/src/app/metadata/filter/reducer/index.ts +++ b/ui/src/app/metadata/filter/reducer/index.ts @@ -50,11 +50,7 @@ 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 getCollectionOrder = createSelector(getCollectionState, fromCollection.getOrder); export const getFilterEntities = createSelector(getCollectionState, fromCollection.selectFilterEntities); export const getSelectedFilterId = createSelector(getCollectionState, fromCollection.getSelectedFilterId); @@ -62,6 +58,19 @@ export const getSelectedFilter = createSelector(getFilterEntities, getSelectedFi export const getFilterIds = createSelector(getCollectionState, fromCollection.selectFilterIds); export const getFilterCollectionIsLoaded = createSelector(getCollectionState, fromCollection.getIsLoaded); +export const filterPluginTypes = ['RequiredValidUntil', 'SignatureValidation', 'EntityRoleWhiteList']; +export const isAdditionalFilter = (type) => filterPluginTypes.indexOf(type) === -1; +export const isFilterPlugin = (type) => filterPluginTypes.indexOf(type) >= 0; + +export const filterTypeFn = filters => [...filters.filter(f => isAdditionalFilter(f['@type']))]; +export const filterOrderFn = (filters, order) => order.filter(id => filters.hasOwnProperty(id) && isAdditionalFilter(filters[id]['@type'])); +export const pluginOrderFn = (filters, order) => order.filter(id => filters.hasOwnProperty(id) && isFilterPlugin(filters[id]['@type'])); + +export const getFilterList = createSelector(getAllFilters, filterTypeFn); +export const getAdditionalFilterOrder = createSelector(getFilterEntities, getCollectionOrder, filterOrderFn); +export const getAdditionalFilters = createSelector(getFilterList, getAdditionalFilterOrder, utils.mergeOrderFn); +export const getPluginFilterOrder = createSelector(getFilterEntities, getCollectionOrder, pluginOrderFn); + /* * Combine pieces of State */ diff --git a/ui/src/app/metadata/filter/reducer/search.reducer.spec.ts b/ui/src/app/metadata/filter/reducer/search.reducer.spec.ts index 7c77afc30..f778451da 100644 --- a/ui/src/app/metadata/filter/reducer/search.reducer.spec.ts +++ b/ui/src/app/metadata/filter/reducer/search.reducer.spec.ts @@ -1,4 +1,4 @@ -import { reducer } from './search.reducer'; +import { reducer, initialState as snapshot } from './search.reducer'; import * as fromFilter from './search.reducer'; import { SearchActionTypes, @@ -6,16 +6,11 @@ import { CancelViewMore, QueryEntityIds, LoadEntityIdsError, - LoadEntityIdsSuccess + LoadEntityIdsSuccess, + ClearSearch } from '../action/search.action'; - -const snapshot: fromFilter.SearchState = { - entityIds: [], - viewMore: false, - loading: false, - error: null, - term: '', -}; +import { FilterCollectionActionTypes, UpdateFilterSuccess, AddFilterSuccess } from '../action/collection.action'; +import { EntityAttributesFilterEntity } from '../../domain/entity'; describe('Filter Reducer', () => { describe('undefined action', () => { @@ -69,4 +64,61 @@ describe('Filter Reducer', () => { expect(result.error).toBe(err); }); }); + + describe(`${FilterCollectionActionTypes.UPDATE_FILTER_SUCCESS} action`, () => { + it('should reset the state', () => { + const update = { + id: 'foo', + changes: new EntityAttributesFilterEntity({ resourceId: 'foo', name: 'bar', createdDate: new Date().toLocaleDateString() }), + }; + const action = new UpdateFilterSuccess(update); + const result = reducer(snapshot, action); + + expect(result).toEqual(snapshot); + }); + }); + + describe(`${FilterCollectionActionTypes.ADD_FILTER_SUCCESS} action`, () => { + it('should reset the state', () => { + const filter = new EntityAttributesFilterEntity( + { resourceId: 'foo', name: 'bar', createdDate: new Date().toLocaleDateString() } + ); + const action = new AddFilterSuccess(filter); + const result = reducer(snapshot, action); + + expect(result).toEqual(snapshot); + }); + }); + + describe('selector methods', () => { + describe('getViewMore', () => { + it('should return the state viewMore', () => { + expect(fromFilter.getViewMore(snapshot)).toBe(snapshot.viewMore); + }); + }); + + describe('getEntityIds', () => { + it('should return the state entityIds', () => { + expect(fromFilter.getEntityIds(snapshot)).toBe(snapshot.entityIds); + }); + }); + + describe('getError', () => { + it('should return the state error', () => { + expect(fromFilter.getError(snapshot)).toBe(snapshot.error); + }); + }); + + describe('getLoading', () => { + it('should return the state loading', () => { + expect(fromFilter.getLoading(snapshot)).toBe(snapshot.loading); + }); + }); + + describe('getTerm', () => { + it('should return the state term', () => { + expect(fromFilter.getTerm(snapshot)).toBe(snapshot.term); + }); + }); + }); }); 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 88ec0b19e..35ece7de0 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 @@ -8,7 +8,7 @@ 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'; +import { ChangeProviderOrderUp, ChangeProviderOrderDown } from '../../provider/action/collection.action'; @Component({ selector: 'dashboard-providers-list', @@ -26,7 +26,7 @@ export class DashboardProvidersListComponent implements OnInit { ) { } ngOnInit(): void { - this.providers$ = this.store.select(getOrderedProviders); + this.providers$ = this.store.select(getOrderedProviders) as Observable; this.providersOpen$ = this.store.select(getOpenProviders); } @@ -47,10 +47,10 @@ export class DashboardProvidersListComponent implements OnInit { } updateOrderUp(provider: MetadataProvider): void { - this.store.dispatch(new ChangeOrderUp(provider.resourceId)); + this.store.dispatch(new ChangeProviderOrderUp(provider.resourceId)); } updateOrderDown(provider: MetadataProvider): void { - this.store.dispatch(new ChangeOrderDown(provider.resourceId)); + this.store.dispatch(new ChangeProviderOrderDown(provider.resourceId)); } } diff --git a/ui/src/app/metadata/provider/action/collection.action.ts b/ui/src/app/metadata/provider/action/collection.action.ts index 637c58115..3eb1f1889 100644 --- a/ui/src/app/metadata/provider/action/collection.action.ts +++ b/ui/src/app/metadata/provider/action/collection.action.ts @@ -1,7 +1,6 @@ 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', @@ -25,12 +24,12 @@ export enum ProviderCollectionActionTypes { 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', + SET_ORDER_PROVIDER_SUCCESS = '[Metadata Provider Collection] Set Order Provider Success', + SET_ORDER_PROVIDER_FAIL = '[Metadata Provider Collection] Set Order 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', + GET_ORDER_PROVIDER_REQUEST = '[Metadata Provider Collection] Get Order Provider Request', + GET_ORDER_PROVIDER_SUCCESS = '[Metadata Provider Collection] Get Order Provider Success', + GET_ORDER_PROVIDER_FAIL = '[Metadata Provider Collection] Get Order Provider Fail', CHANGE_PROVIDER_ORDER_UP = '[Metadata Provider Collection] Change Order Up', CHANGE_PROVIDER_ORDER_DOWN = '[Metadata Provider Collection] Change Order Down', @@ -129,7 +128,7 @@ export class RemoveProviderFail implements Action { export class SetOrderProviderRequest implements Action { readonly type = ProviderCollectionActionTypes.SET_ORDER_PROVIDER_REQUEST; - constructor(public payload: ProviderOrder) { } + constructor(public payload: string[]) { } } export class SetOrderProviderSuccess implements Action { @@ -153,7 +152,7 @@ export class GetOrderProviderRequest implements Action { export class GetOrderProviderSuccess implements Action { readonly type = ProviderCollectionActionTypes.GET_ORDER_PROVIDER_SUCCESS; - constructor(public payload: ProviderOrder) { } + constructor(public payload: string[]) { } } export class GetOrderProviderFail implements Action { @@ -162,13 +161,13 @@ export class GetOrderProviderFail implements Action { constructor(public payload: Error) { } } -export class ChangeOrderUp implements Action { +export class ChangeProviderOrderUp implements Action { readonly type = ProviderCollectionActionTypes.CHANGE_PROVIDER_ORDER_UP; constructor(public payload: string) { } } -export class ChangeOrderDown implements Action { +export class ChangeProviderOrderDown implements Action { readonly type = ProviderCollectionActionTypes.CHANGE_PROVIDER_ORDER_DOWN; constructor(public payload: string) { } @@ -196,5 +195,5 @@ export type ProviderCollectionActionsUnion = | GetOrderProviderRequest | GetOrderProviderSuccess | GetOrderProviderFail - | ChangeOrderUp - | ChangeOrderDown; + | ChangeProviderOrderUp + | ChangeProviderOrderDown; 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 3f29393c0..2e13ae19f 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 @@ -34,7 +34,7 @@ - + @@ -44,18 +44,17 @@ - - 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 0b1fee37b..73b59eb8a 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,13 +1,21 @@ -import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Component, OnDestroy } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable, Subject } from 'rxjs'; -import { skipWhile, distinctUntilChanged, takeUntil, map } from 'rxjs/operators'; +import { skipWhile, takeUntil, withLatestFrom } 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, RemoveFilterRequest } from '../../filter/action/collection.action'; + +import { + UpdateFilterRequest, + LoadFilterRequest, + ChangeFilterOrderUp, + ChangeFilterOrderDown, + RemoveFilterRequest, + ClearFilters +} from '../../filter/action/collection.action'; @Component({ selector: 'provider-filter-list', @@ -27,7 +35,7 @@ export class ProviderFilterListComponent implements OnDestroy { constructor( private store: Store ) { - this.filters$ = this.store.select(fromFilter.getAdditionalFilters); + this.filters$ = this.store.select(fromFilter.getAdditionalFilters) as Observable; this.provider$ = this.store.select(fromProvider.getSelectedProvider).pipe(skipWhile(p => !p)); this.provider$ .pipe( @@ -46,6 +54,14 @@ export class ProviderFilterListComponent implements OnDestroy { this.store.dispatch(new UpdateFilterRequest({ ...filter, filterEnabled: !filter.filterEnabled })); } + updateOrderUp(filter: MetadataFilter): void { + this.store.dispatch(new ChangeFilterOrderUp(filter.resourceId)); + } + + updateOrderDown(filter: MetadataFilter): void { + this.store.dispatch(new ChangeFilterOrderDown(filter.resourceId)); + } + remove(id: string): void { this.store.dispatch(new RemoveFilterRequest(id)); } @@ -53,5 +69,7 @@ export class ProviderFilterListComponent implements OnDestroy { ngOnDestroy(): void { this.ngUnsubscribe.next(); this.ngUnsubscribe.complete(); + + this.store.dispatch(new ClearFilters()); } } diff --git a/ui/src/app/metadata/provider/effect/collection.effect.ts b/ui/src/app/metadata/provider/effect/collection.effect.ts index 73ea1937b..eb6b0af4b 100644 --- a/ui/src/app/metadata/provider/effect/collection.effect.ts +++ b/ui/src/app/metadata/provider/effect/collection.effect.ts @@ -25,12 +25,13 @@ import { SetOrderProviderRequest, SetOrderProviderSuccess, SetOrderProviderFail, - ChangeOrderUp, - ChangeOrderDown + ChangeProviderOrderUp, + ChangeProviderOrderDown } from '../action/collection.action'; import { MetadataProviderService } from '../../domain/service/provider.service'; import * as fromProvider from '../reducer'; import * as fromRoot from '../../../app.reducer'; +import { array_move } from '../../../shared/util'; import { ClearProvider, ResetChanges } from '../action/entity.action'; import { ShowContentionAction } from '../../../contention/action/contention.action'; import { ContentionService } from '../../../contention/service/contention.service'; @@ -183,15 +184,14 @@ export class CollectionEffects { @Effect() changeOrderUp$ = this.actions$.pipe( - ofType(ProviderCollectionActionTypes.CHANGE_PROVIDER_ORDER_UP), + ofType(ProviderCollectionActionTypes.CHANGE_PROVIDER_ORDER_UP), map(action => action.payload), withLatestFrom(this.store.select(fromProvider.getProviderOrder)), - map(([id, orderSet]) => { - const order = orderSet.resourceIds; + map(([id, order]) => { const index = order.indexOf(id); if (index > 0) { - const newOrder = this.array_move(order, index, index - 1); - return new SetOrderProviderRequest({ resourceIds: newOrder }); + const newOrder = array_move(order, index, index - 1); + return new SetOrderProviderRequest(newOrder); } else { return new SetOrderProviderFail(new Error(`could not change order: ${ id }`)); } @@ -200,32 +200,20 @@ export class CollectionEffects { @Effect() changeOrderDown$ = this.actions$.pipe( - ofType(ProviderCollectionActionTypes.CHANGE_PROVIDER_ORDER_DOWN), + ofType(ProviderCollectionActionTypes.CHANGE_PROVIDER_ORDER_DOWN), map(action => action.payload), withLatestFrom(this.store.select(fromProvider.getProviderOrder)), - map(([id, orderSet]) => { - const order = orderSet.resourceIds; + map(([id, order]) => { const index = order.indexOf(id); if (index < order.length - 1) { - const newOrder = this.array_move(order, index, index + 1); - return new SetOrderProviderRequest({ resourceIds: newOrder }); + const newOrder = array_move(order, index, index + 1); + return new SetOrderProviderRequest(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, diff --git a/ui/src/app/metadata/provider/reducer/collection.reducer.ts b/ui/src/app/metadata/provider/reducer/collection.reducer.ts index 04dfb0297..8a2959851 100644 --- a/ui/src/app/metadata/provider/reducer/collection.reducer.ts +++ b/ui/src/app/metadata/provider/reducer/collection.reducer.ts @@ -1,12 +1,11 @@ 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; - order: ProviderOrder; + order: string[]; } export const adapter: EntityAdapter = createEntityAdapter({ @@ -16,7 +15,7 @@ export const adapter: EntityAdapter = createEntityAdapter { @@ -74,23 +75,4 @@ describe(`provider reducer/selector functions`, () => { 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 1fefd6406..f9def08d4 100644 --- a/ui/src/app/metadata/provider/reducer/index.ts +++ b/ui/src/app/metadata/provider/reducer/index.ts @@ -8,8 +8,6 @@ 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; @@ -101,14 +99,4 @@ export const getProviderNames = createSelector(getAllProviders, (providers: Meta 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); +export const getOrderedProviders = createSelector(getAllProviders, getProviderOrder, utils.mergeOrderFn); diff --git a/ui/src/app/shared/util.ts b/ui/src/app/shared/util.ts index 2e506bffa..4224f5b8a 100644 --- a/ui/src/app/shared/util.ts +++ b/ui/src/app/shared/util.ts @@ -50,3 +50,14 @@ export function pick(approvedProperties: string[]): Function { return newObj; }, {}); } + +export function 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; +}
Filter Name Filter Type
{{ i + 1 }} {{ filter.name }} {{ filter['@type'] }}