From b99ea273c42ce7ecd5f729777433770949112c1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Flores?= <raphael.flores@inrae.fr> Date: Mon, 2 Aug 2021 16:00:17 +0200 Subject: [PATCH 01/16] Fix deployment variable and jobs. --- .secrets.baseline | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.secrets.baseline b/.secrets.baseline index 58672400..84b78e9c 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,11 @@ "files": "frontend/package-lock.json|^.secrets.baseline$", "lines": null }, +<<<<<<< HEAD "generated_at": "2021-08-02T15:23:45Z", +======= + "generated_at": "2021-08-02T14:00:12Z", +>>>>>>> 1edcdd6 (Fix deployment variable and jobs.) "plugins_used": [ { "name": "AWSKeyDetector" @@ -58,14 +62,18 @@ "hashed_secret": "dd447c7c799dd4ebaacca8f0ad3da45a097d7211", "is_secret": false, "is_verified": false, - "line_number": 228, + "line_number": 204, "type": "Base64 High Entropy String" }, { "hashed_secret": "8074db38f8a8acec1a147bc5daf2799ff6693fff", "is_secret": false, "is_verified": false, +<<<<<<< HEAD "line_number": 247, +======= + "line_number": 221, +>>>>>>> 1edcdd6 (Fix deployment variable and jobs.) "type": "Base64 High Entropy String" } ], -- GitLab From 56851da214f348c8646f2d89af3aeb6b9e731413 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Flores?= <raphael.flores@inrae.fr> Date: Mon, 2 Aug 2021 16:53:47 +0200 Subject: [PATCH 02/16] Change cache policy. --- .secrets.baseline | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.secrets.baseline b/.secrets.baseline index 84b78e9c..cbb5acf0 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,11 +3,15 @@ "files": "frontend/package-lock.json|^.secrets.baseline$", "lines": null }, +<<<<<<< HEAD <<<<<<< HEAD "generated_at": "2021-08-02T15:23:45Z", ======= "generated_at": "2021-08-02T14:00:12Z", >>>>>>> 1edcdd6 (Fix deployment variable and jobs.) +======= + "generated_at": "2021-08-02T14:53:42Z", +>>>>>>> 998abab (Change cache policy.) "plugins_used": [ { "name": "AWSKeyDetector" @@ -55,25 +59,37 @@ "hashed_secret": "2907dcd1b70a82032e52be9b6b804abbb4a7525e", "is_secret": false, "is_verified": false, +<<<<<<< HEAD "line_number": 127, +======= + "line_number": 125, +>>>>>>> 998abab (Change cache policy.) "type": "Base64 High Entropy String" }, { "hashed_secret": "dd447c7c799dd4ebaacca8f0ad3da45a097d7211", "is_secret": false, "is_verified": false, +<<<<<<< HEAD "line_number": 204, +======= + "line_number": 226, +>>>>>>> 998abab (Change cache policy.) "type": "Base64 High Entropy String" }, { "hashed_secret": "8074db38f8a8acec1a147bc5daf2799ff6693fff", "is_secret": false, "is_verified": false, +<<<<<<< HEAD <<<<<<< HEAD "line_number": 247, ======= "line_number": 221, >>>>>>> 1edcdd6 (Fix deployment variable and jobs.) +======= + "line_number": 245, +>>>>>>> 998abab (Change cache policy.) "type": "Base64 High Entropy String" } ], -- GitLab From 45af6fb4312f8a460bc2d6b63d5d638f35a3c2a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Flores?= <raphael.flores@inrae.fr> Date: Mon, 2 Aug 2021 17:04:40 +0200 Subject: [PATCH 03/16] Fix ES version --- .secrets.baseline | 32 ++++---------------------------- 1 file changed, 4 insertions(+), 28 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index cbb5acf0..514c5ecc 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,15 +3,7 @@ "files": "frontend/package-lock.json|^.secrets.baseline$", "lines": null }, -<<<<<<< HEAD -<<<<<<< HEAD - "generated_at": "2021-08-02T15:23:45Z", -======= - "generated_at": "2021-08-02T14:00:12Z", ->>>>>>> 1edcdd6 (Fix deployment variable and jobs.) -======= - "generated_at": "2021-08-02T14:53:42Z", ->>>>>>> 998abab (Change cache policy.) + "generated_at": "2021-08-02T15:04:37Z", "plugins_used": [ { "name": "AWSKeyDetector" @@ -59,37 +51,21 @@ "hashed_secret": "2907dcd1b70a82032e52be9b6b804abbb4a7525e", "is_secret": false, "is_verified": false, -<<<<<<< HEAD - "line_number": 127, -======= - "line_number": 125, ->>>>>>> 998abab (Change cache policy.) + "line_number": 126, "type": "Base64 High Entropy String" }, { "hashed_secret": "dd447c7c799dd4ebaacca8f0ad3da45a097d7211", "is_secret": false, "is_verified": false, -<<<<<<< HEAD - "line_number": 204, -======= - "line_number": 226, ->>>>>>> 998abab (Change cache policy.) + "line_number": 227, "type": "Base64 High Entropy String" }, { "hashed_secret": "8074db38f8a8acec1a147bc5daf2799ff6693fff", "is_secret": false, "is_verified": false, -<<<<<<< HEAD -<<<<<<< HEAD - "line_number": 247, -======= - "line_number": 221, ->>>>>>> 1edcdd6 (Fix deployment variable and jobs.) -======= - "line_number": 245, ->>>>>>> 998abab (Change cache policy.) + "line_number": 246, "type": "Base64 High Entropy String" } ], -- GitLab From 468379883973563f9cdc8c6ac7ddd250bda059c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Flores?= <raphael.flores@inrae.fr> Date: Mon, 2 Aug 2021 17:24:19 +0200 Subject: [PATCH 04/16] Configure docker MTU --- .secrets.baseline | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index 514c5ecc..58672400 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "frontend/package-lock.json|^.secrets.baseline$", "lines": null }, - "generated_at": "2021-08-02T15:04:37Z", + "generated_at": "2021-08-02T15:23:45Z", "plugins_used": [ { "name": "AWSKeyDetector" @@ -51,21 +51,21 @@ "hashed_secret": "2907dcd1b70a82032e52be9b6b804abbb4a7525e", "is_secret": false, "is_verified": false, - "line_number": 126, + "line_number": 127, "type": "Base64 High Entropy String" }, { "hashed_secret": "dd447c7c799dd4ebaacca8f0ad3da45a097d7211", "is_secret": false, "is_verified": false, - "line_number": 227, + "line_number": 228, "type": "Base64 High Entropy String" }, { "hashed_secret": "8074db38f8a8acec1a147bc5daf2799ff6693fff", "is_secret": false, "is_verified": false, - "line_number": 246, + "line_number": 247, "type": "Base64 High Entropy String" } ], -- GitLab From 8b3c2c742ecb6d8d192a781aa8e20f65144179b0 Mon Sep 17 00:00:00 2001 From: jnizet <jb@ninja-squad.com> Date: Fri, 13 Aug 2021 11:54:27 +0200 Subject: [PATCH 05/16] chore: remove ide files and simplify gitignore --- .gitignore | 46 +-------------------------------------- .idea/compiler.xml | 15 ------------- .idea/encodings.xml | 4 ---- .idea/misc.xml | 9 -------- .idea/modules.xml | 14 ------------ .idea/vcs.xml | 6 ----- backend/src/main/main.iml | 39 --------------------------------- backend/src/test/test.iml | 12 ---------- 8 files changed, 1 insertion(+), 144 deletions(-) delete mode 100644 .idea/compiler.xml delete mode 100644 .idea/encodings.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/vcs.xml delete mode 100644 backend/src/main/main.iml delete mode 100644 backend/src/test/test.iml diff --git a/.gitignore b/.gitignore index 0648667b..897c8ff3 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 diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index ca99a00e..00000000 --- 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 15a15b21..00000000 --- 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 ff8249e4..00000000 --- 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 3b2a562b..00000000 --- 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 94a25f7f..00000000 --- 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/src/main/main.iml b/backend/src/main/main.iml deleted file mode 100644 index 50f3d6bc..00000000 --- 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/test/test.iml b/backend/src/test/test.iml deleted file mode 100644 index 6e30bb1c..00000000 --- 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> -- GitLab From 01d4324a53d8ceec916037ae0333789535dceebe Mon Sep 17 00:00:00 2001 From: jnizet <jb@ninja-squad.com> Date: Fri, 13 Aug 2021 12:27:13 +0200 Subject: [PATCH 06/16] feat: setup thymeleaf - remove the frontend project from gradle - remove the angular filter - create a basic layout and home page using thymeleaf --- backend/build.gradle.kts | 5 +- .../faidare/filter/AngularRouteFilter.java | 115 ------------------ .../inra/urgi/faidare/web/HomeController.java | 19 +++ .../static/assets/images/favicon.ico | Bin 0 -> 1150 bytes .../src/main/resources/templates/index.html | 18 +++ .../main/resources/templates/layout/main.html | 29 +++++ settings.gradle.kts | 2 +- 7 files changed, 68 insertions(+), 120 deletions(-) delete mode 100644 backend/src/main/java/fr/inra/urgi/faidare/filter/AngularRouteFilter.java create mode 100644 backend/src/main/java/fr/inra/urgi/faidare/web/HomeController.java create mode 100644 backend/src/main/resources/static/assets/images/favicon.ico create mode 100644 backend/src/main/resources/templates/index.html create mode 100644 backend/src/main/resources/templates/layout/main.html diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts index 9ff14054..586fc953 100644 --- a/backend/build.gradle.kts +++ b/backend/build.gradle.kts @@ -57,12 +57,8 @@ tasks { val bootJar by getting(BootJar::class) { archiveName = "${rootProject.name}.jar" - dependsOn(":frontend:assemble") dependsOn(buildInfo) - into("BOOT-INF/classes/static") { - from("${project(":frontend").projectDir}/dist/frontend") - } into("BOOT-INF/classes/META-INF") { from(buildInfo.destinationDir) } @@ -98,6 +94,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/filter/AngularRouteFilter.java b/backend/src/main/java/fr/inra/urgi/faidare/filter/AngularRouteFilter.java deleted file mode 100644 index 81978e5f..00000000 --- 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/web/HomeController.java b/backend/src/main/java/fr/inra/urgi/faidare/web/HomeController.java new file mode 100644 index 00000000..6734b6aa --- /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/resources/static/assets/images/favicon.ico b/backend/src/main/resources/static/assets/images/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..7fc066f132eb58eeaf30b6259da59f5ed92aca8e GIT binary patch literal 1150 zcma))OH30%7{|vT5{MBn!Pm(X(TkCTfe;f98so)_4+sz%j3J5-f{8{63W-1fHIYXF zr3Hdg5J4&BEp*GHg;E}+yb7fhDlD`VTG|5JZg;6O#Yq}Y?B?75_y5iJ&Fsz&07u{* z6a?V&Ja~K*fKvc~3osypWS>I;0FE8J#62mJ^^K_rx7!VHSa;3m7EPLKG(-DghScud z4OVUFuiI%yFy_LRyJW5Co_WKv5Qdm~SswFA^u&?boa`=;<f*Nc^(^8#3mKl&U1W;t z%y-8UeADcf@e1N_?Z_J89lM}4!KgPVZ<V$_+nm(PI3~Lx^yiqMx>wu#l4e|fsS>X4 zw?>-IpCPycts;ZdSEF-6j^-eZ${Ur!XsV<!mK~jMe}NbZhGl%v`ts#*PQ|#e3bA^v zJRNE7gyee%IUkQ}msS<Gd7=ax?Q#TQhLt~3_v^29?J~o%K56nF17^qenK`Q|EL}y^ z;~FQub3)XgDaPJgKT69K$agM{Jx!5T&2XoQGInndBo2v&aGk|Qh7zNt71(?0XBl6L zyo{WVLGLeFor^&}X}HzHQEsuVgi1!g@Vxs_&hD|GV4%DY^L$l%4c<5Je#&V(*&@qn z!*y7<;QsN3YZ{TCFfHDEATBZ2EeyX!dyyy?dA<wR)^@*|Vpx~2YN<KJN!tSz(*-;w zIT6(|+m(rG8Z`ayYc;a$O3gs-l*JH{HQDR=C%B&&<MC$n{hR@HSz)dCzD>W@7hO8Y zm*~v;NXq3r?wLb7l!ml$M#?4DbWyk$scB1p>1*3_<MM{)D$O_p{>-*#7xfp*(U@SW zgS$ghfewcDhvzpiu1~N;F)n}aev~UTZLdExJz}$3Qs|t{^mT*Ha1rqaHIXNrmwL`l zH9UXi8hP%)UhG#kwGm4F;6oc_J&l>wu^o~uE+*%<*1|bGm88YF8-m9Ut4-R$)VQu9 zs$Mtp=CD29buN}UR-kIHrdh@p^9(lt5z7FSOaq`?0Kn1)Lj;5PpF?l{27vJ$0K4%3 MIAD$21cTa-f5P$oj{pDw literal 0 HcmV?d00001 diff --git a/backend/src/main/resources/templates/index.html b/backend/src/main/resources/templates/index.html new file mode 100644 index 00000000..ae925eac --- /dev/null +++ b/backend/src/main/resources/templates/index.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> + +<html + xmlns:th="http://www.thymeleaf.org" + xmlns:biom="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 00000000..a8f2c38c --- /dev/null +++ b/backend/src/main/resources/templates/layout/main.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<html lang="fr" th:fragment="layout (title, content)" 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 href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous"> + + <link rel="shortcut icon" th:href="@{/static/assets/images/favicon.ico}" type="image/x-icon" /> + </head> + + <body> + <div class="container"> + <header> + Common header + </header> + + <div th:replace="${content}"> + <p>Layout content</p> + </div> + + <footer> + common footer + </footer> + </div> + </body> +</html> diff --git a/settings.gradle.kts b/settings.gradle.kts index 4bd5a482..59e6a72b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,2 +1,2 @@ rootProject.name = "faidare" -include("backend", "frontend") +include("backend") -- GitLab From 9675514a0ce17ec848eaa39a8f01d81a48707d2a Mon Sep 17 00:00:00 2001 From: jnizet <jb@ninja-squad.com> Date: Wed, 1 Sep 2021 10:32:37 +0200 Subject: [PATCH 07/16] ci: adapt the ci config now that frontend is gone --- .gitlab-ci.yml | 33 ++---- .../filter/AngularRouteFilterTest.java | 105 ------------------ 2 files changed, 7 insertions(+), 131 deletions(-) delete mode 100644 backend/src/test/java/fr/inra/urgi/faidare/filter/AngularRouteFilterTest.java diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b5511df7..7df34804 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/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 b71e9e39..00000000 --- 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)); - } - -} -- GitLab From 21d252aed67ed5fa927a1f85eb527918b06bc806 Mon Sep 17 00:00:00 2001 From: jnizet <jb@ninja-squad.com> Date: Fri, 13 Aug 2021 18:53:38 +0200 Subject: [PATCH 08/16] feat: start working on site card xrefs missing, latitude/longitude TODO --- .../urgi/faidare/web/site/SiteController.java | 61 ++++++++++ .../inra/urgi/faidare/web/site/SiteModel.java | 106 ++++++++++++++++++ .../main/resources/static/assets/style.css | 3 + .../resources/templates/fragments/row.html | 21 ++++ .../main/resources/templates/layout/main.html | 1 + .../src/main/resources/templates/site.html | 66 +++++++++++ 6 files changed, 258 insertions(+) create mode 100644 backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteController.java create mode 100644 backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteModel.java create mode 100644 backend/src/main/resources/static/assets/style.css create mode 100644 backend/src/main/resources/templates/fragments/row.html create mode 100644 backend/src/main/resources/templates/site.html 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 00000000..d39535d8 --- /dev/null +++ b/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteController.java @@ -0,0 +1,61 @@ +package fr.inra.urgi.faidare.web.site; + +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.repository.es.LocationRepository; +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 +@RequestMapping("/sites") +public class SiteController { + + private final LocationRepository locationRepository; + private final FaidareProperties faidareProperties; + + public SiteController(LocationRepository locationRepository, + FaidareProperties faidareProperties) { + this.locationRepository = locationRepository; + this.faidareProperties = faidareProperties; + } + + @GetMapping("/{siteId}") + public ModelAndView site(@PathVariable("siteId") String siteId) { + // LocationVO site = locationRepository.getById(siteId); + 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()))); + } + + 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; + } +} 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 00000000..839f7446 --- /dev/null +++ b/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteModel.java @@ -0,0 +1,106 @@ +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; + +/** + * TODO add javadoc + * + * @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<Map.Entry<String, Object>> additionalInfoProperties; + + public SiteModel(LocationVO site, DataSource source) { + this.site = site; + this.source = source; + this.additionalInfo = site.getAdditionalInfo() == null ? Collections.emptyMap() : site.getAdditionalInfo().getProperties(); + 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; + } +} diff --git a/backend/src/main/resources/static/assets/style.css b/backend/src/main/resources/static/assets/style.css new file mode 100644 index 00000000..59bcd117 --- /dev/null +++ b/backend/src/main/resources/static/assets/style.css @@ -0,0 +1,3 @@ +.label { + font-weight: 500; +} 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 00000000..5b523cef --- /dev/null +++ b/backend/src/main/resources/templates/fragments/row.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> + +<html xmlns:th="http://www.thymeleaf.org"> + +<body> + +<div th:fragment="row(label, content)" class="row py-2"> + <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> + +<div th:fragment="text-row(label, text)" th:if="${!#strings.isEmpty(text)}" class="row py-2"> + <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/layout/main.html b/backend/src/main/resources/templates/layout/main.html index a8f2c38c..22bcdfd5 100644 --- a/backend/src/main/resources/templates/layout/main.html +++ b/backend/src/main/resources/templates/layout/main.html @@ -7,6 +7,7 @@ <meta content="width=device-width, initial-scale=1" name="viewport" /> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous"> + <link th:href="@{/assets/style.css}" rel="stylesheet"> <link rel="shortcut icon" th:href="@{/static/assets/images/favicon.ico}" type="image/x-icon" /> </head> diff --git a/backend/src/main/resources/templates/site.html b/backend/src/main/resources/templates/site.html new file mode 100644 index 00000000..abcd0dbd --- /dev/null +++ b/backend/src/main/resources/templates/site.html @@ -0,0 +1,66 @@ +<!DOCTYPE html> + +<html + xmlns:th="http://www.thymeleaf.org" + xmlns:biom="http://www.thymeleaf.org" + th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main})}" +> +<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:if="${model.site.uri && !model.site.uri.startsWith('urn:')}" + th:replace="fragments/row::text-row(label='Permanent unique identifier', text=${model.site.uri})"> + </div> + + <div th:if="${model.source}" + th:replace="fragments/row::row(label='Source', content=~{::#source}, text='')"> + <a id="source" target="_blank" th:href="${model.source.url}"> + <img style="max-height: 60px;" th:src="${model.source.image}" th:alt="${model.source.name} + ' logo'" /> + </a> + </div> + + <div th:if="${model.site.url && model.source}" + th:replace="fragments/row::row(label='Data link', content=~{::#url}, text='')"> + <a id="url" target="_blank" th:href="${model.site.url}"> + Link to this site on <th:block th:text="${model.source.name}" /> + </a> + </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> + <div th:if="${model.site.latitude}" + th:replace="fragments/row::text-row(label='Latitude', text=${model.site.latitude + ' - TODO in degrees'})"></div> + <div th:if="${model.site.longitude}" + th:replace="fragments/row::text-row(label='Longitude', text=${model.site.longitude + ' - TODO in degrees'})"></div> + <div th:replace="fragments/row::text-row(label='Geographical location', text=${model.geographicalLocation})"></div> + <div th:if="${model.site.countryName && !model.geographicalLocation}" + th:replace="fragments/row::text-row(label='Country name', text=${model.site.countryName})"></div> + <div th:if="${model.site.countryCode && !model.geographicalLocation}" + th:replace="fragments/row::text-row(label='Country code', text=${model.site.countryName})"></div> + <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> + + <h2>Additional info</h2> + <th:block th:each="prop : ${model.additionalInfoProperties}"> + <div th:replace="fragments/row::text-row(label=${prop.key}, text=${prop.value})"></div> + </th:block> + +</main> +</body> +</html> -- GitLab From 8ddd89536ed0eb698b832109d79e572ea13109e6 Mon Sep 17 00:00:00 2001 From: jnizet <jb@ninja-squad.com> Date: Wed, 25 Aug 2021 10:00:40 +0200 Subject: [PATCH 09/16] feat: add xrefs and latitude/longitude to site card --- .../xref/XRefDocumentSearchCriteria.java | 7 ++ .../urgi/faidare/web/site/SiteController.java | 43 +++++++++-- .../inra/urgi/faidare/web/site/SiteModel.java | 11 ++- .../faidare/web/thymeleaf/Coordinates.java | 71 +++++++++++++++++++ .../web/thymeleaf/CoordinatesDialect.java | 28 ++++++++ .../CoordinatesExpressionFactory.java | 36 ++++++++++ .../resources/templates/fragments/xrefs.html | 37 ++++++++++ .../src/main/resources/templates/site.html | 13 ++-- 8 files changed, 236 insertions(+), 10 deletions(-) create mode 100644 backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/Coordinates.java create mode 100644 backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/CoordinatesDialect.java create mode 100644 backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/CoordinatesExpressionFactory.java create mode 100644 backend/src/main/resources/templates/fragments/xrefs.html 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 7ca7b26a..6dde515e 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/web/site/SiteController.java b/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteController.java index d39535d8..b6fecd50 100644 --- 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 @@ -1,10 +1,17 @@ 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.response.PaginatedList; +import fr.inra.urgi.faidare.domain.xref.XRefDocumentSearchCriteria; +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; @@ -21,24 +28,42 @@ public class SiteController { private final LocationRepository locationRepository; private final FaidareProperties faidareProperties; + private final XRefDocumentRepository xRefDocumentRepository; public SiteController(LocationRepository locationRepository, - FaidareProperties faidareProperties) { + FaidareProperties faidareProperties, + XRefDocumentRepository xRefDocumentRepository) { this.locationRepository = locationRepository; this.faidareProperties = faidareProperties; + this.xRefDocumentRepository = xRefDocumentRepository; } @GetMapping("/{siteId}") public ModelAndView site(@PathVariable("siteId") String siteId) { - // LocationVO site = locationRepository.getById(siteId); - LocationVO site = createSite(); + 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()))); + new SiteModel( + site, + faidareProperties.getByUri(site.getSourceUri()), + crossReferences + ) + ); } private LocationVO createSite() { @@ -58,4 +83,14 @@ public class SiteController { 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 index 839f7446..900dca16 100644 --- 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 @@ -10,6 +10,7 @@ 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; /** * TODO add javadoc @@ -32,12 +33,16 @@ public final class SiteModel { 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) { + 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() @@ -103,4 +108,8 @@ public final class SiteModel { public List<Map.Entry<String, Object>> getAdditionalInfoProperties() { return additionalInfoProperties; } + + public List<XRefDocumentVO> getCrossReferences() { + return crossReferences; + } } 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 00000000..554929b1 --- /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 00000000..d443e589 --- /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 00000000..e3c25cc8 --- /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/resources/templates/fragments/xrefs.html b/backend/src/main/resources/templates/fragments/xrefs.html new file mode 100644 index 00000000..d51f1186 --- /dev/null +++ b/backend/src/main/resources/templates/fragments/xrefs.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> + +<html xmlns:th="http://www.thymeleaf.org"> + +<body> + +<div th:fragment="xrefs(crossReferences)" th:if="${!#lists.isEmpty(crossReferences)}"> + <h2>Cross references</h2> + + <div class="table-responsive scroll-big-table table-card-body"> + <div class="card"> + <table class="table table-sm table-striped"> + <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 th:text="${#strings.abbreviate(crossRef.description, 120)}"></td> + </tr> + </tbody> + </table> + </div> + </div> + +</div> + +</body> + +</html> diff --git a/backend/src/main/resources/templates/site.html b/backend/src/main/resources/templates/site.html index abcd0dbd..e9867c75 100644 --- a/backend/src/main/resources/templates/site.html +++ b/backend/src/main/resources/templates/site.html @@ -39,9 +39,9 @@ <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> <div th:if="${model.site.latitude}" - th:replace="fragments/row::text-row(label='Latitude', text=${model.site.latitude + ' - TODO in degrees'})"></div> + th:replace="fragments/row::text-row(label='Latitude', text=${#coordinates.formatLatitude(model.site.latitude)})"></div> <div th:if="${model.site.longitude}" - th:replace="fragments/row::text-row(label='Longitude', text=${model.site.longitude + ' - TODO in degrees'})"></div> + th:replace="fragments/row::text-row(label='Longitude', text=${#coordinates.formatLongitude(model.site.longitude)})"></div> <div th:replace="fragments/row::text-row(label='Geographical location', text=${model.geographicalLocation})"></div> <div th:if="${model.site.countryName && !model.geographicalLocation}" th:replace="fragments/row::text-row(label='Country name', text=${model.site.countryName})"></div> @@ -56,11 +56,14 @@ <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> - <h2>Additional info</h2> - <th:block th:each="prop : ${model.additionalInfoProperties}"> - <div th:replace="fragments/row::text-row(label=${prop.key}, text=${prop.value})"></div> + <th:block th:if="${!#lists.isEmpty(model.additionalInfoProperties)}"> + <h2>Additional info</h2> + <th:block th:each="prop : ${model.additionalInfoProperties}"> + <div th:replace="fragments/row::text-row(label=${prop.key}, text=${prop.value})"></div> + </th:block> </th:block> + <div th:replace="fragments/xrefs::xrefs(crossReferences=${model.crossReferences})"></div> </main> </body> </html> -- GitLab From 698ba5f35f2b80794cee14cd600fb286e4a5c0b5 Mon Sep 17 00:00:00 2001 From: jnizet <jb@ninja-squad.com> Date: Wed, 25 Aug 2021 14:28:33 +0200 Subject: [PATCH 10/16] fix: fix tests in site card --- .../urgi/faidare/web/site/SiteController.java | 4 +- .../inra/urgi/faidare/web/site/SiteModel.java | 3 +- .../src/main/resources/templates/index.html | 1 - .../src/main/resources/templates/site.html | 58 +++++++++++-------- 4 files changed, 35 insertions(+), 31 deletions(-) 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 index b6fecd50..1ed29438 100644 --- 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 @@ -7,8 +7,6 @@ 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.response.PaginatedList; -import fr.inra.urgi.faidare.domain.xref.XRefDocumentSearchCriteria; import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO; import fr.inra.urgi.faidare.repository.es.LocationRepository; import fr.inra.urgi.faidare.repository.es.XRefDocumentRepository; @@ -22,7 +20,7 @@ import org.springframework.web.servlet.ModelAndView; * Controller used to display a site card based on its ID. * @author JB Nizet */ -@Controller +@Controller("webSiteController") @RequestMapping("/sites") public class SiteController { 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 index 900dca16..84cd92e2 100644 --- 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 @@ -13,8 +13,7 @@ import fr.inra.urgi.faidare.domain.datadiscovery.data.DataSource; import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO; /** - * TODO add javadoc - * + * The model used py the site page * @author JB Nizet */ public final class SiteModel { diff --git a/backend/src/main/resources/templates/index.html b/backend/src/main/resources/templates/index.html index ae925eac..49f401b0 100644 --- a/backend/src/main/resources/templates/index.html +++ b/backend/src/main/resources/templates/index.html @@ -2,7 +2,6 @@ <html xmlns:th="http://www.thymeleaf.org" - xmlns:biom="http://www.thymeleaf.org" th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main})}" > <head> diff --git a/backend/src/main/resources/templates/site.html b/backend/src/main/resources/templates/site.html index e9867c75..ad6affc4 100644 --- a/backend/src/main/resources/templates/site.html +++ b/backend/src/main/resources/templates/site.html @@ -2,7 +2,6 @@ <html xmlns:th="http://www.thymeleaf.org" - xmlns:biom="http://www.thymeleaf.org" th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main})}" > <head> @@ -14,23 +13,26 @@ <main> <h1>Site <th:block th:text="${model.site.locationName}" /></h1> - <div th:if="${model.site.uri && !model.site.uri.startsWith('urn:')}" - th:replace="fragments/row::text-row(label='Permanent unique identifier', text=${model.site.uri})"> - </div> + <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:if="${model.source}" - th:replace="fragments/row::row(label='Source', content=~{::#source}, text='')"> - <a id="source" target="_blank" th:href="${model.source.url}"> - <img style="max-height: 60px;" th:src="${model.source.image}" th:alt="${model.source.name} + ' logo'" /> - </a> - </div> + <th:block th:if="${model.source != null}"> + <div th:replace="fragments/row::row(label='Source', content=~{::#source})"> + <a id="source" target="_blank" th:href="${model.source.url}"> + <img style="max-height: 60px;" th:src="${model.source.image}" th:alt="${model.source.name} + ' logo'" /> + </a> + </div> + </th:block> - <div th:if="${model.site.url && model.source}" - th:replace="fragments/row::row(label='Data link', content=~{::#url}, text='')"> - <a id="url" target="_blank" th:href="${model.site.url}"> - Link to this site on <th:block th:text="${model.source.name}" /> - </a> - </div> + <th:block th:if="${model.site.url != null && model.source != null}"> + <div + th:replace="fragments/row::row(label='Data link', content=~{::#url})"> + <a id="url" target="_blank" th:href="${model.site.url}"> + Link to this site on <th:block th:text="${model.source.name}" /> + </a> + </div> + </th:block> <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> @@ -38,15 +40,21 @@ <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> - <div th:if="${model.site.latitude}" - th:replace="fragments/row::text-row(label='Latitude', text=${#coordinates.formatLatitude(model.site.latitude)})"></div> - <div th:if="${model.site.longitude}" - th:replace="fragments/row::text-row(label='Longitude', text=${#coordinates.formatLongitude(model.site.longitude)})"></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> - <div th:if="${model.site.countryName && !model.geographicalLocation}" - th:replace="fragments/row::text-row(label='Country name', text=${model.site.countryName})"></div> - <div th:if="${model.site.countryCode && !model.geographicalLocation}" - th:replace="fragments/row::text-row(label='Country code', text=${model.site.countryName})"></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> @@ -56,7 +64,7 @@ <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> - <th:block th:if="${!#lists.isEmpty(model.additionalInfoProperties)}"> + <th:block th:unless="${#lists.isEmpty(model.additionalInfoProperties)}"> <h2>Additional info</h2> <th:block th:each="prop : ${model.additionalInfoProperties}"> <div th:replace="fragments/row::text-row(label=${prop.key}, text=${prop.value})"></div> -- GitLab From 5c77f169242500937da2137540c4dc85d6a20ad8 Mon Sep 17 00:00:00 2001 From: jnizet <jb@ninja-squad.com> Date: Wed, 25 Aug 2021 14:29:06 +0200 Subject: [PATCH 11/16] feat: study card --- .../faidare/web/study/StudyController.java | 137 ++++++++++++ .../urgi/faidare/web/study/StudyModel.java | 82 +++++++ .../src/main/resources/templates/study.html | 206 ++++++++++++++++++ 3 files changed, 425 insertions(+) create mode 100644 backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyController.java create mode 100644 backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyModel.java create mode 100644 backend/src/main/resources/templates/study.html 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 00000000..35767b09 --- /dev/null +++ b/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyController.java @@ -0,0 +1,137 @@ +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.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.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 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; + + public StudyController(StudyRepository studyRepository, + FaidareProperties faidareProperties, + XRefDocumentRepository xRefDocumentRepository, + GermplasmRepository germplasmRepository, + CropOntologyRepository cropOntologyRepository, + TrialRepository trialRepository) { + this.studyRepository = studyRepository; + this.faidareProperties = faidareProperties; + this.xRefDocumentRepository = xRefDocumentRepository; + this.germplasmRepository = germplasmRepository; + this.cropOntologyRepository = cropOntologyRepository; + this.trialRepository = trialRepository; + } + + @GetMapping("/{studyId}") + public ModelAndView site(@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); + + return new ModelAndView("study", + "model", + new StudyModel( + study, + faidareProperties.getByUri(study.getSourceUri()), + germplasms, + variables, + trials, + crossReferences + ) + ); + } + + 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 00000000..980c78d4 --- /dev/null +++ b/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyModel.java @@ -0,0 +1,82 @@ +package fr.inra.urgi.faidare.web.study; + +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.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; + +/** + * 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 List<Map.Entry<String, Object>> additionalInfoProperties; + + public StudyModel(StudyDetailVO study, + DataSource source, + List<GermplasmVO> germplasms, + List<ObservationVariableVO> variables, + List<TrialVO> trials, + List<XRefDocumentVO> crossReferences) { + this.study = study; + this.source = source; + this.germplasms = germplasms; + this.variables = variables; + this.trials = trials; + this.crossReferences = crossReferences; + + 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; + } +} diff --git a/backend/src/main/resources/templates/study.html b/backend/src/main/resources/templates/study.html new file mode 100644 index 00000000..a1cafc65 --- /dev/null +++ b/backend/src/main/resources/templates/study.html @@ -0,0 +1,206 @@ +<!DOCTYPE html> + +<html + xmlns:th="http://www.thymeleaf.org" + th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main})}" +> +<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> + + <h2>Identification</h2> + + <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> + + <th:block th:if="${model.source != null}"> + <div th:replace="fragments/row::row(label='Source', content=~{::#source}, text='')"> + <a id="source" target="_blank" th:href="${model.source.url}"> + <img style="max-height: 60px;" th:src="${model.source.image}" th:alt="${model.source.name} + ' logo'" /> + </a> + </div> + </th:block> + + <th:block th:if="${model.study.url != null && model.source != null}"> + <div th:replace="fragments/row::row(label='Data link', content=~{::#url}, text='')"> + <a id="url" target="_blank" th:href="${model.study.url}"> + Link to this study on <th:block th:text="${model.source.name}" /> + </a> + </div> + </th:block> + + <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> + + <th:block th:unles="${#lists.isEmpty(model.germplasms)}"> + <h2>Genotype</h2> + + <div class="table-responsive scroll-table table-card-body"> + <div class="card"> + <table class="table table-sm table-striped"> + <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> + </th:block> + + <th:block th:unless="${#lists.isEmpty(model.variables)}"> + <h2>Variables</h2> + <div class="table-responsive scroll-table table-card-body"> + <div class="card"> + <table class="table table-sm table-striped"> + <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> + </th:block> + + <th:block th:unless="${#lists.isEmpty(model.trials)}"> + <h2>Data Set</h2> + <div class="table-responsive scroll-big-table table-card-body"> + <div class="card"> + <table class="table table-sm table-striped"> + <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> + </th:block> + + <th:block th:unless="${#lists.isEmpty(model.study.contacts)}"> + <h2>Contact</h2> + <div class="table-responsive scroll-table table-card-body"> + <div class="card"> + <table class="table table-sm table-striped"> + <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> + </th:block> + + <th:block th:unless="${#lists.isEmpty(model.additionalInfoProperties)}"> + <h2>Additional information</h2> + <div class="table-responsive scroll-table table-card-body"> + <div class="card"> + <table class="table table-sm table-striped"> + <tbody> + <tr th:each="row : ${model.additionalInfoProperties}"> + <td style="width: 50%;" th:text="${row.key}"></td> + <td th:text="${row.value}"></td> + </tr> + </tbody> + </table> + </div> + </div> + </th:block> + + <div th:replace="fragments/xrefs::xrefs(crossReferences=${model.crossReferences})"></div> +</main> +</body> +</html> -- GitLab From 21b56eaf47bf274c5509bd0a45f6ac49ab64ee44 Mon Sep 17 00:00:00 2001 From: jnizet <jb@ninja-squad.com> Date: Thu, 26 Aug 2021 15:34:25 +0200 Subject: [PATCH 12/16] feat: germplasm card --- .../web/germplasm/GermplasmController.java | 392 ++++++++++++++++ .../faidare/web/germplasm/GermplasmModel.java | 139 ++++++ .../urgi/faidare/web/site/SiteController.java | 2 +- .../faidare/web/study/StudyController.java | 2 +- .../faidare/web/thymeleaf/FaidareDialect.java | 25 ++ .../thymeleaf/FaidareExpressionFactory.java | 33 ++ .../web/thymeleaf/FaidareExpressions.java | 65 +++ .../main/resources/static/assets/style.css | 4 + .../templates/fragments/institute.html | 34 ++ .../resources/templates/fragments/link.html | 20 + .../resources/templates/fragments/row.html | 31 +- .../resources/templates/fragments/source.html | 35 ++ .../resources/templates/fragments/xrefs.html | 5 + .../main/resources/templates/germplasm.html | 418 ++++++++++++++++++ .../main/resources/templates/layout/main.html | 22 + .../src/main/resources/templates/site.html | 17 +- .../src/main/resources/templates/study.html | 16 +- 17 files changed, 1226 insertions(+), 34 deletions(-) create mode 100644 backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmController.java create mode 100644 backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmModel.java create mode 100644 backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareDialect.java create mode 100644 backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressionFactory.java create mode 100644 backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressions.java create mode 100644 backend/src/main/resources/templates/fragments/institute.html create mode 100644 backend/src/main/resources/templates/fragments/link.html create mode 100644 backend/src/main/resources/templates/fragments/source.html create mode 100644 backend/src/main/resources/templates/germplasm.html 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 00000000..cf343049 --- /dev/null +++ b/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmController.java @@ -0,0 +1,392 @@ +package fr.inra.urgi.faidare.web.germplasm; + +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"); + result.setOriginSite(originSite); + + 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); + + result.setCollectingSite(originSite); + 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 00000000..8acdf78f --- /dev/null +++ b/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmModel.java @@ -0,0 +1,139 @@ +package fr.inra.urgi.faidare.web.germplasm; + +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.datadiscovery.data.DataSource; +import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO; +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())); + } +} 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 index 1ed29438..151da527 100644 --- 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 @@ -37,7 +37,7 @@ public class SiteController { } @GetMapping("/{siteId}") - public ModelAndView site(@PathVariable("siteId") String siteId) { + public ModelAndView get(@PathVariable("siteId") String siteId) { LocationVO site = locationRepository.getById(siteId); // List<XRefDocumentVO> crossReferences = xRefDocumentRepository.find( 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 index 35767b09..ff6aee3b 100644 --- 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 @@ -58,7 +58,7 @@ public class StudyController { } @GetMapping("/{studyId}") - public ModelAndView site(@PathVariable("studyId") String studyId) { + public ModelAndView get(@PathVariable("studyId") String studyId) { StudyDetailVO study = studyRepository.getById(studyId); // List<XRefDocumentVO> crossReferences = xRefDocumentRepository.find( 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 00000000..2a02a842 --- /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 00000000..e873375c --- /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 00000000..a9f699de --- /dev/null +++ b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressions.java @@ -0,0 +1,65 @@ +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 fr.inra.urgi.faidare.domain.data.germplasm.CollPopVO; +import fr.inra.urgi.faidare.domain.data.germplasm.TaxonSourceVO; +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 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 Base64.getUrlEncoder().encodeToString(("urn:URGI/location/" + siteId).getBytes(StandardCharsets.US_ASCII)); + } + + 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; + } + + 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/resources/static/assets/style.css b/backend/src/main/resources/static/assets/style.css index 59bcd117..340b22ea 100644 --- a/backend/src/main/resources/static/assets/style.css +++ b/backend/src/main/resources/static/assets/style.css @@ -1,3 +1,7 @@ .label { font-weight: 500; } + +.popover { + max-width: min(80vw, 600px); +} 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 00000000..0efa76dd --- /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 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 00000000..d7c43bbd --- /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/row.html b/backend/src/main/resources/templates/fragments/row.html index 5b523cef..e5eb9e7f 100644 --- a/backend/src/main/resources/templates/fragments/row.html +++ b/backend/src/main/resources/templates/fragments/row.html @@ -4,6 +4,21 @@ <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 py-2"> <div class="col-md-4 label pb-1 pb-md-0" th:text="${label}"></div> <div class="col"> @@ -11,7 +26,21 @@ </div> </div> -<div th:fragment="text-row(label, text)" th:if="${!#strings.isEmpty(text)}" class="row py-2"> +<!-- +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 py-2"> <div class="col-md-4 label pb-1 pb-md-0" th:text="${label}"></div> <div class="col" th:text="${text}"></div> </div> 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 00000000..795717ca --- /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 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 index d51f1186..508b8923 100644 --- a/backend/src/main/resources/templates/fragments/xrefs.html +++ b/backend/src/main/resources/templates/fragments/xrefs.html @@ -4,6 +4,11 @@ <body> +<!-- +Reusable fragment displaying a cross references section, with its title. +The unique argument (crossReferences) is a List<XRefDocumentVO> +--> + <div th:fragment="xrefs(crossReferences)" th:if="${!#lists.isEmpty(crossReferences)}"> <h2>Cross references</h2> diff --git a/backend/src/main/resources/templates/germplasm.html b/backend/src/main/resources/templates/germplasm.html new file mode 100644 index 00000000..840b0d44 --- /dev/null +++ b/backend/src/main/resources/templates/germplasm.html @@ -0,0 +1,418 @@ +<!DOCTYPE html> + +<html + xmlns:th="http://www.thymeleaf.org" + th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main})}" +> +<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 class="row align-items-center justify-content-center"> + <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> + + <button class="btn btn-link p-0" + data-bs-toggle="popover" + 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> + </button> + </div> + + + <div class="col-12 col-lg"> + <h2>Identification</h2> + + <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> + <button class="btn btn-link p-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> + </button> + </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> + + <th:block th:if="${model.germplasm.holdingInstitute}"> + <h2>Depositary</h2> + <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})"> + <button id="institution" + 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" + class="btn btn-link p-0" + th:text="${model.germplasm.holdingInstitute.instituteName}"></button> + </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> + </th:block> + + <th:block th:if="${model.collecting}"> + <h2>Collector</h2> + <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})"> + <button id="collecting-institution" + 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" + class="btn btn-link p-0" + th:text="${model.germplasm.collector.institute.instituteName}"></button> + </div> + </th:block> + + <div th:replace="fragments/row::text-row(label='Accession number', text=${model.germplasm.collector.accessionNumber})"></div> + </th:block> + + <th:block th:if="${model.breeding}"> + <h2>Breeder</h2> + <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})"> + <button id="breeding-institution" + 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" + class="btn btn-link p-0" + th:text="${model.germplasm.breeder.institute.instituteName}"></button> + </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> + </th:block> + + <th:block th:unless="${#lists.isEmpty(model.germplasm.donors)}"> + <h2>Donors</h2> + <div class="table-responsive scroll-table table-card-body"> + <div class="card"> + <table class="table table-sm table-striped"> + <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> + <button 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" + class="btn btn-link p-0" + th:text="${row.donorInstitute.instituteName}"></button> + </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> + </th:block> + + <th:block th:unless="${#lists.isEmpty(model.germplasm.distributors)}"> + <h2>Donors</h2> + <div class="table-responsive scroll-table table-card-body"> + <div class="card"> + <table class="table table-sm table-striped"> + <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> + <button data-bs-toggle="popover" + th:data-bs-title="${row.institute.instituteName}" + th:data-bs-element="${'#distributor-institute-popover-' + distributorIterStat.index}" + data-bs-container="body" + data-bs-trigger="focus" + class="btn btn-link p-0" + th:text="${row.institute.instituteName}"></button> + </td> + <td th:text="${row.accessionNumber}"></td> + <td th:text="${row.distributionStatus}"></td> + </tr> + </tbody> + </table> + </div> + </div> + </th:block> + + <th:block th:unless="${#lists.isEmpty(model.attributes)}"> + <h2>Evaluation Data</h2> + <th:block th:each="descriptor : ${model.attributes}"> + <div th:replace="fragments/row::text-row(label=${descriptor.attributeName}, text=${descriptor.value})"></div> + </th:block> + </th:block> + + <th:block th:if="${model.genealogyPresent}"> + <h2>Genealogy</h2> + + <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-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> + + </th:block> + + <th:block th:unless="${#lists.isEmpty(model.germplasm.population)}"> + <h2>Population</h2> + <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> + </th:block> + + <th:block th:unless="${#lists.isEmpty(model.germplasm.collection)}"> + <h2>Collection</h2> + <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> + </th:block> + + <th:block th:unless="${#lists.isEmpty(model.germplasm.panel)}"> + <h2>Panel</h2> + <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> + </th:block> + + <div th:replace="fragments/xrefs::xrefs(crossReferences=${model.crossReferences})"></div> +</main> +</body> +</html> diff --git a/backend/src/main/resources/templates/layout/main.html b/backend/src/main/resources/templates/layout/main.html index 22bcdfd5..4cd33f70 100644 --- a/backend/src/main/resources/templates/layout/main.html +++ b/backend/src/main/resources/templates/layout/main.html @@ -26,5 +26,27 @@ common footer </footer> </div> + <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-U1DAWAznBHeqEIlVSCgzq+c9gqGAJn5c/t99JyeKa9xxaYpSvHU5awsuZVVFIhvj" crossorigin="anonymous"></script> + <script type="text/javascript"> + const popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')) + popoverTriggerList.forEach(popoverTriggerEl => { + const options = {}; + const contentSelector = popoverTriggerEl.dataset.bsElement; + if (contentSelector) { + const content = document.querySelector(contentSelector); + if (content) { + options.content = () => { + const element = document.createElement('div'); + element.innerHTML = content.innerHTML; + return element; + }; + options.html = true; + } else { + throw new Error('element with selector ' + contentSelector + ' not found'); + } + } + return new bootstrap.Popover(popoverTriggerEl, options); + }); + </script> </body> </html> diff --git a/backend/src/main/resources/templates/site.html b/backend/src/main/resources/templates/site.html index ad6affc4..d5d65e5c 100644 --- a/backend/src/main/resources/templates/site.html +++ b/backend/src/main/resources/templates/site.html @@ -17,22 +17,7 @@ <div th:replace="fragments/row::text-row(label='Permanent unique identifier', text=${model.site.uri})"></div> </th:block> - <th:block th:if="${model.source != null}"> - <div th:replace="fragments/row::row(label='Source', content=~{::#source})"> - <a id="source" target="_blank" th:href="${model.source.url}"> - <img style="max-height: 60px;" th:src="${model.source.image}" th:alt="${model.source.name} + ' logo'" /> - </a> - </div> - </th:block> - - <th:block th:if="${model.site.url != null && model.source != null}"> - <div - th:replace="fragments/row::row(label='Data link', content=~{::#url})"> - <a id="url" target="_blank" th:href="${model.site.url}"> - Link to this site on <th:block th:text="${model.source.name}" /> - </a> - </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> diff --git a/backend/src/main/resources/templates/study.html b/backend/src/main/resources/templates/study.html index a1cafc65..7bd5bbbd 100644 --- a/backend/src/main/resources/templates/study.html +++ b/backend/src/main/resources/templates/study.html @@ -18,21 +18,7 @@ <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> - <th:block th:if="${model.source != null}"> - <div th:replace="fragments/row::row(label='Source', content=~{::#source}, text='')"> - <a id="source" target="_blank" th:href="${model.source.url}"> - <img style="max-height: 60px;" th:src="${model.source.image}" th:alt="${model.source.name} + ' logo'" /> - </a> - </div> - </th:block> - - <th:block th:if="${model.study.url != null && model.source != null}"> - <div th:replace="fragments/row::row(label='Data link', content=~{::#url}, text='')"> - <a id="url" target="_blank" th:href="${model.study.url}"> - Link to this study on <th:block th:text="${model.source.name}" /> - </a> - </div> - </th:block> + <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> -- GitLab From 1fd79b03b91ad222342431fb92a8d2db29aa0781 Mon Sep 17 00:00:00 2001 From: jnizet <jb@ninja-squad.com> Date: Thu, 26 Aug 2021 21:32:24 +0200 Subject: [PATCH 13/16] feat: maps --- .../fr/inra/urgi/faidare/utils/Sites.java | 14 +++ .../web/germplasm/GermplasmController.java | 28 ++++- .../faidare/web/germplasm/GermplasmModel.java | 18 ++++ .../urgi/faidare/web/site/MapLocation.java | 82 ++++++++++++++ .../inra/urgi/faidare/web/site/SiteModel.java | 4 + .../faidare/web/study/StudyController.java | 26 ++++- .../urgi/faidare/web/study/StudyModel.java | 16 ++- .../web/thymeleaf/FaidareExpressions.java | 15 ++- .../static/assets/images/marker-icon-blue.png | Bin 0 -> 1747 bytes .../assets/images/marker-icon-green.png | Bin 0 -> 1875 bytes .../assets/images/marker-icon-purple.png | Bin 0 -> 4446 bytes .../static/assets/images/marker-icon-red.png | Bin 0 -> 1869 bytes .../main/resources/static/assets/script.js | 100 ++++++++++++++++++ .../main/resources/static/assets/style.css | 8 ++ .../resources/templates/fragments/map.html | 23 ++++ .../main/resources/templates/germplasm.html | 12 ++- .../main/resources/templates/layout/main.html | 34 +++--- .../src/main/resources/templates/site.html | 11 +- .../src/main/resources/templates/study.html | 11 +- 19 files changed, 366 insertions(+), 36 deletions(-) create mode 100644 backend/src/main/java/fr/inra/urgi/faidare/utils/Sites.java create mode 100644 backend/src/main/java/fr/inra/urgi/faidare/web/site/MapLocation.java create mode 100644 backend/src/main/resources/static/assets/images/marker-icon-blue.png create mode 100644 backend/src/main/resources/static/assets/images/marker-icon-green.png create mode 100644 backend/src/main/resources/static/assets/images/marker-icon-purple.png create mode 100644 backend/src/main/resources/static/assets/images/marker-icon-red.png create mode 100644 backend/src/main/resources/static/assets/script.js create mode 100644 backend/src/main/resources/templates/fragments/map.html 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 00000000..67fde44b --- /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/germplasm/GermplasmController.java b/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmController.java index cf343049..cd24d06d 100644 --- 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 @@ -1,5 +1,6 @@ package fr.inra.urgi.faidare.web.germplasm; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; @@ -105,7 +106,6 @@ public class GermplasmController { createXref("bazbing") ); - sortDonors(germplasm); sortPopulations(germplasm); sortCollections(germplasm); @@ -117,8 +117,7 @@ public class GermplasmController { faidareProperties.getByUri(germplasm.getSourceUri()), attributes, pedigree, - crossReferences - ) + crossReferences) ); } @@ -205,8 +204,23 @@ public class GermplasmController { 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"); @@ -241,7 +255,13 @@ public class GermplasmController { collector.setAccessionNumber("567"); result.setCollector(collector); - result.setCollectingSite(originSite); + 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(); 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 index 8acdf78f..ffa24336 100644 --- 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 @@ -1,13 +1,16 @@ 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; /** @@ -136,4 +139,19 @@ public final class GermplasmModel { || 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 00000000..3b096853 --- /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/SiteModel.java b/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteModel.java index 84cd92e2..0d7e1c0d 100644 --- 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 @@ -111,4 +111,8 @@ public final class SiteModel { 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 index ff6aee3b..833c4547 100644 --- 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 @@ -12,16 +12,20 @@ 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; @@ -42,19 +46,22 @@ public class StudyController { 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) { + 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}") @@ -77,6 +84,11 @@ public class StudyController { 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", @@ -86,11 +98,19 @@ public class StudyController { germplasms, variables, trials, - crossReferences + 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(); @@ -125,6 +145,8 @@ public class StudyController { .collect(Collectors.toList()); } + + private XRefDocumentVO createXref(String name) { XRefDocumentVO xref = new XRefDocumentVO(); xref.setName(name); 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 index 980c78d4..bc77dfc3 100644 --- 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 @@ -1,11 +1,8 @@ package fr.inra.urgi.faidare.web.study; -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; @@ -15,6 +12,7 @@ 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 @@ -27,6 +25,7 @@ public final class StudyModel { 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, @@ -34,13 +33,15 @@ public final class StudyModel { List<GermplasmVO> germplasms, List<ObservationVariableVO> variables, List<TrialVO> trials, - List<XRefDocumentVO> crossReferences) { + 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(); @@ -79,4 +80,11 @@ public final class StudyModel { 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/FaidareExpressions.java b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressions.java index a9f699de..9ba16d34 100644 --- 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 @@ -9,8 +9,11 @@ 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; /** @@ -21,6 +24,7 @@ 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<>(); @@ -38,7 +42,7 @@ public class FaidareExpressions { } public String toSiteParam(String siteId) { - return Base64.getUrlEncoder().encodeToString(("urn:URGI/location/" + siteId).getBytes(StandardCharsets.US_ASCII)); + return Sites.siteIdToLocationId(siteId); } public String collPopTitle(CollPopVO collPopVO) { @@ -55,6 +59,15 @@ public class FaidareExpressions { 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()); 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 GIT binary patch literal 1747 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 literal 0 HcmV?d00001 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 GIT binary patch literal 1875 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 N002ovPDHLkV1jAYejES* literal 0 HcmV?d00001 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 GIT binary patch literal 4446 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 literal 0 HcmV?d00001 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 GIT binary patch literal 1869 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 Hu0mjf0`GLc literal 0 HcmV?d00001 diff --git a/backend/src/main/resources/static/assets/script.js b/backend/src/main/resources/static/assets/script.js new file mode 100644 index 00000000..a01603cc --- /dev/null +++ b/backend/src/main/resources/static/assets/script.js @@ -0,0 +1,100 @@ +const faidare = (() => { + function initializePopovers() { + const popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')) + popoverTriggerList.forEach(popoverTriggerEl => { + const options = {}; + const contentSelector = popoverTriggerEl.dataset.bsElement; + if (contentSelector) { + const content = document.querySelector(contentSelector); + if (content) { + options.content = () => { + const element = document.createElement('div'); + element.innerHTML = content.innerHTML; + return element; + }; + options.html = true; + } else { + throw new Error('element with selector ' + contentSelector + ' not found'); + } + } + return new bootstrap.Popover(popoverTriggerEl, options); + }); + } + + function markerColor(location) { + switch (location.locationType) { + case 'Origin site': + return 'red'; + case 'Collecting site': + return 'blue'; + case 'Evaluation site': + return 'green'; + } + return 'purple'; + } + + function markerIconUrl(contextPath, location) { + return `${contextPath}/assets/images/marker-icon-${markerColor(location)}.png`; + } + + function initializeMap(options) { + if (!options.locations.length) { + return; + } + + const mapContainerElement = document.querySelector('#map-container'); + mapContainerElement.classList.remove("d-none"); + const mapElement = document.querySelector('#map'); + 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 = []; + 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; + const typeElement = document.createElement('div'); + typeElement.innerText = location.locationType; + const linkElement = document.createElement('a'); + linkElement.innerText = 'Details'; + linkElement.href = `${options.contextPath}/sites/${location.locationDbId}`; + popupElement.appendChild(titleElement); + popupElement.appendChild(typeElement); + 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); + } + + return { + initializePopovers, + initializeMap + }; +})(); + + diff --git a/backend/src/main/resources/static/assets/style.css b/backend/src/main/resources/static/assets/style.css index 340b22ea..87c396ca 100644 --- a/backend/src/main/resources/static/assets/style.css +++ b/backend/src/main/resources/static/assets/style.css @@ -5,3 +5,11 @@ .popover { max-width: min(80vw, 600px); } + +#map { + height: min(400px, 60vh); +} + +.map-legend img { + height: 1.5rem; +} 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 00000000..7fce9d1e --- /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"> + <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/germplasm.html b/backend/src/main/resources/templates/germplasm.html index 840b0d44..864b1813 100644 --- a/backend/src/main/resources/templates/germplasm.html +++ b/backend/src/main/resources/templates/germplasm.html @@ -2,7 +2,7 @@ <html xmlns:th="http://www.thymeleaf.org" - th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main})}" + th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main}, script=~{::script})}" > <head> <title>Germplasm: <th:block th:text="${model.germplasm.germplasmName}" /></title> @@ -18,6 +18,8 @@ </div> </div> + <div th:replace="fragments/map::map"></div> + <div class="row align-items-center justify-content-center"> <div class="col-auto field" th:if="${model.germplasm.photo != null && model.germplasm.photo.thumbnailFile != null}"> <template id="photo-popover"> @@ -414,5 +416,13 @@ <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/layout/main.html b/backend/src/main/resources/templates/layout/main.html index 4cd33f70..b428faaf 100644 --- a/backend/src/main/resources/templates/layout/main.html +++ b/backend/src/main/resources/templates/layout/main.html @@ -1,5 +1,5 @@ <!DOCTYPE html> -<html lang="fr" th:fragment="layout (title, content)" xmlns:th="http://www.thymeleaf.org"> +<html lang="fr" th:fragment="layout (title, content, script)" xmlns:th="http://www.thymeleaf.org"> <head> <title th:replace="${title}">Layout Title</title> @@ -8,6 +8,11 @@ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous"> <link th:href="@{/assets/style.css}" rel="stylesheet"> + <link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" + integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A==" + crossorigin=""/> + <link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.1.0/dist/MarkerCluster.css" /> + <link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.1.0/dist/MarkerCluster.Default.css" /> <link rel="shortcut icon" th:href="@{/static/assets/images/favicon.ico}" type="image/x-icon" /> </head> @@ -27,26 +32,11 @@ </footer> </div> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-U1DAWAznBHeqEIlVSCgzq+c9gqGAJn5c/t99JyeKa9xxaYpSvHU5awsuZVVFIhvj" crossorigin="anonymous"></script> - <script type="text/javascript"> - const popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')) - popoverTriggerList.forEach(popoverTriggerEl => { - const options = {}; - const contentSelector = popoverTriggerEl.dataset.bsElement; - if (contentSelector) { - const content = document.querySelector(contentSelector); - if (content) { - options.content = () => { - const element = document.createElement('div'); - element.innerHTML = content.innerHTML; - return element; - }; - options.html = true; - } else { - throw new Error('element with selector ' + contentSelector + ' not found'); - } - } - return new bootstrap.Popover(popoverTriggerEl, options); - }); - </script> + <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js" + integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA==" + crossorigin=""></script> + <script src="https://unpkg.com/leaflet.markercluster@1.1.0/dist/leaflet.markercluster.js"></script> + <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 index d5d65e5c..d0fa7dd7 100644 --- a/backend/src/main/resources/templates/site.html +++ b/backend/src/main/resources/templates/site.html @@ -2,7 +2,7 @@ <html xmlns:th="http://www.thymeleaf.org" - th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main})}" + th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main}, script=~{::script})}" > <head> <title>Site <th:block th:text="${model.site.locationName}" /></title> @@ -13,6 +13,8 @@ <main> <h1>Site <th:block th:text="${model.site.locationName}" /></h1> + <div th:replace="fragments/map::map"></div> + <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> @@ -58,5 +60,12 @@ <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 index 7bd5bbbd..c5111944 100644 --- a/backend/src/main/resources/templates/study.html +++ b/backend/src/main/resources/templates/study.html @@ -2,7 +2,7 @@ <html xmlns:th="http://www.thymeleaf.org" - th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main})}" + 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> @@ -13,6 +13,8 @@ <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> + <h2>Identification</h2> <div th:replace="fragments/row::text-row(label='Name', text=${model.study.studyName})"></div> @@ -188,5 +190,12 @@ <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> -- GitLab From 3294e52de14a2858ef243cd8677fd1f144ab3ac4 Mon Sep 17 00:00:00 2001 From: jnizet <jb@ninja-squad.com> Date: Fri, 27 Aug 2021 10:29:15 +0200 Subject: [PATCH 14/16] feat: move css and js to web project built with webpack --- .gitignore | 1 + backend/build.gradle.kts | 39 +- backend/src/main/resources/application.yml | 8 + .../main/resources/static/assets/script.js | 100 -- .../main/resources/static/assets/style.css | 15 - .../main/resources/templates/layout/main.html | 11 - gradle/wrapper/gradle-wrapper.jar | Bin 55741 -> 59536 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 282 ++-- gradlew.bat | 43 +- settings.gradle.kts | 2 +- web/build.gradle.kts | 42 + web/package.json | 35 + web/src/bootstrap/popovers.ts | 19 + web/src/index.ts | 7 + web/src/map/map.ts | 93 ++ web/src/style/_custom-bootstrap.scss | 53 + web/src/style/style.scss | 21 + web/tsconfig.json | 72 + web/webpack.common.js | 46 + web/webpack.dev.js | 7 + web/webpack.prod.js | 23 + web/yarn.lock | 1359 +++++++++++++++++ 23 files changed, 2013 insertions(+), 267 deletions(-) delete mode 100644 backend/src/main/resources/static/assets/script.js delete mode 100644 backend/src/main/resources/static/assets/style.css create mode 100644 web/build.gradle.kts create mode 100644 web/package.json create mode 100644 web/src/bootstrap/popovers.ts create mode 100644 web/src/index.ts create mode 100644 web/src/map/map.ts create mode 100644 web/src/style/_custom-bootstrap.scss create mode 100644 web/src/style/style.scss create mode 100644 web/tsconfig.json create mode 100644 web/webpack.common.js create mode 100644 web/webpack.dev.js create mode 100644 web/webpack.prod.js create mode 100644 web/yarn.lock diff --git a/.gitignore b/.gitignore index 897c8ff3..ef6f3225 100644 --- a/.gitignore +++ b/.gitignore @@ -226,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/backend/build.gradle.kts b/backend/build.gradle.kts index 586fc953..64acb331 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,21 +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(buildInfo) + 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 @@ -73,7 +92,7 @@ tasks { outputs.dir(snippetsDir) } - val jacocoTestReport by getting(JacocoReport::class) { + jacocoTestReport { reports { xml.setEnabled(true) html.setEnabled(true) diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 26c55fc3..2b684f60 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/script.js b/backend/src/main/resources/static/assets/script.js deleted file mode 100644 index a01603cc..00000000 --- a/backend/src/main/resources/static/assets/script.js +++ /dev/null @@ -1,100 +0,0 @@ -const faidare = (() => { - function initializePopovers() { - const popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')) - popoverTriggerList.forEach(popoverTriggerEl => { - const options = {}; - const contentSelector = popoverTriggerEl.dataset.bsElement; - if (contentSelector) { - const content = document.querySelector(contentSelector); - if (content) { - options.content = () => { - const element = document.createElement('div'); - element.innerHTML = content.innerHTML; - return element; - }; - options.html = true; - } else { - throw new Error('element with selector ' + contentSelector + ' not found'); - } - } - return new bootstrap.Popover(popoverTriggerEl, options); - }); - } - - function markerColor(location) { - switch (location.locationType) { - case 'Origin site': - return 'red'; - case 'Collecting site': - return 'blue'; - case 'Evaluation site': - return 'green'; - } - return 'purple'; - } - - function markerIconUrl(contextPath, location) { - return `${contextPath}/assets/images/marker-icon-${markerColor(location)}.png`; - } - - function initializeMap(options) { - if (!options.locations.length) { - return; - } - - const mapContainerElement = document.querySelector('#map-container'); - mapContainerElement.classList.remove("d-none"); - const mapElement = document.querySelector('#map'); - 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 = []; - 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; - const typeElement = document.createElement('div'); - typeElement.innerText = location.locationType; - const linkElement = document.createElement('a'); - linkElement.innerText = 'Details'; - linkElement.href = `${options.contextPath}/sites/${location.locationDbId}`; - popupElement.appendChild(titleElement); - popupElement.appendChild(typeElement); - 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); - } - - return { - initializePopovers, - initializeMap - }; -})(); - - diff --git a/backend/src/main/resources/static/assets/style.css b/backend/src/main/resources/static/assets/style.css deleted file mode 100644 index 87c396ca..00000000 --- a/backend/src/main/resources/static/assets/style.css +++ /dev/null @@ -1,15 +0,0 @@ -.label { - font-weight: 500; -} - -.popover { - max-width: min(80vw, 600px); -} - -#map { - height: min(400px, 60vh); -} - -.map-legend img { - height: 1.5rem; -} diff --git a/backend/src/main/resources/templates/layout/main.html b/backend/src/main/resources/templates/layout/main.html index b428faaf..47bfcf0d 100644 --- a/backend/src/main/resources/templates/layout/main.html +++ b/backend/src/main/resources/templates/layout/main.html @@ -6,13 +6,7 @@ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta content="width=device-width, initial-scale=1" name="viewport" /> - <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous"> <link th:href="@{/assets/style.css}" rel="stylesheet"> - <link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" - integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A==" - crossorigin=""/> - <link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.1.0/dist/MarkerCluster.css" /> - <link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.1.0/dist/MarkerCluster.Default.css" /> <link rel="shortcut icon" th:href="@{/static/assets/images/favicon.ico}" type="image/x-icon" /> </head> @@ -31,11 +25,6 @@ common footer </footer> </div> - <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-U1DAWAznBHeqEIlVSCgzq+c9gqGAJn5c/t99JyeKa9xxaYpSvHU5awsuZVVFIhvj" crossorigin="anonymous"></script> - <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js" - integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA==" - crossorigin=""></script> - <script src="https://unpkg.com/leaflet.markercluster@1.1.0/dist/leaflet.markercluster.js"></script> <script type="text/javascript" th:src="@{/assets/script.js}"></script> <script type="text/javascript" th:replace="${script}"></script> </body> diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 457aad0d98108420a977756b7145c93c8910b076..7454180f2ae8848c63b8b4dea2cb829da983f2fa 100644 GIT binary patch delta 33114 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 TC&=K0so)<)a0}UIUq1aG&G$N0 delta 29441 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ٗ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 b{r{#xMHy(Yqj2!40srZNdsTAa`P=^kHCx82 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 75b8c7c8..ffed3a25 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 af6708ff..1b6c7873 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 6d57edc7..107acd32 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 59e6a72b..b8faef34 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,2 +1,2 @@ rootProject.name = "faidare" -include("backend") +include("backend", "web") diff --git a/web/build.gradle.kts b/web/build.gradle.kts new file mode 100644 index 00000000..b49ff9de --- /dev/null +++ b/web/build.gradle.kts @@ -0,0 +1,42 @@ +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.common.js") + inputs.file("webpack.prod.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 00000000..e1348699 --- /dev/null +++ b/web/package.json @@ -0,0 +1,35 @@ +{ + "name": "faidaire-web", + "version": "1.0.0", + "description": "", + "private": true, + "scripts": { + "build": "webpack --config webpack.dev.js", + "build:prod": "webpack --config webpack.prod.js", + "watch": "webpack --config webpack.dev.js --watch", + "watch:prod": "webpack --config webpack.prod.js --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", + "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", + "webpack-merge": "5.8.0" + } +} diff --git a/web/src/bootstrap/popovers.ts b/web/src/bootstrap/popovers.ts new file mode 100644 index 00000000..83b703de --- /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 00000000..c906b28e --- /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 00000000..9fc2a960 --- /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 00000000..9de7c3dc --- /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 00000000..4dd56d61 --- /dev/null +++ b/web/src/style/style.scss @@ -0,0 +1,21 @@ +@import 'custom-bootstrap'; +@import '~leaflet/dist/leaflet.css'; +@import '~leaflet.markercluster/dist/MarkerCluster.css'; +@import '~leaflet.markercluster/dist/MarkerCluster.Default.css'; + +.label { + font-weight: 500; +} + +.popover { + max-width: min(80vw, 600px); +} + +#map { + height: min(400px, 60vh); +} + +.map-legend img { + height: 1.5rem; +} + diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 00000000..714af2e5 --- /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.common.js b/web/webpack.common.js new file mode 100644 index 00000000..83633dec --- /dev/null +++ b/web/webpack.common.js @@ -0,0 +1,46 @@ +const path = require('path'); +const { CleanWebpackPlugin } = require('clean-webpack-plugin'); +const MiniCssExtractPlugin = require("mini-css-extract-plugin"); + +module.exports = { + plugins: [ + // cleans the output directory before each build + new CleanWebpackPlugin(), + // allows extracting the CSS into a CSS file instead of bundling it in a JS file + new MiniCssExtractPlugin() + ], + 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: { + path: path.resolve(__dirname, 'build/dist/assets'), + filename: '[name].js' + } +}; diff --git a/web/webpack.dev.js b/web/webpack.dev.js new file mode 100644 index 00000000..ca85da79 --- /dev/null +++ b/web/webpack.dev.js @@ -0,0 +1,7 @@ +const { merge } = require('webpack-merge'); +const common = require('./webpack.common.js'); + +module.exports = merge(common, { + mode: 'development', + devtool: 'inline-source-map' +}); diff --git a/web/webpack.prod.js b/web/webpack.prod.js new file mode 100644 index 00000000..cf236a59 --- /dev/null +++ b/web/webpack.prod.js @@ -0,0 +1,23 @@ +const { merge } = require('webpack-merge'); +const common = require('./webpack.common.js'); +const MiniCssExtractPlugin = require("mini-css-extract-plugin"); + +const mergedConfig = merge(common, { + mode: 'production', + output: { + filename: '[name].[contenthash].js' + } +}); + +mergedConfig.plugins = mergedConfig.plugins.map(plugin => { + if (plugin instanceof MiniCssExtractPlugin) { + return new MiniCssExtractPlugin({ + filename: '[name].[contenthash].css' + }); + } else { + return plugin + } +}); + +module.exports = mergedConfig; + diff --git a/web/yarn.lock b/web/yarn.lock new file mode 100644 index 00000000..885dcc69 --- /dev/null +++ b/web/yarn.lock @@ -0,0 +1,1359 @@ +# 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== + +"@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" + +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== + +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.14.5: + 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-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== + +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: + 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-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" + +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== + +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" + +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" + +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-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-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== + +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" + +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" + +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== + +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" + +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.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-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-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: + 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-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: + 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== + +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== + +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" + +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" + +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== + +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== + +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.8.0, 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== + +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== -- GitLab From 42da092ffd086ba20d76fb306d8db363b0c44a52 Mon Sep 17 00:00:00 2001 From: jnizet <jb@ninja-squad.com> Date: Fri, 27 Aug 2021 15:01:54 +0200 Subject: [PATCH 15/16] feat: implement styling --- .../resources/static/assets/images/logo.png | Bin 0 -> 36294 bytes .../templates/fragments/institute.html | 2 +- .../resources/templates/fragments/map.html | 2 +- .../resources/templates/fragments/row.html | 4 +- .../resources/templates/fragments/source.html | 2 +- .../resources/templates/fragments/xrefs.html | 12 +- .../main/resources/templates/germplasm.html | 574 +++++++++--------- .../main/resources/templates/layout/main.html | 17 +- .../src/main/resources/templates/site.html | 79 +-- .../src/main/resources/templates/study.html | 130 ++-- web/src/style/style.scss | 76 ++- 11 files changed, 499 insertions(+), 399 deletions(-) create mode 100644 backend/src/main/resources/static/assets/images/logo.png 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 GIT binary patch literal 36294 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 No{ph*g_a}g{{ePjMBxAc literal 0 HcmV?d00001 diff --git a/backend/src/main/resources/templates/fragments/institute.html b/backend/src/main/resources/templates/fragments/institute.html index 0efa76dd..b2e8da3a 100644 --- a/backend/src/main/resources/templates/fragments/institute.html +++ b/backend/src/main/resources/templates/fragments/institute.html @@ -11,7 +11,7 @@ Its unique argument (institute) is an InstituteVO <th:block th:fragment="institute(institute)"> <div class="text-center py-2" th:if="${institute.logo}"> - <img th:src="${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> diff --git a/backend/src/main/resources/templates/fragments/map.html b/backend/src/main/resources/templates/fragments/map.html index 7fce9d1e..35ebed33 100644 --- a/backend/src/main/resources/templates/fragments/map.html +++ b/backend/src/main/resources/templates/fragments/map.html @@ -10,7 +10,7 @@ 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"> + <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"/> diff --git a/backend/src/main/resources/templates/fragments/row.html b/backend/src/main/resources/templates/fragments/row.html index e5eb9e7f..8676b590 100644 --- a/backend/src/main/resources/templates/fragments/row.html +++ b/backend/src/main/resources/templates/fragments/row.html @@ -19,7 +19,7 @@ into a block with the condition: </th:block> --> -<div th:fragment="row(label, content)" class="row py-2"> +<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}" /> @@ -40,7 +40,7 @@ into a block with the condition: <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 py-2"> +<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> diff --git a/backend/src/main/resources/templates/fragments/source.html b/backend/src/main/resources/templates/fragments/source.html index 795717ca..b0963881 100644 --- a/backend/src/main/resources/templates/fragments/source.html +++ b/backend/src/main/resources/templates/fragments/source.html @@ -16,7 +16,7 @@ The entityType argument is a string, which is used in the message <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 style="max-height: 60px;" th:src="${source.image}" th:alt="${source.name} + ' logo'" /> + <img class="img-fluid" style="max-height: 60px;" th:src="${source.image}" th:alt="${source.name} + ' logo'" /> </a> </div> </th:block> diff --git a/backend/src/main/resources/templates/fragments/xrefs.html b/backend/src/main/resources/templates/fragments/xrefs.html index 508b8923..c60310b6 100644 --- a/backend/src/main/resources/templates/fragments/xrefs.html +++ b/backend/src/main/resources/templates/fragments/xrefs.html @@ -9,12 +9,11 @@ Reusable fragment displaying a cross references section, with its title. The unique argument (crossReferences) is a List<XRefDocumentVO> --> -<div th:fragment="xrefs(crossReferences)" th:if="${!#lists.isEmpty(crossReferences)}"> +<div class="f-card" th:fragment="xrefs(crossReferences)" th:if="${!#lists.isEmpty(crossReferences)}"> <h2>Cross references</h2> - - <div class="table-responsive scroll-big-table table-card-body"> - <div class="card"> - <table class="table table-sm table-striped"> + <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> @@ -28,13 +27,12 @@ The unique argument (crossReferences) is a List<XRefDocumentVO> <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 th:text="${#strings.abbreviate(crossRef.description, 120)}"></td> + <td style="min-width: 30rem;" th:text="${#strings.abbreviate(crossRef.description, 120)}"></td> </tr> </tbody> </table> </div> </div> - </div> </body> diff --git a/backend/src/main/resources/templates/germplasm.html b/backend/src/main/resources/templates/germplasm.html index 864b1813..f5c6a4ce 100644 --- a/backend/src/main/resources/templates/germplasm.html +++ b/backend/src/main/resources/templates/germplasm.html @@ -20,7 +20,7 @@ <div th:replace="fragments/map::map"></div> - <div class="row align-items-center justify-content-center"> + <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"> @@ -34,212 +34,227 @@ </div> </template> - <button class="btn btn-link p-0" - data-bs-toggle="popover" - th:data-bs-title="${model.germplasm.photo.photoName}" - data-bs-element="#photo-popover" - data-bs-container="body" - data-bs-trigger="focus"> + <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> - </button> + </a> </div> - <div class="col-12 col-lg"> - <h2>Identification</h2> - - <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 class="f-card"> + <h2>Identification</h2> - <div th:replace="fragments/source::source(source=${model.source}, url=${model.germplasm.url}, entityType='germplasm')"></div> + <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> - <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> + <div th:replace="fragments/source::source(source=${model.source}, url=${model.germplasm.url}, entityType='germplasm')"></div> - <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.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="${#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> + <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> - </div> - </div> - </th:block> + </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> - <button class="btn btn-link p-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> - </button> - </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> + <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> + <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> - </th:block> + </div> </div> </div> - <th:block th:if="${model.germplasm.holdingInstitute}"> + <div class="f-card" th:if="${model.germplasm.holdingInstitute}"> <h2>Depositary</h2> - <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})"> - <button id="institution" - 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" - class="btn btn-link p-0" - th:text="${model.germplasm.holdingInstitute.instituteName}"></button> - </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 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> - <div th:replace="fragments/row::text-row(label='Presence status', text=${model.germplasm.presenceStatus})"></div> - </th:block> + <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> - <th:block th:if="${model.collecting}"> + <div class="f-card" th:if="${model.collecting}"> <h2>Collector</h2> - <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 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> + <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="${!#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})"> - <button id="collecting-institution" - 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" - class="btn btn-link p-0" - th:text="${model.germplasm.collector.institute.instituteName}"></button> - </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> - </th:block> + <div th:replace="fragments/row::text-row(label='Accession number', text=${model.germplasm.collector.accessionNumber})"></div> + </div> + </div> - <th:block th:if="${model.breeding}"> + <div class="f-card" th:if="${model.breeding}"> <h2>Breeder</h2> - <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})"> - <button id="breeding-institution" - 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" - class="btn btn-link p-0" - th:text="${model.germplasm.breeder.institute.instituteName}"></button> - </div> - </th:block> + <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> - </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> - <th:block th:unless="${#lists.isEmpty(model.germplasm.donors)}"> + <div class="f-card" th:unless="${#lists.isEmpty(model.germplasm.donors)}"> <h2>Donors</h2> - <div class="table-responsive scroll-table table-card-body"> - <div class="card"> - <table class="table table-sm table-striped"> + <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> @@ -255,13 +270,14 @@ <template th:id="${'donor-institute-popover-' + donorIterStat.index}"> <div th:replace="fragments/institute::institute(institute=${row.donorInstitute})"></div> </template> - <button 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" - class="btn btn-link p-0" - th:text="${row.donorInstitute.instituteName}"></button> + <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> @@ -272,13 +288,13 @@ </table> </div> </div> - </th:block> + </div> - <th:block th:unless="${#lists.isEmpty(model.germplasm.distributors)}"> - <h2>Donors</h2> - <div class="table-responsive scroll-table table-card-body"> - <div class="card"> - <table class="table table-sm table-striped"> + <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> @@ -292,13 +308,13 @@ <template th:id="${'distributor-institute-popover-' + distributorIterStat.index}"> <div th:replace="fragments/institute::institute(institute=${row.institute})"></div> </template> - <button data-bs-toggle="popover" - th:data-bs-title="${row.institute.instituteName}" - th:data-bs-element="${'#distributor-institute-popover-' + distributorIterStat.index}" - data-bs-container="body" - data-bs-trigger="focus" - class="btn btn-link p-0" - th:text="${row.institute.instituteName}"></button> + <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> @@ -307,112 +323,120 @@ </table> </div> </div> - </th:block> + </div> - <th:block th:unless="${#lists.isEmpty(model.attributes)}"> + <div class="f-card" th:unless="${#lists.isEmpty(model.attributes)}"> <h2>Evaluation Data</h2> - <th:block th:each="descriptor : ${model.attributes}"> - <div th:replace="fragments/row::text-row(label=${descriptor.attributeName}, text=${descriptor.value})"></div> - </th:block> - </th:block> + <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> - <th:block th:if="${model.genealogyPresent}"> + <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 != 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: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> + <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> - </div> + </th:block> </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> + <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> - </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-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> + + <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> - </div> - </div> - </th:block> - - </th:block> + </th:block> - <th:block th:unless="${#lists.isEmpty(model.germplasm.population)}"> - <h2>Population</h2> - <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: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> - <th:block th:if="${population.germplasmRef == null}"> - <div th:replace="fragments/row::text-row(label=${#faidare.collPopTitle(population)}, text=${population.germplasmCount + ' accession(s)'})"></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> - </th:block> - </th:block> - - <th:block th:unless="${#lists.isEmpty(model.germplasm.collection)}"> - <h2>Collection</h2> - <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> - </th:block> + </div> + </div> - <th:block th:unless="${#lists.isEmpty(model.germplasm.panel)}"> + <div class="f-card" th:unless="${#lists.isEmpty(model.germplasm.panel)}"> <h2>Panel</h2> - <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> - </th:block> + <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> diff --git a/backend/src/main/resources/templates/layout/main.html b/backend/src/main/resources/templates/layout/main.html index 47bfcf0d..22c36a5e 100644 --- a/backend/src/main/resources/templates/layout/main.html +++ b/backend/src/main/resources/templates/layout/main.html @@ -12,18 +12,17 @@ </head> <body> - <div class="container"> - <header> - Common header - </header> - + <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> - - <footer> - common footer - </footer> </div> <script type="text/javascript" th:src="@{/assets/script.js}"></script> <script type="text/javascript" th:replace="${script}"></script> diff --git a/backend/src/main/resources/templates/site.html b/backend/src/main/resources/templates/site.html index d0fa7dd7..69deac41 100644 --- a/backend/src/main/resources/templates/site.html +++ b/backend/src/main/resources/templates/site.html @@ -15,48 +15,55 @@ <div th:replace="fragments/map::map"></div> - <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 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/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> + <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> + <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 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> - <th:block th:unless="${#lists.isEmpty(model.additionalInfoProperties)}"> + <div class="f-card" th:unless="${#lists.isEmpty(model.additionalInfoProperties)}"> <h2>Additional info</h2> - <th:block th:each="prop : ${model.additionalInfoProperties}"> - <div th:replace="fragments/row::text-row(label=${prop.key}, text=${prop.value})"></div> - </th:block> - </th:block> + <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> diff --git a/backend/src/main/resources/templates/study.html b/backend/src/main/resources/templates/study.html index c5111944..5d26f834 100644 --- a/backend/src/main/resources/templates/study.html +++ b/backend/src/main/resources/templates/study.html @@ -15,51 +15,53 @@ <div th:replace="fragments/map::map"></div> - <h2>Identification</h2> - - <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 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> - </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> - <th:block th:unles="${#lists.isEmpty(model.germplasms)}"> + <div class="f-card" th:unles="${#lists.isEmpty(model.germplasms)}"> <h2>Genotype</h2> - - <div class="table-responsive scroll-table table-card-body"> - <div class="card"> - <table class="table table-sm table-striped"> + <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> @@ -79,13 +81,13 @@ </table> </div> </div> - </th:block> + </div> - <th:block th:unless="${#lists.isEmpty(model.variables)}"> + <div class="f-card" th:unless="${#lists.isEmpty(model.variables)}"> <h2>Variables</h2> - <div class="table-responsive scroll-table table-card-body"> - <div class="card"> - <table class="table table-sm table-striped"> + <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> @@ -110,13 +112,13 @@ </table> </div> </div> - </th:block> + </div> - <th:block th:unless="${#lists.isEmpty(model.trials)}"> + <div class="f-card" th:unless="${#lists.isEmpty(model.trials)}"> <h2>Data Set</h2> - <div class="table-responsive scroll-big-table table-card-body"> - <div class="card"> - <table class="table table-sm table-striped"> + <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> @@ -144,13 +146,13 @@ </table> </div> </div> - </th:block> + </div> - <th:block th:unless="${#lists.isEmpty(model.study.contacts)}"> + <div class="f-card" th:unless="${#lists.isEmpty(model.study.contacts)}"> <h2>Contact</h2> - <div class="table-responsive scroll-table table-card-body"> - <div class="card"> - <table class="table table-sm table-striped"> + <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> @@ -170,23 +172,23 @@ </table> </div> </div> - </th:block> + </div> - <th:block th:unless="${#lists.isEmpty(model.additionalInfoProperties)}"> + <div class="f-card" th:unless="${#lists.isEmpty(model.additionalInfoProperties)}"> <h2>Additional information</h2> - <div class="table-responsive scroll-table table-card-body"> - <div class="card"> - <table class="table table-sm table-striped"> + <div class="f-card-body"> + <div class="scroll-table-container"> + <table class="table table-sm"> <tbody> <tr th:each="row : ${model.additionalInfoProperties}"> - <td style="width: 50%;" th:text="${row.key}"></td> + <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> - </th:block> + </div> <div th:replace="fragments/xrefs::xrefs(crossReferences=${model.crossReferences})"></div> </main> diff --git a/web/src/style/style.scss b/web/src/style/style.scss index 4dd56d61..42aa2bb3 100644 --- a/web/src/style/style.scss +++ b/web/src/style/style.scss @@ -1,14 +1,53 @@ +$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'; -.label { - font-weight: 500; +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, 600px); + max-width: min(80vw, 500px); +} + +.popover-header { + font-weight: 700; } #map { @@ -19,3 +58,34 @@ 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; + } + } +} -- GitLab From a313aa36070760af7e25eb98a19fd2a58078ec99 Mon Sep 17 00:00:00 2001 From: jnizet <jb@ninja-squad.com> Date: Sat, 28 Aug 2021 18:38:34 +0200 Subject: [PATCH 16/16] refactor: simplify webpack build and optimize css --- web/build.gradle.kts | 3 +- web/package.json | 12 +- web/{webpack.common.js => webpack.config.js} | 33 +- web/webpack.dev.js | 7 - web/webpack.prod.js | 23 - web/yarn.lock | 483 ++++++++++++++++++- 6 files changed, 509 insertions(+), 52 deletions(-) rename web/{webpack.common.js => webpack.config.js} (60%) delete mode 100644 web/webpack.dev.js delete mode 100644 web/webpack.prod.js diff --git a/web/build.gradle.kts b/web/build.gradle.kts index b49ff9de..44076adb 100644 --- a/web/build.gradle.kts +++ b/web/build.gradle.kts @@ -27,8 +27,7 @@ tasks { val yarnBuildProd by registering(YarnTask::class) { args.set(listOf("run", "build:prod")) dependsOn(prepare) - inputs.file("webpack.common.js") - inputs.file("webpack.prod.js") + inputs.file("webpack.config.js") inputs.file("tsconfig.json") inputs.file("package.json") inputs.file("yarn.lock") diff --git a/web/package.json b/web/package.json index e1348699..4ce0986c 100644 --- a/web/package.json +++ b/web/package.json @@ -4,10 +4,10 @@ "description": "", "private": true, "scripts": { - "build": "webpack --config webpack.dev.js", - "build:prod": "webpack --config webpack.prod.js", - "watch": "webpack --config webpack.dev.js --watch", - "watch:prod": "webpack --config webpack.prod.js --watch", + "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": "", @@ -22,6 +22,7 @@ "@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", @@ -29,7 +30,6 @@ "ts-loader": "9.2.5", "typescript": "4.3.5", "webpack": "5.51.1", - "webpack-cli": "4.8.0", - "webpack-merge": "5.8.0" + "webpack-cli": "4.8.0" } } diff --git a/web/webpack.common.js b/web/webpack.config.js similarity index 60% rename from web/webpack.common.js rename to web/webpack.config.js index 83633dec..1c7b8080 100644 --- a/web/webpack.common.js +++ b/web/webpack.config.js @@ -1,13 +1,23 @@ 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 = { +module.exports = (env, argv) => ({ + context: path.resolve(__dirname, '.'), + // inline source maps only in development mode + devtool: argv.mode === 'production' ? undefined : 'inline-source-map', plugins: [ - // cleans the output directory before each build - new CleanWebpackPlugin(), // allows extracting the CSS into a CSS file instead of bundling it in a JS file - new MiniCssExtractPlugin() + 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 @@ -40,7 +50,18 @@ module.exports = { extensions: ['.ts', '.js'], }, output: { + // the output is stored in build/dist/assets path: path.resolve(__dirname, 'build/dist/assets'), - filename: '[name].js' + filename: argv.mode === 'production' ? '[name].[contenthash].js' : '[name].js' + }, + optimization: { + minimizer: [ + '...', + new CssMinimizerPlugin() + ] + }, + performance: { + maxAssetSize: 300000, + maxEntrypointSize: 300000 } -}; +}); diff --git a/web/webpack.dev.js b/web/webpack.dev.js deleted file mode 100644 index ca85da79..00000000 --- a/web/webpack.dev.js +++ /dev/null @@ -1,7 +0,0 @@ -const { merge } = require('webpack-merge'); -const common = require('./webpack.common.js'); - -module.exports = merge(common, { - mode: 'development', - devtool: 'inline-source-map' -}); diff --git a/web/webpack.prod.js b/web/webpack.prod.js deleted file mode 100644 index cf236a59..00000000 --- a/web/webpack.prod.js +++ /dev/null @@ -1,23 +0,0 @@ -const { merge } = require('webpack-merge'); -const common = require('./webpack.common.js'); -const MiniCssExtractPlugin = require("mini-css-extract-plugin"); - -const mergedConfig = merge(common, { - mode: 'production', - output: { - filename: '[name].[contenthash].js' - } -}); - -mergedConfig.plugins = mergedConfig.plugins.map(plugin => { - if (plugin instanceof MiniCssExtractPlugin) { - return new MiniCssExtractPlugin({ - filename: '[name].[contenthash].css' - }); - } else { - return plugin - } -}); - -module.exports = mergedConfig; - diff --git a/web/yarn.lock b/web/yarn.lock index 885dcc69..ba30e63f 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -12,6 +12,11 @@ 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" @@ -268,6 +273,11 @@ ajv@^6.12.5: 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" @@ -305,6 +315,11 @@ binary-extensions@^2.0.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" @@ -325,7 +340,7 @@ braces@^3.0.1, braces@~3.0.2: dependencies: fill-range "^7.0.1" -browserslist@^4.14.5: +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== @@ -341,7 +356,17 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -caniuse-lite@^1.0.30001251: +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== @@ -402,6 +427,11 @@ color-name@~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" @@ -412,7 +442,7 @@ commander@^2.20.0: 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.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== @@ -431,6 +461,18 @@ cross-spawn@^7.0.3: 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" @@ -445,11 +487,105 @@ css-loader@6.2.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" @@ -463,6 +599,36 @@ del@^4.1.1: 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" @@ -476,6 +642,11 @@ enhanced-resolve@^5.0.0, enhanced-resolve@^5.8.0: 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" @@ -674,6 +845,11 @@ interpret@^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" @@ -731,6 +907,11 @@ is-plain-object@^2.0.4: 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" @@ -785,6 +966,11 @@ leaflet@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" @@ -797,6 +983,16 @@ locate-path@^5.0.0: 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" @@ -804,6 +1000,11 @@ lru-cache@^6.0.0: 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" @@ -868,6 +1069,11 @@ normalize-path@^3.0.0, normalize-path@~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" @@ -875,6 +1081,13 @@ npm-run-path@^4.0.1: 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" @@ -901,7 +1114,7 @@ p-limit@^2.2.0: dependencies: p-try "^2.0.0" -p-limit@^3.1.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== @@ -984,6 +1197,106 @@ pkg-dir@^4.2.0: 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" @@ -1012,7 +1325,98 @@ postcss-modules-values@^4.0.0: dependencies: icss-utils "^5.0.0" -postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: +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== @@ -1020,12 +1424,29 @@ postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: cssesc "^3.0.0" util-deprecate "^1.0.2" -postcss-value-parser@^4.1.0: +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.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== @@ -1177,11 +1598,24 @@ source-map@~0.7.2: 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" @@ -1196,6 +1630,19 @@ supports-color@^8.0.0: 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" @@ -1222,6 +1669,11 @@ terser@^5.7.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" @@ -1244,6 +1696,11 @@ typescript@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" @@ -1261,6 +1718,11 @@ v8-compile-cache@^2.2.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" @@ -1288,7 +1750,7 @@ webpack-cli@4.8.0: v8-compile-cache "^2.2.0" webpack-merge "^5.7.3" -webpack-merge@5.8.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== @@ -1353,6 +1815,11 @@ yallist@^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" -- GitLab