diff --git a/backend/src/main/java/fr/inra/urgi/gpds/domain/brapi/v1/data/BrapiTrial.java b/backend/src/main/java/fr/inra/urgi/gpds/domain/brapi/v1/data/BrapiTrial.java index a3897ff11b80baa543b6c2ad595e71b294fd7150..90c5f8413827d629d14870abd6b1057a77cd9939 100644 --- a/backend/src/main/java/fr/inra/urgi/gpds/domain/brapi/v1/data/BrapiTrial.java +++ b/backend/src/main/java/fr/inra/urgi/gpds/domain/brapi/v1/data/BrapiTrial.java @@ -4,7 +4,6 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonView; import fr.inra.urgi.gpds.domain.JSONView; -import java.io.Serializable; import java.util.Date; import java.util.List; @@ -13,7 +12,7 @@ import java.util.List; * @link https://github.com/plantbreeding/API/blob/master/Specification/Trials/GetTrialById.md * @link https://github.com/plantbreeding/API/blob/master/Specification/Trials/ListTrialSummaries.md */ -public interface BrapiTrial extends Serializable { +public interface BrapiTrial extends HasBrapiDocumentationURL { // Trial @JsonView(JSONView.BrapiFields.class) @@ -58,4 +57,6 @@ public interface BrapiTrial extends Serializable { @JsonView(JSONView.BrapiFields.class) BrapiAdditionalInfo getAdditionalInfo(); + @Override + String getDocumentationURL(); } diff --git a/backend/src/main/java/fr/inra/urgi/gpds/domain/data/TrialVO.java b/backend/src/main/java/fr/inra/urgi/gpds/domain/data/TrialVO.java index 6429603f982cf0e6c4264b077ede7690e276e800..b2b39d4cd72590d48231830735ba4aa441dd1ce6 100644 --- a/backend/src/main/java/fr/inra/urgi/gpds/domain/data/TrialVO.java +++ b/backend/src/main/java/fr/inra/urgi/gpds/domain/data/TrialVO.java @@ -52,6 +52,15 @@ public class TrialVO implements GnpISInternal, BrapiTrial, HasURI, HasURL, Inclu private String url; private String sourceUri; + @Override + public String getDocumentationURL() { + return url; + } + + public void setDocumentationURL(String documentationURL) { + this.url = documentationURL; + } + @Override public String getUri() { return uri; diff --git a/frontend/e2e/src/app.e2e-spec.ts b/frontend/e2e/src/app.e2e-spec.ts index 43b214801bfc798c535c81f6c039ce2274bad6a2..0d794af9ad135a2f3269e8958561173c83befe6d 100644 --- a/frontend/e2e/src/app.e2e-spec.ts +++ b/frontend/e2e/src/app.e2e-spec.ts @@ -14,7 +14,7 @@ describe('workspace-project App', () => { it('should display site card message', () => { page.navigateTo('/sites/FOO'); - expect(page.getTitleText()).toEqual('site-card works!'); + expect(page.getTitleText()).toEqual('sites-card works!'); }); it('should display study card message', () => { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1eac5e493dcaa4d8576095c11ace83634271bf8b..2327dddfaf2dc6e019094e2968b3b63bf5e8dd7c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -5,26 +5,26 @@ "requires": true, "dependencies": { "@angular-devkit/architect": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.12.0.tgz", - "integrity": "sha512-p7VtPmEq9/9wfIEqXIeWDFt89xmJptBGGLWAn98wgXZNrWc1erahsVmYEEruPRBdwq+KL1xQNaodGFDkUCZxXQ==", + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.12.2.tgz", + "integrity": "sha512-32Eim3PM/CJKGcCF1FJQ91ohuF2vBGMd4t1DILaoOMXHWmPLI9N4ILzWHfqFLRvb8QFgLn4VNG7CI9K7GcSBlQ==", "dev": true, "requires": { - "@angular-devkit/core": "7.2.0", + "@angular-devkit/core": "7.2.2", "rxjs": "6.3.3" } }, "@angular-devkit/build-angular": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-0.12.0.tgz", - "integrity": "sha512-Mm0PbkKA/Jt4fBQp7kyqS/jIqE+SA3WOxqZaGN8ponfq55LOY/7Qh0dgGbQP9UtM4ohg+eOAn3mRNbeicfkPMw==", + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-0.12.2.tgz", + "integrity": "sha512-4PDykCNDjjFo6Ximhq2efiufoUP6pj8KvhB8UI03mLbn/Os1W0y1lmiPJn+NjeBLwFWH9DqW9Vxk/pYek7MtEA==", "dev": true, "requires": { - "@angular-devkit/architect": "0.12.0", - "@angular-devkit/build-optimizer": "0.12.0", - "@angular-devkit/build-webpack": "0.12.0", - "@angular-devkit/core": "7.2.0", - "@ngtools/webpack": "7.2.0", + "@angular-devkit/architect": "0.12.2", + "@angular-devkit/build-optimizer": "0.12.2", + "@angular-devkit/build-webpack": "0.12.2", + "@angular-devkit/core": "7.2.2", + "@ngtools/webpack": "7.2.2", "ajv": "6.6.2", "autoprefixer": "9.4.3", "circular-dependency-plugin": "5.0.2", @@ -45,7 +45,7 @@ "opn": "5.3.0", "parse5": "4.0.0", "portfinder": "1.0.17", - "postcss": "7.0.5", + "postcss": "7.0.11", "postcss-import": "12.0.0", "postcss-loader": "3.0.0", "raw-loader": "0.5.1", @@ -54,25 +54,25 @@ "semver": "5.5.1", "source-map-loader": "0.2.4", "source-map-support": "0.5.9", - "speed-measure-webpack-plugin": "1.2.3", + "speed-measure-webpack-plugin": "1.2.5", "stats-webpack-plugin": "0.7.0", "style-loader": "0.23.1", "stylus": "0.54.5", "stylus-loader": "3.0.2", - "terser-webpack-plugin": "1.1.0", + "terser-webpack-plugin": "1.2.1", "tree-kill": "1.2.0", - "webpack": "4.23.1", + "webpack": "4.28.4", "webpack-dev-middleware": "3.4.0", - "webpack-dev-server": "3.1.10", + "webpack-dev-server": "3.1.14", "webpack-merge": "4.1.4", "webpack-sources": "1.3.0", "webpack-subresource-integrity": "1.1.0-rc.6" } }, "@angular-devkit/build-optimizer": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-optimizer/-/build-optimizer-0.12.0.tgz", - "integrity": "sha512-EW2Ky0pi37x/3PBDh3buGHdekYrB/EyN041o59KWytvvWWCYDOv0RTjfXZRg6jgRZd4jLtnfcMRJeyBKo762GA==", + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-optimizer/-/build-optimizer-0.12.2.tgz", + "integrity": "sha512-5SARSE18X5/churU0Qc0gOfDt5EwuwKsJmIA7hHBzi44iotQm5c8ea0q0acua4/U4K+jOsF6A4Faa08Vr2624A==", "dev": true, "requires": { "loader-utils": "1.1.0", @@ -114,20 +114,20 @@ } }, "@angular-devkit/build-webpack": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.12.0.tgz", - "integrity": "sha512-23U5Rv+ofbbrXcKiwIQ0tW7puBb/ont25sg1qRQoT+bKhmz2wbTnRbXSls+o7dPiRPeLFDw0Tn5zwCkKPEc9PQ==", + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.12.2.tgz", + "integrity": "sha512-Uv3f8XJc/5UTj2T7XjxFYDhuybFIIitLGxBpp/hEIc7eXI4MsJKB6CoDJy+2BQch7c/QjKH7W3dmTxzuSJ2j3g==", "dev": true, "requires": { - "@angular-devkit/architect": "0.12.0", - "@angular-devkit/core": "7.2.0", + "@angular-devkit/architect": "0.12.2", + "@angular-devkit/core": "7.2.2", "rxjs": "6.3.3" } }, "@angular-devkit/core": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-7.2.0.tgz", - "integrity": "sha512-0T9SJQ2f8qxGClonulQd4pMFfOrEv/it+doPgnalsM8UhQ3RRSrziFGEB97kpLTjH8fIPEgC/80YbtwOarfojg==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-7.2.2.tgz", + "integrity": "sha512-gDF8iXiPN870WuBMl7bCQ5+Qz5SjGL/qMcvP4hli5hkn+kMAhgG38ligUK1bbhPQUJ+Z/nSOEmyv8gLHO09SZg==", "dev": true, "requires": { "ajv": "6.6.2", @@ -654,12 +654,12 @@ } }, "@babel/generator": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.2.2.tgz", - "integrity": "sha512-I4o675J/iS8k+P38dvJ3IBGqObLXyQLTxtrR4u9cSUJOURvafeEWb/pFMOTwtNrmq73mJzyF6ueTbO1BtN0Zeg==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.3.0.tgz", + "integrity": "sha512-dZTwMvTgWfhmibq4V9X+LMf6Bgl7zAodRn9PvcPdhlzFMbvUutx74dbEv7Atz3ToeEpevYEJtAwfxq/bDCzHWg==", "dev": true, "requires": { - "@babel/types": "^7.2.2", + "@babel/types": "^7.3.0", "jsesc": "^2.5.1", "lodash": "^4.17.10", "source-map": "^0.5.0", @@ -729,9 +729,9 @@ } }, "@babel/parser": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.2.3.tgz", - "integrity": "sha512-0LyEcVlfCoFmci8mXx8A5oIkpkOgyo8dRHtxBnK9RRBwxO2+JZPNsqtVEZQ7mJFPxnXF9lfmU24mHOPI0qnlkA==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.3.1.tgz", + "integrity": "sha512-ATz6yX/L8LEnC3dtLQnIx4ydcPxhLcoy9Vl6re00zb2w5lG6itY6Vhnr1KFRPq/FHNsgl/gh2mjNN20f9iJTTA==", "dev": true }, "@babel/template": { @@ -786,9 +786,9 @@ } }, "@babel/types": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.2.2.tgz", - "integrity": "sha512-fKCuD6UFUMkR541eDWL+2ih/xFZBXPOg/7EQFeTluMDebfqR4jrpaCjLhkWlQS4hT6nRa2PMEgXKbRB5/H2fpg==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.0.tgz", + "integrity": "sha512-QkFPw68QqWU1/RVPyBe8SO7lXbPfjtqAxRYQKpFpaB8yMq7X2qAqfwK5LKoQufEkSmO5NQ70O6Kc3Afk03RwXw==", "dev": true, "requires": { "esutils": "^2.0.2", @@ -813,12 +813,12 @@ } }, "@ngtools/webpack": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-7.2.0.tgz", - "integrity": "sha512-Ll0EF9sryXdEpOK1MTC4rmhh6lyndsZ7FsKZRWt673R6RpwnR1hL4tf4uU8EmhL9aeMTVVcyIFS+x7v+FC2uHQ==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-7.2.2.tgz", + "integrity": "sha512-xjvQ8tlyyReE69q+whAubwX4fayPoy4NHSIDa429qdcUypkvhSScAtou003oVAKG519rznykDrUHAWtvFMVf4Q==", "dev": true, "requires": { - "@angular-devkit/core": "7.2.0", + "@angular-devkit/core": "7.2.2", "enhanced-resolve": "4.1.0", "rxjs": "6.3.3", "tree-kill": "1.2.0", @@ -922,6 +922,11 @@ } } }, + "@types/geojson": { + "version": "7946.0.5", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.5.tgz", + "integrity": "sha512-rLlMXpd3rdlrp0+xsrda/hFfOpIxgqFcRpk005UKbHtcdFK+QXAjhBAPnvO58qF4O1LdDXrcaiJxMgstCIlcaw==" + }, "@types/jasmine": { "version": "2.8.12", "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-2.8.12.tgz", @@ -937,6 +942,22 @@ "@types/jasmine": "*" } }, + "@types/leaflet": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.2.14.tgz", + "integrity": "sha512-acP2w5DygY0V7bwmjFmaen5I2iBl8RkWx9kon1IJA7k9mNFgBb6702WApjZSrM4AG1ucJVxFcTlS6nr4HvahEw==", + "requires": { + "@types/geojson": "*" + } + }, + "@types/leaflet.markercluster": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/leaflet.markercluster/-/leaflet.markercluster-1.0.3.tgz", + "integrity": "sha512-rz4xQcsD3Ha9TcX4nMba9wpNe7HPQ03Hvo8Osi3SLpfaDCydHMoTquOG1IsjQ2aFm/LIHz4Uo4hYoeLv7q082w==", + "requires": { + "@types/leaflet": "*" + } + }, "@types/node": { "version": "8.9.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-8.9.5.tgz", @@ -981,174 +1002,174 @@ } }, "@webassemblyjs/ast": { - "version": "1.7.10", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.7.10.tgz", - "integrity": "sha512-wTUeaByYN2EA6qVqhbgavtGc7fLTOx0glG2IBsFlrFG51uXIGlYBTyIZMf4SPLo3v1bgV/7lBN3l7Z0R6Hswew==", + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.7.11.tgz", + "integrity": "sha512-ZEzy4vjvTzScC+SH8RBssQUawpaInUdMTYwYYLh54/s8TuT0gBLuyUnppKsVyZEi876VmmStKsUs28UxPgdvrA==", "dev": true, "requires": { - "@webassemblyjs/helper-module-context": "1.7.10", - "@webassemblyjs/helper-wasm-bytecode": "1.7.10", - "@webassemblyjs/wast-parser": "1.7.10" + "@webassemblyjs/helper-module-context": "1.7.11", + "@webassemblyjs/helper-wasm-bytecode": "1.7.11", + "@webassemblyjs/wast-parser": "1.7.11" } }, "@webassemblyjs/floating-point-hex-parser": { - "version": "1.7.10", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.7.10.tgz", - "integrity": "sha512-gMsGbI6I3p/P1xL2UxqhNh1ga2HCsx5VBB2i5VvJFAaqAjd2PBTRULc3BpTydabUQEGlaZCzEUQhLoLG7TvEYQ==", + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.7.11.tgz", + "integrity": "sha512-zY8dSNyYcgzNRNT666/zOoAyImshm3ycKdoLsyDw/Bwo6+/uktb7p4xyApuef1dwEBo/U/SYQzbGBvV+nru2Xg==", "dev": true }, "@webassemblyjs/helper-api-error": { - "version": "1.7.10", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.7.10.tgz", - "integrity": "sha512-DoYRlPWtuw3yd5BOr9XhtrmB6X1enYF0/54yNvQWGXZEPDF5PJVNI7zQ7gkcKfTESzp8bIBWailaFXEK/jjCsw==", + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.7.11.tgz", + "integrity": "sha512-7r1qXLmiglC+wPNkGuXCvkmalyEstKVwcueZRP2GNC2PAvxbLYwLLPr14rcdJaE4UtHxQKfFkuDFuv91ipqvXg==", "dev": true }, "@webassemblyjs/helper-buffer": { - "version": "1.7.10", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.7.10.tgz", - "integrity": "sha512-+RMU3dt/dPh4EpVX4u5jxsOlw22tp3zjqE0m3ftU2tsYxnPULb4cyHlgaNd2KoWuwasCQqn8Mhr+TTdbtj3LlA==", + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.7.11.tgz", + "integrity": "sha512-MynuervdylPPh3ix+mKZloTcL06P8tenNH3sx6s0qE8SLR6DdwnfgA7Hc9NSYeob2jrW5Vql6GVlsQzKQCa13w==", "dev": true }, "@webassemblyjs/helper-code-frame": { - "version": "1.7.10", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.7.10.tgz", - "integrity": "sha512-UiytbpKAULOEab2hUZK2ywXen4gWJVrgxtwY3Kn+eZaaSWaRM8z/7dAXRSoamhKFiBh1uaqxzE/XD9BLlug3gw==", + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.7.11.tgz", + "integrity": "sha512-T8ESC9KMXFTXA5urJcyor5cn6qWeZ4/zLPyWeEXZ03hj/x9weSokGNkVCdnhSabKGYWxElSdgJ+sFa9G/RdHNw==", "dev": true, "requires": { - "@webassemblyjs/wast-printer": "1.7.10" + "@webassemblyjs/wast-printer": "1.7.11" } }, "@webassemblyjs/helper-fsm": { - "version": "1.7.10", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.7.10.tgz", - "integrity": "sha512-w2vDtUK9xeSRtt5+RnnlRCI7wHEvLjF0XdnxJpgx+LJOvklTZPqWkuy/NhwHSLP19sm9H8dWxKeReMR7sCkGZA==", + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.7.11.tgz", + "integrity": "sha512-nsAQWNP1+8Z6tkzdYlXT0kxfa2Z1tRTARd8wYnc/e3Zv3VydVVnaeePgqUzFrpkGUyhUUxOl5ML7f1NuT+gC0A==", "dev": true }, "@webassemblyjs/helper-module-context": { - "version": "1.7.10", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.7.10.tgz", - "integrity": "sha512-yE5x/LzZ3XdPdREmJijxzfrf+BDRewvO0zl8kvORgSWmxpRrkqY39KZSq6TSgIWBxkK4SrzlS3BsMCv2s1FpsQ==", + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.7.11.tgz", + "integrity": "sha512-JxfD5DX8Ygq4PvXDucq0M+sbUFA7BJAv/GGl9ITovqE+idGX+J3QSzJYz+LwQmL7fC3Rs+utvWoJxDb6pmC0qg==", "dev": true }, "@webassemblyjs/helper-wasm-bytecode": { - "version": "1.7.10", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.7.10.tgz", - "integrity": "sha512-u5qy4SJ/OrxKxZqJ9N3qH4ZQgHaAzsopsYwLvoWJY6Q33r8PhT3VPyNMaJ7ZFoqzBnZlCcS/0f4Sp8WBxylXfg==", + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.7.11.tgz", + "integrity": "sha512-cMXeVS9rhoXsI9LLL4tJxBgVD/KMOKXuFqYb5oCJ/opScWpkCMEz9EJtkonaNcnLv2R3K5jIeS4TRj/drde1JQ==", "dev": true }, "@webassemblyjs/helper-wasm-section": { - "version": "1.7.10", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.7.10.tgz", - "integrity": "sha512-Ecvww6sCkcjatcyctUrn22neSJHLN/TTzolMGG/N7S9rpbsTZ8c6Bl98GpSpV77EvzNijiNRHBG0+JO99qKz6g==", + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.7.11.tgz", + "integrity": "sha512-8ZRY5iZbZdtNFE5UFunB8mmBEAbSI3guwbrsCl4fWdfRiAcvqQpeqd5KHhSWLL5wuxo53zcaGZDBU64qgn4I4Q==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.7.10", - "@webassemblyjs/helper-buffer": "1.7.10", - "@webassemblyjs/helper-wasm-bytecode": "1.7.10", - "@webassemblyjs/wasm-gen": "1.7.10" + "@webassemblyjs/ast": "1.7.11", + "@webassemblyjs/helper-buffer": "1.7.11", + "@webassemblyjs/helper-wasm-bytecode": "1.7.11", + "@webassemblyjs/wasm-gen": "1.7.11" } }, "@webassemblyjs/ieee754": { - "version": "1.7.10", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.7.10.tgz", - "integrity": "sha512-HRcWcY+YWt4+s/CvQn+vnSPfRaD4KkuzQFt5MNaELXXHSjelHlSEA8ZcqT69q0GTIuLWZ6JaoKar4yWHVpZHsQ==", + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.7.11.tgz", + "integrity": "sha512-Mmqx/cS68K1tSrvRLtaV/Lp3NZWzXtOHUW2IvDvl2sihAwJh4ACE0eL6A8FvMyDG9abes3saB6dMimLOs+HMoQ==", "dev": true, "requires": { "@xtuc/ieee754": "^1.2.0" } }, "@webassemblyjs/leb128": { - "version": "1.7.10", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.7.10.tgz", - "integrity": "sha512-og8MciYlA8hvzCLR71hCuZKPbVBfLQeHv7ImKZ4nlyxrYbG7uJHYtHiHu6OV9SqrGuD03H/HtXC4Bgdjfm9FHw==", + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.7.11.tgz", + "integrity": "sha512-vuGmgZjjp3zjcerQg+JA+tGOncOnJLWVkt8Aze5eWQLwTQGNgVLcyOTqgSCxWTR4J42ijHbBxnuRaL1Rv7XMdw==", "dev": true, "requires": { "@xtuc/long": "4.2.1" } }, "@webassemblyjs/utf8": { - "version": "1.7.10", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.7.10.tgz", - "integrity": "sha512-Ng6Pxv6siyZp635xCSnH3mKmIFgqWPCcGdoo0GBYgyGdxu7cUj4agV7Uu1a8REP66UYUFXJLudeGgd4RvuJAnQ==", + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.7.11.tgz", + "integrity": "sha512-C6GFkc7aErQIAH+BMrIdVSmW+6HSe20wg57HEC1uqJP8E/xpMjXqQUxkQw07MhNDSDcGpxI9G5JSNOQCqJk4sA==", "dev": true }, "@webassemblyjs/wasm-edit": { - "version": "1.7.10", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.7.10.tgz", - "integrity": "sha512-e9RZFQlb+ZuYcKRcW9yl+mqX/Ycj9+3/+ppDI8nEE/NCY6FoK8f3dKBcfubYV/HZn44b+ND4hjh+4BYBt+sDnA==", + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.7.11.tgz", + "integrity": "sha512-FUd97guNGsCZQgeTPKdgxJhBXkUbMTY6hFPf2Y4OedXd48H97J+sOY2Ltaq6WGVpIH8o/TGOVNiVz/SbpEMJGg==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.7.10", - "@webassemblyjs/helper-buffer": "1.7.10", - "@webassemblyjs/helper-wasm-bytecode": "1.7.10", - "@webassemblyjs/helper-wasm-section": "1.7.10", - "@webassemblyjs/wasm-gen": "1.7.10", - "@webassemblyjs/wasm-opt": "1.7.10", - "@webassemblyjs/wasm-parser": "1.7.10", - "@webassemblyjs/wast-printer": "1.7.10" + "@webassemblyjs/ast": "1.7.11", + "@webassemblyjs/helper-buffer": "1.7.11", + "@webassemblyjs/helper-wasm-bytecode": "1.7.11", + "@webassemblyjs/helper-wasm-section": "1.7.11", + "@webassemblyjs/wasm-gen": "1.7.11", + "@webassemblyjs/wasm-opt": "1.7.11", + "@webassemblyjs/wasm-parser": "1.7.11", + "@webassemblyjs/wast-printer": "1.7.11" } }, "@webassemblyjs/wasm-gen": { - "version": "1.7.10", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.7.10.tgz", - "integrity": "sha512-M0lb6cO2Y0PzDye/L39PqwV+jvO+2YxEG5ax+7dgq7EwXdAlpOMx1jxyXJTScQoeTpzOPIb+fLgX/IkLF8h2yw==", + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.7.11.tgz", + "integrity": "sha512-U/KDYp7fgAZX5KPfq4NOupK/BmhDc5Kjy2GIqstMhvvdJRcER/kUsMThpWeRP8BMn4LXaKhSTggIJPOeYHwISA==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.7.10", - "@webassemblyjs/helper-wasm-bytecode": "1.7.10", - "@webassemblyjs/ieee754": "1.7.10", - "@webassemblyjs/leb128": "1.7.10", - "@webassemblyjs/utf8": "1.7.10" + "@webassemblyjs/ast": "1.7.11", + "@webassemblyjs/helper-wasm-bytecode": "1.7.11", + "@webassemblyjs/ieee754": "1.7.11", + "@webassemblyjs/leb128": "1.7.11", + "@webassemblyjs/utf8": "1.7.11" } }, "@webassemblyjs/wasm-opt": { - "version": "1.7.10", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.7.10.tgz", - "integrity": "sha512-R66IHGCdicgF5ZliN10yn5HaC7vwYAqrSVJGjtJJQp5+QNPBye6heWdVH/at40uh0uoaDN/UVUfXK0gvuUqtVg==", + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.7.11.tgz", + "integrity": "sha512-XynkOwQyiRidh0GLua7SkeHvAPXQV/RxsUeERILmAInZegApOUAIJfRuPYe2F7RcjOC9tW3Cb9juPvAC/sCqvg==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.7.10", - "@webassemblyjs/helper-buffer": "1.7.10", - "@webassemblyjs/wasm-gen": "1.7.10", - "@webassemblyjs/wasm-parser": "1.7.10" + "@webassemblyjs/ast": "1.7.11", + "@webassemblyjs/helper-buffer": "1.7.11", + "@webassemblyjs/wasm-gen": "1.7.11", + "@webassemblyjs/wasm-parser": "1.7.11" } }, "@webassemblyjs/wasm-parser": { - "version": "1.7.10", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.7.10.tgz", - "integrity": "sha512-AEv8mkXVK63n/iDR3T693EzoGPnNAwKwT3iHmKJNBrrALAhhEjuPzo/lTE4U7LquEwyvg5nneSNdTdgrBaGJcA==", + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.7.11.tgz", + "integrity": "sha512-6lmXRTrrZjYD8Ng8xRyvyXQJYUQKYSXhJqXOBLw24rdiXsHAOlvw5PhesjdcaMadU/pyPQOJ5dHreMjBxwnQKg==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.7.10", - "@webassemblyjs/helper-api-error": "1.7.10", - "@webassemblyjs/helper-wasm-bytecode": "1.7.10", - "@webassemblyjs/ieee754": "1.7.10", - "@webassemblyjs/leb128": "1.7.10", - "@webassemblyjs/utf8": "1.7.10" + "@webassemblyjs/ast": "1.7.11", + "@webassemblyjs/helper-api-error": "1.7.11", + "@webassemblyjs/helper-wasm-bytecode": "1.7.11", + "@webassemblyjs/ieee754": "1.7.11", + "@webassemblyjs/leb128": "1.7.11", + "@webassemblyjs/utf8": "1.7.11" } }, "@webassemblyjs/wast-parser": { - "version": "1.7.10", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.7.10.tgz", - "integrity": "sha512-YTPEtOBljkCL0VjDp4sHe22dAYSm3ZwdJ9+2NTGdtC7ayNvuip1wAhaAS8Zt9Q6SW9E5Jf5PX7YE3XWlrzR9cw==", + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.7.11.tgz", + "integrity": "sha512-lEyVCg2np15tS+dm7+JJTNhNWq9yTZvi3qEhAIIOaofcYlUp0UR5/tVqOwa/gXYr3gjwSZqw+/lS9dscyLelbQ==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.7.10", - "@webassemblyjs/floating-point-hex-parser": "1.7.10", - "@webassemblyjs/helper-api-error": "1.7.10", - "@webassemblyjs/helper-code-frame": "1.7.10", - "@webassemblyjs/helper-fsm": "1.7.10", + "@webassemblyjs/ast": "1.7.11", + "@webassemblyjs/floating-point-hex-parser": "1.7.11", + "@webassemblyjs/helper-api-error": "1.7.11", + "@webassemblyjs/helper-code-frame": "1.7.11", + "@webassemblyjs/helper-fsm": "1.7.11", "@xtuc/long": "4.2.1" } }, "@webassemblyjs/wast-printer": { - "version": "1.7.10", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.7.10.tgz", - "integrity": "sha512-mJ3QKWtCchL1vhU/kZlJnLPuQZnlDOdZsyP0bbLWPGdYsQDnSBvyTLhzwBA3QAMlzEL9V4JHygEmK6/OTEyytA==", + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.7.11.tgz", + "integrity": "sha512-m5vkAsuJ32QpkdkDOUPGSltrg8Cuk3KBx4YrmAGQwCZPRdUHXxG4phIOuuycLemHFr74sWL9Wthqss4fzdzSwg==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.7.10", - "@webassemblyjs/wast-parser": "1.7.10", + "@webassemblyjs/ast": "1.7.11", + "@webassemblyjs/wast-parser": "1.7.11", "@xtuc/long": "4.2.1" } }, @@ -1235,9 +1256,9 @@ "dev": true }, "ajv-keywords": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz", - "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.3.0.tgz", + "integrity": "sha512-CMzN9S62ZOO4sA/mJZIO4S++ZM7KFWzH3PPWkveLhy4OZ9i1/VatgwWMD46w/XbGCBy7Ye0gCk+Za6mmyfKK7g==", "dev": true }, "amdefine": { @@ -1253,9 +1274,9 @@ "dev": true }, "ansi-escapes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz", - "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", "dev": true }, "ansi-html": { @@ -1513,25 +1534,6 @@ "num2fraction": "^1.2.2", "postcss": "^7.0.6", "postcss-value-parser": "^3.3.1" - }, - "dependencies": { - "postcss": { - "version": "7.0.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.8.tgz", - "integrity": "sha512-WudsIzuTKRw9IInRTPBgVXJ7DKR26HT09Rxp0g3w0Fqh3TUtYICcUmvC0xURj04o3vdcDtnjCAUCECg/p341iQ==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.0.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } } }, "aws-sign2": { @@ -1995,20 +1997,20 @@ } }, "browserslist": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.3.7.tgz", - "integrity": "sha512-pWQv51Ynb0MNk9JGMCZ8VkM785/4MQNXiFYtPqI7EEP0TJO+/d/NqRVn1uiAN0DNbnlUSpL2sh16Kspasv3pUQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.4.1.tgz", + "integrity": "sha512-pEBxEXg7JwaakBXjATYw/D1YZh4QUSCX/Mnd/wnqSRPPSi1U39iDhDoKGoBUcraKdxDlrYqJxSI5nNvD+dWP2A==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30000925", - "electron-to-chromium": "^1.3.96", + "caniuse-lite": "^1.0.30000929", + "electron-to-chromium": "^1.3.103", "node-releases": "^1.1.3" } }, "browserstack": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/browserstack/-/browserstack-1.5.1.tgz", - "integrity": "sha512-O8VMT64P9NOLhuIoD4YngyxBURefaSdR4QdhG8l6HZ9VxtU7jc3m6jLufFwKA5gaf7fetfB2TnRJnMxyob+heg==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/browserstack/-/browserstack-1.5.2.tgz", + "integrity": "sha512-+6AFt9HzhKykcPF79W6yjEUJcdvZOV0lIXdkORXMJftGrDl0OKWqRF4GHqpDNkxiceDT/uB7Fb/aDwktvXX7dg==", "dev": true, "requires": { "https-proxy-agent": "^2.2.1" @@ -2152,9 +2154,9 @@ } }, "caniuse-lite": { - "version": "1.0.30000927", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000927.tgz", - "integrity": "sha512-ogq4NbUWf1uG/j66k0AmiO3GjqJAlQyF8n4w8a954cbCyFKmYGvRtgz6qkq2fWuduTXHibX7GyYL5Pg58Aks2g==", + "version": "1.0.30000933", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000933.tgz", + "integrity": "sha512-d3QXv7eFTU40DSedSP81dV/ajcGSKpT+GW+uhtWmLvQm9bPk0KK++7i1e2NSW/CXGZhWFt2mFbFtCJ5I5bMuVA==", "dev": true }, "canonical-path": { @@ -3031,9 +3033,9 @@ } }, "dir-glob": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.0.tgz", - "integrity": "sha512-YqrO+bduKFqPgspvpjDAaKk0qhmvY+SY7NjIRljCDAy6CX7Ft65irIduHbrYXhy+BxJnYKjWuREw6X42w9/+DQ==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", + "integrity": "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==", "dev": true, "requires": { "path-type": "^3.0.0" @@ -3135,9 +3137,9 @@ "dev": true }, "electron-to-chromium": { - "version": "1.3.100", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.100.tgz", - "integrity": "sha512-cEUzis2g/RatrVf8x26L8lK5VEls1AGnLHk6msluBUg/NTB4wcXzExTsGscFq+Vs4WBBU2zbLLySvD4C0C3hwg==", + "version": "1.3.109", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.109.tgz", + "integrity": "sha512-1qhgVZD9KIULMyeBkbjU/dWmm30zpPUfdWZfVO3nPhbtqMHJqHr4Ua5wBcWtAymVFrUCuAJxjMF1OhG+bR21Ow==", "dev": true }, "elliptic": { @@ -3397,9 +3399,9 @@ "dev": true }, "events": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.0.0.tgz", + "integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==", "dev": true }, "eventsource": { @@ -4017,9 +4019,9 @@ "dev": true }, "fsevents": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", - "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.7.tgz", + "integrity": "sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw==", "dev": true, "optional": true, "requires": { @@ -4045,7 +4047,7 @@ "optional": true }, "are-we-there-yet": { - "version": "1.1.4", + "version": "1.1.5", "bundled": true, "dev": true, "optional": true, @@ -4069,7 +4071,7 @@ } }, "chownr": { - "version": "1.0.1", + "version": "1.1.1", "bundled": true, "dev": true, "optional": true @@ -4105,7 +4107,7 @@ } }, "deep-extend": { - "version": "0.5.1", + "version": "0.6.0", "bundled": true, "dev": true, "optional": true @@ -4154,7 +4156,7 @@ } }, "glob": { - "version": "7.1.2", + "version": "7.1.3", "bundled": true, "dev": true, "optional": true, @@ -4174,12 +4176,12 @@ "optional": true }, "iconv-lite": { - "version": "0.4.21", + "version": "0.4.24", "bundled": true, "dev": true, "optional": true, "requires": { - "safer-buffer": "^2.1.0" + "safer-buffer": ">= 2.1.2 < 3" } }, "ignore-walk": { @@ -4240,16 +4242,16 @@ "dev": true }, "minipass": { - "version": "2.2.4", + "version": "2.3.5", "bundled": true, "dev": true, "requires": { - "safe-buffer": "^5.1.1", + "safe-buffer": "^5.1.2", "yallist": "^3.0.0" } }, "minizlib": { - "version": "1.1.0", + "version": "1.2.1", "bundled": true, "dev": true, "optional": true, @@ -4272,7 +4274,7 @@ "optional": true }, "needle": { - "version": "2.2.0", + "version": "2.2.4", "bundled": true, "dev": true, "optional": true, @@ -4283,18 +4285,18 @@ } }, "node-pre-gyp": { - "version": "0.10.0", + "version": "0.10.3", "bundled": true, "dev": true, "optional": true, "requires": { "detect-libc": "^1.0.2", "mkdirp": "^0.5.1", - "needle": "^2.2.0", + "needle": "^2.2.1", "nopt": "^4.0.1", "npm-packlist": "^1.1.6", "npmlog": "^4.0.2", - "rc": "^1.1.7", + "rc": "^1.2.7", "rimraf": "^2.6.1", "semver": "^5.3.0", "tar": "^4" @@ -4311,13 +4313,13 @@ } }, "npm-bundled": { - "version": "1.0.3", + "version": "1.0.5", "bundled": true, "dev": true, "optional": true }, "npm-packlist": { - "version": "1.1.10", + "version": "1.2.0", "bundled": true, "dev": true, "optional": true, @@ -4392,12 +4394,12 @@ "optional": true }, "rc": { - "version": "1.2.7", + "version": "1.2.8", "bundled": true, "dev": true, "optional": true, "requires": { - "deep-extend": "^0.5.1", + "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" @@ -4427,16 +4429,16 @@ } }, "rimraf": { - "version": "2.6.2", + "version": "2.6.3", "bundled": true, "dev": true, "optional": true, "requires": { - "glob": "^7.0.5" + "glob": "^7.1.3" } }, "safe-buffer": { - "version": "5.1.1", + "version": "5.1.2", "bundled": true, "dev": true }, @@ -4453,7 +4455,7 @@ "optional": true }, "semver": { - "version": "5.5.0", + "version": "5.6.0", "bundled": true, "dev": true, "optional": true @@ -4504,17 +4506,17 @@ "optional": true }, "tar": { - "version": "4.4.1", + "version": "4.4.8", "bundled": true, "dev": true, "optional": true, "requires": { - "chownr": "^1.0.1", + "chownr": "^1.1.1", "fs-minipass": "^1.2.5", - "minipass": "^2.2.4", - "minizlib": "^1.1.0", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.1", + "safe-buffer": "^5.1.2", "yallist": "^3.0.2" } }, @@ -4525,12 +4527,12 @@ "optional": true }, "wide-align": { - "version": "1.1.2", + "version": "1.1.3", "bundled": true, "dev": true, "optional": true, "requires": { - "string-width": "^1.0.2" + "string-width": "^1.0.2 || 2" } }, "wrappy": { @@ -4539,7 +4541,7 @@ "dev": true }, "yallist": { - "version": "3.0.2", + "version": "3.0.3", "bundled": true, "dev": true } @@ -4726,9 +4728,9 @@ "dev": true }, "handle-thing": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-1.2.5.tgz", - "integrity": "sha1-/Xqtcmvxpf0W38KbL3pmAdJxOcQ=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz", + "integrity": "sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ==", "dev": true }, "handlebars": { @@ -5863,9 +5865,9 @@ "integrity": "sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg==" }, "js-base64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.0.tgz", - "integrity": "sha512-wlEBIZ5LP8usDylWbDNhKPEFVFdI5hCHpnVoT/Ysvoi/PRhJENm/Rlh9TvjYB38HFfKZN7OzEbRjmjvLkFw11g==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz", + "integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==", "dev": true, "optional": true }, @@ -6141,6 +6143,16 @@ "invert-kv": "^1.0.0" } }, + "leaflet": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.3.4.tgz", + "integrity": "sha512-FYL1LGFdj6v+2Ifpw+AcFIuIOqjNggfoLUwuwQv6+3sS21Za7Wvapq+LhbSE4NDXrEj6eYnW3y7LsaBICpyXtw==" + }, + "leaflet.markercluster": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.4.1.tgz", + "integrity": "sha512-ZSEpE/EFApR0bJ1w/dUGwTSUvWlpalKqIzkaYdYB7jaftQA/Y2Jav+eT4CMtEYFj+ZK4mswP13Q2acnPBnhGOw==" + }, "less": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/less/-/less-3.9.0.tgz", @@ -6229,9 +6241,9 @@ } }, "loader-runner": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.3.1.tgz", - "integrity": "sha512-By6ZFY7ETWOc9RFaAIb23IjJVcM4dvJC/N57nmdz9RSkMXvAXGI7SyVlAw3v8vjtDRlqThgVDVmTnr9fqMlxkw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", + "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", "dev": true }, "loader-utils": { @@ -6414,9 +6426,9 @@ } }, "math-random": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz", - "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz", + "integrity": "sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==", "dev": true }, "md5.js": { @@ -6437,14 +6449,14 @@ "dev": true }, "mem": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.0.0.tgz", - "integrity": "sha512-WQxG/5xYc3tMbYLXoXPm81ET2WDULiU5FxbuIoNbJqLOOI8zehXFdZuiUEgfdrU2mVB1pxBZUGlYORSrpuJreA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.1.0.tgz", + "integrity": "sha512-I5u6Q1x7wxO0kdOpYBB28xueHADYps5uty/zg936CiG8NTe5sJL8EjrCuLneuDW3PlMdZBGDIn8BirEVdovZvg==", "dev": true, "requires": { "map-age-cleaner": "^0.1.1", "mimic-fn": "^1.0.0", - "p-is-promise": "^1.1.0" + "p-is-promise": "^2.0.0" } }, "memory-fs": { @@ -6792,9 +6804,9 @@ } }, "node-libs-browser": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.1.0.tgz", - "integrity": "sha512-5AzFzdoIMb89hBGMZglEegffzgRg+ZFoUmisQ8HI4j1KDdpx13J0taNp2y9xPbur6W61gepGDDotGBVQ7mfUCg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.0.tgz", + "integrity": "sha512-5MQunG/oyOaBdttrL40dA7bUfPORLRWMUJLQtMg7nluxUvk5XwnLdL9twQHFAjRx/y7mIMkLKT9++qPbbk6BZA==", "dev": true, "requires": { "assert": "^1.1.1", @@ -6804,7 +6816,7 @@ "constants-browserify": "^1.0.0", "crypto-browserify": "^3.11.0", "domain-browser": "^1.1.1", - "events": "^1.0.0", + "events": "^3.0.0", "https-browserify": "^1.0.0", "os-browserify": "^0.3.0", "path-browserify": "0.0.0", @@ -6818,7 +6830,7 @@ "timers-browserify": "^2.0.4", "tty-browserify": "0.0.0", "url": "^0.11.0", - "util": "^0.10.3", + "util": "^0.11.0", "vm-browserify": "0.0.4" }, "dependencies": { @@ -6831,9 +6843,9 @@ } }, "node-releases": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.3.tgz", - "integrity": "sha512-6VrvH7z6jqqNFY200kdB6HdzkgM96Oaj9v3dqGfgp6mF+cHmU4wyQKZ2/WPDRVoR0Jz9KqbamaBN0ZhdUaysUQ==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.6.tgz", + "integrity": "sha512-lODUVHEIZutZx+TDdOk47qLik8FJMXzJ+WnyUGci1MTvTOyzZrz5eVPIIpc5Hb3NfHZGeGHeuwrRYVI1PEITWg==", "dev": true, "requires": { "semver": "^5.3.0" @@ -6907,9 +6919,9 @@ } }, "normalize-package-data": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.2.tgz", + "integrity": "sha512-YcMnjqeoUckXTPKZSAsPjUPLxH85XotbpqK3w4RyCwdFQSU5FxxBys8buehkSfg0j9fKvV1hn7O0+8reEgkAiw==", "dev": true, "requires": { "hosted-git-info": "^2.1.4", @@ -7232,9 +7244,9 @@ "dev": true }, "p-is-promise": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz", - "integrity": "sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.0.0.tgz", + "integrity": "sha512-pzQPhYMCAgLAKPWD2jC3Se9fEfrD9npNos0y150EeqZll7akhEgGhTW/slB6lHku8AvYGiJ+YJ5hfHKePPgFWg==", "dev": true }, "p-limit": { @@ -7268,9 +7280,9 @@ "dev": true }, "pako": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.7.tgz", - "integrity": "sha512-3HNK5tW4x8o5mO8RuHZp3Ydw9icZXx0RANAOMzlMzx7LVXhMJ4mo3MOBpzyd7r/+RUu8BmndP47LXT+vzjtWcQ==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.8.tgz", + "integrity": "sha512-6i0HVbUfcKaTv+EG8ZTr75az7GFXcLYk9UyLEg7Notv/Ma+z/UG3TCoz6GiNeOrn1E/e63I0X/Hpw18jHOTUnA==", "dev": true }, "parallel-transform": { @@ -7285,16 +7297,17 @@ } }, "parse-asn1": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", - "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.3.tgz", + "integrity": "sha512-VrPoetlz7B/FqjBLD2f5wBVZvsZVLnRUrxVLfRYhGXCODa/NWE4p3Wp+6+aV3ZPL3KM7/OZmxDIwwijD7yuucg==", "dev": true, "requires": { "asn1.js": "^4.0.0", "browserify-aes": "^1.0.0", "create-hash": "^1.1.0", "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3" + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" } }, "parse-glob": { @@ -7495,14 +7508,14 @@ "dev": true }, "postcss": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.5.tgz", - "integrity": "sha512-HBNpviAUFCKvEh7NZhw1e8MBPivRszIiUnhrJ+sBFVSYSqubrzwX3KG51mYgcRHX8j/cAgZJedONZcm5jTBdgQ==", + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.11.tgz", + "integrity": "sha512-9AXb//5UcjeOEof9T+yPw3XTa5SL207ZOIC/lHYP4mbUTEh4M0rDAQekQpVANCZdwQwKhBtFZCk3i3h3h2hdWg==", "dev": true, "requires": { - "chalk": "^2.4.1", + "chalk": "^2.4.2", "source-map": "^0.6.1", - "supports-color": "^5.5.0" + "supports-color": "^6.1.0" }, "dependencies": { "source-map": { @@ -7510,15 +7523,6 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } } } }, @@ -8026,9 +8030,9 @@ } }, "reflect-metadata": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.12.tgz", - "integrity": "sha512-n+IyV+nGz3+0q3/Yf1ra12KpCyi001bi4XFxSjbiWWjfqb52iTTtpGXmCCAOWWIAn9KEuFZKGqBERHmrtScZ3A==", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", "dev": true }, "regenerate": { @@ -8974,38 +8978,81 @@ "dev": true }, "spdy": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-3.4.7.tgz", - "integrity": "sha1-Qv9B7OXMD5mjpsKKq7c/XDsDrLw=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.0.tgz", + "integrity": "sha512-ot0oEGT/PGUpzf/6uk4AWLqkq+irlqHXkrdbk51oWONh3bxQmBuljxPNl66zlRRcIJStWq0QkLUCPOPjgjvU0Q==", "dev": true, "requires": { - "debug": "^2.6.8", - "handle-thing": "^1.2.5", + "debug": "^4.1.0", + "handle-thing": "^2.0.0", "http-deceiver": "^1.2.7", - "safe-buffer": "^5.0.1", "select-hose": "^2.0.0", - "spdy-transport": "^2.0.18" + "spdy-transport": "^3.0.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + } } }, "spdy-transport": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-2.1.1.tgz", - "integrity": "sha512-q7D8c148escoB3Z7ySCASadkegMmUZW8Wb/Q1u0/XBgDKMO880rLQDj8Twiew/tYi7ghemKUi/whSYOwE17f5Q==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", "dev": true, "requires": { - "debug": "^2.6.8", - "detect-node": "^2.0.3", + "debug": "^4.1.0", + "detect-node": "^2.0.4", "hpack.js": "^2.1.6", - "obuf": "^1.1.1", - "readable-stream": "^2.2.9", - "safe-buffer": "^5.0.1", - "wbuf": "^1.7.2" + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + }, + "readable-stream": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.1.1.tgz", + "integrity": "sha512-DkN66hPyqDhnIQ6Jcsvx9bFjhw214O4poMBcIMgPVpQvNy9a0e0Uhg5SqySyDKAmUlwt8LonTBz1ezOnM8pUdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } } }, "speed-measure-webpack-plugin": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/speed-measure-webpack-plugin/-/speed-measure-webpack-plugin-1.2.3.tgz", - "integrity": "sha512-p+taQ69VkRUXYMoZOx2nxV/Tz8tt79ahctoZJyJDHWP7fEYvwFNf5Pd73k5kZ6auu0pTsPNLEUwWpM8mCk85Zw==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/speed-measure-webpack-plugin/-/speed-measure-webpack-plugin-1.2.5.tgz", + "integrity": "sha512-S/guYjC4Izn5wY2d0+M4zowED/F77Lxh9yjkTZ+XAr244pr9c1MYNcXcRe9lx2hmAj0GPbOrBXgOF2YIp/CZ8A==", "dev": true, "requires": { "chalk": "^2.0.1" @@ -9027,9 +9074,9 @@ "dev": true }, "sshpk": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.0.tgz", - "integrity": "sha512-Zhev35/y7hRMcID/upReIvRse+I9SVhyVre/KTJSJQWMz3C3+G+HpO7m1wK/yckEtujKZ7dS4hkVxAnmHaIGVQ==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", "dev": true, "requires": { "asn1": "~0.2.3", @@ -9099,9 +9146,9 @@ } }, "stream-browserify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", - "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", + "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", "dev": true, "requires": { "inherits": "~2.0.1", @@ -9281,9 +9328,9 @@ } }, "supports-color": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", - "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", "dev": true, "requires": { "has-flag": "^3.0.0" @@ -9333,9 +9380,9 @@ } }, "terser-webpack-plugin": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.1.0.tgz", - "integrity": "sha512-61lV0DSxMAZ8AyZG7/A4a3UPlrbOBo8NIQ4tJzLPAdGOQ+yoNC7l5ijEow27lBAL2humer01KLS6bGIMYQxKoA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.2.1.tgz", + "integrity": "sha512-GGSt+gbT0oKcMDmPx4SRSfJPE1XaN3kQRWG4ghxKQw9cn5G9x6aCKSsgYdvyM0na9NJ4Drv0RG6jbBByZ5CMjw==", "dev": true, "requires": { "cacache": "^11.0.2", @@ -9698,9 +9745,9 @@ }, "dependencies": { "resolve": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.9.0.tgz", - "integrity": "sha512-TZNye00tI67lwYvzxCxHGjwTNlUV70io54/Ed4j6PscB8xVfuBJpRenI/o6dVk0cY0PYTY27AgCoGGxRnYuItQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz", + "integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==", "dev": true, "requires": { "path-parse": "^1.0.6" @@ -9817,56 +9864,6 @@ } } }, - "uglifyjs-webpack-plugin": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.3.0.tgz", - "integrity": "sha512-ovHIch0AMlxjD/97j9AYovZxG5wnHOPkL7T1GKochBADp/Zwc44pEWNqpKl1Loupp1WhFg7SlYmHZRUfdAacgw==", - "dev": true, - "requires": { - "cacache": "^10.0.4", - "find-cache-dir": "^1.0.0", - "schema-utils": "^0.4.5", - "serialize-javascript": "^1.4.0", - "source-map": "^0.6.1", - "uglify-es": "^3.3.4", - "webpack-sources": "^1.1.0", - "worker-farm": "^1.5.2" - }, - "dependencies": { - "commander": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz", - "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==", - "dev": true - }, - "schema-utils": { - "version": "0.4.7", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz", - "integrity": "sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-keywords": "^3.1.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "uglify-es": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz", - "integrity": "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==", - "dev": true, - "requires": { - "commander": "~2.13.0", - "source-map": "~0.6.1" - } - } - } - }, "ultron": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", @@ -10046,9 +10043,9 @@ } }, "util": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", - "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", "dev": true, "requires": { "inherits": "2.0.3" @@ -10154,15 +10151,15 @@ } }, "webpack": { - "version": "4.23.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.23.1.tgz", - "integrity": "sha512-iE5Cu4rGEDk7ONRjisTOjVHv3dDtcFfwitSxT7evtYj/rANJpt1OuC/Kozh1pBa99AUBr1L/LsaNB+D9Xz3CEg==", + "version": "4.28.4", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.28.4.tgz", + "integrity": "sha512-NxjD61WsK/a3JIdwWjtIpimmvE6UrRi3yG54/74Hk9rwNj5FPkA4DJCf1z4ByDWLkvZhTZE+P3C/eh6UD5lDcw==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.7.10", - "@webassemblyjs/helper-module-context": "1.7.10", - "@webassemblyjs/wasm-edit": "1.7.10", - "@webassemblyjs/wasm-parser": "1.7.10", + "@webassemblyjs/ast": "1.7.11", + "@webassemblyjs/helper-module-context": "1.7.11", + "@webassemblyjs/wasm-edit": "1.7.11", + "@webassemblyjs/wasm-parser": "1.7.11", "acorn": "^5.6.2", "acorn-dynamic-import": "^3.0.0", "ajv": "^6.1.0", @@ -10180,7 +10177,7 @@ "node-libs-browser": "^2.0.0", "schema-utils": "^0.4.4", "tapable": "^1.1.0", - "uglifyjs-webpack-plugin": "^1.2.4", + "terser-webpack-plugin": "^1.1.0", "watchpack": "^1.5.0", "webpack-sources": "^1.3.0" }, @@ -10245,9 +10242,9 @@ } }, "webpack-dev-server": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.1.10.tgz", - "integrity": "sha512-RqOAVjfqZJtQcB0LmrzJ5y4Jp78lv9CK0MZ1YJDTaTmedMZ9PU9FLMQNrMCfVu8hHzaVLVOJKBlGEHMN10z+ww==", + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.1.14.tgz", + "integrity": "sha512-mGXDgz5SlTxcF3hUpfC8hrQ11yhAttuUQWf1Wmb+6zo3x6rb7b9mIfuQvAPLdfDRCGRGvakBWHdHOa0I9p/EVQ==", "dev": true, "requires": { "ansi-html": "0.0.7", @@ -10269,12 +10266,14 @@ "portfinder": "^1.0.9", "schema-utils": "^1.0.0", "selfsigned": "^1.9.1", + "semver": "^5.6.0", "serve-index": "^1.7.2", "sockjs": "0.3.19", "sockjs-client": "1.3.0", - "spdy": "^3.4.1", + "spdy": "^4.0.0", "strip-ansi": "^3.0.0", "supports-color": "^5.1.0", + "url": "^0.11.0", "webpack-dev-middleware": "3.4.0", "webpack-log": "^2.0.0", "yargs": "12.0.2" @@ -10460,6 +10459,12 @@ "once": "^1.3.1" } }, + "semver": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", + "dev": true + }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 8518e439d78d5bc9ec5c72a58c80decc8765e33b..3814d1f71e95bd6f9a0ac10834666b48f5a735bf 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,22 +21,20 @@ "@angular/platform-browser": "7.0.4", "@angular/platform-browser-dynamic": "7.0.4", "@angular/router": "7.0.4", - "@types/leaflet": "^1.2.14", - "@types/leaflet.markercluster": "^1.0.3", + "@types/leaflet": "1.2.14", + "@types/leaflet.markercluster": "1.0.3", "@ng-bootstrap/ng-bootstrap": "4.0.0", "bootstrap": "4.1.3", "core-js": "2.5.7", "font-awesome": "4.7.0", - "jquery": "^3.3.1", - "leaflet": "^1.3.4", - "leaflet.markercluster": "^1.4.1", - "popper": "^1.0.1", + "leaflet": "1.3.4", + "leaflet.markercluster": "1.4.1", "rxjs": "6.3.3", "trait-ontology-widget": "git+https://github.com/gnpis/trait-ontology-widget.git#v2.2.1", "zone.js": "0.8.26" }, "devDependencies": { - "@angular-devkit/build-angular": "^0.12.2", + "@angular-devkit/build-angular": "0.12.2", "@angular/cli": "7.0.6", "@angular/compiler-cli": "7.0.4", "@angular/language-service": "7.0.4", diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 9d785120efcba6a806b32fc668647f506a98c327..f3486f6fcb9ba85c2cabf2a77e9fe73d513e47b0 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -19,7 +19,10 @@ import { ErrorComponent } from './error/error.component'; import { ErrorInterceptorService } from './error-interceptor.service'; import { TraitOntologyWidgetComponent } from './form/trait-ontology-widget/trait-ontology-widget.component'; import { FacetsComponent } from './result-page/facets/facets.component'; - +import { CardRowComponent } from './card-row/card-row.component'; +import { CardSectionComponent } from './card-section/card-section.component'; +import { LoadingSpinnerComponent } from './loading-spinner/loading-spinner.component'; +import { CardTableComponent } from './card-table/card-table.component'; @NgModule({ declarations: [ @@ -35,7 +38,11 @@ import { FacetsComponent } from './result-page/facets/facets.component'; DocumentComponent, ErrorComponent, TraitOntologyWidgetComponent, - FacetsComponent + FacetsComponent, + CardRowComponent, + CardSectionComponent, + LoadingSpinnerComponent, + CardTableComponent, ], imports: [ BrowserModule, diff --git a/frontend/src/app/brapi.service.spec.ts b/frontend/src/app/brapi.service.spec.ts index 36ef86b27e238e09e7b3ad3dc3f5513aa4b0a76b..898d0c90a492a020fb63d66c71df9f865ac6a6ee 100644 --- a/frontend/src/app/brapi.service.spec.ts +++ b/frontend/src/app/brapi.service.spec.ts @@ -2,7 +2,17 @@ import { TestBed } from '@angular/core/testing'; import { BrapiService } from './brapi.service'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { SiteModel } from './models/site.model'; +import { + BrapiContacts, + BrapiGermplasm, + BrapiLocation, + BrapiObservationVariable, + BrapiResult, + BrapiResults, + BrapiStudy, + BrapiTrial +} from './models/brapi.model'; +import { DataDiscoverySource } from './models/data-discovery.model'; describe('BrapiService', () => { @@ -20,30 +30,185 @@ describe('BrapiService', () => { afterAll(() => http.verify()); - it('should return an Observable of 1 SiteModel', () => { - const hardCodedSite: SiteModel = { - result: { - locationDbId: 1, - latitude: 1, - longitude: 1, - altitude: 1, - institutionName: '', - institutionAdress: '', - countryName: '', - countryCode: '', - locationType: '', - abbreviation: '', - name: 'site1', - additionalInfo: {} - } + const location: BrapiLocation = { + locationDbId: '1', + locationName: 'loc1', + locationType: 'Collecting site', + abbreviation: null, + countryCode: 'Fr', + countryName: 'France', + institutionAddress: null, + institutionName: 'Insti', + altitude: null, + latitude: null, + longitude: null, + }; + + const contacts: BrapiContacts = { + contactDbId: 'c1', + name: 'contact1', + email: 'contact1@email.com', + type: 'contact', + institutionName: 'Inst', + }; + + const searchStudy: BrapiResult<BrapiStudy> = { + metadata: null, + result: { + studyDbId: 's1', + studyType: 'phenotype', + studyName: 'study1', + studyDescription: null, + seasons: ['winter', '2019'], + startDate: '2018', + endDate: null, + active: true, + programDbId: 'p1', + programName: 'program1', + trialDbIds: ['10', '20'], + location: location, + contacts: [contacts], + additionalInfo: null, + dataLinks: [] + } + }; + + const trial1: BrapiResult<BrapiTrial> = { + metadata: null, + result: { + trialDbId: '10', + trialName: 'trial_10', + trialType: 'project', + active: true, + studies: [ + { studyDbId: 's1', studyName: 'study1' }, + { studyDbId: 's2', studyName: 'study2' } + ] + + } + }; + + const osbVariable: BrapiResults<BrapiObservationVariable> = { + metadata: null, + result: { + data: [{ + observationVariableDbId: 'var1', + contextOfUse: null, + institution: 'Insti', + crop: 'WoodyPlant', + name: 'varaiable1', + ontologyDbId: 'WPO', + ontologyName: 'Woody Plant Ontology', + synonyms: ['First synonym'], + language: 'EN', + trait: { + traitDbId: 't1', + name: 'trait1', + description: null, + }, + documentationURL: null, + }] + } + }; + + const germplasm: BrapiResults<BrapiGermplasm> = { + metadata: null, + result: { + data: [{ + germplasmDbId: 'g1', + accessionNumber: 'G_10', + germplasmName: 'germplam1', + genus: 'Populus', + species: 'x generosa', + subtaxa: '' + }, { + germplasmDbId: 'g2', + accessionNumber: 'G_20', + germplasmName: 'germplam2', + genus: 'Triticum', + species: 'aestivum', + subtaxa: 'subsp' + }], + } + }; + + const source: DataDiscoverySource = { + '@id': 'src1', + '@type': ['schema:DataCatalog'], + 'schema:identifier': 'srcId', + 'schema:name': 'source1', + 'schema:url': 'srcUrl', + 'schema:image': null + }; + + it('should fetch the study', () => { + let fetchedStudy: BrapiResult<BrapiStudy>; + const studyDbId: string = searchStudy.result.studyDbId; + brapiService.study(searchStudy.result.studyDbId).subscribe(response => { + fetchedStudy = response; + }); + http.expectOne(`brapi/v1/studies/${studyDbId}`) + .flush(searchStudy); + + expect(fetchedStudy).toEqual(searchStudy); + + }); + + it('should fetch the germplasm', () => { + + let fetchedGermplasm: BrapiResults<BrapiGermplasm>; + const studyDbId: string = searchStudy.result.studyDbId; + brapiService.studyGermplasms(searchStudy.result.studyDbId).subscribe(response => { + fetchedGermplasm = response; + }); + http.expectOne(`brapi/v1/studies/${studyDbId}/germplasm`) + .flush(germplasm); + + expect(fetchedGermplasm).toEqual(germplasm); + + }); + + it('should fetch the variables', () => { + + let fetchedVariables: BrapiResults<BrapiObservationVariable>; + const studyDbId: string = searchStudy.result.studyDbId; + brapiService.studyObservationVariables(searchStudy.result.studyDbId).subscribe(response => { + fetchedVariables = response; + }); + http.expectOne(`brapi/v1/studies/${studyDbId}/observationVariables`) + .flush(osbVariable); + + expect(fetchedVariables).toEqual(osbVariable); + + }); + + it('should fetch the trials', () => { + + let fetchedTrials: BrapiResult<BrapiTrial>; + const trialDbId: string = trial1.result.trialDbId; + brapiService.studyTrials(trialDbId).subscribe(response => { + fetchedTrials = response; + }); + http.expectOne(`brapi/v1/trials/${trialDbId}`) + .flush(trial1); + + expect(fetchedTrials).toEqual(trial1); + + }); + + it('should fetch 1 location', () => { + const mockResponse: BrapiResult<BrapiLocation> = { + metadata: null, + result: location }; - let actualSite: SiteModel; - const locationId = hardCodedSite.result.locationDbId; - brapiService.location(hardCodedSite.result.locationDbId).subscribe(site => actualSite = site); + let actualLocation: BrapiLocation; + const locationId = mockResponse.result.locationDbId; + brapiService.location(mockResponse.result.locationDbId).subscribe(response => actualLocation = response.result); http.expectOne(`brapi/v1/locations/${locationId}`) - .flush(hardCodedSite); + .flush(mockResponse); - expect(actualSite).toEqual(hardCodedSite); + expect(actualLocation).toEqual(location); }); + }); diff --git a/frontend/src/app/brapi.service.ts b/frontend/src/app/brapi.service.ts index 9200ef99ca740c45503cc32799326bde93298535..be27a43a0e39d9dfe0ea5a302973ee38a6e48a0b 100644 --- a/frontend/src/app/brapi.service.ts +++ b/frontend/src/app/brapi.service.ts @@ -1,48 +1,61 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { HttpClient } from '@angular/common/http'; -import { delay } from 'rxjs/operators'; -import { SiteModel, SiteResultModel } from './models/site.model'; +import { + BrapiGermplasm, + BrapiLocation, + BrapiObservationVariable, + BrapiResult, + BrapiResults, + BrapiStudy, + BrapiTrial +} from './models/brapi.model'; + +export const BASE_URL = 'brapi/v1'; @Injectable({ providedIn: 'root' }) - export class BrapiService { constructor(private http: HttpClient) { } germplasm(germplasmDbId: string): Observable<object> { - return this.http.get<object>(`brapi/v1/germplasm/${germplasmDbId}`); + return this.http.get<object>(`${BASE_URL}/germplasm/${germplasmDbId}`); } germplasmPedigree(germplasmDbId: string): Observable<object> { - return this.http.get<object>(`brapi/v1/germplasm/${germplasmDbId}/pedigree`); + return this.http.get<object>(`${BASE_URL}/germplasm/${germplasmDbId}/pedigree`); } germplasmProgeny(germplasmDbId: string): Observable<object> { - return this.http.get<object>(`brapi/v1/germplasm/${germplasmDbId}/progeny`); + return this.http.get<object>(`${BASE_URL}/germplasm/${germplasmDbId}/progeny`); } germplasmAttributes(germplasmDbId: string): Observable<object> { - return this.http.get<object>(`brapi/v1/germplasm/${germplasmDbId}/attributes`); + return this.http.get<object>(`${BASE_URL}/germplasm/${germplasmDbId}/attributes`); + } + + study(studyDbId: string): Observable<BrapiResult<BrapiStudy>> { + const options = { headers: { 'Accept': 'application/ld+json,application/json' } }; + return this.http.get<BrapiResult<BrapiStudy>>(`${BASE_URL}/studies/${studyDbId}`, options); } - study(studyDbId: string): Observable<object> { - return this.http.get<object>(`brapi/v1/studies/${studyDbId}`); + studyGermplasms(studyDbId: string): Observable<BrapiResults<BrapiGermplasm>> { + return this.http.get<BrapiResults<BrapiGermplasm>>(`${BASE_URL}/studies/${studyDbId}/germplasm`); } - studyGermplasms(studyDbId: string): Observable<string[]> { - return this.http.get<string[]>(`brapi/v1/studies/${studyDbId}/germplasm`); + studyObservationVariables(studyDbId: string): Observable<BrapiResults<BrapiObservationVariable>> { + return this.http.get<BrapiResults<BrapiObservationVariable>>(`${BASE_URL}/studies/${studyDbId}/observationVariables`); } - studyObservationVariables(studyDbId: string): Observable<string[]> { - return this.http.get<string[]>(`brapi/v1/studies/${studyDbId}/observationVariables`); + location(locationId: string): Observable<BrapiResult<BrapiLocation>> { + return this.http.get<BrapiResult<BrapiLocation>>(`${BASE_URL}/locations/${locationId}`); } - location(locationId: number): Observable<SiteModel> { - return this.http.get<SiteModel>(`brapi/v1/locations/${locationId}`); + studyTrials(trialsId: string): Observable<BrapiResult<BrapiTrial>> { + return this.http.get<BrapiResult<BrapiTrial>>(`${BASE_URL}/trials/${trialsId}`); } } diff --git a/frontend/src/app/card-row/card-row.component.html b/frontend/src/app/card-row/card-row.component.html new file mode 100644 index 0000000000000000000000000000000000000000..e3d9f2587f5fe4f6857effbfb70828c6073eb35d --- /dev/null +++ b/frontend/src/app/card-row/card-row.component.html @@ -0,0 +1,14 @@ +<ng-container *ngIf="test && (value === undefined || value)"> + <div class="row row-sep"> + <div class="col-4 field my-2"> + {{ label }} + </div> + <div class="col my-2"> + <ng-template #includeTemplate [ngTemplateOutlet]="template"></ng-template> + <ng-template #includeStringValue> + {{ value }} + </ng-template> + <ng-container *ngIf="value; then includeStringValue else includeTemplate"></ng-container> + </div> + </div> +</ng-container> diff --git a/frontend/src/app/card-row/card-row.component.scss b/frontend/src/app/card-row/card-row.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..5f6ecd43d609ce9c88484a500fe32cc215501c3c --- /dev/null +++ b/frontend/src/app/card-row/card-row.component.scss @@ -0,0 +1,7 @@ +.row-sep { + border-top: 0.01px solid #f0f0f0; +} + +.field { + font-weight: bold; +} diff --git a/frontend/src/app/card-row/card-row.component.spec.ts b/frontend/src/app/card-row/card-row.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..95ab00d83c82f44f1dd4c222345ef3e7dfe75f0d --- /dev/null +++ b/frontend/src/app/card-row/card-row.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CardRowComponent } from './card-row.component'; + +describe('CardRowComponent', () => { + let component: CardRowComponent; + let fixture: ComponentFixture<CardRowComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [CardRowComponent] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CardRowComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/card-row/card-row.component.ts b/frontend/src/app/card-row/card-row.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..9def776858b0589e60ae47c14ce1047fcc2a1c76 --- /dev/null +++ b/frontend/src/app/card-row/card-row.component.ts @@ -0,0 +1,16 @@ +import { Component, ContentChild, Input, TemplateRef } from '@angular/core'; + +@Component({ + selector: 'gpds-card-row', + templateUrl: './card-row.component.html', + styleUrls: ['./card-row.component.scss'] +}) +export class CardRowComponent { + + @Input() label: string; + @Input() test: any = true; + @Input() value: string; + + @ContentChild(TemplateRef) template: TemplateRef<any>; + +} diff --git a/frontend/src/app/card-section/card-section.component.html b/frontend/src/app/card-section/card-section.component.html new file mode 100644 index 0000000000000000000000000000000000000000..ffaa0ba6b3a5afb787f18d12f65b042585dc4b8a --- /dev/null +++ b/frontend/src/app/card-section/card-section.component.html @@ -0,0 +1,6 @@ +<div class="card mb-3 mt-4" *ngIf="test"> + <div class="card-header"> + {{ header }} + </div> + <ng-container [ngTemplateOutlet]="template"></ng-container> +</div> diff --git a/frontend/src/app/card-section/card-section.component.scss b/frontend/src/app/card-section/card-section.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..155466cf1b6c0b21bbf28661089d1da24001ad64 --- /dev/null +++ b/frontend/src/app/card-section/card-section.component.scss @@ -0,0 +1,15 @@ +.card { + margin-top: 25px; + margin-bottom: 10px; + border-color: rgba(89, 89, 89, 0.33); + border-width: 1.5px; +} + +.card-header { + font-size: large; + font-weight: bold; + color: #0f6191; + background-color: #eaeaea; + border-color: rgba(29, 29, 29, 0.16); + border-width: 2.5px; +} diff --git a/frontend/src/app/card-section/card-section.component.spec.ts b/frontend/src/app/card-section/card-section.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..874070e7d85edb2aa22393ae5ab055ea5cb0b5aa --- /dev/null +++ b/frontend/src/app/card-section/card-section.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CardSectionComponent } from './card-section.component'; + +describe('CardSectionComponent', () => { + let component: CardSectionComponent; + let fixture: ComponentFixture<CardSectionComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [CardSectionComponent] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CardSectionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/card-section/card-section.component.ts b/frontend/src/app/card-section/card-section.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..bd014e2e38fe4019d4284108b0428b299ef47c24 --- /dev/null +++ b/frontend/src/app/card-section/card-section.component.ts @@ -0,0 +1,15 @@ +import { Component, ContentChild, Input, TemplateRef } from '@angular/core'; + +@Component({ + selector: 'gpds-card-section', + templateUrl: './card-section.component.html', + styleUrls: ['./card-section.component.scss'] +}) +export class CardSectionComponent { + + @Input() header: string; + @Input() test: any = true; + + @ContentChild(TemplateRef) template: TemplateRef<any>; + +} diff --git a/frontend/src/app/card-table/card-table.component.html b/frontend/src/app/card-table/card-table.component.html new file mode 100644 index 0000000000000000000000000000000000000000..beb8c09450a0d12acbc7dfdd9ff074745d034690 --- /dev/null +++ b/frontend/src/app/card-table/card-table.component.html @@ -0,0 +1,17 @@ +<div class="table-responsive scroll-table"> + <table class="table table-sm table-striped"> + <thead *ngIf="headers"> + <tr> + <th *ngFor="let header of headers" scope="col"> + {{ header }} + </th> + </tr> + </thead> + <tbody> + <ng-container *ngFor="let row of rows" + [ngTemplateOutlet]="template" + [ngTemplateOutletContext]="{$implicit: row}"> + </ng-container> + </tbody> + </table> +</div> diff --git a/frontend/src/app/card-table/card-table.component.scss b/frontend/src/app/card-table/card-table.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..f721891f8501e63874515eb0119c44fcf7ce090a --- /dev/null +++ b/frontend/src/app/card-table/card-table.component.scss @@ -0,0 +1,11 @@ + +.scroll-table { + max-height: 200px; + overflow-y: auto; + + thead { + position: sticky; + top: 0; + background-color: white; + } +} diff --git a/frontend/src/app/card-table/card-table.component.spec.ts b/frontend/src/app/card-table/card-table.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..e21f25a54632d5ec9310ad09b42518d824f37dbf --- /dev/null +++ b/frontend/src/app/card-table/card-table.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CardTableComponent } from './card-table.component'; + +describe('CardTableComponent', () => { + let component: CardTableComponent; + let fixture: ComponentFixture<CardTableComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [CardTableComponent] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CardTableComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/card-table/card-table.component.ts b/frontend/src/app/card-table/card-table.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..dd871e030080e7ff6679341f87f95ac4ec1254d6 --- /dev/null +++ b/frontend/src/app/card-table/card-table.component.ts @@ -0,0 +1,15 @@ +import { Component, ContentChild, Input, TemplateRef } from '@angular/core'; + +@Component({ + selector: 'gpds-card-table', + templateUrl: './card-table.component.html', + styleUrls: ['./card-table.component.scss'] +}) +export class CardTableComponent { + + @Input() headers: string[]; + @Input() rows: any[]; + + @ContentChild(TemplateRef) template: TemplateRef<any>; + +} diff --git a/frontend/src/app/error/error.component.html b/frontend/src/app/error/error.component.html index 69490868e8b4365c5afaca66941da02a5fad4887..a52b913ffddb93318d8e0dfc91b4e3519d075c03 100644 --- a/frontend/src/app/error/error.component.html +++ b/frontend/src/app/error/error.component.html @@ -9,7 +9,9 @@ <a href="javascript:window.location.reload(true)"> Refresh the page </a> - or try later.<br/> + or try later. + <br/> + Please contact <a href="mailto:urgi-support@inra.fr">urgi-support@inra.fr</a> if the problem persists. </p> <small *ngIf="error.status" id="error-status"> Status: {{ error.status }} diff --git a/frontend/src/app/form/form.component.html b/frontend/src/app/form/form.component.html index f55d6443959040c7e34682c77ffb8466fdf50c81..3df3dbe20dcb972a632008200d563d2b1c63ada5 100644 --- a/frontend/src/app/form/form.component.html +++ b/frontend/src/app/form/form.component.html @@ -30,7 +30,7 @@ inputId="crops" criteriaField="crops" [criteria$]="criteria$" - placeholder="Select or search crops"> + placeholder="Search crops"> </gpds-suggestion-field> </div> </div> @@ -47,7 +47,7 @@ inputId="germplasmList" criteriaField="germplasmLists" [criteria$]="criteria$" - placeholder="Select or search germplasm lists"> + placeholder="Search germplasm lists"> </gpds-suggestion-field> </div> </div> @@ -64,7 +64,7 @@ inputId="accessions" criteriaField="accessions" [criteria$]="criteria$" - placeholder="Select or search germplasm accession"> + placeholder="Search germplasm accession"> </gpds-suggestion-field> </div> </div> diff --git a/frontend/src/app/form/form.component.ts b/frontend/src/app/form/form.component.ts index b19825b4d283f0368d15dcb466c0260d1bf8fbdb..b546c41a84870bf690f8fc60c1751a898ff00838 100644 --- a/frontend/src/app/form/form.component.ts +++ b/frontend/src/app/form/form.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { DataDiscoveryCriteria } from '../model/data-discovery.model'; +import { DataDiscoveryCriteria } from '../models/data-discovery.model'; import { BehaviorSubject } from 'rxjs'; @Component({ diff --git a/frontend/src/app/form/suggestion-field/suggestion-field.component.spec.ts b/frontend/src/app/form/suggestion-field/suggestion-field.component.spec.ts index 7b7e7060b49025e8ce71639b7e2ec7617c3a4d95..f20bd4547fa0d6b7c56f1514aee1768fecf7ec16 100644 --- a/frontend/src/app/form/suggestion-field/suggestion-field.component.spec.ts +++ b/frontend/src/app/form/suggestion-field/suggestion-field.component.spec.ts @@ -5,7 +5,7 @@ import { ReactiveFormsModule } from '@angular/forms'; import { NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap'; import { GnpisService } from '../../gnpis.service'; import { BehaviorSubject, of } from 'rxjs'; -import { DataDiscoveryCriteriaUtils } from '../../model/data-discovery.model'; +import { DataDiscoveryCriteriaUtils } from '../../models/data-discovery.model'; import { ComponentTester } from 'ngx-speculoos'; import { HttpClientTestingModule } from '@angular/common/http/testing'; diff --git a/frontend/src/app/form/suggestion-field/suggestion-field.component.ts b/frontend/src/app/form/suggestion-field/suggestion-field.component.ts index 2b06553c2643469bb997902b8a74c322dd987afe..6505693e38a78cecfddab250afd61ecc709bfbb5 100644 --- a/frontend/src/app/form/suggestion-field/suggestion-field.component.ts +++ b/frontend/src/app/form/suggestion-field/suggestion-field.component.ts @@ -4,7 +4,7 @@ import { FormControl } from '@angular/forms'; import { NgbTypeahead, NgbTypeaheadSelectItemEvent } from '@ng-bootstrap/ng-bootstrap'; import { GnpisService } from '../../gnpis.service'; import { debounceTime, filter, map, switchMap } from 'rxjs/operators'; -import { DataDiscoveryCriteria } from '../../model/data-discovery.model'; +import { DataDiscoveryCriteria } from '../../models/data-discovery.model'; @Component({ selector: 'gpds-suggestion-field', diff --git a/frontend/src/app/form/trait-ontology-widget/trait-ontology-widget.component.spec.ts b/frontend/src/app/form/trait-ontology-widget/trait-ontology-widget.component.spec.ts index def69cf3957b3231b11a81ca90043e2c4dcaadef..0ca592a60211ae1e0dcf6770d991fd1b82daafaa 100644 --- a/frontend/src/app/form/trait-ontology-widget/trait-ontology-widget.component.spec.ts +++ b/frontend/src/app/form/trait-ontology-widget/trait-ontology-widget.component.spec.ts @@ -3,7 +3,7 @@ import { TestBed } from '@angular/core/testing'; import { CropOntologyWidgetFactory, TraitOntologyWidgetComponent } from './trait-ontology-widget.component'; import { GnpisService } from '../../gnpis.service'; import { BehaviorSubject, of } from 'rxjs'; -import { DataDiscoveryCriteriaUtils } from '../../model/data-discovery.model'; +import { DataDiscoveryCriteriaUtils } from '../../models/data-discovery.model'; import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap'; import { HttpClientTestingModule } from '@angular/common/http/testing'; diff --git a/frontend/src/app/form/trait-ontology-widget/trait-ontology-widget.component.ts b/frontend/src/app/form/trait-ontology-widget/trait-ontology-widget.component.ts index 7750151d926c1fdcb798e3a835bad94210445300..b27eece17dd62178d011f70235a9f32b96c264b0 100644 --- a/frontend/src/app/form/trait-ontology-widget/trait-ontology-widget.component.ts +++ b/frontend/src/app/form/trait-ontology-widget/trait-ontology-widget.component.ts @@ -1,6 +1,6 @@ import { Component, EventEmitter, Injectable, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; import { CropOntologyWidget } from 'trait-ontology-widget/dist/module/cropOntologyWidget.module'; -import { DataDiscoveryCriteria } from '../../model/data-discovery.model'; +import { DataDiscoveryCriteria } from '../../models/data-discovery.model'; import { BehaviorSubject } from 'rxjs'; import { GnpisService } from '../../gnpis.service'; import { filter } from 'rxjs/operators'; diff --git a/frontend/src/app/gnpis.service.spec.ts b/frontend/src/app/gnpis.service.spec.ts index b89ac8263987bb31743a1a05a0fb1d18f82e3d94..6ffe4c0cc04c92de9a9582301bf609184bd7bc0d 100644 --- a/frontend/src/app/gnpis.service.spec.ts +++ b/frontend/src/app/gnpis.service.spec.ts @@ -1,9 +1,9 @@ import { TestBed } from '@angular/core/testing'; -import { GnpisService } from './gnpis.service'; +import { BASE_URL, GnpisService } from './gnpis.service'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { BrapiMetaData, BrapiResults } from './model/brapi.model'; -import { DataDiscoveryCriteria, DataDiscoveryResults, DataDiscoverySource } from './model/data-discovery.model'; +import { BrapiMetaData, BrapiResults } from './models/brapi.model'; +import { DataDiscoveryCriteria, DataDiscoverySource } from './models/data-discovery.model'; describe('GnpisService', () => { let service: GnpisService; @@ -43,7 +43,7 @@ describe('GnpisService', () => { service = TestBed.get(GnpisService); const req = httpMock.expectOne({ method: 'GET', - url: `${GnpisService.BASE_URL}/sources` + url: `${BASE_URL}/sources` }); req.flush(sources); @@ -62,7 +62,7 @@ describe('GnpisService', () => { }); const req = httpMock.expectOne({ - url: `${GnpisService.BASE_URL}/suggest?field=${field}&text=${text}&fetchSize=${fetchSize}`, + url: `${BASE_URL}/suggest?field=${field}&text=${text}&fetchSize=${fetchSize}`, method: 'POST' }); req.flush(expectedSuggestions); @@ -112,7 +112,7 @@ describe('GnpisService', () => { }); const req = httpMock.expectOne({ - url: `${GnpisService.BASE_URL}/search`, + url: `${BASE_URL}/search`, method: 'POST' }); req.flush(rawResult); diff --git a/frontend/src/app/gnpis.service.ts b/frontend/src/app/gnpis.service.ts index 81862244ab6f6165c9f6441716588a6db84024c8..967550c891b5d5ad5ee02d68005f89ce33475bdd 100644 --- a/frontend/src/app/gnpis.service.ts +++ b/frontend/src/app/gnpis.service.ts @@ -1,15 +1,16 @@ import { Injectable } from '@angular/core'; import { Observable, ReplaySubject, zip } from 'rxjs'; import { HttpClient } from '@angular/common/http'; -import { DataDiscoveryCriteria, DataDiscoveryFacet, DataDiscoveryResults, DataDiscoverySource } from './model/data-discovery.model'; -import { BrapiResults } from './model/brapi.model'; +import { DataDiscoveryCriteria, DataDiscoveryFacet, DataDiscoveryResults, DataDiscoverySource } from './models/data-discovery.model'; +import { BrapiResults } from './models/brapi.model'; import { map } from 'rxjs/operators'; +export const BASE_URL = 'gnpis/v1/datadiscovery'; + @Injectable({ providedIn: 'root' }) export class GnpisService { - static BASE_URL = 'gnpis/v1/datadiscovery'; sourceByURI$ = new ReplaySubject<{ [key: string]: DataDiscoverySource }>(1); constructor(private http: HttpClient) { @@ -17,7 +18,7 @@ export class GnpisService { } private fetchSources(): void { - this.http.get(`${GnpisService.BASE_URL}/sources`).subscribe( + this.http.get(`${BASE_URL}/sources`).subscribe( (response: BrapiResults<DataDiscoverySource>) => { const sourceByURI = {}; for (const source of response.result.data) { @@ -44,7 +45,7 @@ export class GnpisService { ): Observable<string[]> { const params = { field, text, fetchSize: fetchSize.toString() }; return this.http.post<string[]>( - `${GnpisService.BASE_URL}/suggest`, criteria, { params } + `${BASE_URL}/suggest`, criteria, { params } ); } @@ -60,7 +61,7 @@ export class GnpisService { // Get source by URI this.sourceByURI$, // Get documents by criteria - this.http.post<any>(`${GnpisService.BASE_URL}/search`, criteria) + this.http.post<any>(`${BASE_URL}/search`, criteria) ).pipe(map(([sourceByURI, response]) => { // Extract BrAPI documents from result const documents = response.result.data; @@ -88,4 +89,11 @@ export class GnpisService { })); } + /** + * Get data source by URI + */ + getSource(sourceURI: string): Observable<DataDiscoverySource> { + return this.sourceByURI$.pipe(map(sourceByURI => sourceByURI[sourceURI])); + } + } diff --git a/frontend/src/app/loading-spinner/loading-spinner.component.html b/frontend/src/app/loading-spinner/loading-spinner.component.html new file mode 100644 index 0000000000000000000000000000000000000000..01060a7dd7c293b6db27feaf2c3928cc9680e766 --- /dev/null +++ b/frontend/src/app/loading-spinner/loading-spinner.component.html @@ -0,0 +1,15 @@ +<!-- Loading (see CSS for animation)--> +<div *ngIf="loading" class="lds-spinner mx-auto"> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> + <div></div> +</div> diff --git a/frontend/src/app/loading-spinner/loading-spinner.component.scss b/frontend/src/app/loading-spinner/loading-spinner.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..3c571d096f78cb16969f3c8100e0be9a06173ab4 --- /dev/null +++ b/frontend/src/app/loading-spinner/loading-spinner.component.scss @@ -0,0 +1,96 @@ +// Loading spinner +.lds-spinner { + $scale: 1.2; + + color: black; + display: block; + position: relative; + width: 64px * $scale; + height: 64px * $scale; + + div { + transform-origin: 32px * $scale 32px * $scale; + animation: lds-spinner 1.2s linear infinite; + } + + div:after { + content: " "; + display: block; + position: absolute; + top: 3px; + left: 29px * $scale; + width: 5px * $scale; + height: 14px * $scale; + border-radius: 20%; + background: black; + } + + div:nth-child(1) { + transform: rotate(0deg); + animation-delay: -1.1s; + } + + div:nth-child(2) { + transform: rotate(30deg); + animation-delay: -1s; + } + + div:nth-child(3) { + transform: rotate(60deg); + animation-delay: -0.9s; + } + + div:nth-child(4) { + transform: rotate(90deg); + animation-delay: -0.8s; + } + + div:nth-child(5) { + transform: rotate(120deg); + animation-delay: -0.7s; + } + + div:nth-child(6) { + transform: rotate(150deg); + animation-delay: -0.6s; + } + + div:nth-child(7) { + transform: rotate(180deg); + animation-delay: -0.5s; + } + + div:nth-child(8) { + transform: rotate(210deg); + animation-delay: -0.4s; + } + + div:nth-child(9) { + transform: rotate(240deg); + animation-delay: -0.3s; + } + + div:nth-child(10) { + transform: rotate(270deg); + animation-delay: -0.2s; + } + + div:nth-child(11) { + transform: rotate(300deg); + animation-delay: -0.1s; + } + + div:nth-child(12) { + transform: rotate(330deg); + animation-delay: 0s; + } +} + +@keyframes lds-spinner { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } +} diff --git a/frontend/src/app/loading-spinner/loading-spinner.component.spec.ts b/frontend/src/app/loading-spinner/loading-spinner.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..88109e14e18227b499a5a3ca4ca007fa86424592 --- /dev/null +++ b/frontend/src/app/loading-spinner/loading-spinner.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LoadingSpinnerComponent } from './loading-spinner.component'; + +describe('LoadingSpinnerComponent', () => { + let component: LoadingSpinnerComponent; + let fixture: ComponentFixture<LoadingSpinnerComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [LoadingSpinnerComponent] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LoadingSpinnerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/loading-spinner/loading-spinner.component.ts b/frontend/src/app/loading-spinner/loading-spinner.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..24cb590256c408c5580e06bc674dcbb31fbaaf62 --- /dev/null +++ b/frontend/src/app/loading-spinner/loading-spinner.component.ts @@ -0,0 +1,18 @@ +import { Component, Input, OnInit } from '@angular/core'; + +@Component({ + selector: 'gpds-loading-spinner', + templateUrl: './loading-spinner.component.html', + styleUrls: ['./loading-spinner.component.scss'] +}) +export class LoadingSpinnerComponent implements OnInit { + + @Input() loading: boolean; + + constructor() { + } + + ngOnInit() { + } + +} diff --git a/frontend/src/app/map/map.component.html b/frontend/src/app/map/map.component.html index d0fa5e94cbc93811f478ecd047adf32815b1cd9c..d6143c137693b3d868a4eedde59cad686e1a3c64 100644 --- a/frontend/src/app/map/map.component.html +++ b/frontend/src/app/map/map.component.html @@ -1,12 +1,12 @@ -<div id="map"> +<div id="map" class="rounded"> </div> <div id="maplegend"> - <img src="assets/gpds/images/marker-icon-red.png" id="red" /> + <img src="assets/gpds/images/marker-icon-red.png" id="red"/> <label for="red">Origin site</label> - <img src="assets/gpds/images/marker-icon-blue.png" id="blue" /> + <img src="assets/gpds/images/marker-icon-blue.png" id="blue"/> <label for="blue">Collecting site</label> - <img src="assets/gpds/images/marker-icon-green.png" id="green" /> + <img src="assets/gpds/images/marker-icon-green.png" id="green"/> <label for="green">Evaluation site</label> - <img src="assets/gpds/images/marker-icon-purple.png" id="purple" /> + <img src="assets/gpds/images/marker-icon-purple.png" id="purple"/> <label for="purple">Multi-purpose site</label> </div> diff --git a/frontend/src/app/map/map.component.scss b/frontend/src/app/map/map.component.scss index cb6f34692d608ed2bf0f401844506fa885c7f17d..5f9ab2fdae863ad7361c7e6b0043d310a6c1a131 100644 --- a/frontend/src/app/map/map.component.scss +++ b/frontend/src/app/map/map.component.scss @@ -1,5 +1,5 @@ #map { - border: #79c93f solid 1px; + border: #0f6e9f solid 1px; height: 600px; width: 100% } diff --git a/frontend/src/app/map/map.component.spec.ts b/frontend/src/app/map/map.component.spec.ts index 76a8c02b14627cdd6bc774e546a4f3e269e4b660..275f029147e165dc5351e4a77bb19aadd0bee088 100644 --- a/frontend/src/app/map/map.component.spec.ts +++ b/frontend/src/app/map/map.component.spec.ts @@ -1,40 +1,38 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { MapComponent } from './map.component'; -import { SiteModel } from '../models/site.model'; +import { BrapiLocation } from '../models/brapi.model'; describe('MapComponent', () => { let component: MapComponent; let fixture: ComponentFixture<MapComponent>; - const site: SiteModel = { - result: { - locationDbId: 1, - latitude: 1, - longitude: 1, - altitude: 1, - institutionName: '', - institutionAdress: '', - countryName: '', - countryCode: '', - locationType: '', - abbreviation: '', - name: 'site1', - additionalInfo: { - Topography: '', - Slope: '', - Comment: '', - Exposure: '', - 'Coordinates precision': '', - 'Direction from city': '', - 'Distance to city': '', - 'Environment type': '', - 'Geographical location': '', - 'Site status': '' - } + const location: BrapiLocation = { + locationDbId: '1', + latitude: 1, + longitude: 1, + altitude: 1, + institutionName: '', + institutionAddress: '', + countryName: '', + countryCode: '', + locationType: '', + abbreviation: '', + locationName: 'site1', + additionalInfo: { + Topography: '', + Slope: '', + Comment: '', + Exposure: '', + 'Coordinates precision': '', + 'Direction from city': '', + 'Distance to city': '', + 'Environment type': '', + 'Geographical location': '', + 'Site status': '' } }; - const sites: Array<SiteModel> = new Array<SiteModel>(); - sites.push(site); + const locations: BrapiLocation[] = []; + locations.push(location); beforeEach(async(() => { TestBed.configureTestingModule({ @@ -49,20 +47,20 @@ describe('MapComponent', () => { }); it('should create component', () => { - component.sites = sites; + component.locations = locations; fixture.detectChanges(); expect(component).toBeTruthy(); }); it('should display map', () => { - component.sites = sites; + component.locations = locations; fixture.detectChanges(); const element = fixture.nativeElement; expect(element.querySelector('#map')).toBeTruthy(); }); it('should display map legend', () => { - component.sites = sites; + component.locations = locations; fixture.detectChanges(); const element = fixture.nativeElement; expect(element.querySelector('#maplegend')).toBeTruthy(); diff --git a/frontend/src/app/map/map.component.ts b/frontend/src/app/map/map.component.ts index f80a54253b652f9663a5d3263ef47bbf78e6b8be..23d72dcbf0717295bb101778057db91eccb82c0f 100644 --- a/frontend/src/app/map/map.component.ts +++ b/frontend/src/app/map/map.component.ts @@ -1,7 +1,7 @@ import { Component, Input, OnInit } from '@angular/core'; -import { SiteModel } from '../models/site.model'; import * as L from 'leaflet'; import { MarkerClusterGroup } from 'leaflet.markercluster/src'; +import { BrapiLocation } from '../models/brapi.model'; @Component({ selector: 'gpds-map', @@ -10,32 +10,32 @@ import { MarkerClusterGroup } from 'leaflet.markercluster/src'; }) export class MapComponent implements OnInit { - @Input() sites: Array<SiteModel>; + @Input() locations: BrapiLocation[]; constructor() { } ngOnInit() { // initialize map centered on the first site - const firstSite: SiteModel = this.sites[0]; + const firstLocation: BrapiLocation = this.locations[0]; const container = L.DomUtil.get('map'); if (container) { - const map = L.map('map').setView([firstSite.result.latitude, firstSite.result.longitude], 5); + const map = L.map('map').setView([firstLocation.latitude, firstLocation.longitude], 5); 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); - // add markers for all sites using markercluster plugin + // add markers for all locations using markercluster plugin const markers = new MarkerClusterGroup(); - for (const site of this.sites) { + for (const site of this.locations) { const icon = L.icon({ iconUrl: this.getMarkerIconUrl(site) }); - let iconText: string = '<b>' + site.result.name + '</b><br/>'; - iconText += site.result.locationType + '<br/>'; - iconText += `<a href="sites/${site.result.locationDbId}">Details</a>`; + let iconText: string = '<b>' + site.locationName + '</b><br/>'; + iconText += site.locationType + '<br/>'; + iconText += `<a href="sites/${site.locationDbId}">Details</a>`; markers.addLayer(L.marker( - [site.result.latitude, site.result.longitude], + [site.latitude, site.longitude], { icon: icon } ).bindPopup(iconText) ); @@ -48,17 +48,16 @@ export class MapComponent implements OnInit { } } - getMarkerIconUrl(site: SiteModel): string { - if (site.result.locationType === 'Origin site') { + getMarkerIconUrl(site: BrapiLocation): string { + if (site.locationType === 'Origin site') { return 'assets/gpds/images/marker-icon-red.png'; } - if (site.result.locationType === 'Collecting site') { + if (site.locationType === 'Collecting site') { return 'assets/gpds/images/marker-icon-blue.png'; } - if (site.result.locationType === 'Evaluation site') { + if (site.locationType === 'Evaluation site') { return 'assets/gpds/images/marker-icon-green.png'; } return 'assets/gpds/images/marker-icon-purple.png'; } - } diff --git a/frontend/src/app/model/brapi.model.ts b/frontend/src/app/model/brapi.model.ts deleted file mode 100644 index 05c06088347023c948680538f86ec50dda6b1547..0000000000000000000000000000000000000000 --- a/frontend/src/app/model/brapi.model.ts +++ /dev/null @@ -1,20 +0,0 @@ - -interface BrapiData<T> { - data: T[]; -} - -export interface BrapiResults<T> { - metadata: BrapiMetaData; - result: BrapiData<T>; -} - -export interface BrapiMetaData { - - pagination: { - pageSize: number; - currentPage: number; - totalCount: number; - totalPages: number; - }; - -} diff --git a/frontend/src/app/models/brapi.model.ts b/frontend/src/app/models/brapi.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..4360105b11800d1bcf052540a0526e112bd3ec75 --- /dev/null +++ b/frontend/src/app/models/brapi.model.ts @@ -0,0 +1,121 @@ +export interface BrapiMetaData { + pagination: { + pageSize: number; + currentPage: number; + totalCount: number; + totalPages: number; + }; +} + + +export interface BrapiResult<T> { + metadata: BrapiMetaData; + result: T; +} + + +interface BrapiData<T> { + data: T[]; +} + + +export interface BrapiResults<T> extends BrapiResult<BrapiData<T>> { +} + + +interface BrapiHasDocumentationURL { + documentationURL?: string; +} + + +export interface BrapiStudy extends BrapiHasDocumentationURL { + studyDbId: string; + studyType: string; + studyName: string; + studyDescription: string; + seasons: string[]; + startDate: string; + endDate: string; + active: boolean; + programDbId: string; + programName: string; + trialDbIds: string[]; + location: BrapiLocation; + contacts: BrapiContacts[]; + additionalInfo: AdditionalInfo; + dataLinks: { + name: string; + type: string; + url: string; + }[]; +} + + +export interface BrapiLocation extends BrapiHasDocumentationURL { + locationDbId: string; + locationName: string; + locationType: string; + abbreviation: string; + countryCode: string; + countryName: string; + institutionAddress: string; + institutionName: string; + altitude: number; + latitude: number; + longitude: number; + additionalInfo?: AdditionalInfo; +} + + +export interface AdditionalInfo { + [key: string]: string; +} + + +export interface BrapiContacts { + contactDbId: string; + name: string; + email: string; + type: string; + institutionName: string; +} + + +export interface BrapiObservationVariable extends BrapiHasDocumentationURL { + observationVariableDbId: string; + contextOfUse: string[]; + institution: string; + crop: string; + name: string; + ontologyDbId: string; + ontologyName: string; + synonyms: string[]; + language: string; + trait: { + traitDbId: string; + name: string; + description: string; + }; +} + + +export interface BrapiGermplasm extends BrapiHasDocumentationURL { + germplasmDbId: string; + accessionNumber: string; + germplasmName: string; + genus: string; + species: string; + subtaxa: string; +} + + +export interface BrapiTrial extends BrapiHasDocumentationURL { + trialDbId: string; + trialName: string; + trialType: string; + active: boolean; + studies: { + studyDbId: string; + studyName: string; + }[]; +} diff --git a/frontend/src/app/model/data-discovery.model.ts b/frontend/src/app/models/data-discovery.model.ts similarity index 100% rename from frontend/src/app/model/data-discovery.model.ts rename to frontend/src/app/models/data-discovery.model.ts diff --git a/frontend/src/app/models/site.model.ts b/frontend/src/app/models/site.model.ts deleted file mode 100644 index 4d85ae51fe216f9dd969eb6cbbc72b5787ec9bb6..0000000000000000000000000000000000000000 --- a/frontend/src/app/models/site.model.ts +++ /dev/null @@ -1,22 +0,0 @@ -export interface SiteModel { - result: SiteResultModel; -} - -export interface SiteResultModel { - locationDbId: number; - name: string; - locationType?: string; - abbreviation?: string; - countryCode?: string; - countryName?: string; - institutionAdress?: string; - institutionName?: string; - altitude?: number; - latitude?: number; - longitude?: number; - additionalInfo?: AdditionalInfo; -} - -export interface AdditionalInfo { - [key: string]: string; -} diff --git a/frontend/src/app/result-page/document/document.component.spec.ts b/frontend/src/app/result-page/document/document.component.spec.ts index d0c4d19f56a2cbeb734b67a2cd94fddae9b5d3de..05928ff743a8f770521bd591f05cb7f1846c2e61 100644 --- a/frontend/src/app/result-page/document/document.component.spec.ts +++ b/frontend/src/app/result-page/document/document.component.spec.ts @@ -2,7 +2,7 @@ import { TestBed } from '@angular/core/testing'; import { DocumentComponent } from './document.component'; import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { DataDiscoverySource } from '../../model/data-discovery.model'; +import { DataDiscoverySource } from '../../models/data-discovery.model'; import { ComponentTester, speculoosMatchers } from 'ngx-speculoos'; describe('DocumentComponent', () => { diff --git a/frontend/src/app/result-page/document/document.component.ts b/frontend/src/app/result-page/document/document.component.ts index 22dd1f45b525424ff432899cf3bf70d7b3d56983..749101e45a1d04a31bf9828bfb7ab63bd0a9939a 100644 --- a/frontend/src/app/result-page/document/document.component.ts +++ b/frontend/src/app/result-page/document/document.component.ts @@ -1,5 +1,5 @@ import { Component, Input, OnInit } from '@angular/core'; -import { DataDiscoveryDocument, DataDiscoveryType } from '../../model/data-discovery.model'; +import { DataDiscoveryDocument, DataDiscoveryType } from '../../models/data-discovery.model'; @Component({ selector: 'gpds-document', diff --git a/frontend/src/app/result-page/facets/facets.component.spec.ts b/frontend/src/app/result-page/facets/facets.component.spec.ts index 1408a559b269a376c43cb4657737b3a9821703c9..b00fa981b5676c9a48bb15705c682a9cfeca32c4 100644 --- a/frontend/src/app/result-page/facets/facets.component.spec.ts +++ b/frontend/src/app/result-page/facets/facets.component.spec.ts @@ -1,9 +1,9 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TestBed } from '@angular/core/testing'; import { FacetsComponent } from './facets.component'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { ReactiveFormsModule } from '@angular/forms'; import { ComponentTester, speculoosMatchers } from 'ngx-speculoos'; -import { DataDiscoveryCriteria, DataDiscoveryCriteriaUtils, DataDiscoveryFacet } from '../../model/data-discovery.model'; +import { DataDiscoveryCriteria, DataDiscoveryCriteriaUtils, DataDiscoveryFacet } from '../../models/data-discovery.model'; import { BehaviorSubject } from 'rxjs'; import { take } from 'rxjs/operators'; diff --git a/frontend/src/app/result-page/facets/facets.component.ts b/frontend/src/app/result-page/facets/facets.component.ts index 4388717911f682708d5c91159a3f9a736ad000d0..4aa06655470ddb67e8ad5d3f3fe4cdcc022880dc 100644 --- a/frontend/src/app/result-page/facets/facets.component.ts +++ b/frontend/src/app/result-page/facets/facets.component.ts @@ -1,5 +1,5 @@ import { Component, Input, OnInit } from '@angular/core'; -import { DataDiscoveryCriteria, DataDiscoveryFacet } from '../../model/data-discovery.model'; +import { DataDiscoveryCriteria, DataDiscoveryFacet } from '../../models/data-discovery.model'; import { FormControl, FormGroup } from '@angular/forms'; import { BehaviorSubject } from 'rxjs'; import { filter } from 'rxjs/operators'; diff --git a/frontend/src/app/result-page/result-page.component.spec.ts b/frontend/src/app/result-page/result-page.component.spec.ts index 0fba21f87f14a7c34e7af87adeb8ee4be7e95ff8..145bb866c1bc67992ce1fee493b1e3a948cacd1a 100644 --- a/frontend/src/app/result-page/result-page.component.spec.ts +++ b/frontend/src/app/result-page/result-page.component.spec.ts @@ -12,9 +12,9 @@ import { DataDiscoveryCriteriaUtils, DataDiscoveryDocument, DataDiscoverySource -} from '../model/data-discovery.model'; +} from '../models/data-discovery.model'; import { GnpisService } from '../gnpis.service'; -import { BrapiResults } from '../model/brapi.model'; +import { BrapiResults } from '../models/brapi.model'; class ResultPageComponentTester extends ComponentTester<ResultPageComponent> { diff --git a/frontend/src/app/result-page/result-page.component.ts b/frontend/src/app/result-page/result-page.component.ts index e9062499bb1c905d3880cf7f8da19d5a28c27820..f8422b8fa546e85d850887fe44902fef7a706fdb 100644 --- a/frontend/src/app/result-page/result-page.component.ts +++ b/frontend/src/app/result-page/result-page.component.ts @@ -7,7 +7,7 @@ import { DataDiscoveryFacet, DEFAULT_PAGE_SIZE, MAX_RESULTS -} from '../model/data-discovery.model'; +} from '../models/data-discovery.model'; import { BehaviorSubject } from 'rxjs'; import { GnpisService } from '../gnpis.service'; import { filter } from 'rxjs/operators'; diff --git a/frontend/src/app/site-card/site-card.component.html b/frontend/src/app/site-card/site-card.component.html index a635a1338a58c0fb33c6a7b8b94f2459050e01d3..f30867c67e9e182427c2daa7f74b6d78dfbfc436 100644 --- a/frontend/src/app/site-card/site-card.component.html +++ b/frontend/src/app/site-card/site-card.component.html @@ -1,75 +1,78 @@ -<div id="error" class="alert alert-danger" *ngIf="loadingError"> - Error: this site cannot be loaded. - <br/> - Please contact urgi-support@inra.fr. -</div> -<div *ngIf="site"> - <h1> - Site: {{ site.result.name }} - </h1> -</div> -<gpds-map [sites]="sites" *ngIf="site && site.result.latitude && site.result.longitude"></gpds-map> -<div *ngIf="site"> - <br/> - <h4>Details:</h4> - <br/> - <div class="container-fluid"> - <div class="row"> - <table class="table float-right w-75 p-2 table-sm"> - <tr *ngIf="site.result.locationType"> - <td><b>Site type</b></td> - <td>{{ site.result.locationType }}</td> - </tr> - <tr *ngIf="site.result.abbreviation"> - <td><b>Abbreviation</b></td> - <td>{{ site.result.abbreviation }}</td> - </tr> - <tr *ngIf="site.result.countryCode"> - <td><b>Country code</b></td> - <td>{{ site.result.countryCode }}</td> - </tr> - <tr *ngIf="site.result.countryName && !site.result.additionalInfo['Geographical location']"> - <td><b>Country name</b></td> - <td>{{ site.result.countryName }}</td> - </tr> - <tr *ngIf="site.result.institutionAdress"> - <td><b>Institution address</b></td> - <td>{{ site.result.institutionAdress }}</td> - </tr> - <tr *ngIf="site.result.institutionName"> - <td><b>Institution name</b></td> - <td>{{ site.result.institutionName }}</td> - </tr> - <tr *ngIf="site.result.altitude"> - <td><b>Altitude</b></td> - <td>{{ site.result.altitude }}</td> - </tr> - <tr *ngIf="site.result.latitude"> - <td><b>Latitude</b></td> - <td>{{ site.result.latitude }}</td> - </tr> - <tr *ngIf="site.result.longitude"> - <td><b>Longitude</b></td> - <td>{{ site.result.longitude }}</td> - </tr> - </table> - </div> - </div> - <div *ngIf="site.result.additionalInfo"> - <br/> - <h4>Additional informations:</h4> - <br/> - <div class="container-fluid"> - <div class="row"> - <table class="table table-sm table-striped"> - <tbody> - <tr *ngFor="let key of additionalInfoKeys"> - <td><b>{{ key }}</b></td> - <td>{{ site.result.additionalInfo[key] }}</td> - </tr> - </tbody> - </table> +<gpds-loading-spinner [loading]="loading"></gpds-loading-spinner> + +<ng-container *ngIf="location"> + <h3> + Site: {{ location.locationName }} + </h3> + + <gpds-map [locations]="[location]" *ngIf="location && location.latitude && location.longitude"></gpds-map> + + <gpds-card-section + header="Details"> + <ng-template> + <div class="card-body"> + <gpds-card-row + label="Site type" + [value]="location.locationType"> + </gpds-card-row> + + <gpds-card-row + label="Abbreviation" + [value]="location.abbreviation"> + </gpds-card-row> + + <gpds-card-row + label="Country code" + [value]="location.countryCode"> + </gpds-card-row> + + <gpds-card-row + label="Country name" + [test]="location.countryName && !location.additionalInfo['Geographical location']" + [value]="location.countryName"> + </gpds-card-row> + + <gpds-card-row + label="Institution address" + [value]="location.institutionAddress"> + </gpds-card-row> + + <gpds-card-row + label="Institution name" + [value]="location.institutionAddress"> + </gpds-card-row> + + <gpds-card-row + label="Altitude" + [value]="location.altitude"> + </gpds-card-row> + + <gpds-card-row + label="Latitude" + [value]="location.latitude"> + </gpds-card-row> + + <gpds-card-row + label="Longitude" + [value]="location.longitude"> + </gpds-card-row> </div> - </div> - </div> -</div> + </ng-template> + </gpds-card-section> + + <gpds-card-section + header="Additional information" + [test]="additionalInfos && additionalInfos.length != 0"> + <ng-template> + <gpds-card-table + [rows]="additionalInfos"> + <ng-template let-row> + <tr> + <td width="50%">{{ row.key }}</td> + <td>{{ row.value }}</td> + </tr> + </ng-template> + </gpds-card-table> + </ng-template> + </gpds-card-section> +</ng-container> diff --git a/frontend/src/app/site-card/site-card.component.scss b/frontend/src/app/site-card/site-card.component.scss index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..55181845b07a912a715c27dd619fd2bb24d19400 100644 --- a/frontend/src/app/site-card/site-card.component.scss +++ b/frontend/src/app/site-card/site-card.component.scss @@ -0,0 +1,9 @@ + +h3 { + font-weight: bold; + color: #0f6191; +} + +a { + text-decoration: underline; +} diff --git a/frontend/src/app/site-card/site-card.component.spec.ts b/frontend/src/app/site-card/site-card.component.spec.ts index 931f97b313267084e98d5d4d04f632b8eeffce26..322fb4b94eb7677e700782b6886e57effa33e007 100644 --- a/frontend/src/app/site-card/site-card.component.spec.ts +++ b/frontend/src/app/site-card/site-card.component.spec.ts @@ -3,27 +3,32 @@ import { async, TestBed } from '@angular/core/testing'; import { SiteCardComponent } from './site-card.component'; import { MapComponent } from '../map/map.component'; import { BrapiService } from '../brapi.service'; -import { SiteModel } from '../models/site.model'; import { ActivatedRoute, convertToParamMap } from '@angular/router'; import { of } from 'rxjs'; +import { BrapiLocation, BrapiResult } from '../models/brapi.model'; +import { CardRowComponent } from '../card-row/card-row.component'; +import { LoadingSpinnerComponent } from '../loading-spinner/loading-spinner.component'; +import { CardTableComponent } from '../card-table/card-table.component'; +import { CardSectionComponent } from '../card-section/card-section.component'; describe('SiteCardComponent', () => { const brapiService = jasmine.createSpyObj( 'BrapiService', ['location'] ); - const site: SiteModel = { + const response: BrapiResult<BrapiLocation> = { + metadata: null, result: { - locationDbId: 1, + locationDbId: '1', latitude: 1, longitude: 1, altitude: 1, institutionName: '', - institutionAdress: '', + institutionAddress: '', countryName: '', countryCode: '', locationType: '', abbreviation: '', - name: 'site1', + locationName: 'site1', additionalInfo: { Topography: '', Slope: '', @@ -41,15 +46,16 @@ describe('SiteCardComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [SiteCardComponent, MapComponent], + declarations: [ + SiteCardComponent, MapComponent, LoadingSpinnerComponent, + CardRowComponent, CardSectionComponent, CardTableComponent + ], providers: [ { provide: BrapiService, useValue: brapiService }, { provide: ActivatedRoute, useValue: { - snapshot: { - paramMap: convertToParamMap( { id: 1 } ) - } + paramMap: of(convertToParamMap({ id: 1 })) } } ] @@ -65,19 +71,9 @@ describe('SiteCardComponent', () => { it('should display site', () => { const fixture = TestBed.createComponent(SiteCardComponent); const component = fixture.componentInstance; - brapiService.location.and.returnValues(of(site)); + brapiService.location.and.returnValues(of(response)); fixture.detectChanges(); const element = fixture.nativeElement; - expect(element.querySelector('h1').textContent).toBe(' Site: site1 '); - }); - - it('should display error message when site loading is in error', () => { - const fixture = TestBed.createComponent(SiteCardComponent); - const component = fixture.componentInstance; - brapiService.location.and.returnValues(of(site)); - component.loadingError = true; - fixture.detectChanges(); - const element = fixture.nativeElement; - expect(element.querySelector('#error')).toBeTruthy(); + expect(element.querySelector('h3').textContent).toBe(' Site: site1 '); }); }); diff --git a/frontend/src/app/site-card/site-card.component.ts b/frontend/src/app/site-card/site-card.component.ts index acb9d79959a29fcf215c4288c4a2944386fa85f3..c92353088b7450ebf51fc75f54ae7b16e870959f 100644 --- a/frontend/src/app/site-card/site-card.component.ts +++ b/frontend/src/app/site-card/site-card.component.ts @@ -1,7 +1,8 @@ import { Component, OnInit } from '@angular/core'; import { BrapiService } from '../brapi.service'; import { ActivatedRoute } from '@angular/router'; -import { SiteModel } from '../models/site.model'; +import { BrapiLocation } from '../models/brapi.model'; +import { KeyValueObject } from '../utils'; @Component({ selector: 'gpds-site-card', @@ -10,28 +11,30 @@ import { SiteModel } from '../models/site.model'; }) export class SiteCardComponent implements OnInit { - site: SiteModel; - - sites: Array<SiteModel> = new Array<SiteModel>(); - - additionalInfoKeys: string[] = []; - - loadingError = false; + location: BrapiLocation; + additionalInfos: KeyValueObject[]; + loading = true; constructor(private brapiService: BrapiService, private route: ActivatedRoute) { } ngOnInit() { - // initialize site from location index - const locationId = +this.route.snapshot.paramMap.get('id'); - this.brapiService.location(locationId).subscribe( - site => { - this.site = site; this.sites.push(site); - if (site.result.additionalInfo) { - this.additionalInfoKeys = Object.keys(site.result.additionalInfo); - } - }, - () => { console.log('Unable to load site...'); this.loadingError = true; } - ); + this.route.paramMap.subscribe(paramMap => { + // initialize site from location ID + const locationId = paramMap.get('id'); + + this.brapiService.location(locationId).subscribe( + response => { + this.location = response.result; + + this.additionalInfos = []; + if (this.location.additionalInfo) { + this.additionalInfos = KeyValueObject.fromObject(this.location.additionalInfo); + } + this.loading = false; + } + ); + }); + } } diff --git a/frontend/src/app/study-card/study-card.component.html b/frontend/src/app/study-card/study-card.component.html index 4012bc02ad58a62835b40d95844664c9a8c44baa..e85da88328d5d9bcf220f0a13c78f7cf27198efc 100644 --- a/frontend/src/app/study-card/study-card.component.html +++ b/frontend/src/app/study-card/study-card.component.html @@ -1,59 +1,258 @@ -<h3> - Study: {{ study.result.name }} -</h3> - - -<div class="row"> - <table class="table table-sm"> - <!--<tr> - <td><b>Data source</b></td> - <td>{{ }}</td> - </tr>--> - <tr> - <td><b>Name</b></td> - <td>{{ study.result.name }}</td> - </tr> - <tr> - <td><b>Program name</b></td> - <td>{{ study.result.programName }}</td> - </tr> - <tr> - <td><b>Description</b></td> - <td>{{ study.result.studyDescription }}</td> - </tr> - <tr> - <td><b>Permanent Unique Identifier</b></td> - <td></td> - </tr> - <tr> - <td><b>Active</b></td> - <td>{{ study.result.active }}</td> - </tr> - <tr> - <td><b>Seasons</b></td> - <td *ngFor="let season of study.result.seasons"><div>{{ season }}</div></td> - </tr> - <tr *ngIf="study.result.startDate != null"> - <td><b>Date</b></td> - <td *ngIf="study.result.endDate != null; else withoutEnd">From {{ study.result.startDate }} to {{ study.result.endDate }}</td> - <ng-template><td>{{ study.result.startDate }}</td></ng-template> - </tr> - <tr> - <td><b>Location name</b></td> - <td>{{ study.result.locationName }}</td> - </tr> - <tr> - <td><b>Source</b></td> - <td>{{ study.result.dataLinks.name }} {{ study.result.dataLinks.documentationURL }}</td> - </tr> - <!-- <ng-container *ngFor="let info of objectKeys(study.result.additionalInfo)"> - <tr> - <td><b>{{ info }}</b></td><td>{{ study.result.additionalInfo[info] }}</td> - </tr> - </ng-container>--> - <!--<tr> - <td>Additional information</td> - <td>{{ study.result.additionalInfo }}</td> - </tr>--> - </table> -</div> +<gpds-loading-spinner [loading]="loading"></gpds-loading-spinner> + +<ng-container *ngIf="study"> + <h3> + Study {{ study.studyType }}: {{ study.studyName }} + </h3> + + <!-- Display the map --> + <gpds-map *ngIf="checkLocation(study.location)" [locations]="[study.location]"></gpds-map> + + <!-- Display the study's info --> + <gpds-card-section + header="Identification"> + <ng-template> + <div class="card-body"> + + <gpds-card-row + label="Name" + [value]="study.studyName"> + </gpds-card-row> + + <gpds-card-row + label="Identifier" + [test]="study.studyDbId"> + <ng-template> + {{ study.studyDbId }} + <a target="_blank" *ngIf="studySource && study.documentationURL" [href]="study.documentationURL"> + ( Link to this study on {{ studySource["schema:identifier"] }} ) + </a> + </ng-template> + </gpds-card-row> + + <gpds-card-row + label="Permanent unique identifier" + [test]="study['@id'] && isNotURN(study['@id'])" + [value]="study['@id']"> + </gpds-card-row> + + <gpds-card-row + label="Source" + [test]="studySource"> + <ng-template> + <a target="_blank" + [href]="studySource['schema:url']"> + <img [src]="studySource['schema:image']" alt="Study source image"/> + </a> + </ng-template> + </gpds-card-row> + + <gpds-card-row + label="Data source" + [test]="studySource && study.documentationURL"> + <ng-template> + <a target="_blank" *ngIf="study.documentationURL" [href]="study.documentationURL"> + ( Link to this study on {{ studySource["schema:identifier"] }} ) + </a> + </ng-template> + </gpds-card-row> + + <gpds-card-row + label="Project name" + [value]="study.programName"> + </gpds-card-row> + + <gpds-card-row + label="Description" + [value]="study.studyDescription"> + </gpds-card-row> + + <gpds-card-row + label="Active" + [test]="study.active != null"> + <ng-template> + {{ study.active ? "Yes" : "No" }} + </ng-template> + </gpds-card-row> + + <gpds-card-row + label="Seasons" + [test]="study.seasons && study.seasons.length != 0"> + <ng-template> + {{ study.seasons.join(', ') }} + </ng-template> + </gpds-card-row> + + <gpds-card-row + label="Date" + [test]="study.startDate"> + <ng-template> + {{ study.endDate ? + 'From ' + study.startDate + ' to ' + study.endDate : + 'Started on ' + study.startDate }} + </ng-template> + </gpds-card-row> + + <gpds-card-row + label="Location name" + [test]="study.location && study.location.locationDbId"> + <ng-template> + <a [routerLink]="['/sites', study.location.locationDbId]"> + {{ study.location.locationName }} + </a> + </ng-template> + </gpds-card-row> + + <gpds-card-row + label="Data files" + [test]="study.dataLinks && study.dataLinks.length != 0"> + <ng-template> + <div *ngFor="let dataLink of study.dataLinks"> + <a [href]="dataLink.url"> + {{ dataLink.name }} + </a> + </div> + </ng-template> + </gpds-card-row> + </div> + + </ng-template> + </gpds-card-section> + + + <gpds-card-section + header="Genotype" + [test]="studyGermplasms && studyGermplasms.length != 0"> + <ng-template> + <gpds-card-table + [headers]="[ + 'Accession number', + 'Name', + 'Taxon' + ]" + [rows]="studyGermplasms"> + <ng-template let-row> + <tr> + <td> + <a [routerLink]="['/germplasm', row.germplasmDbId]"> + {{ row.accessionNumber }} + </a> + </td> + <td>{{ row.germplasmName }}</td> + <td>{{ row.genus }} {{ row.species }} {{ row.subtaxa }}</td> + </tr> + </ng-template> + </gpds-card-table> + </ng-template> + </gpds-card-section> + + + <gpds-card-section + header="Variables" + [test]="studyObservationVariables && studyObservationVariables.length != 0"> + <ng-template> + <gpds-card-table + [headers]="[ + 'Variable id', + 'Variable short name', + 'Variable long name', + 'Ontology name', + 'Trait description' + ]" + [rows]="studyObservationVariables"> + <ng-template let-row> + <tr> + <td> + <ng-template #name>{{ row.observationVariableDbId }}</ng-template> + <ng-template #link> + <a [href]=row.documentationURL>{{ row.observationVariableDbId }}</a> + </ng-template> + <ng-container *ngIf="row.documentationURL; then link else name"></ng-container> + </td> + <td>{{ row.name }}</td> + <td>{{ row.synonyms[0] }}</td> + <td>{{ row.ontologyName }}</td> + <td>{{ row.trait.description }}</td> + </tr> + </ng-template> + </gpds-card-table> + </ng-template> + </gpds-card-section> + + <gpds-card-section + header="Data Set" + [test]="studyDataset && studyDataset.length != 0"> + <ng-template> + <gpds-card-table + [headers]="[ + 'Name', + 'Type', + 'Linked studies identifiers' + ]" + [rows]="studyDataset"> + <ng-template let-row> + <tr> + <td> + <ng-template #name>{{ row.trialName }}</ng-template> + <ng-template #link> + <a [href]=row.documentationURL>{{ row.trialName }}</a> + </ng-template> + <ng-container *ngIf="row.documentationURL; then link else name"></ng-container> + </td> + <td>{{ row.trialType }}</td> + <td width="60%"> + <ng-container *ngFor="let trialStudy of row.studies"> + <a + [routerLink]="['/studies', trialStudy.studyDbId]"> + {{ trialStudy.studyName.trim() }} + </a>; + </ng-container> + </td> + </tr> + </ng-template> + </gpds-card-table> + </ng-template> + </gpds-card-section> + + <gpds-card-section + header="Contact" + [test]="study.contacts && study.contacts.length != 0"> + <ng-template> + <gpds-card-table + [headers]="[ + 'Role', + 'Name', + 'Email', + 'Institution' + ]" + [rows]="study.contacts"> + <ng-template let-row> + <tr> + <td>{{ row.type }}</td> + <td>{{ row.name }}</td> + <td>{{ row.email }}</td> + <td>{{ row.institutionName }}</td> + </tr> + </ng-template> + </gpds-card-table> + </ng-template> + </gpds-card-section> + + <gpds-card-section + header="Additional information" + [test]="additionalInfos && additionalInfos.length != 0"> + <ng-template> + <gpds-card-table + [rows]="additionalInfos"> + <ng-template let-row> + <tr> + <td width="50%">{{ row.key }}</td> + <td>{{ row.value }}</td> + </tr> + </ng-template> + </gpds-card-table> + </ng-template> + </gpds-card-section> + +</ng-container> + diff --git a/frontend/src/app/study-card/study-card.component.scss b/frontend/src/app/study-card/study-card.component.scss index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..55181845b07a912a715c27dd619fd2bb24d19400 100644 --- a/frontend/src/app/study-card/study-card.component.scss +++ b/frontend/src/app/study-card/study-card.component.scss @@ -0,0 +1,9 @@ + +h3 { + font-weight: bold; + color: #0f6191; +} + +a { + text-decoration: underline; +} diff --git a/frontend/src/app/study-card/study-card.component.spec.ts b/frontend/src/app/study-card/study-card.component.spec.ts index f37f7425304ec09a5ba119bd9c81fd76801d4912..e2407e4792107bf857cc42f5c40fa3c3720c924b 100644 --- a/frontend/src/app/study-card/study-card.component.spec.ts +++ b/frontend/src/app/study-card/study-card.component.spec.ts @@ -1,21 +1,264 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { async, TestBed } from '@angular/core/testing'; import { StudyCardComponent } from './study-card.component'; +import { ComponentTester, fakeRoute, speculoosMatchers } from 'ngx-speculoos'; +import { ActivatedRoute } from '@angular/router'; +import { of } from 'rxjs'; +import { + BrapiContacts, + BrapiGermplasm, + BrapiLocation, + BrapiObservationVariable, + BrapiResult, + BrapiResults, + BrapiStudy, + BrapiTrial +} from '../models/brapi.model'; +import { BrapiService } from '../brapi.service'; +import { GnpisService } from '../gnpis.service'; +import { DataDiscoverySource } from '../models/data-discovery.model'; +import { MapComponent } from '../map/map.component'; +import { RouterTestingModule } from '@angular/router/testing'; +import { CardSectionComponent } from '../card-section/card-section.component'; +import { CardRowComponent } from '../card-row/card-row.component'; +import { LoadingSpinnerComponent } from '../loading-spinner/loading-spinner.component'; +import { CardTableComponent } from '../card-table/card-table.component'; describe('StudyCardComponent', () => { - let component: StudyCardComponent; - let fixture: ComponentFixture<StudyCardComponent>; + beforeEach(() => jasmine.addMatchers(speculoosMatchers)); + + class StudyCardComponentTester extends ComponentTester<StudyCardComponent> { + constructor() { + super(StudyCardComponent); + } + + get title() { + return this.element('h3'); + } + + get cardHeader() { + return this.elements('div.card-header'); + } + + get studyInfo() { + return this.elements('div.col'); + } + + get map() { + return this.element('gpds-map'); + } + } + + const brapiService = jasmine.createSpyObj( + 'BrapiService', [ + 'study', + 'studyTrials', + 'studyObservationVariables', + 'studyGermplasms' + ] + ); + + const gnpisService = jasmine.createSpyObj( + 'GnpisService', ['getSource'] + ); + + const activatedRoute = fakeRoute({ + params: of({ id: 's1' }) + }); + + const location: BrapiLocation = { + locationDbId: '1', + locationName: 'loc1', + locationType: 'Collecting site', + abbreviation: null, + countryCode: 'Fr', + countryName: 'France', + institutionAddress: null, + institutionName: 'Insti', + altitude: null, + latitude: null, + longitude: null, + }; + + const contacts: BrapiContacts = { + contactDbId: 'c1', + name: 'contact1', + email: 'contact1@email.com', + type: 'contact', + institutionName: 'Inst', + }; + + const searchStudy: BrapiResult<BrapiStudy> = { + metadata: null, + result: { + studyDbId: 's1', + studyType: 'phenotype', + studyName: 'study1', + studyDescription: null, + seasons: ['winter', '2019'], + startDate: '2018', + endDate: null, + active: true, + programDbId: 'p1', + programName: 'program1', + trialDbIds: ['10', '20'], + location: location, + contacts: [contacts], + additionalInfo: null, + documentationURL: 'http://example.com/Study/s1', + dataLinks: [], + 'schema:includedInDataCatalog': 'src1' + } as BrapiStudy + }; + + const trial1: BrapiResult<BrapiTrial> = { + metadata: null, + result: { + trialDbId: '10', + trialName: 'trial_10', + trialType: 'project', + active: true, + studies: [ + { studyDbId: 's1', studyName: 'study1' }, + { studyDbId: 's2', studyName: 'study2' } + ] + + } + }; + const trial2: BrapiResult<BrapiTrial> = { + metadata: null, + result: { + trialDbId: '20', + trialName: 'trial_20', + trialType: 'project', + active: true, + studies: [ + { studyDbId: 's3', studyName: 'study3' }, + { studyDbId: 's4', studyName: 'study4' } + ] + + } + }; + const osbVariable: BrapiResults<BrapiObservationVariable> = { + metadata: null, + result: { + data: [{ + observationVariableDbId: 'var1', + contextOfUse: null, + institution: 'Insti', + crop: 'WoodyPlant', + name: 'varaiable1', + ontologyDbId: 'WPO', + ontologyName: 'Woody Plant Ontology', + synonyms: ['First synonym'], + language: 'EN', + trait: { + traitDbId: 't1', + name: 'trait1', + description: null, + }, + documentationURL: null, + }] + } + }; + + const germplasm: BrapiResults<BrapiGermplasm> = { + metadata: null, + result: { + data: [{ + germplasmDbId: 'g1', + accessionNumber: 'G_10', + germplasmName: 'germplam1', + genus: 'Populus', + species: 'x generosa', + subtaxa: '' + }, { + germplasmDbId: 'g2', + accessionNumber: 'G_20', + germplasmName: 'germplam2', + genus: 'Triticum', + species: 'aestivum', + subtaxa: 'subsp' + }], + } + }; + + const source: DataDiscoverySource = { + '@id': 'src1', + '@type': ['schema:DataCatalog'], + 'schema:identifier': 'srcId', + 'schema:name': 'source1', + 'schema:url': 'srcUrl', + 'schema:image': null + }; + + brapiService.study.and.returnValue(of(searchStudy)); + brapiService.studyTrials.withArgs('10').and.returnValue(of(trial1)); + brapiService.studyTrials.withArgs('20').and.returnValue(of(trial2)); + brapiService.studyObservationVariables.and.returnValue(of(osbVariable)); + brapiService.studyGermplasms.and.returnValue(of(germplasm)); + gnpisService.getSource.and.returnValue(of(source)); beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [StudyCardComponent] - }) - .compileComponents(); + imports: [RouterTestingModule], + declarations: [ + StudyCardComponent, MapComponent, CardSectionComponent, + CardRowComponent, LoadingSpinnerComponent, CardTableComponent + ], + providers: [ + { provide: ActivatedRoute, useValue: activatedRoute }, + { provide: BrapiService, useValue: brapiService }, + { provide: GnpisService, useValue: gnpisService } + ] + }); })); - beforeEach(() => { - fixture = TestBed.createComponent(StudyCardComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); + + it('should fetch the study data information but not display map', async(() => { + const tester = new StudyCardComponentTester(); + const component = tester.componentInstance; + tester.detectChanges(); + + component.loaded.then(() => { + expect(component.study).toBeTruthy(); + component.study.location.longitude = null; + component.study.location.latitude = null; + tester.detectChanges(); + + expect(tester.map).toBeFalsy(); + expect(tester.title).toContainText('Study phenotype: study1'); + + expect(tester.cardHeader[0]).toContainText('Identification'); + + expect(tester.studyInfo[1]).toContainText('Link to this study on srcId'); + + expect(tester.cardHeader[1]).toContainText('Genotype'); + expect(component.studyGermplasms.length).toEqual(2); + + expect(tester.cardHeader[2]).toContainText('Variable'); + expect(component.studyObservationVariables.length).toEqual(1); + + expect(tester.cardHeader[3]).toContainText(' Data Set '); + expect(component.trialsIds.length).toEqual(2); + expect(component.studyDataset.length).toEqual(2); + + expect(tester.cardHeader[4]).toContainText('Contact'); + }); + })); + + it('should display map', async(() => { + const tester = new StudyCardComponentTester(); + const component = tester.componentInstance; + tester.detectChanges(); + + component.loaded.then(() => { + expect(component.study).toBeTruthy(); + component.study.location.latitude = 48.8534; + component.study.location.longitude = 2.3488; + tester.detectChanges(); + + expect(tester.map).toBeTruthy(); + }); + })); }); diff --git a/frontend/src/app/study-card/study-card.component.ts b/frontend/src/app/study-card/study-card.component.ts index 72341f2625f9ab572a36f00b989bfbf0b56c52a5..a8f729b2bef6861b93a19043912a311248e5b14f 100644 --- a/frontend/src/app/study-card/study-card.component.ts +++ b/frontend/src/app/study-card/study-card.component.ts @@ -1,6 +1,11 @@ import { Component, OnInit } from '@angular/core'; import { BrapiService } from '../brapi.service'; import { ActivatedRoute } from '@angular/router'; +import { BrapiGermplasm, BrapiLocation, BrapiObservationVariable, BrapiStudy, BrapiTrial } from '../models/brapi.model'; + +import { GnpisService } from '../gnpis.service'; +import { DataDiscoverySource } from '../models/data-discovery.model'; +import { KeyValueObject } from '../utils'; @Component({ selector: 'gpds-study-card', @@ -9,27 +14,89 @@ import { ActivatedRoute } from '@angular/router'; }) export class StudyCardComponent implements OnInit { - study: object = {}; - studyGermplasms: string[] = []; - studyObservationVariables: string[] = []; - objectKeys = Object.keys; + study: BrapiStudy; + studySource: DataDiscoverySource; + studyGermplasms: BrapiGermplasm[]; + studyObservationVariables: BrapiObservationVariable[]; + additionalInfos: KeyValueObject[]; + studyDataset: BrapiTrial[]; + trialsIds: string[] = []; + loading: boolean; + loaded: Promise<any>; - constructor(private brapiService: BrapiService, private route: ActivatedRoute) { + constructor(private brapiService: BrapiService, private gnpisService: GnpisService, private route: ActivatedRoute) { } ngOnInit() { - const studyDbId = this.route.snapshot.paramMap.get('id'); - this.brapiService.study(studyDbId) - .subscribe(study => { - this.study = study; - }); - this.brapiService.studyObservationVariables(studyDbId) - .subscribe(studyObsVar => { - this.studyObservationVariables.concat(studyObsVar); - }); - this.brapiService.studyGermplasms(studyDbId) - .subscribe(studyGermplasm => { - this.studyGermplasms.concat(studyGermplasm); + this.route.paramMap.subscribe(paramMap => { + this.loading = true; + const studyDbId = paramMap.get('id'); + + const study$ = this.brapiService.study(studyDbId).toPromise(); + study$ + .then(response => { + this.study = response.result; + + this.additionalInfos = []; + if (this.study.additionalInfo) { + this.additionalInfos = KeyValueObject.fromObject(this.study.additionalInfo); + } + + // Get study trials + this.trialsIds = this.study.trialDbIds; + this.studyDataset = []; + if (this.trialsIds && this.trialsIds !== []) { + for (const trialsId of this.trialsIds) { + this.brapiService.studyTrials(trialsId) + .subscribe(trialResponse => { + const trial = trialResponse.result; + + // Remove current study from trial studies + trial.studies = trial.studies + .filter(study => study.studyDbId !== studyDbId); + this.studyDataset.push(trial); + }); + } + } + + // Get study source + const sourceURI = this.study['schema:includedInDataCatalog']; + if (sourceURI) { + const source$ = this.gnpisService.getSource(sourceURI); + source$ + .subscribe(src => { + this.studySource = src; + }); + } + }); + + this.studyObservationVariables = []; + const variable$ = this.brapiService.studyObservationVariables(studyDbId).toPromise(); + variable$ + .then(response => { + this.studyObservationVariables = response.result.data; + }); + + this.studyGermplasms = []; + const germplasm$ = this.brapiService.studyGermplasms(studyDbId).toPromise(); + germplasm$ + .then(studyGermplasm => { + this.studyGermplasms = studyGermplasm.result.data; + }); + + this.loaded = Promise.all([study$, variable$, germplasm$]); + this.loaded.then(() => { + this.loading = false; }); + }); + + } + + checkLocation(location: BrapiLocation) { + return location && location.longitude && location.latitude; + } + + isNotURN(pui: string) { + return !(pui.substring(0, 3) === 'urn'); } } diff --git a/frontend/src/app/utils.ts b/frontend/src/app/utils.ts index d91e5d9590533fc0c763e622564fff7ffa1c76b7..4ff7410ef9771ae2047cdec6ee695a931ca8a2ad 100644 --- a/frontend/src/app/utils.ts +++ b/frontend/src/app/utils.ts @@ -7,3 +7,20 @@ export function asArray(obj) { } return [obj]; } + +export class KeyValueObject { + public key: string; + public value: string; + + constructor(key: string, value: string) { + this.key = key; + this.value = value; + } + + static fromObject(o: object): KeyValueObject[] { + return Object.entries(o) + .filter(([key, value]) => !!key && !!value) + .map(([key, value]) => new KeyValueObject(key, value)); + } +} + diff --git a/frontend/src/assets/gpds/theme.scss b/frontend/src/assets/gpds/theme.scss index a649ce5ab7e37c471dcc90d1a9c0a0c66c7d4007..168116f3953fb0a269af34063b9f1c647c2a2871 100644 --- a/frontend/src/assets/gpds/theme.scss +++ b/frontend/src/assets/gpds/theme.scss @@ -16,6 +16,7 @@ $enable-shadows: true; @import "~bootstrap/scss/functions"; @import "~bootstrap/scss/variables"; @import "~bootstrap/scss/mixins"; +@import "~bootstrap/scss/tables"; // public custom variables used in this theme, and in component styles diff --git a/frontend/src/tslint.json b/frontend/src/tslint.json index 0c38e753c0333b8b1cb1f74a0aad16f7985e092e..7e545ab7d80894dd3456be1fee601237e43b7bff 100644 --- a/frontend/src/tslint.json +++ b/frontend/src/tslint.json @@ -13,6 +13,9 @@ "gpds", "kebab-case" ], - "template-cyclomatic-complexity": [true, 15] + "template-cyclomatic-complexity": [ + true, + 10 + ] } }