diff --git a/.gitignore b/.gitignore index 0648667bdb50f03dbf8151eff52f71c863b4bcf7..ef6f322552824d51d4938c0b1be7ea2d68e4e69f 100644 --- a/.gitignore +++ b/.gitignore @@ -95,43 +95,11 @@ local.properties *.ipr # User-specific stuff -.idea/* -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf - -# Generated files -.idea/**/contentModel.xml - -# Sensitive or high-churn files -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml - -# Gradle -.idea/**/gradle.xml -.idea/**/libraries - -# Gradle and Maven with auto-import -# When using Gradle or Maven with auto-import, you should exclude module files, -# since they will be recreated, and may cause churn. Uncomment if using -# auto-import. -# .idea/modules.xml -# .idea/*.iml -# .idea/modules +.idea # CMake cmake-build-*/ -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - # File-based project format *.iws @@ -144,21 +112,12 @@ out/ # JIRA plugin atlassian-ide-plugin.xml -# Cursive Clojure plugin -.idea/replstate.xml - # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties -# Editor-based Rest Client -.idea/httpRequests - -# Android studio 3.1+ serialized cache file -.idea/caches/build_file_checksums.ser - ### Intellij Patch ### # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 @@ -167,9 +126,6 @@ fabric.properties # .idea/misc.xml # *.ipr -# Sonarlint plugin -.idea/sonarlint - ### Kotlin ### # Compiled class file *.class @@ -270,3 +226,4 @@ gradle-app.setting # End of https://www.gitignore.io/api/gradle,eclipse,intellij,visualstudiocode,kotlin,git,macos,linux frontend/package-lock.json +/web/node_modules/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b5511df7904d649e1cf5dfc50a6bd2c50ad4e3bc..7df3480415c742159af12bd1c3e36c778000c472 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -30,8 +30,6 @@ cache: key: "$CI_COMMIT_REF_NAME" paths: - ".gradle" - - "frontend/.gradle/" - - "frontend/node_modules/" # PRE-BUILD @@ -62,20 +60,6 @@ build-loader-docker-image: # TESTS -lint: - stage: test - tags: - - openstack - script: "./gradlew lint" - cache: - key: "$CI_COMMIT_REF_NAME" - policy: pull - paths: - - ".gradle" - - "frontend/.gradle/" - - "frontend/node_modules/" - - test-and-sonarqube: stage: test tags: @@ -104,20 +88,19 @@ test-and-sonarqube: policy: pull-push paths: - ".gradle" - - "frontend/.gradle/" - - "frontend/node_modules/" - .sonar/cache script: - - ./gradlew :frontend:assemble --parallel - - ./gradlew :backend:test jacocoTestReport --parallel - - find /tmp/node/*/bin -name node -exec ln -s {} /tmp/node/node \; - - export PATH="/tmp/node/:$PATH" - - ./gradlew -s sonarqube -x test + - ./gradlew test jacocoTestReport --parallel + # disable sonarqube because it apparently needs node, but I don't know why, and it can't find it anymore now that + # there is no frontend project anymore, and it takes sooooo much time to complete anyway for results that nobody + # will ever look + # - find /tmp/node/*/bin -name node -exec ln -s {} /tmp/node/node \; + # - export PATH="/tmp/node/:$PATH" + # - ./gradlew -s sonarqube -x test artifacts: reports: junit: - ./backend/build/test-results/test/TEST-*.xml - # - ./frontend/karma-junit-tests-report/TEST*.xml only: refs: - merge_requests @@ -144,8 +127,6 @@ build: policy: pull paths: - ".gradle" - - "frontend/.gradle/" - - "frontend/node_modules/" artifacts: paths: - "$JAR_PATH" diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index ca99a00edbda7ccc394869431fec04f9a5ac33a6..0000000000000000000000000000000000000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,15 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<project version="4"> - <component name="CompilerConfiguration"> - <annotationProcessing> - <profile name="Gradle Imported" enabled="true"> - <outputRelativeToContentRoot value="true" /> - <processorPath useClasspath="false"> - <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-configuration-processor/2.1.2.RELEASE/db9671c321defb942a6700fae8a7700a137a25e/spring-boot-configuration-processor-2.1.2.RELEASE.jar" /> - </processorPath> - <module name="faidare.backend.main" /> - </profile> - </annotationProcessing> - <bytecodeTargetLevel target="1.8" /> - </component> -</project> \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml deleted file mode 100644 index 15a15b218a29e09c9190992732698d646e4d659a..0000000000000000000000000000000000000000 --- a/.idea/encodings.xml +++ /dev/null @@ -1,4 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<project version="4"> - <component name="Encoding" addBOMForNewFiles="with NO BOM" /> -</project> \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index ff8249e49968d1849269b0d8a2fe49a31c72a63f..0000000000000000000000000000000000000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,9 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<project version="4"> - <component name="FrameworkDetectionExcludesConfiguration"> - <file type="web" url="file://$PROJECT_DIR$" /> - </component> - <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK"> - <output url="file://$PROJECT_DIR$/out" /> - </component> -</project> \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 3b2a562bc3214c46382cc1761196d85196a94d66..0000000000000000000000000000000000000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,14 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<project version="4"> - <component name="ProjectModuleManager"> - <modules> - <module fileurl="file://$PROJECT_DIR$/.idea/modules/faidare.iml" filepath="$PROJECT_DIR$/.idea/modules/faidare.iml" /> - <module fileurl="file://$PROJECT_DIR$/.idea/modules/backend/faidare.backend.iml" filepath="$PROJECT_DIR$/.idea/modules/backend/faidare.backend.iml" /> - <module fileurl="file://$PROJECT_DIR$/.idea/modules/backend/faidare.backend.main.iml" filepath="$PROJECT_DIR$/.idea/modules/backend/faidare.backend.main.iml" /> - <module fileurl="file://$PROJECT_DIR$/.idea/modules/backend/faidare.backend.test.iml" filepath="$PROJECT_DIR$/.idea/modules/backend/faidare.backend.test.iml" /> - <module fileurl="file://$PROJECT_DIR$/.idea/modules/frontend/faidare.frontend.iml" filepath="$PROJECT_DIR$/.idea/modules/frontend/faidare.frontend.iml" /> - <module fileurl="file://$PROJECT_DIR$/backend/src/main/main.iml" filepath="$PROJECT_DIR$/backend/src/main/main.iml" /> - <module fileurl="file://$PROJECT_DIR$/backend/src/test/test.iml" filepath="$PROJECT_DIR$/backend/src/test/test.iml" /> - </modules> - </component> -</project> \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7f4cb416c083d265558da75d457237d671..0000000000000000000000000000000000000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<project version="4"> - <component name="VcsDirectoryMappings"> - <mapping directory="$PROJECT_DIR$" vcs="Git" /> - </component> -</project> \ No newline at end of file diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts index 9ff140544f08a54151ddf9ca56e164de76e37a35..64acb331b41fea7957dcb7ea7c0d7713e561dd43 100644 --- a/backend/build.gradle.kts +++ b/backend/build.gradle.kts @@ -1,11 +1,8 @@ import org.gradle.api.tasks.testing.logging.TestExceptionFormat import org.springframework.boot.gradle.tasks.buildinfo.BuildInfo -import org.springframework.boot.gradle.tasks.bundling.BootJar -import org.springframework.boot.gradle.tasks.run.BootRun buildscript { repositories { - mavenLocal() mavenCentral() } } @@ -20,7 +17,6 @@ plugins { id("org.owasp.dependencycheck") version "6.0.3" } - java { sourceCompatibility = JavaVersion.VERSION_1_8 } @@ -40,7 +36,7 @@ tasks { options.compilerArgs.add("-parameters") } - getByName<Copy>("processResources") { + processResources { inputs.property("app", "gnpis") filesMatching("bootstrap.yml") {} @@ -51,25 +47,44 @@ tasks { // but it's better to do that than using the bootInfo() method of the springBoot closure, because that // makes the test task out of date, which makes the build much longer. // See https://github.com/spring-projects/spring-boot/issues/13152 - val buildInfo by creating(BuildInfo::class) { + val buildInfo by registering(BuildInfo::class) { destinationDir = file("$buildDir/buildInfo") } - val bootJar by getting(BootJar::class) { + bootJar { archiveName = "${rootProject.name}.jar" - dependsOn(":frontend:assemble") dependsOn(buildInfo) - - into("BOOT-INF/classes/static") { - from("${project(":frontend").projectDir}/dist/frontend") + dependsOn(":web:assemble") + + // replace the script.js and style.css file names referenced in main.html + // by their actual name, containing the content hash + filesMatching("**/layout/main.html") { + val webAssetsDir = project(":web").file("build/dist/assets/"); + val scriptFileName = webAssetsDir.list().first { it.startsWith("script") && it.endsWith(".js") } + val styleFileName = webAssetsDir.list().first { it.startsWith("style") && it.endsWith(".css") } + + filter { line -> + if (line.contains("script.js")) { + line.replace("script.js", scriptFileName) + } + else if (line.contains("style.css")) { + line.replace("style.css", styleFileName) + } else { + line + } + } } + into("BOOT-INF/classes/META-INF") { - from(buildInfo.destinationDir) + from(buildInfo.map { it.destinationDir }) + } + into("BOOT-INF/classes/static") { + from(project(":web").file("build/dist")) } launchScript() } - val test by getting(Test::class) { + test { useJUnitPlatform() testLogging { exceptionFormat = TestExceptionFormat.FULL @@ -77,7 +92,7 @@ tasks { outputs.dir(snippetsDir) } - val jacocoTestReport by getting(JacocoReport::class) { + jacocoTestReport { reports { xml.setEnabled(true) html.setEnabled(true) @@ -98,6 +113,7 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-actuator") implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.cloud:spring-cloud-starter-config") + implementation("org.springframework.boot:spring-boot-starter-thymeleaf") // Elasticsearch implementation("org.elasticsearch:elasticsearch:6.6.2") diff --git a/backend/src/main/java/fr/inra/urgi/faidare/domain/xref/XRefDocumentSearchCriteria.java b/backend/src/main/java/fr/inra/urgi/faidare/domain/xref/XRefDocumentSearchCriteria.java index 7ca7b26afe0f212fa6c997315c92214409f74b88..6dde515e2d0fa5fedf1d2b36d7d2b30de43dd5f5 100644 --- a/backend/src/main/java/fr/inra/urgi/faidare/domain/xref/XRefDocumentSearchCriteria.java +++ b/backend/src/main/java/fr/inra/urgi/faidare/domain/xref/XRefDocumentSearchCriteria.java @@ -4,6 +4,7 @@ import fr.inra.urgi.faidare.domain.criteria.base.PaginationCriteriaImpl; import fr.inra.urgi.faidare.elasticsearch.criteria.annotation.CriteriaForDocument; import fr.inra.urgi.faidare.elasticsearch.criteria.annotation.DocumentPath; +import java.util.Collections; import java.util.List; /** @@ -18,6 +19,12 @@ public class XRefDocumentSearchCriteria extends PaginationCriteriaImpl { @DocumentPath("linkedRessourcesID") private List<String> linkedRessourcesID; + public static XRefDocumentSearchCriteria forXRefId(String resourceId) { + XRefDocumentSearchCriteria criteria = new XRefDocumentSearchCriteria(); + criteria.setLinkedRessourcesID(Collections.singletonList(resourceId)); + return criteria; + } + public String getEntryType() { return entryType; } diff --git a/backend/src/main/java/fr/inra/urgi/faidare/filter/AngularRouteFilter.java b/backend/src/main/java/fr/inra/urgi/faidare/filter/AngularRouteFilter.java deleted file mode 100644 index 81978e5f3bfaca602e70eff0f109477d55e1466a..0000000000000000000000000000000000000000 --- a/backend/src/main/java/fr/inra/urgi/faidare/filter/AngularRouteFilter.java +++ /dev/null @@ -1,115 +0,0 @@ -package fr.inra.urgi.faidare.filter; - -import com.google.common.base.Charsets; -import com.google.common.io.ByteSource; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.io.ResourceLoader; -import org.springframework.stereotype.Component; - -import javax.servlet.*; -import javax.servlet.annotation.WebFilter; -import javax.servlet.http.HttpServletRequest; -import java.io.IOException; -import java.io.InputStream; -import java.util.Arrays; - -/** - * Filter that intercepts all request to potential Angular routes - * (ex: /studies/ID) to send back the Angular `index.html` file with a correct - * base href set to the spring server context path. - * - * Potential angular routes are devised by process of elimination: - * - They should be GET requests - * - They should not end with common static file suffixes {@link AngularRouteFilter#STATIC_SUFFIXES} - * - They should not start with API prefixes {@link AngularRouteFilter#API_PREFIXES} - * - * <p> - * Adapted from data-discovery - * - * @author gcornut - */ -@Component -@WebFilter("/*") -public class AngularRouteFilter implements Filter { - - private static final String[] API_PREFIXES = { - "/brapi/v1", "/faidare/v1", "/actuator", "/v2/api-docs", "/swagger-resources" - }; - - private static final String[] STATIC_SUFFIXES = { - ".html", ".js", ".css", ".ico", ".png", ".jpg", ".gif", ".eot", ".svg", - ".woff2", ".ttf", ".woff", ".md" - }; - - @Value("${server.servlet.context-path}") - private String serverContextPath; - - private final ResourceLoader resourceLoader; - - @Autowired - public AngularRouteFilter(ResourceLoader resourceLoader) { - this.resourceLoader = resourceLoader; - } - - @Override - public void doFilter( - ServletRequest req, - ServletResponse response, - FilterChain chain - ) throws IOException, ServletException { - HttpServletRequest request = (HttpServletRequest) req; - - if (isAngularRoute(request)) { - // Angular route - InputStream inputStream = resourceLoader.getResource("classpath:static/index.html").getInputStream(); - - ByteSource byteSource = new ByteSource() { - @Override - public InputStream openStream() { - return inputStream; - } - }; - - String content = byteSource.asCharSource(Charsets.UTF_8).read(); - String replacedContent = content.replace( - "<base href=\"./\">", - "<base href=\"" + serverContextPath + "/\">" - ); - response.getWriter().write(replacedContent); - return; - } - - // Otherwise nothing to do - chain.doFilter(request, response); - } - - private boolean isAngularRoute(HttpServletRequest request) { - if (!request.getMethod().equals("GET")) { - return false; - } - - String fullUri = request.getRequestURI(); - String contextPath = request.getContextPath(); - String uri = fullUri.substring(contextPath.length()); - - return !isApiOrStaticResource(uri); - } - - private boolean isApiOrStaticResource(String relativePath) { - // Starts with API prefix - return Arrays.stream(API_PREFIXES).anyMatch(relativePath::startsWith) - // or has static file suffix - || Arrays.stream(STATIC_SUFFIXES).anyMatch(relativePath::endsWith); - } - - @Override - public void init(FilterConfig filterConfig) { - // nothing to do - } - - @Override - public void destroy() { - // nothing to do - } -} diff --git a/backend/src/main/java/fr/inra/urgi/faidare/utils/Sites.java b/backend/src/main/java/fr/inra/urgi/faidare/utils/Sites.java new file mode 100644 index 0000000000000000000000000000000000000000..67fde44b8117296f13defc599adc6dcd0cd970a3 --- /dev/null +++ b/backend/src/main/java/fr/inra/urgi/faidare/utils/Sites.java @@ -0,0 +1,14 @@ +package fr.inra.urgi.faidare.utils; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +/** + * Utilities for sites + * @author JB Nizet + */ +public class Sites { + public static String siteIdToLocationId(String siteId) { + return Base64.getUrlEncoder().encodeToString(("urn:URGI/location/" + siteId).getBytes(StandardCharsets.US_ASCII)); + } +} diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/HomeController.java b/backend/src/main/java/fr/inra/urgi/faidare/web/HomeController.java new file mode 100644 index 0000000000000000000000000000000000000000..6734b6aa97a2ae445af4eb61be86a07883b2a58a --- /dev/null +++ b/backend/src/main/java/fr/inra/urgi/faidare/web/HomeController.java @@ -0,0 +1,19 @@ +package fr.inra.urgi.faidare.web; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.ModelAndView; + +/** + * Controller for the home page, which doesn't display much + * @author JB Nizet + */ +@Controller +@RequestMapping("") +public class HomeController { + @GetMapping + public ModelAndView home() { + return new ModelAndView("index"); + } +} diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmController.java b/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmController.java new file mode 100644 index 0000000000000000000000000000000000000000..cd24d06dc61698b217db1422c9585ed4b7b97b05 --- /dev/null +++ b/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmController.java @@ -0,0 +1,412 @@ +package fr.inra.urgi.faidare.web.germplasm; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import fr.inra.urgi.faidare.api.NotFoundException; +import fr.inra.urgi.faidare.config.FaidareProperties; +import fr.inra.urgi.faidare.domain.brapi.v1.data.BrapiGermplasmAttributeValue; +import fr.inra.urgi.faidare.domain.brapi.v1.data.BrapiSibling; +import fr.inra.urgi.faidare.domain.criteria.GermplasmAttributeCriteria; +import fr.inra.urgi.faidare.domain.criteria.GermplasmGETSearchCriteria; +import fr.inra.urgi.faidare.domain.data.germplasm.CollPopVO; +import fr.inra.urgi.faidare.domain.data.germplasm.DonorVO; +import fr.inra.urgi.faidare.domain.data.germplasm.GenealogyVO; +import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmAttributeValueVO; +import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmInstituteVO; +import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmVO; +import fr.inra.urgi.faidare.domain.data.germplasm.InstituteVO; +import fr.inra.urgi.faidare.domain.data.germplasm.PedigreeVO; +import fr.inra.urgi.faidare.domain.data.germplasm.PhotoVO; +import fr.inra.urgi.faidare.domain.data.germplasm.PuiNameValueVO; +import fr.inra.urgi.faidare.domain.data.germplasm.SiblingVO; +import fr.inra.urgi.faidare.domain.data.germplasm.SimpleVO; +import fr.inra.urgi.faidare.domain.data.germplasm.SiteVO; +import fr.inra.urgi.faidare.domain.data.germplasm.TaxonSourceVO; +import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO; +import fr.inra.urgi.faidare.repository.es.GermplasmAttributeRepository; +import fr.inra.urgi.faidare.repository.es.GermplasmRepository; +import fr.inra.urgi.faidare.repository.es.XRefDocumentRepository; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.servlet.ModelAndView; + +/** + * Controller used to display a germplasm card based on its ID. + * @author JB Nizet + */ +@Controller("webGermplasmController") +@RequestMapping("/germplasms") +public class GermplasmController { + + private final GermplasmRepository germplasmRepository; + private final FaidareProperties faidareProperties; + private final XRefDocumentRepository xRefDocumentRepository; + private GermplasmAttributeRepository germplasmAttributeRepository; + + public GermplasmController(GermplasmRepository germplasmRepository, + FaidareProperties faidareProperties, + XRefDocumentRepository xRefDocumentRepository, + GermplasmAttributeRepository germplasmAttributeRepository) { + this.germplasmRepository = germplasmRepository; + this.faidareProperties = faidareProperties; + this.xRefDocumentRepository = xRefDocumentRepository; + this.germplasmAttributeRepository = germplasmAttributeRepository; + } + + @GetMapping("/{germplasmId}") + public ModelAndView get(@PathVariable("germplasmId") String germplasmId) { + // GermplasmVO germplasm = germplasmRepository.getById(germplasmId); + + // TODO replace this block by the above commented one + GermplasmVO germplasm = createGermplasm(); + + if (germplasm == null) { + throw new NotFoundException("Germplasm with ID " + germplasmId + " not found"); + } + + return toModelAndView(germplasm); + } + + @GetMapping(params = "pui") + public ModelAndView getByPui(@RequestParam("pui") String pui) { + GermplasmGETSearchCriteria criteria = new GermplasmGETSearchCriteria(); + criteria.setGermplasmPUI(Collections.singletonList(pui)); + List<GermplasmVO> germplasms = germplasmRepository.find(criteria); + if (germplasms.size() != 1) { + throw new NotFoundException("Germplasm with PUI " + pui + " not found"); + } + + return toModelAndView(germplasms.get(0)); + } + + private ModelAndView toModelAndView(GermplasmVO germplasm) { + // List<BrapiGermplasmAttributeValue> attributes = getAttributes(germplasm); + // List<XRefDocumentVO> crossReferences = xRefDocumentRepository.find( + // XRefDocumentSearchCriteria.forXRefId(site.getLocationDbId())); + // PedigreeVO pedigree = getPedigree(germplasm); + // List<XRefDocumentVO> crossReferences = xRefDocumentRepository.find( + // XRefDocumentSearchCriteria.forXRefId(germplasm.getGermplasmDbId()) + // ); + + // TODO replace this block by the above commented one + List<BrapiGermplasmAttributeValue> attributes = Arrays.asList( + createAttribute() + ); + PedigreeVO pedigree = createPedigree(); + List<XRefDocumentVO> crossReferences = Arrays.asList( + createXref("foobar"), + createXref("bazbing") + ); + + sortDonors(germplasm); + sortPopulations(germplasm); + sortCollections(germplasm); + sortPanels(germplasm); + return new ModelAndView("germplasm", + "model", + new GermplasmModel( + germplasm, + faidareProperties.getByUri(germplasm.getSourceUri()), + attributes, + pedigree, + crossReferences) + ); + } + + private void sortPopulations(GermplasmVO germplasm) { + if (germplasm.getPopulation() != null) { + germplasm.setPopulation(germplasm.getPopulation() + .stream() + .sorted(Comparator.comparing( + CollPopVO::getName)) + .collect(Collectors.toList())); + } + } + + private void sortCollections(GermplasmVO germplasm) { + if (germplasm.getCollection() != null) { + germplasm.setCollection(germplasm.getCollection() + .stream() + .sorted(Comparator.comparing(CollPopVO::getName)) + .collect(Collectors.toList())); + } + } + + private void sortPanels(GermplasmVO germplasm) { + if (germplasm.getPanel() != null) { + germplasm.setPanel(germplasm.getPanel() + .stream() + .sorted(Comparator.comparing(CollPopVO::getName)) + .collect(Collectors.toList())); + } + } + + private void sortDonors(GermplasmVO germplasm) { + if (germplasm.getDonors() != null) { + germplasm.setDonors(germplasm.getDonors() + .stream() + .sorted(Comparator.comparing(donor -> donor.getDonorInstitute() + .getInstituteName())) + .collect(Collectors.toList())); + } + } + + private List<BrapiGermplasmAttributeValue> getAttributes(GermplasmVO germplasm) { + GermplasmAttributeCriteria criteria = new GermplasmAttributeCriteria(); + criteria.setGermplasmDbId(germplasm.getGermplasmDbId()); + return germplasmAttributeRepository.find(criteria) + .stream() + .flatMap(vo -> vo.getData().stream()) + .sorted(Comparator.comparing(BrapiGermplasmAttributeValue::getAttributeName)) + .collect(Collectors.toList()); + } + + private PedigreeVO getPedigree(GermplasmVO germplasm) { + return germplasmRepository.findPedigree(germplasm.getGermplasmDbId()); + } + + private BrapiGermplasmAttributeValue createAttribute() { + GermplasmAttributeValueVO result = new GermplasmAttributeValueVO(); + result.setAttributeName("A1"); + result.setValue("V1"); + return result; + } + + private GermplasmVO createGermplasm() { + GermplasmVO result = new GermplasmVO(); + + result.setGermplasmName("BLE BARBU DU ROUSSILLON"); + result.setAccessionNumber("1408"); + result.setSynonyms(Arrays.asList("BLE DU ROUSSILLON", "FRA051:1699", "ROUSSILLON")); + PhotoVO photo = new PhotoVO(); + photo.setPhotoName("Blé du roussillon"); + photo.setCopyright("INRA, Emmanuelle BOULAT/Lionel BARDY 2012"); + photo.setThumbnailFile("https://urgi.versailles.inrae.fr/files/siregal/images/accession/CEREALS/thumbnails/thumb_1408_R09_S.jpg"); + photo.setFile("https://urgi.versailles.inrae.fr/files/siregal/images/accession/CEREALS/1408_R09_S.jpg"); + result.setPhoto(photo); + + InstituteVO holdingGenBank = new InstituteVO(); + holdingGenBank.setLogo("https://urgi.versailles.inra.fr/files/siregal/images/grc/inra_brc_en.png"); + holdingGenBank.setInstituteName("INRA BRC"); + holdingGenBank.setWebSite("http://google.fr"); + result.setHoldingGenbank(holdingGenBank); + + result.setBiologicalStatusOfAccessionCode("Traditional cultivar/landrace "); + result.setPedigree("LV"); + SiteVO originSite = new SiteVO(); + originSite.setSiteId("1234"); + originSite.setSiteName("Le Moulon"); + originSite.setSiteType("Origin site"); + originSite.setLatitude(47.0F); + originSite.setLongitude(12.0F); + result.setOriginSite(originSite); + + List<SiteVO> evaluationSites = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + SiteVO evaluationSite = new SiteVO(); + evaluationSite.setSiteId(Integer.toString(12347 + i)); + evaluationSite.setSiteType("Evaluation site"); + evaluationSite.setSiteName("Site " + i); + evaluationSite.setLatitude(46.0F + i); + evaluationSite.setLongitude(13.0F + i); + evaluationSites.add(evaluationSite); + } + result.setEvaluationSites(evaluationSites); + + result.setGenus("Genus 1"); + result.setSpecies("Species 1"); + result.setSpeciesAuthority("Species Auth"); + result.setSourceUri("https://urgi.versailles.inrae.fr/gnpis"); + result.setSubtaxa("Subtaxa 1"); + result.setGenusSpeciesSubtaxa("Triticum aestivum subsp. aestivum"); + result.setSubtaxaAuthority("INRAE"); + result.setTaxonIds(Arrays.asList(createTaxonId(), createTaxonId())); + result.setTaxonComment("C'est bon le blé"); + result.setTaxonCommonNames(Arrays.asList("Blé tendre", "Bread wheat", "Soft wheat")); + result.setTaxonSynonyms(Arrays.asList("Blé tendre1", "Bread wheat1", "Soft wheat1")); + + InstituteVO holdingInstitute = new InstituteVO(); + holdingInstitute.setInstituteName("GDEC - UMR Génétique, Diversité et Ecophysiologie des Céréales"); + holdingInstitute.setLogo("https://urgi.versailles.inra.fr/files/siregal/images/grc/inra_brc_en.png"); + holdingInstitute.setWebSite("https://google.fr/q=qsdqsdqsdslqlsdnqlsdqlsdlqskdlqdqlsdqsdqsdqd"); + holdingInstitute.setInstituteCode("GDEC"); + holdingInstitute.setInstituteType("Type1"); + holdingInstitute.setAcronym("G.D.E.C"); + holdingInstitute.setAddress("Lyon"); + holdingInstitute.setOrganisation("SAS"); + result.setHoldingInstitute(holdingInstitute); + + result.setPresenceStatus("Maintained"); + + GermplasmInstituteVO collector = new GermplasmInstituteVO(); + collector.setMaterialType("Fork"); + collector.setCollectors("Joe, Jack, William, Averell"); + InstituteVO collectingInstitute = new InstituteVO(); + collectingInstitute.setInstituteName("Ninja Squad"); + collector.setInstitute(collectingInstitute); + collector.setAccessionNumber("567"); + result.setCollector(collector); + + SiteVO collectingSite = new SiteVO(); + collectingSite.setSiteId("1235"); + collectingSite.setSiteName("St Just"); + collectingSite.setSiteType("Collecting site"); + collectingSite.setLatitude(48.0F); + collectingSite.setLongitude(13.0F); + result.setCollectingSite(collectingSite); + result.setAcquisitionDate("In the summer"); + + GermplasmInstituteVO breeder = new GermplasmInstituteVO(); + InstituteVO breedingInstitute = new InstituteVO(); + breedingInstitute.setInstituteName("Microsoft"); + breeder.setInstitute(breedingInstitute); + breeder.setAccessionCreationDate(2015); + breeder.setAccessionNumber("678"); + breeder.setRegistrationYear(2016); + breeder.setDeregistrationYear(2019); + result.setBreeder(breeder); + + result.setDonors(Arrays.asList( + createDonor() + )); + + result.setDistributors(Arrays.asList( + createDistributor() + )); + + result.setChildren(Arrays.asList(createChild(), createChild())); + + result.setGermplasmPUI("germplasmPUI"); + result.setPopulation(Arrays.asList(createPopulation1(), createPopulation2(), createPopulation3())); + + result.setCollection(Arrays.asList(createCollection())); + + result.setPanel(Arrays.asList(createPanel())); + + return result; + } + + private DonorVO createDonor() { + DonorVO result = new DonorVO(); + result.setDonorGermplasmPUI("PUI1"); + result.setDonationDate(2017); + result.setDonorAccessionNumber("3456"); + result.setDonorInstituteCode("GD46U"); + InstituteVO institute = new InstituteVO(); + institute.setInstituteName("Hello"); + result.setDonorInstitute(institute); + return result; + } + + private GermplasmInstituteVO createDistributor() { + GermplasmInstituteVO result = new GermplasmInstituteVO(); + InstituteVO institute = new InstituteVO(); + institute.setInstituteName("Microsoft"); + result.setInstitute(institute); + result.setAccessionNumber("678"); + result.setDistributionStatus("OK"); + return result; + } + + private PedigreeVO createPedigree() { + PedigreeVO result = new PedigreeVO(); + result.setPedigree("Pedigree 1"); + result.setParent1DbId("12345"); + result.setParent1Name("Parent 1"); + result.setParent1Type("P1"); + result.setParent2DbId("12346"); + result.setParent2Name("Parent 2"); + result.setParent2Type("P2"); + result.setCrossingPlan("crossing plan 1"); + result.setCrossingYear("2012"); + result.setSiblings(Arrays.asList(createBrapiSibling())); + return result; + } + + private BrapiSibling createBrapiSibling() { + SiblingVO sibling = new SiblingVO(); + sibling.setGermplasmDbId("5678"); + sibling.setDefaultDisplayName("Sibling 5678"); + return sibling; + } + + private GenealogyVO createChild() { + GenealogyVO result = new GenealogyVO(); + result.setFirstParentName("CP1"); + result.setSecondParentName("CP2"); + result.setSibblings(Arrays.asList(createPuiNameValueVO(), createPuiNameValueVO())); + return result; + } + + private PuiNameValueVO createPuiNameValueVO() { + PuiNameValueVO result = new PuiNameValueVO(); + result.setName("Child 1"); + result.setPui("pui1"); + return result; + } + + private CollPopVO createPopulation1() { + CollPopVO result = new CollPopVO(); + result.setName("Population 1"); + result.setType("Pop Type 1"); + result.setGermplasmCount(3); + result.setGermplasmRef(createPuiNameValueVO()); + return result; + } + + private CollPopVO createPopulation2() { + CollPopVO result = new CollPopVO(); + result.setName("Population 2"); + result.setGermplasmCount(3); + PuiNameValueVO puiNameValueVO = createPuiNameValueVO(); + puiNameValueVO.setPui("germplasmPUI"); + result.setGermplasmRef(puiNameValueVO); + return result; + } + + private CollPopVO createPopulation3() { + CollPopVO result = new CollPopVO(); + result.setName("Population 3"); + result.setGermplasmCount(5); + return result; + } + + private CollPopVO createCollection() { + CollPopVO result = new CollPopVO(); + result.setName("Collection 1"); + result.setGermplasmCount(7); + return result; + } + + private CollPopVO createPanel() { + CollPopVO result = new CollPopVO(); + result.setName("The_panel_1"); + result.setGermplasmCount(2); + return result; + } + + private TaxonSourceVO createTaxonId() { + TaxonSourceVO result = new TaxonSourceVO(); + result.setTaxonId("taxon1"); + result.setSourceName("ThePlantList"); + return result; + } + + private XRefDocumentVO createXref(String name) { + XRefDocumentVO xref = new XRefDocumentVO(); + xref.setName(name); + xref.setDescription("A very large description for the xref " + name + " which has way more than 120 characters bla bla bla bla bla bla bla bla bla bla bla bla"); + xref.setDatabaseName("db_" + name); + xref.setUrl("https://google.com"); + xref.setEntryType("type " + name); + return xref; + } +} diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmModel.java b/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmModel.java new file mode 100644 index 0000000000000000000000000000000000000000..ffa24336f395b2d00817babacc09b38981318178 --- /dev/null +++ b/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmModel.java @@ -0,0 +1,157 @@ +package fr.inra.urgi.faidare.web.germplasm; + +import java.util.ArrayList; +import java.util.List; + +import fr.inra.urgi.faidare.domain.brapi.v1.data.BrapiGermplasmAttributeValue; +import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmInstituteVO; +import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmVO; +import fr.inra.urgi.faidare.domain.data.germplasm.PedigreeVO; +import fr.inra.urgi.faidare.domain.data.germplasm.SiteVO; +import fr.inra.urgi.faidare.domain.datadiscovery.data.DataSource; +import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO; +import fr.inra.urgi.faidare.web.site.MapLocation; +import org.apache.logging.log4j.util.Strings; + +/** + * The model used by the germplasm page + * @author JB Nizet + */ +public final class GermplasmModel { + private final GermplasmVO germplasm; + private final DataSource source; + private final List<BrapiGermplasmAttributeValue> attributes; + private final PedigreeVO pedigree; + private final List<XRefDocumentVO> crossReferences; + + public GermplasmModel(GermplasmVO germplasm, + DataSource source, + List<BrapiGermplasmAttributeValue> attributes, + PedigreeVO pedigree, + List<XRefDocumentVO> crossReferences) { + this.germplasm = germplasm; + this.source = source; + this.attributes = attributes; + this.pedigree = pedigree; + this.crossReferences = crossReferences; + } + + public GermplasmVO getGermplasm() { + return germplasm; + } + + public DataSource getSource() { + return source; + } + + public List<BrapiGermplasmAttributeValue> getAttributes() { + return attributes; + } + + public PedigreeVO getPedigree() { + return pedigree; + } + + public List<XRefDocumentVO> getCrossReferences() { + return crossReferences; + } + + public String getTaxon() { + if (Strings.isNotBlank(this.germplasm.getGenusSpeciesSubtaxa())) { + return this.germplasm.getGenusSpeciesSubtaxa(); + } else if (Strings.isNotBlank(this.germplasm.getGenusSpecies())) { + return this.germplasm.getGenusSpecies(); + } else if (Strings.isNotBlank(this.germplasm.getSubtaxa())) { + return this.germplasm.getGenus() + " " + this.germplasm.getSpecies() + " " + this.germplasm.getSubtaxa(); + } else if (Strings.isNotBlank(this.germplasm.getSpecies())) { + return this.germplasm.getGenus() + " " + this.germplasm.getSpecies(); + } else { + return this.germplasm.getGenus(); + } + } + + public String getTaxonAuthor() { + if (Strings.isNotBlank(this.germplasm.getGenusSpeciesSubtaxa())) { + return this.germplasm.getSubtaxaAuthority(); + } else if (Strings.isNotBlank(this.germplasm.getGenusSpecies())) { + return this.germplasm.getSpeciesAuthority(); + } else if (Strings.isNotBlank(this.germplasm.getSubtaxa())) { + return this.germplasm.getSubtaxaAuthority(); + } else if (Strings.isNotBlank(this.germplasm.getSpecies())) { + return this.germplasm.getSpeciesAuthority(); + } else { + return null; + } + } + + public boolean isCollecting() { + return this.isCollectingSitePresent() + || this.isCollectorInstitutePresent() + || this.isCollectorIntituteFieldPresent(); + } + + private boolean isCollectingSitePresent() { + return this.germplasm.getCollectingSite() != null && Strings.isNotBlank(this.germplasm.getCollectingSite().getSiteName()); + } + + private boolean isCollectorInstitutePresent() { + return this.germplasm.getCollector() != null && + this.germplasm.getCollector().getInstitute() != null && + Strings.isNotBlank(this.germplasm.getCollector().getInstitute().getInstituteName()); + } + + private boolean isCollectorIntituteFieldPresent() { + GermplasmInstituteVO collector = this.germplasm.getCollector(); + return (collector != null) && + (Strings.isNotBlank(collector.getAccessionNumber()) + || collector.getAccessionCreationDate() != null + || Strings.isNotBlank(collector.getMaterialType()) + || Strings.isNotBlank(collector.getCollectors()) + || collector.getRegistrationYear() != null + || collector.getDeregistrationYear() != null + || Strings.isNotBlank(collector.getDistributionStatus()) + ); + } + + public boolean isBreeding() { + GermplasmInstituteVO breeder = this.germplasm.getBreeder(); + return breeder != null && + ((breeder.getInstitute() != null && Strings.isNotBlank(breeder.getInstitute().getInstituteName())) || + breeder.getAccessionCreationDate() != null || + Strings.isNotBlank(breeder.getAccessionNumber()) || + breeder.getRegistrationYear() != null || + breeder.getDeregistrationYear() != null); + } + + public boolean isGenealogyPresent() { + return isPedigreePresent() || isProgenyPresent(); + } + + private boolean isProgenyPresent() { + return germplasm.getChildren() != null && !germplasm.getChildren().isEmpty(); + } + + private boolean isPedigreePresent() { + return this.pedigree != null && + (Strings.isNotBlank(this.pedigree.getParent1Name()) + || Strings.isNotBlank(this.pedigree.getParent2Name()) + || Strings.isNotBlank(this.pedigree.getCrossingPlan()) + || Strings.isNotBlank(this.pedigree.getCrossingYear()) + || Strings.isNotBlank(this.pedigree.getFamilyCode())); + } + + public List<MapLocation> getMapLocations() { + List<SiteVO> sites = new ArrayList<>(); + if (germplasm.getCollectingSite() != null) { + sites.add(germplasm.getCollectingSite()); + } + if (germplasm.getOriginSite() != null) { + sites.add(germplasm.getOriginSite()); + } + if (germplasm.getEvaluationSites() != null) { + sites.addAll(germplasm.getEvaluationSites()); + } + + return MapLocation.sitesToDisplayableMapLocations(sites); + } +} diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/site/MapLocation.java b/backend/src/main/java/fr/inra/urgi/faidare/web/site/MapLocation.java new file mode 100644 index 0000000000000000000000000000000000000000..3b096853cfff4a467d7a277f9fb4b41e65b2ad01 --- /dev/null +++ b/backend/src/main/java/fr/inra/urgi/faidare/web/site/MapLocation.java @@ -0,0 +1,82 @@ +package fr.inra.urgi.faidare.web.site; + +import java.util.List; +import java.util.stream.Collectors; + +import fr.inra.urgi.faidare.domain.data.LocationVO; +import fr.inra.urgi.faidare.domain.data.germplasm.SiteVO; +import fr.inra.urgi.faidare.utils.Sites; + +/** + * An object that can be serialized to JSON to serve as a map marker. + * @author JB Nizet + */ +public final class MapLocation { + private final String locationDbId; + private final String locationType; + private final String locationName; + private final double latitude; + private final double longitude; + + public MapLocation(String locationDbId, + String locationType, + String locationName, + double latitude, + double longitude) { + this.locationDbId = locationDbId; + this.locationType = locationType; + this.locationName = locationName; + this.latitude = latitude; + this.longitude = longitude; + } + + public MapLocation(LocationVO site) { + this(site.getLocationDbId(), + site.getLocationType(), + site.getLocationName(), + site.getLatitude(), + site.getLongitude()); + } + + public MapLocation(SiteVO site) { + this(Sites.siteIdToLocationId(site.getSiteId()), + site.getSiteType(), + site.getSiteName(), + site.getLatitude(), + site.getLongitude()); + } + + public static List<MapLocation> locationsToDisplayableMapLocations(List<LocationVO> locations) { + return locations.stream() + .filter(location -> location.getLatitude() != null && location.getLongitude() != null) + .map(MapLocation::new) + .collect(Collectors.toList()); + } + + public static List<MapLocation> sitesToDisplayableMapLocations(List<SiteVO> sites) { + return sites.stream() + .filter(site -> site.getLatitude() != null && site.getLongitude() != null) + .map(MapLocation::new) + .collect(Collectors.toList()); + } + + public String getLocationDbId() { + return locationDbId; + } + + public String getLocationType() { + return locationType; + } + + public String getLocationName() { + return locationName; + } + + public double getLatitude() { + return latitude; + } + + public double getLongitude() { + return longitude; + } +} diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteController.java b/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteController.java new file mode 100644 index 0000000000000000000000000000000000000000..151da527d4393d1f388de8d6098b52d4219d98b2 --- /dev/null +++ b/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteController.java @@ -0,0 +1,94 @@ +package fr.inra.urgi.faidare.web.site; + +import java.util.Arrays; +import java.util.List; + +import fr.inra.urgi.faidare.api.NotFoundException; +import fr.inra.urgi.faidare.config.FaidareProperties; +import fr.inra.urgi.faidare.domain.brapi.v1.data.BrapiAdditionalInfo; +import fr.inra.urgi.faidare.domain.data.LocationVO; +import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO; +import fr.inra.urgi.faidare.repository.es.LocationRepository; +import fr.inra.urgi.faidare.repository.es.XRefDocumentRepository; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.ModelAndView; + +/** + * Controller used to display a site card based on its ID. + * @author JB Nizet + */ +@Controller("webSiteController") +@RequestMapping("/sites") +public class SiteController { + + private final LocationRepository locationRepository; + private final FaidareProperties faidareProperties; + private final XRefDocumentRepository xRefDocumentRepository; + + public SiteController(LocationRepository locationRepository, + FaidareProperties faidareProperties, + XRefDocumentRepository xRefDocumentRepository) { + this.locationRepository = locationRepository; + this.faidareProperties = faidareProperties; + this.xRefDocumentRepository = xRefDocumentRepository; + } + + @GetMapping("/{siteId}") + public ModelAndView get(@PathVariable("siteId") String siteId) { + LocationVO site = locationRepository.getById(siteId); + + // List<XRefDocumentVO> crossReferences = xRefDocumentRepository.find( + // XRefDocumentSearchCriteria.forXRefId(site.getLocationDbId())); + List<XRefDocumentVO> crossReferences = Arrays.asList( + createXref("foobar"), + createXref("bazbing") + ); + + // LocationVO site = createSite(); + + if (site == null) { + throw new NotFoundException("Site with ID " + siteId + " not found"); + } + + + return new ModelAndView("site", + "model", + new SiteModel( + site, + faidareProperties.getByUri(site.getSourceUri()), + crossReferences + ) + ); + } + + private LocationVO createSite() { + LocationVO site = new LocationVO(); + site.setLocationName("France"); + site.setSourceUri("https://urgi.versailles.inrae.fr/gnpis"); + site.setUri("Test URI"); + site.setUrl("https://google.com"); + site.setLatitude(45.65); + site.setLongitude(1.34); + BrapiAdditionalInfo additionalInfo = new BrapiAdditionalInfo(); + additionalInfo.addProperty("Slope", 4.32); + additionalInfo.addProperty("Distance to city", "3 km"); + additionalInfo.addProperty("foo", "bar"); + additionalInfo.addProperty("baz", "zing"); + additionalInfo.addProperty("blob", null); + site.setAdditionalInfo(additionalInfo); + return site; + } + + private XRefDocumentVO createXref(String name) { + XRefDocumentVO xref = new XRefDocumentVO(); + xref.setName(name); + xref.setDescription("A very large description for the xref " + name + " which has way more than 120 characters bla bla bla bla bla bla bla bla bla bla bla bla"); + xref.setDatabaseName("db_" + name); + xref.setUrl("https://google.com"); + xref.setEntryType("type " + name); + return xref; + } +} diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteModel.java b/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteModel.java new file mode 100644 index 0000000000000000000000000000000000000000..0d7e1c0dcef28b8182a3a5ac8f4aa160f067255c --- /dev/null +++ b/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteModel.java @@ -0,0 +1,118 @@ +package fr.inra.urgi.faidare.web.site; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import fr.inra.urgi.faidare.domain.data.LocationVO; +import fr.inra.urgi.faidare.domain.datadiscovery.data.DataSource; +import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO; + +/** + * The model used py the site page + * @author JB Nizet + */ +public final class SiteModel { + private static final Set<String> IGNORED_PROPERTIES = + new HashSet<>(Arrays.asList("Site status", + "Coordinates precision", + "Slope", + "Exposure", + "Geographical location", + "Distance to city", + "Direction from city", + "Environment type", + "Topography", + "Comment")); + + private final LocationVO site; + private final DataSource source; + private final Map<String, Object> additionalInfo; + private final List<XRefDocumentVO> crossReferences; + private final List<Map.Entry<String, Object>> additionalInfoProperties; + + public SiteModel(LocationVO site, + DataSource source, + List<XRefDocumentVO> crossReferences) { + this.site = site; + this.source = source; + this.additionalInfo = site.getAdditionalInfo() == null ? Collections.emptyMap() : site.getAdditionalInfo().getProperties(); + this.crossReferences = crossReferences; + this.additionalInfoProperties = + this.additionalInfo + .entrySet() + .stream() + .filter(entry -> !IGNORED_PROPERTIES.contains(entry.getKey())) + .filter(entry -> entry.getValue() != null && !entry.getValue().toString().isEmpty()) + .sorted(Map.Entry.comparingByKey()) + .collect(Collectors.toList()); + } + + public LocationVO getSite() { + return site; + } + + public DataSource getSource() { + return source; + } + + public Map<String, Object> getAdditionalInfo() { + return this.additionalInfo; + } + + public Object getSiteStatus() { + return this.additionalInfo.get("Site status"); + } + + public Object getCoordinatesPrecision() { + return this.additionalInfo.get("Coordinates precision"); + } + + public Object getGeographicalLocation() { + return this.additionalInfo.get("Geographical location"); + } + + public Object getSlope() { + return this.additionalInfo.get("Slope"); + } + + public Object getExposure() { + return this.additionalInfo.get("Exposure"); + } + + public Object getTopography() { + return this.additionalInfo.get("Topography"); + } + + public Object getEnvironmentType() { + return this.additionalInfo.get("Environment type"); + } + + public Object getDistanceToCity() { + return this.additionalInfo.get("Distance to city"); + } + + public Object getDirectionFromCity() { + return this.additionalInfo.get("Direction from city"); + } + + public Object getComment() { + return this.additionalInfo.get("Comment"); + } + + public List<Map.Entry<String, Object>> getAdditionalInfoProperties() { + return additionalInfoProperties; + } + + public List<XRefDocumentVO> getCrossReferences() { + return crossReferences; + } + + public List<MapLocation> getMapLocations() { + return MapLocation.locationsToDisplayableMapLocations(Collections.singletonList(this.site)); + } +} diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyController.java b/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyController.java new file mode 100644 index 0000000000000000000000000000000000000000..833c45471061a5ef02d7f5fc57319fc9afc8a12b --- /dev/null +++ b/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyController.java @@ -0,0 +1,159 @@ +package fr.inra.urgi.faidare.web.study; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import com.google.common.collect.Lists; +import fr.inra.urgi.faidare.api.NotFoundException; +import fr.inra.urgi.faidare.config.FaidareProperties; +import fr.inra.urgi.faidare.domain.criteria.GermplasmPOSTSearchCriteria; +import fr.inra.urgi.faidare.domain.data.LocationVO; +import fr.inra.urgi.faidare.domain.data.TrialVO; +import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmVO; +import fr.inra.urgi.faidare.domain.data.study.StudyDetailVO; +import fr.inra.urgi.faidare.domain.data.variable.ObservationVariableVO; +import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO; +import fr.inra.urgi.faidare.repository.es.GermplasmRepository; +import fr.inra.urgi.faidare.repository.es.LocationRepository; +import fr.inra.urgi.faidare.repository.es.StudyRepository; +import fr.inra.urgi.faidare.repository.es.TrialRepository; +import fr.inra.urgi.faidare.repository.es.XRefDocumentRepository; +import fr.inra.urgi.faidare.repository.file.CropOntologyRepository; +import fr.inra.urgi.faidare.web.site.MapLocation; +import org.apache.logging.log4j.util.Strings; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.ModelAndView; + +/** + * Controller used to display a study card based on its ID. + * @author JB Nizet + */ +@Controller("webStudyController") +@RequestMapping("/studies") +public class StudyController { + + private final StudyRepository studyRepository; + private final FaidareProperties faidareProperties; + private final XRefDocumentRepository xRefDocumentRepository; + private final GermplasmRepository germplasmRepository; + private final CropOntologyRepository cropOntologyRepository; + private final TrialRepository trialRepository; + private final LocationRepository locationRepository; + + public StudyController(StudyRepository studyRepository, + FaidareProperties faidareProperties, + XRefDocumentRepository xRefDocumentRepository, + GermplasmRepository germplasmRepository, + CropOntologyRepository cropOntologyRepository, + TrialRepository trialRepository, + LocationRepository locationRepository) { + this.studyRepository = studyRepository; + this.faidareProperties = faidareProperties; + this.xRefDocumentRepository = xRefDocumentRepository; + this.germplasmRepository = germplasmRepository; + this.cropOntologyRepository = cropOntologyRepository; + this.trialRepository = trialRepository; + this.locationRepository = locationRepository; + } + + @GetMapping("/{studyId}") + public ModelAndView get(@PathVariable("studyId") String studyId) { + StudyDetailVO study = studyRepository.getById(studyId); + + // List<XRefDocumentVO> crossReferences = xRefDocumentRepository.find( + // XRefDocumentSearchCriteria.forXRefId(site.getLocationDbId())); + List<XRefDocumentVO> crossReferences = Arrays.asList( + createXref("foobar"), + createXref("bazbing") + ); + + // LocationVO site = createSite(); + + if (study == null) { + throw new NotFoundException("Study with ID " + studyId + " not found"); + } + + List<GermplasmVO> germplasms = getGermplasms(study); + List<ObservationVariableVO>variables = getVariables(study); + List<TrialVO> trials = getTrials(study); + LocationVO location = getLocation(study); + + // TODO remove this + location.setLatitude(34.0); + location.setLongitude(14.0); + + return new ModelAndView("study", + "model", + new StudyModel( + study, + faidareProperties.getByUri(study.getSourceUri()), + germplasms, + variables, + trials, + crossReferences, + location + ) + ); + } + + private LocationVO getLocation(StudyDetailVO study) { + if (Strings.isBlank(study.getLocationDbId())) { + return null; + } + return locationRepository.getById(study.getLocationDbId()); + } + + private List<GermplasmVO> getGermplasms(StudyDetailVO study) { + if (study.getGermplasmDbIds() == null || study.getGermplasmDbIds().isEmpty()) { + return Collections.emptyList(); + } else { + GermplasmPOSTSearchCriteria germplasmCriteria = new GermplasmPOSTSearchCriteria(); + germplasmCriteria.setGermplasmDbIds(Lists.newArrayList(study.getGermplasmDbIds())); + return germplasmRepository.find(germplasmCriteria) + .stream() + .sorted(Comparator.comparing(GermplasmVO::getGermplasmName)) + .collect(Collectors.toList()); + } + } + + private List<ObservationVariableVO> getVariables(StudyDetailVO study) { + Set<String> variableIds = studyRepository.getVariableIds(study.getStudyDbId()); + return cropOntologyRepository.getVariableByIds(variableIds) + .stream() + .sorted(Comparator.comparing(ObservationVariableVO::getObservationVariableDbId)) + .collect(Collectors.toList()); + } + + private List<TrialVO> getTrials(StudyDetailVO study) { + if (study.getTrialDbIds() == null || study.getTrialDbIds().isEmpty()) { + return Collections.emptyList(); + } + + return study.getTrialDbIds() + .stream() + .sorted(Comparator.naturalOrder()) + .map(trialRepository::getById) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + + + private XRefDocumentVO createXref(String name) { + XRefDocumentVO xref = new XRefDocumentVO(); + xref.setName(name); + xref.setDescription("A very large description for the xref " + name + " which has way more than 120 characters bla bla bla bla bla bla bla bla bla bla bla bla"); + xref.setDatabaseName("db_" + name); + xref.setUrl("https://google.com"); + xref.setEntryType("type " + name); + return xref; + } +} diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyModel.java b/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyModel.java new file mode 100644 index 0000000000000000000000000000000000000000..bc77dfc3d9b9fdb7da63dd7b47afb15ab1e3bff6 --- /dev/null +++ b/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyModel.java @@ -0,0 +1,90 @@ +package fr.inra.urgi.faidare.web.study; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import fr.inra.urgi.faidare.domain.data.LocationVO; +import fr.inra.urgi.faidare.domain.data.TrialVO; +import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmVO; +import fr.inra.urgi.faidare.domain.data.study.StudyDetailVO; +import fr.inra.urgi.faidare.domain.data.variable.ObservationVariableVO; +import fr.inra.urgi.faidare.domain.datadiscovery.data.DataSource; +import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO; +import fr.inra.urgi.faidare.web.site.MapLocation; + +/** + * The model used by the study page + * @author JB Nizet + */ +public final class StudyModel { + private final StudyDetailVO study; + private final DataSource source; + private final List<GermplasmVO> germplasms; + private final List<ObservationVariableVO> variables; + private final List<TrialVO> trials; + private final List<XRefDocumentVO> crossReferences; + private final LocationVO location; + private final List<Map.Entry<String, Object>> additionalInfoProperties; + + public StudyModel(StudyDetailVO study, + DataSource source, + List<GermplasmVO> germplasms, + List<ObservationVariableVO> variables, + List<TrialVO> trials, + List<XRefDocumentVO> crossReferences, + LocationVO location) { + this.study = study; + this.source = source; + this.germplasms = germplasms; + this.variables = variables; + this.trials = trials; + this.crossReferences = crossReferences; + this.location = location; + + Map<String, Object> additionalInfo = + study.getAdditionalInfo() == null ? Collections.emptyMap() : study.getAdditionalInfo().getProperties(); + this.additionalInfoProperties = + additionalInfo.entrySet() + .stream() + .filter(entry -> entry.getValue() != null && !entry.getValue().toString().isEmpty()) + .sorted(Map.Entry.comparingByKey()) + .collect(Collectors.toList()); + } + + public StudyDetailVO getStudy() { + return study; + } + + public DataSource getSource() { + return source; + } + + public List<XRefDocumentVO> getCrossReferences() { + return crossReferences; + } + + public List<GermplasmVO> getGermplasms() { + return germplasms; + } + + public List<ObservationVariableVO> getVariables() { + return variables; + } + + public List<TrialVO> getTrials() { + return trials; + } + + public List<Map.Entry<String, Object>> getAdditionalInfoProperties() { + return additionalInfoProperties; + } + + public List<MapLocation> getMapLocations() { + if (this.location == null) { + return Collections.emptyList(); + } + return MapLocation.locationsToDisplayableMapLocations(Collections.singletonList(this.location)); + } +} diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/Coordinates.java b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/Coordinates.java new file mode 100644 index 0000000000000000000000000000000000000000..554929b11d88e103093f808be0ba969118c47772 --- /dev/null +++ b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/Coordinates.java @@ -0,0 +1,71 @@ +package fr.inra.urgi.faidare.web.thymeleaf; + +import java.text.DecimalFormat; +import java.util.Locale; + +/** + * The actual object offering Coordinates helper methods to thymeleaf + * @author JB Nizet + */ +public class Coordinates { + private final Locale locale; + + public Coordinates(Locale locale) { + this.locale = locale; + } + + public String format(Double value) { + if (value == null) { + return ""; + } + return DecimalFormat.getInstance(locale).format(value); + } + + public String formatLatitude(Double value) { + if (value == null) { + return ""; + } + return this.format(value) + " — " + this.toLatitudeDegrees(value); + } + + public String formatLongitude(Double value) { + if (value == null) { + return ""; + } + return this.format(value) + " — " + this.toLongitudeDegrees(value); + } + + public String toLatitudeDegrees(Double latitude) { + if (latitude == null) { + return ""; + } + + return toDegrees(latitude) + " " + ((latitude < 0) ? "S" : "N"); + } + + public String toLongitudeDegrees(Double longitude) { + if (longitude == null) { + return ""; + } + + return toDegrees(longitude) + " " + ((longitude < 0) ? "W" : "E"); + } + + private String toDegrees(double value) { + double absoluteDegrees = Math.abs(value); + int fullDegrees = (int) absoluteDegrees; + double remainingMinutes = (absoluteDegrees - fullDegrees) * 60; + int minutes = (int) remainingMinutes; + double remainingSeconds = (remainingMinutes - minutes) * 60; + int seconds = (int) remainingSeconds; + if (seconds == 60) { + minutes += 1; + seconds = 0; + } + if (minutes == 60) { + fullDegrees += 1; + minutes = 0; + } + return fullDegrees + "°" + minutes + "'" + seconds; + } +} diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/CoordinatesDialect.java b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/CoordinatesDialect.java new file mode 100644 index 0000000000000000000000000000000000000000..d443e589ca1c8e82c00960faeb9f15733db0e102 --- /dev/null +++ b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/CoordinatesDialect.java @@ -0,0 +1,28 @@ +package fr.inra.urgi.faidare.web.thymeleaf; + +import org.springframework.stereotype.Component; +import org.thymeleaf.dialect.AbstractDialect; +import org.thymeleaf.dialect.IDialect; +import org.thymeleaf.dialect.IExpressionObjectDialect; +import org.thymeleaf.expression.IExpressionObjectFactory; +import org.thymeleaf.extras.java8time.dialect.Java8TimeExpressionFactory; + +/** + * A thymeleaf dialect allowing to transform coordinates (latitude and longitude) + * to degrees. + * @author JB Nizet + */ +@Component +public class CoordinatesDialect extends AbstractDialect implements IExpressionObjectDialect { + + private final IExpressionObjectFactory COORDINATES_EXPRESSION_OBJECTS_FACTORY = new CoordinatesExpressionFactory(); + + protected CoordinatesDialect() { + super("coordinates"); + } + + @Override + public IExpressionObjectFactory getExpressionObjectFactory() { + return COORDINATES_EXPRESSION_OBJECTS_FACTORY; + } +} diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/CoordinatesExpressionFactory.java b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/CoordinatesExpressionFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..e3c25cc8577aae0cac73a894915d6c33edd6d8ca --- /dev/null +++ b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/CoordinatesExpressionFactory.java @@ -0,0 +1,36 @@ +package fr.inra.urgi.faidare.web.thymeleaf; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.thymeleaf.context.IExpressionContext; +import org.thymeleaf.expression.IExpressionObjectFactory; +import org.thymeleaf.extras.java8time.expression.Temporals; + +/** + * The object factory for the {@link CoordinatesDialect} + * @author JB Nizet + */ +public class CoordinatesExpressionFactory implements IExpressionObjectFactory { + private static final String COORDINATES_EVALUATION_VARIABLE_NAME = "coordinates"; + + private static final Set<String> ALL_EXPRESSION_OBJECT_NAMES = + Collections.singleton(COORDINATES_EVALUATION_VARIABLE_NAME); + + @Override + public Set<String> getAllExpressionObjectNames() { + return ALL_EXPRESSION_OBJECT_NAMES; + } + + @Override + public Object buildObject(IExpressionContext context, String expressionObjectName) { + return new Coordinates(context.getLocale()); + } + + @Override + public boolean isCacheable(String expressionObjectName) { + return true; + } +} diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareDialect.java b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareDialect.java new file mode 100644 index 0000000000000000000000000000000000000000..2a02a842a28ba411f8f6d16d6c5254e41a53524b --- /dev/null +++ b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareDialect.java @@ -0,0 +1,25 @@ +package fr.inra.urgi.faidare.web.thymeleaf; + +import org.springframework.stereotype.Component; +import org.thymeleaf.dialect.AbstractDialect; +import org.thymeleaf.dialect.IExpressionObjectDialect; +import org.thymeleaf.expression.IExpressionObjectFactory; + +/** + * A thymeleaf dialect allowing to perform various tasks in the template related to Faidare + * @author JB Nizet + */ +@Component +public class FaidareDialect extends AbstractDialect implements IExpressionObjectDialect { + + private final IExpressionObjectFactory FAIDARE_EXPRESSION_OBJECTS_FACTORY = new FaidareExpressionFactory(); + + protected FaidareDialect() { + super("faidare"); + } + + @Override + public IExpressionObjectFactory getExpressionObjectFactory() { + return FAIDARE_EXPRESSION_OBJECTS_FACTORY; + } +} diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressionFactory.java b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressionFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..e873375c1a7681d2837c0bf6591174d1aa557e3d --- /dev/null +++ b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressionFactory.java @@ -0,0 +1,33 @@ +package fr.inra.urgi.faidare.web.thymeleaf; + +import java.util.Collections; +import java.util.Set; + +import org.thymeleaf.context.IExpressionContext; +import org.thymeleaf.expression.IExpressionObjectFactory; + +/** + * The object factory for the {@link FaidareDialect} + * @author JB Nizet + */ +public class FaidareExpressionFactory implements IExpressionObjectFactory { + private static final String FAIDARE_EVALUATION_VARIABLE_NAME = "faidare"; + + private static final Set<String> ALL_EXPRESSION_OBJECT_NAMES = + Collections.singleton(FAIDARE_EVALUATION_VARIABLE_NAME); + + @Override + public Set<String> getAllExpressionObjectNames() { + return ALL_EXPRESSION_OBJECT_NAMES; + } + + @Override + public Object buildObject(IExpressionContext context, String expressionObjectName) { + return new FaidareExpressions(context.getLocale()); + } + + @Override + public boolean isCacheable(String expressionObjectName) { + return true; + } +} diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressions.java b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressions.java new file mode 100644 index 0000000000000000000000000000000000000000..9ba16d342d94e9097e720c5b3ad2e370d564fe6d --- /dev/null +++ b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressions.java @@ -0,0 +1,78 @@ +package fr.inra.urgi.faidare.web.thymeleaf; + +import java.nio.charset.StandardCharsets; +import java.text.DecimalFormat; +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.function.Function; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import fr.inra.urgi.faidare.domain.data.germplasm.CollPopVO; +import fr.inra.urgi.faidare.domain.data.germplasm.TaxonSourceVO; +import fr.inra.urgi.faidare.utils.Sites; +import org.apache.logging.log4j.util.Strings; + +/** + * The actual object offering Faidare helper methods to thymeleaf + * @author JB Nizet + */ +public class FaidareExpressions { + + private static final Map<String, Function<String, String>> TAXON_ID_URL_FACTORIES_BY_SOURCE_NAME = + createTaxonIdUrlFactories(); + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private static Map<String, Function<String, String>> createTaxonIdUrlFactories() { + Map<String, Function<String, String>> result = new HashMap<>(); + result.put("NCBI", s -> "https://www.ncbi.nlm.nih.gov/Taxonomy/Browser/wwwtax.cgi?mode=Info&id=" + s); + result.put("ThePlantList", s -> "http://www.theplantlist.org/tpl1.1/record/" + s); + result.put("TAXREF", s -> "https://inpn.mnhn.fr/espece/cd_nom/" + s); + result.put("CatalogueOfLife", s -> "http://www.catalogueoflife.org/col/details/species/id/" + s); + return Collections.unmodifiableMap(result); + } + + private final Locale locale; + + public FaidareExpressions(Locale locale) { + this.locale = locale; + } + + public String toSiteParam(String siteId) { + return Sites.siteIdToLocationId(siteId); + } + + public String collPopTitle(CollPopVO collPopVO) { + return collPopTitle(collPopVO, Function.identity()); + } + + public String collPopTitleWithoutUnderscores(CollPopVO collPopVO) { + return collPopTitle(collPopVO, s -> s.replace('_', ' ')); + } + + public String taxonIdUrl(TaxonSourceVO taxonSource) { + Function<String, String> urlFactory = + TAXON_ID_URL_FACTORIES_BY_SOURCE_NAME.get(taxonSource.getSourceName()); + return urlFactory != null ? urlFactory.apply(taxonSource.getTaxonId()) : null; + } + + public String toJson(Object value) { + try { + return objectMapper.writeValueAsString(value); + } + catch (JsonProcessingException e) { + throw new IllegalArgumentException(e); + } + } + + private String collPopTitle(CollPopVO collPopVO, Function<String, String> nameTransformer) { + if (Strings.isBlank(collPopVO.getType())) { + return nameTransformer.apply(collPopVO.getName()); + } else { + return nameTransformer.apply(collPopVO.getName()) + " (" + collPopVO.getType() + ")"; + } + } +} diff --git a/backend/src/main/main.iml b/backend/src/main/main.iml deleted file mode 100644 index 50f3d6bc79c58fa92df31bb59ab229d1e7c0c12e..0000000000000000000000000000000000000000 --- a/backend/src/main/main.iml +++ /dev/null @@ -1,39 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<module type="JAVA_MODULE" version="4"> - <component name="NewModuleRootManager" inherit-compiler-output="true"> - <exclude-output /> - <content url="file://$MODULE_DIR$"> - <sourceFolder url="file://$MODULE_DIR$/java" isTestSource="false" /> - </content> - <orderEntry type="inheritedJdk" /> - <orderEntry type="sourceFolder" forTests="false" /> - <orderEntry type="library" name="Gradle: io.swagger:swagger-annotations:1.5.21" level="project" /> - <orderEntry type="library" name="Gradle: org.springframework:spring-beans:5.1.4.RELEASE" level="project" /> - <orderEntry type="library" name="Gradle: org.springframework:spring-web:5.1.4.RELEASE" level="project" /> - <orderEntry type="library" name="Gradle: io.springfox:springfox-core:2.9.2" level="project" /> - <orderEntry type="library" name="Gradle: io.springfox:springfox-spring-web:2.9.2" level="project" /> - <orderEntry type="library" name="Gradle: javax.validation:validation-api:2.0.1.Final" level="project" /> - <orderEntry type="library" name="Gradle: com.google.guava:guava:27.0.1-jre" level="project" /> - <orderEntry type="library" name="Gradle: commons-collections:commons-collections:3.2.2" level="project" /> - <orderEntry type="library" name="Gradle: org.apache.httpcomponents:httpcore:4.4.10" level="project" /> - <orderEntry type="library" name="Gradle: org.elasticsearch.client:elasticsearch-rest-client:6.4.3" level="project" /> - <orderEntry type="library" name="Gradle: org.apache.httpcomponents:httpcore-nio:4.4.10" level="project" /> - <orderEntry type="library" name="Gradle: org.springframework.boot:spring-boot-autoconfigure:2.1.2.RELEASE" level="project" /> - <orderEntry type="library" name="Gradle: org.springframework:spring-context:5.1.4.RELEASE" level="project" /> - <orderEntry type="library" name="Gradle: com.fasterxml.jackson.core:jackson-databind:2.9.8" level="project" /> - <orderEntry type="library" name="Gradle: org.springframework.boot:spring-boot:2.1.2.RELEASE" level="project" /> - <orderEntry type="library" name="Gradle: org.springframework.security:spring-security-config:5.1.3.RELEASE" level="project" /> - <orderEntry type="library" name="Gradle: io.springfox:springfox-spi:2.9.2" level="project" /> - <orderEntry type="library" name="Gradle: io.springfox:springfox-swagger2:2.9.2" level="project" /> - <orderEntry type="library" name="Gradle: org.elasticsearch:elasticsearch:6.5.4" level="project" /> - <orderEntry type="library" name="Gradle: org.slf4j:slf4j-api:1.7.25" level="project" /> - <orderEntry type="library" name="Gradle: org.elasticsearch.client:elasticsearch-rest-high-level-client:6.5.4" level="project" /> - <orderEntry type="library" name="Gradle: org.elasticsearch:elasticsearch-core:6.5.4" level="project" /> - <orderEntry type="library" name="Gradle: org.springframework:spring-core:5.1.4.RELEASE" level="project" /> - <orderEntry type="library" name="Gradle: org.apache.tomcat.embed:tomcat-embed-core:9.0.14" level="project" /> - <orderEntry type="library" name="Gradle: com.opencsv:opencsv:4.4" level="project" /> - <orderEntry type="library" name="Gradle: org.apache.commons:commons-lang3:3.8.1" level="project" /> - <orderEntry type="library" name="Gradle: org.apache.lucene:lucene-join:7.5.0" level="project" /> - <orderEntry type="library" name="Gradle: com.fasterxml.jackson.core:jackson-annotations:2.9.0" level="project" /> - </component> -</module> \ No newline at end of file diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 26c55fc3839132b3f6aca48b40f00c061a6b180f..2b684f600c3e548456c9e68ef473116839440929 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -90,3 +90,11 @@ server: servlet: context-path: /faidare-dev +--- +spring: + profiles: + dev + resources: + static-locations: + - classpath:/static/ + - file:./web/build/dist/ diff --git a/backend/src/main/resources/static/assets/images/favicon.ico b/backend/src/main/resources/static/assets/images/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..7fc066f132eb58eeaf30b6259da59f5ed92aca8e Binary files /dev/null and b/backend/src/main/resources/static/assets/images/favicon.ico differ diff --git a/backend/src/main/resources/static/assets/images/logo.png b/backend/src/main/resources/static/assets/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..86b3ab108693a19cca5a05c95faebd8573e23389 Binary files /dev/null and b/backend/src/main/resources/static/assets/images/logo.png differ diff --git a/backend/src/main/resources/static/assets/images/marker-icon-blue.png b/backend/src/main/resources/static/assets/images/marker-icon-blue.png new file mode 100644 index 0000000000000000000000000000000000000000..e2e9f757f515ded172e6f72c3ce55bbe15579649 Binary files /dev/null and b/backend/src/main/resources/static/assets/images/marker-icon-blue.png differ diff --git a/backend/src/main/resources/static/assets/images/marker-icon-green.png b/backend/src/main/resources/static/assets/images/marker-icon-green.png new file mode 100644 index 0000000000000000000000000000000000000000..6b4fb278f611f802d0c4e9cf88edad80e1e1c975 Binary files /dev/null and b/backend/src/main/resources/static/assets/images/marker-icon-green.png differ diff --git a/backend/src/main/resources/static/assets/images/marker-icon-purple.png b/backend/src/main/resources/static/assets/images/marker-icon-purple.png new file mode 100644 index 0000000000000000000000000000000000000000..63e423d250842ad5c9666507a4dd843a8f6a2b93 Binary files /dev/null and b/backend/src/main/resources/static/assets/images/marker-icon-purple.png differ diff --git a/backend/src/main/resources/static/assets/images/marker-icon-red.png b/backend/src/main/resources/static/assets/images/marker-icon-red.png new file mode 100644 index 0000000000000000000000000000000000000000..e3c0026ef246271f89aad81b6cf47b2ff63596d9 Binary files /dev/null and b/backend/src/main/resources/static/assets/images/marker-icon-red.png differ diff --git a/backend/src/main/resources/templates/fragments/institute.html b/backend/src/main/resources/templates/fragments/institute.html new file mode 100644 index 0000000000000000000000000000000000000000..b2e8da3ad2ef0e0d12bccaabfed479d893a2631c --- /dev/null +++ b/backend/src/main/resources/templates/fragments/institute.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> + +<html xmlns:th="http://www.thymeleaf.org"> + +<body> + +<!-- +Reusable fragment displaying the content of an institute popover. +Its unique argument (institute) is an InstituteVO +--> + +<th:block th:fragment="institute(institute)"> + <div class="text-center py-2" th:if="${institute.logo}"> + <img class="img-fluid" th:src="${institute.logo}" /> + </div> + <div th:replace="fragments/row::text-row(label='Code', text=${institute.instituteCode})"></div> + <div th:replace="fragments/row::text-row(label='Acronym', text=${institute.acronym})"></div> + <div th:replace="fragments/row::text-row(label='Organization', text=${institute.organisation})"></div> + <div th:replace="fragments/row::text-row(label='Type', text=${institute.instituteType})"></div> + <div th:replace="fragments/row::text-row(label='Address', text=${institute.address})"></div> + + <th:block th:if="${institute.webSite}"> + <div th:replace="fragments/row::row(label='Website', content=~{::.institute-website})"> + <a class="institute-website" + target="_blank" + th:href="${institute.webSite}" + th:text="${#strings.abbreviate(institute.webSite, 25)}"></a> + </div> + </th:block> +</th:block> + +</body> + +</html> diff --git a/backend/src/main/resources/templates/fragments/link.html b/backend/src/main/resources/templates/fragments/link.html new file mode 100644 index 0000000000000000000000000000000000000000..d7c43bbdb422c31c5bce647d302c803270672669 --- /dev/null +++ b/backend/src/main/resources/templates/fragments/link.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> + +<html xmlns:th="http://www.thymeleaf.org"> + +<body> +<!-- +Reusable fragment displaying a link with a label if the provided url is not +empty, or a span with the label if the provided url is empty. +Both arguments are strings. +--> +<th:block th:fragment="link(label, url)"> + <a th:unless="${#strings.isEmpty(url)}" + th:href="${url}" + th:text="${label}"></a> + <span th:if="${#strings.isEmpty(url)}" th:text="${label}"></span> +</th:block> + +</body> + +</html> diff --git a/backend/src/main/resources/templates/fragments/map.html b/backend/src/main/resources/templates/fragments/map.html new file mode 100644 index 0000000000000000000000000000000000000000..35ebed333c5c5eaf55160f400e5ebde01313cf24 --- /dev/null +++ b/backend/src/main/resources/templates/fragments/map.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> + +<html xmlns:th="http://www.thymeleaf.org"> + +<body> +<!-- +Reusable fragment displaying a map and its legend. +The map is initially hidden. The JavaScript displays it if there are locations +to display +--> +<div th:fragment="map" id="map-container" class="d-none"> + <div id="map" class="border rounded"></div> + <div class="map-legend mt-1 small"> + <img th:src="@{/assets/images/marker-icon-red.png}" id="red"/> + <label for="red" class="me-2">Origin site</label> + <img th:src="@{/assets/images/marker-icon-blue.png}" id="blue"/> + <label for="blue" class="me-2">Collecting site</label> + <img th:src="@{/assets/images/marker-icon-green.png}" id="green"/> + <label for="green" class="me-2">Evaluation site</label> + <img th:src="@{/assets/images/marker-icon-purple.png}" id="purple"/> + <label for="purple">Multi-purpose site</label> + </div> +</div> diff --git a/backend/src/main/resources/templates/fragments/row.html b/backend/src/main/resources/templates/fragments/row.html new file mode 100644 index 0000000000000000000000000000000000000000..8676b5906b77ae97d6e8c2ca826839450ce28842 --- /dev/null +++ b/backend/src/main/resources/templates/fragments/row.html @@ -0,0 +1,50 @@ +<!DOCTYPE html> + +<html xmlns:th="http://www.thymeleaf.org"> + +<body> + +<!-- +Reusable fragment displaying a responsive row containing a label and a content. +The label argument is a string. +The content argument is a fragment which is displayed at the right of the label. + +Note that `th:if` is not evaluated when th:replace is used. So if this row must +be displayed only if some condition is true, the fragment should be enclosed +into a block with the condition: + <th:block th:if="${someCondition}"> + <div th:replace="fragments/row::row(label='Some label', content=~{::#some-content-id})"> + <span id="some-content-id">the content here</span> + </div> + </th:block> +--> + +<div th:fragment="row(label, content)" class="row f-row"> + <div class="col-md-4 label pb-1 pb-md-0" th:text="${label}"></div> + <div class="col"> + <th:block th:replace="${content}" /> + </div> +</div> + +<!-- +Reusable fragment displaying a responsive row containing a label and a textual content. +The label argument is a string. +The text argument is a string which is displayed at the right of the label. +The whole row is omitted if the textual content is empty, so the caller does not +need to test that condition. + +Note that `th:if` is not evaluated when th:replace is used. So if this row must +be displayed only if some other condition is true, the fragment should be enclosed +into a block with the condition: + <th:block th:if="${someCondition}"> + <div th:replace="fragments/row::text-row(label='Some label', text=${someTextExpression})"></div> + </th:block> +--> +<div th:fragment="text-row(label, text)" th:unless="${#strings.isEmpty(text)}" class="row f-row"> + <div class="col-md-4 label pb-1 pb-md-0" th:text="${label}"></div> + <div class="col" th:text="${text}"></div> +</div> + +</body> + +</html> diff --git a/backend/src/main/resources/templates/fragments/source.html b/backend/src/main/resources/templates/fragments/source.html new file mode 100644 index 0000000000000000000000000000000000000000..b09638810885559be62d0c5448adecd617dbe2fb --- /dev/null +++ b/backend/src/main/resources/templates/fragments/source.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> + +<html xmlns:th="http://www.thymeleaf.org"> + +<body> + +<!-- +Reusable fragment displaying the source and the data links of an entity (site, study or germplasm). +The source argument is a DataSource. +The url argument is a string, which is the URL of the entity. +The entityType argument is a string, which is used in the message +"Link to this <entityType>". +--> + +<th:block th:fragment="source(source, url, entityType)"> + <th:block th:if="${source != null}"> + <div th:replace="fragments/row::row(label='Source', content=~{::.source})"> + <a class="source" target="_blank" th:href="${source.url}"> + <img class="img-fluid" style="max-height: 60px;" th:src="${source.image}" th:alt="${source.name} + ' logo'" /> + </a> + </div> + </th:block> + + <th:block th:if="${url != null && source != null}"> + <div th:replace="fragments/row::row(label='Data link', content=~{::.source-url})"> + <a class="source-url" target="_blank" th:href="${url}"> + Link to this <span th:text="${entityType}"></span> on <th:block th:text="${source.name}" /> + </a> + </div> + </th:block> +</th:block> + +</body> + +</html> diff --git a/backend/src/main/resources/templates/fragments/xrefs.html b/backend/src/main/resources/templates/fragments/xrefs.html new file mode 100644 index 0000000000000000000000000000000000000000..c60310b69c5d852b7a636d9ab7392d0a52d2b553 --- /dev/null +++ b/backend/src/main/resources/templates/fragments/xrefs.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> + +<html xmlns:th="http://www.thymeleaf.org"> + +<body> + +<!-- +Reusable fragment displaying a cross references section, with its title. +The unique argument (crossReferences) is a List<XRefDocumentVO> +--> + +<div class="f-card" th:fragment="xrefs(crossReferences)" th:if="${!#lists.isEmpty(crossReferences)}"> + <h2>Cross references</h2> + <div class="f-card-body"> + <div class="scroll-table-container scroll-table-container-big"> + <table class="table table-sm table-striped table-sticky table-responsive-sm"> + <thead> + <tr> + <th scope="col">Name</th> + <th scope="col">Source</th> + <th scope="col">Type</th> + <th scope="col">Description</th> + </tr> + </thead> + <tbody> + <tr th:each="crossRef : ${crossReferences}"> + <td><a th:href="${crossRef.url}" target="_blank" th:text="${crossRef.name}"></a></td> + <td th:text="${crossRef.databaseName}"></td> + <td th:text="${crossRef.entryType}"></td> + <td style="min-width: 30rem;" th:text="${#strings.abbreviate(crossRef.description, 120)}"></td> + </tr> + </tbody> + </table> + </div> + </div> +</div> + +</body> + +</html> diff --git a/backend/src/main/resources/templates/germplasm.html b/backend/src/main/resources/templates/germplasm.html new file mode 100644 index 0000000000000000000000000000000000000000..f5c6a4ce2fdfa2f12bd1e2826f01dfea0c0fa48c --- /dev/null +++ b/backend/src/main/resources/templates/germplasm.html @@ -0,0 +1,452 @@ +<!DOCTYPE html> + +<html + xmlns:th="http://www.thymeleaf.org" + th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main}, script=~{::script})}" +> +<head> + <title>Germplasm: <th:block th:text="${model.germplasm.germplasmName}" /></title> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> +</head> + +<body> +<main> + <div class="d-flex"> + <h1 class="flex-grow-1">Germplasm: <th:block th:text="${model.germplasm.germplasmName}" /></h1> + <div th:if="${model.germplasm.holdingGenbank != null && model.germplasm.holdingGenbank.logo != null}"> + <img th:src="${model.germplasm.holdingGenbank.logo}" th:alt="${model.germplasm.holdingGenbank.instituteName}" /> + </div> + </div> + + <div th:replace="fragments/map::map"></div> + + <div class="row align-items-center justify-content-center mt-4"> + <div class="col-auto field" th:if="${model.germplasm.photo != null && model.germplasm.photo.thumbnailFile != null}"> + <template id="photo-popover"> + <div class="card"> + <img th:src="${model.germplasm.photo.file}" class="card-img-top" alt=""> + <div class="card-body"> + <div th:replace="fragments/row::text-row(label='Accession name', text=${model.germplasm.germplasmName})"></div> + <div th:replace="fragments/row::text-row(label='Photo name', text=${model.germplasm.photo.photoName})"></div> + <div th:replace="fragments/row::text-row(label='Description', text=${model.germplasm.photo.description})"></div> + <div th:replace="fragments/row::text-row(label='Copyright', text=${model.germplasm.photo.copyright})"></div> + </div> + </div> + </template> + + <a role="button" + class="d-flex flex-column align-items-center" + data-bs-toggle="popover" + tabindex="0" + th:data-bs-title="${model.germplasm.photo.photoName}" + data-bs-element="#photo-popover" + data-bs-container="body" + data-bs-trigger="focus"> + <img th:src="${model.germplasm.photo.thumbnailFile}" class="img-fluid" /> + + <figcaption class="figure-caption"> + © <span th:text="${model.germplasm.photo.copyright}"></span> + </figcaption> + </a> + </div> + + <div class="col-12 col-lg"> + <div class="f-card"> + <h2>Identification</h2> + + <div class="f-card-body"> + <div th:replace="fragments/row::text-row(label='Germplasm name', text=${model.germplasm.germplasmName})"></div> + <div th:replace="fragments/row::text-row(label='Accession number', text=${model.germplasm.accessionNumber})"></div> + + <div th:replace="fragments/source::source(source=${model.source}, url=${model.germplasm.url}, entityType='germplasm')"></div> + + <th:block th:unless="${#lists.isEmpty(model.germplasm.synonyms)}"> + <div th:replace="fragments/row::row(label='Accession synonyms', content=~{::#accession-synonyms})"> + <div id="accession-synonyms" class="content-overflow" th:text="${#strings.listJoin(model.germplasm.synonyms, ', ')}"></div> + </div> + </th:block> + + <th:block th:unless="${#strings.isEmpty(model.taxon)}"> + <div th:replace="fragments/row::row(label='Taxon', content=~{::#taxon})"> + <div id="taxon"> + <template id="taxon-popover"> + <th:block th:unless="${#strings.isEmpty(model.germplasm.genus)}"> + <div th:replace="fragments/row::row(label='Genus', content=~{::#taxon-genus})"> + <em id="taxon-genus" th:text="${model.germplasm.genus}"></em> + </div> + </th:block> + <th:block th:unless="${#strings.isEmpty(model.germplasm.species)}"> + <div th:replace="fragments/row::row(label='Species', content=~{::#taxon-species})"> + <span id="taxon-species"> + <em th:text="${model.germplasm.species}"></em> + <span th:unless="${#strings.isEmpty(model.germplasm.speciesAuthority)}" + th:text="${'(' + model.germplasm.speciesAuthority + ')'}"></span> + </span> + </div> + </th:block> + <th:block th:unless="${#strings.isEmpty(model.germplasm.subtaxa)}"> + <div th:replace="fragments/row::row(label='Subtaxa', content=~{::#taxon-subtaxa})"> + <span id="taxon-subtaxa"> + <em th:text="${model.germplasm.subtaxa}"></em> + <span th:unless="${#strings.isEmpty(model.germplasm.subtaxaAuthority)}" + th:text="${'(' + model.germplasm.subtaxaAuthority + ')'}"></span> + </span> + </div> + </th:block> + + <div th:replace="fragments/row::text-row(label='Authority', text=${model.taxonAuthor})"></div> + + <th:block th:unless="${#lists.isEmpty(model.germplasm.taxonIds)}"> + <div th:replace="fragments/row::row(label='Taxon IDs', content=~{::#taxon-ids})"> + <div id="taxon-ids"> + <div th:each="taxonId : ${model.germplasm.taxonIds}" class="row"> + <div class="col-6 text-nowrap" th:text="${taxonId.sourceName}"></div> + <div class="col-6"> + <span class="taxon-id" + th:replace="fragments/link::link(label=${taxonId.taxonId}, url=${#faidare.taxonIdUrl(taxonId)})"></span> + </div> + </div> + </div> + </div> + </th:block> + + <div th:replace="fragments/row::text-row(label='Comment', text=${model.germplasm.taxonComment})"></div> + <th:block th:unless="${#lists.isEmpty(model.germplasm.taxonCommonNames)}"> + <div th:replace="fragments/row::row(label='Taxon common names', content=~{::#taxon-common-names})"> + <div id="taxon-common-names" class="content-overflow" th:text="${#strings.listJoin(model.germplasm.taxonCommonNames, ', ')}"></div> + </div> + </th:block> + <th:block th:unless="${#lists.isEmpty(model.germplasm.taxonSynonyms)}"> + <div th:replace="fragments/row::row(label='Taxon common names', content=~{::#taxon-synonyms})"> + <div id="taxon-synonyms" class="content-overflow" th:text="${#strings.listJoin(model.germplasm.taxonSynonyms, ', ')}"></div> + </div> + </th:block> + </template> + <a role="button" + tabindex="0" + data-bs-toggle="popover" + th:data-bs-title="${model.taxon}" + data-bs-element="#taxon-popover" + data-bs-container="body" + data-bs-trigger="focus"> + <em th:text="${model.taxon}"></em> + <th:block th:unless="${#strings.isEmpty(model.taxonAuthor)}">(<span th:text="${model.taxonAuthor}"></span>)</th:block> + </a> + </div> + </div> + </th:block> + + <div th:replace="fragments/row::text-row(label='Biological status', text=${model.germplasm.biologicalStatusOfAccessionCode})"></div> + <div th:replace="fragments/row::text-row(label='Genetic nature', text=${model.germplasm.geneticNature})"></div> + <div th:replace="fragments/row::text-row(label='Seed source', text=${model.germplasm.seedSource})"></div> + <div th:replace="fragments/row::text-row(label='Pedigree', text=${model.germplasm.pedigree})"></div> + <div th:replace="fragments/row::text-row(label='Comments', text=${model.germplasm.comment})"></div> + + <th:block th:if="${model.germplasm.originSite != null && !#strings.isEmpty(model.germplasm.originSite.siteName)}"> + <div th:replace="fragments/row::row(label='Origin site', content=~{::#origin-site})"> + <a id="origin-site" th:href="@{/sites/{siteId}(siteId=${#faidare.toSiteParam(model.germplasm.originSite.siteId)})}" th:text="${model.germplasm.originSite.siteName}"></a> + </div> + </th:block> + </div> + </div> + </div> + </div> + + <div class="f-card" th:if="${model.germplasm.holdingInstitute}"> + <h2>Depositary</h2> + <div class="f-card-body"> + <template id="holding-institute-popover"> + <div th:replace="fragments/institute::institute(institute=${model.germplasm.holdingInstitute})"></div> + </template> + <div th:replace="fragments/row::row(label='Institution', content=~{::#institution})"> + <a id="institution" + role="button" + tabindex="0" + data-bs-toggle="popover" + th:data-bs-title="${model.germplasm.holdingInstitute.instituteName}" + data-bs-element="#holding-institute-popover" + data-bs-container="body" + data-bs-trigger="focus" + th:text="${model.germplasm.holdingInstitute.instituteName}"></a> + </div> + + <th:block th:if="${model.germplasm.holdingGenbank != null && !#strings.isEmpty(model.germplasm.holdingGenbank.instituteName) && !#strings.isEmpty(model.germplasm.holdingGenbank.webSite)}"> + <div th:replace="fragments/row::row(label='Stock center name', content=~{::#stock-center-name})"> + <a id="stock-center-name" + target="_blank" + th:href="${model.germplasm.holdingGenbank.webSite}" + th:text="${model.germplasm.holdingGenbank.instituteName}"></a> + </div> + </th:block> + + <div th:replace="fragments/row::text-row(label='Presence status', text=${model.germplasm.presenceStatus})"></div> + </div> + </div> + + <div class="f-card" th:if="${model.collecting}"> + <h2>Collector</h2> + <div class="f-card-body"> + <th:block th:if="${model.germplasm.collectingSite != null && !#strings.isEmpty(model.germplasm.collectingSite.siteName)}"> + <div th:replace="fragments/row::row(label='Collecting site', content=~{::#collecting-site})"> + <a id="collecting-site" + th:href="@{/sites/{siteId}(siteId=${#faidare.toSiteParam(model.germplasm.collectingSite.siteId)})}" + th:text="${model.germplasm.collectingSite.siteName}" + ></a> + </div> + </th:block> + + <div th:replace="fragments/row::text-row(label='Material type', text=${model.germplasm.collector.materialType})"></div> + <div th:replace="fragments/row::text-row(label='Collectors', text=${model.germplasm.collector.collectors})"></div> + + <th:block th:if="${!#strings.isEmpty(model.germplasm.acquisitionDate) && model.germplasm.collector.accessionCreationDate == null}"> + <div th:replace="fragments/row::text-row(label='Acquisition / Creation date', text=${model.germplasm.acquisitionDate})"></div> + </th:block> + + <th:block th:if="${model.germplasm.collector.institute != null && !#strings.isEmpty(model.germplasm.collector.institute.instituteName)}"> + <template id="collector-institute-popover"> + <div th:replace="fragments/institute::institute(institute=${model.germplasm.collector.institute})"></div> + </template> + <div th:replace="fragments/row::row(label='Institution', content=~{::#collecting-institution})"> + <a id="collecting-institution" + role="button" + tabindex="0" + data-bs-toggle="popover" + th:data-bs-title="${model.germplasm.collector.institute.instituteName}" + data-bs-element="#collector-institute-popover" + data-bs-container="body" + data-bs-trigger="focus" + th:text="${model.germplasm.collector.institute.instituteName}"></a> + </div> + </th:block> + + <div th:replace="fragments/row::text-row(label='Accession number', text=${model.germplasm.collector.accessionNumber})"></div> + </div> + </div> + + <div class="f-card" th:if="${model.breeding}"> + <h2>Breeder</h2> + <div class="f-card-body"> + <th:block th:if="${model.germplasm.breeder.institute != null && !#strings.isEmpty(model.germplasm.breeder.institute.instituteName)}"> + <template id="breeder-institute-popover"> + <div th:replace="fragments/institute::institute(institute=${model.germplasm.breeder.institute})"></div> + </template> + <div th:replace="fragments/row::row(label='Institute', content=~{::#breeding-institution})"> + <a id="breeding-institution" + role="button" + tabindex="0" + data-bs-toggle="popover" + th:data-bs-title="${model.germplasm.breeder.institute.instituteName}" + data-bs-element="#breeder-institute-popover" + data-bs-container="body" + data-bs-trigger="focus" + th:text="${model.germplasm.breeder.institute.instituteName}"></a> + </div> + </th:block> + + <div th:replace="fragments/row::text-row(label='Accession creation year', text=${model.germplasm.breeder.accessionCreationDate})"></div> + <div th:replace="fragments/row::text-row(label='Accession number', text=${model.germplasm.breeder.accessionNumber})"></div> + <div th:replace="fragments/row::text-row(label='Catalog registration year', text=${model.germplasm.breeder.registrationYear})"></div> + <div th:replace="fragments/row::text-row(label='Catalog deregistration year', text=${model.germplasm.breeder.deregistrationYear})"></div> + </div> + </div> + + <div class="f-card" th:unless="${#lists.isEmpty(model.germplasm.donors)}"> + <h2>Donors</h2> + <div class="f-card-body"> + <div class="scroll-table-container"> + <table class="table table-sm table-striped table-sticky table-responsive-sm"> + <thead> + <tr> + <th scope="col">Institute name</th> + <th scope="col">Institute code</th> + <th scope="col">Donation date</th> + <th scope="col">Accession number</th> + <th scope="col">Accession PUI</th> + </tr> + </thead> + <tbody> + <tr th:each="row, donorIterStat : ${model.germplasm.donors}"> + <td> + <template th:id="${'donor-institute-popover-' + donorIterStat.index}"> + <div th:replace="fragments/institute::institute(institute=${row.donorInstitute})"></div> + </template> + <a role="button" + tabindex="0" + data-bs-toggle="popover" + th:data-bs-title="${row.donorInstitute.instituteName}" + th:data-bs-element="${'#donor-institute-popover-' + donorIterStat.index}" + data-bs-container="body" + data-bs-trigger="focus" + th:text="${row.donorInstitute.instituteName}"></a> + </td> + <td th:text="${row.donorInstituteCode}"></td> + <td th:text="${row.donationDate}"></td> + <td th:text="${row.donorAccessionNumber}"></td> + <td th:text="${row.donorGermplasmPUI}"></td> + </tr> + </tbody> + </table> + </div> + </div> + </div> + + <div class="f-card" th:unless="${#lists.isEmpty(model.germplasm.distributors)}"> + <h2>Distributors</h2> + <div class="f-card-body"> + <div class="scroll-table-container"> + <table class="table table-sm table-striped table-sticky table-responsive-sm"> + <thead> + <tr> + <th scope="col">Institute</th> + <th scope="col">Accession number</th> + <th scope="col">Distribution status</th> + </tr> + </thead> + <tbody> + <tr th:each="row, distributorIterStat : ${model.germplasm.distributors}"> + <td> + <template th:id="${'distributor-institute-popover-' + distributorIterStat.index}"> + <div th:replace="fragments/institute::institute(institute=${row.institute})"></div> + </template> + <a role="button" + tabindex="0" + th:data-bs-title="${row.institute.instituteName}" + th:data-bs-element="${'#distributor-institute-popover-' + distributorIterStat.index}" + data-bs-container="body" + data-bs-trigger="focus" + th:text="${row.institute.instituteName}"></a> + </td> + <td th:text="${row.accessionNumber}"></td> + <td th:text="${row.distributionStatus}"></td> + </tr> + </tbody> + </table> + </div> + </div> + </div> + + <div class="f-card" th:unless="${#lists.isEmpty(model.attributes)}"> + <h2>Evaluation Data</h2> + <div class="f-card-body"> + <th:block th:each="descriptor : ${model.attributes}"> + <div th:replace="fragments/row::text-row(label=${descriptor.attributeName}, text=${descriptor.value})"></div> + </th:block> + </div> + </div> + + <div class="f-card" th:if="${model.genealogyPresent}"> + <h2>Genealogy</h2> + <div class="f-card-body"> + <th:block th:if="${model.pedigree != null}"> + <div th:replace="fragments/row::text-row(label='Crossing plan', text=${model.pedigree.crossingPlan})"></div> + <div th:replace="fragments/row::text-row(label='Crossing year', text=${model.pedigree.crossingYear})"></div> + <div th:replace="fragments/row::text-row(label='Family code', text=${model.pedigree.familyCode})"></div> + <th:block th:unless="${#strings.isEmpty(model.pedigree.parent1Name)}"> + <div th:replace="fragments/row::row(label='Parent accessions', content=~{::#parent-accessions})"> + <div id="parent-accessions"> + <th:block th:if="${model.pedigree.parent1DbId}"> + <div th:replace="fragments/row::row(label=${model.pedigree.parent1Type}, content=~{::#parent1-link})"> + <a id="parent1-link" th:href="@{/germplasms/{germplasmId}(germplasmId=${model.pedigree.parent1DbId})}" th:text="${model.pedigree.parent1Name}"></a> + </div> + </th:block> + + <th:block th:if="${model.pedigree.parent2DbId}"> + <div th:replace="fragments/row::row(label=${model.pedigree.parent2Type}, content=~{::#parent2-link})"> + <a id="parent2-link" th:href="@{/germplasms/{germplasmId}(germplasmId=${model.pedigree.parent2DbId})}" th:text="${model.pedigree.parent2Name}"></a> + </div> + </th:block> + </div> + </div> + </th:block> + + <th:block th:unless="${#lists.isEmpty(model.pedigree.siblings)}"> + <div th:replace="fragments/row::row(label='Sibling accessions', content=~{::#sibling-accessions})"> + <div id="sibling-accessions" class="content-overflow"> + <a th:each="sibling : ${model.pedigree.siblings}" + th:href="@{/germplasms/{germplasmId}(germplasmId=${sibling.germplasmDbId})}" + th:text="${sibling.defaultDisplayName}"></a> + </div> + </div> + </th:block> + </th:block> + + <th:block th:unless="${#lists.isEmpty(model.germplasm.children)}"> + <div th:replace="fragments/row::row(label='Descendants', content=~{::#descendants})"> + <div id="descendants" class="content-overflow content-overflow-big"> + <th:block th:each="child : ${model.germplasm.children}"> + <div th:replace="fragments/row::row(label=${#strings.isEmpty(child.secondParentName) ? ('children of ' + child.firstParentName) : ('children of ' + child.firstParentName + ' x ' + child.secondParentName) }, content=~{::.descendant-child})"> + <div class="descendant-child"> + <th:block th:each="sibling, siblingIterStat : ${child.sibblings}"> + <a th:href="@{/germplasms(pui=${sibling.pui})}" + th:text="${sibling.name}"></a><th:block th:unless="${siblingIterStat.last}">, </th:block> + </th:block> + </div> + </div> + </th:block> + </div> + </div> + </th:block> + </div> + </div> + + <div class="f-card" th:unless="${#lists.isEmpty(model.germplasm.population)}"> + <h2>Population</h2> + <div class="f-card-body"> + <th:block th:each="population : ${model.germplasm.population}"> + + <th:block th:if="${population.germplasmRef != null}"> + <th:block th:unless="${#strings.isEmpty(population.germplasmRef.pui)}"> + <div th:replace="fragments/row::row(label=${#faidare.collPopTitle(population)}, content=~{::.population-1})"> + <div class="population-1"> + <a th:if="${population.germplasmRef.pui != model.germplasm.germplasmPUI}" + th:href="@{/germplasms(pui=${population.germplasmRef.pui})}" + th:text="${population.germplasmRef.name}"></a> + <span th:if="${population.germplasmRef.pui == model.germplasm.germplasmPUI}" + th:text="${population.germplasmRef.name}"></span> + is composed by <span th:text="${population.germplasmCount}"></span> accession(s) + <!-- TODO there was a link pointing at a search here --> + </div> + </div> + </th:block> + </th:block> + + <th:block th:if="${population.germplasmRef == null}"> + <div th:replace="fragments/row::text-row(label=${#faidare.collPopTitle(population)}, text=${population.germplasmCount + ' accession(s)'})"></div> + <!-- TODO there was a link pointing at a search here --> + </th:block> + </th:block> + </div> + </div> + + <div class="f-card" th:unless="${#lists.isEmpty(model.germplasm.collection)}"> + <h2>Collection</h2> + <div class="f-card-body"> + <th:block th:each="collection : ${model.germplasm.collection}"> + <div th:replace="fragments/row::text-row(label=${#faidare.collPopTitle(collection)}, text=${collection.germplasmCount + ' accession(s)'})"></div> + <!-- TODO there was a link pointing at a search here --> + </th:block> + </div> + </div> + + <div class="f-card" th:unless="${#lists.isEmpty(model.germplasm.panel)}"> + <h2>Panel</h2> + <div class="f-card-body"> + <th:block th:each="panel : ${model.germplasm.panel}"> + <div th:replace="fragments/row::text-row(label=${#faidare.collPopTitleWithoutUnderscores(panel)}, text=${panel.germplasmCount + ' accession(s)'})"></div> + <!-- TODO there was a link pointing at a search here --> + </th:block> + </div> + </div> + + <div th:replace="fragments/xrefs::xrefs(crossReferences=${model.crossReferences})"></div> +</main> + +<script th:inline="javascript"> + faidare.initializePopovers(); + faidare.initializeMap({ + contextPath: [[${#request.getContextPath()}]], + locations: /*[[${model.mapLocations}]]*/ [] + }); +</script> +</body> +</html> diff --git a/backend/src/main/resources/templates/index.html b/backend/src/main/resources/templates/index.html new file mode 100644 index 0000000000000000000000000000000000000000..49f401b0284647d171f51ec5643987e27d836a05 --- /dev/null +++ b/backend/src/main/resources/templates/index.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> + +<html + xmlns:th="http://www.thymeleaf.org" + th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main})}" +> +<head> + <title>Faidare</title> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> +</head> + +<body> +<main> + <h1>Welcome to Faidare</h1> +</main> +</body> +</html> diff --git a/backend/src/main/resources/templates/layout/main.html b/backend/src/main/resources/templates/layout/main.html new file mode 100644 index 0000000000000000000000000000000000000000..22c36a5efb6dfe860a20b41f520ba826d160e96b --- /dev/null +++ b/backend/src/main/resources/templates/layout/main.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<html lang="fr" th:fragment="layout (title, content, script)" xmlns:th="http://www.thymeleaf.org"> + <head> + <title th:replace="${title}">Layout Title</title> + + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> + <meta content="width=device-width, initial-scale=1" name="viewport" /> + + <link th:href="@{/assets/style.css}" rel="stylesheet"> + + <link rel="shortcut icon" th:href="@{/static/assets/images/favicon.ico}" type="image/x-icon" /> + </head> + + <body> + <nav class="navbar navbar-expand-lg navbar-light bg-light"> + <div class="container"> + <span class="navbar-brand py-0"> + <img th:src="@{/assets/images/logo.png}" style="height: 40px"/> + </span> + </div> + </nav> + <div class="container mt-3"> + <div th:replace="${content}"> + <p>Layout content</p> + </div> + </div> + <script type="text/javascript" th:src="@{/assets/script.js}"></script> + <script type="text/javascript" th:replace="${script}"></script> + </body> +</html> diff --git a/backend/src/main/resources/templates/site.html b/backend/src/main/resources/templates/site.html new file mode 100644 index 0000000000000000000000000000000000000000..69deac41e56ee63e84e22618db267da846f230f1 --- /dev/null +++ b/backend/src/main/resources/templates/site.html @@ -0,0 +1,78 @@ +<!DOCTYPE html> + +<html + xmlns:th="http://www.thymeleaf.org" + th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main}, script=~{::script})}" +> +<head> + <title>Site <th:block th:text="${model.site.locationName}" /></title> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> +</head> + +<body> +<main> + <h1>Site <th:block th:text="${model.site.locationName}" /></h1> + + <div th:replace="fragments/map::map"></div> + + <div class="f-card mt-4"> + <h2>Details</h2> + <div class="f-card-body"> + <th:block th:if="${model.site.uri != null && !model.site.uri.startsWith('urn:')}"> + <div th:replace="fragments/row::text-row(label='Permanent unique identifier', text=${model.site.uri})"></div> + </th:block> + + <div th:replace="fragments/source::source(source=${model.source}, url=${model.site.url}, entityType='site')"></div> + + <div th:replace="fragments/row::text-row(label='Abbreviation', text=${model.site.abbreviation})"></div> + <div th:replace="fragments/row::text-row(label='Type', text=${model.site.locationType})"></div> + <div th:replace="fragments/row::text-row(label='Status', text=${model.siteStatus})"></div> + <div th:replace="fragments/row::text-row(label='Institution/Landowner', text=${model.site.instituteName})"></div> + <div th:replace="fragments/row::text-row(label='Institution address', text=${model.site.instituteAddress})"></div> + <div th:replace="fragments/row::text-row(label='Coordinates precision', text=${model.coordinatesPrecision})"></div> + <th:block th:if="${model.site.latitude}"> + <div th:replace="fragments/row::text-row(label='Latitude', text=${#coordinates.formatLatitude(model.site.latitude)})"></div> + </th:block> + <th:block th:if="${model.site.longitude}"> + <div th:replace="fragments/row::text-row(label='Longitude', text=${#coordinates.formatLongitude(model.site.longitude)})"></div> + </th:block> + <div th:replace="fragments/row::text-row(label='Geographical location', text=${model.geographicalLocation})"></div> + <th:block th:if="${model.site.countryName != null && model.geographicalLocation == null}"> + <div th:replace="fragments/row::text-row(label='Country name', text=${model.site.countryName})"></div> + </th:block> + + <th:block th:if="${model.site.countryCode != null && model.geographicalLocation == null}"> + <div th:replace="fragments/row::text-row(label='Country code', text=${model.site.countryName})"></div> + </th:block> + + <div th:replace="fragments/row::text-row(label='Altitude', text=${model.site.altitude})"></div> + <div th:replace="fragments/row::text-row(label='Slope', text=${model.slope})"></div> + <div th:replace="fragments/row::text-row(label='Exposure', text=${model.exposure})"></div> + <div th:replace="fragments/row::text-row(label='Topography', text=${model.topography})"></div> + <div th:replace="fragments/row::text-row(label='Environment type', text=${model.environmentType})"></div> + <div th:replace="fragments/row::text-row(label='Distance to city', text=${model.distanceToCity})"></div> + <div th:replace="fragments/row::text-row(label='Direction from city', text=${model.directionFromCity})"></div> + <div th:replace="fragments/row::text-row(label='Comment', text=${model.comment})"></div> + </div> + </div> + + <div class="f-card" th:unless="${#lists.isEmpty(model.additionalInfoProperties)}"> + <h2>Additional info</h2> + <div class="f-card-body"> + <th:block th:each="prop : ${model.additionalInfoProperties}"> + <div th:replace="fragments/row::text-row(label=${prop.key}, text=${prop.value})"></div> + </th:block> + </div> + </div> + + <div th:replace="fragments/xrefs::xrefs(crossReferences=${model.crossReferences})"></div> +</main> + +<script th:inline="javascript"> + faidare.initializeMap({ + contextPath: [[${#request.getContextPath()}]], + locations: /*[[${model.mapLocations}]]*/ [] + }); +</script> +</body> +</html> diff --git a/backend/src/main/resources/templates/study.html b/backend/src/main/resources/templates/study.html new file mode 100644 index 0000000000000000000000000000000000000000..5d26f8340dd27dd0bb2d1d850043ad556cd29aec --- /dev/null +++ b/backend/src/main/resources/templates/study.html @@ -0,0 +1,203 @@ +<!DOCTYPE html> + +<html + xmlns:th="http://www.thymeleaf.org" + th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main}, script=~{::script})}" +> +<head> + <title>Study <th:block th:text="${model.study.studyType}" />: <th:block th:text="${model.study.studyName}" /></title> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> +</head> + +<body> +<main> + <h1>Study <th:block th:text="${model.study.studyType}" />: <th:block th:text="${model.study.studyName}" /></h1> + + <div th:replace="fragments/map::map"></div> + + <div class="f-card mt-4"> + <h2>Identification</h2> + <div class="f-card-body"> + <div th:replace="fragments/row::text-row(label='Name', text=${model.study.studyName})"></div> + <div th:replace="fragments/row::text-row(label='Identifier', text=${model.study.studyDbId})"></div> + + <div th:replace="fragments/source::source(source=${model.source}, url=${model.study.url}, entityType='study')"></div> + + <div th:replace="fragments/row::text-row(label='Project name', text=${model.study.programName})"></div> + <div th:replace="fragments/row::text-row(label='Description', text=${model.study.studyDescription})"></div> + <th:block th:if="${model.study.active != null}"> + <div th:replace="fragments/row::text-row(label='Active', text=${model.study.active ? 'Yes' : 'No'})"></div> + </th:block> + + <th:block th:unless="${#lists.isEmpty(model.study.seasons)}"> + <div th:replace="fragments/row::text-row(label='Seasons', text=${#strings.listJoin(model.study.seasons, ',')})"></div> + </th:block> + <th:block th:if="${model.study.startDate != null && model.study.endDate != null}"> + <div th:replace="fragments/row::text-row(label='Date', text=${'From ' + #dates.format(model.study.startDate, 'yyyy-MM-dd') + ' to ' + #dates.format(model.study.endDate, 'yyyy-MM-dd') })"></div> + </th:block> + <th:block th:if="${model.study.startDate != null && model.study.endDate == null}"> + <div th:replace="fragments/row::text-row(label='Date', text=${'Started on ' + #dates.format(model.study.startDate, 'yyyy-MM-dd')})"></div> + </th:block> + + <th:block th:if="${model.study.locationDbId}"> + <div th:replace="fragments/row::row(label='Location name', content=~{::#location})"> + <a id="location" th:href="@{/sites/{siteId}(siteId=${model.study.locationDbId})}" th:text="${model.study.locationName}"></a> + </div> + </th:block> + + <th:block th:unless="${#lists.isEmpty(model.study.dataLinks)}"> + <div th:replace="fragments/row::row(label='Data files', content=~{::#data-files})"> + <ul id="data-files" class="list-unstyled"> + <li th:each="dataLink : ${model.study.dataLinks}"> + <a target="_blank" th:href="${dataLink.url}" th:text="${dataLink.name}"></a> + </li> + </ul> + </div> + </th:block> + </div> + </div> + + <div class="f-card" th:unles="${#lists.isEmpty(model.germplasms)}"> + <h2>Genotype</h2> + <div class="f-card-body"> + <div class="scroll-table-container scroll-table-container-big"> + <table class="table table-sm table-striped table-sticky table-responsive-sm"> + <thead> + <tr> + <th scope="col">Accession number</th> + <th scope="col">Name</th> + <th scope="col">Taxon</th> + </tr> + </thead> + <tbody> + <tr th:each="row : ${model.germplasms}"> + <td> + <a th:href="@{/germplasms/{germplasmId}(germplasmId=${row.germplasmDbId})}" th:text="${row.accessionNumber}"></a> + </td> + <td th:text="${row.germplasmName}"></td> + <td th:text="${(row.genus == null ? '' : row.genus) + ' ' + (row.species == null ? '' : row.species)+ ' ' + (row.subtaxa == null ? '' : row.subtaxa) }"></td> + </tr> + </tbody> + </table> + </div> + </div> + </div> + + <div class="f-card" th:unless="${#lists.isEmpty(model.variables)}"> + <h2>Variables</h2> + <div class="f-card-body"> + <div class="scroll-table-container"> + <table class="table table-sm table-striped table-sticky table-responsive-sm"> + <thead> + <tr> + <th scope="col">Variable ID</th> + <th scope="col">Variable short name</th> + <th scope="col">Variable long name</th> + <th scope="col">Ontology name</th> + <th scope="col">Trait description</th> + </tr> + </thead> + <tbody> + <tr th:each="row : ${model.variables}"> + <td> + <a th:unless="${#strings.isEmpty(row.documentationURL)}" th:href="${row.documentationURL}" th:text="${row.observationVariableDbId}" target="_blank" ></a> + <span th:if="${#strings.isEmpty(row.documentationURL)}" th:text="${row.observationVariableDbId}"></span> + </td> + <td th:text="${row.name}"></td> + <td th:text="${#lists.isEmpty(row.synonyms) ? '' : row.synonyms[0]}"></td> + <td th:text="${row.ontologyName}"></td> + <td th:text="${row.trait.description}"></td> + </tr> + </tbody> + </table> + </div> + </div> + </div> + + <div class="f-card" th:unless="${#lists.isEmpty(model.trials)}"> + <h2>Data Set</h2> + <div class="f-card-body"> + <div class="scroll-table-container scroll-table-container-big"> + <table class="table table-sm table-striped table-sticky table-responsive-sm"> + <thead> + <tr> + <th scope="col">Name</th> + <th scope="col">Type</th> + <th scope="col">Linked studies identifier</th> + </tr> + </thead> + <tbody> + <tr th:each="row : ${model.trials}"> + <td> + <a th:unless="${#strings.isEmpty(row.documentationURL)}" th:href="${row.documentationURL}" th:text="${row.trialName}" target="_blank" ></a> + <span th:if="${#strings.isEmpty(row.documentationURL)}" th:text="${row.trialName}"></span> + </td> + <td th:text="${row.trialType}"></td> + <td style="width: 60%"> + <th:block th:each="trialStudy, iterStat : ${row.studies}" + th:if="${trialStudy.studyDbId != model.study.studyDbId}"> + <a th:href="@{/studies/{studyId}(studyId=${trialStudy.studyDbId})}" + th:text="${trialStudy.studyName.trim()}"> + </a><th:block th:if="${iterStat.last}">; </th:block> + </th:block> + </td> + </tr> + </tbody> + </table> + </div> + </div> + </div> + + <div class="f-card" th:unless="${#lists.isEmpty(model.study.contacts)}"> + <h2>Contact</h2> + <div class="f-card-body"> + <div class="scroll-table-container"> + <table class="table table-sm table-striped table-sticky table-responsive-sm"> + <thead> + <tr> + <th scope="col">Role</th> + <th scope="col">Name</th> + <th scope="col">Email</th> + <th scope="col">Institution</th> + </tr> + </thead> + <tbody> + <tr th:each="row : ${model.study.contacts}"> + <td th:text="${row.type}"></td> + <td th:text="${row.name}"></td> + <td th:text="${row.email}"></td> + <td th:text="${row.institutionName}"></td> + </tr> + </tbody> + </table> + </div> + </div> + </div> + + <div class="f-card" th:unless="${#lists.isEmpty(model.additionalInfoProperties)}"> + <h2>Additional information</h2> + <div class="f-card-body"> + <div class="scroll-table-container"> + <table class="table table-sm"> + <tbody> + <tr th:each="row : ${model.additionalInfoProperties}"> + <th class="label" style="width: 33.33%" th:text="${row.key}" scope="row"></th> + <td th:text="${row.value}"></td> + </tr> + </tbody> + </table> + </div> + </div> + </div> + + <div th:replace="fragments/xrefs::xrefs(crossReferences=${model.crossReferences})"></div> +</main> + +<script th:inline="javascript"> + faidare.initializeMap({ + contextPath: [[${#request.getContextPath()}]], + locations: /*[[${model.mapLocations}]]*/ [] + }); +</script> +</body> +</html> diff --git a/backend/src/test/java/fr/inra/urgi/faidare/filter/AngularRouteFilterTest.java b/backend/src/test/java/fr/inra/urgi/faidare/filter/AngularRouteFilterTest.java deleted file mode 100644 index b71e9e39509201d931b6e469ff747524f7a4ddd4..0000000000000000000000000000000000000000 --- a/backend/src/test/java/fr/inra/urgi/faidare/filter/AngularRouteFilterTest.java +++ /dev/null @@ -1,105 +0,0 @@ -package fr.inra.urgi.faidare.filter; - -import fr.inra.urgi.faidare.Application; -import fr.inra.urgi.faidare.config.SecurityConfig; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Import; -import org.springframework.core.io.Resource; -import org.springframework.core.io.ResourceLoader; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.test.util.ReflectionTestUtils; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; - -import java.io.ByteArrayInputStream; - -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl; - -/** - * Unit tests for {@link AngularRouteFilter} - * - * @author gcornut - */ -@ExtendWith(SpringExtension.class) -@Import(SecurityConfig.class) -@SpringBootTest(classes = Application.class) -class AngularRouteFilterTest { - - @Autowired - private WebApplicationContext context; - - @MockBean - private ResourceLoader resourceLoader; - - private MockMvc mockMvc; - - private AngularRouteFilter filter; - - @BeforeEach - void setUp() { - filter = new AngularRouteFilter(resourceLoader); - mockMvc = MockMvcBuilders.webAppContextSetup(context) - .addFilter(filter, "/*") - .build(); - } - - @ParameterizedTest - @ValueSource(strings = { - // Static files - "/index.html", - "/script.js", - "/style.css", - "/image.gif", - "/icon.ico", - "/image.png", - "/image.jpg", - "/font.woff", - "/font.ttf", - // APIs - "/brapi/v1/studies", - "/faidare/v1/datadiscovery/suggest", - "/actuator/info", - }) - void shouldNotForward(String url) throws Exception { - mockMvc.perform(get(url)).andExpect(forwardedUrl(null)); - } - - @ParameterizedTest - @ValueSource(strings = { - "/home", - "/studies/foo", - "/germplasm/bar", - }) - void shouldForward(String url) throws Exception { - String indexBefore = "<html>\n" + - " <base href=\"./\">\n" + - "</html>"; - String indexAfter = "<html>\n" + - " <base href=\"/gnpis-test/faidare/\">\n" + - "</html>"; - - ReflectionTestUtils.setField(filter, "serverContextPath", "/gnpis-test/faidare"); - - Resource mockResource = mock(Resource.class); - when(mockResource.getInputStream()) - .thenReturn(new ByteArrayInputStream(indexBefore.getBytes())); - when(resourceLoader.getResource(anyString())) - .thenReturn(mockResource); - - mockMvc.perform(get(url)) - .andExpect(content().string(indexAfter)); - } - -} diff --git a/backend/src/test/test.iml b/backend/src/test/test.iml deleted file mode 100644 index 6e30bb1c6134aa08b8bcfb9660df0484e83585c6..0000000000000000000000000000000000000000 --- a/backend/src/test/test.iml +++ /dev/null @@ -1,12 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<module type="JAVA_MODULE" version="4"> - <component name="NewModuleRootManager" inherit-compiler-output="true"> - <exclude-output /> - <content url="file://$MODULE_DIR$"> - <sourceFolder url="file://$MODULE_DIR$/java" isTestSource="true" /> - </content> - <orderEntry type="inheritedJdk" /> - <orderEntry type="sourceFolder" forTests="false" /> - <orderEntry type="module" module-name="main" /> - </component> -</module> diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 457aad0d98108420a977756b7145c93c8910b076..7454180f2ae8848c63b8b4dea2cb829da983f2fa 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 75b8c7c8c67a003599a36935d1c6a41519fc2207..ffed3a254e91df704a9acc0f2745c0e340d9b582 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index af6708ff229fda75da4f7cc4da4747217bac4d53..1b6c787337ffb79f0e3cf8b1e9f00f680a959de1 100755 --- a/gradlew +++ b/gradlew @@ -1,78 +1,129 @@ -#!/usr/bin/env sh +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m"' +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -81,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -89,84 +140,95 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=$((i+1)) + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 6d57edc706c93465988754383a2d7ff353d4e79f..107acd32c4e687021ef32db511e8a206129b88ec 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -13,15 +29,18 @@ if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -35,7 +54,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -45,28 +64,14 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell diff --git a/settings.gradle.kts b/settings.gradle.kts index 4bd5a482217fc2c6b3921cafc0bdb602ebd96641..b8faef34872a40284f74d8be029e0dd7861df5d7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,2 +1,2 @@ rootProject.name = "faidare" -include("backend", "frontend") +include("backend", "web") diff --git a/web/build.gradle.kts b/web/build.gradle.kts new file mode 100644 index 0000000000000000000000000000000000000000..44076adb023409ae78bfcc8bd986a412a52afc11 --- /dev/null +++ b/web/build.gradle.kts @@ -0,0 +1,41 @@ +import com.github.gradle.node.yarn.task.YarnInstallTask +import com.github.gradle.node.yarn.task.YarnTask + +plugins { + base + id("com.github.node-gradle.node") version "3.0.1" +} + +node { + version.set("14.17.0") + npmVersion.set("6.14.10") + yarnVersion.set("1.22.10") + download.set(true) +} + +tasks { + npmInstall { + enabled = false + } + + val prepare by registering { + dependsOn(YarnInstallTask.NAME) + } + + // This is not a yarn_build task because the task to run is `yarn build:prod` + // and tasks with colons are not supported + val yarnBuildProd by registering(YarnTask::class) { + args.set(listOf("run", "build:prod")) + dependsOn(prepare) + inputs.file("webpack.config.js") + inputs.file("tsconfig.json") + inputs.file("package.json") + inputs.file("yarn.lock") + inputs.dir("src") + outputs.dir("$buildDir/dist") + } + + assemble { + dependsOn(yarnBuildProd) + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000000000000000000000000000000000000..4ce0986c188f17659ca176c2deb119cd63c73421 --- /dev/null +++ b/web/package.json @@ -0,0 +1,35 @@ +{ + "name": "faidaire-web", + "version": "1.0.0", + "description": "", + "private": true, + "scripts": { + "build": "webpack --mode development", + "build:prod": "webpack --mode production", + "watch": "webpack --mode development --watch", + "watch:prod": "webpack --mode production --watch", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "MIT", + "dependencies": { + "@types/leaflet": "1.7.4", + "bootstrap": "5.1.0", + "leaflet": "1.7.1" + }, + "devDependencies": { + "@types/bootstrap": "5.1.2", + "@types/leaflet.markercluster": "1.4.5", + "clean-webpack-plugin": "4.0.0-alpha.0", + "css-loader": "6.2.0", + "css-minimizer-webpack-plugin": "3.0.2", + "leaflet.markercluster": "1.5.0", + "mini-css-extract-plugin": "2.2.0", + "sass": "1.38.1", + "sass-loader": "12.1.0", + "ts-loader": "9.2.5", + "typescript": "4.3.5", + "webpack": "5.51.1", + "webpack-cli": "4.8.0" + } +} diff --git a/web/src/bootstrap/popovers.ts b/web/src/bootstrap/popovers.ts new file mode 100644 index 0000000000000000000000000000000000000000..83b703de77469e545f992f1ad2396706db3621bd --- /dev/null +++ b/web/src/bootstrap/popovers.ts @@ -0,0 +1,19 @@ +import { Popover } from 'bootstrap'; + +export function initializePopovers() { + const popoverTriggerList: Array<HTMLElement> = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')) + popoverTriggerList.forEach(popoverTriggerEl => { + const options: Partial<Popover.Options> = {}; + const contentSelector = popoverTriggerEl.dataset.bsElement; + if (contentSelector) { + const content = document.querySelector(contentSelector); + if (content) { + options.content = content.innerHTML; + options.html = true; + } else { + throw new Error('element with selector ' + contentSelector + ' not found'); + } + } + return new Popover(popoverTriggerEl, options); + }); +} diff --git a/web/src/index.ts b/web/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..c906b28eb7d5cb4d78785bd6ea5bd01ff4b92fd2 --- /dev/null +++ b/web/src/index.ts @@ -0,0 +1,7 @@ +import { initializePopovers } from './bootstrap/popovers'; +import { initializeMap } from './map/map'; + +(window as any).faidare = { + initializePopovers, + initializeMap +} diff --git a/web/src/map/map.ts b/web/src/map/map.ts new file mode 100644 index 0000000000000000000000000000000000000000..9fc2a96004bca2ea53eaf342371595c7ea3f5b29 --- /dev/null +++ b/web/src/map/map.ts @@ -0,0 +1,93 @@ +import * as L from 'leaflet'; +import 'leaflet.markercluster'; + +interface MapLocation { + locationDbId: string; + locationType: 'Origin site' | 'Collecting site' | 'Evaluation site' | null; + locationName: string; + latitude: number; + longitude: number; +} + +interface MapOptions { + contextPath: string; + locations: Array<MapLocation>; +} + +function markerColor(location: MapLocation) { + switch (location.locationType) { + case 'Origin site': + return 'red'; + case 'Collecting site': + return 'blue'; + case 'Evaluation site': + return 'green'; + } + return 'purple'; +} + +function markerIconUrl(contextPath: string, location: MapLocation) { + return `${contextPath}/assets/images/marker-icon-${markerColor(location)}.png`; +} + +export function initializeMap(options: MapOptions) { + if (!options.locations.length) { + return; + } + + const mapContainerElement = document.querySelector('#map-container'); + mapContainerElement!.classList.remove("d-none"); + const mapElement = document.querySelector('#map') as HTMLElement; + const map = L.map(mapElement); + L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}', { + attribution: 'Tiles © Esri — Source: Esri, DeLorme, NAVTEQ, USGS, Intermap, iPC, NRCAN, Esri Japan, METI, ' + + 'Esri China (Hong Kong), Esri (Thailand), TomTom, 2012' + }).addTo(map); + + const firstLocation = options.locations[0]; + map.setView([firstLocation.latitude, firstLocation.longitude], 5); + + const markers = L.markerClusterGroup(); + const mapMarkers: Array<L.Marker> = []; + for (const location of options.locations) { + const icon = L.icon({ + iconUrl: markerIconUrl(options.contextPath, location), + iconAnchor: [12, 41], // point of the icon which will correspond to marker's location + }); + + const popupElement = document.createElement('div'); + + const titleElement = document.createElement('strong'); + titleElement.innerText = location.locationName; + popupElement.appendChild(titleElement); + popupElement.appendChild(document.createElement('br')); + + if (location.locationType) { + const typeElement = document.createElement('span'); + typeElement.innerText = location.locationType; + popupElement.appendChild(typeElement); + popupElement.appendChild(document.createElement('br')); + } + + const linkElement = document.createElement('a'); + linkElement.innerText = 'Details'; + linkElement.href = `${options.contextPath}/sites/${location.locationDbId}`; + popupElement.appendChild(linkElement); + + const marker = L.marker( + [location.latitude, location.longitude], + { icon: icon } + ); + markers.addLayer(marker.bindPopup(popupElement)); + mapMarkers.push(marker); + } + const initialZoom = map.getZoom(); + + map.fitBounds(L.featureGroup(mapMarkers).getBounds()); + const markerZoom = map.getZoom(); + + setTimeout(() => { + map.setZoom(Math.min(initialZoom, markerZoom)); + map.addLayer(markers); + }, 100); +} diff --git a/web/src/style/_custom-bootstrap.scss b/web/src/style/_custom-bootstrap.scss new file mode 100644 index 0000000000000000000000000000000000000000..9de7c3dce4b8fbea20fd88c39b1bc001c72b69a0 --- /dev/null +++ b/web/src/style/_custom-bootstrap.scss @@ -0,0 +1,53 @@ +/*! + * Bootstrap v5.1.0 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors + * Copyright 2011-2021 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ + +// scss-docs-start import-stack +// Configuration +@import '~bootstrap/scss/functions'; +@import '~bootstrap/scss/variables'; +@import '~bootstrap/scss/mixins'; +@import '~bootstrap/scss/utilities'; + +// Layout & components +@import '~bootstrap/scss/root'; +@import '~bootstrap/scss/reboot'; +@import '~bootstrap/scss/type'; +@import '~bootstrap/scss/images'; +@import '~bootstrap/scss/containers'; +@import '~bootstrap/scss/grid'; +@import '~bootstrap/scss/tables'; +// @import '~bootstrap/scss/forms'; +@import '~bootstrap/scss/buttons'; +@import '~bootstrap/scss/transitions'; +@import '~bootstrap/scss/dropdown'; +// @import '~bootstrap/scss/button-group'; +@import '~bootstrap/scss/nav'; +@import '~bootstrap/scss/navbar'; +@import '~bootstrap/scss/card'; +// @import '~bootstrap/scss/accordion'; +// @import '~bootstrap/scss/breadcrumb'; +// @import '~bootstrap/scss/pagination'; +// @import '~bootstrap/scss/badge'; +// @import '~bootstrap/scss/alert'; +// @import '~bootstrap/scss/progress'; +// @import '~bootstrap/scss/list-group'; +// @import '~bootstrap/scss/close'; +// @import '~bootstrap/scss/toasts'; +// @import '~bootstrap/scss/modal'; +// @import '~bootstrap/scss/tooltip'; +@import '~bootstrap/scss/popover'; +// @import '~bootstrap/scss/carousel'; +// @import '~bootstrap/scss/spinners'; +// @import '~bootstrap/scss/offcanvas'; +// @import '~bootstrap/scss/placeholders'; + +// Helpers +// @import '~bootstrap/scss/helpers'; + +// Utilities +@import '~bootstrap/scss/utilities/api'; +// scss-docs-end import-stack diff --git a/web/src/style/style.scss b/web/src/style/style.scss new file mode 100644 index 0000000000000000000000000000000000000000..42aa2bb3a883a5991772d6b9cbe4eddb0eb7d267 --- /dev/null +++ b/web/src/style/style.scss @@ -0,0 +1,91 @@ +$headings-color: #0f6191; +$border-color: #0f6e9f; +$link-color: #0f6fa1; +$link-decoration: none; +$link-hover-decoration: underline; +$enable-shadows: true; +$table-border-color: #dee2e6; +$table-group-separator-color: $table-border-color; + +@import 'custom-bootstrap'; +@import '~leaflet/dist/leaflet.css'; +@import '~leaflet.markercluster/dist/MarkerCluster.css'; +@import '~leaflet.markercluster/dist/MarkerCluster.Default.css'; + +a[role=button] { + color: $link-color !important; +} + +.f-row { + border-top: 1px solid $gray-300; + padding: 0.5rem 0; + .label { + font-weight: 700; + } +} + +.f-card { + border: 1px solid $border-color; + border-radius: 0.25rem; + margin: 0.5rem 0; + h2 { + font-size: $h4-font-size; + padding: 0.5rem 1rem; + background-image: repeating-linear-gradient(#0f96cd, #0f6191, #0f76a5); + color: $white; + } + .f-card-body { + padding: 0.25rem 1rem; + .f-row:first-of-type { + border-top: 0; + } + } +} + +.popover { + max-width: min(80vw, 500px); +} + +.popover-header { + font-weight: 700; +} + +#map { + height: min(400px, 60vh); +} + +.map-legend img { + height: 1.5rem; +} + +.content-overflow { + max-height: 200px; + overflow-y: auto; + overflow-x: hidden; + &.content-overflow-big { + max-height: 275px; + } +} + +.scroll-table-container { + max-height: 200px; + overflow-y: auto; + padding-top: 0; + &.scroll-table-container-big { + max-height: 500px; + } +} + +.table-sticky { + width: 100%; + thead th { + position: sticky; + position: -webkit-sticky; + top: 0; + background-color: white; + border-top-width: 0; + th { + padding-top: 0; + } + } +} diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..714af2e552e4d168d08d85597be03eecb652ac23 --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,72 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */ + "module": "es2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "skipLibCheck": true, /* Skip type checking of declaration files. */ + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + } +} diff --git a/web/webpack.config.js b/web/webpack.config.js new file mode 100644 index 0000000000000000000000000000000000000000..1c7b808029f88e4107f429df70a5fb93e383a29b --- /dev/null +++ b/web/webpack.config.js @@ -0,0 +1,67 @@ +const path = require('path'); +const { CleanWebpackPlugin } = require('clean-webpack-plugin'); +const MiniCssExtractPlugin = require("mini-css-extract-plugin"); +const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); + +module.exports = (env, argv) => ({ + context: path.resolve(__dirname, '.'), + // inline source maps only in development mode + devtool: argv.mode === 'production' ? undefined : 'inline-source-map', + plugins: [ + // allows extracting the CSS into a CSS file instead of bundling it in a JS file + new MiniCssExtractPlugin({ + filename: argv.mode === 'production' ? '[name].[contenthash].css' : '[name].css' + }), + // cleans the output directory before each build + new CleanWebpackPlugin({ + // and the empty, useless style.js after each build + protectWebpackAssets: false, + cleanAfterEveryBuildPatterns: ['style*.js'] + }) + ], + entry: { + // a JS bundle is generated for the index.ts entry point. Since the application is really small + // and does not have much JavaScript logic, a single bundle is sufficient + script: './src/index.ts', + // A CSS bundle is generated for the style.scss entry point. + style: './src/style/style.scss' + }, + module: { + rules: [ + { + test: /\.tsx?$/, + use: 'ts-loader', + exclude: /node_modules/, + }, + { + // .scss files are loaded by the sass-loader, which transforms them into css loaded by the css-loader + // which are then bundled into a css file by the MiniCssExtractPlugin loader + test: /\.scss$/i, + use: [ + MiniCssExtractPlugin.loader, + 'css-loader', + 'sass-loader' + ], + }, + ], + }, + resolve: { + // our files are .ts files, but libraries are bundled in .js files + extensions: ['.ts', '.js'], + }, + output: { + // the output is stored in build/dist/assets + path: path.resolve(__dirname, 'build/dist/assets'), + filename: argv.mode === 'production' ? '[name].[contenthash].js' : '[name].js' + }, + optimization: { + minimizer: [ + '...', + new CssMinimizerPlugin() + ] + }, + performance: { + maxAssetSize: 300000, + maxEntrypointSize: 300000 + } +}); diff --git a/web/yarn.lock b/web/yarn.lock new file mode 100644 index 0000000000000000000000000000000000000000..ba30e63f6aa70b15fb6a0bde06e91ea9c5230bf2 --- /dev/null +++ b/web/yarn.lock @@ -0,0 +1,1826 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@discoveryjs/json-ext@^0.5.0": + version "0.5.3" + resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.3.tgz#90420f9f9c6d3987f176a19a7d8e764271a2f55d" + integrity sha512-Fxt+AfXgjMoin2maPIYzFZnQjAXjAL0PHscM5pRTtatFqB+vZxAM9tLp2Optnuw3QOQC40jTNeGYFOMvyf7v9g== + +"@popperjs/core@^2.9.2": + version "2.9.3" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.3.tgz#8b68da1ebd7fc603999cf6ebee34a4899a14b88e" + integrity sha512-xDu17cEfh7Kid/d95kB6tZsLOmSWKCZKtprnhVepjsSaCij+lM3mItSJDuuHDMbCWTh8Ejmebwb+KONcCJ0eXQ== + +"@trysound/sax@0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.1.1.tgz#3348564048e7a2d7398c935d466c0414ebb6a669" + integrity sha512-Z6DoceYb/1xSg5+e+ZlPZ9v0N16ZvZ+wYMraFue4HYrE4ttONKtsvruIRf6t9TBR0YvSOfi1hUU0fJfBLCDYow== + +"@types/bootstrap@5.1.2": + version "5.1.2" + resolved "https://registry.yarnpkg.com/@types/bootstrap/-/bootstrap-5.1.2.tgz#24f08f1957ff5859633f4bf620e921d296e6c3a2" + integrity sha512-dSQvMi2dMyNwJU6LZjP0pimuBowsMUvGScYdfqqeiDUoj9TxXZCpfu0cTl94U0Zvw/tdH9j/9ToOhi4LKNLZhg== + dependencies: + "@popperjs/core" "^2.9.2" + "@types/jquery" "*" + +"@types/eslint-scope@^3.7.0": + version "3.7.1" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.1.tgz#8dc390a7b4f9dd9f1284629efce982e41612116e" + integrity sha512-SCFeogqiptms4Fg29WpOTk5nHIzfpKCemSN63ksBQYKTcXoJEmJagV+DhVmbapZzY4/5YaOV1nZwrsU79fFm1g== + dependencies: + "@types/eslint" "*" + "@types/estree" "*" + +"@types/eslint@*": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.28.0.tgz#7e41f2481d301c68e14f483fe10b017753ce8d5a" + integrity sha512-07XlgzX0YJUn4iG1ocY4IX9DzKSmMGUs6ESKlxWhZRaa0fatIWaHWUVapcuGa8r5HFnTqzj+4OCjd5f7EZ/i/A== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*", "@types/estree@^0.0.50": + version "0.0.50" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83" + integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw== + +"@types/geojson@*": + version "7946.0.8" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.8.tgz#30744afdb385e2945e22f3b033f897f76b1f12ca" + integrity sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA== + +"@types/glob@^7.1.1": + version "7.1.4" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.4.tgz#ea59e21d2ee5c517914cb4bc8e4153b99e566672" + integrity sha512-w+LsMxKyYQm347Otw+IfBXOv9UWVjpHpCDdbBMt8Kz/xbvCYNjP+0qPh91Km3iKfSRLBB0P7fAMf0KHrPu+MyA== + dependencies: + "@types/minimatch" "*" + "@types/node" "*" + +"@types/jquery@*": + version "3.5.6" + resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.6.tgz#97ac8e36dccd8ad8ed3f3f3b48933614d9fd8cf0" + integrity sha512-SmgCQRzGPId4MZQKDj9Hqc6kSXFNWZFHpELkyK8AQhf8Zr6HKfCzFv9ZC1Fv3FyQttJZOlap3qYb12h61iZAIg== + dependencies: + "@types/sizzle" "*" + +"@types/json-schema@*", "@types/json-schema@^7.0.8": + version "7.0.9" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" + integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== + +"@types/leaflet.markercluster@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@types/leaflet.markercluster/-/leaflet.markercluster-1.4.5.tgz#dc4457e2dff9baaacc17c9c04c65ab69b5f361f7" + integrity sha512-R9Ql//z6muSGI5mPfr+FaKQQB7EIdQQyivYweVSdOrWr8WyNNFcSwfl+mqGYJFhRRCO/6lbZiM3scEyp9LdaFg== + dependencies: + "@types/leaflet" "*" + +"@types/leaflet@*", "@types/leaflet@1.7.4": + version "1.7.4" + resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.7.4.tgz#bb9430f69d588ca5829c1ba82657e179454f93a1" + integrity sha512-a3qYlMwJ62+WRoiDmYODUD4KywA14jP2XohAkAWtELGuMAD3MohZa/MmIvQDqF52xNI9OYaY8BMsL+9z7yf2HQ== + dependencies: + "@types/geojson" "*" + +"@types/minimatch@*": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40" + integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ== + +"@types/node@*": + version "16.7.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.7.2.tgz#0465a39b5456b61a04d98bd5545f8b34be340cb7" + integrity sha512-TbG4TOx9hng8FKxaVrCisdaxKxqEwJ3zwHoCWXZ0Jw6mnvTInpaB99/2Cy4+XxpXtjNv9/TgfGSvZFyfV/t8Fw== + +"@types/sizzle@*": + version "2.3.3" + resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.3.tgz#ff5e2f1902969d305225a047c8a0fd5c915cebef" + integrity sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ== + +"@webassemblyjs/ast@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" + integrity sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw== + dependencies: + "@webassemblyjs/helper-numbers" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + +"@webassemblyjs/floating-point-hex-parser@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz#f6c61a705f0fd7a6aecaa4e8198f23d9dc179e4f" + integrity sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ== + +"@webassemblyjs/helper-api-error@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz#1a63192d8788e5c012800ba6a7a46c705288fd16" + integrity sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg== + +"@webassemblyjs/helper-buffer@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz#832a900eb444884cde9a7cad467f81500f5e5ab5" + integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA== + +"@webassemblyjs/helper-numbers@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz#64d81da219fbbba1e3bd1bfc74f6e8c4e10a62ae" + integrity sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.11.1" + "@webassemblyjs/helper-api-error" "1.11.1" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/helper-wasm-bytecode@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz#f328241e41e7b199d0b20c18e88429c4433295e1" + integrity sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q== + +"@webassemblyjs/helper-wasm-section@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz#21ee065a7b635f319e738f0dd73bfbda281c097a" + integrity sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + +"@webassemblyjs/ieee754@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz#963929e9bbd05709e7e12243a099180812992614" + integrity sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.1.tgz#ce814b45574e93d76bae1fb2644ab9cdd9527aa5" + integrity sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.1.tgz#d1f8b764369e7c6e6bae350e854dec9a59f0a3ff" + integrity sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ== + +"@webassemblyjs/wasm-edit@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz#ad206ebf4bf95a058ce9880a8c092c5dec8193d6" + integrity sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/helper-wasm-section" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + "@webassemblyjs/wasm-opt" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + "@webassemblyjs/wast-printer" "1.11.1" + +"@webassemblyjs/wasm-gen@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz#86c5ea304849759b7d88c47a32f4f039ae3c8f76" + integrity sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/ieee754" "1.11.1" + "@webassemblyjs/leb128" "1.11.1" + "@webassemblyjs/utf8" "1.11.1" + +"@webassemblyjs/wasm-opt@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz#657b4c2202f4cf3b345f8a4c6461c8c2418985f2" + integrity sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + +"@webassemblyjs/wasm-parser@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz#86ca734534f417e9bd3c67c7a1c75d8be41fb199" + integrity sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-api-error" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/ieee754" "1.11.1" + "@webassemblyjs/leb128" "1.11.1" + "@webassemblyjs/utf8" "1.11.1" + +"@webassemblyjs/wast-printer@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz#d0c73beda8eec5426f10ae8ef55cee5e7084c2f0" + integrity sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@xtuc/long" "4.2.2" + +"@webpack-cli/configtest@^1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-1.0.4.tgz#f03ce6311c0883a83d04569e2c03c6238316d2aa" + integrity sha512-cs3XLy+UcxiP6bj0A6u7MLLuwdXJ1c3Dtc0RkKg+wiI1g/Ti1om8+/2hc2A2B60NbBNAbMgyBMHvyymWm/j4wQ== + +"@webpack-cli/info@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-1.3.0.tgz#9d78a31101a960997a4acd41ffd9b9300627fe2b" + integrity sha512-ASiVB3t9LOKHs5DyVUcxpraBXDOKubYu/ihHhU+t1UPpxsivg6Od2E2qU4gJCekfEddzRBzHhzA/Acyw/mlK/w== + dependencies: + envinfo "^7.7.3" + +"@webpack-cli/serve@^1.5.2": + version "1.5.2" + resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.5.2.tgz#ea584b637ff63c5a477f6f21604b5a205b72c9ec" + integrity sha512-vgJ5OLWadI8aKjDlOH3rb+dYyPd2GTZuQC/Tihjct6F9GpXGZINo3Y/IVuZVTM1eDQB+/AOsjPUWH/WySDaXvw== + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +acorn-import-assertions@^1.7.6: + version "1.7.6" + resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.7.6.tgz#580e3ffcae6770eebeec76c3b9723201e9d01f78" + integrity sha512-FlVvVFA1TX6l3lp8VjDnYYq7R1nyW6x3svAt4nDgrWQ9SBaSh9CnbwgSUTasgfNfOG5HlM1ehugCvM+hjo56LA== + +acorn@^8.4.1: + version "8.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.4.1.tgz#56c36251fc7cabc7096adc18f05afe814321a28c" + integrity sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA== + +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +alphanum-sort@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" + integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM= + +ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +anymatch@~3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" + integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk= + dependencies: + array-uniq "^1.0.1" + +array-uniq@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +boolbase@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= + +bootstrap@5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.1.0.tgz#543ef8f44f4b9af67b0230f19508542fec38ef55" + integrity sha512-bs74WNI9BgBo3cEovmdMHikSKoXnDgA6VQjJ7TyTotU6L7d41ZyCEEelPwkYEzsG/Zjv3ie9IE3EMAje0W9Xew== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^3.0.1, braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.16.0, browserslist@^4.16.6: + version "4.16.8" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.8.tgz#cb868b0b554f137ba6e33de0ecff2eda403c4fb0" + integrity sha512-sc2m9ohR/49sWEbPj14ZSSZqp+kbi16aLao42Hmn3Z8FpjuMaq2xCA2l4zl9ITfyzvnvyE0hcg62YkIGKxgaNQ== + dependencies: + caniuse-lite "^1.0.30001251" + colorette "^1.3.0" + electron-to-chromium "^1.3.811" + escalade "^3.1.1" + node-releases "^1.1.75" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +caniuse-api@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0" + integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw== + dependencies: + browserslist "^4.0.0" + caniuse-lite "^1.0.0" + lodash.memoize "^4.1.2" + lodash.uniq "^4.5.0" + +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001251: + version "1.0.30001252" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001252.tgz#cb16e4e3dafe948fc4a9bb3307aea054b912019a" + integrity sha512-I56jhWDGMtdILQORdusxBOH+Nl/KgQSdDmpJezYddnAkVOmnoU8zwjTV9xAjMIYxr0iPreEAVylCGcmHCjfaOw== + +chalk@^4.1.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +"chokidar@>=3.0.0 <4.0.0": + version "3.5.2" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75" + integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chrome-trace-event@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" + integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== + +clean-webpack-plugin@4.0.0-alpha.0: + version "4.0.0-alpha.0" + resolved "https://registry.yarnpkg.com/clean-webpack-plugin/-/clean-webpack-plugin-4.0.0-alpha.0.tgz#2aef48dfe7565360d128f5caa0904097d969d053" + integrity sha512-+X6mASBbGSVyw8L9/1rhQ+vS4uaQMopf194kX7Aes8qfezgCFL+qv5W0nwP3a0Tud5kUckARk8tFcoyOSKEjhg== + dependencies: + del "^4.1.1" + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +colord@^2.0.1, colord@^2.6: + version "2.7.0" + resolved "https://registry.yarnpkg.com/colord/-/colord-2.7.0.tgz#706ea36fe0cd651b585eb142fe64b6480185270e" + integrity sha512-pZJBqsHz+pYyw3zpX6ZRXWoCHM1/cvFikY9TV8G3zcejCaKE0lhankoj8iScyrrePA8C7yJ5FStfA9zbcOnw7Q== + +colorette@^1.2.1, colorette@^1.2.2, colorette@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.3.0.tgz#ff45d2f0edb244069d3b772adeb04fed38d0a0af" + integrity sha512-ecORCqbSFP7Wm8Y6lyqMJjexBQqXSF7SSeaTyGGphogUjBlFP9m9o08wy86HL2uB7fMTxtOUzLMk7ogKcxMg1w== + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commander@^7.0.0, commander@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +css-color-names@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-1.0.1.tgz#6ff7ee81a823ad46e020fa2fd6ab40a887e2ba67" + integrity sha512-/loXYOch1qU1biStIFsHH8SxTmOseh1IJqFvy8IujXOm1h+QjUdDhkzOrR5HG8K8mlxREj0yfi8ewCHx0eMxzA== + +css-declaration-sorter@^6.0.3: + version "6.1.1" + resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.1.1.tgz#77b32b644ba374bc562c0fc6f4fdaba4dfb0b749" + integrity sha512-BZ1aOuif2Sb7tQYY1GeCjG7F++8ggnwUkH5Ictw0mrdpqpEd+zWmcPdstnH2TItlb74FqR0DrVEieon221T/1Q== + dependencies: + timsort "^0.3.0" + +css-loader@6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.2.0.tgz#9663d9443841de957a3cb9bcea2eda65b3377071" + integrity sha512-/rvHfYRjIpymZblf49w8jYcRo2y9gj6rV8UroHGmBxKrIyGLokpycyKzp9OkitvqT29ZSpzJ0Ic7SpnJX3sC8g== + dependencies: + icss-utils "^5.1.0" + postcss "^8.2.15" + postcss-modules-extract-imports "^3.0.0" + postcss-modules-local-by-default "^4.0.0" + postcss-modules-scope "^3.0.0" + postcss-modules-values "^4.0.0" + postcss-value-parser "^4.1.0" + semver "^7.3.5" + +css-minimizer-webpack-plugin@3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.0.2.tgz#8fadbdf10128cb40227bff275a4bb47412534245" + integrity sha512-B3I5e17RwvKPJwsxjjWcdgpU/zqylzK1bPVghcmpFHRL48DXiBgrtqz1BJsn68+t/zzaLp9kYAaEDvQ7GyanFQ== + dependencies: + cssnano "^5.0.6" + jest-worker "^27.0.2" + p-limit "^3.0.2" + postcss "^8.3.5" + schema-utils "^3.0.0" + serialize-javascript "^6.0.0" + source-map "^0.6.1" + +css-select@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.1.3.tgz#a70440f70317f2669118ad74ff105e65849c7067" + integrity sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA== + dependencies: + boolbase "^1.0.0" + css-what "^5.0.0" + domhandler "^4.2.0" + domutils "^2.6.0" + nth-check "^2.0.0" + +css-tree@^1.1.2, css-tree@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" + integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== + dependencies: + mdn-data "2.0.14" + source-map "^0.6.1" + +css-what@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.0.1.tgz#3efa820131f4669a8ac2408f9c32e7c7de9f4cad" + integrity sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg== + +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + +cssnano-preset-default@^5.1.4: + version "5.1.4" + resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-5.1.4.tgz#359943bf00c5c8e05489f12dd25f3006f2c1cbd2" + integrity sha512-sPpQNDQBI3R/QsYxQvfB4mXeEcWuw0wGtKtmS5eg8wudyStYMgKOQT39G07EbW1LB56AOYrinRS9f0ig4Y3MhQ== + dependencies: + css-declaration-sorter "^6.0.3" + cssnano-utils "^2.0.1" + postcss-calc "^8.0.0" + postcss-colormin "^5.2.0" + postcss-convert-values "^5.0.1" + postcss-discard-comments "^5.0.1" + postcss-discard-duplicates "^5.0.1" + postcss-discard-empty "^5.0.1" + postcss-discard-overridden "^5.0.1" + postcss-merge-longhand "^5.0.2" + postcss-merge-rules "^5.0.2" + postcss-minify-font-values "^5.0.1" + postcss-minify-gradients "^5.0.2" + postcss-minify-params "^5.0.1" + postcss-minify-selectors "^5.1.0" + postcss-normalize-charset "^5.0.1" + postcss-normalize-display-values "^5.0.1" + postcss-normalize-positions "^5.0.1" + postcss-normalize-repeat-style "^5.0.1" + postcss-normalize-string "^5.0.1" + postcss-normalize-timing-functions "^5.0.1" + postcss-normalize-unicode "^5.0.1" + postcss-normalize-url "^5.0.2" + postcss-normalize-whitespace "^5.0.1" + postcss-ordered-values "^5.0.2" + postcss-reduce-initial "^5.0.1" + postcss-reduce-transforms "^5.0.1" + postcss-svgo "^5.0.2" + postcss-unique-selectors "^5.0.1" + +cssnano-utils@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-2.0.1.tgz#8660aa2b37ed869d2e2f22918196a9a8b6498ce2" + integrity sha512-i8vLRZTnEH9ubIyfdZCAdIdgnHAUeQeByEeQ2I7oTilvP9oHO6RScpeq3GsFUVqeB8uZgOQ9pw8utofNn32hhQ== + +cssnano@^5.0.6: + version "5.0.8" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.0.8.tgz#39ad166256980fcc64faa08c9bb18bb5789ecfa9" + integrity sha512-Lda7geZU0Yu+RZi2SGpjYuQz4HI4/1Y+BhdD0jL7NXAQ5larCzVn+PUGuZbDMYz904AXXCOgO5L1teSvgu7aFg== + dependencies: + cssnano-preset-default "^5.1.4" + is-resolvable "^1.1.0" + lilconfig "^2.0.3" + yaml "^1.10.2" + +csso@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" + integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== + dependencies: + css-tree "^1.1.2" + +del@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/del/-/del-4.1.1.tgz#9e8f117222ea44a31ff3a156c049b99052a9f0b4" + integrity sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ== + dependencies: + "@types/glob" "^7.1.1" + globby "^6.1.0" + is-path-cwd "^2.0.0" + is-path-in-cwd "^2.0.0" + p-map "^2.0.0" + pify "^4.0.1" + rimraf "^2.6.3" + +dom-serializer@^1.0.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.2.tgz#6206437d32ceefaec7161803230c7a20bc1b4d91" + integrity sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.2.0" + entities "^2.0.0" + +domelementtype@^2.0.1, domelementtype@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57" + integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== + +domhandler@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.2.0.tgz#f9768a5f034be60a89a27c2e4d0f74eba0d8b059" + integrity sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA== + dependencies: + domelementtype "^2.2.0" + +domutils@^2.6.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" + integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== + dependencies: + dom-serializer "^1.0.1" + domelementtype "^2.2.0" + domhandler "^4.2.0" + +electron-to-chromium@^1.3.811: + version "1.3.818" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.818.tgz#32ed024fa8316e5d469c96eecbea7d2463d80085" + integrity sha512-c/Z9gIr+jDZAR9q+mn40hEc1NharBT+8ejkarjbCDnBNFviI6hvcC5j2ezkAXru//bTnQp5n6iPi0JA83Tla1Q== + +enhanced-resolve@^5.0.0, enhanced-resolve@^5.8.0: + version "5.8.2" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.8.2.tgz#15ddc779345cbb73e97c611cd00c01c1e7bf4d8b" + integrity sha512-F27oB3WuHDzvR2DOGNTaYy0D5o0cnrv8TeI482VM4kYgQd/FT9lUQwuNsJ0oOHtBUq7eiW5ytqzp7nBFknL+GA== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +entities@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== + +envinfo@^7.7.3: + version "7.8.1" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475" + integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw== + +es-module-lexer@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.7.1.tgz#c2c8e0f46f2df06274cdaf0dd3f3b33e0a0b267d" + integrity sha512-MgtWFl5No+4S3TmhDmCz2ObFGm6lEpTnzbQi+Dd+pw4mlTIZTmM2iAs5gRlmx5zS9luzobCSBSI90JM/1/JgOw== + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +eslint-scope@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" + integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== + +events@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fastest-levenshtein@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz#9990f7d3a88cc5a9ffd1f1745745251700d497e2" + integrity sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow== + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +find-up@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +glob@^7.0.3, glob@^7.1.3: + version "7.1.7" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" + integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globby@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" + integrity sha1-9abXDoOV4hyFj7BInWTfAkJNUGw= + dependencies: + array-union "^1.0.1" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +graceful-fs@^4.1.2, graceful-fs@^4.2.4: + version "4.2.8" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" + integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +icss-utils@^5.0.0, icss-utils@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" + integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== + +import-local@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.0.2.tgz#a8cfd0431d1de4a2199703d003e3e62364fa6db6" + integrity sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +interpret@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" + integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== + +is-absolute-url@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698" + integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-core-module@^2.2.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.6.0.tgz#d7553b2526fe59b92ba3e40c8df757ec8a709e19" + integrity sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ== + dependencies: + has "^1.0.3" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-path-cwd@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" + integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== + +is-path-in-cwd@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz#bfe2dca26c69f397265a4009963602935a053acb" + integrity sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ== + dependencies: + is-path-inside "^2.1.0" + +is-path-inside@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-2.1.0.tgz#7c9810587d659a40d27bcdb4d5616eab059494b2" + integrity sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg== + dependencies: + path-is-inside "^1.0.2" + +is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-resolvable@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" + integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg== + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= + +jest-worker@^27.0.2: + version "27.0.6" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.0.6.tgz#a5fdb1e14ad34eb228cfe162d9f729cdbfa28aed" + integrity sha512-qupxcj/dRuA3xHPMUd40gr2EaAurFbkwzOh7wfPaeE9id7hyjURRQoqNfHifHK3XjJU6YJJUQKILGUnwGPEOCA== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +json-parse-better-errors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" + integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +klona@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.4.tgz#7bb1e3affb0cb8624547ef7e8f6708ea2e39dfc0" + integrity sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA== + +leaflet.markercluster@1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/leaflet.markercluster/-/leaflet.markercluster-1.5.0.tgz#54db42485da32fc3d92c7ae22d0d7982879e0b67" + integrity sha512-Fvf/cq4o806mJL50n+fZW9+QALDDLPvt7vuAjlD2vfnxx3srMDs2vWINJze4nKYJYRY45OC6tM/669C3pLwMCA== + +leaflet@1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.7.1.tgz#10d684916edfe1bf41d688a3b97127c0322a2a19" + integrity sha512-/xwPEBidtg69Q3HlqPdU3DnrXQOvQU/CCHA1tcDQVzOwm91YMYaILjNp7L4Eaw5Z4sOYdbBz6koWyibppd8Zqw== + +lilconfig@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.3.tgz#68f3005e921dafbd2a2afb48379986aa6d2579fd" + integrity sha512-EHKqr/+ZvdKCifpNrJCKxBTgk5XupZA3y/aCPY9mxfgBzmgh93Mt/WqjjQ38oMxXuvDokaKiM3lAgvSH2sjtHg== + +loader-runner@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.2.0.tgz#d7022380d66d14c5fb1d496b89864ebcfd478384" + integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw== + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +lodash.memoize@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= + +lodash.uniq@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +mdn-data@2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" + integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +micromatch@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" + integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== + dependencies: + braces "^3.0.1" + picomatch "^2.2.3" + +mime-db@1.49.0: + version "1.49.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.49.0.tgz#f3dfde60c99e9cf3bc9701d687778f537001cbed" + integrity sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA== + +mime-types@^2.1.27: + version "2.1.32" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.32.tgz#1d00e89e7de7fe02008db61001d9e02852670fd5" + integrity sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A== + dependencies: + mime-db "1.49.0" + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +mini-css-extract-plugin@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.2.0.tgz#48cb6d2bea8fa9eb36709856e003662eebb3eb92" + integrity sha512-91HeVHbq7PUJ4TwOuMTlFWfVWrLqf3SF0PlEDPV+wtgsfxrMebN9LLzflyQqdKLp4/H3PexRB1WLKsCqpWKkxQ== + dependencies: + schema-utils "^3.1.0" + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +nanoid@^3.1.23: + version "3.1.25" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.25.tgz#09ca32747c0e543f0e1814b7d3793477f9c8e152" + integrity sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q== + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +node-releases@^1.1.75: + version "1.1.75" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.75.tgz#6dd8c876b9897a1b8e5a02de26afa79bb54ebbfe" + integrity sha512-Qe5OUajvqrqDSy6wrWFmMwfJ0jVgwiw4T3KqmbTcZ62qW0gQkheXYhcFM1+lOVcGUoRxcEcfyvFMAnDgaF1VWw== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +normalize-url@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" + integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== + +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +nth-check@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.0.tgz#1bb4f6dac70072fc313e8c9cd1417b5074c0a125" + integrity sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q== + dependencies: + boolbase "^1.0.0" + +object-assign@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2, p-limit@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-map@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" + integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-is-inside@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3: + version "2.3.0" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" + integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== + +pify@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= + +pify@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" + integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= + +pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +postcss-calc@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-8.0.0.tgz#a05b87aacd132740a5db09462a3612453e5df90a" + integrity sha512-5NglwDrcbiy8XXfPM11F3HeC6hoT9W7GUH/Zi5U/p7u3Irv4rHhdDcIZwG0llHXV4ftsBjpfWMXAnXNl4lnt8g== + dependencies: + postcss-selector-parser "^6.0.2" + postcss-value-parser "^4.0.2" + +postcss-colormin@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-5.2.0.tgz#2b620b88c0ff19683f3349f4cf9e24ebdafb2c88" + integrity sha512-+HC6GfWU3upe5/mqmxuqYZ9B2Wl4lcoUUNkoaX59nEWV4EtADCMiBqui111Bu8R8IvaZTmqmxrqOAqjbHIwXPw== + dependencies: + browserslist "^4.16.6" + caniuse-api "^3.0.0" + colord "^2.0.1" + postcss-value-parser "^4.1.0" + +postcss-convert-values@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-5.0.1.tgz#4ec19d6016534e30e3102fdf414e753398645232" + integrity sha512-C3zR1Do2BkKkCgC0g3sF8TS0koF2G+mN8xxayZx3f10cIRmTaAnpgpRQZjNekTZxM2ciSPoh2IWJm0VZx8NoQg== + dependencies: + postcss-value-parser "^4.1.0" + +postcss-discard-comments@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-5.0.1.tgz#9eae4b747cf760d31f2447c27f0619d5718901fe" + integrity sha512-lgZBPTDvWrbAYY1v5GYEv8fEO/WhKOu/hmZqmCYfrpD6eyDWWzAOsl2rF29lpvziKO02Gc5GJQtlpkTmakwOWg== + +postcss-discard-duplicates@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-5.0.1.tgz#68f7cc6458fe6bab2e46c9f55ae52869f680e66d" + integrity sha512-svx747PWHKOGpAXXQkCc4k/DsWo+6bc5LsVrAsw+OU+Ibi7klFZCyX54gjYzX4TH+f2uzXjRviLARxkMurA2bA== + +postcss-discard-empty@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-5.0.1.tgz#ee136c39e27d5d2ed4da0ee5ed02bc8a9f8bf6d8" + integrity sha512-vfU8CxAQ6YpMxV2SvMcMIyF2LX1ZzWpy0lqHDsOdaKKLQVQGVP1pzhrI9JlsO65s66uQTfkQBKBD/A5gp9STFw== + +postcss-discard-overridden@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-5.0.1.tgz#454b41f707300b98109a75005ca4ab0ff2743ac6" + integrity sha512-Y28H7y93L2BpJhrdUR2SR2fnSsT+3TVx1NmVQLbcnZWwIUpJ7mfcTC6Za9M2PG6w8j7UQRfzxqn8jU2VwFxo3Q== + +postcss-merge-longhand@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-5.0.2.tgz#277ada51d9a7958e8ef8cf263103c9384b322a41" + integrity sha512-BMlg9AXSI5G9TBT0Lo/H3PfUy63P84rVz3BjCFE9e9Y9RXQZD3+h3YO1kgTNsNJy7bBc1YQp8DmSnwLIW5VPcw== + dependencies: + css-color-names "^1.0.1" + postcss-value-parser "^4.1.0" + stylehacks "^5.0.1" + +postcss-merge-rules@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-5.0.2.tgz#d6e4d65018badbdb7dcc789c4f39b941305d410a" + integrity sha512-5K+Md7S3GwBewfB4rjDeol6V/RZ8S+v4B66Zk2gChRqLTCC8yjnHQ601omj9TKftS19OPGqZ/XzoqpzNQQLwbg== + dependencies: + browserslist "^4.16.6" + caniuse-api "^3.0.0" + cssnano-utils "^2.0.1" + postcss-selector-parser "^6.0.5" + vendors "^1.0.3" + +postcss-minify-font-values@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-5.0.1.tgz#a90cefbfdaa075bd3dbaa1b33588bb4dc268addf" + integrity sha512-7JS4qIsnqaxk+FXY1E8dHBDmraYFWmuL6cgt0T1SWGRO5bzJf8sUoelwa4P88LEWJZweHevAiDKxHlofuvtIoA== + dependencies: + postcss-value-parser "^4.1.0" + +postcss-minify-gradients@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-5.0.2.tgz#7c175c108f06a5629925d698b3c4cf7bd3864ee5" + integrity sha512-7Do9JP+wqSD6Prittitt2zDLrfzP9pqKs2EcLX7HJYxsxCOwrrcLt4x/ctQTsiOw+/8HYotAoqNkrzItL19SdQ== + dependencies: + colord "^2.6" + cssnano-utils "^2.0.1" + postcss-value-parser "^4.1.0" + +postcss-minify-params@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-5.0.1.tgz#371153ba164b9d8562842fdcd929c98abd9e5b6c" + integrity sha512-4RUC4k2A/Q9mGco1Z8ODc7h+A0z7L7X2ypO1B6V8057eVK6mZ6xwz6QN64nHuHLbqbclkX1wyzRnIrdZehTEHw== + dependencies: + alphanum-sort "^1.0.2" + browserslist "^4.16.0" + cssnano-utils "^2.0.1" + postcss-value-parser "^4.1.0" + uniqs "^2.0.0" + +postcss-minify-selectors@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-5.1.0.tgz#4385c845d3979ff160291774523ffa54eafd5a54" + integrity sha512-NzGBXDa7aPsAcijXZeagnJBKBPMYLaJJzB8CQh6ncvyl2sIndLVWfbcDi0SBjRWk5VqEjXvf8tYwzoKf4Z07og== + dependencies: + alphanum-sort "^1.0.2" + postcss-selector-parser "^6.0.5" + +postcss-modules-extract-imports@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d" + integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw== + +postcss-modules-local-by-default@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#ebbb54fae1598eecfdf691a02b3ff3b390a5a51c" + integrity sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ== + dependencies: + icss-utils "^5.0.0" + postcss-selector-parser "^6.0.2" + postcss-value-parser "^4.1.0" + +postcss-modules-scope@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06" + integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg== + dependencies: + postcss-selector-parser "^6.0.4" + +postcss-modules-values@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c" + integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== + dependencies: + icss-utils "^5.0.0" + +postcss-normalize-charset@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-5.0.1.tgz#121559d1bebc55ac8d24af37f67bd4da9efd91d0" + integrity sha512-6J40l6LNYnBdPSk+BHZ8SF+HAkS4q2twe5jnocgd+xWpz/mx/5Sa32m3W1AA8uE8XaXN+eg8trIlfu8V9x61eg== + +postcss-normalize-display-values@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-5.0.1.tgz#62650b965981a955dffee83363453db82f6ad1fd" + integrity sha512-uupdvWk88kLDXi5HEyI9IaAJTE3/Djbcrqq8YgjvAVuzgVuqIk3SuJWUisT2gaJbZm1H9g5k2w1xXilM3x8DjQ== + dependencies: + cssnano-utils "^2.0.1" + postcss-value-parser "^4.1.0" + +postcss-normalize-positions@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-5.0.1.tgz#868f6af1795fdfa86fbbe960dceb47e5f9492fe5" + integrity sha512-rvzWAJai5xej9yWqlCb1OWLd9JjW2Ex2BCPzUJrbaXmtKtgfL8dBMOOMTX6TnvQMtjk3ei1Lswcs78qKO1Skrg== + dependencies: + postcss-value-parser "^4.1.0" + +postcss-normalize-repeat-style@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.0.1.tgz#cbc0de1383b57f5bb61ddd6a84653b5e8665b2b5" + integrity sha512-syZ2itq0HTQjj4QtXZOeefomckiV5TaUO6ReIEabCh3wgDs4Mr01pkif0MeVwKyU/LHEkPJnpwFKRxqWA/7O3w== + dependencies: + cssnano-utils "^2.0.1" + postcss-value-parser "^4.1.0" + +postcss-normalize-string@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-5.0.1.tgz#d9eafaa4df78c7a3b973ae346ef0e47c554985b0" + integrity sha512-Ic8GaQ3jPMVl1OEn2U//2pm93AXUcF3wz+OriskdZ1AOuYV25OdgS7w9Xu2LO5cGyhHCgn8dMXh9bO7vi3i9pA== + dependencies: + postcss-value-parser "^4.1.0" + +postcss-normalize-timing-functions@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.0.1.tgz#8ee41103b9130429c6cbba736932b75c5e2cb08c" + integrity sha512-cPcBdVN5OsWCNEo5hiXfLUnXfTGtSFiBU9SK8k7ii8UD7OLuznzgNRYkLZow11BkQiiqMcgPyh4ZqXEEUrtQ1Q== + dependencies: + cssnano-utils "^2.0.1" + postcss-value-parser "^4.1.0" + +postcss-normalize-unicode@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-5.0.1.tgz#82d672d648a411814aa5bf3ae565379ccd9f5e37" + integrity sha512-kAtYD6V3pK0beqrU90gpCQB7g6AOfP/2KIPCVBKJM2EheVsBQmx/Iof+9zR9NFKLAx4Pr9mDhogB27pmn354nA== + dependencies: + browserslist "^4.16.0" + postcss-value-parser "^4.1.0" + +postcss-normalize-url@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-5.0.2.tgz#ddcdfb7cede1270740cf3e4dfc6008bd96abc763" + integrity sha512-k4jLTPUxREQ5bpajFQZpx8bCF2UrlqOTzP9kEqcEnOfwsRshWs2+oAFIHfDQB8GO2PaUaSE0NlTAYtbluZTlHQ== + dependencies: + is-absolute-url "^3.0.3" + normalize-url "^6.0.1" + postcss-value-parser "^4.1.0" + +postcss-normalize-whitespace@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.0.1.tgz#b0b40b5bcac83585ff07ead2daf2dcfbeeef8e9a" + integrity sha512-iPklmI5SBnRvwceb/XH568yyzK0qRVuAG+a1HFUsFRf11lEJTiQQa03a4RSCQvLKdcpX7XsI1Gen9LuLoqwiqA== + dependencies: + postcss-value-parser "^4.1.0" + +postcss-ordered-values@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-5.0.2.tgz#1f351426977be00e0f765b3164ad753dac8ed044" + integrity sha512-8AFYDSOYWebJYLyJi3fyjl6CqMEG/UVworjiyK1r573I56kb3e879sCJLGvR3merj+fAdPpVplXKQZv+ey6CgQ== + dependencies: + cssnano-utils "^2.0.1" + postcss-value-parser "^4.1.0" + +postcss-reduce-initial@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-5.0.1.tgz#9d6369865b0f6f6f6b165a0ef5dc1a4856c7e946" + integrity sha512-zlCZPKLLTMAqA3ZWH57HlbCjkD55LX9dsRyxlls+wfuRfqCi5mSlZVan0heX5cHr154Dq9AfbH70LyhrSAezJw== + dependencies: + browserslist "^4.16.0" + caniuse-api "^3.0.0" + +postcss-reduce-transforms@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-5.0.1.tgz#93c12f6a159474aa711d5269923e2383cedcf640" + integrity sha512-a//FjoPeFkRuAguPscTVmRQUODP+f3ke2HqFNgGPwdYnpeC29RZdCBvGRGTsKpMURb/I3p6jdKoBQ2zI+9Q7kA== + dependencies: + cssnano-utils "^2.0.1" + postcss-value-parser "^4.1.0" + +postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5: + version "6.0.6" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz#2c5bba8174ac2f6981ab631a42ab0ee54af332ea" + integrity sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + +postcss-svgo@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-5.0.2.tgz#bc73c4ea4c5a80fbd4b45e29042c34ceffb9257f" + integrity sha512-YzQuFLZu3U3aheizD+B1joQ94vzPfE6BNUcSYuceNxlVnKKsOtdo6hL9/zyC168Q8EwfLSgaDSalsUGa9f2C0A== + dependencies: + postcss-value-parser "^4.1.0" + svgo "^2.3.0" + +postcss-unique-selectors@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-5.0.1.tgz#3be5c1d7363352eff838bd62b0b07a0abad43bfc" + integrity sha512-gwi1NhHV4FMmPn+qwBNuot1sG1t2OmacLQ/AX29lzyggnjd+MnVD5uqQmpXO3J17KGL2WAxQruj1qTd3H0gG/w== + dependencies: + alphanum-sort "^1.0.2" + postcss-selector-parser "^6.0.5" + uniqs "^2.0.0" + +postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" + integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== + +postcss@^8.2.15, postcss@^8.3.5: + version "8.3.6" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.6.tgz#2730dd76a97969f37f53b9a6096197be311cc4ea" + integrity sha512-wG1cc/JhRgdqB6WHEuyLTedf3KIRuD0hG6ldkFEZNCjRxiC+3i6kkWUUbiJQayP28iwG35cEmAbe98585BYV0A== + dependencies: + colorette "^1.2.2" + nanoid "^3.1.23" + source-map-js "^0.6.2" + +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +rechoir@^0.7.0: + version "0.7.1" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.7.1.tgz#9478a96a1ca135b5e88fc027f03ee92d6c645686" + integrity sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg== + dependencies: + resolve "^1.9.0" + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve@^1.9.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" + integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== + dependencies: + is-core-module "^2.2.0" + path-parse "^1.0.6" + +rimraf@^2.6.3: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + +safe-buffer@^5.1.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +sass-loader@12.1.0: + version "12.1.0" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-12.1.0.tgz#b73324622231009da6fba61ab76013256380d201" + integrity sha512-FVJZ9kxVRYNZTIe2xhw93n3xJNYZADr+q69/s98l9nTCrWASo+DR2Ot0s5xTKQDDEosUkatsGeHxcH4QBp5bSg== + dependencies: + klona "^2.0.4" + neo-async "^2.6.2" + +sass@1.38.1: + version "1.38.1" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.38.1.tgz#54dfb17fb168846b5850324b82fc62dc68f51bad" + integrity sha512-Lj8nPaSYOuRhgqdyShV50fY5jKnvaRmikUNalMPmbH+tKMGgEKVkltI/lP30PEfO2T1t6R9yc2QIBLgOc3uaFw== + dependencies: + chokidar ">=3.0.0 <4.0.0" + +schema-utils@^3.0.0, schema-utils@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" + integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +semver@^7.3.4, semver@^7.3.5: + version "7.3.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" + integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== + dependencies: + lru-cache "^6.0.0" + +serialize-javascript@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" + integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== + dependencies: + randombytes "^2.1.0" + +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +signal-exit@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" + integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== + +source-map-js@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e" + integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug== + +source-map-support@~0.5.19: + version "0.5.19" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" + integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0, source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +source-map@~0.7.2: + version "0.7.3" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" + integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== + +stable@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" + integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +stylehacks@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-5.0.1.tgz#323ec554198520986806388c7fdaebc38d2c06fb" + integrity sha512-Es0rVnHIqbWzveU1b24kbw92HsebBepxfcqe5iix7t9j0PQqhs0IxXVXv0pY2Bxa08CgMkzD6OWql7kbGOuEdA== + dependencies: + browserslist "^4.16.0" + postcss-selector-parser "^6.0.4" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +svgo@^2.3.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.5.0.tgz#3c9051b606d85a02fcb59f459b19970d2cc2c9bf" + integrity sha512-FSdBOOo271VyF/qZnOn1PgwCdt1v4Dx0Sey+U1jgqm1vqRYjPGdip0RGrFW6ItwtkBB8rHgHk26dlVr0uCs82Q== + dependencies: + "@trysound/sax" "0.1.1" + colorette "^1.3.0" + commander "^7.2.0" + css-select "^4.1.3" + css-tree "^1.1.3" + csso "^4.2.0" + stable "^0.1.8" + +tapable@^2.1.1, tapable@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.0.tgz#5c373d281d9c672848213d0e037d1c4165ab426b" + integrity sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw== + +terser-webpack-plugin@^5.1.3: + version "5.1.4" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.1.4.tgz#c369cf8a47aa9922bd0d8a94fe3d3da11a7678a1" + integrity sha512-C2WkFwstHDhVEmsmlCxrXUtVklS+Ir1A7twrYzrDrQQOIMOaVAYykaoo/Aq1K0QRkMoY2hhvDQY1cm4jnIMFwA== + dependencies: + jest-worker "^27.0.2" + p-limit "^3.1.0" + schema-utils "^3.0.0" + serialize-javascript "^6.0.0" + source-map "^0.6.1" + terser "^5.7.0" + +terser@^5.7.0: + version "5.7.2" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.7.2.tgz#d4d95ed4f8bf735cb933e802f2a1829abf545e3f" + integrity sha512-0Omye+RD4X7X69O0eql3lC4Heh/5iLj3ggxR/B5ketZLOtLiOqukUgjw3q4PDnNQbsrkKr3UMypqStQG3XKRvw== + dependencies: + commander "^2.20.0" + source-map "~0.7.2" + source-map-support "~0.5.19" + +timsort@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" + integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +ts-loader@9.2.5: + version "9.2.5" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.2.5.tgz#127733a5e9243bf6dafcb8aa3b8a266d8041dca9" + integrity sha512-al/ATFEffybdRMUIr5zMEWQdVnCGMUA9d3fXJ8dBVvBlzytPvIszoG9kZoR+94k6/i293RnVOXwMaWbXhNy9pQ== + dependencies: + chalk "^4.1.0" + enhanced-resolve "^5.0.0" + micromatch "^4.0.0" + semver "^7.3.4" + +typescript@4.3.5: + version "4.3.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4" + integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA== + +uniqs@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" + integrity sha1-/+3ks2slKQaW5uFl1KWe25mOawI= + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +util-deprecate@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +v8-compile-cache@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" + integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== + +vendors@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e" + integrity sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w== + +watchpack@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.2.0.tgz#47d78f5415fe550ecd740f99fe2882323a58b1ce" + integrity sha512-up4YAn/XHgZHIxFBVCdlMiWDj6WaLKpwVeGQk2I5thdYxF/KmF0aaz6TfJZ/hfl1h/XlcDr7k1KH7ThDagpFaA== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + +webpack-cli@4.8.0: + version "4.8.0" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-4.8.0.tgz#5fc3c8b9401d3c8a43e2afceacfa8261962338d1" + integrity sha512-+iBSWsX16uVna5aAYN6/wjhJy1q/GKk4KjKvfg90/6hykCTSgozbfz5iRgDTSJt/LgSbYxdBX3KBHeobIs+ZEw== + dependencies: + "@discoveryjs/json-ext" "^0.5.0" + "@webpack-cli/configtest" "^1.0.4" + "@webpack-cli/info" "^1.3.0" + "@webpack-cli/serve" "^1.5.2" + colorette "^1.2.1" + commander "^7.0.0" + execa "^5.0.0" + fastest-levenshtein "^1.0.12" + import-local "^3.0.2" + interpret "^2.2.0" + rechoir "^0.7.0" + v8-compile-cache "^2.2.0" + webpack-merge "^5.7.3" + +webpack-merge@^5.7.3: + version "5.8.0" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.8.0.tgz#2b39dbf22af87776ad744c390223731d30a68f61" + integrity sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q== + dependencies: + clone-deep "^4.0.1" + wildcard "^2.0.0" + +webpack-sources@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.0.tgz#b16973bcf844ebcdb3afde32eda1c04d0b90f89d" + integrity sha512-fahN08Et7P9trej8xz/Z7eRu8ltyiygEo/hnRi9KqBUs80KeDcnf96ZJo++ewWd84fEf3xSX9bp4ZS9hbw0OBw== + +webpack@5.51.1: + version "5.51.1" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.51.1.tgz#41bebf38dccab9a89487b16dbe95c22e147aac57" + integrity sha512-xsn3lwqEKoFvqn4JQggPSRxE4dhsRcysWTqYABAZlmavcoTmwlOb9b1N36Inbt/eIispSkuHa80/FJkDTPos1A== + dependencies: + "@types/eslint-scope" "^3.7.0" + "@types/estree" "^0.0.50" + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/wasm-edit" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + acorn "^8.4.1" + acorn-import-assertions "^1.7.6" + browserslist "^4.14.5" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.8.0" + es-module-lexer "^0.7.1" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.4" + json-parse-better-errors "^1.0.2" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.1.0" + tapable "^2.1.1" + terser-webpack-plugin "^5.1.3" + watchpack "^2.2.0" + webpack-sources "^3.2.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wildcard@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" + integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yaml@^1.10.2: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==