diff --git a/.gitignore b/.gitignore index ee5c9d8336b76b3bb5658d0888bdec86eb7b0990..dca7088f54475232a1e94a0a1127a18183ed3eb3 100644 --- a/.gitignore +++ b/.gitignore @@ -18,11 +18,8 @@ *.sublime-workspace # IDE - VSCode -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json +.vscode + # misc /.sass-cache diff --git a/config/config-dev.json b/config/config-dev.json index adf822d5cb7103ae1e517c14ba05e845c154d0c4..fafda28003d1c1fb0d069a53e406741ae014584b 100644 --- a/config/config-dev.json +++ b/config/config-dev.json @@ -13,5 +13,17 @@ }, "middlewareLegacyAuth": { "url": "https://kong-dev.alpha.grandlyon.com/middleware-legacy/" + }, + "changelog": { + "url": "https://kong-dev.alpha.grandlyon.com/changelog/" + }, + "credits": { + "url": "https://kong-dev.alpha.grandlyon.com/credits/credits/" + }, + "restHeartAggregations": { + "url": "https://kong-dev.alpha.grandlyon.com/indexer-logs/indexerdb/" + }, + "reuses": { + "url": "https://kong-dev.alpha.grandlyon.com/reuses/" } } diff --git a/config/config-rec.json b/config/config-rec.json index 804b6b67d735472bc7d026d357d0ff410c63004f..d1d5b2d87a719ae58ff25c27c0059e4ad87e685a 100644 --- a/config/config-rec.json +++ b/config/config-rec.json @@ -13,5 +13,17 @@ }, "middlewareLegacyAuth": { "url": "https://kong-rec.alpha.grandlyon.com/middleware-legacy/" + }, + "changelog": { + "url": "https://kong-rec.alpha.grandlyon.com/changelog/" + }, + "credits": { + "url": "https://kong-rec.alpha.grandlyon.com/credits/credits/" + }, + "reuses": { + "url": "https://kong-rec.alpha.grandlyon.com/reuses/" + }, + "restHeartAggregations": { + "url": "https://kong-rec.alpha.grandlyon.com/indexer-logs/indexerdb/" } } diff --git a/package-lock.json b/package-lock.json index 0addc5a51aa9f1e45293776e4b13a5ee7e197b37..811cd7cd1efe1b94e51e38bfce35ed1aa5f987a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "admin-gui", - "version": "1.1.0", + "version": "1.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1147,6 +1147,21 @@ } } }, + "@types/highlight.js": { + "version": "9.12.3", + "resolved": "https://registry.npmjs.org/@types/highlight.js/-/highlight.js-9.12.3.tgz", + "integrity": "sha512-pGF/zvYOACZ/gLGWdQH8zSwteQS1epp68yRcVLJMgUck/MjEn/FBYmPub9pXT8C1e4a8YZfHo1CKyV8q1vKUnQ==", + "dev": true + }, + "@types/highlightjs": { + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/@types/highlightjs/-/highlightjs-9.12.0.tgz", + "integrity": "sha512-MmUcjkDtCBfx2BPeLLTtJ5mFmGgWk9nAgZmNesixaGHOr0tCecsTU2iUgYvhRsWJSts2WbcpAtVPuIzZ0ybJ1A==", + "dev": true, + "requires": { + "@types/highlight.js": "*" + } + }, "@types/jasmine": { "version": "2.8.16", "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-2.8.16.tgz", @@ -1575,6 +1590,7 @@ "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "dev": true, + "optional": true, "requires": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" @@ -1661,9 +1677,7 @@ "asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", - "dev": true, - "optional": true + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" }, "asn1": { "version": "0.2.4", @@ -2273,8 +2287,7 @@ "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" }, "buffer-indexof": { "version": "1.1.1", @@ -2310,6 +2323,11 @@ "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.7.5.tgz", "integrity": "sha512-cX98TIn0I6sKba/DhW0FBjtaDpxTelU166pf7ICXpCCuplHWyu6C9LYZmL5PEsnePIeJaiorsTEzzNk3Tsm1hw==" }, + "bulma-switch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bulma-switch/-/bulma-switch-2.0.0.tgz", + "integrity": "sha512-myD38zeUfjmdduq+pXabhJEe3x2hQP48l/OI+Y0fO3HdDynZUY/VJygucvEAJKRjr4HxD5DnEm4yx+oDOBXpAA==" + }, "bytes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", @@ -2430,6 +2448,39 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "chart.js": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.8.0.tgz", + "integrity": "sha512-Di3wUL4BFvqI5FB5K26aQ+hvWh8wnP9A3DWGvXHVkO13D3DSnaSsdZx29cXlEsYKVkn1E2az+ZYFS4t0zi8x0w==", + "requires": { + "chartjs-color": "^2.1.0", + "moment": "^2.10.2" + } + }, + "chartjs-color": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.3.0.tgz", + "integrity": "sha512-hEvVheqczsoHD+fZ+tfPUE+1+RbV6b+eksp2LwAhwRTVXEjCSEavvk+Hg3H6SZfGlPh/UfmWKGIvZbtobOEm3g==", + "requires": { + "chartjs-color-string": "^0.6.0", + "color-convert": "^0.5.3" + }, + "dependencies": { + "color-convert": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz", + "integrity": "sha1-vbbGnOZg+t/+CwAHzER+G59ygr0=" + } + } + }, + "chartjs-color-string": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz", + "integrity": "sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==", + "requires": { + "color-name": "^1.0.0" + } + }, "chokidar": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", @@ -2721,7 +2772,6 @@ "version": "1.6.2", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, "requires": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", @@ -2783,7 +2833,8 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true + "dev": true, + "optional": true }, "constants-browserify": { "version": "1.0.0", @@ -2868,8 +2919,7 @@ "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "cosmiconfig": { "version": "4.0.0", @@ -3179,7 +3229,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true + "dev": true, + "optional": true }, "depd": { "version": "1.1.2", @@ -4266,7 +4317,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -4287,12 +4339,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4307,17 +4361,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -4434,7 +4491,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -4446,6 +4504,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4460,6 +4519,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -4467,12 +4527,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -4491,6 +4553,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -4571,7 +4634,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -4583,6 +4647,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -4668,7 +4733,8 @@ "safe-buffer": { "version": "5.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -4704,6 +4770,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -4723,6 +4790,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -4766,12 +4834,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -4780,6 +4850,7 @@ "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", "dev": true, + "optional": true, "requires": { "graceful-fs": "^4.1.2", "inherits": "~2.0.0", @@ -4792,6 +4863,7 @@ "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "dev": true, + "optional": true, "requires": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", @@ -4829,7 +4901,8 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "dev": true + "dev": true, + "optional": true }, "get-stream": { "version": "3.0.0", @@ -5065,7 +5138,8 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "dev": true + "dev": true, + "optional": true }, "has-value": { "version": "1.0.0", @@ -5119,6 +5193,11 @@ "minimalistic-assert": "^1.0.1" } }, + "highlight.js": { + "version": "9.15.10", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.15.10.tgz", + "integrity": "sha512-RoV7OkQm0T3os3Dd2VHLNMoaoDVx77Wygln3n9l5YV172XonWG6rgQD3XnF/BuFFZw9A0TJgmMSO8FEWQgvcXw==" + }, "hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -5154,6 +5233,23 @@ "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=", "dev": true }, + "http-basic": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/http-basic/-/http-basic-2.5.1.tgz", + "integrity": "sha1-jORHvbW2xXf4pj4/p4BW7Eu02/s=", + "requires": { + "caseless": "~0.11.0", + "concat-stream": "^1.4.6", + "http-response-object": "^1.0.0" + }, + "dependencies": { + "caseless": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", + "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=" + } + } + }, "http-cache-semantics": { "version": "3.8.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz", @@ -5228,6 +5324,11 @@ "micromatch": "^3.1.9" } }, + "http-response-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-1.1.0.tgz", + "integrity": "sha1-p8TnWq6C87tJBOT0P2FWc7TVGMM=" + }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -5317,6 +5418,11 @@ "minimatch": "^3.0.4" } }, + "image-extensions": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/image-extensions/-/image-extensions-1.1.0.tgz", + "integrity": "sha1-uOa/YDnfAFbjM1AqALZjejEF2JQ=" + }, "image-size": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", @@ -5738,6 +5844,24 @@ "is-extglob": "^2.1.1" } }, + "is-image": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-image/-/is-image-1.0.1.tgz", + "integrity": "sha1-b9UadSoaERUG0GDZUhGLC5ibQm4=", + "requires": { + "image-extensions": "^1.0.1" + } + }, + "is-image-url": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/is-image-url/-/is-image-url-1.1.8.tgz", + "integrity": "sha1-qmK/l1fFvlQCJpmcdMOiGtqYuDw=", + "requires": { + "is-image": "^1.0.1", + "is-url": "^1.2.1", + "sync-request": "^2.1.0" + } + }, "is-number": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", @@ -5821,11 +5945,17 @@ "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", "dev": true }, + "is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==" + }, "is-utf8": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", - "dev": true + "dev": true, + "optional": true }, "is-windows": { "version": "1.0.2", @@ -5842,8 +5972,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isbinaryfile": { "version": "3.0.3", @@ -6433,6 +6562,7 @@ "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "dev": true, + "optional": true, "requires": { "graceful-fs": "^4.1.2", "parse-json": "^2.2.0", @@ -6445,7 +6575,8 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true + "dev": true, + "optional": true } } }, @@ -6732,7 +6863,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", - "dev": true + "dev": true, + "optional": true }, "map-visit": { "version": "1.0.0", @@ -7015,6 +7147,11 @@ "minimist": "0.0.8" } }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -7366,6 +7503,7 @@ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "dev": true, + "optional": true, "requires": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", @@ -7573,6 +7711,11 @@ "lcid": "^1.0.0" } }, + "os-shim": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/os-shim/-/os-shim-0.1.3.tgz", + "integrity": "sha1-a2LDeRz3kJ6jXtRuF2WLtBfLORc=" + }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", @@ -8123,15 +8266,12 @@ "process-nextick-args": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" }, "promise": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", - "dev": true, - "optional": true, "requires": { "asap": "~2.0.3" } @@ -8364,8 +8504,7 @@ "qs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" }, "querystring": { "version": "0.2.0", @@ -8473,6 +8612,7 @@ "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", "dev": true, + "optional": true, "requires": { "load-json-file": "^1.0.0", "normalize-package-data": "^2.3.2", @@ -8484,6 +8624,7 @@ "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", "dev": true, + "optional": true, "requires": { "graceful-fs": "^4.1.2", "pify": "^2.0.0", @@ -8494,7 +8635,8 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true + "dev": true, + "optional": true } } }, @@ -8503,6 +8645,7 @@ "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", "dev": true, + "optional": true, "requires": { "find-up": "^1.0.0", "read-pkg": "^1.0.0" @@ -8513,6 +8656,7 @@ "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", "dev": true, + "optional": true, "requires": { "path-exists": "^2.0.0", "pinkie-promise": "^2.0.0" @@ -8523,6 +8667,7 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", "dev": true, + "optional": true, "requires": { "pinkie-promise": "^2.0.0" } @@ -8533,7 +8678,6 @@ "version": "2.3.6", "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -8854,8 +8998,7 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "safe-regex": { "version": "1.1.0", @@ -9523,6 +9666,15 @@ "integrity": "sha512-CYAPYdBu34781kLHkaW3m6b/uUSyMOC2R61gcYMWooeuaGtjof86ZA/8T+qVPPt7np1085CR9hmMGrySwEc8Xg==", "dev": true }, + "spawn-sync": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/spawn-sync/-/spawn-sync-1.0.15.tgz", + "integrity": "sha1-sAeZVX63+wyDdsKdROih6mfldHY=", + "requires": { + "concat-stream": "^1.4.7", + "os-shim": "^0.1.2" + } + }, "spdx-correct": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.2.tgz", @@ -9805,7 +9957,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "requires": { "safe-buffer": "~5.1.0" } @@ -9823,6 +9974,7 @@ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", "dev": true, + "optional": true, "requires": { "is-utf8": "^0.2.0" } @@ -9917,6 +10069,17 @@ "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", "dev": true }, + "sync-request": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/sync-request/-/sync-request-2.2.0.tgz", + "integrity": "sha1-p70sES+glGPrkUnP8OnUKMR5do8=", + "requires": { + "concat-stream": "^1.4.7", + "http-response-object": "^1.0.1", + "spawn-sync": "^1.0.1", + "then-request": "^2.0.1" + } + }, "tapable": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.1.tgz", @@ -10127,6 +10290,26 @@ } } }, + "then-request": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/then-request/-/then-request-2.2.0.tgz", + "integrity": "sha1-ZnizL6DKIY/laZgbvYhxtZQGDYE=", + "requires": { + "caseless": "~0.11.0", + "concat-stream": "^1.4.7", + "http-basic": "^2.5.1", + "http-response-object": "^1.1.0", + "promise": "^7.1.1", + "qs": "^6.1.0" + }, + "dependencies": { + "caseless": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", + "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=" + } + } + }, "through": { "version": "2.3.8", "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -10457,8 +10640,7 @@ "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", - "dev": true + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, "typescript": { "version": "3.2.4", @@ -10661,8 +10843,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "utils-merge": { "version": "1.0.1", @@ -11231,6 +11412,7 @@ "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "dev": true, + "optional": true, "requires": { "string-width": "^1.0.2 || 2" } diff --git a/package.json b/package.json index fae0d245c8f2f151cde8da873bc68ef38ffd1d04..13be3fff3d1aa4d8884b9189d90f6c4015b157b4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "admin-gui", - "version": "1.2.0", + "version": "1.3.0", "scripts": { "ng": "ng", "start": "ng serve", @@ -25,7 +25,11 @@ "@angular/platform-browser-dynamic": "^7.2.4", "@angular/router": "^7.2.4", "bulma": "^0.7.5", + "bulma-switch": "^2.0.0", + "chart.js": "^2.5.0", "core-js": "^2.6.4", + "highlight.js": "^9.15.10", + "is-image-url": "^1.1.8", "node-rsa": "^1.0.5", "rxjs": "^6.4.0", "rxjs-tslint": "^0.1.7", @@ -54,4 +58,4 @@ "tslint-config-airbnb": "^5.11.1", "typescript": "~3.2.4" } -} +} \ No newline at end of file diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 994fd9e1ac95c651f574ab12b569a2e5710c373c..c494026aa4f529dd12701bb7c7a31208dc8e3a94 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -37,7 +37,6 @@ export class AppComponent implements OnInit { } loggout() { - console.log('blabla') this._userService.resetAuth(); this.closeUserDropdown(); this._router.navigate(['/login']); diff --git a/src/app/app.module.ts b/src/app/app.module.ts index e08e4f57d586c603d9d14761b331bb57983f3b25..2cd76bcaf31df57efb6ca59481ff4c73bbcb790d 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -11,6 +11,7 @@ import { AppComponents } from './components'; import { UserModule } from './user/user.module'; import { UserService } from './user/services'; import { AppDirectives } from './directives'; +import { FilterPipe } from './components/logs-dashboard/logs-slugs/filter.pipe'; import { HttpErrorResponseInterceptor } from './interceptors/http-error-response-interceptor'; // Function used by APP_INITIALIZER before the app start: init user info / statut (expect a promise) @@ -35,6 +36,7 @@ export function initAppConfig(appConfigService: AppConfigService) { @NgModule({ declarations: [ AppComponent, + FilterPipe, ...AppComponents, ...AppDirectives, ], @@ -49,6 +51,7 @@ export function initAppConfig(appConfigService: AppConfigService) { UserModule, ], providers: [ + FilterPipe, ...AppServices, { provide: HTTP_INTERCEPTORS, diff --git a/src/app/app.routing.module.ts b/src/app/app.routing.module.ts index f58c4ca7d2b473f77f8e8f9d00f94bf8bc158155..d3c9bdf1a9bc12f4b564434a7295039f51a05821 100644 --- a/src/app/app.routing.module.ts +++ b/src/app/app.routing.module.ts @@ -6,8 +6,20 @@ import { OrganizationFormComponent } from './components/organizations/edit/organ import { ResourcesComponent } from './components/resources/list/resources.component'; import { ResourceFormComponent } from './components/resources/edit/resource-form.component'; import { ResourceDetailComponent } from './components/resources/detail/resource-detail.component'; -import { FormatsComponent, FormatDetailComponent, FormatFormComponent } from './components'; +import { + LogsPreReportComponent, LogsReportComponent, LogsHomeComponent, FormatsComponent, + FormatDetailComponent, FormatFormComponent, ChangelogDetailComponent, ChangelogFormComponent, + CreditsComponent, CreditFormComponent, CreditDetailComponent, ReusesComponent, ReuseFormComponent, + ReuseDetailComponent, + ProjectionsComponent, + ProjectionFormComponent, + ProjectionDetailComponent, + MediaComponent, + MediaFormComponent, + MediaDetailComponent, +} from './components'; import { AuthenticatedGuard } from './user/guards/authenticated.guard'; +import { ChangelogComponent } from './components/changelog/list/changelog.component'; const appRoutes: Routes = [ { @@ -28,7 +40,7 @@ const appRoutes: Routes = [ component: OrganizationFormComponent, canActivate: [AuthenticatedGuard], data: { - title: 'Nouveau producteur de données', + title: 'Nouveau partenaire', }, }, { @@ -36,7 +48,7 @@ const appRoutes: Routes = [ component: OrganizationFormComponent, canActivate: [AuthenticatedGuard], data: { - title: 'Modifier le producteur de données', + title: 'Modifier le partenaire', }, }, { @@ -44,7 +56,7 @@ const appRoutes: Routes = [ component: OrganizationDetailComponent, canActivate: [AuthenticatedGuard], data: { - title: 'Detail du producteur de données', + title: 'Detail du partenaire', }, }, { @@ -111,6 +123,190 @@ const appRoutes: Routes = [ title: 'Detail du format', }, }, + { + path: 'changelog', + component: ChangelogComponent, + canActivate: [AuthenticatedGuard], + data: { + title: 'Changelog', + }, + }, + { + path: 'changelog/new', + component: ChangelogFormComponent, + canActivate: [AuthenticatedGuard], + data: { + title: 'Nouveau changelog', + }, + }, + { + path: 'changelog/:id/edit', + component: ChangelogFormComponent, + canActivate: [AuthenticatedGuard], + data: { + title: 'Modifier le changelog', + }, + }, + { + path: 'changelog/:id', + component: ChangelogDetailComponent, + canActivate: [AuthenticatedGuard], + data: { + title: 'Detail du changelog', + }, + }, + { + path: 'credits', + component: CreditsComponent, + canActivate: [AuthenticatedGuard], + data: { + title: 'Crédits', + }, + }, + { + path: 'credits/new', + component: CreditFormComponent, + canActivate: [AuthenticatedGuard], + data: { + title: 'Nouveau crédit', + }, + }, + { + path: 'credits/:id/edit', + component: CreditFormComponent, + canActivate: [AuthenticatedGuard], + data: { + title: 'Modifier le crédit', + }, + }, + { + path: 'credits/:id', + component: CreditDetailComponent, + canActivate: [AuthenticatedGuard], + data: { + title: 'Detail du crédit', + }, + }, + { + path: 'reutilisations', + component: ReusesComponent, + canActivate: [AuthenticatedGuard], + data: { + title: 'Réutilisations', + }, + }, + { + path: 'reutilisations/new', + component: ReuseFormComponent, + canActivate: [AuthenticatedGuard], + data: { + title: 'Nouvelle réutilisation', + }, + }, + { + path: 'reutilisations/:id/edit', + component: ReuseFormComponent, + canActivate: [AuthenticatedGuard], + data: { + title: 'Modifier la réutilisation', + }, + }, + { + path: 'reutilisations/:id', + component: ReuseDetailComponent, + canActivate: [AuthenticatedGuard], + data: { + title: 'Detail de la réutilisation', + }, + }, + { + path: 'datalogs', + component: LogsHomeComponent, + canActivate: [AuthenticatedGuard], + data: { + title: "Logs d'indexation: recherche", + }, + }, + { + path: 'datalogs/preReport/:type/:id', + component: LogsPreReportComponent, + canActivate: [AuthenticatedGuard], + data: { + title: "Logs d'indexation: recherche affinée", + }, + }, + { + path: 'datalogs/report/:slug/:sessionId/:urlCode', + component: LogsReportComponent, + canActivate: [AuthenticatedGuard], + data: { + title: "Rapport final d'indexation", + }, + }, + { + path: 'projections', + component: ProjectionsComponent, + canActivate: [AuthenticatedGuard], + data: { + title: 'Projections', + }, + }, + { + path: 'projections/new', + component: ProjectionFormComponent, + canActivate: [AuthenticatedGuard], + data: { + title: 'Nouvelle projection', + }, + }, + { + path: 'projections/:id/edit', + component: ProjectionFormComponent, + canActivate: [AuthenticatedGuard], + data: { + title: 'Modifier la projection', + }, + }, + { + path: 'projections/:id', + component: ProjectionDetailComponent, + canActivate: [AuthenticatedGuard], + data: { + title: 'Detail de la projection', + }, + }, + { + path: 'media', + component: MediaComponent, + canActivate: [AuthenticatedGuard], + data: { + title: 'Médias', + }, + }, + { + path: 'media/new', + component: MediaFormComponent, + canActivate: [AuthenticatedGuard], + data: { + title: 'Nouveau média', + }, + }, + { + path: 'media/:id/edit', + component: MediaFormComponent, + canActivate: [AuthenticatedGuard], + data: { + title: 'Modifier le média', + }, + }, + { + path: 'media/:id', + component: MediaDetailComponent, + canActivate: [AuthenticatedGuard], + data: { + title: 'Detail du média', + }, + }, ]; @NgModule({ diff --git a/src/app/components/changelog/detail/changelog-detail.component.html b/src/app/components/changelog/detail/changelog-detail.component.html new file mode 100644 index 0000000000000000000000000000000000000000..4c4fb3fff5f023ad41c79c189b39a3208802973f --- /dev/null +++ b/src/app/components/changelog/detail/changelog-detail.component.html @@ -0,0 +1,61 @@ +<section class="section page-container" *ngIf="changelog"> + <app-page-header [pageInfo]="{title: title}"></app-page-header> + + <div class="columns is-centered"> + <div class="column is-8"> + <div class="card"> + <header class="card-header"> + <p class="card-header-title has-text-centered"> + Version {{changelog.version}} + </p> + </header> + <div class="card-content"> + <div class="content"> + <p> + <span class="has-text-weight-bold">Id: </span> + <span>{{changelog._id}}</span> + </p> + <p> + <span class="has-text-weight-bold">Date de création: </span> + <span>{{changelog.createDate | date:'dd-LL-yyyy HH:mm:ss'}}</span> + </p> + <p> + <span class="has-text-weight-bold">Date de dernière mise à jour: </span> + <span>{{changelog.updateDate | date:'dd-LL-yyyy HH:mm:ss'}}</span> + </p> + <p> + <span class="has-text-weight-bold">Langue: </span> + <span>{{changelog.language}}</span> + </p> + <div class="improvment-list-container" + *ngIf="changelog.majorImprovements && changelog.majorImprovements.length > 0"> + <span class="has-text-weight-bold">Amélioration(s) majeure(s): </span> + <ul> + <li *ngFor="let major of changelog.majorImprovements"> + {{ major }} + </li> + </ul> + </div> + <div class="improvment-list-container" + *ngIf="changelog.minorImprovements && changelog.minorImprovements.length > 0"> + <span class="has-text-weight-bold">Amélioration(s) mineure(s): </span> + <ul> + <li *ngFor="let minor of changelog.minorImprovements"> + {{ minor }} + </li> + </ul> + </div> + <div class="improvment-list-container" *ngIf="changelog.bugFixes && changelog.bugFixes.length > 0"> + <span class="has-text-weight-bold">Correction(s) de bug(s): </span> + <ul> + <li *ngFor="let bug of changelog.bugFixes"> + {{ bug }} + </li> + </ul> + </div> + </div> + </div> + </div> + </div> + </div> +</section> \ No newline at end of file diff --git a/src/app/components/changelog/detail/changelog-detail.component.scss b/src/app/components/changelog/detail/changelog-detail.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..6c4c597c8549528b13c5ef646c06badf7a49fa60 --- /dev/null +++ b/src/app/components/changelog/detail/changelog-detail.component.scss @@ -0,0 +1,11 @@ +.card-header-title { + justify-content: center; +} + +.improvment-list-container:not(:last-of-type) { + margin-bottom: 1rem; + + ul { + margin-top: 0.5rem; + } +} diff --git a/src/app/components/changelog/detail/changelog-detail.component.ts b/src/app/components/changelog/detail/changelog-detail.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..13c119ceea40d68dbb3a539e5ebcaffa27d4b4ec --- /dev/null +++ b/src/app/components/changelog/detail/changelog-detail.component.ts @@ -0,0 +1,31 @@ + +import { switchMap } from 'rxjs/operators'; +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, ParamMap } from '@angular/router'; +import { FormatService, ChangelogService } from 'src/app/services'; +import { Format } from 'src/app/models/format.model'; +import { Changelog } from '../../../models/changelog.model'; + +@Component({ + selector: 'app-changelog-detail', + templateUrl: './changelog-detail.component.html', + styleUrls: ['./changelog-detail.component.scss'], +}) +export class ChangelogDetailComponent implements OnInit { + + changelog: Changelog; + title: string; + + constructor( + private _route: ActivatedRoute, + private _changelogService: ChangelogService, + ) { + } + + ngOnInit(): void { + this.title = this._route.snapshot.data.title; + this._route.paramMap.pipe( + switchMap((params: ParamMap) => this._changelogService.findById(params.get('id')))) + .subscribe((changelog: Changelog) => this.changelog = changelog); + } +} diff --git a/src/app/components/changelog/edit/changelog-form.component.html b/src/app/components/changelog/edit/changelog-form.component.html new file mode 100644 index 0000000000000000000000000000000000000000..b4598bb289af245ec4b3c89b3047b8ad3170f6a3 --- /dev/null +++ b/src/app/components/changelog/edit/changelog-form.component.html @@ -0,0 +1,124 @@ +<section class="section page-container"> + <app-page-header [pageInfo]="{title: title}"></app-page-header> + + <form [formGroup]="form" (ngSubmit)="onSubmit()" class="columns is-centered is-marginless"> + <div class="column is-7"> + <input type="hidden" formControlName="_id" [value]="changelog._id"> + + <div class="field"> + <label class="label required" for="version">Version</label> + <div class="control"> + <input class="input" type="text" [value]="changelog.version" formControlName="version" id="version" required> + </div> + <div *ngIf="version.invalid && (version.dirty || version.touched)" class="alert alert-danger"> + <p *ngIf="version.errors['required']" class="help is-danger"> + La version est obligatoire. + </p> + </div> + </div> + + <div class="field"> + <label class="label required" for="language">Langue</label> + <div class="select"> + <select [value]="changelog.language" formControlName="language" id="language"> + <option value="EN">Anglais</option> + <option value="FR">Français</option> + </select> + </div> + </div> + + <div class="field"> + <div class="form-array-header"> + <label class="label">Amélioration(s) majeure(s)</label> + <span class="icon" tabindex=0 (click)="addMajorImprovement()" (keyup.enter)="addMajorImprovement()" + title="Ajouter une amélioration majeure"> + <i class="fas fa-plus"></i> + </span> + </div> + + <div formArrayName="majorImprovements"> + <div *ngFor="let majorImprovement of majorImprovements.controls; let i = index;" class="form-array-item"> + <div class="form-array-input-wrapper"> + <div class="control"> + <input class="input" type="text" formControlName="{{i}}" required> + </div> + <div *ngIf="majorImprovement.invalid && (majorImprovement.dirty || majorImprovement.touched)" + class="alert alert-danger"> + <p *ngIf="majorImprovement.hasError('required')" class="help is-danger"> + Vous devez saisir la description de l'amélioration majeure. + </p> + </div> + </div> + <span class="icon" tabindex=0 (click)="removeMajorImprovement(i)" (keyup.enter)="removeMajorImprovement(i)" + title="Supprimer l'amélioration majeure"> + <i class="fas fa-trash"></i> + </span> + </div> + </div> + </div> + + <div class="field"> + <div class="form-array-header"> + <label class="label">Amélioration(s) mineure(s)</label> + <span class="icon" tabindex=0 (click)="addMinorImprovement()" (keyup.enter)="addMinorImprovement()" + title="Ajouter une amélioration mineure"> + <i class="fas fa-plus"></i> + </span> + </div> + + <div formArrayName="minorImprovements"> + <div *ngFor="let minorImprovement of minorImprovements.controls; let i = index;" class="form-array-item"> + <div class="form-array-input-wrapper"> + <div class="control"> + <input class="input" type="text" formControlName="{{i}}" required> + </div> + <div *ngIf="minorImprovement.invalid && (minorImprovement.dirty || minorImprovement.touched)" + class="alert alert-danger"> + <p *ngIf="minorImprovement.hasError('required')" class="help is-danger"> + Vous devez saisir la description de l'amélioration mineur. + </p> + </div> + </div> + <span class="icon" tabindex=0 (click)="removeMinorImprovement(i)" (keyup.enter)="removeMinorImprovement(i)" + title="Supprimer l'amélioration mineure"> + <i class="fas fa-trash"></i> + </span> + </div> + </div> + </div> + + <div class="field"> + <div class="form-array-header"> + <label class="label">Correction(s) de bug(s)</label> + <span class="icon" tabindex=0 (click)="addBugFix()" (keyup.enter)="addBugFix()" + title="Ajouter une correction de bug"> + <i class="fas fa-plus"></i> + </span> + </div> + + <div formArrayName="bugFixes"> + <div *ngFor="let bugFix of bugFixes.controls; let i = index;" class="form-array-item"> + <div class="form-array-input-wrapper"> + <div class="control"> + <input class="input" type="text" formControlName="{{i}}" required> + </div> + <div *ngIf="bugFix.invalid && (bugFix.dirty || bugFix.touched)" class="alert alert-danger"> + <p *ngIf="bugFix.hasError('required')" class="help is-danger"> + Vous devez saisir la description de la correction de bug. + </p> + </div> + </div> + <span class="icon" tabindex=0 (click)="removeBugFix(i)" (keyup.keyup.enter)="removeBugFix(i)" + title="Supprimer la correction de bug"> + <i class="fas fa-trash"></i> + </span> + </div> + </div> + </div> + + <div class="has-text-right"> + <button class="button button-gl" type="submit" [disabled]="formInvalid == true">Valider</button> + </div> + </div> + </form> +</section> \ No newline at end of file diff --git a/src/app/components/changelog/edit/changelog-form.component.scss b/src/app/components/changelog/edit/changelog-form.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..4bc5c21221ab9f7ba636ca989a4038e8b3db6af0 --- /dev/null +++ b/src/app/components/changelog/edit/changelog-form.component.scss @@ -0,0 +1,43 @@ +.full-width { + width: 100%; +} + +h1 { + text-align: center +} + +.icon { + cursor: pointer; + + &:hover { + .fa-plus { + color: lightblue; + } + + .fa-trash { + color: #d5232a; + } + } +} + +.form-array-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0.5em; + + label { + margin-bottom: 0; + } +} + +.form-array-item { + display: flex; + align-items: center; + margin-bottom: 0.5em; +} + +.form-array-input-wrapper { + flex-grow: 1; + margin-right: 0.5rem; +} diff --git a/src/app/components/changelog/edit/changelog-form.component.ts b/src/app/components/changelog/edit/changelog-form.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..7bd875d87f7dff96a484c9ea9487d54e486daf91 --- /dev/null +++ b/src/app/components/changelog/edit/changelog-form.component.ts @@ -0,0 +1,145 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, ParamMap, Router } from '@angular/router'; +import { FormBuilder, FormGroup, Validators, FormArray, FormControl } from '@angular/forms'; +import { filter, switchMap } from 'rxjs/operators'; +import { Format } from 'src/app/models/format.model'; +import { FormatService, ChangelogService, NotificationService } from 'src/app/services'; +import { Changelog } from '../../../models/changelog.model'; + +@Component({ + selector: 'app-changelog-form', + templateUrl: './changelog-form.component.html', + styleUrls: ['./changelog-form.component.scss'], +}) +export class ChangelogFormComponent implements OnInit { + + changelog: Changelog = new Changelog(); + form: FormGroup; + title: string; + + constructor( + private _changelogService: ChangelogService, + private _route: ActivatedRoute, + private _router: Router, + private _fb: FormBuilder, + private _notificationService: NotificationService, + ) { + } + + ngOnInit() { + this.title = this._route.snapshot.data.title; + this.initForm(); + + this._route.paramMap.pipe( + filter((paramMap: ParamMap) => (paramMap.get('id') !== null)), + switchMap((paramMap: ParamMap) => this._changelogService.findById(paramMap.get('id')))) + .subscribe((changelog: Changelog) => { + + this.changelog = changelog; + + this.initForm(); + + }); + } + + initForm() { + this.form = this._fb.group({ + _id: [this.changelog._id], + version: [this.changelog.version, Validators.required], + language: [this.changelog.language, Validators.required], + majorImprovements: new FormArray(this.changelog.majorImprovements.map(major => new FormControl(major))), + minorImprovements: new FormArray(this.changelog.minorImprovements.map(minor => new FormControl(minor))), + bugFixes: new FormArray(this.changelog.bugFixes.map(bug => new FormControl(bug))), + }); + } + + onSubmit() { + if (!this.formInvalid) { + this.changelog = new Changelog(this.form.value); + + if (this.changelog._id) { + this._changelogService.update(this.changelog).subscribe( + (changelogCreated) => { + this._notificationService.notify({ + type: 'success', + message: 'Le changelog a bien été mis à jour.', + }); + this._router.navigate(['/changelog', changelogCreated._id]); + }, + () => { + this._notificationService.notify({ + type: 'error', + message: 'Une erreur est survenue lors de la mise à jour du changelog.', + }); + }, + ); + } else { + this._changelogService.create(this.changelog).subscribe( + (changelogCreated) => { + this._notificationService.notify({ + type: 'success', + message: 'Le changelog a bien été créé.', + }); + this._router.navigate(['/changelog', changelogCreated._id]); + }, + () => { + this._notificationService.notify({ + type: 'error', + message: 'Une erreur est survenue lors de la création du changelog.', + }); + }, + ); + } + } + } + + addMajorImprovement() { + this.majorImprovements.push(new FormControl()); + } + + addMinorImprovement() { + this.minorImprovements.push(new FormControl()); + } + + addBugFix() { + this.bugFixes.push(new FormControl()); + } + + removeMajorImprovement(index) { + this.majorImprovements.removeAt(index); + } + + removeMinorImprovement(index) { + this.minorImprovements.removeAt(index); + } + + removeBugFix(index) { + this.bugFixes.removeAt(index); + } + + // Getters for each property + get version() { + return this.form.controls['version']; + } + + get language() { + return this.form.controls['language']; + } + + get majorImprovements(): FormArray { + return this.form.get('majorImprovements') as FormArray; + } + + get minorImprovements(): FormArray { + return this.form.get('minorImprovements') as FormArray; + } + + get bugFixes(): FormArray { + return this.form.get('bugFixes') as FormArray; + } + + get formInvalid() { + return this.form.invalid; + } + +} diff --git a/src/app/components/changelog/list/changelog.component.html b/src/app/components/changelog/list/changelog.component.html new file mode 100644 index 0000000000000000000000000000000000000000..5485e47fa3865ad82522cbf13925b1c79c072985 --- /dev/null +++ b/src/app/components/changelog/list/changelog.component.html @@ -0,0 +1,90 @@ +<section class="section page-container"> + <app-page-header [pageInfo]="pageHeaderInfo" [hideBackButton]="true"></app-page-header> + <div class="add-item-link has-text-right"> + <a class="button button-gl" [routerLink]="['new']"> + Ajouter + </a> + </div> + <div class="table entity-list-table" *ngIf="changelogs"> + <div class="header columns is-marginless"> + <div class="column is-2"> + <span (click)="sortBy('version')" class="is-sortable"> + <span class="sort-icons"> + <span class="icon"> + <i class="fas fa-sort-up" + [ngClass]="{'icon-red': sortOptions.value === 'version' && sortOptions.order === 'desc'}"></i> + </span> + <span class="icon"> + <i class="fas fa-sort-down" + [ngClass]="{'icon-red': sortOptions.value === 'version' && sortOptions.order === 'asc'}"></i> + </span> + </span> + <span class="column-title" [ngClass]="{'active': sortOptions.value === version}">Version</span> + </span> + </div> + <div class="column is-2"> + <span (click)="sortBy('language')" class="is-sortable"> + <span class="sort-icons"> + <span class="icon"> + <i class="fas fa-sort-up" + [ngClass]="{'icon-red': sortOptions.value === 'language' && sortOptions.order === 'desc'}"></i> + </span> + <span class="icon"> + <i class="fas fa-sort-down" + [ngClass]="{'icon-red': sortOptions.value === 'language' && sortOptions.order === 'asc'}"></i> + </span> + </span> + <span class="column-title" [ngClass]="{'active': sortOptions.value === language}">Langue</span> + </span> + </div> + <div class="column is-2"> + <span class="column-title">Amélioration(s) majeure(s)</span> + </div> + <div class="column is-2"> + <span class="column-title">Amélioration(s) mineure(s)</span> + </div> + <div class="column is-2"> + <span class="column-title">Correction(s) de bug(s)</span> + </div> + <div class="column is-offset-1 is-1 has-text-centered"> + <span class="column-title">Actions</span> + </div> + </div> + <div class="data-list"> + <div class="data columns is-multiline is-vcentered is-marginless" + *ngFor="let changelog of changelogs; let i=index; let odd=odd; let even=even;" + [ngClass]="{ odd: odd, even: even }"> + <div class="column is-2"> + <span>{{ changelog.version }}</span> + </div> + <div class="column is-2"> + <span>{{ changelog.language }}</span> + </div> + <div class="column is-2"> + <span>{{ changelog.majorImprovements ? changelog.majorImprovements.length : 0 }}</span> + </div> + <div class="column is-2"> + <span>{{ changelog.minorImprovements ? changelog.minorImprovements.length : 0 }}</span> + </div> + <div class="column is-2"> + <span>{{ changelog.bugFixes ? changelog.bugFixes.length : 0 }}</span> + </div> + <div class="column is-offset-1 is-1 has-text-centered actions"> + <app-crud-buttons [id]="changelog._id" (delete)="displayDeletePopup($event)"></app-crud-buttons> + </div> + </div> + </div> + <div class="columns is-marginless paginator"> + <div class="column"> + <app-paginator *ngIf="paginator.length > 0" [length]="paginator.length" [pageSize]="paginator.limit" + [pageSizeOptions]="paginator.pageSizeOptions" [pageIndex]="paginator.pageIndex" [pagesToShow]="5" + [showFirstLastButtons]="true" (page)="changePagination($event)"> + </app-paginator> + </div> + </div> + </div> +</section> + +<app-confirmation-modal (cancel)="objectToBeDeletedId=null" (continue)="deleteChangelog()" [texts]="deleteModalTexts" + [isOpened]="objectToBeDeletedId !== null"> +</app-confirmation-modal> \ No newline at end of file diff --git a/src/app/components/changelog/list/changelog.component.scss b/src/app/components/changelog/list/changelog.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/app/components/changelog/list/changelog.component.ts b/src/app/components/changelog/list/changelog.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..169fce8b9b7266bc9e950793d30384030e981082 --- /dev/null +++ b/src/app/components/changelog/list/changelog.component.ts @@ -0,0 +1,144 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { PaginatorOptions } from 'src/app/models/paginator-options.model'; +import { IPageHeaderInfo } from '../../../models/page.model'; +import { Subscription } from 'rxjs'; +import { ChangelogService, NotificationService } from '../../../services'; +import { ChangelogRO, Changelog } from '../../../models/changelog.model'; + +@Component({ + selector: 'app-changelog', + templateUrl: './changelog.component.html', + styleUrls: ['./changelog.component.scss'], +}) +export class ChangelogComponent implements OnInit, OnDestroy { + + pageHeaderInfo: IPageHeaderInfo = { + title: '', + }; + objectToBeDeletedId = null; + deleteModalTexts = { + main: 'Si vous poursuivez, le changelog sera définitivement supprimé.', + cancel: 'Annuler', + continue: 'Supprimer', + }; + changelogs: Changelog[] = []; + searchChangeSub: Subscription; + + // Paginator options + paginator: PaginatorOptions; + pageSize = 10; + pageSizeOptions = [5, 10, 25, 100]; + + sortValue: string; + + totalElement: number; + filters = { + name: '', + }; + where = {}; + + constructor( + private _changelogService: ChangelogService, + private _notificationService: NotificationService, + ) { + this.paginator = { + pageIndex: this._changelogService.pageNumber, + length: 0, + limit: this._changelogService.limit, + pageSizeOptions: [5, 10, 20], + }; + } + + ngOnInit(): void { + this._changelogService.sortOptions = { + value: 'version', + order: 'desc', + }; + this.search(); + + this.searchChangeSub = this._changelogService.searchChange$.subscribe( + () => { + this.search(); + }, + ); + } + + private search() { + this._changelogService.getChangelogs() + .subscribe( + (items: ChangelogRO) => { + this.changelogs = items.changelogs; + this.totalElement = items.totalCount; + + this.pageHeaderInfo.title = this.totalElement > 1 ? + `${this.totalElement} changelogs trouvés` : + `${this.totalElement} changelog trouvé`; + + this.paginator.limit = this._changelogService.limit; + this.paginator.pageIndex = this._changelogService.pageNumber; + this.paginator.length = items.totalCount; + }, + () => { + this.pageHeaderInfo.title = '0 changelog trouvé'; + this._notificationService.notify({ + type: 'error', + message: 'Une erreur est survenue lors du chargement des changelogs.', + }); + }, + ); + } + + // When pagination is changed by user, we update datasetList with new pagination options + changePagination(pageIndex) { + this._changelogService.paginationChanged(this.paginator.limit, pageIndex); + } + + changePageSize(pageSize) { + this._changelogService.paginationChanged(pageSize, 1); + } + + sortBy(key: string) { + if (this._changelogService.sortOptions.value === key) { + this._changelogService.reverseSortOrder(); + } else { + this._changelogService.sortOptions.value = key; + this._changelogService.sortOptions.order = 'asc'; + } + this.search(); + } + + get sortOptions() { + return this._changelogService.sortOptions; + } + + displayDeletePopup(id) { + this.objectToBeDeletedId = id; + } + + deleteChangelog() { + this._changelogService.delete(this.objectToBeDeletedId).subscribe( + () => { + this._notificationService.notify({ + type: 'success', + message: 'Le changelog a été supprimé avec succès.', + }); + this._changelogService.pageNumber = 1; + this.search(); + }, + () => { + this._notificationService.notify({ + type: 'error', + message: 'Une erreur est survenue lors de la suppression du changelog.', + }); + }, + () => { + this.objectToBeDeletedId = null; + }, + ); + + } + + ngOnDestroy() { + this.searchChangeSub.unsubscribe(); + } +} diff --git a/src/app/components/confirmation-modal/confirmation-modal.component.html b/src/app/components/confirmation-modal/confirmation-modal.component.html new file mode 100644 index 0000000000000000000000000000000000000000..81ebcd8a1cc17e2a51771d8d9a8c69ead71e1da9 --- /dev/null +++ b/src/app/components/confirmation-modal/confirmation-modal.component.html @@ -0,0 +1,13 @@ +<div class="modal" [ngClass]="{'is-active': isOpened === true}"> + <div class="modal-background"></div> + <div class="modal-card"> + <section class="modal-card-body"> + <p>{{ texts.main }}</p> + </section> + <footer class="modal-card-foot"> + <button class="button btn-red-text" (click)="close()">{{ texts.cancel }}</button> + <button class="button button-gl" (click)="carryOn()">{{ texts.continue }}</button> + </footer> + </div> + <button class="modal-close is-large" aria-label="close" (click)="close()"></button> +</div> \ No newline at end of file diff --git a/src/app/components/confirmation-modal/confirmation-modal.component.scss b/src/app/components/confirmation-modal/confirmation-modal.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..8cc9988f537af838eb65e787a12be7b7ca29cf19 --- /dev/null +++ b/src/app/components/confirmation-modal/confirmation-modal.component.scss @@ -0,0 +1,8 @@ +.modal-card-body { + border-top-left-radius: 6px; + border-top-right-radius: 6px; +} + +.modal-card-foot { + justify-content: flex-end; +} diff --git a/src/app/components/confirmation-modal/confirmation-modal.component.ts b/src/app/components/confirmation-modal/confirmation-modal.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..09b6de5727e9d4204b7053e4d6447f31c14f42e8 --- /dev/null +++ b/src/app/components/confirmation-modal/confirmation-modal.component.ts @@ -0,0 +1,38 @@ +import { Component, Input, EventEmitter, Output, Renderer2, OnChanges } from '@angular/core'; + +@Component({ + selector: 'app-confirmation-modal', + templateUrl: './confirmation-modal.component.html', + styleUrls: ['./confirmation-modal.component.scss'], +}) +export class ConfirmationModalComponent implements OnChanges { + + @Input() isOpened: boolean; + @Input() texts: { + main: string; + cancel: string; + continue: string; + }; + @Output() cancel = new EventEmitter<boolean>(); + @Output() continue = new EventEmitter<boolean>(); + + constructor( + private renderer: Renderer2, + ) { } + + ngOnChanges() { + if (this.isOpened) { + this.renderer.addClass(document.body, 'is-clipped'); + } else { + this.renderer.removeClass(document.body, 'is-clipped'); + } + } + + close() { + this.cancel.emit(true); + } + + carryOn() { + this.continue.emit(true); + } +} diff --git a/src/app/components/credits/detail/credit-detail.component.html b/src/app/components/credits/detail/credit-detail.component.html new file mode 100644 index 0000000000000000000000000000000000000000..3bb156fe9c838717d528d354848458737ac55b3b --- /dev/null +++ b/src/app/components/credits/detail/credit-detail.component.html @@ -0,0 +1,43 @@ +<section class="section page-container" *ngIf="credit"> + <app-page-header [pageInfo]="{title: title}"></app-page-header> + <div class="columns is-centered"> + <div class="column is-8"> + <div class="card"> + <header class="card-header"> + <p class="card-header-title has-text-centered"> + {{credit.name}} + </p> + </header> + <div class="card-image"> + <figure class="image"> + <img [src]="credit.logo" alt="Logo du credit"> + </figure> + </div> + <div class="card-content"> + + <div class="content"> + <div> + <p>{{credit.description}}</p> + </div> + <br> + <div> + <p><span class="has-text-weight-bold">Statut:</span> {{ credit.published ? 'Publié' : 'Brouillon' }} + </p> + </div> + <br> + <div> + <p><span class="has-text-weight-bold">Id:</span> {{ credit.id}}</p> + </div> + <br> + <div> + <p class="has-text-weight-bold">Liens</p> + <p *ngFor="let link of credit.links"> + <a href="{{ link.url }}" target="_blank">{{link.url}}</a> + </p> + </div> + </div> + </div> + </div> + </div> + </div> +</section> \ No newline at end of file diff --git a/src/app/components/credits/detail/credit-detail.component.scss b/src/app/components/credits/detail/credit-detail.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..63a36307795f8cebd94c864bbbaa3deba1ae7da7 --- /dev/null +++ b/src/app/components/credits/detail/credit-detail.component.scss @@ -0,0 +1,13 @@ +figure { + text-align: center; +} + +figure img { + max-width: 150px; + display: inline-block; + margin-top: 20px; +} + +.card-header-title { + justify-content: center; +} diff --git a/src/app/components/credits/detail/credit-detail.component.ts b/src/app/components/credits/detail/credit-detail.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..4a707c38d54934939e5494f02d8d86ccb4108d80 --- /dev/null +++ b/src/app/components/credits/detail/credit-detail.component.ts @@ -0,0 +1,31 @@ + +import { switchMap } from 'rxjs/operators'; +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, ParamMap } from '@angular/router'; + +import { Credit } from 'src/app/models/credit.model'; +import { CreditService } from 'src/app/services/credit.service'; + +@Component({ + selector: 'app-credit-detail', + templateUrl: './credit-detail.component.html', + styleUrls: ['./credit-detail.component.scss'], +}) +export class CreditDetailComponent implements OnInit { + + credit: Credit; + title: string; + + constructor( + private _route: ActivatedRoute, + private _creditService: CreditService, + ) { + } + + ngOnInit(): void { + this.title = this._route.snapshot.data.title; + this._route.paramMap.pipe( + switchMap((params: ParamMap) => this._creditService.findById(params.get('id')))) + .subscribe((credit: Credit) => this.credit = credit); + } +} diff --git a/src/app/components/credits/edit/credit-form.component.html b/src/app/components/credits/edit/credit-form.component.html new file mode 100644 index 0000000000000000000000000000000000000000..34fccf9b7361ed73e1bf9ce29be4bb33e7e30f33 --- /dev/null +++ b/src/app/components/credits/edit/credit-form.component.html @@ -0,0 +1,91 @@ +<section class="section page-container" *ngIf="credit"> + <form [formGroup]="form" (ngSubmit)="onSubmit()" class="columns is-centered is-marginless is-multiline"> + <div class="column is-12 header-with-publication-status"> + <app-page-header [pageInfo]="{title: title}"></app-page-header> + <div class="field status-field"> + <span class="fake-label" *ngIf="form.get('published').value === true">Publié</span> + <span class="fake-label" *ngIf="form.get('published').value === false">Brouillon</span> + <input id="published" type="checkbox" formControlName="published" class="switch is-rounded"> + <label for="published"></label> + </div> + </div> + <div class="column is-7"> + <input type="hidden" formControlName="id" value="{{credit.id}}"> + + + <div class="field"> + <label class="label required" for="name">Nom</label> + <div class="control"> + <input class="input" type="text" [value]="credit.name" formControlName="name" id="name" required> + </div> + <div *ngIf="name.invalid && (name.dirty || name.touched)" class="alert alert-danger"> + <p *ngIf="name.errors['required']" class="help is-danger"> + Le nom du crédit est obligatoire. + </p> + </div> + </div> + + <app-image-upload [fieldParams]="logoFieldParams" (fileChanged)="logoChanged($event)" + (imageRemoved)="removeLogo()"> + </app-image-upload> + + <div class="field"> + <label class="label required" for="description">Description <span class="is-italic has-text-weight-normal">(150 + caractères max)</span></label> + <div class="control"> + <textarea class="textarea" formControlName="description" id="description" required> + {{ credit.description }} + </textarea> + </div> + <div *ngIf="description.invalid && (description.dirty || description.touched)" class="alert alert-danger"> + <p *ngIf="description.errors['required']" class="help is-danger"> + La description du crédit est obligatoire. + </p> + <p *ngIf="description.errors['maxlength']" class="help is-danger"> + La description ne peut dépasser 150 caractères. + </p> + </div> + </div> + + <div class="field links"> + <div class="columns"> + <div class="column is-11"> + <label class="label">Liens</label> + </div> + <div class="column is-1"> + <span class="icon" (click)="addLink()" title="Ajouter un lien"> + <i class="fas fa-plus"></i> + </span> + </div> + </div> + + <div formArrayName="links"> + <div *ngFor="let link of formLinks.controls; let i = index;" [formGroupName]="i" + class="columns is-multiline field"> + <div class="column is-11"> + <div class="control"> + <input class="input" type="text" formControlName="url" required> + </div> + <div *ngIf="link['controls'].url.invalid && (link['controls'].url.dirty || link['controls'].url.touched)" + class="alert alert-danger"> + <p *ngIf="link.hasError('required', 'url')" class="help is-danger"> + L'URL du lien est obligatoire. + </p> + </div> + </div> + + <div class="column is-1"> + <span class="icon" (click)="removeLink(i)" title="Supprimer le lien"> + <i class="fas fa-trash"></i> + </span> + </div> + </div> + </div> + </div> + <br> + <div class="has-text-right"> + <button class="button button-gl" type="submit" [disabled]="formInvalid == true">Valider</button> + </div> + </div> + </form> +</section> \ No newline at end of file diff --git a/src/app/components/credits/edit/credit-form.component.scss b/src/app/components/credits/edit/credit-form.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..0762aa33dcef6109e5521239a03190ee9c3670a1 --- /dev/null +++ b/src/app/components/credits/edit/credit-form.component.scss @@ -0,0 +1,25 @@ +.full-width { + width: 100%; +} + +.page-container { + position: relative; +} + +h1 { + text-align: center +} + +.icon { + cursor: pointer; + + &:hover { + .fa-plus { + color: lightblue; + } + + .fa-trash { + color: #d5232a; + } + } +} diff --git a/src/app/components/credits/edit/credit-form.component.ts b/src/app/components/credits/edit/credit-form.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..160ab094596df2af8528566750c94c91e2e0002f --- /dev/null +++ b/src/app/components/credits/edit/credit-form.component.ts @@ -0,0 +1,240 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, ParamMap, Router } from '@angular/router'; +import { Credit } from 'src/app/models/credit.model'; +import { FormBuilder, FormGroup, Validators, FormArray } from '@angular/forms'; +import { filter, switchMap, mergeMap, catchError } from 'rxjs/operators'; +import { IImageUploadFieldParams } from 'src/app/models/image-upload.model'; +import { NotificationService, MediaService, CreditService } from 'src/app/services'; +import { throwError } from 'rxjs'; +import { Media } from '../../../models/media.model'; + +@Component({ + selector: 'app-credit-form', + templateUrl: './credit-form.component.html', + styleUrls: ['./credit-form.component.scss'], +}) +export class CreditFormComponent implements OnInit { + + credit: Credit; + form: FormGroup; + logoFile: File; + logoFieldParams: IImageUploadFieldParams = { + inputName: 'logo', + label: 'Logo', + existingImageUrl: null, + isRequired: true, + }; + logo: File; + title: string; + + constructor( + private _creditService: CreditService, + private _mediaService: MediaService, + private _notificationService: NotificationService, + private _route: ActivatedRoute, + private _router: Router, + private _fb: FormBuilder, + ) { + } + + ngOnInit() { + this.title = this._route.snapshot.data.title; + this.initForm(); + + this._route.paramMap.pipe( + filter((paramMap: ParamMap) => (paramMap.get('id') !== null)), + switchMap((paramMap: ParamMap) => this._creditService.findById(paramMap.get('id')))) + .subscribe((credit: Credit) => { + + this.credit = credit; + + const arr = new FormArray([]); + this.credit.links.forEach((y) => { + arr.push(this._fb.group({ + url: y.url, + creditId: y.creditId, + })); + }); + + this.logoFieldParams.existingImageUrl = credit.logo; + + this.form = this._fb.group( + { + id: [this.credit.id], + name: [credit.name, Validators.required], + description: [credit.description, [Validators.required, Validators.maxLength(150)]], + logo: [credit.logo], + links: arr, + published: [this.credit.published], + }); + }); + + } + + initForm() { + this.credit = new Credit(); + const arr = new FormArray([]); + this.logoFieldParams.existingImageUrl = null; + this.form = this._fb.group( + { + id: [this.credit.id], + name: [this.credit.name, Validators.required], + description: [this.credit.description, [Validators.required, Validators.maxLength(150)]], + logo: [this.credit.logo], + links: arr, + published: [this.credit.published], + }); + } + + initItemLinkForm() { + return this._fb.group({ + name: '', + url: ['', Validators.required], + }); + } + + addLink() { + if (!this.form.controls.links) { + this.credit.links = []; + } + (this.form.controls.links as FormArray).push(this._fb.group( + { + id: null, name: '', + url: ['', Validators.required], + creditId: this.credit.id, + }), + ); + } + + removeLink(index) { + (this.form.controls.links as FormArray).removeAt(index); + } + + get formLinks() { + return this.form.controls.links as FormArray; + } + + onSubmit() { + if (!this.formInvalid) { + this.credit = new Credit(this.form.value); + + if (this.credit.id) { + if (this.logoFile) { + this._mediaService.uploadFile(this.logoFile).pipe( + catchError(() => { + return throwError('Une erreur est survenue lors de l\'upload du logo du crédit.'); + }), + mergeMap((response: Media) => { + this.credit.logo = response.url; + return this._creditService.update(this.credit).pipe( + catchError(() => { + return throwError('Une erreur est survenue lors de la mise à jour du crédit.'); + }), + ); + }), + ).subscribe( + (creditUpdated) => { + this._notificationService.notify({ + message: 'Le crédit a été mis à jour avec succès.', + type: 'success', + }); + this._router.navigate(['/credits', creditUpdated.id]); + }, + (err) => { + this._notificationService.notify({ + message: err, + type: 'error', + }); + }, + ); + } else { + return this._creditService.update(this.credit).subscribe( + (creditUpdated) => { + this._notificationService.notify({ + message: 'Le crédit a été mis à jour avec succès.', + type: 'success', + }); + this._router.navigate(['/credits', creditUpdated.id]); + }, + () => { + this._notificationService.notify({ + message: 'Une erreur est survenue lors de la mise à jour du crédit.', + type: 'error', + }); + }, + ); + } + } else { + if (this.logoFile) { + this._mediaService.uploadFile(this.logoFile).pipe( + catchError(() => { + return throwError('Une erreur est survenue lors de l\'upload du logo du crédit.'); + }), + mergeMap((response: Media) => { + this.credit.logo = response.url; + return this._creditService.create(this.credit).pipe( + catchError(() => { + return throwError('Une erreur est survenue lors de la création du crédit.'); + }), + ); + }), + ).subscribe( + (creditCreated) => { + this._notificationService.notify({ + message: 'Le crédit a été créé avec succès.', + type: 'success', + }); + this._router.navigate(['/credits', creditCreated.id]); + }, + (err) => { + this._notificationService.notify({ + message: err, + type: 'error', + }); + }, + ); + } else { + return this._creditService.create(this.credit).subscribe( + (creditUpdated) => { + this._notificationService.notify({ + message: 'Le crédit a été créé avec succès.', + type: 'success', + }); + this._router.navigate(['/credits', creditUpdated.id]); + }, + () => { + this._notificationService.notify({ + message: 'Une erreur est survenue lors de la création du crédit.', + type: 'error', + }); + }, + ); + } + } + } + } + + // Getters for each property + get name() { + return this.form.controls['name']; + } + + get description() { + return this.form.controls['description']; + } + + get formInvalid() { + return this.form.invalid || + (!this.credit.logo && !this.logoFile); + } + + logoChanged(fileList: FileList) { + if (fileList && fileList.length > 0) { + this.logoFile = fileList[0]; + } + } + + removeLogo() { + this.form.get('logo').setValue(null); + } +} diff --git a/src/app/components/credits/list/credits.component.html b/src/app/components/credits/list/credits.component.html new file mode 100644 index 0000000000000000000000000000000000000000..cd73d71338e91035a7536f3749855c9e8a6f15a0 --- /dev/null +++ b/src/app/components/credits/list/credits.component.html @@ -0,0 +1,97 @@ +<div class="section page-container"> + <app-page-header [pageInfo]="pageHeaderInfo" [hideBackButton]="true"></app-page-header> + <div class="add-item-link has-text-right"> + <a class="button button-gl" [routerLink]="['new']"> + Ajouter + </a> + </div> + <div class="table entity-list-table" *ngIf="credits"> + <div class="header columns is-marginless"> + <div class="column is-2"> + <span (click)="sortBy('name')" class="is-sortable"> + <span class="sort-icons"> + <span class="icon"> + <i class="fas fa-sort-up" + [ngClass]="{'icon-red': sortOptions.value === 'name' && sortOptions.order === 'desc'}"></i> + </span> + <span class="icon"> + <i class="fas fa-sort-down" + [ngClass]="{'icon-red': sortOptions.value === 'name' && sortOptions.order === 'asc'}"></i> + </span> + </span> + <span class="column-title" [ngClass]="{'active': sortOptions.value === name}">Nom</span> + </span> + </div> + <div class="column is-1 has-text-centered"> + <span class="column-title">Publié</span> + </div> + <div class="column is-1 has-text-centered"> + <span class="column-title">Logo</span> + </div> + <div class="column is-4"> + <span (click)="sortBy('description')" class="is-sortable"> + <span class="sort-icons"> + <span class="icon"> + <i class="fas fa-sort-up" + [ngClass]="{'icon-red': sortOptions.value === 'description' && sortOptions.order === 'desc'}"></i> + </span> + <span class="icon"> + <i class="fas fa-sort-down" + [ngClass]="{'icon-red': sortOptions.value === 'description' && sortOptions.order === 'asc'}"></i> + </span> + </span> + <span class="column-title">Description</span> + </span> + </div> + <div class="column is-3"> + <span class="column-title">Liens</span> + </div> + <div class="column is-1 has-text-centered"> + <span class="column-title">Actions</span> + </div> + </div> + <div class="data-list"> + <div class="data columns is-multiline is-vcentered is-marginless" + *ngFor="let credits of credits; let i=index; let odd=odd; let even=even;" [ngClass]="{ odd: odd, even: even }"> + <div class="column is-2"> + <span>{{ credits.name}}</span> + </div> + <div class="column is-1 has-text-centered"> + <span class="icon has-text-success" *ngIf="credits.published"> + <i class="far fa-check-circle"></i> + </span> + <span class="icon has-text-danger" *ngIf="!credits.published"> + <i class="far fa-times-circle"></i> + </span> + </div> + <div class="column is-1 has-text-centered"> + <img class="entity-logo-in-list" [src]="credits.logo" alt=""> + </div> + <div class="column is-4"> + <span>{{ credits.description | slice:0:200}}...</span> + </div> + <div class="column is-3"> + <p *ngFor="let link of credits.links"> + <a href="{{ link.url }}" target="_blank">{{link.url}}</a> + </p> + </div> + <div class="column is-1 has-text-centered actions"> + <app-crud-buttons [id]="credits.id" (delete)="displayDeletePopup($event)"></app-crud-buttons> + </div> + + </div> + </div> + <div class="columns is-marginless paginator"> + <div class="column"> + <app-paginator *ngIf="paginator.length > 0" [length]="paginator.length" [pageSize]="paginator.limit" + [pageSizeOptions]="paginator.pageSizeOptions" [pageIndex]="paginator.pageIndex" [pagesToShow]="5" + [showFirstLastButtons]="true" (page)="changePagination($event)" (pageSizeChanged)="changePageSize($event)"> + </app-paginator> + </div> + </div> + </div> +</div> + +<app-confirmation-modal (cancel)="objectToBeDeletedId=null" (continue)="deleteCredit()" [texts]="deleteModalTexts" + [isOpened]="objectToBeDeletedId !== null"> +</app-confirmation-modal> \ No newline at end of file diff --git a/src/app/components/credits/list/credits.component.scss b/src/app/components/credits/list/credits.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..d1ab57677f61171791f67b163f6b933ef1dd4a41 --- /dev/null +++ b/src/app/components/credits/list/credits.component.scss @@ -0,0 +1,3 @@ +img { + max-width: 100px; +} diff --git a/src/app/components/credits/list/credits.component.ts b/src/app/components/credits/list/credits.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..d417e76fe35c8d1eba3c39f18c7f628a89c58bb9 --- /dev/null +++ b/src/app/components/credits/list/credits.component.ts @@ -0,0 +1,142 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { PaginatorOptions } from 'src/app/models/paginator-options.model'; +import { IPageHeaderInfo } from '../../../models/page.model'; +import { NotificationService, CreditService } from '../../../services'; +import { Credit, CreditRO } from '../../../models/credit.model'; + +@Component({ + selector: 'app-credits', + templateUrl: './credits.component.html', + styleUrls: ['./credits.component.scss'], +}) +export class CreditsComponent implements OnInit, OnDestroy { + + pageHeaderInfo: IPageHeaderInfo = { + title: '', + }; + objectToBeDeletedId = null; + deleteModalTexts = { + main: 'Si vous poursuivez, le crédit sera définitivement supprimé.', + cancel: 'Annuler', + continue: 'Supprimer', + }; + credits: Credit[] = []; + searchChangeSub: Subscription; + + // Paginator options + paginator: PaginatorOptions; + + sortValue: string; + + totalElement: number; + pageSize = 10; + pageSizeOptions = [5, 10, 25, 100]; + filters = { + name: '', + }; + where = {}; + + constructor( + private _creditService: CreditService, + private _notificationService: NotificationService, + ) { + this.paginator = { + pageIndex: this._creditService.pageNumber, + length: 0, + limit: this._creditService.limit, + pageSizeOptions: [5, 10, 20], + }; + } + + ngOnInit(): void { + this._creditService.sortOptions = { + value: 'name', + order: 'asc', + }; + this.search(); + + this.searchChangeSub = this._creditService.searchChange$.subscribe( + () => { + this.search(); + }, + ); + } + + private search() { + this._creditService.getCredits() + .subscribe( + (items: CreditRO) => { + this.credits = items.credits; + this.totalElement = items.totalCount; + + this.pageHeaderInfo.title = this.totalElement > 1 ? + `${this.totalElement} crédits trouvés` : `${this.totalElement} crédit trouvé`; + + this.paginator.limit = this._creditService.limit; + this.paginator.pageIndex = this._creditService.pageNumber; + this.paginator.length = items.totalCount; + }, + () => { + this.pageHeaderInfo.title = '0 crédit trouvé'; + this._notificationService.notify({ + type: 'error', + message: 'Une erreur est survenue lors du chargement des crédits.', + }); + }, + ); + } + + // When pagination is changed by user, we update datasetList with new pagination options + changePagination(pageIndex) { + this._creditService.paginationChanged(this.paginator.limit, pageIndex); + } + + changePageSize(pageSize) { + this._creditService.paginationChanged(pageSize, 1); + } + + sortBy(key: string) { + if (this._creditService.sortOptions.value === key) { + this._creditService.reverseSortOrder(); + } else { + this._creditService.sortOptions.value = key; + this._creditService.sortOptions.order = 'asc'; + } + this.search(); + } + + get sortOptions() { + return this._creditService.sortOptions; + } + + displayDeletePopup(id) { + this.objectToBeDeletedId = id; + } + + deleteCredit() { + this._creditService.delete(this.objectToBeDeletedId).subscribe( + () => { + this._notificationService.notify({ + type: 'success', + message: 'Le crédit a été supprimé avec succès.', + }); + this._creditService.pageNumber = 1; + this.search(); + }, + () => { + this._notificationService.notify({ + type: 'error', + message: 'Une erreur est survenue lors de la suppression du crédit.', + }); + }, + () => { + this.objectToBeDeletedId = null; + }, + ); + } + + ngOnDestroy() { + this.searchChangeSub.unsubscribe(); + } +} diff --git a/src/app/components/formats/edit/format-form.component.ts b/src/app/components/formats/edit/format-form.component.ts index cd1a1d6d67dbb99887d6434e6e58f933e098cc05..e494f7ab5ae5361b6d0c1814408578d886fb707c 100644 --- a/src/app/components/formats/edit/format-form.component.ts +++ b/src/app/components/formats/edit/format-form.component.ts @@ -3,7 +3,7 @@ import { ActivatedRoute, ParamMap, Router } from '@angular/router'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { filter, switchMap } from 'rxjs/operators'; import { Format } from 'src/app/models/format.model'; -import { FormatService } from 'src/app/services'; +import { FormatService, NotificationService } from 'src/app/services'; @Component({ selector: 'app-format-form', @@ -21,6 +21,7 @@ export class FormatFormComponent implements OnInit { private _route: ActivatedRoute, private _router: Router, private _fb: FormBuilder, + private _notificationService: NotificationService, ) { } @@ -52,15 +53,39 @@ export class FormatFormComponent implements OnInit { onSubmit() { if (!this.formInvalid) { this.format = new Format(this.form.value); - this._formatService.replaceOrCreate(this.format) - .subscribe( + if (this.format.id) { + this._formatService.update(this.format).subscribe( + (formatUpdate) => { + this._notificationService.notify({ + type: 'success', + message: 'Le format a bien été mis à jour.', + }); + this._router.navigate(['/formats', formatUpdate.id]); + }, + () => { + this._notificationService.notify({ + type: 'error', + message: 'Une erreur est survenue lors de la mise à jour du format.', + }); + }, + ); + } else { + this._formatService.create(this.format).subscribe( (formatCreated) => { + this._notificationService.notify({ + type: 'success', + message: 'Le format a bien été créé.', + }); this._router.navigate(['/formats', formatCreated.id]); }, - (err) => { - alert(err.message); + () => { + this._notificationService.notify({ + type: 'error', + message: 'Une erreur est survenue lors de la création du format.', + }); }, ); + } } } diff --git a/src/app/components/formats/list/formats.component.html b/src/app/components/formats/list/formats.component.html index ff125702863e6c03ef08f1cf50fd1958f2101b5a..987f71d2484d077424d82ae18e4de0f52e8596c8 100644 --- a/src/app/components/formats/list/formats.component.html +++ b/src/app/components/formats/list/formats.component.html @@ -1,11 +1,11 @@ -<section class="section page-container" *ngIf="formats"> +<section class="section page-container"> <app-page-header [pageInfo]="pageHeaderInfo" [hideBackButton]="true"></app-page-header> <div class="add-item-link has-text-right"> <a class="button button-gl" [routerLink]="['new']"> Ajouter </a> </div> - <div class="table entity-list-table"> + <div class="table entity-list-table" *ngIf="formats"> <div class="header columns is-marginless"> <div class="column is-2"> <span (click)="sortBy('name')" class="is-sortable"> @@ -84,4 +84,8 @@ </div> </div> </div> -</section> \ No newline at end of file +</section> + +<app-confirmation-modal (cancel)="objectToBeDeletedId=null" (continue)="deleteFormat()" [texts]="deleteModalTexts" + [isOpened]="objectToBeDeletedId !== null"> +</app-confirmation-modal> \ No newline at end of file diff --git a/src/app/components/formats/list/formats.component.ts b/src/app/components/formats/list/formats.component.ts index 950708a57bc2a87bbf015edbf48ff40fb99248a6..93c3b7ad6b10e15a7e6fc0ce399497aab3ff7bd4 100644 --- a/src/app/components/formats/list/formats.component.ts +++ b/src/app/components/formats/list/formats.component.ts @@ -1,21 +1,28 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, OnDestroy } from '@angular/core'; import { FormatService } from 'src/app/services/format.service'; import { Subscription } from 'rxjs'; import { PaginatorOptions } from 'src/app/models/paginator-options.model'; import { Format, FormatRO } from 'src/app/models/format.model'; import { IPageHeaderInfo } from '../../../models/page.model'; +import { NotificationService } from '../../../services'; @Component({ selector: 'app-formats', templateUrl: './formats.component.html', styleUrls: ['./formats.component.scss'], }) -export class FormatsComponent implements OnInit { +export class FormatsComponent implements OnInit, OnDestroy { pageHeaderInfo: IPageHeaderInfo = { title: '', }; - formats: Format[]; + objectToBeDeletedId = null; + deleteModalTexts = { + main: 'Si vous poursuivez, le format sera définitivement supprimé.', + cancel: 'Annuler', + continue: 'Supprimer', + }; + formats: Format[] = []; searchChangeSub: Subscription; // Paginator options @@ -32,24 +39,25 @@ export class FormatsComponent implements OnInit { where = {}; constructor( - private formatsService: FormatService, + private _formatService: FormatService, + private _notificationService: NotificationService, ) { this.paginator = { - pageIndex: this.formatsService.pageNumber, + pageIndex: this._formatService.pageNumber, length: 0, - limit: this.formatsService.limit, + limit: this._formatService.limit, pageSizeOptions: [5, 10, 20], }; } ngOnInit(): void { - this.formatsService.sortOptions = { + this._formatService.sortOptions = { value: 'name', order: 'asc', }; this.search(); - this.searchChangeSub = this.formatsService.searchChange$.subscribe( + this.searchChangeSub = this._formatService.searchChange$.subscribe( () => { this.search(); }, @@ -57,49 +65,81 @@ export class FormatsComponent implements OnInit { } private search() { - this.formatsService.getFormats() - .subscribe((items: FormatRO) => { - this.formats = items.formats; - this.totalElement = items.totalCount; - - this.pageHeaderInfo.title = `${this.totalElement} formats trouvés`; - - this.paginator.limit = this.formatsService.limit; - this.paginator.pageIndex = this.formatsService.pageNumber; - this.paginator.length = items.totalCount; - }); + this._formatService.getFormats() + .subscribe( + (items: FormatRO) => { + this.formats = items.formats; + this.totalElement = items.totalCount; + + this.pageHeaderInfo.title = `${this.totalElement} formats trouvés`; + this.pageHeaderInfo.title = this.totalElement > 1 ? + `${this.totalElement} formats trouvés` : + `${this.totalElement} format trouvé`; + + this.paginator.limit = this._formatService.limit; + this.paginator.pageIndex = this._formatService.pageNumber; + this.paginator.length = items.totalCount; + }, + () => { + this.pageHeaderInfo.title = '0 format trouvé'; + this._notificationService.notify({ + type: 'error', + message: 'Une erreur est survenue lors du chargement des formats.', + }); + }, + ); } // When pagination is changed by user, we update datasetList with new pagination options changePagination(pageIndex) { - this.formatsService.paginationChanged(this.paginator.limit, pageIndex); + this._formatService.paginationChanged(this.paginator.limit, pageIndex); } changePageSize(pageSize) { - this.formatsService.paginationChanged(pageSize, 1); + this._formatService.paginationChanged(pageSize, 1); } sortBy(key: string) { - if (this.formatsService.sortOptions.value === key) { - this.formatsService.reverseSortOrder(); + if (this._formatService.sortOptions.value === key) { + this._formatService.reverseSortOrder(); } else { - this.formatsService.sortOptions.value = key; - this.formatsService.sortOptions.order = 'asc'; + this._formatService.sortOptions.value = key; + this._formatService.sortOptions.order = 'asc'; } this.search(); } get sortOptions() { - return this.formatsService.sortOptions; + return this._formatService.sortOptions; + } + + displayDeletePopup(id) { + this.objectToBeDeletedId = id; } - displayDeletePopup(formatId) { - const pop = confirm('Etes vous sûr de vouloir supprimer ce format ?'); - if (pop) { - this.formatsService.delete(formatId).subscribe(() => { - this.formatsService.pageNumber = 1; + deleteFormat() { + this._formatService.delete(this.objectToBeDeletedId).subscribe( + () => { + this._notificationService.notify({ + type: 'success', + message: 'Le format a été supprimé avec succès.', + }); + this._formatService.pageNumber = 1; this.search(); - }); - } + }, + () => { + this._notificationService.notify({ + type: 'error', + message: 'Une erreur est survenue lors de la suppression du format.', + }); + }, + () => { + this.objectToBeDeletedId = null; + }, + ); + } + + ngOnDestroy() { + this.searchChangeSub.unsubscribe(); } } diff --git a/src/app/components/image-upload/image-upload.component.scss b/src/app/components/image-upload/image-upload.component.scss index d4a9d5613206de3aa75a786ce5adcc2bb26cfa48..b0d727c7ce9831cd3f3e45564d4111d7744d33e5 100644 --- a/src/app/components/image-upload/image-upload.component.scss +++ b/src/app/components/image-upload/image-upload.component.scss @@ -2,6 +2,10 @@ display: inline-block; position: relative; max-height: 5rem; + + img { + max-height: 5rem; + } } button { diff --git a/src/app/components/index.ts b/src/app/components/index.ts index 90f5e966751673233933f0a7cb01fc0caef66634..24f8f061a7ae06f7172508b4591f8816c511090b 100644 --- a/src/app/components/index.ts +++ b/src/app/components/index.ts @@ -13,7 +13,32 @@ import { FormatDetailComponent } from './formats/detail/format-detail.component' import { FormatFormComponent } from './formats/edit/format-form.component'; import { ImageUploadComponent } from './image-upload/image-upload.component'; import { NotificationsComponent } from './notifications/notifications.component'; +import { LogsInfoComponent } from './logs-dashboard/report/logs-info/logs-info.component'; +import { LogsErrorComponent } from './logs-dashboard/report/logs-error/logs-error.component'; +import { LogsGraphComponent } from './logs-dashboard/report/logs-graph/logs-graph.component'; +import { LogsSessionsComponent } from './logs-dashboard/logs-sessions/logs-sessions.component'; +import { LogsSlugsComponent } from './logs-dashboard/logs-slugs/logs-slugs.component'; +import { FilterPipe } from './logs-dashboard/logs-slugs/filter.pipe'; +import { LogsPreReportComponent } from './logs-dashboard/report/logs-pre-report/logs-pre-report.component'; +import { LogsReportComponent } from './logs-dashboard/report/logs-report/logs-report.component'; +import { LogsHomeComponent } from './logs-dashboard/logs-home/logs-home.component'; import { PageHeaderComponent } from './page-header/page-header.component'; +import { ChangelogComponent } from './changelog/list/changelog.component'; +import { ChangelogDetailComponent } from './changelog/detail/changelog-detail.component'; +import { ChangelogFormComponent } from './changelog/edit/changelog-form.component'; +import { ConfirmationModalComponent } from './confirmation-modal/confirmation-modal.component'; +import { CreditDetailComponent } from './credits/detail/credit-detail.component'; +import { CreditFormComponent } from './credits/edit/credit-form.component'; +import { CreditsComponent } from './credits/list/credits.component'; +import { ReusesComponent } from './reuses/list/reuses.component'; +import { ReuseFormComponent } from './reuses/edit/reuse-form.component'; +import { ReuseDetailComponent } from './reuses/detail/reuse-detail.component'; +import { ProjectionsComponent } from './projections/list/projections.component'; +import { ProjectionDetailComponent } from './projections/detail/projection-detail.component'; +import { ProjectionFormComponent } from './projections/edit/projection-form.component'; +import { MediaComponent } from './media/list/media.component'; +import { MediaFormComponent } from './media/edit/media-form.component'; +import { MediaDetailComponent } from './media/detail/media-detail.component'; export { MenuComponent, @@ -31,7 +56,32 @@ export { FormatFormComponent, ImageUploadComponent, NotificationsComponent, + LogsInfoComponent, + LogsErrorComponent, + LogsGraphComponent, + LogsSessionsComponent, + LogsSlugsComponent, + LogsPreReportComponent, + LogsReportComponent, + LogsHomeComponent, PageHeaderComponent, + FilterPipe, + ChangelogComponent, + ChangelogDetailComponent, + ChangelogFormComponent, + ConfirmationModalComponent, + CreditDetailComponent, + CreditFormComponent, + CreditsComponent, + ReuseDetailComponent, + ReuseFormComponent, + ReusesComponent, + ProjectionsComponent, + ProjectionDetailComponent, + ProjectionFormComponent, + MediaComponent, + MediaFormComponent, + MediaDetailComponent, }; // tslint:disable-next-line:variable-name @@ -51,5 +101,29 @@ export const AppComponents = [ FormatFormComponent, ImageUploadComponent, NotificationsComponent, + LogsInfoComponent, + LogsErrorComponent, + LogsGraphComponent, + LogsSessionsComponent, + LogsSlugsComponent, + LogsPreReportComponent, + LogsReportComponent, + LogsHomeComponent, PageHeaderComponent, + ChangelogComponent, + ChangelogDetailComponent, + ChangelogFormComponent, + ConfirmationModalComponent, + CreditDetailComponent, + CreditFormComponent, + CreditsComponent, + ReuseDetailComponent, + ReuseFormComponent, + ReusesComponent, + ProjectionsComponent, + ProjectionDetailComponent, + ProjectionFormComponent, + MediaComponent, + MediaFormComponent, + MediaDetailComponent, ]; diff --git a/src/app/components/logs-dashboard/logs-home/logs-home.component.html b/src/app/components/logs-dashboard/logs-home/logs-home.component.html new file mode 100644 index 0000000000000000000000000000000000000000..fee2694bc8bd11d8afd6c89dd221c02709578098 --- /dev/null +++ b/src/app/components/logs-dashboard/logs-home/logs-home.component.html @@ -0,0 +1,56 @@ +<div class="section page-container"> + <app-page-header [pageInfo]="pageHeaderInfo" [hideBackButton]="true"></app-page-header> + <div *ngIf="isRestheartDown"> + <h3> {{serverDownMessage}}</h3> + </div> + <div *ngIf="!isRestheartDown"> + <div class="columns is-centered is-marginless"> + + + <div class="input-field"> + <form> + <div class="table"> + <div class="data-list"> + <div class="data columns is-multiline is-vcentered is-marginless"> + <!-- <div class="column is-12 has-text-left is marginless"> + <a (click)="information()">Information</a> + </div> --> + <div class="column is-5 has-text-left"> + <label class="label"> Slug/Uuid </label> + <input class="input" type="text" size="45" name="slug" [(ngModel)]="slug" list="slugsList"> + </div> + <div class="column is-5 has-text-left"> + <label class="label"> Session Id </label> + <input class="input" type="text" size="45" name="sessionId" [(ngModel)]="sessionId" list="sessionsList"> + </div> + <div class="column is-2 has-text-left"> + <label class="label"> </label> + + <a class="button button-gl" (click)="trimAndNavigate(slug, sessionId)"> + <i class="fas fa-search"></i> + </a> + </div> + </div> + </div> + </div> + </form> + <br> + + <!-- Tabs --> + <ul class="tabs-container"> + <li *ngFor="let tab of tabs" (click)="tabsToggler(tab.name)" [ngClass]="{'is-active':tab.isActive}"> + <span class="tab-label">{{ tab.name }}</span> + </li> + </ul> + </div> + </div> + <div class="columns is-centered is-marginless"> + <div class="column has-text-centered is-marginless"> + <app-logs-sessions [hidden]="isToggled" [sessionsList]="sessionsList" [childNbSessions]="nbSessionId"> + </app-logs-sessions> + <app-logs-slugs [hidden]="!isToggled" [slugsList]="slugsList" [childNbSlugs]="nbSlug"></app-logs-slugs> + </div> + </div> + </div> +</div> +<app-confirmation-modal [isOpened]="isInfoOpened" [texts]= "texts" (continue)= "isInfoOpened"></app-confirmation-modal> \ No newline at end of file diff --git a/src/app/components/logs-dashboard/logs-home/logs-home.component.scss b/src/app/components/logs-dashboard/logs-home/logs-home.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..5ce2ea74326a2bb2f4f2f4ff033dca6dd38979e9 --- /dev/null +++ b/src/app/components/logs-dashboard/logs-home/logs-home.component.scss @@ -0,0 +1,116 @@ +@import "../../../../scss/variables.scss"; +@import "../../../../../node_modules/bulma/sass/utilities/_all.sass"; + +figure { + text-align: center; + } + figure img{ + max-width: 9.3rem; + display: inline-block; + margin-top: 1.25rem; + } + + .card-header-title { + justify-content: center; + } + + .mini-log-card{ + margin-top: 1.25rem; + padding: 1.25rem; + background-color: yellow; + + } + .input-field{ + width: 200%; + margin-top: 1.25rem; + } + + .hidden { + display: none; + } + .icon { + cursor: pointer; + &:hover { + .fa-plus { + color: lightblue; + } + .fa-trash { + color: #d5232a; + } + } + } + .tabs-container { + display: flex; + align-content: stretch; + flex-wrap: wrap; + border-bottom: 1px solid $grey-super-light-color; + + li { + display: flex; + align-items: center; + padding-right: 2rem; + padding-bottom: 0.4rem; + padding-top: 0.4rem; + cursor: pointer; + position: relative; + margin-bottom: 1px; + width: 50%; + + @media screen and (min-width: $tablet) { + width: 33%; + padding-right: 3rem; + } + + @media screen and (min-width: $desktop) { + width: 20%; + } + } + + li:last-child { + padding-right: 0; + } + + .tab-label { + font-size: 1rem; + font-weight: 500; + color: $brand-color; + } + + li:hover { + .tab-label { + font-weight: 700; + } + } + + li:focus{ + .tab-label { + color: $tomato-color; + font-weight: 700; + } + } + li.is-active { + .tab-label { + color: $tomato-color; + font-weight: 600; + } + } + + li::after { + content: ''; + display: block; + height: .1rem; + background: $tomato-color; + width: 0; + position: absolute; + bottom: -.1rem; + } + + li.is-active::after { + transition: width 0.3s; + width: 100%; + } + + .button-div{ + padding: 1.25rem ; + } +} diff --git a/src/app/components/logs-dashboard/logs-home/logs-home.component.ts b/src/app/components/logs-dashboard/logs-home/logs-home.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..995a53f1b682a3f8a2c39a838bbcbce52a4208bd --- /dev/null +++ b/src/app/components/logs-dashboard/logs-home/logs-home.component.ts @@ -0,0 +1,164 @@ +import { Component, OnInit } from '@angular/core'; +import { DataLogsService } from 'src/app/services/data-logs.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ILogs } from 'src/app/models/logs.model'; +import { ISimpleTab } from 'src/app/models/basic-tabs.model'; +import { IPageHeaderInfo } from 'src/app/models/page.model'; +import { NotificationService } from 'src/app/services'; +@Component({ + selector: 'app-logs-home', + templateUrl: './logs-home.component.html', + styleUrls: ['./logs-home.component.scss'], +}) +export class LogsHomeComponent implements OnInit { + pageHeaderInfo: IPageHeaderInfo = { + title: "Logs d'indexation: recherche", + }; + isUuid: boolean = false; + slugsList: any; + serverDownMessage: string = ''; + sessionsList: any; + nbSessionId: number; + nbUuid: number; + isInfoOpened: boolean = false; + texts: Object = { + main : 'hello', + cancel: 'end', + continue: 'ok', + }; + nbSlug: number; + isToggled: boolean = true; + data: any = []; + uuid: string = 'c1b069ca-181d-4265-9838-8d182f207bd3'; + sessionId: string = '372b59a4-72b0-4a3a-b518-843e516da0cd'; + slug: any = 'arbres-alignement-metropole-lyon'; + allStepsData: any = []; + allFields: any = []; + isRestheartDown: boolean = true; + tabs: ISimpleTab[] = [{ + name: 'Slug', + isActive: true}, + { + name: 'Session', + isActive: false}, + ]; + constructor( + private _dataLogsService: DataLogsService, + private _notificationService: NotificationService, + private _route: ActivatedRoute, + private _router: Router, + ) { } + + ngOnInit() { + this.getAllUniqueFields(); + } + + getAllUniqueFields() { + this._dataLogsService.getAllUniqueFields().subscribe( + (result) => { + this.allFields = result[0]; + this.nbSessionId = result[0]['session_id_list'].length; + this.nbUuid = result[0]['uuid_list'].length; + this.nbSlug = result[0]['slug_list'].length; + this.slugsList = result[0]['slug_list']; + this.sessionsList = result[0]['session_id_list']; + this.isRestheartDown = false; + }, + (error) => { + this._notificationService.notify( + { + type: 'error', + message: "Le serveur restheart n'est pas disponible.", + }); + this.allFields = []; + this.nbSessionId = 0; + this.nbUuid = 0; + this.nbSlug = 0; + this.slugsList = []; + this.sessionsList = []; + this.serverDownMessage = "Le serveur restheart n'est pas disponible."; + }); + } + tabsToggler(tabName) { + if (tabName === 'Session') { + this.isToggled = false; + this.tabs[0].isActive = false; + this.tabs[1].isActive = true; + } + if (tabName === 'Slug') { + this.isToggled = true; + this.tabs[0].isActive = true; + this.tabs[1].isActive = false; + } + } + + information() { + this.isInfoOpened = true; + } + + trimAndNavigate(slugOrUuid, sessionId) { + const trimmedSlugOrUuid = slugOrUuid.trim(); + this.sessionId = sessionId.trim(); + this.uuidDetectorAndNavigate(trimmedSlugOrUuid); + } + + uuidDetectorAndNavigate(slugOrUuid) { + const regexp = new RegExp('^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'); + this.isUuid = regexp.test(slugOrUuid); + if (this.isUuid) { + this.convertUuidToSlugAndNavigate(slugOrUuid); + } else { + this.mightyGeneralRouter(slugOrUuid, this.sessionId); + } + } + + convertUuidToSlugAndNavigate(uuidInput) { + this._dataLogsService.getSlugFromUuid(uuidInput).subscribe( + (result) => { + this.slug = result; + this.mightyGeneralRouter(this.slug, this.sessionId); + }, + ); + } + + mightyGeneralRouter(slug, sessionId) { + let slugUuidIsEmpty = true; + let sessionIdIsEmpty = true; + + if (slug !== '') { + slugUuidIsEmpty = false; + } + if (sessionId !== '') { + sessionIdIsEmpty = false; + } + + if (slugUuidIsEmpty && !sessionIdIsEmpty) { + this.goToSessionPage(sessionId); + } + if (!slugUuidIsEmpty && sessionIdIsEmpty) { + this.goToSlugPage(slug); + } + if (slugUuidIsEmpty && sessionIdIsEmpty) { + return; + } + if (!slugUuidIsEmpty && !sessionIdIsEmpty) { + this.goToFinalReportPage(slug, sessionId); + } + + } + + goToSessionPage(sessionId) { + this._router.navigate([this._route, 'preReport', 'session', sessionId]); + + } + + goToSlugPage(slug) { + this._router.navigate([this._route, 'preReport', 'slug', slug]); + + } + + goToFinalReportPage (slug, sessionId) { + this._router.navigate([this._route, 'report', slug, sessionId, 'home']); + } + +} diff --git a/src/app/components/logs-dashboard/logs-sessions/logs-sessions.component.html b/src/app/components/logs-dashboard/logs-sessions/logs-sessions.component.html new file mode 100644 index 0000000000000000000000000000000000000000..39c3dd673866fe866c14a822a64f6e24414d1013 --- /dev/null +++ b/src/app/components/logs-dashboard/logs-sessions/logs-sessions.component.html @@ -0,0 +1,154 @@ +<div class="input-field is-centered"> + <div class="columns is-5 is-vcentered"> + <div class="column is-5 has-text-left"> + <label class="label"> Recherche par session: </label> + <input class="input" type="text" [(ngModel)]="foundSession" (keyup)="getAllInfoForOneSession($event)" + id="foundSession" list="sessionsList" /> + <datalist id="sessionsList"> + <option *ngFor="let session of sessionsList" [value]="session"> {{session}}</option> + </datalist> + </div> + </div> +</div> + +<div class="columns is-centered is-marginless"> + <div class="column is-6 has-text-left"> + <p>{{ childNbSessions }} session(s) dans la base de données</p> + </div> + <div class="column is-6 has-text-left"> + <p>Pour chaque session, les valeurs affichées concernent le slug le plus récent.</p> + </div> +</div> + +<div class="table entity-list-table"> + <div class="header columns is-marginless"> + <div class="column is-4 has-text-centered"> + <span class="is-sortable"> + <span class="column-title">SessionId</span> + <p></p> + </span> + </div> + <div class="column is-1 has-text-left"> + <span class="is-sortable"> +<!-- <span class="sort-icons">--> +<!-- <span class="icon">--> +<!-- <i class="fas fa-sort-up"></i>--> +<!-- </span>--> +<!-- <span class="icon">--> +<!-- <i class="fas fa-sort-down"></i>--> +<!-- </span>--> +<!-- </span>--> + <span class="column-title">Slugs</span> + </span> + </div> + <div class="column is-2 has-text-left"> + <span class="is-sortable"> + <span class="sort-icons"> + <span class="icon"> + <i class="fas fa-sort-up" + (click)="sortInfos('_id.slug', -1, 'latest.completionDate', -1, this.childNbSessions, 1);"></i> + </span> + <span class="icon"> + <i class="fas fa-sort-down" + (click)="sortInfos('_id.slug', -1, 'latest.completionDate', 1, this.childNbSessions, 1);"></i> + </span> + </span> + <span class="column-title">Dernière date d'exécution</span> + </span> + </div> + <div class="column is-1 has-text-left"> + <span class="is-sortable"> +<!-- <span class="sort-icons">--> +<!-- <span class="icon">--> +<!-- <i class="fas fa-sort-up"--> +<!-- (click)="sortInfos('_id.slug', -1, 'latest.duration', -1, this.childNbSessions, 1);"></i>--> +<!-- </span>--> +<!-- <span class="icon">--> +<!-- <i class="fas fa-sort-down"--> +<!-- (click)="sortInfos('_id.slug', -1, 'latest.duration', 1, this.childNbSessions, 1);"></i>--> +<!-- </span>--> +<!-- </span>--> + <span class="column-title">Durée</span> + </span> + </div> + <div class="column is-1 has-text-left"> + <span class="is-sortable"> + <span class="sort-icons"> + <span class="icon"> + <i class="fas fa-sort-up" + (click)="sortInfos('_id.slug', -1, 'latest.count.INFO', -1, this.childNbSessions, 1);"></i> + </span> + <span class="icon"> + <i class="fas fa-sort-down" + (click)="sortInfos('_id.slug', -1, 'latest.count.INFO', 1, this.childNbSessions, 1);"></i> + </span> + </span> + <span class="column-title">Infos</span> + </span> + </div> + <div class="column is-1 has-text-left"> + <span class="is-sortable"> + <span class="sort-icons"> + <span class="icon"> + <i class="fas fa-sort-up" + (click)="sortInfos('_id.slug', -1, 'latest.count.ERROR', -1, this.childNbSessions, 1);"></i> + </span> + <span class="icon"> + <i class="fas fa-sort-down" + (click)="sortInfos('_id.slug', -1, 'latest.count.ERROR', 1, this.childNbSessions, 1);"></i> + </span> + </span> + <span class="column-title">Erreurs</span> + </span> + </div> + <div class="column is-1 has-text-left"> + <span class="column-title"></span> + </div> + </div> + <div class="data-list"> + <div class="data columns is-multiline is-vcentered is-marginless" + *ngFor="let session of allSessionsInfoList; let i=index; let odd=odd; let even=even;" + [ngClass]="{ odd: odd, even: even }"> + <div class="column is-4 has-text-left"> + {{ session._id.session_id }} + </div> + <div class="column is-1 has-text-left"> + {{ session.all.length }} + </div> + <div class="column is-2 has-text-left"> + {{ session.latest.completionDate.$date | date: 'EE dd/MM/yyyy hh:mm:ss'}} + </div> + <div class="column is-1 has-text-left"> + <span class="column-title"> + {{ session.latest.duration.hours }}: + {{ session.latest.duration.minutes }}: + {{ session.latest.duration.seconds }}</span> + </div> + <div class="column is-1 has-text-left actions"> + {{ session.latest.count.INFO ? session.latest.count.INFO : 0 }} + </div> + <div class="column is-1 has-text-left actions"> + {{ session.latest.count.ERROR ? +session.latest.count.ERROR:'Non'}} + </div> + <div class="column is-1 actions"> + <a class="button button-gl " [routerLink]="['preReport','session', session._id.session_id]"> + <i class="fas fa-eye"></i> + </a> + </div> + </div> + </div> +</div> +<div class="columns is-marginless"> + <div class="column is-10"> + <app-paginator [length]="paginator.length" [pageSize]="paginator.limit" + [pageSizeOptions]="paginator.pageSizeOptions" [pageIndex]="paginator.pageIndex" [pagesToShow]="1" + [showFirstLastButtons]="true" (page)="changePagination($event)" (pageSizeChanged)="changePageSize($event)"> + </app-paginator> + </div> + <div class="column is-1 is-vcentered"> + <a class="button button-gl is-left" (click)="reset('_id.slug', -1, + 'latest.completionDate', -1, this.childNbSessions, 1)"> + <p class="bold-white">Reset</p> + </a> + </div> +</div> diff --git a/src/app/components/logs-dashboard/logs-sessions/logs-sessions.component.scss b/src/app/components/logs-dashboard/logs-sessions/logs-sessions.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..22f7b197fd884a7acc1e0611cd15c065f5af62dc --- /dev/null +++ b/src/app/components/logs-dashboard/logs-sessions/logs-sessions.component.scss @@ -0,0 +1,12 @@ + +@import "../../../../scss/variables.scss"; +@import "../../../../../node_modules/bulma/sass/utilities/_all.sass"; + +.small-text{ + font-size:0.8em; +} + +.bold-white{ + color: white; + font-weight: bold; +} \ No newline at end of file diff --git a/src/app/components/logs-dashboard/logs-sessions/logs-sessions.component.ts b/src/app/components/logs-dashboard/logs-sessions/logs-sessions.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..af1d160f4c6c4b3b4bdceb2d66721da5f8bc82c3 --- /dev/null +++ b/src/app/components/logs-dashboard/logs-sessions/logs-sessions.component.ts @@ -0,0 +1,134 @@ +import { Component, OnChanges, SimpleChanges, Input, OnDestroy } from '@angular/core'; +import { DataLogsService } from 'src/app/services/data-logs.service'; +import { PaginatorOptions } from 'src/app/models/paginator-options.model'; +import { Subscription } from 'rxjs'; + +@Component({ + selector: 'app-logs-sessions', + templateUrl: './logs-sessions.component.html', + styleUrls: ['./logs-sessions.component.scss'], +}) +export class LogsSessionsComponent implements OnChanges, OnDestroy { + @Input() childNbSessions: number; + @Input() sessionsList: any; + + sessionChangeSub: Subscription; + nbOfItemsPerPage: number = 10; + nbSessions: number = 0; + foundSession: string = ''; + allSessionsIdInfo: any = []; + allSessionsInfoList: any = []; + paginator: PaginatorOptions; + urlCode: string = 'sessionUrl'; + sortFilter1:string = '_id'; + orderFilter1: number = -1; + isResearchFieldActive: boolean = false; + sortFilter2:string = '_id'; + orderFilter2: number = -1; + constructor( + private _dataLogsService: DataLogsService, + ) { + this.paginator = { + pageIndex: this._dataLogsService.pageNumber, + length: 1, + limit: this._dataLogsService.limit, + pageSizeOptions: [5, 10, 20], + }; + } + + ngOnChanges(changes: SimpleChanges): void { + this.sessionChangeSub = this._dataLogsService.sessionLogChange$.subscribe( + () => { + if (this.isResearchFieldActive) { + this.getAllInfoForOneSession(''); + } else { + this.paginator.limit = this._dataLogsService.limit; + this.paginator.pageIndex = this._dataLogsService.pageNumber; + this.paginator.length = this._dataLogsService.length; + this.getAllSessionsInfo( + this.sortFilter1, this.orderFilter1, this.sortFilter2, this.orderFilter2, this.childNbSessions, this.paginator.pageIndex); + } + }, + (error) => { + console.log('error in logs-session:', error); + }, + ); + this.getAllSessionsInfo('_id.slug', -1, 'latest.completionDate', -1, this.childNbSessions, 1); + } + ngOnDestroy() { + this.sessionChangeSub.unsubscribe(); + } + + getAllSessionsInfo(sortFilter1, order1, sortFilter2, order2, length, pageNumber) { + this._dataLogsService.getAllInfoForAllSessions(sortFilter1, order1, sortFilter2, order2, length, pageNumber).subscribe( + (result) => { + this.foundSession = ''; + this.allSessionsInfoList = result; + this.nbSessions = result.length; + this.paginator.limit = this._dataLogsService.limit; + this.paginator.pageIndex = this._dataLogsService.pageNumber; + this.paginator.length = this.childNbSessions; + this.formatDate(); + }, + (error) => { + console.log('error in getAllSessionsInfo:', error); + this.allSessionsInfoList = []; + this.nbSessions = 0; + this.paginator.limit = 0; + this.paginator.pageIndex = 0; + this.paginator.length = 0; + }, + ); + } + sortInfos(sortFilter1, order1, sortFilter2, order2, length, pageNumber) { + if (!this.isResearchFieldActive) { + this.sortFilter1 = sortFilter1; + this.orderFilter1 = order1; + this.sortFilter2 = sortFilter2; + this.orderFilter2 = order2; + this.getAllSessionsInfo(this.sortFilter1, this.orderFilter1, this.sortFilter2, this.orderFilter2, length, pageNumber); + } + } + + reset(sortFilter1, order1, sortFilter2, order2, length, pageNumber) { + this.isResearchFieldActive = false; + this.paginator.length = this.nbOfItemsPerPage; + this.getAllSessionsInfo(sortFilter1, order1, sortFilter2, order2, length, pageNumber); + } + + getAllInfoForOneSession($event) { + // const foundSlug = (<HTMLInputElement>document.getElementById('foundSlug')).value; + if (this.foundSession === '') { + this.isResearchFieldActive = false; + this.paginator.limit = this.nbOfItemsPerPage; + this.getAllSessionsInfo('_id.slug', -1, 'latest.completionDate', 1, this.childNbSessions, 1); + } else { + this._dataLogsService.getAllInfoForOneSession(this.foundSession).subscribe((result) => { + // console.log('result for getAllInfoForOneSlug', result); + this.isResearchFieldActive = true; + this.allSessionsInfoList = result; + this.nbSessions = 1; + this.paginator.limit = this.nbOfItemsPerPage; + this.paginator.pageIndex = 1; + this.paginator.length = 1; + this.formatDate(); + }, + ); + } + } + + formatDate() { + for (let i = 0; i < this.nbSessions; i += 1) { + this.allSessionsInfoList[i].latest.completionDate.$date = new Date(this.allSessionsInfoList[i].latest.completionDate.$date); + } + } + + // When pagination is changed by user, we update datasetList with new pagination options + changePagination(pageIndex) { + this._dataLogsService.paginationChanged(this.paginator.limit, pageIndex, 'SESSION'); + } + + changePageSize(pageSize) { + this._dataLogsService.paginationChanged(pageSize, 1, 'SESSION'); + } +} diff --git a/src/app/components/logs-dashboard/logs-slugs/filter.pipe.ts b/src/app/components/logs-dashboard/logs-slugs/filter.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..ac204fd5f6d5b52422f5ec33413b9ba279873dca --- /dev/null +++ b/src/app/components/logs-dashboard/logs-slugs/filter.pipe.ts @@ -0,0 +1,12 @@ +import { Pipe, PipeTransform } from '@angular/core'; +@Pipe({ + name: 'filter', +}) +export class FilterPipe implements PipeTransform { + transform(items: any[], value: string, label:string): any[] { + if (!items) return []; + if (!value) return items; + if (value === '' || value === null) return []; + return items.filter(e => e['_id'][label].toLowerCase().indexOf(value) > -1); + } +} diff --git a/src/app/components/logs-dashboard/logs-slugs/logs-slugs.component.html b/src/app/components/logs-dashboard/logs-slugs/logs-slugs.component.html new file mode 100644 index 0000000000000000000000000000000000000000..b21f8509c20fa0dfc8953d29a0865c4a7afb08b8 --- /dev/null +++ b/src/app/components/logs-dashboard/logs-slugs/logs-slugs.component.html @@ -0,0 +1,148 @@ +<div class="input-field is-centered"> + <div class="columns is-5 is-vcentered"> + <div class="column is-5 has-text-left"> + <label class="label"> Recherche par slug: </label> + <input class="input" type="text" [(ngModel)]="foundSlug" (keyup)="getAllInfoForOneSlug($event)" id="foundSlug" list="slugsList" /> + <datalist id="slugsList"> + <option *ngFor="let slug of slugsList" [value]="slug"> {{slug}}</option> + </datalist> + </div> + </div> +</div> + +<div class="columns is-centered is-marginless"> + <div class="column is-6 has-text-left"> + <P>{{ childNbSlugs }} slug(s) dans la base de données</P> + </div> + <div class="column is-6 has-text-left"> + <p>Pour chaque slug, les valeurs affichées concernent la session la plus récente.</p> + </div> +</div> +<div class="table entity-list-table"> + <div class="header columns is-marginless"> + <div class="column is-3 has-text-centered"> + <span class="is-sortable"> + <span class="sort-icons"> + <span class="icon"> + <i class="fas fa-sort-up" (click)="sortInfos('completionDate', -1, '_id.slug', -1, this.childNbSlugs, 1);"></i> + </span> + <span class="icon"> + <i class="fas fa-sort-down" (click)="sortInfos('completionDate', -1, '_id.slug', 1, this.childNbSlugs, 1);"> + </i> + </span> + </span> + <span class="column-title">Slugs</span> + </span> + </div> + <div class="column is-1 has-text-left"> + <span class="column-title">Donnée complète</span> + </div> + <div class="column is-1 has-text-left"> + <span class="column-title">Sessions</span> + </div> + <div class="column is-2 has-text-left"> + <span class="is-sortable"> + <span class="sort-icons"> + <span class="icon"> + <i class="fas fa-sort-up" (click)="sortInfos('_id.slug', -1, 'latest.completionDate', -1, this.childNbSlugs, 1);"></i> + </span> + <span class="icon"> + <i class="fas fa-sort-down" (click)="sortInfos('_id.slug', -1, 'latest.completionDate', 1, this.childNbSlugs, 1);"></i> + </span> + </span> + <span class="column-title">Dernière date d'exécution</span> + </span> + </div> + <div class="column is-1 has-text-left"> + <span class="is-sortable"> +<!-- <span class="sort-icons">--> +<!-- <span class="icon">--> +<!-- <i class="fas fa-sort-up" (click)="sortInfos('_id.slug', -1, 'latest.duration', -1, this.childNbSlugs, 1);"></i>--> +<!-- </span>--> +<!-- <span class="icon">--> +<!-- <i class="fas fa-sort-down" (click)="sortInfos('_id.slug', -1, 'latest.duration', 1, this.childNbSlugs, 1);"></i>--> +<!-- </span>--> +<!-- </span>--> + <span class="column-title">Durée</span> + </span> </div> + <div class="column is-1 has-text-left"> + <span class="is-sortable"> + <span class="sort-icons"> + <span class="icon"> + <i class="fas fa-sort-up" (click)="sortInfos('_id.slug', -1, 'latest.count.INFO', -1, this.childNbSlugs, 1);"></i> + </span> + <span class="icon"> + <i class="fas fa-sort-down" (click)="sortInfos('_id.slug', -1, 'latest.count.INFO', 1, this.childNbSlugs, 1);"></i> + </span> + </span> + <span class="column-title">Infos</span> + </span> + </div> + <div class="column is-1 has-text-left"> + <span class="is-sortable"> + <span class="sort-icons"> + <span class="icon"> + <i class="fas fa-sort-up" (click)="sortInfos('_id.slug', -1, 'latest.count.ERROR', -1, this.childNbSlugs, 1);"></i> + </span> + <span class="icon"> + <i class="fas fa-sort-down" (click)="sortInfos('_id.slug', -1, 'latest.count.ERROR', 1, this.childNbSlugs, 1);"></i> + </span> + </span> + <span class="column-title">Erreurs</span> + </span> + </div> + <div class="column is-1 has-text-left"> + <span class="column-title"></span> + </div> + </div> + <div class="data-list"> + <div *ngFor="let slug of allSlugsInfoList | filter : searchSlug: 'slug'; let i=index; let odd=odd; let even=even;"> + <div class="data columns is-multiline is-vcentered is-marginless" *ngIf="slug._id.slug" + [ngClass]="{ odd: odd, even: even }"> + <div class="column is-3 has-text-left"> + {{slug._id.slug}} <br> + <div class="small-text"> + <p>{{slug._id.uuid}}</p> + </div> + </div> + <div class="column is-1 has-text-left"> + {{ slug.latest.uuid_suffix === 'full' ? 'Oui' : 'Non'}} + </div> + <div class="column is-1 has-text-left"> + {{slug.all.length}} + </div> + <div class="column is-2 has-text-left"> + {{slug.latest.completionDate.$date | date: 'EE dd/MM/yyyy hh:mm:ss'}} + </div> + <div class="column is-1 has-text-left"> + {{slug.latest.duration.hours}}:{{slug.latest.duration.minutes}}:{{slug.latest.duration.seconds}} + </div> + <div class="column is-1 has-text-left actions"> + {{ slug.latest.count.INFO ? slug.latest.count.INFO : 0}} + </div> + <div class="column is-1 has-text-left actions"> + {{ slug.latest.count.ERROR ? slug.latest.count.ERROR:'No'}} + </div> + <div class="column is-1 has-text-left actions"> + <a class="button button-gl is-left" [routerLink]="['preReport', 'slug', slug._id.slug]"> + <i class="fas fa-eye"></i> + </a> + </div> + </div> + </div> + </div> +</div> +<div class="columns is-marginless"> + <div class="column is-10"> + <app-paginator *ngIf="paginator.length > 0" [length]="paginator.length" [pageSize]="paginator.limit" + [pageSizeOptions]="paginator.pageSizeOptions" [pageIndex]="paginator.pageIndex" [pagesToShow]="1" + [showFirstLastButtons]="true" (page)="changePagination($event)" (pageSizeChanged)="changePageSize($event)"> + </app-paginator> + </div> + <div class="column is-1 is-vcentered"> + <a class="button button-gl is-left" (click)="reset('_id.slug', -1, + 'latest.completionDate', -1, this.childNbSlugs, 1);"> + <p class="bold-white">Reset</p> + </a> + </div> +</div> diff --git a/src/app/components/logs-dashboard/logs-slugs/logs-slugs.component.scss b/src/app/components/logs-dashboard/logs-slugs/logs-slugs.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..2f5cc96d6937ebd6aaa1c9b3cb90cfad6e11c37a --- /dev/null +++ b/src/app/components/logs-dashboard/logs-slugs/logs-slugs.component.scss @@ -0,0 +1,11 @@ +@import "../../../../scss/variables.scss"; +@import "../../../../../node_modules/bulma/sass/utilities/_all.sass"; + +.small-text{ + font-size:0.8em; +} + +.bold-white{ + color: white; + font-weight: bold; +} \ No newline at end of file diff --git a/src/app/components/logs-dashboard/logs-slugs/logs-slugs.component.ts b/src/app/components/logs-dashboard/logs-slugs/logs-slugs.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..5152942ce9db5c66d17b6327e723b11fe6cc8517 --- /dev/null +++ b/src/app/components/logs-dashboard/logs-slugs/logs-slugs.component.ts @@ -0,0 +1,132 @@ +import { Component, OnChanges, SimpleChanges, Input, OnDestroy } from '@angular/core'; +import { DataLogsService } from 'src/app/services/data-logs.service'; +import { PaginatorOptions } from 'src/app/models/paginator-options.model'; +import { Subscription } from 'rxjs'; +import { FilterPipe }from './filter.pipe'; + +@Component({ + selector: 'app-logs-slugs', + templateUrl: './logs-slugs.component.html', + styleUrls: ['./logs-slugs.component.scss'], +}) +export class LogsSlugsComponent implements OnChanges, OnDestroy { + @Input() childNbSlugs: number; + @Input() slugsList: any; + + slugChangeSub: Subscription; + nbOfItemsPerPage: number = 10; + foundSlug: string = ''; + sortFilter1:string = '_id'; + orderFilter1: number = -1; + sortFilter2:string = '_id'; + orderFilter2: number = -1; + allSlugsInfoList:any = []; + searchSlug: string = ''; + slugInOnePage:number = 1; + isResearchFieldActive: boolean = false; + paginator: PaginatorOptions; + constructor( + private _dataLogsService: DataLogsService, + private _pipe: FilterPipe, + ) { + this.paginator = { + pageIndex: this._dataLogsService.pageNumber, + length: 1, + limit: this._dataLogsService.limit, + pageSizeOptions: [5, 10, 20], + }; + } + + ngOnChanges(changes: SimpleChanges): void { + this.slugChangeSub = this._dataLogsService.slugLogChange$.subscribe( + () => { + if (this.isResearchFieldActive) { + this.getAllInfoForOneSlug(''); + } else { + this.paginator.limit = this._dataLogsService.limit; + this.paginator.pageIndex = this._dataLogsService.pageNumber; + this.paginator.length = this.childNbSlugs; + this.getAllInfoForAllSlugs( + this.sortFilter1, this.orderFilter1, this.sortFilter2, this.orderFilter2, this.childNbSlugs, this.paginator.pageIndex); + } + }, + ); + // console.log(' this.slugsList', this.slugsList); + this.getAllInfoForAllSlugs('_id.slug', -1, 'latest.completionDate', 1, this.childNbSlugs, 1); + } + ngOnDestroy() { + this.slugChangeSub.unsubscribe(); + } + getAllInfoForAllSlugs(sortFilter1, order1, sortFilter2, order2, length, pageNumber) { + this._dataLogsService.getAllInfoForAllSlugs(sortFilter1, order1, sortFilter2, order2, length, pageNumber).subscribe( + (result) => { + this.foundSlug = ''; + this.allSlugsInfoList = result; + this.slugInOnePage = result.length; + this.paginator.limit = this._dataLogsService.limit; + this.paginator.pageIndex = this._dataLogsService.pageNumber; + this.paginator.length = this.childNbSlugs; + this.formatDate(); + // console.log('slugs result', result); + }, + (error) => { + console.log('error in getAllInfoForAllSlugs:', error); + this.allSlugsInfoList = []; + this.slugInOnePage = 0; + this.paginator.limit = 0; + this.paginator.pageIndex = 0; + this.paginator.length = 0; + }, + ); + } + reset(sortFilter1, order1, sortFilter2, order2, length, pageNumber) { + this.isResearchFieldActive = false; + this.paginator.length = this.nbOfItemsPerPage; + this.getAllInfoForAllSlugs(sortFilter1, order1, sortFilter2, order2, length, pageNumber); + } + + sortInfos(sortFilter1, order1, sortFilter2, order2, length, pageNumber) { + if (!this.isResearchFieldActive){ + this.sortFilter1 = sortFilter1; + this.orderFilter1 = order1; + this.sortFilter2 = sortFilter2; + this.orderFilter2 = order2; + this.getAllInfoForAllSlugs(this.sortFilter1, this.orderFilter1, this.sortFilter2, this.orderFilter2, length, pageNumber); + } + } + + getAllInfoForOneSlug($event) { + if (this.foundSlug === '') { + this.isResearchFieldActive = false; + this.paginator.limit = this.nbOfItemsPerPage; + this.getAllInfoForAllSlugs('_id.slug', -1, 'latest.completionDate', 1, this.childNbSlugs, 1); + } else { + // const foundSlug = (<HTMLInputElement>document.getElementById('foundSlug')).value; + this._dataLogsService.getAllInfoForOneSlug(this.foundSlug).subscribe((result) => { + this.isResearchFieldActive = true; + this.allSlugsInfoList = result; + this.slugInOnePage = 1; + this.paginator.limit = this.nbOfItemsPerPage; + this.paginator.pageIndex = 1; + this.paginator.length = 1; + this.formatDate(); + }, + ); + } + } + + formatDate() { + for (let i = 0; i < this.slugInOnePage; i += 1) { + this.allSlugsInfoList[i].latest.completionDate.$date = new Date(this.allSlugsInfoList[i].latest.completionDate.$date); + } + } + // When pagination is changed by user, we update datasetList with new pagination options + changePagination(pageIndex) { + this._dataLogsService.paginationChanged(this.paginator.limit, pageIndex, 'SLUG'); + } + + changePageSize(pageSize) { + this._dataLogsService.paginationChanged(pageSize, 1, 'SLUG'); + } + +} diff --git a/src/app/components/logs-dashboard/report/logs-error/logs-error.component.html b/src/app/components/logs-dashboard/report/logs-error/logs-error.component.html new file mode 100644 index 0000000000000000000000000000000000000000..5968072c06cd4564255b8121afa4dc888bf632f1 --- /dev/null +++ b/src/app/components/logs-dashboard/report/logs-error/logs-error.component.html @@ -0,0 +1,103 @@ +<div class="columns is-centered is-marginless"> + + <div class="column is-4 has-text-left is-marginless"> + + <div class="table entity-list-table is-marginless" > + <div class="header columns is-marginless is-multiline is-vcentered"> + <div class="column is-6 has-text-left"> + <p> Toutes les étapes:</p> + </div> + </div> + <div class="data-list"> + <div *ngFor="let oneStepData of allStepsData; let i=index; let odd=odd; let even=even;"> + <div *ngIf="oneStepData" [ngClass]="{ odd: odd, even: even }" (click)='getLogsSelector(oneStepData._id.step, oneStepData.counts);'> + <div class="data columns is-multiline is-vcentered is-marginless" [ngClass]="{ odd: odd, even: even, 'is-selected': displayedStep === oneStepData._id.step }" > + <div class="column is-6 has-text-left"> + <br> + <p [ngClass]="{'is-selected': displayedStep === oneStepData._id.step}"> + <span class="bold-text"> Etape : </span> + {{oneStepData._id.step}}</p> + <br> + <p [ngClass]="{'is-selected': displayedStep === oneStepData._id.step}"> + <span class="bold-text"> Durée : </span> + {{oneStepData.totalHoursSpent}}h : {{oneStepData.totalMinutesSpent}}m : + {{oneStepData.totalSecondsSpent}}s</p> + <br> + <p [ngClass]="{'is-selected': displayedStep === oneStepData._id.step}"> + <span class="bold-text"> Info(s) : </span> + {{oneStepData.counts.INFO ? oneStepData.counts.INFO : 0 }}</p> + <br> + </div> + <div class="column is-6 has-text-right"> + <br><br> + <div class="arrows" [ngClass]="{'is-displayed': displayedStep === oneStepData._id.step}"> + <svg xmlns="https://www.w3.org/2000/svg" id="chevron" viewBox="0 0 15 9"> + <path + d="M7.5 7.5c-.1 0-.3-.1-.4-.1l-6-6C1 1.1 1 .8 1.1.6s.5-.2.7 0l5.6 5.6L13 .6c.2-.2.5-.2.7 0s.2.5 0 .7l-6 6c.1.1-.1.2-.2.2z" + class="brandcolor" /> + <path + d="M7.5 7.5c-.1 0-.3-.1-.4-.1l-6-6C1 1.1 1 .8 1.1.6s.5-.2.7 0l5.6 5.6L13 .6c.2-.2.5-.2.7 0s.2.5 0 .7l-6 6c.1.1-.1.2-.2.2z" + class="brandcolor" /> + </svg> + </div> + <p [ngClass]="{'is-selected': displayedStep === oneStepData._id.step}"> + <br> + <span class="bold-text"> Erreur(s) : </span> + {{oneStepData.counts.ERROR ? oneStepData.counts.ERROR : 0}}</p> + </div> + </div> + </div> + </div> + </div> + </div> + + + + </div> + + <div class="column is-8 has-text-left"> + <div class="table entity-list-table is-marginless" *ngIf="allErrorLogs[0] ; else noErrors"> + <div class="header columns is-marginless is-multiline is-centered is-vcentered"> + <div class="column is-2 has-text-left"> + logs d'erreur pour {{allErrorLogs[0].step}}</div> + <div class="column is-10 has-text-centered"> + <app-paginator [length]="paginator.length" [pageSize]="paginator.limit" + [pageSizeOptions]="paginator.pageSizeOptions" [pageIndex]="paginator.pageIndex" [pagesToShow]="5" + [showFirstLastButtons]="true" (page)="changePagination($event)" (pageSizeChanged)="changePageSize($event)"> + </app-paginator> + </div> + </div> + <div class="data-list" *ngFor="let oneErrorLog of allErrorLogs; let i=index; let odd=odd; let even=even;"> + + <div class="data columns is-multiline is-vcentered is-marginless" [ngClass]="{ odd: odd, even: even }"> + <div class="columns is-marginless"> + <div class="column is-12 has-text-left"> + <p> + <span class="bold-text"> Etape : </span> + {{oneErrorLog.step}}</p> + <br> + <p> + <span class="bold-text"> Status : </span> + {{oneErrorLog.status}}</p> + <br> + <p> + <span class="bold-text"> Info : </span> + {{oneErrorLog.info}}</p> + <br> + <p> + <span class="bold-text"> Progression : </span> + {{oneErrorLog.progress_ratio}}</p> + </div> + </div> + </div> + </div> + </div> + + <ng-template #noErrors class="is-centered is-marginless" > + <p>Pas d'erreurs</p> + </ng-template> + + <div> + </div> + </div> +</div> diff --git a/src/app/components/logs-dashboard/report/logs-error/logs-error.component.scss b/src/app/components/logs-dashboard/report/logs-error/logs-error.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..b95e30a3a22a52ad678940ba4cc1a8854c030d65 --- /dev/null +++ b/src/app/components/logs-dashboard/report/logs-error/logs-error.component.scss @@ -0,0 +1,67 @@ +@import "../../../../../scss/variables.scss"; +@import "../../../../../../node_modules/bulma/sass/utilities/_all.sass"; + +figure { + text-align: center; + } + figure img{ + max-width: 9.3rem; + display: inline-block; + margin-top: 1.25rem; + } + + .card-header-title { + justify-content: center; + } + + .mini-info-card{ + margin-top: 1.25rem; + padding: 1.25rem; + background-color: yellow; + + } + .mini-log-card{ + margin-top: 1.25rem; + padding: 1.25rem; + background-color: greenyellow; + + } + + .mini-error-card{ + margin-top: 1.25rem; + padding: 1.25rem; + background-color: red; + + } + .input-field{ + width: 200%; + margin-top: 1.25rem; + } + + .hidden { + display: none; + } + + .arrows { + svg { + width: 1rem; + fill: $grey-dark-color; + transform: rotate(0deg); + } + } + + .arrows.is-displayed svg { + transform: rotate(-90deg); + } + .paginator-container{ + background-color: yellowgreen; + } + + .is-selected{ + background-color: $tomato-color !important; //to override odd or even class css + color: white; + } + .bold-text { + font-weight:bold; + display:inline; + } diff --git a/src/app/components/logs-dashboard/report/logs-error/logs-error.component.ts b/src/app/components/logs-dashboard/report/logs-error/logs-error.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..55f17fe75f94b2b0e38126198f362681ed434879 --- /dev/null +++ b/src/app/components/logs-dashboard/report/logs-error/logs-error.component.ts @@ -0,0 +1,161 @@ +import { Component, OnChanges, SimpleChanges, Input, OnDestroy } from '@angular/core'; +import { DataLogsService } from 'src/app/services/data-logs.service'; +import { ActivatedRoute } from '@angular/router'; +import { ILogs } from 'src/app/models/logs.model'; +import { PaginatorOptions } from 'src/app/models/paginator-options.model'; +import { Subscription } from 'rxjs'; + +@Component({ + selector: 'app-logs-error', + templateUrl: './logs-error.component.html', + styleUrls: ['./logs-error.component.scss'], +}) +export class LogsErrorComponent implements OnChanges, OnDestroy { + @Input() childUuid: string = ''; + @Input() childSessionId: string = ''; + @Input() childSlug: string; + errorLogChangeSub: Subscription; + selectedStep = { + step:'main', + nbErrors: 0, + nbInfos: 0, + }; + + displayedStep: string = 'none'; + logTypes: any = [ + { + name : 'erreurs', + selected : true, + }, + { + name : 'infos', + selected : false, + }, + ]; + uuid: string; + sessionId: string; + slug: string; + stepData: ILogs; + allLogs: any; + data: any = []; + allStepsData: any = []; + allErrorLogs: any = []; + // Paginator options + paginator: PaginatorOptions; + pageSizeOptions = [5, 10, 25, 100]; + constructor( + private _dataLogsService: DataLogsService, + private route: ActivatedRoute, + ) { + this.paginator = { + pageIndex: this._dataLogsService.pageNumber, + length: 1, + limit: this._dataLogsService.limit, + pageSizeOptions: [5, 10, 20], + }; + } + + ngOnChanges(changes: SimpleChanges): void { + this.slug = this.childSlug; + this.getLogsSteps(); + this.errorLogChangeSub = this._dataLogsService.errorLogChange$.subscribe( + () => { + this.paginator.limit = this._dataLogsService.limit; + this.paginator.pageIndex = this._dataLogsService.pageNumber; + if (this.logTypes[0].selected === true) { + this.getErrorStepsLogs(this.selectedStep.step, this.selectedStep.nbErrors, this.paginator.pageIndex); + } + if (this.logTypes[1].selected === true) { + this.getSimpleStepsLogs(this.selectedStep.step, this.selectedStep.nbInfos, this.paginator.pageIndex); + } + }, + ); + } + ngOnDestroy() { + this.errorLogChangeSub.unsubscribe(); + } + getLogsSteps() { + if ((this.childUuid != null) && (this.childSessionId != null)) { + // console.log('parameters', this.childUuid, this.childSessionId); + + this._dataLogsService.getAllStepsDuration( + this.childUuid, + this.childSessionId, + ).subscribe((results) => { + this.allStepsData = results; + for (const result of results) { + if (result.counts.ERROR > 0) { + this.getLogsSelector(result._id.step, result.counts); + break; + } + } + }, + ); + } + } + getLogsSelector(step, counts) { + this.selectedStep.step = step; + this.selectedStep.nbErrors = counts.ERROR; + this.selectedStep.nbInfos = counts.INFO; + this.allErrorLogs = []; + this.allLogs = []; + this.displayedStep = step; + if (this.logTypes[0].selected === true) { + this.getErrorStepsLogs(step, this.selectedStep.nbErrors, 1); + } + if (this.logTypes[1].selected === true) { + this.getSimpleStepsLogs(step, this.selectedStep.nbInfos, 1); + } + } + getErrorStepsLogs(step, nbErrors, pageNumber) { + if ((this.childUuid != null) && (this.childSessionId != null)) { + // console.log('parameters', this.childUuid, this.childSessionId); + + this._dataLogsService.getLogsStepsLogs( + step, + this.childSessionId, + this.childUuid, + 'ERROR', + nbErrors, + pageNumber, + ).subscribe((results) => { + // console.log('results', results); + this.allErrorLogs = results; + this.paginator.limit = this._dataLogsService.limit; + this.paginator.pageIndex = this._dataLogsService.pageNumber; + this.paginator.length = this._dataLogsService.length; + }, + ); + } + } + getSimpleStepsLogs(step, nbInfos, pageNumber) { + if ((this.childUuid != null) && (this.childSessionId != null)) { + // console.log('parameters', this.childUuid, this.childSessionId); + + this._dataLogsService.getLogsStepsLogs( + step, + this.childSessionId, + this.childUuid, + 'INFO', + nbInfos, + pageNumber, + ).subscribe((results) => { + // console.log('results', results); + this.allLogs = results; + this.paginator.limit = this._dataLogsService.limit; + this.paginator.pageIndex = this._dataLogsService.pageNumber; + this.paginator.length = this._dataLogsService.length; + }, + ); + } + } + + // When pagination is changed by user, we update datasetList with new pagination options + changePagination(pageIndex) { + this._dataLogsService.paginationChanged(this.paginator.limit, pageIndex, 'ERROR'); + } + + changePageSize(pageSize) { + this._dataLogsService.paginationChanged(pageSize, 1, 'ERROR'); + } +} diff --git a/src/app/components/logs-dashboard/report/logs-graph/logs-graph.component.html b/src/app/components/logs-dashboard/report/logs-graph/logs-graph.component.html new file mode 100644 index 0000000000000000000000000000000000000000..481d25f57f903c987d750e021abfbdad132afbc2 --- /dev/null +++ b/src/app/components/logs-dashboard/report/logs-graph/logs-graph.component.html @@ -0,0 +1,23 @@ + +<div class="section" *ngIf="isNoError; else errorGraph"> + <div class="graph-component"> + + <div class="columns is-centered"> + <div class="graph-div-canvas"> + <canvas id="graphCanvas">{{ chart }}</canvas> + </div> + </div> + <div class="columns is-centered"> + <a class="button button-gl is-centered" (click)="replotGraph()"> + Recharger le graphique + </a> + </div> + </div> +</div> +<ng-template #errorGraph> + <h3> + Pas de données pour ce couple: slug-session + <br> + Veuillez vérifier vos paramètre de recherche SVP. + </h3> +</ng-template> \ No newline at end of file diff --git a/src/app/components/logs-dashboard/report/logs-graph/logs-graph.component.scss b/src/app/components/logs-dashboard/report/logs-graph/logs-graph.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..81e12ca709368a7309dbf7d9663108997ba8538d --- /dev/null +++ b/src/app/components/logs-dashboard/report/logs-graph/logs-graph.component.scss @@ -0,0 +1,4 @@ +.graph-div-canvas{ + width: 80%; + left: 10%; +} \ No newline at end of file diff --git a/src/app/components/logs-dashboard/report/logs-graph/logs-graph.component.ts b/src/app/components/logs-dashboard/report/logs-graph/logs-graph.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..3010b22202aa812c209fd0fc8b6f506576600027 --- /dev/null +++ b/src/app/components/logs-dashboard/report/logs-graph/logs-graph.component.ts @@ -0,0 +1,126 @@ +import { Component, OnChanges, SimpleChanges, Input } from '@angular/core'; +import { Chart } from 'chart.js'; +import { DataLogsService } from 'src/app/services/data-logs.service'; + +@Component({ + selector: 'app-logs-graph', + templateUrl: './logs-graph.component.html', + styleUrls: ['./logs-graph.component.scss'], +}) +export class LogsGraphComponent implements OnChanges { + @Input() childUuid: string; + @Input() childSessionId: string; + @Input() childSlug: string; + + public myChart: Chart; + uuid: string; + sessionId: string; + isNoError: boolean = true; + graphData: any = [0.2, 0.3]; + dataset = []; + + constructor( + private dataLogsService: DataLogsService, + ) { } + ngOnChanges(changes: SimpleChanges): void { + if ((this.childUuid != null) && (this.childSessionId != null)) { + this.getData(); + } + } + + plotGraph() { + + this.myChart = new Chart(document.getElementById('graphCanvas'), { + type: 'scatter', + data: { + datasets: this.dataset, + }, + options: { + title: { + display: true, + text: "Rapport d'avancement des scripts d'indexation.", + }, + legend: { + display: true, + position: 'right', + }, + elements: { + line: { + tension: 0, // disables bezier curves + }, + }, + responsive: true, + scales: { + xAxes: [{ + type: 'time', + display: true, + scaleLabel: { + display: true, + labelString: 'temps', + }, + ticks: { + major: { + fontStyle: 'bold', + fontColor: '#FF0000', + }, + }, + }], + yAxes: [{ + display: true, + scaleLabel: { + display: true, + labelString: 'progression', + }, + }], + }, + }, + + }); + } + + getXYValuesFromData() { + const color = ['red', 'yellow', 'blue', 'black', 'pink', 'green', 'orange']; + let c = 0; + this.dataset = []; + for (const data of this.graphData) { + const arrayOfXY = []; + for (const i in data.values) { + const XY = { + x: data.timestamps[i].$date, + + y: data.values[i], + }; + arrayOfXY.push(XY); + } + const tempObj = { + label: data._id.step, + showLine: true, + backgroundColor: color[c], + borderColor: color[c], + fill: false, + data: arrayOfXY, + lineTension: 0.3, + }; + this.dataset.push(tempObj); + c += 1; + } + } + replotGraph() { + this.getData(); + this.myChart.update(); + } + getData() { + this.dataLogsService.getData(this.childUuid, this.childSessionId).subscribe((results) => { + try { + const dataLenght = results[0].lenght; + } catch (e) { + console.log('error message :', e); + this.isNoError = false; + } + this.graphData = results; + this.getXYValuesFromData(); + this.plotGraph(); + }, + ); + } +} diff --git a/src/app/components/logs-dashboard/report/logs-info/logs-info.component.html b/src/app/components/logs-dashboard/report/logs-info/logs-info.component.html new file mode 100644 index 0000000000000000000000000000000000000000..c60db205b68efa558877650f4f4b9aae52c37188 --- /dev/null +++ b/src/app/components/logs-dashboard/report/logs-info/logs-info.component.html @@ -0,0 +1,101 @@ +<div class="columns is-centered is-marginless"> + + <div class="column is-4 has-text-left is-marginless"> + + <div class="table entity-list-table is-marginless" *ngIf="allLogs; else noLogs"> + <div class="header columns is-marginless is-multiline is-vcentered"> + <div class="column is-6 has-text-left"> + <p> Toutes les étapes:</p> + </div> + </div> + <div class="data-list"> + <div *ngFor="let oneStepData of allStepsData; let i=index; let odd=odd; let even=even;"> + <div *ngIf="oneStepData" [ngClass]="{odd: odd, even: even}" (click)='getLogsSelector(oneStepData._id.step, oneStepData.counts);'> + <div class="data columns is-multiline is-vcentered is-marginless" [ngClass]="{ odd: odd, even: even, 'is-selected': displayedStep === oneStepData._id.step }" > + <div class="column is-6 has-text-left"> + <br> + <p [ngClass]="{'is-selected': displayedStep === oneStepData._id.step}"> + <span class="bold-text">Etape : </span> + {{oneStepData._id.step}}</p> + <br> + <p [ngClass]="{'is-selected': displayedStep === oneStepData._id.step}" > + <span class="bold-text">Durée : </span> + {{oneStepData.totalHoursSpent}}h : {{oneStepData.totalMinutesSpent}}m : + {{oneStepData.totalSecondsSpent}}s + </p> + <br> + <p [ngClass]="{'is-selected': displayedStep === oneStepData._id.step}"> + <span class="bold-text">Info(s) : </span> + {{oneStepData.counts.INFO ? oneStepData.counts.INFO : 0 }}</p> + <br> + </div> + <div class="column is-6 has-text-right"> + <br><br> + <div class="arrows" [ngClass]="{'is-displayed': displayedStep === oneStepData._id.step}"> + <svg xmlns="https://www.w3.org/2000/svg" id="chevron" viewBox="0 0 15 9"> + <path + d="M7.5 7.5c-.1 0-.3-.1-.4-.1l-6-6C1 1.1 1 .8 1.1.6s.5-.2.7 0l5.6 5.6L13 .6c.2-.2.5-.2.7 0s.2.5 0 .7l-6 6c.1.1-.1.2-.2.2z" + class="brandcolor" /> + <path + d="M7.5 7.5c-.1 0-.3-.1-.4-.1l-6-6C1 1.1 1 .8 1.1.6s.5-.2.7 0l5.6 5.6L13 .6c.2-.2.5-.2.7 0s.2.5 0 .7l-6 6c.1.1-.1.2-.2.2z" + class="brandcolor" /> + </svg> + </div> + <p [ngClass]="{'is-selected': displayedStep === oneStepData._id.step}"> + <br> + <span class="bold-text">Erreur(s) : </span> + {{ oneStepData.counts.ERROR ? oneStepData.counts.ERROR : 0}} + </div> + </div> + </div> + </div> + </div> + </div> + + <ng-template class="is-centered is-marginless" #noLogs> + <p>Pas de Logs</p> + </ng-template> + + </div> + + <div class="column is-8 has-text-left"> + <div class="table entity-list-table is-marginless" *ngIf="allLogs"> + <div class="header columns is-marginless is-multiline is-centered is-vcentered"> + <div class="column is-2 has-text-left"> + logs pour {{allLogs[0].step}}</div> + <div class="column is-10 has-text-left"> + <app-paginator [length]="paginator.length" [pageSize]="paginator.limit" + [pageSizeOptions]="paginator.pageSizeOptions" [pageIndex]="paginator.pageIndex" [pagesToShow]="5" + [showFirstLastButtons]="true" (page)="changePagination($event)" (pageSizeChanged)="changePageSize($event)"> + </app-paginator> + </div> + </div> + <div class="data-list" *ngFor="let oneLog of allLogs; let i=index; let odd=odd; let even=even;"> + + <div class="data columns is-multiline is-vcentered is-marginless" [ngClass]="{ odd: odd, even: even }"> + <div class="columns is-marginless"> + <div class="column is-12 has-text-left"> + <p> + <span class="bold-text">Step : </span> + {{oneLog.step}}</p> + <br> + <p> + <span class="bold-text">Status : </span> + {{oneLog.status}}</p> + <br> + <p> + <span class="bold-text">Info : </span> + {{oneLog.info}}</p> + <br> + <p> + <span class="bold-text">Progression : </span> + {{oneLog.progress_ratio}}</p> + </div> + </div> + </div> + </div> + </div> + <div> + </div> + </div> +</div> diff --git a/src/app/components/logs-dashboard/report/logs-info/logs-info.component.scss b/src/app/components/logs-dashboard/report/logs-info/logs-info.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..cc915ee76b3916f7afca907a7ef4a96dc2c818bf --- /dev/null +++ b/src/app/components/logs-dashboard/report/logs-info/logs-info.component.scss @@ -0,0 +1,71 @@ +@import "../../../../../scss/variables.scss"; +@import "../../../../../../node_modules/bulma/sass/utilities/_all.sass"; + +figure { + text-align: center; + } + figure img{ + max-width: 9.3rem; + display: inline-block; + margin-top: 1.25rem; + } + + .card-header-title { + justify-content: center; + } + + .mini-info-card{ + margin-top: 1.25rem; + padding: 1.25rem; + background-color: yellow; + + } + .color-green{ + + background-color: greenyellow; + + } + + .mini-error-card{ + margin-top: 1.25rem; + padding: 1.25rem; + background-color: red; + + } + .input-field{ + width: 200%; + margin-top: 1.25rem; + } + + .hidden { + display: none; + } + + .arrows { + svg { + width: 1rem; + fill: $grey-dark-color; + transform: rotate(0deg); + } + } + + .arrows.is-displayed svg { + transform: rotate(-90deg); + } + .paginator-container{ + background-color: yellowgreen; + } + + .steps-header{ + padding: 0; + margin:0; + } + .is-selected{ + background-color: $tomato-color !important; //to override odd or even class css + color: white; + } + .bold-text { + font-weight:bold; + display:inline; + } + diff --git a/src/app/components/logs-dashboard/report/logs-info/logs-info.component.ts b/src/app/components/logs-dashboard/report/logs-info/logs-info.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..b608ac5ee36ee10f5c1ade6b66ee10be2cd04988 --- /dev/null +++ b/src/app/components/logs-dashboard/report/logs-info/logs-info.component.ts @@ -0,0 +1,115 @@ +import { Component, OnChanges, SimpleChanges, Input, OnDestroy } from '@angular/core'; +import { DataLogsService } from 'src/app/services/data-logs.service'; +import { ActivatedRoute } from '@angular/router'; +import { ILogs } from 'src/app/models/logs.model'; +import { PaginatorOptions } from 'src/app/models/paginator-options.model'; +import { Subscription } from 'rxjs'; + +@Component({ + selector: 'app-logs-info', + templateUrl: './logs-info.component.html', + styleUrls: ['./logs-info.component.scss'], +}) +export class LogsInfoComponent implements OnChanges, OnDestroy { + @Input() childUuid: string = ''; + @Input() childSessionId: string = ''; + @Input() childSlug: string; + infoChangeSub: Subscription; + selectedStep = { + step: 'main', + nbErrors: 1, + nbInfos: 0, + }; + + displayedStep: string = 'none'; + uuid: string; + sessionId: string; + slug: string; + allLogs: any; + data:any = []; + allStepsData:any = []; + + // Paginator options + paginator: PaginatorOptions; + pageSizeOptions = [5, 10, 25, 100]; + constructor( + private _dataLogsService: DataLogsService, + private route: ActivatedRoute, + ) { + this.paginator = { + pageIndex: this._dataLogsService.pageNumber, + length: 1, + limit: this._dataLogsService.limit, + pageSizeOptions: [5, 10, 20], + }; + } + + ngOnChanges(changes: SimpleChanges): void { + this.slug = this.childSlug; + this.getLogsSteps(); + this.infoChangeSub = this._dataLogsService.infoLogChange$.subscribe( + () => { + this.paginator.limit = this._dataLogsService.limit; + this.paginator.pageIndex = this._dataLogsService.pageNumber; + this.paginator.length = this._dataLogsService.length; + this.getSimpleStepsLogs(this.selectedStep.step, this.selectedStep.nbInfos, this.paginator.pageIndex); + }, + ); + } + ngOnDestroy() { + this.infoChangeSub.unsubscribe(); + } + + getLogsSteps() { + if ((this.childUuid != null) && (this.childSessionId != null)) { + // console.log('parameters', this.childUuid, this.childSessionId); + + this._dataLogsService.getAllStepsDuration( + this.childUuid, + this.childSessionId, + ).subscribe((results) => { + this.allStepsData = results; + this.getLogsSelector(results[0]._id.step, results[0].counts); + }, + ); + } + } + getLogsSelector(step, counts) { + this.selectedStep.step = step; + this.selectedStep.nbInfos = counts.INFO; + this.allLogs = []; + this.displayedStep = step; + this.getSimpleStepsLogs(step, this.selectedStep.nbInfos, 1); + } + + getSimpleStepsLogs(step, nbInfos, pageNumber) { + if ((this.childUuid != null) && (this.childSessionId != null)) { + // console.log('parameters', this.childUuid, this.childSessionId); + + this._dataLogsService.getLogsStepsLogs( + step, + this.childSessionId, + this.childUuid, + 'INFO', + nbInfos, + pageNumber, + ).subscribe((results) => { + // console.log('results', results); + this.allLogs = results; + this.paginator.limit = this._dataLogsService.limit; + this.paginator.pageIndex = this._dataLogsService.pageNumber; + this.paginator.length = this._dataLogsService.length; + }, + ); + } + } + + // When pagination is changed by user, we update datasetList with new pagination options + changePagination(pageIndex) { + this._dataLogsService.paginationChanged(this.paginator.limit, pageIndex, 'INFO'); + } + + changePageSize(pageSize) { + this._dataLogsService.paginationChanged(pageSize, 1, 'INFO'); + } +} diff --git a/src/app/components/logs-dashboard/report/logs-pre-report/logs-pre-report.component.html b/src/app/components/logs-dashboard/report/logs-pre-report/logs-pre-report.component.html new file mode 100644 index 0000000000000000000000000000000000000000..2ca3f7a7050304923a0f23a16dfe1d80d4565c88 --- /dev/null +++ b/src/app/components/logs-dashboard/report/logs-pre-report/logs-pre-report.component.html @@ -0,0 +1,295 @@ +<div class="section page-container" *ngIf="nbObjects!==0"> + <app-page-header [pageInfo]="pageHeaderInfo" [goToThisUrl]="previousUrl" [hideBackButton]="false"></app-page-header> + + <div [ngClass]="{'hidden': isSession}"> + <div class="section"> + <div class="columns is-left is-marginless"> + <div class="column has-text-left"> + <span>{{nbObjects}} sessions(s) trouvées pour:</span> + <br> + <span class="bold-text">slug : </span> + <span> {{id}} </span> + <br> + <span class="bold-text">uuid : </span> + <span> {{uuid}} </span> + <br> + <br> + <span> Pour chaque session, les données affichées concernent le slug en question (ici pas le slug le plus récent ou la durée cumulée de tous les slugs). </span> + <br> + </div> + </div> + + <div class="table entity-list-table"> + <div class="header columns is-marginless"> + <div class="column is-4 has-text-left"> + <span class="is-sortable"> + <span class="column-title">SessionId</span> + <p></p> + </span> + </div> + <div class="column is-1 has-text-left"> + <span class="is-sortable"> +<!-- <span class="sort-icons">--> +<!-- <span class="icon">--> +<!-- <i class="fas fa-sort-up"></i>--> +<!-- </span>--> +<!-- <span class="icon">--> +<!-- <i class="fas fa-sort-down"></i>--> +<!-- </span>--> +<!-- </span>--> + <span class="column-title">Slugs</span> + </span> + </div> + <div class="column is-2 has-text-left"> + <span class="is-sortable"> + <span class="sort-icons"> + <span class="icon"> + <i class="fas fa-sort-up"></i> + </span> + <span class="icon"> + <i class="fas fa-sort-down"></i> + </span> + </span> + <span class="column-title">Date d'exécution</span> + </span> + </div> + <div class="column is-1 has-text-left"> + <span class="is-sortable"> +<!-- <span class="sort-icons">--> +<!-- <span class="icon">--> +<!-- <i class="fas fa-sort-up"></i>--> +<!-- </span>--> +<!-- <span class="icon">--> +<!-- <i class="fas fa-sort-down"></i>--> +<!-- </span>--> +<!-- </span>--> + <span class="column-title">Durée</span> + </span> + </div> + <div class="column is-1 has-text-left"> + <span class="is-sortable"> +<!-- <span class="sort-icons">--> +<!-- <span class="icon">--> +<!-- <i class="fas fa-sort-up"></i>--> +<!-- </span>--> +<!-- <span class="icon">--> +<!-- <i class="fas fa-sort-down"></i>--> +<!-- </span>--> +<!-- </span>--> + <span class="column-title">Infos</span> + </span> + </div> + <div class="column is-1 has-text-left"> + <span class="is-sortable"> +<!-- <span class="sort-icons">--> +<!-- <span class="icon">--> +<!-- <i class="fas fa-sort-up"></i>--> +<!-- </span>--> +<!-- <span class="icon">--> +<!-- <i class="fas fa-sort-down"></i>--> +<!-- </span>--> +<!-- </span>--> + <span class="column-title">Erreurs</span> + </span> + </div> + <div class="column is-1 has-text-left"> + <span class="column-title"></span> + </div> + </div> + <div class="data-list"> + <div *ngFor="let session of allSlugSessionInfoList; let i=index; let odd=odd; let even=even;"> + <div class="data columns is-multiline is-vcentered is-marginless" + *ngIf="session._id.slug_list" + [ngClass]="{ odd: odd, even: even }"> + <div class="column is-4 has-text-left"> + {{ session._id.session_id }} + </div> + <div class="column is-1 has-text-left"> + {{session._id.slug_list.length}} + </div> + <div class="column is-2 has-text-left"> + {{ session._id.completionDate.$date | date: 'EE dd/MM/yyyy hh:mm:ss' }} + </div> + <div class="column is-1 has-text-left"> + {{session._id.duration.hours}}:{{session._id.duration.minutes}}:{{session._id.duration.seconds}} + </div> + <div class="column is-1 has-text-left actions"> + {{ session._id.count.INFO ? session._id.count.INFO: 0}} + </div> + <div class="column is-1 has-text-left actions"> + {{ session._id.count.ERROR ? session._id.count.ERROR: 0}} + </div> + <div class="column is-1 actions"> + <a class="button button-gl " + [routerLink]="['/','datalogs','report', id, session._id.session_id , 'slug']"> + <i class="fas fa-eye"></i> + </a> + </div> + </div> + </div> + </div> + + </div> + <div class="columns is-marginless"> + <div class="column"> + <app-paginator *ngIf="paginator.length > 0" [length]="paginator.length" [pageSize]="paginator.limit" + [pageSizeOptions]="paginator.pageSizeOptions" [pageIndex]="paginator.pageIndex" [pagesToShow]="5" + [showFirstLastButtons]="true" (page)="changePagination($event)" (pageSizeChanged)="changePageSize($event)"> + </app-paginator> + </div> + </div> + </div> + + </div> + + <div [ngClass]="{'hidden': !isSession}"> + <div class="section"> + <div class="input-field is-centered"> + <div class="columns is-5 is-vcentered"> + <div class="column is-5 has-text-left"> + <label class="label"> Recherche par slug: </label> + <input class="input" type="text" [(ngModel)]="foundSlug" (keyup)="getAllInfoForOneSlug($event)" id="foundSlug" list="slugsList" /> + <datalist id="slugsList"> + <option *ngFor="let slug of completeSlugsList" [value]="slug"> {{slug}}</option> + </datalist> + </div> + </div> + </div> + <div class="columns is-left is-marginless"> + <div class="column has-text-left"> + <div class="columns is-centered is-marginless"> + + <div class="column has-text-left"> + <span>{{nbObjects}} slug(s) trouvés pour la session : </span> + <br> + <span class="bold-text"> {{id}} </span> + </div> + + </div> + </div> + </div> + + <div class="table entity-list-table"> + <div class="header columns is-marginless"> + <div class="column is-3 has-text-centered"> + <span class="is-sortable"> +<!-- <span class="sort-icons">--> +<!-- <span class="icon">--> +<!-- <i class="fas fa-sort-up"></i>--> +<!-- </span>--> +<!-- <span class="icon">--> +<!-- <i class="fas fa-sort-down"></i>--> +<!-- </span>--> +<!-- </span>--> + <span class="column-title">Slugs</span> + </span> + </div> + <div class="column is-1 has-text-left"> + <span class="column-title">Donnée complète?</span> + </div> + <div class="column is-1 has-text-left"> + <span class="column-title">Sessions</span> + </div> + <div class="column is-2 has-text-left"> + <span class="is-sortable"> +<!-- <span class="sort-icons">--> +<!-- <span class="icon">--> +<!-- <i class="fas fa-sort-up"></i>--> +<!-- </span>--> +<!-- <span class="icon">--> +<!-- <i class="fas fa-sort-down"></i>--> +<!-- </span>--> +<!-- </span>--> + <span class="column-title">Date d'exécution</span> + </span> + </div> + <div class="column is-1 has-text-left"> + <span class="is-sortable"> +<!-- <span class="sort-icons">--> +<!-- <span class="icon">--> +<!-- <i class="fas fa-sort-up"></i>--> +<!-- </span>--> +<!-- <span class="icon">--> +<!-- <i class="fas fa-sort-down"></i>--> +<!-- </span>--> +<!-- </span>--> + <span class="column-title">Durée</span> + </span> </div> + <div class="column is-1 has-text-left"> + <span class="is-sortable"> +<!-- <span class="sort-icons">--> +<!-- <span class="icon">--> +<!-- <i class="fas fa-sort-up"></i>--> +<!-- </span>--> +<!-- <span class="icon">--> +<!-- <i class="fas fa-sort-down"></i>--> +<!-- </span>--> +<!-- </span>--> + <span class="column-title">Infos</span> + </span> + </div> + <div class="column is-1 has-text-left"> + <span class="is-sortable"> +<!-- <span class="sort-icons">--> +<!-- <span class="icon">--> +<!-- <i class="fas fa-sort-up"></i>--> +<!-- </span>--> +<!-- <span class="icon">--> +<!-- <i class="fas fa-sort-down"></i>--> +<!-- </span>--> +<!-- </span>--> + <span class="column-title">Erreurs</span> + </span> + </div> + <div class="column is-1 has-text-left"> + <span class="column-title"></span> + </div> + </div> + <div class="data-list"> + <div *ngFor="let slug of allSlugSessionInfoList; let i=index; let odd=odd; let even=even;"> + <div class="data columns is-multiline is-vcentered is-marginless" + *ngIf="slug._id.session_list" + [ngClass]="{ odd: odd, even: even }"> + <div class="column is-3 has-text-left"> + {{slug._id.slug}} <br> + <div class="small-text"> + <p>{{slug._id.uuid}}</p> + </div> + </div> + <div class="column is-1 has-text-left"> + {{ slug._id.uuid_suffix === 'full' ? 'Oui' : 'Non'}} + </div> + <div class="column is-1 has-text-left"> + {{slug._id.session_list.length}} + </div> + <div class="column is-2 has-text-left"> + {{slug._id.completionDate.$date | date: 'EE dd/MM/yyyy hh:mm:ss'}} + </div> + <div class="column is-1 has-text-left"> + {{slug._id.duration.hours}}:{{slug._id.duration.minutes}}:{{slug._id.duration.seconds}} + </div> + <div class="column is-1 has-text-left actions"> + {{ slug._id.count.INFO ? slug._id.count.INFO: 0}} + </div> + <div class="column is-1 has-text-left actions"> + {{ slug._id.count.ERROR ? slug._id.count.ERROR: 0}} + </div> + <div class="column is-1 has-text-left actions"> + <a class="button button-gl is-left" + [routerLink]="['/','datalogs','report', slug._id.slug, id, 'session']"> + <i class="fas fa-eye"></i> + </a> + </div> + </div> + </div> + </div> + </div> + <div class="column"> + <app-paginator *ngIf="paginator.length > 0" [length]="paginator.length" [pageSize]="paginator.limit" + [pageSizeOptions]="paginator.pageSizeOptions" [pageIndex]="paginator.pageIndex" [pagesToShow]="5" + [showFirstLastButtons]="true" (page)="changePagination($event)" (pageSizeChanged)="changePageSize($event)"> + </app-paginator> + </div> + </div> + </div> +</div> diff --git a/src/app/components/logs-dashboard/report/logs-pre-report/logs-pre-report.component.scss b/src/app/components/logs-dashboard/report/logs-pre-report/logs-pre-report.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..aaa6c7250fd49a1fdb867a75fa5d537b47935d87 --- /dev/null +++ b/src/app/components/logs-dashboard/report/logs-pre-report/logs-pre-report.component.scss @@ -0,0 +1,38 @@ +@import "../../../../../scss/variables.scss"; +@import "../../../../../../node_modules/bulma/sass/utilities/_all.sass"; + +.small-text{ + font-size:0.8em; +} +.full-width { + width: 100%; +} + +h1 { + text-align: center +} + +.icon { + cursor: pointer; + &:hover { + .fa-plus { + color: lightblue; + } + .fa-trash { + color: #d5232a; + } + } +} + +.hidden { + display: none; +} + +.bold-text { + font-weight:bold; + display:inline; +} +.italic-text { + font-weight:italic; + display:inline; +} \ No newline at end of file diff --git a/src/app/components/logs-dashboard/report/logs-pre-report/logs-pre-report.component.ts b/src/app/components/logs-dashboard/report/logs-pre-report/logs-pre-report.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..f665bdbeb7b68f85ce1a21d0119e1d4714ddce1c --- /dev/null +++ b/src/app/components/logs-dashboard/report/logs-pre-report/logs-pre-report.component.ts @@ -0,0 +1,181 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, ParamMap } from '@angular/router'; +import { FormGroup } from '@angular/forms'; +import { filter, switchMap } from 'rxjs/operators'; +import { DataLogsService } from 'src/app/services/data-logs.service'; +import { IPageHeaderInfo } from 'src/app/models/page.model'; +import { PaginatorOptions } from 'src/app/models/paginator-options.model'; +import { forkJoin } from 'rxjs'; + +// in this component the pagination and filter cannot be handled with service and http request +// because the displayed table is from a composite object . Two requests for one line of the table +@Component({ + selector: 'app-logs-pre-report', + templateUrl: './logs-pre-report.component.html', + styleUrls: ['./logs-pre-report.component.scss'], +}) +export class LogsPreReportComponent implements OnInit { + pageHeaderInfo: IPageHeaderInfo = { + title: "Logs d'indexation: recherche affinée", + }; + previousUrl: string = '/datalogs'; + nbOfItemsPerPage: number = 10; + responseArray: any = []; + form: FormGroup; + uuid: any = ''; + title: string; + type: string; + id: string; + foundSlug: string = ''; + completeSlugsList: any = []; + nbObjects: number = 0; + isSession: boolean = false; + allSlugSessionInfoList: any = []; + // Paginator options + paginator: PaginatorOptions; + pageSizeOptions = [5, 10, 25, 100]; + pageIndex: number = 1; + + constructor( + private _dataLogsService: DataLogsService, + public _route: ActivatedRoute, + ) { + this.paginator = { + pageIndex: 1, + length: 1, + limit: 10, + pageSizeOptions: [5, 10, 20], + }; + } + + ngOnInit() { + this.title = this._route.snapshot.data.title; + this.id = this._route.snapshot.paramMap.get('id'); + this.type = this._route.snapshot.paramMap.get('type'); + if (this.type === 'session') { + this.isSession = true; + } + this.getComplementaryInformation(); + } + + completeSlugsInfos(slugList, sessionId) { + this.allSlugSessionInfoList = []; + for (let i = 0; i < slugList.length; i += 1) { + this.getAllInfoForOneSlugSession(slugList[i], sessionId, 'slug'); + } + } + + completeSessionsInfos(slug, sessionList) { + // console.log('slug', slug, 'sessionList', sessionList); + this.allSlugSessionInfoList = []; + for (let i = 0; i < sessionList.length; i += 1) { + this.getAllInfoForOneSlugSession(slug, sessionList[i], 'session'); + } + } + getAllInfoForOneSlugSession(slug, sessionId, type) { + if (type === 'session') { + forkJoin( + this._dataLogsService.getAllInfoForSlugSession(slug, sessionId), + this._dataLogsService.getAllInfoForOneSession(sessionId), + ) + .subscribe(([res1, res2]) => { + let sessionData = {}; + res1[0]._id.completionDate.$date = new Date(res1[0]._id.completionDate.$date); + sessionData = res1[0]; + sessionData['_id']['slug_list'] = res2[0]['all']; + this.allSlugSessionInfoList.push(sessionData); + }); + } else { + forkJoin( + this._dataLogsService.getAllInfoForSlugSession(slug, sessionId), + this._dataLogsService.getAllInfoForOneSlug(slug), + this._dataLogsService.getSlugFromSessionId(sessionId), + ) + .subscribe(([res1, res2, res3]) => { + let sessionData = {}; + res1[0]._id.completionDate.$date = new Date(res1[0]._id.completionDate.$date); + sessionData = res1[0]; + sessionData['_id']['session_list'] = res2[0]['all']; + this.allSlugSessionInfoList.push(sessionData); + this.completeSlugsList = res3[0]['slug_list']; + }); + } + } + + getComplementaryInformation() { + this._route.paramMap.pipe( + filter((paramMap: ParamMap) => (paramMap.get('id') !== null)), + filter((paramMap: ParamMap) => (paramMap.get('type') !== null)), + switchMap((paramMap: ParamMap) => this._dataLogsService.getComplementaryFromSlugOrSession( + paramMap.get('type'), + paramMap.get('id')))) + .subscribe((response) => { + + // HERE WE HAVE AN ARRAY TO DEAL WITH PAGINATION, NOT WITH AN HTTP GET REQUEST WITH PAGES + this.responseArray = response[0]; + if (this.type === 'session') { + this.nbObjects = response[0].slug_list.length; + this.paginator.length = response[0].slug_list.length; + this.completeSlugsInfos(response[0].slug_list.slice(0, this.paginator.limit), this.id); + } else { + this.nbObjects = response[0].session_id_list.length; + this.paginator.length = response[0].session_id_list.length; + this.getUuidFromSlug(this.id); + this.completeSessionsInfos(this.id, response[0].session_id_list.slice(0, this.paginator.limit)); + } + }); + } + + // When pagination is changed by user, we update with new pagination options + changePagination(pageIndex) { + this.paginator.pageIndex = pageIndex; + if (this.type === 'slug') { + this.completeSessionsInfos(this.id, this.responseArray.session_id_list.slice( + (this.paginator.pageIndex - 1) * (this.paginator.limit), + (this.paginator.pageIndex - 1) * (this.paginator.limit) + this.paginator.limit, + )); + } else { + this.completeSlugsInfos( + this.responseArray.slug_list.slice( + (this.paginator.pageIndex - 1) * (this.paginator.limit), + (this.paginator.pageIndex - 1) * (this.paginator.limit) + this.paginator.limit, + ), + this.id); + } + } + + getAllInfoForOneSlug($event) { + if (this.foundSlug === '') { + this.getComplementaryInformation(); + + } else { + this._dataLogsService.getAllInfoForOneSlug(this.foundSlug).subscribe((results) => { + this.allSlugSessionInfoList = []; + for (const result of results[0]['all']) { + if (result['session_id'] === this.id) { + this.allSlugSessionInfoList[0] = { _id : result }; + this.allSlugSessionInfoList[0]['_id']['slug'] = results[0]['_id']['slug']; + this.allSlugSessionInfoList[0]['_id']['uuid'] = results[0]['_id']['uuid']; + this.allSlugSessionInfoList[0]['_id']['session_list'] = [this.id]; + } + } + // this.paginator.limit = this._dataLogsService.limit; + this.paginator.limit = this.nbOfItemsPerPage; + this.paginator.length = 1; // nb total of elements, cannot use results[0]['all'].length() here + this.paginator.pageIndex = this._dataLogsService.pageNumber; + }, + ); + } + } + getUuidFromSlug(slug) { + this._dataLogsService.getUuidFromSlug(slug).subscribe((result) => { + this.uuid = result[0]['uuid_list'][0]; + }, + ); + } + changePageSize(pageSize) { + this.paginator.pageIndex = 1; + this.paginator.limit = pageSize; + } + +} diff --git a/src/app/components/logs-dashboard/report/logs-report/logs-report.component.html b/src/app/components/logs-dashboard/report/logs-report/logs-report.component.html new file mode 100644 index 0000000000000000000000000000000000000000..8fe82d1fb8e34cc191b2ae6655eea2a2d6d4acb1 --- /dev/null +++ b/src/app/components/logs-dashboard/report/logs-report/logs-report.component.html @@ -0,0 +1,28 @@ +<div class="section page-container"> + <app-page-header [pageInfo]="pageHeaderInfo" [goToThisUrl]='previousUrl' [hideBackButton]="false"></app-page-header> + <span class="bold-text">slug : </span> + <span> {{slug}}</span> + <br> + <span class="bold-text">uuid : </span> + <span> {{uuid}} </span> + <br> + <span class="bold-text">sessionId : </span> + <span> {{sessionId}} </span> + <hr> + <ul class="tabs-container"> + <li *ngFor="let tab of tabs" (click)="tabsToggler(tab.name)" [ngClass]="{'is-active':tab.isActive}"> + <!-- [routerLink]="tab.fullRouterLinkPath" routerLinkActive="is-active" --> + <span class="tab-label">{{ tab.name }}</span> + </li> + </ul> + <div> + <h2>{{errorMessage}}</h2> + + </div> + <app-logs-graph [hidden]="!isToggled || errorMessage!='' " [childUuid]="uuid" [childSessionId]="sessionId" [childSlug]="slug"> + </app-logs-graph> + <app-logs-info [hidden]="isToggled||isToggled==undefined" [childUuid]="uuid" [childSessionId]="sessionId" + [childSlug]="slug"></app-logs-info> + <app-logs-error [hidden]="isToggled!=undefined" [childUuid]="uuid" [childSessionId]="sessionId" [childSlug]="slug"> + </app-logs-error> +</div> diff --git a/src/app/components/logs-dashboard/report/logs-report/logs-report.component.scss b/src/app/components/logs-dashboard/report/logs-report/logs-report.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..82cd2bd4d3c39bf7de735608a6baad5887f52a18 --- /dev/null +++ b/src/app/components/logs-dashboard/report/logs-report/logs-report.component.scss @@ -0,0 +1,144 @@ +@import "../../../../../scss/variables.scss"; +@import "../../../../../../node_modules/bulma/sass/utilities/_all.sass"; + +figure { + text-align: center; + } + figure img{ + max-width: 9.3rem; + display: inline-block; + margin-top: 20px; + } + + .card-header-title { + justify-content: center; + } + + .mini-log-card{ + margin-top: 1.25rem; + padding: 1.25rem; + background-color: yellow; + + } + .input-field{ + width: 200%; + margin-top: 1.25rem; + } + + .hidden { + display: none; + } + .icon { + cursor: pointer; + &:hover { + .fa-plus { + color: lightblue; + } + .fa-trash { + color: #d5232a; + } + } + } + + .bold-text { + font-weight:bold; + display:inline; + } + + .tabs-container { + display: flex; + align-content: stretch; + flex-wrap: wrap; + border-bottom: 1px solid $grey-super-light-color; + + li { + display: flex; + align-items: center; + padding-right: 2rem; + padding-bottom: 0.4rem; + padding-top: 0.4rem; + cursor: pointer; + position: relative; + margin-bottom: 1px; + width: 50%; + + @media screen and (min-width: $tablet) { + width: 33%; + padding-right: 3rem; + } + + @media screen and (min-width: $desktop) { + width: 20%; + } + } + + li:last-child { + padding-right: 0; + } + + .tab-label { + font-size: 1rem; + font-weight: 500; + color: $brand-color; + } + + li:hover { + .tab-label { + font-weight: 700; + } + } + + li:focus{ + .tab-label { + color: $tomato-color; + font-weight: 700; + } + } + li.is-active { + .tab-label { + color: $tomato-color; + font-weight: 600; + } + } + + li::after { + content: ''; + display: block; + height: .1rem; + background: $tomato-color; + width: 0; + position: absolute; + bottom: -.1rem; + } + + li.is-active::after { + transition: width 0.3s; + width: 100%; + } + + + + .full-width { + width: 100%; + } + + h1 { + text-align: center + } + + .icon { + cursor: pointer; + &:hover { + .fa-plus { + color: lightblue; + } + .fa-trash { + color: #d5232a; + } + } + } + .hidden { + display: none; + } + +} diff --git a/src/app/components/logs-dashboard/report/logs-report/logs-report.component.ts b/src/app/components/logs-dashboard/report/logs-report/logs-report.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..291c80c911729049a79e429b735899165f8b827f --- /dev/null +++ b/src/app/components/logs-dashboard/report/logs-report/logs-report.component.ts @@ -0,0 +1,102 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { DataLogsService } from 'src/app/services/data-logs.service'; +import { IPageHeaderInfo } from 'src/app/models/page.model'; +import { ISimpleTab } from 'src/app/models/basic-tabs.model'; + +@Component({ + selector: 'app-logs-report', + templateUrl: './logs-report.component.html', + styleUrls: ['./logs-report.component.scss'], +}) +export class LogsReportComponent implements OnInit { + pageHeaderInfo: IPageHeaderInfo = { + title: "Rapport final d'indexation", + }; + errorMessage: string = ''; + isToggled: boolean = true; + form: FormGroup; + title: string; + slug: string; + uuid: string; + sessionId: string; + urlCode: string = ''; + previousUrl: string; + tabs: ISimpleTab[] = [ + { + name: 'Graphique', + isActive: true, + }, + { + name: 'Infos', + isActive: false, + }, + { + name: 'Erreurs', + isActive: false, + }, + ]; + constructor( + private _dataLogsService: DataLogsService, + private _route: ActivatedRoute, + private _router: Router, + private _fb: FormBuilder, + ) { + } + + ngOnInit() { + this.title = this._route.snapshot.data.title; + this.sessionId = this._route.snapshot.paramMap.get('sessionId'); + this.slug = this._route.snapshot.paramMap.get('slug'); + this.urlCode = this._route.snapshot.paramMap.get('urlCode'); + if (this.urlCode === 'session') { + this.previousUrl = `/datalogs/preReport/session/${this.sessionId}`; + + } else if (this.urlCode === 'slug') { + this.previousUrl = `/datalogs/preReport/slug/${this.slug}`; + } else { + this.previousUrl = '/datalogs'; + } + this.getUuidFromSlug(this.slug); + } + + getUuidFromSlug(slug) { + this._dataLogsService.getUuidFromSlug(slug).subscribe((result) => { + try { + this.uuid = result[0].uuid_list[0]; + } catch (e) { + this.errorMessage = "Pas d'uuid pour ce slug"; + console.log(this.errorMessage, e); + } + }, + ); + } + + tabsToggler(tabName) { + if (tabName === 'Graphique') { + console.log(this.isToggled); + this.isToggled = true; + this.tabs[1].isActive = false; + this.tabs[0].isActive = true; + this.tabs[2].isActive = false; + + } + if (tabName === 'Infos') { + console.log(this.isToggled); + this.isToggled = false; + this.tabs[1].isActive = true; + this.tabs[0].isActive = false; + this.tabs[2].isActive = false; + + } + if (tabName === 'Erreurs') { + console.log(this.isToggled); + this.isToggled = undefined; + this.tabs[2].isActive = true; + this.tabs[1].isActive = false; + this.tabs[0].isActive = false; + } + } + +} diff --git a/src/app/components/media/detail/media-detail.component.html b/src/app/components/media/detail/media-detail.component.html new file mode 100644 index 0000000000000000000000000000000000000000..7090aa943f775d8028fb0db21a90dd36ea6d21ce --- /dev/null +++ b/src/app/components/media/detail/media-detail.component.html @@ -0,0 +1,59 @@ +<section class="section page-container" *ngIf="media"> + <app-page-header [pageInfo]="{title: title}"></app-page-header> + <div class="columns is-centered"> + <div class="column is-8"> + <div class="card"> + <div class="card-image"> + <figure class="image"> + <img [src]="media.url" alt="Preview du media" onerror="this.src='./assets/img/default-file-logo.png';"> + </figure> + </div> + <div class="card-content"> + + <div class="content"> + <div> + <span class="has-text-weight-bold">URL: </span> + <span *ngIf="media.url; else emptyProperty">{{media.url}}</span> + </div> + <br> + <div> + <span class="has-text-weight-bold">Alt: </span> + <span *ngIf="media.alt; else emptyProperty">{{media.alt}}</span> + </div> + <br> + <div> + <span class="has-text-weight-bold">Caption: </span> + <span *ngIf="media.caption; else emptyProperty">{{media.caption}}</span> + </div> + <br> + <div> + <span class="has-text-weight-bold">Date de création: </span> + <span + *ngIf="media.creationDate; else emptyProperty">{{media.creationDate | date:'EE dd/MM/yyyy HH:mm:ss'}}</span> + </div> + <br> + <div> + <span class="has-text-weight-bold">Date de modification: </span> + <span + *ngIf="media.creationDate; else emptyProperty">{{media.updateDate | date:'EE dd/MM/yyyy HH:mm:ss'}}</span> + </div> + <br> + <div *ngIf="isImage"> + <span class="has-text-weight-bold">Ghost snippet: </span> + <span class="comment">(Ce code html est fait pour intégrer l'image en taille normal. Pour une taille + 'wide' ajoutez la classe 'kg-width-wide' à la balise figure. Pour une taille 'full' ajoutez la classe + 'kg-width-full' à la balise figure)</span> + <pre class="hljs code-preview" [innerHTML]="ghostSnippet"> + <!-- <code ></code> --> + </pre> + </div> + </div> + </div> + </div> + </div> + </div> +</section> + +<ng-template #emptyProperty> + <span class="empty-property">Non renseigné</span> +</ng-template> \ No newline at end of file diff --git a/src/app/components/media/detail/media-detail.component.scss b/src/app/components/media/detail/media-detail.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..92575e3163a599b02afc6efaf4d4e8ef45cfc3e1 --- /dev/null +++ b/src/app/components/media/detail/media-detail.component.scss @@ -0,0 +1,37 @@ +@import '../../../../scss/variables.scss'; + +figure { + text-align: center; +} + +figure img { + max-width: 150px; + display: inline-block; + margin-top: 20px; +} + +.card-header-title { + justify-content: center; +} + +.code-preview { + white-space: pre-wrap; + /* css-3 */ + white-space: -moz-pre-wrap; + /* Mozilla, since 1999 */ + white-space: -pre-wrap; + /* Opera 4-6 */ + white-space: -o-pre-wrap; + /* Opera 7 */ + word-wrap: break-word; + /* Internet Explorer 5.5+ */ + + padding: 0.5rem; + margin-top: 0.5rem; +} + +.comment { + font-size: 12px; + font-style: italic; + color: $grey-super-light-color; +} diff --git a/src/app/components/media/detail/media-detail.component.ts b/src/app/components/media/detail/media-detail.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..03b0919af84fb3118ebe52143101c777a0070464 --- /dev/null +++ b/src/app/components/media/detail/media-detail.component.ts @@ -0,0 +1,55 @@ + +import { switchMap } from 'rxjs/operators'; +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, ParamMap } from '@angular/router'; +import { Media } from '../../../models/media.model'; +import { MediaService } from '../../../services'; +import { highlight } from 'highlight.js'; +import * as isImageUrl from 'is-image-url'; +@Component({ + selector: 'app-media-detail', + templateUrl: './media-detail.component.html', + styleUrls: ['./media-detail.component.scss'], +}) +export class MediaDetailComponent implements OnInit { + + media: Media; + title: string; + + constructor( + private _route: ActivatedRoute, + private _mediaService: MediaService, + ) { + } + + ngOnInit(): void { + this.title = this._route.snapshot.data.title; + this._route.paramMap.pipe( + switchMap((params: ParamMap) => this._mediaService.findById(params.get('id')))) + .subscribe((media: Media) => { + this.media = media; + }); + } + + get isImage() { + return isImageUrl(this.media.url); + } + + // [WARNING] Keep that weird indentation for correct display on the web page + get ghostSnippet() { + let html; + if (this.media.caption) { + html = `<figure class="kg-card kg-image-card kg-card-hascaption"> + <img src="${this.media.url}" alt="${this.media.alt}" class="kg-image"> + <figcaption> + ${this.media.caption} + </figcaption> +</figure>`; + } else { + html = `<figure class="kg-card kg-image-card"> + <img src="${this.media.url}" alt="${this.media.alt}" class="kg-image"> +</figure>`; + } + return highlight('html', html).value; + } +} diff --git a/src/app/components/media/edit/media-form.component.html b/src/app/components/media/edit/media-form.component.html new file mode 100644 index 0000000000000000000000000000000000000000..bee0a47a9583528ae7de6d3f31d192fbb8a4e362 --- /dev/null +++ b/src/app/components/media/edit/media-form.component.html @@ -0,0 +1,45 @@ +<section class="section page-container" *ngIf="media"> + <app-page-header [pageInfo]="{title: title}"></app-page-header> + <form [formGroup]="form" (ngSubmit)="onSubmit()" class="columns is-centered is-marginless is-multiline"> + <div class="column is-7"> + <input type="hidden" formControlName="_id" [value]="media._id"> + + <app-image-upload [fieldParams]="logoFieldParams" (fileChanged)="logoChanged($event)" + (imageRemoved)="removeLogo()" *ngIf="!media.url; else FilePreview"> + </app-image-upload> + + <ng-template #FilePreview> + <div class="file-preview-container"> + <img [src]="media.url" alt="" onerror="this.src='./assets/img/default-file-logo.png';"> + </div> + </ng-template> + + <div class="field" *ngIf="media._id"> + <label class="label" for="url">URL</label> + <div class="control"> + <input class="input" id="url" type="text" [value]="media.url" disabled> + </div> + </div> + + + <div class="field"> + <label class="label" for="alt">Alt</label> + <div class="control"> + <input class="input" type="text" formControlName="alt" id="alt"> + </div> + </div> + + <div class="field"> + <label class="label" for="caption">Caption</label> + <div class="control"> + <input class="input" type="text" formControlName="caption" id="caption"> + </div> + </div> + + <br> + <div class="has-text-right"> + <button class="button button-gl" type="submit" [disabled]="formInvalid == true">Valider</button> + </div> + </div> + </form> +</section> \ No newline at end of file diff --git a/src/app/components/media/edit/media-form.component.scss b/src/app/components/media/edit/media-form.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..a7d23f79fef654ceb378a90dbcf0026a70528d42 --- /dev/null +++ b/src/app/components/media/edit/media-form.component.scss @@ -0,0 +1,19 @@ +.full-width { + width: 100%; +} + +.page-container { + position: relative; +} + +h1 { + text-align: center +} + +.file-preview-container { + text-align: center; + + img { + max-height: 5rem; + } +} diff --git a/src/app/components/media/edit/media-form.component.ts b/src/app/components/media/edit/media-form.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..4f2b06928bdc3db593e7f2ded4d1fa5edfcbcbba --- /dev/null +++ b/src/app/components/media/edit/media-form.component.ts @@ -0,0 +1,132 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, ParamMap, Router } from '@angular/router'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { filter, switchMap } from 'rxjs/operators'; +import { IImageUploadFieldParams } from 'src/app/models/image-upload.model'; +import { NotificationService, MediaService } from 'src/app/services'; +import { Media, MediaDTO } from '../../../models/media.model'; + +@Component({ + selector: 'app-media-form', + templateUrl: './media-form.component.html', + styleUrls: ['./media-form.component.scss'], +}) +export class MediaFormComponent implements OnInit { + + media: Media; + form: FormGroup; + file: File; + logoFieldParams: IImageUploadFieldParams = { + inputName: 'file', + label: 'File', + existingImageUrl: null, + isRequired: true, + }; + title: string; + + constructor( + private _mediaService: MediaService, + private _notificationService: NotificationService, + private _route: ActivatedRoute, + private _router: Router, + private _fb: FormBuilder, + ) { + } + + ngOnInit() { + this.title = this._route.snapshot.data.title; + this.initForm(); + + this._route.paramMap.pipe( + filter((paramMap: ParamMap) => (paramMap.get('id') !== null)), + switchMap((paramMap: ParamMap) => this._mediaService.findById(paramMap.get('id')))) + .subscribe((media: Media) => { + + this.media = media; + + this.logoFieldParams.existingImageUrl = media.url; + + this.form = this._fb.group( + { + _id: [this.media._id], + alt: [media.alt ? media.alt : null], + caption: [media.caption ? media.caption : null], + }); + }); + + } + + initForm() { + this.media = new Media(); + this.logoFieldParams.existingImageUrl = null; + this.form = this._fb.group( + { + _id: [this.media._id], + alt: [this.media.alt ? this.media.alt : null], + caption: [this.media.caption ? this.media.caption : null], + }); + } + + onSubmit() { + if (!this.formInvalid) { + + if (this.media._id) { + this._mediaService.update(new MediaDTO(this.form.value)).subscribe( + (mediaCreated) => { + this._notificationService.notify({ + message: 'Le média a été mis à jour avec succès.', + type: 'success', + }); + this._router.navigate(['/media', mediaCreated._id]); + }, + (err) => { + this._notificationService.notify({ + message: 'Une erreur est survenue lors de la mise à jour du média', + type: 'error', + }); + }, + ); + } else { + this._mediaService.uploadFile(this.file, new MediaDTO(this.form.value)).subscribe( + (mediaCreated) => { + this._notificationService.notify({ + message: 'Le fichier a été uploadé avec succès.', + type: 'success', + }); + this._router.navigate(['/media', mediaCreated._id]); + }, + (err) => { + this._notificationService.notify({ + message: 'Une erreur est survenue lors de l\'upload du fichier', + type: 'error', + }); + }, + ); + } + } + } + + // Getters for each property + get alt() { + return this.form.controls['alt']; + } + + get caption() { + return this.form.controls['caption']; + } + + get formInvalid() { + return this.form.invalid || + (!this.media.url && !this.file); + } + + logoChanged(fileList: FileList) { + if (fileList && fileList.length > 0) { + this.file = fileList[0]; + } + } + + removeLogo() { + this.form.get('file').setValue(null); + } +} diff --git a/src/app/components/media/list/media.component.html b/src/app/components/media/list/media.component.html new file mode 100644 index 0000000000000000000000000000000000000000..5de5ffb98aa89ef2d7d4364b0aaca60b9c752cf6 --- /dev/null +++ b/src/app/components/media/list/media.component.html @@ -0,0 +1,161 @@ +<div class="section page-container"> + <app-page-header [pageInfo]="pageHeaderInfo" [hideBackButton]="true"></app-page-header> + <div class="add-item-link has-text-right"> + <a class="button button-gl" [routerLink]="['new']"> + Ajouter + </a> + </div> + <div class="table entity-list-table" *ngIf="media"> + <div class="header columns is-marginless"> + <div class="column is-1 has-text-centered"> + <span class="column-title">Logo</span> + </div> + <div class="column is-4"> + <span (click)="sortBy('url')" class="is-sortable"> + <span class="sort-icons"> + <span class="icon"> + <i class="fas fa-sort-up" + [ngClass]="{'icon-red': sortOptions.value === 'url' && sortOptions.order === 'desc'}"></i> + </span> + <span class="icon"> + <i class="fas fa-sort-down" + [ngClass]="{'icon-red': sortOptions.value === 'url' && sortOptions.order === 'asc'}"></i> + </span> + </span> + <span class="column-title" [ngClass]="{'active': sortOptions.value === url}">URL</span> + </span> + </div> + <div class="column is-1"> + <span (click)="sortBy('alt')" class="is-sortable"> + <span class="sort-icons"> + <span class="icon"> + <i class="fas fa-sort-up" + [ngClass]="{'icon-red': sortOptions.value === 'alt' && sortOptions.order === 'desc'}"></i> + </span> + <span class="icon"> + <i class="fas fa-sort-down" + [ngClass]="{'icon-red': sortOptions.value === 'alt' && sortOptions.order === 'asc'}"></i> + </span> + </span> + <span class="column-title" [ngClass]="{'active': sortOptions.value === alt}">Alt</span> + </span> + </div> + <div class="column is-1"> + <span (click)="sortBy('caption')" class="is-sortable"> + <span class="sort-icons"> + <span class="icon"> + <i class="fas fa-sort-up" + [ngClass]="{'icon-red': sortOptions.value === 'caption' && sortOptions.order === 'desc'}"></i> + </span> + <span class="icon"> + <i class="fas fa-sort-down" + [ngClass]="{'icon-red': sortOptions.value === 'caption' && sortOptions.order === 'asc'}"></i> + </span> + </span> + <span class="column-title" [ngClass]="{'active': sortOptions.value === caption}">Légende</span> + </span> + </div> + <div class="column is-2"> + <span (click)="sortBy('creationDate')" class="is-sortable"> + <span class="sort-icons"> + <span class="icon"> + <i class="fas fa-sort-up" + [ngClass]="{'icon-red': sortOptions.value === 'creationDate' && sortOptions.order === 'desc'}"></i> + </span> + <span class="icon"> + <i class="fas fa-sort-down" + [ngClass]="{'icon-red': sortOptions.value === 'creationDate' && sortOptions.order === 'asc'}"></i> + </span> + </span> + <span class="column-title">Date de création</span> + </span> + </div> + <div class="column is-3 has-text-centered"> + <span class="column-title">Actions</span> + </div> + </div> + <div class="data-list"> + <div class="data columns is-multiline is-vcentered is-marginless" + *ngFor="let item of media; let i=index; let odd=odd; let even=even;" [ngClass]="{ odd: odd, even: even }"> + <div class="column is-1 has-text-centered"> + <img class="entity-logo-in-list" [src]="item.url" alt="" + onerror="this.src='./assets/img/default-file-logo.png';"> + </div> + <div class="column is-4"> + <span>{{ item.url}}</span> + </div> + <div class="column is-1"> + <span>{{ item.alt}}</span> + </div> + <div class="column is-1"> + <span>{{ item.caption}}</span> + </div> + <div class="column is-2"> + <span>{{ item.creationDate | date:'EE dd/MM/yyyy HH:mm:ss'}}</span> + </div> + <div class="column is-3 has-text-centered actions"> + <button + title="Copier dans le presse-papier le snippet html à copier dans un article/page ghost pour embarquer l'image en mode normal" + class="button copy-button" *ngIf="isImage(item.url)" [ngClass]="{'copied': copiedIndex === i}" + (click)="copyGhostHTMLSnippet(item, '', i)"> + <span *ngIf="copiedIndex === i && copiedSize === ''; else imageNormal">Copié !</span> + <ng-template #imageNormal> + <svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"> + <g fill="#242b3f" fill-rule="nonzero"> + <path d="M2 3a1 1 0 0 1 0-2h12a1 1 0 0 1 0 2H2zm0 12a1 1 0 0 1 0-2h12a1 1 0 0 1 0 2H2z" opacity=".6"> + </path> + <path d="M2 5h12a1 1 0 0 1 1 1v4.001a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1z"></path> + </g> + </svg> + </ng-template> + </button> + <button + title="Copier dans le presse-papier le snippet html à copier dans un article/page ghost pour embarquer l'image en mode wide" + class="button copy-button" *ngIf="isImage(item.url)" [ngClass]="{'copied': copiedIndex === i}" + (click)="copyGhostHTMLSnippet(item, 'kg-width-wide', i)"> + <span *ngIf="copiedIndex === i && copiedSize === 'kg-width-wide'; else imageWide">Copié !</span> + <ng-template #imageWide> + <svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"> + <g fill="#242b3f" fill-rule="nonzero"> + <path d="M6 3a1 1 0 0 1 0-2h4a1 1 0 0 1 0 2H6zm0 12a1 1 0 0 1 0-2h4a1 1 0 0 1 0 2H6z" opacity=".6"> + </path> + <path d="M2 5h12a1 1 0 0 1 1 1v4.001a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1z"></path> + </g> + </svg> + </ng-template> + </button> + <button + title="Copier dans le presse-papier le snippet html à copier dans un article/page ghost pour embarquer l'image en mode full" + class="button copy-button" *ngIf="isImage(item.url)" [ngClass]="{'copied': copiedIndex === i}" + (click)="copyGhostHTMLSnippet(item, 'kg-width-full', i)"> + <span *ngIf="copiedIndex === i && copiedSize === 'kg-width-full'; else imageFull">Copié !</span> + <ng-template #imageFull> + <svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"> + <g fill="#242b3f" fill-rule="nonzero"> + <path d="M0 2a1 1 0 0 1 2 0v12a1 1 0 0 1-2 0V2zm14 0a1 1 0 0 1 2 0v12a1 1 0 0 1-2 0V2z" opacity=".6"> + </path> + <path + d="M10.626 7L9.312 5.691a1 1 0 1 1 1.411-1.417l3.029 3.017c.39.389.392 1.02.005 1.412l-3.029 3.059a1 1 0 0 1-1.421-1.407L10.648 9H5.415l1.342 1.355a1 1 0 0 1-1.422 1.407L2.307 8.703a1 1 0 0 1 .005-1.412L5.34 4.274a1 1 0 0 1 1.412 1.417L5.438 7h5.188z"> + </path> + </g> + </svg> + </ng-template> + </button> + <app-crud-buttons [id]="item._id" (delete)="displayDeletePopup($event)"></app-crud-buttons> + </div> + </div> + </div> + <div class="columns is-marginless paginator"> + <div class="column"> + <app-paginator *ngIf="paginator.length > 0" [length]="paginator.length" [pageSize]="paginator.limit" + [pageSizeOptions]="paginator.pageSizeOptions" [pageIndex]="paginator.pageIndex" [pagesToShow]="5" + [showFirstLastButtons]="true" (page)="changePagination($event)" (pageSizeChanged)="changePageSize($event)"> + </app-paginator> + </div> + </div> + </div> +</div> + +<app-confirmation-modal (cancel)="objectToBeDeletedId=null" (continue)="deleteMedia()" [texts]="deleteModalTexts" + [isOpened]="objectToBeDeletedId !== null"> +</app-confirmation-modal> \ No newline at end of file diff --git a/src/app/components/media/list/media.component.scss b/src/app/components/media/list/media.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..bab3d05913a7d0f533ad657e13fc02e135df5c46 --- /dev/null +++ b/src/app/components/media/list/media.component.scss @@ -0,0 +1,30 @@ +img { + max-width: 100px; +} + +.data span { + word-break: break-all; +} + +.copy-button { + background-color: transparent; + border: none; + padding-left: 0.25rem; + padding-right: 0.25rem; + + svg { + height: 15px; + } + + svg:hover { + g { + fill: #3273dc; + } + } +} + +.actions { + display: flex; + justify-content: center; + align-items: center; +} diff --git a/src/app/components/media/list/media.component.ts b/src/app/components/media/list/media.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..da2a33251212bb8b777985c465d1fb7eebf9b4ad --- /dev/null +++ b/src/app/components/media/list/media.component.ts @@ -0,0 +1,173 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { PaginatorOptions } from 'src/app/models/paginator-options.model'; +import { IPageHeaderInfo } from '../../../models/page.model'; +import { NotificationService, MediaService } from '../../../services'; +import { Media, MediaRO } from '../../../models/media.model'; +import * as isImageUrl from 'is-image-url'; + +@Component({ + selector: 'app-media', + templateUrl: './media.component.html', + styleUrls: ['./media.component.scss'], +}) +export class MediaComponent implements OnInit, OnDestroy { + + pageHeaderInfo: IPageHeaderInfo = { + title: '', + }; + objectToBeDeletedId = null; + deleteModalTexts = { + main: 'Si vous poursuivez, le média sera définitivement supprimé.', + cancel: 'Annuler', + continue: 'Supprimer', + }; + copiedIndex = null; + copiedSize = null; + media: Media[] = []; + searchChangeSub: Subscription; + + // Paginator options + paginator: PaginatorOptions; + + sortValue: string; + + totalElement: number; + pageSize = 10; + pageSizeOptions = [5, 10, 25, 100]; + filters = { + name: '', + }; + where = {}; + + constructor( + private _mediaService: MediaService, + private _notificationService: NotificationService, + ) { + this.paginator = { + pageIndex: this._mediaService.pageNumber, + length: 0, + limit: this._mediaService.limit, + pageSizeOptions: [5, 10, 20], + }; + } + + ngOnInit(): void { + this._mediaService.sortOptions = { + value: 'name', + order: 'asc', + }; + this.search(); + + this.searchChangeSub = this._mediaService.searchChange$.subscribe( + () => { + this.search(); + }, + ); + } + + private search() { + this._mediaService.getMedia() + .subscribe( + (items: MediaRO) => { + this.media = items.media; + this.totalElement = items.mediaCount; + + this.pageHeaderInfo.title = items.mediaCount ? + `${this.totalElement} médias trouvés` : `${this.totalElement} média trouvé`; + + this.paginator.limit = this._mediaService.limit; + this.paginator.pageIndex = this._mediaService.pageNumber; + this.paginator.length = items.mediaCount; + }, + () => { + this.pageHeaderInfo.title = '0 média trouvé'; + this._notificationService.notify({ + type: 'error', + message: 'Une erreur est survenue lors du chargement des médias.', + }); + }, + ); + } + + isImage(url) { + return isImageUrl(url); + } + + copyGhostHTMLSnippet(media: Media, size: string, id) { + if (media.caption) { + navigator['clipboard'].writeText(`<figure class="kg-card kg-image-card kg-card-hascaption ${size}"> + <img src="${media.url}" alt="${media.alt}" class="kg-image"> + <figcaption> + ${media.caption} + </figcaption> +</figure>`); + } else { + navigator['clipboard'].writeText(`<figure class="kg-card kg-image-card ${size}"> + <img src="${media.url}" alt="${media.alt}" class="kg-image"> +</figure>`); + } + this.copiedIndex = id; + this.copiedSize = size; + setTimeout( + () => { + this.copiedIndex = null; + this.copiedSize = null; + }, + 3000, + ); + } + + // When pagination is changed by user, we update datasetList with new pagination options + changePagination(pageIndex) { + this._mediaService.paginationChanged(this.paginator.limit, pageIndex); + } + + changePageSize(pageSize) { + this._mediaService.paginationChanged(pageSize, 1); + } + + sortBy(key: string) { + if (this._mediaService.sortOptions.value === key) { + this._mediaService.reverseSortOrder(); + } else { + this._mediaService.sortOptions.value = key; + this._mediaService.sortOptions.order = 'asc'; + } + this.search(); + } + + get sortOptions() { + return this._mediaService.sortOptions; + } + + displayDeletePopup(id) { + this.objectToBeDeletedId = id; + } + + deleteMedia() { + this._mediaService.delete(this.objectToBeDeletedId).subscribe( + () => { + this._notificationService.notify({ + type: 'success', + message: 'Le média a été supprimé avec succès.', + }); + this._mediaService.pageNumber = 1; + this.search(); + }, + () => { + this._notificationService.notify({ + type: 'error', + message: 'Une erreur est survenue lors de la suppression du média.', + }); + }, + () => { + this.objectToBeDeletedId = null; + }, + ); + } + + ngOnDestroy() { + this.searchChangeSub.unsubscribe(); + } +} diff --git a/src/app/components/menu/menu.component.html b/src/app/components/menu/menu.component.html index f0b484894ad4d17f26478e1cf4342647ca1cfecd..eba774f49829c69f7b2e76702b927a899d8b2ba1 100644 --- a/src/app/components/menu/menu.component.html +++ b/src/app/components/menu/menu.component.html @@ -10,7 +10,21 @@ </g> </svg> </span> - <span class="label-menu">Producteurs de données</span> + <span class="label-menu">Partenaires</span> + </a> + </li> + <li><a [routerLink]="['/', 'credits']" routerLinkActive="active-link"> + <span class="icon"> + <i class="fas fa-dollar-sign"></i> + </span> + <span class="label-menu">Crédits</span> + </a> + </li> + <li><a [routerLink]="['/', 'reutilisations']" routerLinkActive="active-link"> + <span class="icon"> + <i class="fas fa-recycle"></i> + </span> + <span class="label-menu">Réutilisations</span> </a> </li> <li><a [routerLink]="['/', 'resources']" routerLinkActive="active-link"> @@ -27,5 +41,33 @@ <span class="label-menu">Formats</span> </a> </li> + <li><a [routerLink]="['/', 'projections']" routerLinkActive="active-link"> + <span class="icon"> + <i class="fas fa-map-marked-alt"></i> + </span> + <span class="label-menu">Projections</span> + </a> + </li> + <li><a [routerLink]="['/', 'changelog']" routerLinkActive="active-link"> + <span class="icon"> + <i class="fas fa-clipboard-list"></i> + </span> + <span class="label-menu">Changelog</span> + </a> + </li> + <li><a [routerLink]="['/', 'media']" routerLinkActive="active-link"> + <span class="icon"> + <i class="far fa-images"></i> + </span> + <span class="label-menu">Médias</span> + </a> + </li> + <li><a [routerLink]="['/', 'datalogs']" routerLinkActive="active-link"> + <span class="icon"> + <i class="far fa-chart-bar"></i> + </span> + <span class="label-menu">Logs d'indexation</span> + </a> + </li> </ul> </aside> \ No newline at end of file diff --git a/src/app/components/menu/menu.component.scss b/src/app/components/menu/menu.component.scss index abe3f4fa0a50204dbd6df6b16495961d7f709354..5542d59ed389b9fbe7d17cd11a0351ab2ff729db 100644 --- a/src/app/components/menu/menu.component.scss +++ b/src/app/components/menu/menu.component.scss @@ -22,6 +22,7 @@ width: 0; opacity: 0; overflow: hidden; + line-height: 1.3; } } diff --git a/src/app/components/organizations/detail/organization-detail.component.html b/src/app/components/organizations/detail/organization-detail.component.html index fcde85111da378bd3530e34756395e9fa74f2396..07d17543f8a43a148d7d126a17da7bc0368d0467 100644 --- a/src/app/components/organizations/detail/organization-detail.component.html +++ b/src/app/components/organizations/detail/organization-detail.component.html @@ -10,7 +10,7 @@ </header> <div class="card-image"> <figure class="image"> - <img [src]="organization.logo" alt="Logo du producteur de données"> + <img [src]="organization.logo" alt="Logo du partenaire"> </figure> </div> <div class="card-content"> @@ -20,6 +20,11 @@ <p>{{organization.description}}</p> </div> <br> + <div> + <p><span class="has-text-weight-bold">Statut:</span> {{ organization.published ? 'Publié' : 'Brouillon' }} + </p> + </div> + <br> <div> <p><span class="has-text-weight-bold">Id:</span> {{ organization.id}}</p> </div> diff --git a/src/app/components/organizations/detail/organization-detail.component.scss b/src/app/components/organizations/detail/organization-detail.component.scss index dff286c3a01ee11891ef45a0378e9fa7e35f4172..63a36307795f8cebd94c864bbbaa3deba1ae7da7 100644 --- a/src/app/components/organizations/detail/organization-detail.component.scss +++ b/src/app/components/organizations/detail/organization-detail.component.scss @@ -1,7 +1,8 @@ figure { text-align: center; } -figure img{ + +figure img { max-width: 150px; display: inline-block; margin-top: 20px; @@ -9,4 +10,4 @@ figure img{ .card-header-title { justify-content: center; -} \ No newline at end of file +} diff --git a/src/app/components/organizations/edit/organization-form.component.html b/src/app/components/organizations/edit/organization-form.component.html index dc8ea2d64fa1c4dad0e86a157b1f452201085088..840ba6a7347d4fb853b78c8bb110d2c6682fa340 100644 --- a/src/app/components/organizations/edit/organization-form.component.html +++ b/src/app/components/organizations/edit/organization-form.component.html @@ -1,18 +1,26 @@ <section class="section page-container" *ngIf="organization"> - <app-page-header [pageInfo]="{title: title}"></app-page-header> - - <form [formGroup]="form" (ngSubmit)="onSubmit()" class="columns is-centered is-marginless"> + <form [formGroup]="form" (ngSubmit)="onSubmit()" class="columns is-centered is-marginless is-multiline"> + <div class="column is-12 header-with-publication-status"> + <app-page-header [pageInfo]="{title: title}"></app-page-header> + <div class="field status-field"> + <span class="fake-label" *ngIf="form.get('published').value === true">Publié</span> + <span class="fake-label" *ngIf="form.get('published').value === false">Brouillon</span> + <input id="published" type="checkbox" formControlName="published" class="switch is-rounded"> + <label for="published"></label> + </div> + </div> <div class="column is-7"> - <input type="hidden" formControlName="id" value="{{organization.id}}"> + <input type="hidden" formControlName="id" [value]="organization.id"> + <div class="field"> <label class="label required" for="name">Nom</label> <div class="control"> - <input class="input" type="text" [value]="organization.name" formControlName="name" id="name" required> + <input class="input" type="text" formControlName="name" id="name" required> </div> <div *ngIf="name.invalid && (name.dirty || name.touched)" class="alert alert-danger"> <p *ngIf="name.errors['required']" class="help is-danger"> - Le nom du producteur de données est obligatoire. + Le nom du partenaire est obligatoire. </p> </div> </div> @@ -30,16 +38,15 @@ </div> <div *ngIf="description.invalid && (description.dirty || description.touched)" class="alert alert-danger"> <p *ngIf="description.errors['required']" class="help is-danger"> - La description du producteur de données est obligatoire. + La description du partenaire est obligatoire. </p> </div> </div> <div class="field"> - <label class="label" for="elasticSearchName">Nom ElasticSearch du producteur de données</label> + <label class="label" for="elasticSearchName">Nom ElasticSearch du partenaire</label> <div class="control"> - <input class="input" type="text" [value]="organization.elasticSearchName" formControlName="elasticSearchName" - id="elasticSearchName"> + <input class="input" type="text" formControlName="elasticSearchName" id="elasticSearchName"> </div> </div> diff --git a/src/app/components/organizations/edit/organization-form.component.scss b/src/app/components/organizations/edit/organization-form.component.scss index 5bea26fe55b4ab7bb07da39d6140df0c988864d4..0762aa33dcef6109e5521239a03190ee9c3670a1 100644 --- a/src/app/components/organizations/edit/organization-form.component.scss +++ b/src/app/components/organizations/edit/organization-form.component.scss @@ -2,18 +2,24 @@ width: 100%; } +.page-container { + position: relative; +} + h1 { text-align: center } .icon { cursor: pointer; + &:hover { .fa-plus { color: lightblue; } + .fa-trash { color: #d5232a; } } -} \ No newline at end of file +} diff --git a/src/app/components/organizations/edit/organization-form.component.ts b/src/app/components/organizations/edit/organization-form.component.ts index b8a22a5f7fee592f7a91fbfb7863edc7f4d49832..27945895ec6262a9ef666b20209a435ce79f3747 100644 --- a/src/app/components/organizations/edit/organization-form.component.ts +++ b/src/app/components/organizations/edit/organization-form.component.ts @@ -3,9 +3,11 @@ import { ActivatedRoute, ParamMap, Router } from '@angular/router'; import { Organization } from 'src/app/models/organization.model'; import { OrganizationService } from 'src/app/services/organization.service'; import { FormBuilder, FormGroup, Validators, FormArray } from '@angular/forms'; -import { filter, switchMap, mergeMap } from 'rxjs/operators'; +import { filter, switchMap, mergeMap, catchError } from 'rxjs/operators'; import { IImageUploadFieldParams } from 'src/app/models/image-upload.model'; -import { NotificationService } from 'src/app/services'; +import { NotificationService, MediaService } from 'src/app/services'; +import { throwError } from 'rxjs'; +import { Media } from '../../../models/media.model'; @Component({ selector: 'app-organization-form', @@ -23,11 +25,11 @@ export class OrganizationFormComponent implements OnInit { existingImageUrl: null, isRequired: true, }; - logo: File; title: string; constructor( private _organizationService: OrganizationService, + private _mediaService: MediaService, private _notificationService: NotificationService, private _route: ActivatedRoute, private _router: Router, @@ -64,6 +66,7 @@ export class OrganizationFormComponent implements OnInit { elasticSearchName: [organization.elasticSearchName], logo: [organization.logo], links: arr, + published: [this.organization.published], }); }); @@ -81,6 +84,7 @@ export class OrganizationFormComponent implements OnInit { elasticSearchName: [this.organization.elasticSearchName], logo: [this.organization.logo], links: arr, + published: [this.organization.published], }); } @@ -115,29 +119,100 @@ export class OrganizationFormComponent implements OnInit { onSubmit() { if (!this.formInvalid) { this.organization = new Organization(this.form.value); - let action; - if (this.logoFile) { - action = this._organizationService.uploadLogoAndSaveOrganization(this.logoFile, this.organization); + if (this.organization.id) { + if (this.logoFile) { + this._mediaService.uploadFile(this.logoFile).pipe( + catchError(() => { + return throwError('Une erreur est survenue lors de l\'upload du logo du partenaire.'); + }), + mergeMap((response: Media) => { + this.organization.logo = response.url; + return this._organizationService.update(this.organization).pipe( + catchError(() => { + return throwError('Une erreur est survenue lors de la mise à jour du partenaire.'); + }), + ); + }), + ).subscribe( + (organizationUpdated) => { + this._notificationService.notify({ + message: 'Le partenaire a été mis à jour avec succès.', + type: 'success', + }); + this._router.navigate(['/producteur-de-donnees', organizationUpdated.id]); + }, + (err) => { + this._notificationService.notify({ + message: err, + type: 'error', + }); + }, + ); + } else { + return this._organizationService.update(this.organization).subscribe( + (organizationUpdated) => { + this._notificationService.notify({ + message: 'Le partenaire a été mis à jour avec succès.', + type: 'success', + }); + this._router.navigate(['/producteur-de-donnees', organizationUpdated.id]); + }, + () => { + this._notificationService.notify({ + message: 'Une erreur est survenue lors de la mise à jour du partenaire.', + type: 'error', + }); + }, + ); + } } else { - action = this._organizationService.replaceOrCreate(this.organization); + if (this.logoFile) { + this._mediaService.uploadFile(this.logoFile).pipe( + catchError(() => { + return throwError('Une erreur est survenue lors de l\'upload du logo du partenaire.'); + }), + mergeMap((response: Media) => { + this.organization.logo = response.url; + return this._organizationService.create(this.organization).pipe( + catchError(() => { + return throwError('Une erreur est survenue lors de la création du partenaire.'); + }), + ); + }), + ).subscribe( + (organizationCreated) => { + this._notificationService.notify({ + message: 'Le partenaire a été créé avec succès.', + type: 'success', + }); + this._router.navigate(['/producteur-de-donnees', organizationCreated.id]); + }, + (err) => { + this._notificationService.notify({ + message: err, + type: 'error', + }); + }, + ); + } else { + return this._organizationService.create(this.organization).subscribe( + (organizationUpdated) => { + this._notificationService.notify({ + message: 'Le partenaire a été créé avec succès.', + type: 'success', + }); + this._router.navigate(['/producteur-de-donnees', organizationUpdated.id]); + }, + () => { + this._notificationService.notify({ + message: 'Une erreur est survenue lors de la création du partenaire.', + type: 'error', + }); + }, + ); + } } - - action.subscribe( - (organizationCreated) => { - this._notificationService.notify({ - message: 'Le producteur de données a été créée avec succès.', - type: 'success', - }); - this._router.navigate(['/producteur-de-donnees', organizationCreated.id]); - }, - () => { - this._notificationService.notify({ - message: 'Une erreur est survenue lors de la création du producteur de données.', - type: 'error', - }); - }, - ); } } diff --git a/src/app/components/organizations/list/organizations.component.html b/src/app/components/organizations/list/organizations.component.html index f30fe6a126772c926a0eefc3db29202345127685..93d566e69541748f83c81a16e33974593e4f976f 100644 --- a/src/app/components/organizations/list/organizations.component.html +++ b/src/app/components/organizations/list/organizations.component.html @@ -1,11 +1,11 @@ -<div class="section page-container" *ngIf="organizations"> +<div class="section page-container"> <app-page-header [pageInfo]="pageHeaderInfo" [hideBackButton]="true"></app-page-header> <div class="add-item-link has-text-right"> <a class="button button-gl" [routerLink]="['new']"> Ajouter </a> </div> - <div class="table entity-list-table"> + <div class="table entity-list-table" *ngIf="organizations"> <div class="header columns is-marginless"> <div class="column is-2"> <span (click)="sortBy('name')" class="is-sortable"> @@ -22,7 +22,10 @@ <span class="column-title" [ngClass]="{'active': sortOptions.value === name}">Nom</span> </span> </div> - <div class="column is-2 has-text-centered"> + <div class="column is-1 has-text-centered"> + <span class="column-title">Publié</span> + </div> + <div class="column is-1 has-text-centered"> <span class="column-title">Logo</span> </div> <div class="column is-4"> @@ -54,8 +57,16 @@ <div class="column is-2"> <span>{{ organization.name}}</span> </div> - <div class="column is-2 has-text-centered"> - <img src="{{organization.logo}}" alt=""> + <div class="column is-1 has-text-centered"> + <span class="icon has-text-success" *ngIf="organization.published"> + <i class="far fa-check-circle"></i> + </span> + <span class="icon has-text-danger" *ngIf="!organization.published"> + <i class="far fa-times-circle"></i> + </span> + </div> + <div class="column is-1 has-text-centered"> + <img class="entity-logo-in-list" [src]="organization.logo" alt=""> </div> <div class="column is-4"> <span>{{ organization.description | slice:0:200}}...</span> @@ -80,4 +91,8 @@ </div> </div> </div> -</div> \ No newline at end of file +</div> + +<app-confirmation-modal (cancel)="objectToBeDeletedId=null" (continue)="deleteOrganization()" [texts]="deleteModalTexts" + [isOpened]="objectToBeDeletedId !== null"> +</app-confirmation-modal> \ No newline at end of file diff --git a/src/app/components/organizations/list/organizations.component.ts b/src/app/components/organizations/list/organizations.component.ts index c2c4551db3b71a1ab7a5903a0b57b62760762ad7..e3ce2ea799abad24b4c85f8d791154681da7d553 100644 --- a/src/app/components/organizations/list/organizations.component.ts +++ b/src/app/components/organizations/list/organizations.component.ts @@ -1,21 +1,28 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, OnDestroy } from '@angular/core'; import { Organization, OrganizationRO } from 'src/app/models/organization.model'; import { OrganizationService } from 'src/app/services/organization.service'; import { Subscription } from 'rxjs'; import { PaginatorOptions } from 'src/app/models/paginator-options.model'; import { IPageHeaderInfo } from '../../../models/page.model'; +import { NotificationService } from '../../../services'; @Component({ selector: 'app-organizations', templateUrl: './organizations.component.html', styleUrls: ['./organizations.component.scss'], }) -export class OrganizationsComponent implements OnInit { +export class OrganizationsComponent implements OnInit, OnDestroy { pageHeaderInfo: IPageHeaderInfo = { title: '', }; - organizations: Organization[]; + objectToBeDeletedId = null; + deleteModalTexts = { + main: 'Si vous poursuivez, le partenaire sera définitivement supprimé.', + cancel: 'Annuler', + continue: 'Supprimer', + }; + organizations: Organization[] = []; searchChangeSub: Subscription; // Paginator options @@ -32,24 +39,25 @@ export class OrganizationsComponent implements OnInit { where = {}; constructor( - private organizationsService: OrganizationService, + private _organizationService: OrganizationService, + private _notificationService: NotificationService, ) { this.paginator = { - pageIndex: this.organizationsService.pageNumber, + pageIndex: this._organizationService.pageNumber, length: 0, - limit: this.organizationsService.limit, + limit: this._organizationService.limit, pageSizeOptions: [5, 10, 20], }; } ngOnInit(): void { - this.organizationsService.sortOptions = { + this._organizationService.sortOptions = { value: 'name', order: 'asc', }; this.search(); - this.searchChangeSub = this.organizationsService.searchChange$.subscribe( + this.searchChangeSub = this._organizationService.searchChange$.subscribe( () => { this.search(); }, @@ -57,49 +65,79 @@ export class OrganizationsComponent implements OnInit { } private search() { - this.organizationsService.getOrganizations() - .subscribe((items: OrganizationRO) => { - this.organizations = items.organizations; - this.totalElement = items.totalCount; - - this.pageHeaderInfo.title = `${this.totalElement} producteurs de données trouvés`; - - this.paginator.limit = this.organizationsService.limit; - this.paginator.pageIndex = this.organizationsService.pageNumber; - this.paginator.length = items.totalCount; - }); + this._organizationService.getOrganizations() + .subscribe( + (items: OrganizationRO) => { + this.organizations = items.organizations; + this.totalElement = items.totalCount; + + this.pageHeaderInfo.title = items.totalCount ? + `${this.totalElement} partenaires trouvés` : `${this.totalElement} partenaire trouvé`; + + this.paginator.limit = this._organizationService.limit; + this.paginator.pageIndex = this._organizationService.pageNumber; + this.paginator.length = items.totalCount; + }, + () => { + this.pageHeaderInfo.title = '0 partenaire trouvé'; + this._notificationService.notify({ + type: 'error', + message: 'Une erreur est survenue lors du chargement des partenaires.', + }); + }, + ); } // When pagination is changed by user, we update datasetList with new pagination options changePagination(pageIndex) { - this.organizationsService.paginationChanged(this.paginator.limit, pageIndex); + this._organizationService.paginationChanged(this.paginator.limit, pageIndex); } changePageSize(pageSize) { - this.organizationsService.paginationChanged(pageSize, 1); + this._organizationService.paginationChanged(pageSize, 1); } sortBy(key: string) { - if (this.organizationsService.sortOptions.value === key) { - this.organizationsService.reverseSortOrder(); + if (this._organizationService.sortOptions.value === key) { + this._organizationService.reverseSortOrder(); } else { - this.organizationsService.sortOptions.value = key; - this.organizationsService.sortOptions.order = 'asc'; + this._organizationService.sortOptions.value = key; + this._organizationService.sortOptions.order = 'asc'; } this.search(); } get sortOptions() { - return this.organizationsService.sortOptions; + return this._organizationService.sortOptions; + } + + displayDeletePopup(id) { + this.objectToBeDeletedId = id; } - displayDeletePopup(organizationId) { - const pop = confirm('Etes vous sûr de vouloir supprimer ce producteur de données ?'); - if (pop) { - this.organizationsService.delete(organizationId).subscribe(() => { - this.organizationsService.pageNumber = 1; + deleteOrganization() { + this._organizationService.delete(this.objectToBeDeletedId).subscribe( + () => { + this._notificationService.notify({ + type: 'success', + message: 'Le partenaire a été supprimé avec succès.', + }); + this._organizationService.pageNumber = 1; this.search(); - }); - } + }, + () => { + this._notificationService.notify({ + type: 'error', + message: 'Une erreur est survenue lors de la suppression du partenaire.', + }); + }, + () => { + this.objectToBeDeletedId = null; + }, + ); + } + + ngOnDestroy() { + this.searchChangeSub.unsubscribe(); } } diff --git a/src/app/components/page-header/page-header.component.ts b/src/app/components/page-header/page-header.component.ts index 0d824ad8570f807af5ec8d8e5de919426a609ce4..e4fdcc75c432db20f53eb3a9086828601ca02ec9 100644 --- a/src/app/components/page-header/page-header.component.ts +++ b/src/app/components/page-header/page-header.component.ts @@ -16,15 +16,15 @@ export class PageHeaderComponent implements OnInit { ) { } @Input() pageInfo: IPageHeaderInfo; - @Input() customGoToPreviousPage: any; + @Input() goToThisUrl: any; @Input() hideBackButton: boolean; ngOnInit() { } goToPreviousPage() { - if (this.customGoToPreviousPage) { - this.customGoToPreviousPage(); + if (this.goToThisUrl) { + this._router.navigate([this.goToThisUrl]); } else { const index = 1; // Start to retrieve the previous element let url = this._navigationHistoryService.getFromLast(index); diff --git a/src/app/components/paginator/paginator.component.ts b/src/app/components/paginator/paginator.component.ts index 0debb3cfb0985ef9dcbd7c8d8542880645b099d7..f43573de6181223f3110e71912893664025556c3 100644 --- a/src/app/components/paginator/paginator.component.ts +++ b/src/app/components/paginator/paginator.component.ts @@ -58,6 +58,7 @@ export class PaginatorComponent implements OnInit { changePageSize(size: number) { this.pageSize = size; this.pageSizeChanged.emit(this.pageSize); + } getPages(): number[] { diff --git a/src/app/components/projections/detail/projection-detail.component.html b/src/app/components/projections/detail/projection-detail.component.html new file mode 100644 index 0000000000000000000000000000000000000000..490ef8dafc6dfee04ba08b542b0e4fdaae77fe21 --- /dev/null +++ b/src/app/components/projections/detail/projection-detail.component.html @@ -0,0 +1,34 @@ +<section class="section page-container" *ngIf="projection"> + <app-page-header [pageInfo]="{title: title}"></app-page-header> + + <div class="columns is-centered"> + <div class="column is-8"> + <div class="card"> + <header class="card-header"> + <p class="card-header-title has-text-centered"> + {{projection.name}} + </p> + </header> + <div class="card-content"> + <div class="content"> + <p> + <span class="has-text-weight-bold">Id: </span> + <span>{{projection.id}}</span> + </p> + <p> + <span class="has-text-weight-bold">Nom commun: </span> + <span>{{projection.commonName}}</span> + </p> + <p> + <span class="has-text-weight-bold">Description: </span> + <span *ngIf="projection.description; else emptyDescription">{{projection.description}}</span> + <ng-template #emptyDescription> + <span class="empty-property">Non renseigné</span> + </ng-template> + </p> + </div> + </div> + </div> + </div> + </div> +</section> \ No newline at end of file diff --git a/src/app/components/projections/detail/projection-detail.component.scss b/src/app/components/projections/detail/projection-detail.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..163f99f474dc5696a1670be5273eef97a2efbd39 --- /dev/null +++ b/src/app/components/projections/detail/projection-detail.component.scss @@ -0,0 +1,3 @@ +.card-header-title { + justify-content: center; +} \ No newline at end of file diff --git a/src/app/components/projections/detail/projection-detail.component.ts b/src/app/components/projections/detail/projection-detail.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..fc44993764c353fbcf3fd9d2c468198623724417 --- /dev/null +++ b/src/app/components/projections/detail/projection-detail.component.ts @@ -0,0 +1,30 @@ + +import { switchMap } from 'rxjs/operators'; +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, ParamMap } from '@angular/router'; +import { Projection } from '../../../models/projection.model'; +import { ProjectionService } from '../../../services'; + +@Component({ + selector: 'app-projection-detail', + templateUrl: './projection-detail.component.html', + styleUrls: ['./projection-detail.component.scss'], +}) +export class ProjectionDetailComponent implements OnInit { + + projection: Projection; + title: string; + + constructor( + private _route: ActivatedRoute, + private _projectionService: ProjectionService, + ) { + } + + ngOnInit(): void { + this.title = this._route.snapshot.data.title; + this._route.paramMap.pipe( + switchMap((params: ParamMap) => this._projectionService.findById(params.get('id')))) + .subscribe((projection: Projection) => this.projection = projection); + } +} diff --git a/src/app/components/projections/edit/projection-form.component.html b/src/app/components/projections/edit/projection-form.component.html new file mode 100644 index 0000000000000000000000000000000000000000..fb2960a28d12d69bb4b7357a54e8ba618ab4dcea --- /dev/null +++ b/src/app/components/projections/edit/projection-form.component.html @@ -0,0 +1,45 @@ +<section class="section page-container" *ngIf="projection"> + <app-page-header [pageInfo]="{title: title}"></app-page-header> + + <form [formGroup]="form" (ngSubmit)="onSubmit()" class="columns is-centered is-marginless"> + <div class="column is-7"> + <input type="hidden" formControlName="id" value="{{projection.id}}"> + + <div class="field"> + <label class="label required" for="name">Nom</label> + <div class="control"> + <input class="input" type="text" [value]="projection.name" formControlName="name" id="name" required> + </div> + <div *ngIf="name.invalid && (name.dirty || name.touched)" class="alert alert-danger"> + <p *ngIf="name.errors['required']" class="help is-danger"> + Le nom de la projection est obligatoire. + </p> + </div> + </div> + + <div class="field"> + <label class="label required" for="mapServerType">Nom commun</label> + <div class="control"> + <input class="input" type="text" [value]="projection.commonName" formControlName="commonName" id="commonName"> + </div> + <div *ngIf="commonName.invalid && (commonName.dirty || commonName.touched)" class="alert alert-danger"> + <p *ngIf="commonName.errors['required']" class="help is-danger"> + Le nom commun est obligatoire. + </p> + </div> + </div> + + <div class="field"> + <label class="label" for="fileExtension">Description</label> + <div class="control"> + <input class="input" type="text" [value]="projection.description" formControlName="description" + id="description"> + </div> + </div> + + <div class="has-text-right"> + <button class="button button-gl" type="submit" [disabled]="formInvalid == true">Valider</button> + </div> + </div> + </form> +</section> \ No newline at end of file diff --git a/src/app/components/projections/edit/projection-form.component.scss b/src/app/components/projections/edit/projection-form.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..5bea26fe55b4ab7bb07da39d6140df0c988864d4 --- /dev/null +++ b/src/app/components/projections/edit/projection-form.component.scss @@ -0,0 +1,19 @@ +.full-width { + width: 100%; +} + +h1 { + text-align: center +} + +.icon { + cursor: pointer; + &:hover { + .fa-plus { + color: lightblue; + } + .fa-trash { + color: #d5232a; + } + } +} \ No newline at end of file diff --git a/src/app/components/projections/edit/projection-form.component.ts b/src/app/components/projections/edit/projection-form.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..729c68d11f4174564eaf880560e248978957c2f3 --- /dev/null +++ b/src/app/components/projections/edit/projection-form.component.ts @@ -0,0 +1,108 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, ParamMap, Router } from '@angular/router'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { filter, switchMap } from 'rxjs/operators'; +import { NotificationService, ProjectionService } from 'src/app/services'; +import { Projection } from '../../../models/projection.model'; + +@Component({ + selector: 'app-projection-form', + templateUrl: './projection-form.component.html', + styleUrls: ['./projection-form.component.scss'], +}) +export class ProjectionFormComponent implements OnInit { + + projection: Projection = new Projection(); + form: FormGroup; + title: string; + + constructor( + private _projectionService: ProjectionService, + private _route: ActivatedRoute, + private _router: Router, + private _fb: FormBuilder, + private _notificationService: NotificationService, + ) { + } + + ngOnInit() { + this.title = this._route.snapshot.data.title; + this.initForm(); + + this._route.paramMap.pipe( + filter((paramMap: ParamMap) => (paramMap.get('id') !== null)), + switchMap((paramMap: ParamMap) => this._projectionService.findById(paramMap.get('id')))) + .subscribe((projection: Projection) => { + + this.projection = projection; + + this.initForm(); + + }); + } + + initForm() { + this.form = this._fb.group({ + id: [this.projection.id], + name: [this.projection.name, Validators.required], + commonName: [this.projection.commonName, Validators.required], + description: [this.projection.description], + }); + } + + onSubmit() { + if (!this.formInvalid) { + this.projection = new Projection(this.form.value); + if (this.projection.id) { + this._projectionService.update(this.projection).subscribe( + (projectionUpdate) => { + this._notificationService.notify({ + type: 'success', + message: 'Le projection a bien été mise à jour.', + }); + this._router.navigate(['/projections', projectionUpdate.id]); + }, + () => { + this._notificationService.notify({ + type: 'error', + message: 'Une erreur est survenue lors de la mise à jour de la projection.', + }); + }, + ); + } else { + this._projectionService.create(this.projection).subscribe( + (projectionCreated) => { + this._notificationService.notify({ + type: 'success', + message: 'Le projection a bien été créé.', + }); + this._router.navigate(['/projections', projectionCreated.id]); + }, + () => { + this._notificationService.notify({ + type: 'error', + message: 'Une erreur est survenue lors de la création de la projection.', + }); + }, + ); + } + } + } + + // Getters for each property + get name() { + return this.form.controls['name']; + } + + get commonName() { + return this.form.controls['commonName']; + } + + get description() { + return this.form.controls['description']; + } + + get formInvalid() { + return this.form.invalid; + } +} diff --git a/src/app/components/projections/list/projections.component.html b/src/app/components/projections/list/projections.component.html new file mode 100644 index 0000000000000000000000000000000000000000..55daeba83054bb2aaee9752516638c1125d88161 --- /dev/null +++ b/src/app/components/projections/list/projections.component.html @@ -0,0 +1,90 @@ +<section class="section page-container"> + <app-page-header [pageInfo]="pageHeaderInfo" [hideBackButton]="true"></app-page-header> + <div class="add-item-link has-text-right"> + <a class="button button-gl" [routerLink]="['new']"> + Ajouter + </a> + </div> + <div class="table entity-list-table" *ngIf="projections"> + <div class="header columns is-marginless"> + <div class="column is-2"> + <span (click)="sortBy('name')" class="is-sortable"> + <span class="sort-icons"> + <span class="icon"> + <i class="fas fa-sort-up" + [ngClass]="{'icon-red': sortOptions.value === 'name' && sortOptions.order === 'desc'}"></i> + </span> + <span class="icon"> + <i class="fas fa-sort-down" + [ngClass]="{'icon-red': sortOptions.value === 'name' && sortOptions.order === 'asc'}"></i> + </span> + </span> + <span class="column-title" [ngClass]="{'active': sortOptions.value === name}">Nom</span> + </span> + </div> + <div class="column is-2"> + <span (click)="sortBy('commonName')" class="is-sortable"> + <span class="sort-icons"> + <span class="icon"> + <i class="fas fa-sort-up" + [ngClass]="{'icon-red': sortOptions.value === 'commonName' && sortOptions.order === 'desc'}"></i> + </span> + <span class="icon"> + <i class="fas fa-sort-down" + [ngClass]="{'icon-red': sortOptions.value === 'commonName' && sortOptions.order === 'asc'}"></i> + </span> + </span> + <span class="column-title" [ngClass]="{'active': sortOptions.value === commonName}">Nom commun</span> + </span> + </div> + <div class="column is-2"> + <span (click)="sortBy('description')" class="is-sortable"> + <span class="sort-icons"> + <span class="icon"> + <i class="fas fa-sort-up" + [ngClass]="{'icon-red': sortOptions.value === 'description' && sortOptions.order === 'desc'}"></i> + </span> + <span class="icon"> + <i class="fas fa-sort-down" + [ngClass]="{'icon-red': sortOptions.value === 'description' && sortOptions.order === 'asc'}"></i> + </span> + </span> + <span class="column-title" [ngClass]="{'active': sortOptions.value === description}">Description</span> + </span> + </div> + <div class="column is-offset-5 is-1 has-text-centered"> + <span class="column-title">Actions</span> + </div> + </div> + <div class="data-list"> + <div class="data columns is-multiline is-vcentered is-marginless" + *ngFor="let projection of projections; let i=index; let odd=odd; let even=even;" + [ngClass]="{ odd: odd, even: even }"> + <div class="column is-2"> + <span>{{ projection.name}}</span> + </div> + <div class="column is-2"> + <span>{{ projection.commonName}}</span> + </div> + <div class="column is-2"> + <span>{{ projection.description}}</span> + </div> + <div class="column is-offset-5 is-1 has-text-centered actions"> + <app-crud-buttons [id]="projection.id" (delete)="displayDeletePopup($event)"></app-crud-buttons> + </div> + </div> + </div> + <div class="columns is-marginless paginator"> + <div class="column"> + <app-paginator *ngIf="paginator.length > 0" [length]="paginator.length" [pageSize]="paginator.limit" + [pageSizeOptions]="paginator.pageSizeOptions" [pageIndex]="paginator.pageIndex" [pagesToShow]="5" + [showFirstLastButtons]="true" (page)="changePagination($event)" (pageSizeChanged)="changePageSize($event)"> + </app-paginator> + </div> + </div> + </div> +</section> + +<app-confirmation-modal (cancel)="objectToBeDeletedId=null" (continue)="deleteProjection()" [texts]="deleteModalTexts" + [isOpened]="objectToBeDeletedId !== null"> +</app-confirmation-modal> \ No newline at end of file diff --git a/src/app/components/projections/list/projections.component.scss b/src/app/components/projections/list/projections.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/src/app/components/projections/list/projections.component.scss @@ -0,0 +1 @@ + diff --git a/src/app/components/projections/list/projections.component.ts b/src/app/components/projections/list/projections.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..3d132afc284fade3a7113d06ad28847e5728eca4 --- /dev/null +++ b/src/app/components/projections/list/projections.component.ts @@ -0,0 +1,145 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { PaginatorOptions } from 'src/app/models/paginator-options.model'; +import { IPageHeaderInfo } from '../../../models/page.model'; +import { NotificationService } from '../../../services'; +import { Projection, ProjectionRO } from '../../../models/projection.model'; +import { ProjectionService } from '../../../services/projection.service'; + +@Component({ + selector: 'app-projections', + templateUrl: './projections.component.html', + styleUrls: ['./projections.component.scss'], +}) +export class ProjectionsComponent implements OnInit, OnDestroy { + + pageHeaderInfo: IPageHeaderInfo = { + title: '', + }; + objectToBeDeletedId = null; + deleteModalTexts = { + main: 'Si vous poursuivez, la projection sera définitivement supprimée.', + cancel: 'Annuler', + continue: 'Supprimer', + }; + projections: Projection[] = []; + searchChangeSub: Subscription; + + // Paginator options + paginator: PaginatorOptions; + + sortValue: string; + + totalElement: number; + pageSize = 10; + pageSizeOptions = [5, 10, 25, 100]; + filters = { + name: '', + }; + where = {}; + + constructor( + private _projectionService: ProjectionService, + private _notificationService: NotificationService, + ) { + this.paginator = { + pageIndex: this._projectionService.pageNumber, + length: 0, + limit: this._projectionService.limit, + pageSizeOptions: [5, 10, 20], + }; + } + + ngOnInit(): void { + this._projectionService.sortOptions = { + value: 'name', + order: 'asc', + }; + this.search(); + + this.searchChangeSub = this._projectionService.searchChange$.subscribe( + () => { + this.search(); + }, + ); + } + + private search() { + this._projectionService.getProjections() + .subscribe( + (items: ProjectionRO) => { + this.projections = items.projections; + this.totalElement = items.totalCount; + + this.pageHeaderInfo.title = `${this.totalElement} projections trouvées`; + this.pageHeaderInfo.title = this.totalElement > 1 ? + `${this.totalElement} projections trouvées` : + `${this.totalElement} projection trouvée`; + + this.paginator.limit = this._projectionService.limit; + this.paginator.pageIndex = this._projectionService.pageNumber; + this.paginator.length = items.totalCount; + }, + () => { + this.pageHeaderInfo.title = '0 projection trouvée'; + this._notificationService.notify({ + type: 'error', + message: 'Une erreur est survenue lors du chargement des projections.', + }); + }, + ); + } + + // When pagination is changed by user, we update projections list with new pagination options + changePagination(pageIndex) { + this._projectionService.paginationChanged(this.paginator.limit, pageIndex); + } + + changePageSize(pageSize) { + this._projectionService.paginationChanged(pageSize, 1); + } + + sortBy(key: string) { + if (this._projectionService.sortOptions.value === key) { + this._projectionService.reverseSortOrder(); + } else { + this._projectionService.sortOptions.value = key; + this._projectionService.sortOptions.order = 'asc'; + } + this.search(); + } + + get sortOptions() { + return this._projectionService.sortOptions; + } + + displayDeletePopup(id) { + this.objectToBeDeletedId = id; + } + + deleteProjection() { + this._projectionService.delete(this.objectToBeDeletedId).subscribe( + () => { + this._notificationService.notify({ + type: 'success', + message: 'La projection a été supprimée avec succès.', + }); + this._projectionService.pageNumber = 1; + this.search(); + }, + () => { + this._notificationService.notify({ + type: 'error', + message: 'Une erreur est survenue lors de la suppression de la projection.', + }); + }, + () => { + this.objectToBeDeletedId = null; + }, + ); + } + + ngOnDestroy() { + this.searchChangeSub.unsubscribe(); + } +} diff --git a/src/app/components/resources/detail/resource-detail.component.html b/src/app/components/resources/detail/resource-detail.component.html index 142398b016a9d9ce69cb516509d3ad70298f52df..b3316ef37f69bbb1b54ceb2db6a829238b177c5f 100644 --- a/src/app/components/resources/detail/resource-detail.component.html +++ b/src/app/components/resources/detail/resource-detail.component.html @@ -73,10 +73,20 @@ <span class="empty-property">Non renseigné</span> </ng-template> </div> + <br> - <div *ngIf="resource.messageWarning"> - <span class="has-text-weight-bold">Message d'alerte: </span> - <span *ngIf="resource.messageWarning; else emptyMessageWarning">{{resource.messageWarning}}</span> + <div> + <span class="has-text-weight-bold">Message d'alerte (Français): </span> + <span *ngIf="resource.messageWarningFr; else emptyMessageWarning">{{resource.messageWarningFr}}</span> + <ng-template #emptyMessageWarning> + <span class="empty-property">Non renseigné</span> + </ng-template> + </div> + <br> + + <div> + <span class="has-text-weight-bold">Message d'alerte (Anglais): </span> + <span *ngIf="resource.messageWarningEn; else emptyMessageWarning">{{resource.messageWarningEn}}</span> <ng-template #emptyMessageWarning> <span class="empty-property">Non renseigné</span> </ng-template> diff --git a/src/app/components/resources/detail/resource-detail.component.scss b/src/app/components/resources/detail/resource-detail.component.scss index 05012fa666610636fa011ff7250fcdc78764b468..d8e3c25a4e9537b0390658b05c3f7c7f81b1c550 100644 --- a/src/app/components/resources/detail/resource-detail.component.scss +++ b/src/app/components/resources/detail/resource-detail.component.scss @@ -1,8 +1,3 @@ .card-header-title { justify-content: center; } - -.empty-property { - font-style: italic; - color: #818080; -} diff --git a/src/app/components/resources/edit/resource-form.component.html b/src/app/components/resources/edit/resource-form.component.html index c41e9d4629578b708384787c031f58495eeb5a53..d19b840ea02e2a4be8343d223ec9b6baa57edeca 100644 --- a/src/app/components/resources/edit/resource-form.component.html +++ b/src/app/components/resources/edit/resource-form.component.html @@ -94,9 +94,16 @@ </div> <div class="field"> - <label class="label" for="messageWarning">Message d'alerte</label> + <label class="label" for="messageWarningFr">Message d'alerte (Français)</label> <div class="control"> - <input class="input" type="text" formControlName="messageWarning" id="messageWarning"> + <input class="input" type="text" formControlName="messageWarningFr" id="messageWarningFr"> + </div> + </div> + + <div class="field"> + <label class="label" for="messageWarningEn">Message d'alerte (Anglais)</label> + <div class="control"> + <input class="input" type="text" formControlName="messageWarningEn" id="messageWarningEn"> </div> </div> diff --git a/src/app/components/resources/edit/resource-form.component.ts b/src/app/components/resources/edit/resource-form.component.ts index c379dc1024d744291b88030815c2df643de0114f..b69a143dedd91a3f8a108bfe2aa819dd889763d8 100644 --- a/src/app/components/resources/edit/resource-form.component.ts +++ b/src/app/components/resources/edit/resource-form.component.ts @@ -3,10 +3,10 @@ import { ActivatedRoute, ParamMap, Router } from '@angular/router'; import { Resource, IResource } from 'src/app/models/resource.model'; import { ResourceService } from 'src/app/services/resource.service'; import { FormBuilder, FormGroup, Validators, FormArray } from '@angular/forms'; -import { filter, switchMap, mergeMap, merge, concatMap, map } from 'rxjs/operators'; +import { filter, switchMap, mergeMap, merge, concatMap, map, tap, catchError } from 'rxjs/operators'; import { Format } from 'src/app/models/format.model'; -import { FormatService } from 'src/app/services'; -import { from, forkJoin, Observable, of } from 'rxjs'; +import { FormatService, NotificationService } from 'src/app/services'; +import { from, forkJoin, Observable, of, throwError } from 'rxjs'; @Component({ selector: 'app-resource-form', @@ -26,6 +26,7 @@ export class ResourceFormComponent implements OnInit { private _route: ActivatedRoute, private _router: Router, private _fb: FormBuilder, + private _notificationService: NotificationService, ) { } @@ -70,7 +71,8 @@ export class ResourceFormComponent implements OnInit { isStandard: [this.resource.isStandard, Validators.required], description: [this.resource.description], parametersUrl: [this.resource.parametersUrl], - messageWarning: [this.resource.messageWarning], + messageWarningFr: [this.resource.messageWarningFr], + messageWarningEn: [this.resource.messageWarningEn], }); } @@ -79,10 +81,36 @@ export class ResourceFormComponent implements OnInit { const resourceFormats = this.form.controls.resourceFormats['controls'].filter(e => e.dirty).map(e => e.value); const newResource = new Resource(this.form.value); let savedResource: Resource; - this._resourceService.replaceOrCreate(newResource).pipe( + let action = this._resourceService.create(newResource).pipe( + tap(() => { + this._notificationService.notify({ + type: 'success', + message: 'La ressource a été créé avec succès.', + }); + }), + catchError(() => { + return throwError('Une erreur s\'est produite lors de la création de la ressource'); + }), + ); + + if (newResource.id) { + action = this._resourceService.update(newResource).pipe( + tap(() => { + this._notificationService.notify({ + type: 'success', + message: 'La ressource a été mise à jour avec succès.', + }); + }), + catchError(() => { + return throwError('Une erreur s\'est produite lors de la mise à jour de la ressource'); + }), + ); + } + + action.pipe( mergeMap((resource) => { savedResource = resource; - let actions: Observable<any>[] = []; + const actions: Observable<any>[] = []; const toAdd = resourceFormats.filter(e => !e.id); const toUpdate = resourceFormats.filter(e => e.id) @@ -107,12 +135,19 @@ export class ResourceFormComponent implements OnInit { return res; }), - map(() => savedResource.id), + map(() => { return savedResource.id; }), + catchError(() => { + return throwError('Une erreur s\'est produite lors de l\'association des formats à la ressource.'); + }), ).subscribe( (resourceCreatedId) => { this._router.navigate(['/resources', resourceCreatedId]); }, (err) => { + this._notificationService.notify({ + type: 'error', + message: err, + }); }, ); } @@ -127,9 +162,6 @@ export class ResourceFormComponent implements OnInit { } addResourceFormat() { - // if (!this.form.controls.links) { - // this.organization.links = []; - // } (this.form.controls.resourceFormats as FormArray).push(this._fb.group({ formatId: [null, Validators.required], isProjectable: 0, @@ -166,10 +198,6 @@ export class ResourceFormComponent implements OnInit { return this.form.controls.resourceFormats as FormArray; } - // get outputFormats() { - // return this.form.controls['outputFormats']; - // } - get formInvalid() { return this.form.invalid; } diff --git a/src/app/components/resources/list/resources.component.html b/src/app/components/resources/list/resources.component.html index f544a69abeaaa7d31ac75a5bf05f4ade9253d221..f3ce83f4c529dd0730cc9c33a23d3aefde493ac9 100644 --- a/src/app/components/resources/list/resources.component.html +++ b/src/app/components/resources/list/resources.component.html @@ -1,186 +1,188 @@ -<ng-container *ngIf="resources"> - <div class="section page-container"> - <app-page-header [pageInfo]="pageHeaderInfo" [hideBackButton]="true"></app-page-header> - <div class="add-item-link has-text-right"> - <a class="button button-gl" [routerLink]="['new']"> - Ajouter - </a> +<div class="section page-container"> + <app-page-header [pageInfo]="pageHeaderInfo" [hideBackButton]="true"></app-page-header> + <div class="add-item-link has-text-right"> + <a class="button button-gl" [routerLink]="['new']"> + Ajouter + </a> + </div> + <div class="table entity-list-table" *ngIf="resources"> + <div class="header columns is-marginless"> + <div class="column is-2"> + <span (click)="sortBy('name')" class="is-sortable"> + <span class="sort-icons"> + <span class="icon"> + <i class="fas fa-sort-up" + [ngClass]="{'icon-red': sortOptions.value === 'name' && sortOptions.order === 'desc'}"></i> + </span> + <span class="icon"> + <i class="fas fa-sort-down" + [ngClass]="{'icon-red': sortOptions.value === 'name' && sortOptions.order === 'asc'}"></i> + </span> + </span> + <span class="column-title" [ngClass]="{'active': sortOptions.value === name}">Nom</span> + </span> + </div> + <div class="column is-1"> + <span (click)="sortBy('acronym')" class="is-sortable"> + <span class="sort-icons"> + <span class="icon"> + <i class="fas fa-sort-up" + [ngClass]="{'icon-red': sortOptions.value === 'acronym' && sortOptions.order === 'desc'}"></i> + </span> + <span class="icon"> + <i class="fas fa-sort-down" + [ngClass]="{'icon-red': sortOptions.value === 'acronym' && sortOptions.order === 'asc'}"></i> + </span> + </span> + <span class="column-title" [ngClass]="{'active': sortOptions.value === acronym}">Acronyme</span> + </span> + </div> + <div class="column is-1"> + <span (click)="sortBy('type')" class="is-sortable"> + <span class="sort-icons"> + <span class="icon"> + <i class="fas fa-sort-up" + [ngClass]="{'icon-red': sortOptions.value === 'type' && sortOptions.order === 'desc'}"></i> + </span> + <span class="icon"> + <i class="fas fa-sort-down" + [ngClass]="{'icon-red': sortOptions.value === 'type' && sortOptions.order === 'asc'}"></i> + </span> + </span> + <span class="column-title" [ngClass]="{'active': sortOptions.value === type}">Type</span> + </span> + </div> + <div class="column is-4"> + <span (click)="sortBy('description')" class="is-sortable"> + <span class="sort-icons"> + <span class="icon"> + <i class="fas fa-sort-up" + [ngClass]="{'icon-red': sortOptions.value === 'description' && sortOptions.order === 'desc'}"></i> + </span> + <span class="icon"> + <i class="fas fa-sort-down" + [ngClass]="{'icon-red': sortOptions.value === 'description' && sortOptions.order === 'asc'}"></i> + </span> + </span> + <span class="column-title" [ngClass]="{'active': sortOptions.value === description}">Description</span> + </span> + </div> + <div class="column is-1 has-text-centered"> + <span (click)="sortBy('isQueryable')" class="is-sortable"> + <span class="sort-icons"> + <span class="icon"> + <i class="fas fa-sort-up" + [ngClass]="{'icon-red': sortOptions.value === 'isQueryable' && sortOptions.order === 'desc'}"></i> + </span> + <span class="icon"> + <i class="fas fa-sort-down" + [ngClass]="{'icon-red': sortOptions.value === 'isQueryable' && sortOptions.order === 'asc'}"></i> + </span> + </span> + <span class="column-title" [ngClass]="{'active': sortOptions.value === isQueryable}">Requêtable</span> + </span> + </div> + <div class="column is-1 has-text-centered"> + <span (click)="sortBy('isDownloadable')" class="is-sortable"> + <span class="sort-icons"> + <span class="icon"> + <i class="fas fa-sort-up" + [ngClass]="{'icon-red': sortOptions.value === 'isDownloadable' && sortOptions.order === 'desc'}"></i> + </span> + <span class="icon"> + <i class="fas fa-sort-down" + [ngClass]="{'icon-red': sortOptions.value === 'isDownloadable' && sortOptions.order === 'asc'}"></i> + </span> + </span> + <span class="column-title" [ngClass]="{'active': sortOptions.value === isDownloadable}">Téléchargeable</span> + </span> + </div> + <div class="column is-1 has-text-centered"> + <span (click)="sortBy('isStandard')" class="is-sortable"> + <span class="sort-icons"> + <span class="icon"> + <i class="fas fa-sort-up" + [ngClass]="{'icon-red': sortOptions.value === 'isStandard' && sortOptions.order === 'desc'}"></i> + </span> + <span class="icon"> + <i class="fas fa-sort-down" + [ngClass]="{'icon-red': sortOptions.value === 'isStandard' && sortOptions.order === 'asc'}"></i> + </span> + </span> + <span class="column-title" [ngClass]="{'active': sortOptions.value === isStandard}">Standard</span> + </span> + </div> + <div class="column is-1 has-text-centered"> + <span class="column-title">Actions</span> + </div> </div> - <div class="table entity-list-table"> - <div class="header columns is-marginless"> + <div class="data-list"> + <div class="data columns is-multiline is-vcentered is-marginless" + *ngFor="let resource of resources; let i=index; let odd=odd; let even=even;" + [ngClass]="{ odd: odd, even: even }"> <div class="column is-2"> - <span (click)="sortBy('name')" class="is-sortable"> - <span class="sort-icons"> - <span class="icon"> - <i class="fas fa-sort-up" - [ngClass]="{'icon-red': sortOptions.value === 'name' && sortOptions.order === 'desc'}"></i> - </span> - <span class="icon"> - <i class="fas fa-sort-down" - [ngClass]="{'icon-red': sortOptions.value === 'name' && sortOptions.order === 'asc'}"></i> - </span> - </span> - <span class="column-title" [ngClass]="{'active': sortOptions.value === name}">Nom</span> + <span> + {{ resource.name}} </span> + </div> <div class="column is-1"> - <span (click)="sortBy('acronym')" class="is-sortable"> - <span class="sort-icons"> - <span class="icon"> - <i class="fas fa-sort-up" - [ngClass]="{'icon-red': sortOptions.value === 'acronym' && sortOptions.order === 'desc'}"></i> - </span> - <span class="icon"> - <i class="fas fa-sort-down" - [ngClass]="{'icon-red': sortOptions.value === 'acronym' && sortOptions.order === 'asc'}"></i> - </span> - </span> - <span class="column-title" [ngClass]="{'active': sortOptions.value === acronym}">Acronyme</span> + <span> + {{ resource.acronym}} </span> + </div> <div class="column is-1"> - <span (click)="sortBy('type')" class="is-sortable"> - <span class="sort-icons"> - <span class="icon"> - <i class="fas fa-sort-up" - [ngClass]="{'icon-red': sortOptions.value === 'type' && sortOptions.order === 'desc'}"></i> - </span> - <span class="icon"> - <i class="fas fa-sort-down" - [ngClass]="{'icon-red': sortOptions.value === 'type' && sortOptions.order === 'asc'}"></i> - </span> - </span> - <span class="column-title" [ngClass]="{'active': sortOptions.value === type}">Type</span> + <span> + {{ resource.type}} </span> + </div> <div class="column is-4"> - <span (click)="sortBy('description')" class="is-sortable"> - <span class="sort-icons"> - <span class="icon"> - <i class="fas fa-sort-up" - [ngClass]="{'icon-red': sortOptions.value === 'description' && sortOptions.order === 'desc'}"></i> - </span> - <span class="icon"> - <i class="fas fa-sort-down" - [ngClass]="{'icon-red': sortOptions.value === 'description' && sortOptions.order === 'asc'}"></i> - </span> - </span> - <span class="column-title" [ngClass]="{'active': sortOptions.value === description}">Description</span> + <span> + {{ resource.description | slice:0:300}} </span> </div> <div class="column is-1 has-text-centered"> - <span (click)="sortBy('isQueryable')" class="is-sortable"> - <span class="sort-icons"> - <span class="icon"> - <i class="fas fa-sort-up" - [ngClass]="{'icon-red': sortOptions.value === 'isQueryable' && sortOptions.order === 'desc'}"></i> - </span> - <span class="icon"> - <i class="fas fa-sort-down" - [ngClass]="{'icon-red': sortOptions.value === 'isQueryable' && sortOptions.order === 'asc'}"></i> - </span> - </span> - <span class="column-title" [ngClass]="{'active': sortOptions.value === isQueryable}">Requêtable</span> + <span class="icon has-text-success" *ngIf="resource.isQueryable"> + <i class="far fa-check-circle"></i> + </span> + <span class="icon has-text-danger" *ngIf="!resource.isQueryable"> + <i class="far fa-times-circle"></i> </span> </div> <div class="column is-1 has-text-centered"> - <span (click)="sortBy('isDownloadable')" class="is-sortable"> - <span class="sort-icons"> - <span class="icon"> - <i class="fas fa-sort-up" - [ngClass]="{'icon-red': sortOptions.value === 'isDownloadable' && sortOptions.order === 'desc'}"></i> - </span> - <span class="icon"> - <i class="fas fa-sort-down" - [ngClass]="{'icon-red': sortOptions.value === 'isDownloadable' && sortOptions.order === 'asc'}"></i> - </span> - </span> - <span class="column-title" - [ngClass]="{'active': sortOptions.value === isDownloadable}">Téléchargeable</span> + <span class="icon has-text-success" *ngIf="resource.isDownloadable"> + <i class="far fa-check-circle"></i> + </span> + <span class="icon has-text-danger" *ngIf="!resource.isDownloadable"> + <i class="far fa-times-circle"></i> </span> </div> <div class="column is-1 has-text-centered"> - <span (click)="sortBy('isStandard')" class="is-sortable"> - <span class="sort-icons"> - <span class="icon"> - <i class="fas fa-sort-up" - [ngClass]="{'icon-red': sortOptions.value === 'isStandard' && sortOptions.order === 'desc'}"></i> - </span> - <span class="icon"> - <i class="fas fa-sort-down" - [ngClass]="{'icon-red': sortOptions.value === 'isStandard' && sortOptions.order === 'asc'}"></i> - </span> - </span> - <span class="column-title" [ngClass]="{'active': sortOptions.value === isStandard}">Standard</span> + <span class="icon has-text-success" *ngIf="resource.isStandard"> + <i class="far fa-check-circle"></i> + </span> + <span class="icon has-text-danger" *ngIf="!resource.isStandard"> + <i class="far fa-times-circle"></i> </span> </div> <div class="column is-1 has-text-centered"> - <span class="column-title">Actions</span> + <app-crud-buttons [id]="resource.id" (delete)="displayDeletePopup($event)"></app-crud-buttons> </div> </div> - <div class="data-list"> - <div class="data columns is-multiline is-vcentered is-marginless" - *ngFor="let resource of resources; let i=index; let odd=odd; let even=even;" - [ngClass]="{ odd: odd, even: even }"> - <div class="column is-2"> - <span> - {{ resource.name}} - </span> - - </div> - <div class="column is-1"> - <span> - {{ resource.acronym}} - </span> - - </div> - <div class="column is-1"> - <span> - {{ resource.type}} - </span> - - </div> - <div class="column is-4"> - <span> - {{ resource.description | slice:0:300}} - </span> - </div> - <div class="column is-1 has-text-centered"> - <span class="icon has-text-success" *ngIf="resource.isQueryable"> - <i class="far fa-check-circle"></i> - </span> - <span class="icon has-text-danger" *ngIf="!resource.isQueryable"> - <i class="far fa-times-circle"></i> - </span> - </div> - <div class="column is-1 has-text-centered"> - <span class="icon has-text-success" *ngIf="resource.isDownloadable"> - <i class="far fa-check-circle"></i> - </span> - <span class="icon has-text-danger" *ngIf="!resource.isDownloadable"> - <i class="far fa-times-circle"></i> - </span> - </div> - <div class="column is-1 has-text-centered"> - <span class="icon has-text-success" *ngIf="resource.isStandard"> - <i class="far fa-check-circle"></i> - </span> - <span class="icon has-text-danger" *ngIf="!resource.isStandard"> - <i class="far fa-times-circle"></i> - </span> - </div> - <div class="column is-1 has-text-centered"> - <app-crud-buttons [id]="resource.id" (delete)="displayDeletePopup($event)"></app-crud-buttons> - </div> - </div> - </div> - <div class="columns is-marginless paginator"> - <div class="column"> - <app-paginator *ngIf="paginator.length > 0" [length]="paginator.length" [pageSize]="paginator.limit" - [pageSizeOptions]="paginator.pageSizeOptions" [pageIndex]="paginator.pageIndex" [pagesToShow]="5" - [showFirstLastButtons]="true" (page)="changePagination($event)" (pageSizeChanged)="changePageSize($event)"> - </app-paginator> - </div> + </div> + <div class="columns is-marginless paginator"> + <div class="column"> + <app-paginator *ngIf="paginator.length > 0" [length]="paginator.length" [pageSize]="paginator.limit" + [pageSizeOptions]="paginator.pageSizeOptions" [pageIndex]="paginator.pageIndex" [pagesToShow]="5" + [showFirstLastButtons]="true" (page)="changePagination($event)" (pageSizeChanged)="changePageSize($event)"> + </app-paginator> </div> </div> - </div> \ No newline at end of file + </div> +</div> + +<app-confirmation-modal (cancel)="objectToBeDeletedId=null" (continue)="deleteResource()" [texts]="deleteModalTexts" + [isOpened]="objectToBeDeletedId !== null"> +</app-confirmation-modal> \ No newline at end of file diff --git a/src/app/components/resources/list/resources.component.ts b/src/app/components/resources/list/resources.component.ts index b19fe3e458a99f7a4a7a0818d03fe2929d81b218..f37b37a6e0e1874407b906986921ae87570c2d44 100644 --- a/src/app/components/resources/list/resources.component.ts +++ b/src/app/components/resources/list/resources.component.ts @@ -1,21 +1,28 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, OnDestroy } from '@angular/core'; import { Resource, ResourceRO } from 'src/app/models/resource.model'; import { ResourceService } from 'src/app/services/resource.service'; import { Subscription } from 'rxjs'; import { PaginatorOptions } from 'src/app/models/paginator-options.model'; import { IPageHeaderInfo } from '../../../models/page.model'; +import { NotificationService } from '../../../services'; @Component({ selector: 'app-resources', templateUrl: './resources.component.html', styleUrls: ['./resources.component.scss'], }) -export class ResourcesComponent implements OnInit { +export class ResourcesComponent implements OnInit, OnDestroy { pageHeaderInfo: IPageHeaderInfo = { title: '', }; - resources: Resource[]; + objectToBeDeletedId = null; + deleteModalTexts = { + main: 'Si vous poursuivez, la ressource sera définitivement supprimée.', + cancel: 'Annuler', + continue: 'Supprimer', + }; + resources: Resource[] = []; searchChangeSub: Subscription; // Paginator options @@ -32,24 +39,25 @@ export class ResourcesComponent implements OnInit { where = {}; constructor( - private resourcesService: ResourceService, + private _resourceService: ResourceService, + private _notificationService: NotificationService, ) { this.paginator = { - pageIndex: this.resourcesService.pageNumber, + pageIndex: this._resourceService.pageNumber, length: 0, - limit: this.resourcesService.limit, + limit: this._resourceService.limit, pageSizeOptions: [5, 10, 20], }; } ngOnInit(): void { - this.resourcesService.sortOptions = { + this._resourceService.sortOptions = { value: 'name', order: 'asc', }; this.search(); - this.searchChangeSub = this.resourcesService.searchChange$.subscribe( + this.searchChangeSub = this._resourceService.searchChange$.subscribe( () => { this.search(); }, @@ -57,49 +65,81 @@ export class ResourcesComponent implements OnInit { } private search() { - this.resourcesService.getResources() - .subscribe((items: ResourceRO) => { - this.resources = items.resources; - this.totalElement = items.totalCount; - - this.pageHeaderInfo.title = `${this.totalElement} ressources trouvées`; - - this.paginator.limit = this.resourcesService.limit; - this.paginator.pageIndex = this.resourcesService.pageNumber; - this.paginator.length = items.totalCount; - }); + this._resourceService.getResources() + .subscribe( + (items: ResourceRO) => { + this.resources = items.resources; + this.totalElement = items.totalCount; + + this.pageHeaderInfo.title = `${this.totalElement} ressources trouvées`; + this.pageHeaderInfo.title = this.totalElement > 1 ? + `${this.totalElement} ressources trouvées` : + `${this.totalElement} ressource trouvée`; + + this.paginator.limit = this._resourceService.limit; + this.paginator.pageIndex = this._resourceService.pageNumber; + this.paginator.length = items.totalCount; + }, + () => { + this.pageHeaderInfo.title = '0 ressource trouvée'; + this._notificationService.notify({ + type: 'error', + message: 'Une erreur est survenue lors du chargement des ressources.', + }); + }, + ); } // When pagination is changed by user, we update datasetList with new pagination options changePagination(pageIndex) { - this.resourcesService.paginationChanged(this.paginator.limit, pageIndex); + this._resourceService.paginationChanged(this.paginator.limit, pageIndex); } changePageSize(pageSize) { - this.resourcesService.paginationChanged(pageSize, 1); + this._resourceService.paginationChanged(pageSize, 1); } sortBy(key: string) { - if (this.resourcesService.sortOptions.value === key) { - this.resourcesService.reverseSortOrder(); + if (this._resourceService.sortOptions.value === key) { + this._resourceService.reverseSortOrder(); } else { - this.resourcesService.sortOptions.value = key; - this.resourcesService.sortOptions.order = 'asc'; + this._resourceService.sortOptions.value = key; + this._resourceService.sortOptions.order = 'asc'; } this.search(); } get sortOptions() { - return this.resourcesService.sortOptions; + return this._resourceService.sortOptions; + } + + displayDeletePopup(id) { + this.objectToBeDeletedId = id; } - displayDeletePopup(resourceId) { - const pop = confirm('Etes vous sûr de vouloir supprimer cette ressource ?'); - if (pop) { - this.resourcesService.delete(resourceId).subscribe(() => { - this.resourcesService.pageNumber = 1; + deleteResource() { + this._resourceService.delete(this.objectToBeDeletedId).subscribe( + () => { + this._notificationService.notify({ + type: 'success', + message: 'La ressource a été supprimée avec succès.', + }); + this._resourceService.pageNumber = 1; this.search(); - }); - } + }, + () => { + this._notificationService.notify({ + type: 'error', + message: 'Une erreur est survenue lors de la suppression de la ressource.', + }); + }, + () => { + this.objectToBeDeletedId = null; + }, + ); + } + + ngOnDestroy() { + this.searchChangeSub.unsubscribe(); } } diff --git a/src/app/components/reuses/detail/reuse-detail.component.html b/src/app/components/reuses/detail/reuse-detail.component.html new file mode 100644 index 0000000000000000000000000000000000000000..def3b4638daf1cf80179be0aa615741d63343e61 --- /dev/null +++ b/src/app/components/reuses/detail/reuse-detail.component.html @@ -0,0 +1,67 @@ +<section class="section page-container" *ngIf="reuse"> + <app-page-header [pageInfo]="{title: title}"></app-page-header> + + <div class="columns is-centered"> + <div class="column is-8"> + <div class="card"> + <header class="card-header"> + <p class="card-header-title has-text-centered"> + {{reuse.name}} + </p> + </header> + <div class="card-image"> + <figure class="image"> + <img [src]="reuse.logo" alt="Logo de la réutilisation"> + </figure> + </div> + <div class="card-content"> + <div class="content"> + <p> + <span class="has-text-weight-bold">Id: </span> + <span>{{reuse._id}}</span> + </p> + <p> + <span class="has-text-weight-bold">Créateur: </span> + <span>{{reuse.creator}}</span> + </p> + <p> + <span class="has-text-weight-bold">Date de création: </span> + <span>{{reuse.createDate | date:'dd-LL-yyyy HH:mm:ss'}}</span> + </p> + <p> + <span class="has-text-weight-bold">Date de dernière mise à jour: </span> + <span>{{reuse.updateDate | date:'dd-LL-yyyy HH:mm:ss'}}</span> + </p> + + <p> + <span class="has-text-weight-bold">Statut:</span> {{ reuse.published ? 'Publié' : 'Brouillon' }} + </p> + + <p> + <span class="has-text-weight-bold">Site web: </span> + <span><a [href]="reuse.website">{{reuse.website}}</a></span> + </p> + + <div class="list-container" *ngIf="reuse.reuseTypes && reuse.reuseTypes.length > 0"> + <span class="has-text-weight-bold">Type(s) de réutilisation: </span> + <ul> + <li *ngFor="let reuseType of reuseTypes"> + {{ reuseType }} + </li> + </ul> + </div> + + <div class="list-container" *ngIf="reuse.datasetsUsed && reuse.datasetsUsed.length > 0"> + <span class="has-text-weight-bold">Jeu(x) de données réutilisé(s): </span> + <ul> + <li *ngFor="let dataset of reuse.datasetsUsed"> + {{ dataset }} + </li> + </ul> + </div> + </div> + </div> + </div> + </div> + </div> +</section> \ No newline at end of file diff --git a/src/app/components/reuses/detail/reuse-detail.component.scss b/src/app/components/reuses/detail/reuse-detail.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..369170463fc9b68f9075aca173452f0c23f1765d --- /dev/null +++ b/src/app/components/reuses/detail/reuse-detail.component.scss @@ -0,0 +1,21 @@ +.card-header-title { + justify-content: center; +} + +figure { + text-align: center; +} + +figure img { + max-width: 150px; + display: inline-block; + margin-top: 20px; +} + +.list-container:not(:last-of-type) { + margin-bottom: 1rem; + + ul { + margin-top: 0.5rem; + } +} diff --git a/src/app/components/reuses/detail/reuse-detail.component.ts b/src/app/components/reuses/detail/reuse-detail.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..22d807d39a94a065d9317fcd328333769f9e21f3 --- /dev/null +++ b/src/app/components/reuses/detail/reuse-detail.component.ts @@ -0,0 +1,34 @@ + +import { switchMap } from 'rxjs/operators'; +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, ParamMap } from '@angular/router'; +import { Reuse, ReuseTypes } from '../../../models/reuse.model'; +import { ReuseService } from '../../../services'; + +@Component({ + selector: 'app-reuse-detail', + templateUrl: './reuse-detail.component.html', + styleUrls: ['./reuse-detail.component.scss'], +}) +export class ReuseDetailComponent implements OnInit { + + reuse: Reuse; + title: string; + + constructor( + private _route: ActivatedRoute, + private _reuseService: ReuseService, + ) { + } + + ngOnInit(): void { + this.title = this._route.snapshot.data.title; + this._route.paramMap.pipe( + switchMap((params: ParamMap) => this._reuseService.findById(params.get('id')))) + .subscribe((reuse: Reuse) => this.reuse = reuse); + } + + get reuseTypes() { + return this.reuse.reuseTypes.map(e => ReuseTypes[e]); + } +} diff --git a/src/app/components/reuses/edit/reuse-form.component.html b/src/app/components/reuses/edit/reuse-form.component.html new file mode 100644 index 0000000000000000000000000000000000000000..331d49d63dc3b9f4ad9ac1a66d5548509c3451b6 --- /dev/null +++ b/src/app/components/reuses/edit/reuse-form.component.html @@ -0,0 +1,126 @@ +<section class="section page-container" *ngIf="reuse"> + <form [formGroup]="form" (ngSubmit)="onSubmit()" class="columns is-centered is-marginless is-multiline"> + <div class="column is-12 header-with-publication-status"> + <app-page-header [pageInfo]="{title: title}"></app-page-header> + <div class="field status-field"> + <span class="fake-label" *ngIf="form.get('published').value === true">Publié</span> + <span class="fake-label" *ngIf="form.get('published').value === false">Brouillon</span> + <input id="published" type="checkbox" formControlName="published" class="switch is-rounded"> + <label for="published"></label> + </div> + </div> + <div class="column is-7"> + <input type="hidden" formControlName="_id"> + + <div class="field"> + <label class="label required" for="name">Nom</label> + <div class="control"> + <input class="input" type="text" formControlName="name" id="name" required> + </div> + <div *ngIf="name.invalid && (name.dirty || name.touched)" class="alert alert-danger"> + <p *ngIf="name.errors['required']" class="help is-danger"> + Le nom de la réutilisation est obligatoire. + </p> + </div> + </div> + + <app-image-upload [fieldParams]="logoFieldParams" (fileChanged)="logoChanged($event)" + (imageRemoved)="removeLogo()"> + </app-image-upload> + + <div class="field"> + <label class="label required" for="name">Créateur</label> + <div class="control"> + <input class="input" type="text" formControlName="creator" id="creator" required> + </div> + <div *ngIf="creator.invalid && (creator.dirty || creator.touched)" class="alert alert-danger"> + <p *ngIf="creator.errors['required']" class="help is-danger"> + Le créateur est obligatoire. + </p> + </div> + </div> + + <div class="field"> + <label class="label required" for="name">Site web</label> + <div class="control"> + <input class="input" type="text" formControlName="website" id="website" required> + </div> + <div *ngIf="website.invalid && (website.dirty || website.touched)" class="alert alert-danger"> + <p *ngIf="website.errors['required']" class="help is-danger"> + Le site web est obligatoire. + </p> + </div> + </div> + + <div class="field"> + <div class="form-array-header"> + <label class="label">Type de réutilisation</label> + <span class="icon" tabindex=0 (click)="addReuseType()" (keyup.enter)="addReuseType()" + title="Ajouter un type de réutilisation"> + <i class="fas fa-plus"></i> + </span> + </div> + + <div formArrayName="reuseTypes"> + <div *ngFor="let reuseType of reuseTypes.controls; let i = index;" class="form-array-item"> + <div class="form-array-input-wrapper"> + <div class="field"> + <div class="control"> + <div class="select"> + <select type="text" [formControlName]="i" required> + <option hidden value="" disabled selected>Selectionnez un type</option> + <option *ngFor="let key of objectKeys(reuseTypesList)" [value]="key">{{reuseTypesList[key]}} + </option> + </select> + </div> + </div> + </div> + <div *ngIf="reuseType.invalid && (reuseType.dirty || reuseType.touched)" class="alert alert-danger"> + <p *ngIf="reuseType.hasError('required')" class="help is-danger"> + Vous devez saisir le type de réutilisation. + </p> + </div> + </div> + <span class="icon" tabindex=0 (click)="removeReuseType(i)" (keyup.enter)="removeReuseType(i)" + title="Supprimer le type de réutilisation"> + <i class="fas fa-trash"></i> + </span> + </div> + </div> + </div> + + <div class="field"> + <div class="form-array-header"> + <label class="label">Jeu(x) de données réutilisé(s)</label> + <span class="icon" tabindex=0 (click)="addDatasetUsed()" (keyup.enter)="addDatasetUsed()" + title="Ajouter un jeu de données réutilisé"> + <i class="fas fa-plus"></i> + </span> + </div> + + <div formArrayName="datasetsUsed"> + <div *ngFor="let datasetUsed of datasetsUsed.controls; let i = index;" class="form-array-item"> + <div class="form-array-input-wrapper"> + <div class="control"> + <input class="input" type="text" [formControlName]="i" required> + </div> + <div *ngIf="datasetUsed.invalid && (datasetUsed.dirty || datasetUsed.touched)" class="alert alert-danger"> + <p *ngIf="datasetUsed.hasError('required')" class="help is-danger"> + Vous devez saisir le slug du jeu de données réutilisé. + </p> + </div> + </div> + <span class="icon" tabindex=0 (click)="removeDatasetUsed(i)" (keyup.enter)="removeDatasetUsed(i)" + title="Supprimer le jeu de données réutilisé"> + <i class="fas fa-trash"></i> + </span> + </div> + </div> + </div> + + <div class="has-text-right"> + <button class="button button-gl" type="submit" [disabled]="formInvalid == true">Valider</button> + </div> + </div> + </form> +</section> \ No newline at end of file diff --git a/src/app/components/reuses/edit/reuse-form.component.scss b/src/app/components/reuses/edit/reuse-form.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..4bc5c21221ab9f7ba636ca989a4038e8b3db6af0 --- /dev/null +++ b/src/app/components/reuses/edit/reuse-form.component.scss @@ -0,0 +1,43 @@ +.full-width { + width: 100%; +} + +h1 { + text-align: center +} + +.icon { + cursor: pointer; + + &:hover { + .fa-plus { + color: lightblue; + } + + .fa-trash { + color: #d5232a; + } + } +} + +.form-array-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0.5em; + + label { + margin-bottom: 0; + } +} + +.form-array-item { + display: flex; + align-items: center; + margin-bottom: 0.5em; +} + +.form-array-input-wrapper { + flex-grow: 1; + margin-right: 0.5rem; +} diff --git a/src/app/components/reuses/edit/reuse-form.component.ts b/src/app/components/reuses/edit/reuse-form.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..632866a762cb96e9d055d3a4386b19d3260194df --- /dev/null +++ b/src/app/components/reuses/edit/reuse-form.component.ts @@ -0,0 +1,232 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, ParamMap, Router } from '@angular/router'; +import { FormBuilder, FormGroup, Validators, FormArray, FormControl, MaxLengthValidator } from '@angular/forms'; +import { filter, switchMap, catchError, mergeMap } from 'rxjs/operators'; +import { NotificationService, ReuseService, MediaService } from 'src/app/services'; +import { Reuse, ReuseTypes } from '../../../models/reuse.model'; +import { IImageUploadFieldParams } from '../../../models/image-upload.model'; +import { throwError } from 'rxjs'; +import { Media } from '../../../models/media.model'; + +@Component({ + selector: 'app-reuse-form', + templateUrl: './reuse-form.component.html', + styleUrls: ['./reuse-form.component.scss'], +}) +export class ReuseFormComponent implements OnInit { + + reuse: Reuse = new Reuse(); + form: FormGroup; + logoFile: File; + logoFieldParams: IImageUploadFieldParams = { + inputName: 'logo', + label: 'Logo', + existingImageUrl: null, + isRequired: true, + }; + title: string; + objectKeys = Object.keys; // User to iterate over the keys of an object in the template html + + constructor( + private _reuseService: ReuseService, + private _mediaService: MediaService, + private _route: ActivatedRoute, + private _router: Router, + private _fb: FormBuilder, + private _notificationService: NotificationService, + ) { + } + + ngOnInit() { + this.title = this._route.snapshot.data.title; + this.initForm(); + + this._route.paramMap.pipe( + filter((paramMap: ParamMap) => (paramMap.get('id') !== null)), + switchMap((paramMap: ParamMap) => this._reuseService.findById(paramMap.get('id')))) + .subscribe((reuse: Reuse) => { + + this.reuse = reuse; + + this.logoFieldParams.existingImageUrl = reuse.logo; + + this.initForm(); + + }); + } + + initForm() { + this.form = this._fb.group({ + _id: [this.reuse._id], + name: [this.reuse.name, Validators.required], + creator: [this.reuse.creator, Validators.required], + logo: [this.reuse.logo], + website: [this.reuse.website, Validators.required], + published: [this.reuse.published, Validators.required], + reuseTypes: new FormArray(this.reuse.reuseTypes.map(major => new FormControl(major)), Validators.maxLength(3)), + datasetsUsed: new FormArray(this.reuse.datasetsUsed.map(minor => new FormControl(minor))), + }); + } + + onSubmit() { + if (!this.formInvalid) { + this.reuse = new Reuse(this.form.value); + + if (this.reuse._id) { + if (this.logoFile) { + this._mediaService.uploadFile(this.logoFile).pipe( + catchError(() => { + return throwError('Une erreur est survenue lors de l\'upload du logo de la réutilisation.'); + }), + mergeMap((response: Media) => { + this.reuse.logo = response.url; + return this._reuseService.update(this.reuse).pipe( + catchError(() => { + return throwError('Une erreur est survenue lors de la mise à jour de la réutilisation.'); + }), + ); + }), + ).subscribe( + (reuseUpdated) => { + this._notificationService.notify({ + message: 'La réutilisation a été mise à jour avec succès.', + type: 'success', + }); + this._router.navigate(['/reutilisations', this.reuse._id]); + }, + (err) => { + this._notificationService.notify({ + message: err, + type: 'error', + }); + }, + ); + } else { + return this._reuseService.update(this.reuse).subscribe( + (reuseUpdated) => { + this._notificationService.notify({ + message: 'La réutilisation a été mise à jour avec succès.', + type: 'success', + }); + this._router.navigate(['/reutilisations', this.reuse._id]); + }, + () => { + this._notificationService.notify({ + message: 'Une erreur est survenue lors de la mise à jour de la réutilisation.', + type: 'error', + }); + }, + ); + } + } else { + if (this.logoFile) { + this._mediaService.uploadFile(this.logoFile).pipe( + catchError(() => { + return throwError('Une erreur est survenue lors de l\'upload du logo de la réutilisation.'); + }), + mergeMap((response: Media) => { + this.reuse.logo = response.url; + return this._reuseService.create(this.reuse).pipe( + catchError(() => { + return throwError('Une erreur est survenue lors de la création de la réutilisation.'); + }), + ); + }), + ).subscribe( + (reutilisationCreated) => { + this._notificationService.notify({ + message: 'La réutilisation a été créée avec succès.', + type: 'success', + }); + this._router.navigate(['/reutilisations', reutilisationCreated._id]); + }, + (err) => { + this._notificationService.notify({ + message: err, + type: 'error', + }); + }, + ); + } else { + return this._reuseService.create(this.reuse).subscribe( + (reuseCreated) => { + this._notificationService.notify({ + message: 'La réutilisation a été créée avec succès.', + type: 'success', + }); + this._router.navigate(['/reutilisations', reuseCreated._id]); + }, + () => { + this._notificationService.notify({ + message: 'Une erreur est survenue lors de la création de la réutilisation.', + type: 'error', + }); + }, + ); + } + } + } + } + + addReuseType() { + if (this.reuseTypes.length < Object.keys(this.reuseTypesList).length) { + this.reuseTypes.push(new FormControl('')); + } + } + + addDatasetUsed() { + this.datasetsUsed.push(new FormControl()); + } + + removeReuseType(index) { + this.reuseTypes.removeAt(index); + } + + removeDatasetUsed(index) { + this.datasetsUsed.removeAt(index); + } + + // Getters for each property + get name() { + return this.form.controls['name']; + } + + get creator() { + return this.form.controls['creator']; + } + + get website() { + return this.form.controls['website']; + } + + get published() { + return this.form.controls['published']; + } + + get reuseTypes(): FormArray { + return this.form.get('reuseTypes') as FormArray; + } + + get datasetsUsed(): FormArray { + return this.form.get('datasetsUsed') as FormArray; + } + + get formInvalid() { + return this.form.invalid; + } + + get reuseTypesList() { + return ReuseTypes; + } + + logoChanged(fileList: FileList) { + if (fileList && fileList.length > 0) { + this.logoFile = fileList[0]; + } + } + + removeLogo() { + this.form.get('logo').setValue(null); + } + +} diff --git a/src/app/components/reuses/list/reuses.component.html b/src/app/components/reuses/list/reuses.component.html new file mode 100644 index 0000000000000000000000000000000000000000..98c61f33cb9010a27295a02527db69794d72df7a --- /dev/null +++ b/src/app/components/reuses/list/reuses.component.html @@ -0,0 +1,112 @@ +<section class="section page-container"> + <app-page-header [pageInfo]="pageHeaderInfo" [hideBackButton]="true"></app-page-header> + <div class="add-item-link has-text-right"> + <a class="button button-gl" [routerLink]="['new']"> + Ajouter + </a> + </div> + <div class="table entity-list-table" *ngIf="reuses"> + <div class="header columns is-marginless"> + <div class="column is-2"> + <span (click)="sortBy('name')" class="is-sortable"> + <span class="sort-icons"> + <span class="icon"> + <i class="fas fa-sort-up" + [ngClass]="{'icon-red': sortOptions.value === 'name' && sortOptions.order === 'desc'}"></i> + </span> + <span class="icon"> + <i class="fas fa-sort-down" + [ngClass]="{'icon-red': sortOptions.value === 'name' && sortOptions.order === 'asc'}"></i> + </span> + </span> + <span class="column-title" [ngClass]="{'active': sortOptions.value === 'name'}">Nom</span> + </span> + </div> + <div class="column is-1 has-text-centered"> + <span (click)="sortBy('published')" class="is-sortable"> + <span class="sort-icons"> + <span class="icon"> + <i class="fas fa-sort-up" + [ngClass]="{'icon-red': sortOptions.value === 'published' && sortOptions.order === 'desc'}"></i> + </span> + <span class="icon"> + <i class="fas fa-sort-down" + [ngClass]="{'icon-red': sortOptions.value === 'published' && sortOptions.order === 'asc'}"></i> + </span> + </span> + <span class="column-title" [ngClass]="{'active': sortOptions.value === published}">Publié</span> + </span> + </div> + <div class="column is-1 has-text-centered"> + <span class="column-title">Logo</span> + </div> + <div class="column is-2"> + <span (click)="sortBy('creator')" class="is-sortable"> + <span class="sort-icons"> + <span class="icon"> + <i class="fas fa-sort-up" + [ngClass]="{'icon-red': sortOptions.value === 'creator' && sortOptions.order === 'desc'}"></i> + </span> + <span class="icon"> + <i class="fas fa-sort-down" + [ngClass]="{'icon-red': sortOptions.value === 'creator' && sortOptions.order === 'asc'}"></i> + </span> + </span> + <span class="column-title" [ngClass]="{'active': sortOptions.value === creator}">Créateur</span> + </span> + </div> + <div class="column is-2"> + <span class="column-title">Site web</span> + </div> + <div class="column is-2"> + <span class="column-title">Type de réutilisation</span> + </div> + <div class="column is-offset-1 is-1 has-text-centered"> + <span class="column-title">Actions</span> + </div> + </div> + <div class="data-list"> + <div class="data columns is-multiline is-vcentered is-marginless" + *ngFor="let reuse of reuses; let i=index; let odd=odd; let even=even;" [ngClass]="{ odd: odd, even: even }"> + <div class="column is-2"> + <span>{{ reuse.name }}</span> + </div> + <div class="column is-1 has-text-centered"> + <span class="icon has-text-success" *ngIf="reuse.published"> + <i class="far fa-check-circle"></i> + </span> + <span class="icon has-text-danger" *ngIf="!reuse.published"> + <i class="far fa-times-circle"></i> + </span> + </div> + <div class="column is-1 has-text-centered"> + <img class="entity-logo-in-list" [src]="reuse.logo" alt="Logo de la réutilisation"> + </div> + <div class="column is-2"> + <span>{{ reuse.creator }}</span> + </div> + <div class="column is-2"> + <span>{{ reuse.website }}</span> + </div> + <div class="column is-2"> + <span>{{ formatReuseTypes(reuse.reuseTypes) }}</span> + </div> + <div class="column is-offset-1 is-1 has-text-centered actions"> + <app-crud-buttons [id]="reuse._id" (delete)="displayDeletePopup($event)"></app-crud-buttons> + </div> + </div> + </div> + <div class="columns is-marginless paginator"> + <div class="column"> + <app-paginator *ngIf="paginator.length > 0" [length]="paginator.length" [pageSize]="paginator.limit" + [pageSizeOptions]="paginator.pageSizeOptions" [pageIndex]="paginator.pageIndex" [pagesToShow]="5" + [showFirstLastButtons]="true" (page)="changePagination($event)"> + </app-paginator> + </div> + </div> + </div> +</section> + +<app-confirmation-modal (cancel)="objectToBeDeletedId=null" (continue)="deleteChangelog()" [texts]="deleteModalTexts" + [isOpened]="objectToBeDeletedId !== null"> +</app-confirmation-modal> \ No newline at end of file diff --git a/src/app/components/reuses/list/reuses.component.scss b/src/app/components/reuses/list/reuses.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..a499549dab89fa427fa767977d8722ef1158eb67 --- /dev/null +++ b/src/app/components/reuses/list/reuses.component.scss @@ -0,0 +1,3 @@ +.has-text-centered .is-sortable { + justify-content: center; +} diff --git a/src/app/components/reuses/list/reuses.component.ts b/src/app/components/reuses/list/reuses.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..17f361c7d22cf538c6da21297336a6d62693c8ae --- /dev/null +++ b/src/app/components/reuses/list/reuses.component.ts @@ -0,0 +1,147 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { PaginatorOptions } from 'src/app/models/paginator-options.model'; +import { IPageHeaderInfo } from '../../../models/page.model'; +import { Subscription } from 'rxjs'; +import { ReuseService, NotificationService } from '../../../services'; +import { Reuse, ReuseRO, ReuseTypes } from '../../../models/reuse.model'; + +@Component({ + selector: 'app-reuses', + templateUrl: './reuses.component.html', + styleUrls: ['./reuses.component.scss'], +}) +export class ReusesComponent implements OnInit, OnDestroy { + + pageHeaderInfo: IPageHeaderInfo = { + title: '', + }; + objectToBeDeletedId = null; + deleteModalTexts = { + main: 'Si vous poursuivez, la réutilisation sera définitivement supprimée.', + cancel: 'Annuler', + continue: 'Supprimer', + }; + reuses: Reuse[] = []; + searchChangeSub: Subscription; + + // Paginator options + paginator: PaginatorOptions; + pageSize = 10; + pageSizeOptions = [5, 10, 25, 100]; + + sortValue: string; + + totalElement: number; + filters = { + name: '', + }; + where = {}; + + constructor( + private _reuseService: ReuseService, + private _notificationService: NotificationService, + ) { + this.paginator = { + pageIndex: this._reuseService.pageNumber, + length: 0, + limit: this._reuseService.limit, + pageSizeOptions: [5, 10, 20], + }; + } + + ngOnInit(): void { + this._reuseService.sortOptions = { + value: 'name', + order: 'desc', + }; + this.search(); + + this.searchChangeSub = this._reuseService.searchChange$.subscribe( + () => { + this.search(); + }, + ); + } + + private search() { + this._reuseService.getReuses() + .subscribe( + (items: ReuseRO) => { + this.reuses = items.reuses; + this.totalElement = items.totalCount; + + this.pageHeaderInfo.title = this.totalElement > 1 ? + `${this.totalElement} réutilisations trouvées` : + `${this.totalElement} réutilisation trouvée`; + + this.paginator.limit = this._reuseService.limit; + this.paginator.pageIndex = this._reuseService.pageNumber; + this.paginator.length = items.totalCount; + }, + () => { + this.pageHeaderInfo.title = '0 réutilisation trouvée'; + this._notificationService.notify({ + type: 'error', + message: 'Une erreur est survenue lors du chargement des réutilisations.', + }); + }, + ); + } + + changePagination(pageIndex) { + this._reuseService.paginationChanged(this.paginator.limit, pageIndex); + } + + changePageSize(pageSize) { + this._reuseService.paginationChanged(pageSize, 1); + } + + sortBy(key: string) { + if (this._reuseService.sortOptions.value === key) { + this._reuseService.reverseSortOrder(); + } else { + this._reuseService.sortOptions.value = key; + this._reuseService.sortOptions.order = 'asc'; + } + this.search(); + } + + get sortOptions() { + return this._reuseService.sortOptions; + } + + formatReuseTypes(types: string[]) { + return types.map(e => ReuseTypes[e]).join(', '); + } + + displayDeletePopup(id) { + this.objectToBeDeletedId = id; + } + + deleteChangelog() { + this._reuseService.delete(this.objectToBeDeletedId).subscribe( + () => { + this._notificationService.notify({ + type: 'success', + message: 'La réutilisation a été supprimé avec succès.', + }); + this._reuseService.pageNumber = 1; + this.search(); + }, + () => { + this._notificationService.notify({ + type: 'error', + message: 'Une erreur est survenue lors de la suppression de la réutilisation.', + }); + }, + () => { + this.objectToBeDeletedId = null; + }, + ); + + } + + ngOnDestroy() { + this.searchChangeSub.unsubscribe(); + } +} diff --git a/src/app/models/basic-tabs.model.ts b/src/app/models/basic-tabs.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..93caa24433e70b5e3a73a421f703dc654ef653c4 --- /dev/null +++ b/src/app/models/basic-tabs.model.ts @@ -0,0 +1,8 @@ +export interface IBasicTab { + name: string; + fullRouterLinkPath: string; +} +export interface ISimpleTab { + name: string; + isActive: boolean; +} diff --git a/src/app/models/changelog.model.ts b/src/app/models/changelog.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..081b9c7d6b4b58454e46490fe741ee7175591388 --- /dev/null +++ b/src/app/models/changelog.model.ts @@ -0,0 +1,49 @@ +export class ChangelogRO { + changelogs: Changelog[]; + totalCount: number; + + constructor(changelogs, totalCount) { + this.changelogs = changelogs; + this.totalCount = totalCount; + } +} + +export class Changelog { + _id: string; + version: string; + createDate?: string; // bon type ? + updateDate?: string; + language: string; + majorImprovements: string[]; + minorImprovements: string[]; + bugFixes: string[]; + + constructor(changelog?: IChangelog) { + this._id = changelog && changelog._id ? changelog._id : null; + this.version = changelog && changelog.version ? changelog.version : null; + + if (changelog && changelog.createDate) { + this.createDate = changelog.createDate; + } + + if (changelog && changelog.updateDate) { + this.updateDate = changelog.updateDate; + } + + this.language = changelog && changelog.language ? changelog.language : null; + this.majorImprovements = changelog && changelog.majorImprovements ? changelog.majorImprovements : []; + this.minorImprovements = changelog && changelog.minorImprovements ? changelog.minorImprovements : []; + this.bugFixes = changelog && changelog.bugFixes ? changelog.bugFixes : []; + } +} + +export interface IChangelog { + _id: string; + version: string; + createDate: string; // bon type ? + updateDate: string; + language: string; + majorImprovements: string[]; + minorImprovements: string[]; + bugFixes: string[]; +} diff --git a/src/app/models/credit.model.ts b/src/app/models/credit.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..e6bb192f0bef644ef3263ed8dbd99eda10617b15 --- /dev/null +++ b/src/app/models/credit.model.ts @@ -0,0 +1,60 @@ +export class Credit { + id: string; + name: string; + description: string; + logo?: string; + links?: ILink[]; + published: boolean; + + constructor(organization?: ICredit) { + if (organization) { + if (organization.id) { + this.id = organization.id; + } + this.name = organization.name; + this.description = organization.description; + this.links = organization.links; + this.published = organization.published; + + this.links.forEach((link) => { + if (!link.id) { + delete link.id; + } + }); + this.logo = organization.logo; + } else { + this.name = null; + this.description = null; + this.logo = null; + this.links = []; + this.published = false; + } + + } +} + +export class CreditRO { + credits: Credit[]; + totalCount: number; + + constructor(credits, totalCount) { + this.credits = credits; + this.totalCount = totalCount; + } +} + +export interface ICredit { + id: string; + name: string; + description: string; + logo: string; + links: ILink[]; + published: boolean; +} + +interface ILink { + id?: number; + name: string; + url: string; + creditId: number; +} diff --git a/src/app/models/logs.model.ts b/src/app/models/logs.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..58e59348431bf7083fa76564e5fdb0efe09dfca8 --- /dev/null +++ b/src/app/models/logs.model.ts @@ -0,0 +1,8 @@ +export interface ILogs { + step: string; + uuid: string; + sessionId: string; + totalHoursSpent: number; + totalMinutesSpent: number; + totalSecondsSpent: number; +} diff --git a/src/app/models/media.model.ts b/src/app/models/media.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..3c6da00bdeeb9ac1558777669eb0636ddb9bd5bc --- /dev/null +++ b/src/app/models/media.model.ts @@ -0,0 +1,65 @@ +export class Media { + _id: string; + alt: string; + caption: string; + url: string; + creationDate?: string; + updateDate?: string; + + constructor(media?: IMedia) { + if (media) { + this.url = media.url; + if (media._id) { + this._id = media._id; + } + + this.alt = media.alt ? media.alt : ''; + this.caption = media.caption ? media.caption : ''; + + if (media.creationDate) { + this.creationDate = media.creationDate; + } + if (media.updateDate) { + this.updateDate = media.updateDate; + } + } else { + this.alt = ''; + this.caption = ''; + this.url = null; + this.creationDate = null; + this.updateDate = null; + } + + } +} + +export class MediaDTO { + _id: string; + alt: string; + caption: string; + + constructor(media) { + this._id = media._id; + this.alt = media.alt ? media.alt : ''; + this.caption = media.caption ? media.caption : ''; + } +} + +export class MediaRO { + media: Media[]; + mediaCount: number; + + constructor(media, mediaCount) { + this.media = media; + this.mediaCount = mediaCount; + } +} + +export interface IMedia { + _id: string; + alt: string; + caption: string; + url: string; + creationDate: string; + updateDate: string; +} diff --git a/src/app/models/organization.model.ts b/src/app/models/organization.model.ts index 54074cb77548abbd77d3452fe686f213512f5f73..88e89c6de4ca9512351f7adfff52b37144d74505 100644 --- a/src/app/models/organization.model.ts +++ b/src/app/models/organization.model.ts @@ -1,10 +1,11 @@ export class Organization { - id: number; + id: string; name: string; description: string; elasticSearchName?: string; logo?: string; links?: ILink[]; + published: boolean; constructor(organization?: IOrganization) { if (organization) { @@ -15,6 +16,7 @@ export class Organization { this.description = organization.description; this.elasticSearchName = organization.elasticSearchName; this.links = organization.links; + this.published = organization.published; this.links.forEach((link) => { if (!link.id) { @@ -28,6 +30,7 @@ export class Organization { this.logo = null; this.elasticSearchName = null; this.links = []; + this.published = false; } } @@ -44,12 +47,13 @@ export class OrganizationRO { } export interface IOrganization { - id: number; + id: string; name: string; description: string; elasticSearchName: string; logo: string; links: ILink[]; + published: boolean; } interface ILink { diff --git a/src/app/models/projection.model.ts b/src/app/models/projection.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..3be7e304342e762913d47595b04b8bfd81485b49 --- /dev/null +++ b/src/app/models/projection.model.ts @@ -0,0 +1,38 @@ +export class Projection { + id?: number; + name: string; + commonName: string; + description: string; + + constructor(projection?: IProjection) { + if (projection) { + if (projection.id) { + this.id = projection.id; + } + this.name = projection.name; + this.commonName = projection.commonName; + this.description = projection.description; + } else { + this.name = ''; + this.commonName = ''; + this.description = ''; + } + } +} + +export class ProjectionRO { + projections: Projection[]; + totalCount: number; + + constructor(projections, totalCount) { + this.projections = projections; + this.totalCount = totalCount; + } +} + +export interface IProjection { + id: number; + name: string; + commonName: string; + description: string; +} diff --git a/src/app/models/resource.model.ts b/src/app/models/resource.model.ts index 2d28cdda3fc135178cc0515efd361079a1f286ed..bbbf4ebd692b73742c059f4571b810b94cfc7260 100644 --- a/src/app/models/resource.model.ts +++ b/src/app/models/resource.model.ts @@ -10,7 +10,8 @@ export class Resource { isDownloadable: number; isStandard: number; parametersUrl: string; - messageWarning: string; + messageWarningFr: string; + messageWarningEn: string; resourceFormats: IResourceFormat[]; constructor(resource?: IResource) { @@ -27,7 +28,8 @@ export class Resource { this.isDownloadable = resource.isDownloadable; this.isStandard = resource.isStandard; this.parametersUrl = resource.parametersUrl; - this.messageWarning = resource.messageWarning; + this.messageWarningFr = resource.messageWarningFr; + this.messageWarningEn = resource.messageWarningEn; this.resourceFormats = resource.resourceFormats ? resource.resourceFormats : []; } else { this.name = ''; @@ -38,7 +40,8 @@ export class Resource { this.isDownloadable = 0; this.isStandard = 0; this.parametersUrl = ''; - this.messageWarning = ''; + this.messageWarningFr = ''; + this.messageWarningEn = ''; this.resourceFormats = []; } } @@ -64,7 +67,8 @@ export interface IResource { isDownloadable: number; isStandard: number; parametersUrl: string; - messageWarning: string; + messageWarningFr: string; + messageWarningEn: string; resourceFormats: IResourceFormat[]; } diff --git a/src/app/models/reuse.model.ts b/src/app/models/reuse.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..394cab32519a7623c0f726d75f8079aa6f2be50d --- /dev/null +++ b/src/app/models/reuse.model.ts @@ -0,0 +1,60 @@ +export class ReuseRO { + reuses: Reuse[]; + totalCount: number; + + constructor(reuses, totalCount) { + this.reuses = reuses; + this.totalCount = totalCount; + } +} + +export class Reuse { + _id: string; + name: string; + creator: string; + logo: string; + website: string; + reuseTypes: string[]; + datasetsUsed: string[]; + createDate: Date; + updateDate: Date; + published: boolean; + + constructor(reuse?: IReuse) { + this._id = reuse && reuse._id ? reuse._id : null; + this.name = reuse && reuse.name ? reuse.name : null; + this.creator = reuse && reuse.creator ? reuse.creator : null; + this.logo = reuse && reuse.logo ? reuse.logo : null; + this.website = reuse && reuse.website ? reuse.website : null; + this.reuseTypes = reuse && reuse.reuseTypes ? reuse.reuseTypes : []; + this.datasetsUsed = reuse && reuse.datasetsUsed ? reuse.datasetsUsed : []; + if (reuse && reuse.createDate) { + this.createDate = reuse.createDate; + } + + if (reuse && reuse.updateDate) { + this.updateDate = reuse.updateDate; + } + this.published = reuse && reuse.published ? reuse.published : false; + } +} + +export interface IReuse { + _id: string; + name: string; + creator: string; + logo: string; + website: string; + reuseTypes: string[]; + datasetsUsed: string[]; + createDate: Date; + updateDate: Date; + published: boolean; +} + +// tslint:disable-next-line: variable-name +export const ReuseTypes = { + app: 'Application mobile', + web: 'Site web', + article: 'Article', +}; diff --git a/src/app/services/app-config.service.ts b/src/app/services/app-config.service.ts index c8fb34321e489120210a882aa6c17b576b2fe8ae..6159c08faea8c65b8a57e16c47d649300fd162d6 100644 --- a/src/app/services/app-config.service.ts +++ b/src/app/services/app-config.service.ts @@ -16,6 +16,18 @@ export class AppConfig { middlewareLegacyAuth: { url: string; }; + restHeartAggregations:{ + url: string; + }; + changelog: { + url: string; + }; + credits: { + url: string; + }; + reuses: { + url: string; + }; } export let APP_CONFIG: AppConfig; diff --git a/src/app/services/changelog.service.ts b/src/app/services/changelog.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..3728700299dcb7dee4db1449210342206a8936e2 --- /dev/null +++ b/src/app/services/changelog.service.ts @@ -0,0 +1,92 @@ +import { Injectable } from '@angular/core'; +import { Observable, Subject } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { HttpClient } from '@angular/common/http'; +import { APP_CONFIG } from './app-config.service'; +import { ChangelogRO, IChangelog, Changelog } from '../models/changelog.model'; + +@Injectable() +export class ChangelogService { + + changelogServiceUrl: string; + limit: number; + pageNumber: number; + sortOptions: { + value: string, + order: string, + }; + + private _searchChangeSubject: Subject<any>; + + constructor( + private _httpClient: HttpClient) { + this.changelogServiceUrl = `${APP_CONFIG.changelog.url}changelog/`; + this._searchChangeSubject = new Subject<any>(); + this.limit = 10; + this.pageNumber = 1; + } + + getChangelogs(): Observable<ChangelogRO> { + let query = '?'; + query += `limit=${(this.limit ? this.limit : 20)}`; + query += `&offset=${(this.pageNumber ? (this.pageNumber - 1) * this.limit : 0)}`; + query += `&sort_by=${this.sortOptions.value}.${this.sortOptions.order}`; + + return this._httpClient.get<IChangelog[]>(this.changelogServiceUrl + query, { observe: 'response' }).pipe( + map((response) => { + const totalCount = response.headers.get('Content-Range'); + const changelogs = []; + response.body.forEach((changelog) => { + changelogs.push(new Changelog(changelog)); + }); + return new ChangelogRO(changelogs, parseInt(totalCount, 10)); + })); + } + + findById(id): Observable<Changelog> { + return this._httpClient.get<IChangelog>(this.changelogServiceUrl + id).pipe( + map((response) => { + return new Changelog(response); + }), + ); + } + + delete(id) { + return this._httpClient.delete(this.changelogServiceUrl + id, { withCredentials: true }); + } + + create(data) { + return this._httpClient.post<IChangelog>(this.changelogServiceUrl, data, { withCredentials: true }).pipe( + map((response) => { + return new Changelog(response); + }), + ); + } + + update(data) { + return this._httpClient.put<IChangelog>(this.changelogServiceUrl + data._id, data, { withCredentials: true }).pipe( + map((response) => { + return new Changelog(response); + }), + ); + } + + /* PAGINATION */ + paginationChanged(limit: number, pageNumber: number) { + this.limit = limit; + this.pageNumber = pageNumber; + this._searchChangeSubject.next(); + } + + reverseSortOrder(): void { + if (this.sortOptions.order === 'asc') { + this.sortOptions.order = 'desc'; + } else { + this.sortOptions.order = 'asc'; + } + } + + get searchChange$(): Observable<string> { + return this._searchChangeSubject.asObservable(); + } +} diff --git a/src/app/services/credit.service.ts b/src/app/services/credit.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..f80797180b2430ceb13fc72aa29202b5c42d4370 --- /dev/null +++ b/src/app/services/credit.service.ts @@ -0,0 +1,91 @@ +import { Injectable } from '@angular/core'; +import { Subject, Observable } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { CreditRO, ICredit, Credit } from '../models/credit.model'; +import { APP_CONFIG } from './app-config.service'; +import { map } from 'rxjs/operators'; + +@Injectable() +export class CreditService { + + limit: number; + pageNumber: number; + sortOptions: { + value: string, + order: string, + }; + + private _searchChangeSubject: Subject<any>; + + constructor( + private _httpClient: HttpClient) { + this._searchChangeSubject = new Subject<any>(); + this.limit = 10; + this.pageNumber = 1; + } + + getCredits(options?): Observable<CreditRO> { + let query = '?'; + query += `limit=${(this.limit ? this.limit : 20)}`; + query += `&offset=${(this.pageNumber ? (this.pageNumber - 1) * this.limit : 0)}`; + query += `&sort_by=${this.sortOptions.value}.${this.sortOptions.order}`; + + return this._httpClient.get<ICredit[]>(APP_CONFIG.credits.url + query, { withCredentials: true, observe: 'response' }).pipe( + map((response) => { + const totalCount = response.headers.get('Content-Range'); + const credits = []; + response.body.forEach((credit) => { + credits.push(new Credit(credit)); + }); + return new CreditRO(credits, parseInt(totalCount, 10)); + })); + } + + findById(id): Observable<Credit> { + return this._httpClient.get<ICredit>(APP_CONFIG.credits.url + id, { withCredentials: true }).pipe( + map((response) => { + return new Credit(response); + }), + ); + } + + delete(id) { + return this._httpClient.delete(APP_CONFIG.credits.url + id, { withCredentials: true }); + } + + create(data: Credit): Observable<Credit> { + return this._httpClient.post<ICredit>(APP_CONFIG.credits.url, data, { withCredentials: true }).pipe( + map((response) => { + return new Credit(response); + }), + ); + } + + update(data: Credit): Observable<Credit> { + return this._httpClient.put<ICredit>(APP_CONFIG.credits.url + data.id, data, { withCredentials: true }).pipe( + map((response) => { + return new Credit(response); + }), + ); + } + + /* PAGINATION */ + paginationChanged(limit: number, pageNumber: number) { + this.limit = limit; + this.pageNumber = pageNumber; + this._searchChangeSubject.next(); + } + + reverseSortOrder(): void { + if (this.sortOptions.order === 'asc') { + this.sortOptions.order = 'desc'; + } else { + this.sortOptions.order = 'asc'; + } + } + + get searchChange$(): Observable<string> { + return this._searchChangeSubject.asObservable(); + } + +} diff --git a/src/app/services/data-logs.service.ts b/src/app/services/data-logs.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..eebf9ea17fedcdc631765f8cccd62659fb6f7040 --- /dev/null +++ b/src/app/services/data-logs.service.ts @@ -0,0 +1,277 @@ +import { Injectable } from '@angular/core'; +import { Observable, Subject } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { HttpClient } from '@angular/common/http'; +import { APP_CONFIG } from './app-config.service'; + +@Injectable() +export class DataLogsService { + + resourceServiceUrl: string; + limit: number; + pageNumber: number; + length: number; + sortOptions: { + value: string, + order: string, + }; + + private _infoLogSubject: Subject<any>; + private _errorLogSubject: Subject<any>; + private _sessionLogSubject: Subject<any>; + private _slugLogSubject: Subject<any>; + + constructor( + private _httpClient: HttpClient) { + this.resourceServiceUrl = `${APP_CONFIG.restHeartAggregations.url}`; + this._infoLogSubject = new Subject<any>(); + this._errorLogSubject = new Subject<any>(); + this._sessionLogSubject = new Subject<any>(); + this._slugLogSubject = new Subject<any>(); + this.limit = 10; // number of items per pages + this.pageNumber = 1; + this.length = 10; // number total of items in all pages + } + + uuidToSessionId(uuid): Observable<any[]> { + const query = { uuid }; + const aggregationEndPoint = 'indexer_logs/_aggrs/uuidToSessionId?avars='; + const stringQuery = JSON.stringify(query); + return this._httpClient.get<any[]>(this.resourceServiceUrl + aggregationEndPoint + stringQuery).pipe( + map((response) => { + return response; + }), + ); + } + + getAllStepsDuration(uuid, sessionId): Observable<any[]> { + const query = { uuid, session_id: sessionId }; + const aggregationEndPoint = 'indexer_logs/_aggrs/stepTimeAggr?avars='; + const stringQuery = JSON.stringify(query); + return this._httpClient.get<any[]>(this.resourceServiceUrl + aggregationEndPoint + stringQuery).pipe( + map((response) => { + return response; + }), + ); + } + getAllUniqueFields(): Observable<any[]> { + const aggregationEndPoint = 'indexer_logs/_aggrs/getAllUniqueFields'; + return this._httpClient.get<any[]>(this.resourceServiceUrl + aggregationEndPoint).pipe( + map((response) => { + return response; + }), + ); + } + getAllInfoForAllSlugs(sortFilter1, order1, sortFilter2, order2, nbSlugs, pageNumber): Observable<any[]> { + let aggregationEndPoint = 'indexer_logs/_aggrs/allFullSlugAggr?avars='; + // this.length = nbSlugs; + this.pageNumber = pageNumber; + const query = { sortExpr1: { [sortFilter1] : order1 }, sortExpr2 :{ [sortFilter2] : order2 } }; + const stringQuery = JSON.stringify(query); + aggregationEndPoint += stringQuery; + aggregationEndPoint += `&pagesize=${(this.limit)}`; + aggregationEndPoint += `&page=${(pageNumber)}`; + return this._httpClient.get<any[]>(this.resourceServiceUrl + aggregationEndPoint).pipe( + map((response) => { + return response; + }), + ); + } + + getAllInfoForAllSessions(sortFilter1, order1, sortFilter2, order2, nbSessions, pageNumber): Observable<any[]> { + let aggregationEndPoint = 'indexer_logs/_aggrs/allFullSessionAggr?avars='; + const query = { sortExpr1: { [sortFilter1] : order1 }, sortExpr2 :{ [sortFilter2] : order2 } }; + const stringQuery = JSON.stringify(query); + // this.length = nbSessions; + this.pageNumber = pageNumber; + aggregationEndPoint += stringQuery; + aggregationEndPoint += `&pagesize=${(this.limit)}`; + aggregationEndPoint += `&page=${(pageNumber)}`; + return this._httpClient.get<any[]>(this.resourceServiceUrl + aggregationEndPoint).pipe( + map((response) => { + if (!response) { return [{ 'server is down': true }]; } + return response; + }), + ); + } + getAllInfoForOneSlug(slug): Observable<any[]> { + const query = { slug, sortExpr1: { _id : 1 }, sortExpr2 :{ _id : 1 } }; + const stringQuery = JSON.stringify(query); + const aggregationEndPoint = 'indexer_logs/_aggrs/oneFullSlugAggr?avars='; + return this._httpClient.get<any[]>(this.resourceServiceUrl + aggregationEndPoint + stringQuery).pipe( + map((response) => { + return response; + }), + ); + } + + getAllInfoForOneSession(sessionId): Observable<any[]> { + const query = { session_id: sessionId, sortExpr1: { _id : 1 }, sortExpr2 :{ _id : 1 } }; + const stringQuery = JSON.stringify(query); + const aggregationEndPoint = 'indexer_logs/_aggrs/oneFullSessionAggr?avars='; + return this._httpClient.get<any[]>(this.resourceServiceUrl + aggregationEndPoint + stringQuery).pipe( + map((response) => { + return response; + }), + ); + } + + getAllInfoForSlugSession(slug, sessionId): Observable<any[]> { + const query = { slug, session_id: sessionId , sortExpr1: { _id : 1 }, sortExpr2 :{ _id : 1 } }; + const stringQuery = JSON.stringify(query); + const aggregationEndPoint = 'indexer_logs/_aggrs/oneFullSessionSlugAggr?avars='; + return this._httpClient.get<any[]>(this.resourceServiceUrl + aggregationEndPoint + stringQuery).pipe( + map((response) => { + return response; + }), + ); + } + + getData(thisUuid, sessionId): Observable<any[]> { + const query = { uuid: thisUuid, session_id: sessionId }; + const stringQuery = JSON.stringify(query); + const aggregationEndPoint = 'indexer_logs/_aggrs/reportDataAggr?avars='; + return this._httpClient.get<any[]>(this.resourceServiceUrl + aggregationEndPoint + stringQuery).pipe( + map((response) => { + return response; + }), + ); + } + getProcessTimeAggr(sessionId): Observable<any[]> { + const query = { session_id: sessionId }; + const stringQuery = JSON.stringify(query); + const aggregationEndPoint = 'indexer_logs/_aggrs/processTimeAggr?avars='; + return this._httpClient.get<any[]>(this.resourceServiceUrl + aggregationEndPoint + stringQuery).pipe( + map((response) => { + return response; + }), + ); + } + + getSlugFromSessionId(sessionId): Observable<any[]> { + const query = { session_id: sessionId }; + const stringQuery = JSON.stringify(query); + const aggregationEndPoint = 'indexer_logs/_aggrs/sessionIdToSlug?avars='; + return this._httpClient.get<any[]>(this.resourceServiceUrl + aggregationEndPoint + stringQuery).pipe( + map((response) => { + return response; + }), + ); + } + + getUuidFromSlug(slug): Observable<any[]> { + const query = { slug }; + const stringQuery = JSON.stringify(query); + const aggregationEndPoint = 'indexer_logs/_aggrs/slugToUuid?avars='; + return this._httpClient.get<any[]>(this.resourceServiceUrl + aggregationEndPoint + stringQuery).pipe( + map((response) => { + return response; + }), + ); + } + + getSlugFromUuid(uuid): Observable<any[]> { + const query = { uuid }; + const stringQuery = JSON.stringify(query); + const aggregationEndPoint = 'indexer_logs/_aggrs/uuidToSlug?avars='; + return this._httpClient.get<any[]>(this.resourceServiceUrl + aggregationEndPoint + stringQuery).pipe( + map((response) => { + return response[0]['slug_list'][0]; + }), + ); + } + getSessionIdFromSlug(slug): Observable<any[]> { + const query = { slug }; + const stringQuery = JSON.stringify(query); + const aggregationEndPoint = 'indexer_logs/_aggrs/slugToSessionId?avars='; + return this._httpClient.get<any[]>(this.resourceServiceUrl + aggregationEndPoint + stringQuery).pipe( + map((response) => { + return response; + }), + ); + } + + getLogsFromSessionIdAndSlug(sessionId, slug, loglevel): Observable<any[]> { + const query = { slug, loglevel, session_id : sessionId }; + const stringQuery = JSON.stringify(query); + const aggregationEndPoint = 'indexer_logs?filter='; + return this._httpClient.get<any[]>(this.resourceServiceUrl + aggregationEndPoint + stringQuery).pipe( + map((response) => { + return response; + }), + ); + } + + getLogsStepsLogs(step, sessionId, uuid, loglevel, nbLogs, pageNumber): Observable<any[]> { + const query = { step, uuid, loglevel, session_id : sessionId }; + let stringQuery = JSON.stringify(query); + this.length = nbLogs; + this.pageNumber = pageNumber; + stringQuery += `&pagesize=${(this.limit)}`; + stringQuery += `&page=${(pageNumber)}`; + const aggregationEndPoint = 'indexer_logs?filter='; + return this._httpClient.get<any>(this.resourceServiceUrl + aggregationEndPoint + stringQuery).pipe( + map((response) => { + return response; + }), + ); + } + + getComplementaryFromSlugOrSession(type, typeInfo): Observable<any[]> { + if (type === 'slug') { + const query = { slug: typeInfo }; + const stringQuery = JSON.stringify(query); + const aggregationEndPoint = 'indexer_logs/_aggrs/slugToSessionId?avars='; + return this._httpClient.get<any[]>(this.resourceServiceUrl + aggregationEndPoint + stringQuery).pipe( + map((response) => { + return response; + }), + ); + + } + const query = { session_id: typeInfo }; + const stringQuery = JSON.stringify(query); + const aggregationEndPoint = 'indexer_logs/_aggrs/sessionIdToSlug?avars='; + return this._httpClient.get<any[]>(this.resourceServiceUrl + aggregationEndPoint + stringQuery).pipe( + map((response) => { + return response; + }), + ); + } + + /* PAGINATION */ + paginationChanged(limit: number, pageNumber: number, logType:string) { + this.limit = limit; + this.pageNumber = pageNumber; + if (logType === 'INFO') { + this._infoLogSubject.next(); + }if (logType === 'ERROR') { + this._errorLogSubject.next(); + }if (logType === 'SLUG') { + this._slugLogSubject.next(); + }if (logType === 'SESSION') { + this._sessionLogSubject.next(); + } + } + + reverseSortOrder(): void { + if (this.sortOptions.order === 'asc') { + this.sortOptions.order = 'desc'; + } else { + this.sortOptions.order = 'asc'; + } + } + get infoLogChange$(): Observable<string> { + return this._infoLogSubject.asObservable(); + } + get errorLogChange$(): Observable<string> { + return this._errorLogSubject.asObservable(); + } + get sessionLogChange$(): Observable<string> { + return this._sessionLogSubject.asObservable(); + } + get slugLogChange$(): Observable<string> { + return this._slugLogSubject.asObservable(); + } + +} diff --git a/src/app/services/format.service.ts b/src/app/services/format.service.ts index bd2b12a3a6c1c5b27570b675282159bf9922bfa4..e7f321b760c11341a7bc3de360de70f27f15a3fe 100644 --- a/src/app/services/format.service.ts +++ b/src/app/services/format.service.ts @@ -61,14 +61,7 @@ export class FormatService { return this._httpClient.delete(this.formatServiceUrl + id, { withCredentials: true }); } - replaceOrCreate(data): Observable<Format> { - if (data.id) { - return this._httpClient.put<IFormat>(this.formatServiceUrl + data.id, data, { withCredentials: true }).pipe( - map((response) => { - return new Format(response); - }), - ); - } + create(data): Observable<Format> { return this._httpClient.post<IFormat>(this.formatServiceUrl, data, { withCredentials: true }).pipe( map((response) => { return new Format(response); @@ -76,6 +69,14 @@ export class FormatService { ); } + update(data): Observable<Format> { + return this._httpClient.put<IFormat>(this.formatServiceUrl + data.id, data, { withCredentials: true }).pipe( + map((response) => { + return new Format(response); + }), + ); + } + /* PAGINATION */ paginationChanged(limit: number, pageNumber: number) { this.limit = limit; diff --git a/src/app/services/index.ts b/src/app/services/index.ts index 22c1f82163fa750ff151a10e41d7464774c781ee..590e44ae6c280bef6e8f522caf215880f163e960 100644 --- a/src/app/services/index.ts +++ b/src/app/services/index.ts @@ -3,7 +3,13 @@ import { OrganizationService } from './organization.service'; import { ResourceService } from './resource.service'; import { FormatService } from './format.service'; import { NotificationService } from './notification.service'; +import { DataLogsService } from './data-logs.service'; import { NavigationHistoryService } from './navigation-history.service'; +import { ChangelogService } from './changelog.service'; +import { CreditService } from './credit.service'; +import { MediaService } from './media.service'; +import { ReuseService } from './reuse.service'; +import { ProjectionService } from './projection.service'; export { AppConfigService, @@ -11,7 +17,13 @@ export { ResourceService, FormatService, NotificationService, + DataLogsService, NavigationHistoryService, + ChangelogService, + CreditService, + MediaService, + ReuseService, + ProjectionService, }; // tslint:disable-next-line:variable-name @@ -21,5 +33,11 @@ export const AppServices = [ ResourceService, FormatService, NotificationService, + DataLogsService, NavigationHistoryService, + ChangelogService, + CreditService, + MediaService, + ReuseService, + ProjectionService, ]; diff --git a/src/app/services/media.service.ts b/src/app/services/media.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..25e49dcdb5aaca75258ac908a2b3246e6ade01fa --- /dev/null +++ b/src/app/services/media.service.ts @@ -0,0 +1,104 @@ +import { Injectable } from '@angular/core'; +import { Observable, Subject } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { APP_CONFIG } from './app-config.service'; +import { map } from 'rxjs/operators'; +import { MediaRO, IMedia, Media, MediaDTO } from '../models/media.model'; + +@Injectable() +export class MediaService { + + limit: number; + pageNumber: number; + sortOptions: { + value: string, + order: string, + }; + + private _searchChangeSubject: Subject<any>; + + constructor( + private _httpClient: HttpClient) { + this._searchChangeSubject = new Subject<any>(); + this.limit = 10; + this.pageNumber = 1; + } + + getMedia(options?): Observable<MediaRO> { + let query = '?'; + query += `limit=${(this.limit ? this.limit : 20)}`; + query += `&offset=${(this.pageNumber ? (this.pageNumber - 1) * this.limit : 0)}`; + query += `&sort_by=${this.sortOptions.value}.${this.sortOptions.order}`; + + return this._httpClient.get<IMedia[]>( + `${APP_CONFIG.mediaLibrary.url}media${query}`, + { withCredentials: true, observe: 'response' }).pipe( + map((response) => { + const totalCount = response.headers.get('Content-Range'); + const media = []; + response.body.forEach((item) => { + media.push(new Media(item)); + }); + return new MediaRO(media, parseInt(totalCount, 10)); + }), + ); + } + + findById(id): Observable<Media> { + return this._httpClient.get<IMedia>(`${APP_CONFIG.mediaLibrary.url}media/${id}`).pipe( + map((response) => { + return new Media(response); + }), + ); + } + + uploadFile(file, data?: MediaDTO): Observable<Media> { + const formData = new FormData(); + formData.append('file', file); + + if (data) { + for (const prop in data) { + if (data.hasOwnProperty(prop)) { + formData.append(prop, data[prop]); + } + } + } + + return this._httpClient.post<IMedia>(`${APP_CONFIG.mediaLibrary.url}media`, formData, { withCredentials: true }).pipe( + map((response) => { + return new Media(response); + }), + ); + } + + update(data: MediaDTO): Observable<Media> { + return this._httpClient.put<IMedia>(`${APP_CONFIG.mediaLibrary.url}media/${data._id}`, data, { withCredentials: true }).pipe( + map((response) => { + return new Media(response); + }), + ); + } + + delete(id) { + return this._httpClient.delete(`${APP_CONFIG.mediaLibrary.url}media/${id}`, { withCredentials: true }); + } + + /* PAGINATION */ + paginationChanged(limit: number, pageNumber: number) { + this.limit = limit; + this.pageNumber = pageNumber; + this._searchChangeSubject.next(); + } + + reverseSortOrder(): void { + if (this.sortOptions.order === 'asc') { + this.sortOptions.order = 'desc'; + } else { + this.sortOptions.order = 'asc'; + } + } + + get searchChange$(): Observable<string> { + return this._searchChangeSubject.asObservable(); + } +} diff --git a/src/app/services/navigation-history.service.ts b/src/app/services/navigation-history.service.ts index 608c4a52fc5ea94113eb2967ddc079e1b4524f5d..b749461f07589ff1bcdf8f1f4d5b8ea334d8dc22 100644 --- a/src/app/services/navigation-history.service.ts +++ b/src/app/services/navigation-history.service.ts @@ -20,8 +20,8 @@ export class NavigationHistoryService { const position = this.history.length - 1 - index; let res = null; if (position >= 0) { - res = this.history[position]; + res = this.history.splice(position, this.history.length - position); } - return res; + return res ? res[0] : null; } } diff --git a/src/app/services/organization.service.ts b/src/app/services/organization.service.ts index 689c7407ef08430af970d1ed7eb7fabde66bab85..6e1f8b83a821dc7641e687e0a9c3de67d7bcb536 100644 --- a/src/app/services/organization.service.ts +++ b/src/app/services/organization.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { Observable, Subject } from 'rxjs'; -import { map, mergeMap } from 'rxjs/operators'; +import { map } from 'rxjs/operators'; import { HttpClient } from '@angular/common/http'; import { Organization, IOrganization, OrganizationRO } from '../models/organization.model'; import { APP_CONFIG } from './app-config.service'; @@ -30,7 +30,7 @@ export class OrganizationService { query += `&offset=${(this.pageNumber ? (this.pageNumber - 1) * this.limit : 0)}`; query += `&sort_by=${this.sortOptions.value}.${this.sortOptions.order}`; - return this._httpClient.get<IOrganization[]>(APP_CONFIG.organizations.url + query, { observe: 'response' }).pipe( + return this._httpClient.get<IOrganization[]>(APP_CONFIG.organizations.url + query, { withCredentials: true, observe: 'response' }).pipe( map((response) => { const totalCount = response.headers.get('Content-Range'); const organizations = []; @@ -42,7 +42,7 @@ export class OrganizationService { } findById(id): Observable<Organization> { - return this._httpClient.get<IOrganization>(APP_CONFIG.organizations.url + id).pipe( + return this._httpClient.get<IOrganization>(APP_CONFIG.organizations.url + id, { withCredentials: true }).pipe( map((response) => { return new Organization(response); }), @@ -53,36 +53,16 @@ export class OrganizationService { return this._httpClient.delete(APP_CONFIG.organizations.url + id, { withCredentials: true }); } - uploadLogoAndSaveOrganization(logoFile, organization) { - return this.uploadLogo(logoFile).pipe( - mergeMap((response: any) => { - organization.logo = response.mediaUrl; - return this.replaceOrCreate(organization); - }), - ) - } - - uploadLogo(logoFile): Observable<string> { - const formData = new FormData(); - formData.append('file', logoFile); - - return this._httpClient.post<string>(`${APP_CONFIG.mediaLibrary.url}media`, formData, { withCredentials: true }).pipe( + create(data: Organization): Observable<Organization> { + return this._httpClient.post<IOrganization>(APP_CONFIG.organizations.url, data, { withCredentials: true }).pipe( map((response) => { - return response; + return new Organization(response); }), ); } - replaceOrCreate(data: Organization): Observable<Organization> { - if (data.id) { - return this._httpClient.put<IOrganization>(APP_CONFIG.organizations.url + data.id, data, { withCredentials: true }).pipe( - map((response) => { - return new Organization(response); - }), - ); - } - - return this._httpClient.post<IOrganization>(APP_CONFIG.organizations.url, data, { withCredentials: true }).pipe( + update(data: Organization): Observable<Organization> { + return this._httpClient.put<IOrganization>(APP_CONFIG.organizations.url + data.id, data, { withCredentials: true }).pipe( map((response) => { return new Organization(response); }), diff --git a/src/app/services/projection.service.ts b/src/app/services/projection.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..2c6f8f11e1993b232bdc23b438a0833379582e51 --- /dev/null +++ b/src/app/services/projection.service.ts @@ -0,0 +1,98 @@ +import { Injectable } from '@angular/core'; +import { Observable, Subject } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { HttpClient } from '@angular/common/http'; +import { APP_CONFIG } from './app-config.service'; +import { ProjectionRO, IProjection, Projection } from '../models/projection.model'; + +@Injectable() +export class ProjectionService { + + projectionServiceUrl: string; + limit: number; + pageNumber: number; + sortOptions: { + value: string, + order: string, + }; + + private _searchChangeSubject: Subject<any>; + + constructor( + private _httpClient: HttpClient) { + this.projectionServiceUrl = `${APP_CONFIG.resources.url}projections/`; + this._searchChangeSubject = new Subject<any>(); + this.limit = 10; + this.pageNumber = 1; + } + + getProjections(): Observable<ProjectionRO> { + let query = '?'; + query += `limit=${(this.limit ? this.limit : 20)}`; + query += `&offset=${(this.pageNumber ? (this.pageNumber - 1) * this.limit : 0)}`; + query += `&sort_by=${this.sortOptions.value}.${this.sortOptions.order}`; + + return this._httpClient.get<IProjection[]>(this.projectionServiceUrl + query, { observe: 'response' }).pipe( + map((response) => { + const totalCount = response.headers.get('Content-Range'); + const projections = []; + response.body.forEach((projection) => { + projections.push(new Projection(projection)); + }); + return new ProjectionRO(projections, parseInt(totalCount, 10)); + })); + } + + getAllProjections(): Observable<Projection[]> { + return this._httpClient.get<IProjection[]>(this.projectionServiceUrl).pipe( + map(body => body.map(projection => new Projection(projection))), + ); + } + + findById(id): Observable<Projection> { + return this._httpClient.get<IProjection>(this.projectionServiceUrl + id).pipe( + map((response) => { + return new Projection(response); + }), + ); + } + + delete(id) { + return this._httpClient.delete(this.projectionServiceUrl + id, { withCredentials: true }); + } + + create(data): Observable<Projection> { + return this._httpClient.post<IProjection>(this.projectionServiceUrl, data, { withCredentials: true }).pipe( + map((response) => { + return new Projection(response); + }), + ); + } + + update(data): Observable<Projection> { + return this._httpClient.put<IProjection>(this.projectionServiceUrl + data.id, data, { withCredentials: true }).pipe( + map((response) => { + return new Projection(response); + }), + ); + } + + /* PAGINATION */ + paginationChanged(limit: number, pageNumber: number) { + this.limit = limit; + this.pageNumber = pageNumber; + this._searchChangeSubject.next(); + } + + reverseSortOrder(): void { + if (this.sortOptions.order === 'asc') { + this.sortOptions.order = 'desc'; + } else { + this.sortOptions.order = 'asc'; + } + } + + get searchChange$(): Observable<string> { + return this._searchChangeSubject.asObservable(); + } +} diff --git a/src/app/services/resource.service.ts b/src/app/services/resource.service.ts index 52b4b03363f4e926e9f97a8e4ea486cf82c515d0..d5e03ce38c9ffc3f7d175c1f79cf2218711bb096 100644 --- a/src/app/services/resource.service.ts +++ b/src/app/services/resource.service.ts @@ -36,7 +36,6 @@ export class ResourceService { map((response) => { const totalCount = response.headers.get('Content-Range'); const resources = []; - console.log(response); response.body.forEach((resource) => { resources.push(new Resource(resource)); }); @@ -56,14 +55,7 @@ export class ResourceService { return this._httpClient.delete(this.resourceServiceUrl + id, { withCredentials: true }); } - replaceOrCreate(data): Observable<Resource> { - if (data.id) { - return this._httpClient.put<IResource>(this.resourceServiceUrl + data.id, data, { withCredentials: true }).pipe( - map((response) => { - return new Resource(response); - }), - ); - } + create(data) { return this._httpClient.post<IResource>(this.resourceServiceUrl, data, { withCredentials: true }).pipe( map((response) => { return new Resource(response); @@ -71,6 +63,14 @@ export class ResourceService { ); } + update(data) { + return this._httpClient.put<IResource>(this.resourceServiceUrl + data.id, data, { withCredentials: true }).pipe( + map((response) => { + return new Resource(response); + }), + ); + } + createResourceFormats(resourceId: string, resourceFormats: IResourceFormat[]): Observable<IResourceFormat> { return from(resourceFormats).pipe( concatMap((rf) => { diff --git a/src/app/services/reuse.service.ts b/src/app/services/reuse.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..e2331685e2340b5f14b76278e0ea093c403c26de --- /dev/null +++ b/src/app/services/reuse.service.ts @@ -0,0 +1,92 @@ +import { Injectable } from '@angular/core'; +import { Observable, Subject } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { HttpClient } from '@angular/common/http'; +import { APP_CONFIG } from './app-config.service'; +import { ReuseRO, IReuse, Reuse } from '../models/reuse.model'; + +@Injectable() +export class ReuseService { + + reuseServiceUrl: string; + limit: number; + pageNumber: number; + sortOptions: { + value: string, + order: string, + }; + + private _searchChangeSubject: Subject<any>; + + constructor( + private _httpClient: HttpClient) { + this.reuseServiceUrl = `${APP_CONFIG.reuses.url}reuses/`; + this._searchChangeSubject = new Subject<any>(); + this.limit = 10; + this.pageNumber = 1; + } + + getReuses(): Observable<ReuseRO> { + let query = '?'; + query += `limit=${(this.limit ? this.limit : 20)}`; + query += `&offset=${(this.pageNumber ? (this.pageNumber - 1) * this.limit : 0)}`; + query += `&sort_by=${this.sortOptions.value}.${this.sortOptions.order}`; + + return this._httpClient.get<IReuse[]>(this.reuseServiceUrl + query, { withCredentials: true, observe: 'response' }).pipe( + map((response) => { + const totalCount = response.headers.get('Content-Range'); + const reuses = []; + response.body.forEach((reuse) => { + reuses.push(new Reuse(reuse)); + }); + return new ReuseRO(reuses, parseInt(totalCount, 10)); + })); + } + + findById(id): Observable<Reuse> { + return this._httpClient.get<IReuse>(this.reuseServiceUrl + id, { withCredentials: true }).pipe( + map((response) => { + return new Reuse(response); + }), + ); + } + + delete(id) { + return this._httpClient.delete(this.reuseServiceUrl + id, { withCredentials: true }); + } + + create(data) { + return this._httpClient.post<IReuse>(this.reuseServiceUrl, data, { withCredentials: true }).pipe( + map((response) => { + return new Reuse(response); + }), + ); + } + + update(data) { + return this._httpClient.put<IReuse>(this.reuseServiceUrl + data._id, data, { withCredentials: true }).pipe( + map((response) => { + return new Reuse(response); + }), + ); + } + + /* PAGINATION */ + paginationChanged(limit: number, pageNumber: number) { + this.limit = limit; + this.pageNumber = pageNumber; + this._searchChangeSubject.next(); + } + + reverseSortOrder(): void { + if (this.sortOptions.order === 'asc') { + this.sortOptions.order = 'desc'; + } else { + this.sortOptions.order = 'asc'; + } + } + + get searchChange$(): Observable<string> { + return this._searchChangeSubject.asObservable(); + } +} diff --git a/src/app/user/interceptors/auth-interceptor.ts b/src/app/user/interceptors/auth-interceptor.ts index 2f8f113eaf85d5183915cb387743d1faed7300e5..15fef602552523f8d23b2971805db0a3b55188d7 100644 --- a/src/app/user/interceptors/auth-interceptor.ts +++ b/src/app/user/interceptors/auth-interceptor.ts @@ -13,7 +13,7 @@ export class AuthInterceptor implements HttpInterceptor { let request = req; // && req.url.includes('https://data-intothesky.alpha.grandlyon.com/authentication/api/logout' - if (xsrfToken) { + if (xsrfToken && !req.url.includes('indexerdb')) { request = req.clone({ headers: req.headers.set('x-xsrf-token', xsrfToken), }); diff --git a/src/assets/config/config.json b/src/assets/config/config.json index adf822d5cb7103ae1e517c14ba05e845c154d0c4..cb7be29dc905b917b56916f84d3f7c3f2c304d0b 100644 --- a/src/assets/config/config.json +++ b/src/assets/config/config.json @@ -13,5 +13,18 @@ }, "middlewareLegacyAuth": { "url": "https://kong-dev.alpha.grandlyon.com/middleware-legacy/" + }, + "restHeartAggregations": { + "url": "https://kong-dev.alpha.grandlyon.com/indexer-logs/indexerdb/" + }, + "changelog": { + "url": "https://kong-dev.alpha.grandlyon.com/changelog/" + }, + "credits": { + "url": "https://kong-dev.alpha.grandlyon.com/credits/credits/" + }, + "reuses": { + "url": "http://localhost:3008/" } } + diff --git a/src/assets/img/default-file-logo.png b/src/assets/img/default-file-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..e7794f8d4076f561c4d956aaa572f82d17fb8750 Binary files /dev/null and b/src/assets/img/default-file-logo.png differ diff --git a/src/assets/img/ghost-logo.png b/src/assets/img/ghost-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..84c1ec5d2099baaff1f7b1ab4b784ec7dabcc125 Binary files /dev/null and b/src/assets/img/ghost-logo.png differ diff --git a/src/scss/init_bulma.scss b/src/scss/init_bulma.scss index 233d54e3a2b260c24b8dad3fe549d039a43a23d5..105a86142e81fadce62ae9ecb5f3074c5d96abb4 100644 --- a/src/scss/init_bulma.scss +++ b/src/scss/init_bulma.scss @@ -42,3 +42,4 @@ $input-color: $brand-color; @import "../../node_modules/bulma/sass/grid/_all"; @import "../../node_modules/bulma/sass/form/_all"; @import "../../node_modules/bulma/sass/layout/_all"; +@import 'bulma-switch'; diff --git a/src/styles.scss b/src/styles.scss index 09822331d571d2ba51805064dfcb6a158b662a04..480998d60cb2b1158b7f9599ad5ccb755c523c28 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -1,4 +1,6 @@ @import './scss/init_bulma.scss'; +// @import hljs from 'highlight.js/lib/highlight'; +@import 'highlight.js/styles/github.css'; html, body { @@ -83,6 +85,79 @@ textarea.ng-invalid:not(form).ng-touched { /* red */ } +.btn-blue-text { + background: transparent; + border-radius: 2px; + font-size: $size-6; + color: $link-color; + border: 1px solid $grey-super-light-color; + padding: 0.5em 0.75em 0.5em 0.75em; + white-space: normal; + line-height: unset; + min-width: 8rem; + + /* stylelint-disable-next-line */ + span { + font-size: 0.875rem; + color: $link-color; + white-space: normal; + line-height: 1; + } + + &:disabled { + color: $link-color; + } + + &:hover:not(:disabled), + &:focus:not(:disabled) { + color: $link-color; + border-color: $grey-dark-color; + font-weight: bold; + } + + &.is-loading::after { + border-left-color: $link-color; + border-bottom-color: $link-color; + } +} + +.btn-red-text { + background: transparent; + border-radius: 2px; + font-size: $size-6; + color: $tomato-color; + border: 1px solid $grey-super-light-color; + padding: 0.5em 0.75em 0.5em 0.75em; + white-space: normal; + line-height: unset; + min-width: 8rem; + + /* stylelint-disable-next-line */ + span { + font-size: 0.875rem; + color: $tomato-color; + white-space: normal; + line-height: 1; + } + + &:disabled { + color: $tomato-color; + } + + &:hover:not(:disabled), + &:focus:not(:disabled) { + color: $tomato-color; + border-color: $grey-dark-color; + font-weight: bold; + } + + &.is-loading::after { + border-left-color: $tomato-color; + border-bottom-color: $tomato-color; + } +} + + .button-gl { min-width: 8rem; background: $tomato-color; @@ -203,4 +278,44 @@ textarea.ng-invalid:not(form).ng-touched { .paginator { border-top: 1px solid $grey-super-light-color; } + + .entity-logo-in-list { + max-height: 40px; + } +} + +.header-with-publication-status { + display: flex; + align-items: center; + + app-page-header { + flex-grow: 1; + } + + @media screen and (max-width: $tablet) { + flex-flow: column; + align-items: flex-start; + } + + .status-field { + margin-left: 1rem; + + @media screen and (max-width: $tablet) { + margin-left: 0; + } + + .fake-label { + font-weight: bold; + margin-right: 0.5rem; + } + + label { + padding-top: 0; + } + } +} + +.empty-property { + font-style: italic; + color: #818080; }