diff --git a/.github/workflows/basic_test.yml b/.github/workflows/basic_test.yml index 0de401a..eb8a374 100644 --- a/.github/workflows/basic_test.yml +++ b/.github/workflows/basic_test.yml @@ -17,7 +17,7 @@ jobs: run: docker-compose -f test/docker-compose.yml up -d - name: Wait for service to start - run: sleep 20 + run: sleep 30 - name: Check status code run: | @@ -27,4 +27,10 @@ jobs: echo "Request failed with status code:" echo ${status_code} exit 1 - fi \ No newline at end of file + fi + + - name: failed tests 🚩 + if: ${{ failure() }} + run: | + echo "check docker logs" + docker-compose -f test/docker-compose.yml logs \ No newline at end of file diff --git a/webapp/config/HomeContent.html b/webapp/config/HomeContent.html new file mode 100644 index 0000000..08e4d6d --- /dev/null +++ b/webapp/config/HomeContent.html @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/webapp/package-lock.json b/webapp/package-lock.json index 3844d3c..287e6db 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -12,11 +12,12 @@ "@vuepic/vue-datepicker": "^5.4.0", "apexcharts": "^3.41.1", "core-js": "^3.29.0", + "d3": "^7.8.5", "roboto-fontface": "*", "vue": "^3.2.0", "vue-router": "^4.0.0", "vue3-apexcharts": "^1.4.4", - "vuetify": "^3.0.0", + "vuetify": "^3.3.15", "webfontloader": "^1.0.0" }, "devDependencies": { @@ -24,7 +25,7 @@ "eslint": "^8.37.0", "eslint-plugin-vue": "^9.3.0", "sass": "^1.60.0", - "vite": "^4.2.0", + "vite": "^4.4.9", "vite-plugin-vuetify": "^1.0.0" } }, @@ -890,6 +891,14 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, "node_modules/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -943,6 +952,376 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, + "node_modules/d3": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.5.tgz", + "integrity": "sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz", + "integrity": "sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -989,6 +1368,14 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/delaunator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.0.tgz", + "integrity": "sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==", + "dependencies": { + "robust-predicates": "^3.0.0" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -1403,6 +1790,17 @@ "node": ">=8" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -1459,6 +1857,14 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -1875,9 +2281,9 @@ } }, "node_modules/postcss": { - "version": "8.4.26", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.26.tgz", - "integrity": "sha512-jrXHFF8iTloAenySjM/ob3gSj7pCu0Ji49hnjqzsgSRa50hkWCKD0HQ+gMNJkW38jBI68MpAAg7ZWwHwX8NMMw==", + "version": "8.4.29", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz", + "integrity": "sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==", "funding": [ { "type": "opencollective", @@ -2008,10 +2414,15 @@ "resolved": "https://registry.npmjs.org/roboto-fontface/-/roboto-fontface-0.10.0.tgz", "integrity": "sha512-OlwfYEgA2RdboZohpldlvJ1xngOins5d7ejqnIBWr9KaMxsnBqotpptRXTyfNRLnFpqzX6sTDt+X+a+6udnU8g==" }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" + }, "node_modules/rollup": { - "version": "3.26.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.26.2.tgz", - "integrity": "sha512-6umBIGVz93er97pMgQO08LuH3m6PUb3jlDUUGFsNJB6VgTCUaDFpupf5JfU30529m/UKOgmiX+uY6Sx8cOYpLA==", + "version": "3.29.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.0.tgz", + "integrity": "sha512-nszM8DINnx1vSS+TpbWKMkxem0CDWk3cSit/WWCBVs9/JZ1I/XLwOsiUglYuYReaeWWSsW9kge5zE5NZtf/a4w==", "devOptional": true, "bin": { "rollup": "dist/bin/rollup" @@ -2047,6 +2458,16 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "node_modules/sass": { "version": "1.63.6", "resolved": "https://registry.npmjs.org/sass/-/sass-1.63.6.tgz", @@ -2295,14 +2716,14 @@ "dev": true }, "node_modules/vite": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.4.tgz", - "integrity": "sha512-4mvsTxjkveWrKDJI70QmelfVqTm+ihFAb6+xf4sjEU2TmUCTlVX87tmg/QooPEMQb/lM9qGHT99ebqPziEd3wg==", + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz", + "integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==", "devOptional": true, "dependencies": { "esbuild": "^0.18.10", - "postcss": "^8.4.25", - "rollup": "^3.25.2" + "postcss": "^8.4.27", + "rollup": "^3.27.1" }, "bin": { "vite": "bin/vite.js" @@ -2427,9 +2848,9 @@ } }, "node_modules/vuetify": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.3.8.tgz", - "integrity": "sha512-m88MUczIeyNXfINnklBhat2fRknOUmeWyxgGTOZI5b95j9JTZwPH0b1z979nS6gJIyhPDVTuZSS/abp5aUyGBA==", + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.3.15.tgz", + "integrity": "sha512-n7GYBO31k8vA9UfvRwLNyBlkq1WoN3IJ9wNnIBFeV4axleSjFAzzR4WUw7rgj6Ba3q6N2hxXoyxJM21tseQTfQ==", "engines": { "node": "^12.20 || >=14.13" }, diff --git a/webapp/package.json b/webapp/package.json index 0390de6..33b84d4 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -13,11 +13,12 @@ "@vuepic/vue-datepicker": "^5.4.0", "apexcharts": "^3.41.1", "core-js": "^3.29.0", + "d3": "^7.8.5", "roboto-fontface": "*", "vue": "^3.2.0", "vue-router": "^4.0.0", "vue3-apexcharts": "^1.4.4", - "vuetify": "^3.0.0", + "vuetify": "^3.3.15", "webfontloader": "^1.0.0" }, "devDependencies": { @@ -25,7 +26,7 @@ "eslint": "^8.37.0", "eslint-plugin-vue": "^9.3.0", "sass": "^1.60.0", - "vite": "^4.2.0", + "vite": "^4.4.9", "vite-plugin-vuetify": "^1.0.0" } } diff --git a/webapp/public/csv2bufr/csvw_schema.json b/webapp/public/csv2bufr/csvw_schema.json new file mode 100644 index 0000000..951d7f5 --- /dev/null +++ b/webapp/public/csv2bufr/csvw_schema.json @@ -0,0 +1,313 @@ +{ + "@context": "http://www.w3.org/ns/csvw", + "url": "./aws_example_2.csv", + "tableSchema": { + "columns": [ + { + "titles": "wsi_series", + "dc:description": "WIGOS identifier series", + "datatype": {"base": "integer"}, + "null": "null" + }, + { + "titles": "wsi_issuer", + "dc:description": "WIGOS issuer of identifier", + "datatype": {"base": "integer"}, + "null": "null" + }, + { + "titles": "wsi_issue_number", + "dc:description": "WIGOS issue number", + "datatype": {"base": "integer"} + }, + { + "titles": "wsi_local", + "dc:description": "WIGOS local identifier", + "datatype": {"base": "string"} + }, + { + "titles": "wmo_block_number", + "dc:description": "WMO block number", + "datatype": {"base": "integer"} + }, + { + "titles": "wmo_station_number", + "dc:description": "WMO station number", + "datatype": {"base": "integer"} + }, + { + "titles": "station_type", + "dc:description": "Type of observing station, encoding using code table 0 02 001 (set to 0, automatic)", + "datatype": {"base": "integer"} + }, + { + "titles": "year", + "dc:description": "Year (UTC), the time of observation (based on the actual time the barometer is read)", + "datatype": { + "base": "integer", + "minimum": "1900", + "maximum": "2023" + } + }, + { + "titles": "month", + "dc:description": "Month (UTC), the time of observation (based on the actual time the barometer is read)", + "datatype": { + "base": "integer", + "minimum": "1", + "maximum": "12" + } + }, + { + "titles": "day", + "dc:description": "Day (UTC), the time of observation (based on the actual time the barometer is read)", + "datatype": { + "base": "integer", + "minimum": "1", + "maximum": "31" + } + }, + { + "titles": "hour", + "dc:description": "Hour (UTC), the time of observation (based on the actual time the barometer is read)", + "datatype": { + "base": "integer", + "minimum": "0", + "maximum": "23" + } + }, + { + "titles": "minute", + "dc:description": "Minute (UTC), the time of observation (based on the actual time the barometer is read)", + "datatype": { + "base": "integer", + "minimum": "0", + "maximum": "59" + } + }, + { + "titles": "latitude", + "dc:description": "Latitude of the station (to 5 decimal places)", + "datatype": { + "base": "decimal", + "minimum": "-90", + "maximum": "90" + } + }, + { + "titles": "longitude", + "dc:description": "Longitude of the station (to 5 decimal places)", + "datatype": { + "base": "decimal", + "minimum": "-180", + "maximum": "180" + } + }, + { + "titles": "station_height_above_msl", + "dc:description": "Height of the station ground above mean sea level (to 1 decimal place)", + "datatype": {"base": "decimal"} + }, + { + "titles": "barometer_height_above_msl", + "dc:description": "Height of the barometer above mean sea level (to 1 decimal place), typically height of station ground plus the height of the sensor above local ground", + "datatype": {"base": "decimal"} + }, + { + "titles": "station_pressure", + "dc:description": "Pressure observed at the station level to the nearest 10 pascals", + "datatype": { + "base": "integer", + "minimum": "50000", + "maximum": "150000" + } + }, + { + "titles": "msl_pressure", + "dc:description": "Pressure reduced to mean sea level to the nearest 10 pascals", + "datatype": { + "base": "integer", + "minimum": "50000", + "maximum": "150000" + } + }, + { + "titles": "geopotential_height", + "dc:description": "Geoptential height expressed in geopotential meters (gpm) to 0 decimal places", + "datatype": {"base": "integer"} + }, + { + "titles": "thermometer_height", + "dc:description": "Height of thermometer or temperature sensor above the local ground to 2 decimal places", + "datatype": {"base": "decimal"} + }, + { + "titles": "air_temperature", + "dc:description": "Instantaneous air temperature to 2 decimal places", + "datatype": { + "base": "decimal", + "minimum": "0" + } + }, + { + "titles": "dewpoint_temperature", + "dc:description": "Instantaneous dewpoint temperature to 2 decimal places", + "datatype": { + "base": "decimal", + "minimum": "0" + } + }, + { + "titles": "relative_humidity", + "dc:description": "Instantaneous relative humidity to zero decimal places", + "datatype": { + "base": "integer", + "minimum": "0", + "maximum": "150" + } + }, + { + "titles": "method_of_ground_state_measurement", + "dc:description": "Method of observing the state of the ground, encoded using code table 0 02 176", + "datatype": {"base": "integer"} + }, + { + "titles": "ground_state", + "dc:description": "State of the ground encoded using code table 0 20 062", + "datatype": {"base": "integer"} + }, + { + "titles": "method_of_snow_depth_measurement", + "dc:description": "Method of observing the snow depth encoded using code table 0 02 177", + "datatype": {"base": "integer"} + }, + { + "titles": "snow_depth", + "dc:description": "Snow depth at time of observation to 2 decimal places", + "datatype": { + "base": "decimal", + "minimum": "0" + } + }, + { + "titles": "precipitation_intensity", + "dc:description": "Intensity of precipitation at time of observation to 5 decimal places", + "datatype": { + "base": "decimal", + "minimum": "0" + } + }, + { + "titles": "anemometer_height", + "dc:description": "Height of the anemometer above local ground to 2 decimal place", + "datatype": {"base": "decimal"} + }, + { + "titles": "time_period_of_wind", + "dc:description": "Time period over which the wind speed and direction have been averaged. 10 minutes in normal cases or the number of minutes since a significant change occuring in the preceeding 10 minutes.", + "datatype": {"base": "negativeInteger"} + }, + { + "titles": "wind_direction", + "dc:description": "Wind direction (at anemometer height) averaged from the caterisan components over the indicated time period, 0 decimal places", + "datatype": { + "base": "integer", + "minimum": "0", + "maximum": "360" + } + }, + { + "titles": "wind_speed", + "dc:description": "Wind speed (at anemometer height) averaged from the cartesian components over the indicated time period, 1 decimal place", + "datatype": {"base": "decimal"} + }, + { + "titles": "maximum_wind_gust_direction_10_minutes", + "dc:description": "Highest 3 second average over the preceding 10 minutes, 0 decimal places", + "datatype": { + "base": "integer", + "minimum": "0", + "maximum": "360" + } + }, + { + "titles": "maximum_wind_gust_speed_10_minutes", + "dc:description": "Highest 3 second average over the preceding 10 minutes, 1 decimal place", + "datatype": {"base": "decimal"} + }, + { + "titles": "maximum_wind_gust_direction_1_hour", + "dc:description": "Highest 3 second average over the preceding hour, 0 decimal places", + "datatype": { + "base": "integer", + "minimum": "0", + "maximum": "360" + } + }, + { + "titles": "maximum_wind_gust_speed_1_hour", + "dc:description": "Highest 3 second average over the preceding hour, 1 decimal place", + "datatype": {"base": "decimal"} + }, + { + "titles": "maximum_wind_gust_direction_3_hours", + "dc:description": "Highest 3 second average over the preceding 3 hours, 0 decimal places", + "datatype": { + "base": "integer", + "minimum": "0", + "maximum": "360" + } + }, + { + "titles": "maximum_wind_gust_speed_3_hours", + "dc:description": "Highest 3 second average over the preceding 3 hours, 1 decimal place", + "datatype": {"base": "decimal"} + }, + { + "titles": "rain_sensor_height", + "dc:description": "Height of the rain gauge above local ground to 2 decimal place", + "datatype": {"base": "decimal"} + }, + { + "titles": "total_precipitation_1_hour", + "dc:description": "Total precipitation over the past hour, 1 decimal place", + "datatype": { + "base": "decimal", + "minimum": "0" + } + }, + { + "titles": "total_precipitation_3_hours", + "dc:description": "Total precipitation over the past 3 hours, 1 decimal place", + "datatype": { + "base": "decimal", + "minimum": "0" + } + }, + { + "titles": "total_precipitation_6_hours", + "dc:description": "Total precipitation over the past 6 hours, 1 decimal place", + "datatype": { + "base": "decimal", + "minimum": "0" + } + }, + { + "titles": "total_precipitation_12_hours", + "dc:description": "Total precipitation over the past 12 hours, 1 decimal place", + "datatype": { + "base": "decimal", + "minimum": "0" + } + }, + { + "titles": "total_precipitation_24_hours", + "dc:description": "Total precipitation over the past 24 hours, 1 decimal place", + "datatype": { + "base": "decimal", + "minimum": "0" + } + } + ] + } + } \ No newline at end of file diff --git a/webapp/src/components/CsvToBUFRForm.vue b/webapp/src/components/CsvToBUFRForm.vue new file mode 100644 index 0000000..b32241c --- /dev/null +++ b/webapp/src/components/CsvToBUFRForm.vue @@ -0,0 +1,557 @@ + + + diff --git a/webapp/src/components/DownloadButton.vue b/webapp/src/components/DownloadButton.vue index 90e9710..d4c0bb6 100644 --- a/webapp/src/components/DownloadButton.vue +++ b/webapp/src/components/DownloadButton.vue @@ -9,29 +9,61 @@ import { VBtn } from 'vuetify/lib/components/index.mjs'; export default defineComponent({ name: 'DownloadButton', props: { - fileUrl: { + fileName: { type: String, required: true, }, + fileUrl: { + type: String, + required: false, + default: '', + }, + data: { + type: String, + required: false, + default: '', + }, }, components: { VBtn }, setup(props) { - // Extract the file name from the URL - const fileName = props.fileUrl.split('/').pop(); - // function to download file const downloadFile = () => { // Create a temporary anchor element to initiate the download const link = document.createElement('a'); - link.href = props.fileUrl; - link.target = '_blank'; - // Programmatically trigger the click event on the link to start the download - link.click(); + if( props.fileUrl != '' ) { + //console.log("Downloading file from URL: " + props.fileUrl) + link.href = props.fileUrl; + link.target = '_blank'; + // Programmatically trigger the click event on the link to start the download + link.click(); + // Clean up + URL.revokeObjectURL(link.href); + } + else if( props.data != '' ) { + //console.log("Downloading file from data: " + props.data) + // Decode the base64 encoded data + const decodedData = atob(props.data); + // Convert the decoded data to a Uint8Array + const uint8Array = new Uint8Array(decodedData.length); + for (let i = 0; i < decodedData.length; ++i) { + uint8Array[i] = decodedData.charCodeAt(i); + } + // Create a Blob with the Uint8Array data + const blob = new Blob([uint8Array], { type: 'application/octet-stream' }); + link.href = URL.createObjectURL(blob); + link.download = props.fileName; + // Programmatically trigger the click event on the link to start the download + link.click(); + // Clean up + URL.revokeObjectURL(link.href); + } + else { + console.log("No fileURL or data provided"); + } }; return { - fileName, downloadFile }; }, diff --git a/webapp/src/components/HomePage.vue b/webapp/src/components/HomePage.vue new file mode 100644 index 0000000..6a1ae23 --- /dev/null +++ b/webapp/src/components/HomePage.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/webapp/src/components/InspectBufrButton.vue b/webapp/src/components/InspectBufrButton.vue index 7260b3c..71fe7b8 100644 --- a/webapp/src/components/InspectBufrButton.vue +++ b/webapp/src/components/InspectBufrButton.vue @@ -6,6 +6,10 @@ {{ fileName }} +
+ +

No items found in bufr

+
{{ key }}: {{ value }} @@ -26,10 +30,20 @@ import { VCard, VCardTitle, VCardText, VCardItem, VForm, VTextarea, VBtn, VSelec export default defineComponent({ name: 'InspectBufrButton', props: { - fileUrl: { + fileName: { type: String, required: true, }, + fileUrl: { + type: String, + required: false, + default: '', + }, + data: { + type: String, + required: false, + default: '', + }, }, components: { VCard, @@ -42,8 +56,6 @@ export default defineComponent({ VSelect, }, setup(props) { - // Extract the file name from the URL - const fileName = props.fileUrl.split('/').pop(); const itemsInBufr = ref([]); const dialog = ref(false); // function to create new object and to add to store @@ -91,11 +103,23 @@ export default defineComponent({ const callInspect = async () => { // set items_from_bufr back to empty array itemsInBufr.value = []; - var payload = { + let payload; + if (props.fileUrl !== '') { + payload = { + inputs: { + data_url: props.fileUrl + } + }; + } else if (props.data !== '') { + payload = { inputs: { - data_url: props.fileUrl + data: props.data } - }; + }; + } else { + console.error('No data or fileUrl provided'); + return; + } const inspectUrl = `${import.meta.env.VITE_API_URL}/processes/wis2box-bufr2geojson/execution` const response = await fetch(inspectUrl, { method: 'POST', @@ -110,7 +134,7 @@ export default defineComponent({ console.error('HTTP error', response.status); } else { const data = await response.json(); - console.log(data); + //console.log(data); if (data.items) { // Use Array.map to create a new array of the items in the bufr file itemsInBufr.value = data.items.map(item => { @@ -118,18 +142,18 @@ export default defineComponent({ return item.properties; } }); - console.log(itemsInBufr.value); + //console.log(itemsInBufr.value); } } }; return { - fileName, itemsInBufr, inspectFile, - dialog + dialog, + fileName: props.fileName }; }, -}); +}); \ No newline at end of file + diff --git a/webapp/src/components/MonitoringPage.vue b/webapp/src/components/MonitoringPage.vue index c39edf8..6da8168 100644 --- a/webapp/src/components/MonitoringPage.vue +++ b/webapp/src/components/MonitoringPage.vue @@ -1,15 +1,15 @@ diff --git a/webapp/src/components/NotificationDashboard.vue b/webapp/src/components/NotificationDashboard.vue index 578c6d1..6cdb04e 100644 --- a/webapp/src/components/NotificationDashboard.vue +++ b/webapp/src/components/NotificationDashboard.vue @@ -1,119 +1,230 @@ @@ -137,14 +248,21 @@ export default defineComponent({ // is updated when the user selects another dataset in MonitoringPage.vue topicHierarchy(newVal, oldVal) { if (newVal !== oldVal) { - // Run all important methods - this.getNotifications(); - this.getTimestampCounts(); - this.getWsiCounts(); - this.getSummary(); - this.initChartData(); + console.log("Topic hierarchy changed to:", newVal); + this.update_messages(); } - } + }, + messages: { + immediate: false, // To not trigger the watcher immediately on component mount + deep: false, // To not deep watch changes within the array + handler(newMessages) { + console.log("Messages changed"); + // Run the methods dependent on the messages + console.log("Messages: ", newMessages); + this.updateSummary(); + this.updateChartData(); + }, + }, }, components: { VCard, @@ -157,15 +275,57 @@ export default defineComponent({ data() { return { // Messages from API call - messages: [], + messages: [], // Array of messages sorted by pubtime // Example message when Romania synoptic dataset selected by user - testMessageSynoptic: [ + testMessageRomania: [ + { + "id": "8855221f-2112-43fa-b2da-1552e8aa9a2d", + "properties": { + "data_id": "wis2/rou/rnimh/data/core/weather/surface-based-observations/synop/WIGOS_0-20000-0-15020_20220331T000000", + "datetime": "2022-03-31T00:00:00Z", + "pubtime": "2023-09-04T03:58:20Z", + "wigos_station_identifier": "0-20000-0-15020", + "id": "8855221f-2112-43fa-b2da-1552e8aa9a2d" + }, + "links": [ + { + "rel": "canonical", + "type": "application/x-bufr", + "href": "http://3.73.37.35/data/2022-03-31/wis/rou/rnimh/data/core/weather/surface-based-observations/synop/WIGOS_0-20000-0-15020_20220331T000000.bufr4", + }, + { + "rel": "via", + "type": "text/html", + "href": "https://oscar.wmo.int/surface/#/search/station/stationReportDetails/0-20000-0-15015" + }] + }, + { + "id": "8855221f-2112-43fa-b2da-1552e8aa9a2d", + "properties": { + "data_id": "wis2/rou/rnimh/data/core/weather/surface-based-observations/synop/WIGOS_0-20000-0-15020_20220331T000000", + "datetime": "2022-03-31T00:00:00Z", + "pubtime": "2023-09-04T03:58:25Z", + "wigos_station_identifier": "0-20000-0-15020", + "id": "8855221f-2112-43fa-b2da-1552e8aa9a2d" + }, + "links": [ + { + "rel": "canonical", + "type": "application/x-bufr", + "href": "http://3.73.37.35/data/2022-03-31/wis/rou/rnimh/data/core/weather/surface-based-observations/synop/WIGOS_0-20000-0-15020_20220331T000000.bufr4", + }, + { + "rel": "via", + "type": "text/html", + "href": "https://oscar.wmo.int/surface/#/search/station/stationReportDetails/0-20000-0-15015" + }] + }, { "id": "8855221f-2112-43fa-b2da-1552e8aa9a2d", "properties": { "data_id": "wis2/rou/rnimh/data/core/weather/surface-based-observations/synop/WIGOS_0-20000-0-15020_20220331T000000", "datetime": "2022-03-31T00:00:00Z", - "pubtime": "2023-08-24T13:58:20Z", + "pubtime": "2023-09-04T11:58:20Z", "wigos_station_identifier": "0-20000-0-15020", "id": "8855221f-2112-43fa-b2da-1552e8aa9a2d" }, @@ -183,7 +343,7 @@ export default defineComponent({ } ], // Example message when Malawi surface dataset selected by user - testMessageSurface: [ + testMessageMalawi: [ { "id": "af14d8c4-5f63-45af-8171-7730ec9932ba", "properties": { @@ -206,17 +366,6 @@ export default defineComponent({ }] } ], - // Object containing the publish time and associated file URLs of - // each notification from the last 24 hours - notificationData: { - "publishTimes": [], - "fileUrls": [] - }, - // Count for how many files were published at a given time, rounded - // to the nearest minute - timestampCounts: {}, - // Count for how files were published from a given WSI - wsiCounts: {}, // Object for notification summary statistics summaryStats: { "totalFilesLastHour": 0, @@ -231,28 +380,13 @@ export default defineComponent({ // Initiate ApexCharts series to be filled with data later chartSeries: [ { - name: 'BUFR files published', + name: 'WIS2 notifications published', data: [] } - ] - } - }, - computed: { - // Get current time - now() { - return new Date(); - }, - // Get time 1 hour ago from now - oneHourAgo() { - return new Date(this.now.getTime() - 1 * 60 * this.mins); - }, - // Get time 24 hours ago from now - oneDayAgo() { - return new Date(this.now.getTime() - 24 * 60 * this.mins); - }, - // Options for the ApexChart bar graph - chartOptions() { - return { + ], + // ApexCharts options + // Options for the ApexChart bar graph + chartOptions: { chart: { type: 'bar', id: 'realtime', @@ -286,16 +420,16 @@ export default defineComponent({ }, tooltip: { x: { - format: 'dd MMM HH:mm' + format: 'dd MMM HH:mm', } }, colors: ['#00BD9D'], // Colour of bars xaxis: { type: 'datetime', - // Earliest displayed time 1 hour from current - min: this.oneHourAgo.getTime(), - // Latest displayed time is current time - max: this.now.getTime() + categories: this.getLast24Hours(), + labels: { + format: "dd MMM HH:mm", // Format the x-axis labels as desired + } }, yaxis: { min: 0, @@ -309,27 +443,76 @@ export default defineComponent({ show: false } } - } + }, + // Search parameter for published files + fileSearch: null } }, methods: { + getLast24Hours() { + const now = new Date(); + const past24Hours = new Date(now - 24 * 60 * 60 * 1000); // Subtract 24 hours in milliseconds + const timeRange = []; + + for (let time = past24Hours; time <= now; time += 60 * 60 * 1000) { // Generate data points every hour + timeRange.push(time); + } + console.log("Time range: ", timeRange); + return timeRange; + }, + now() { + return new Date(); + }, + // Get time 1 hour ago from now + oneHourAgo() { + return new Date(this.now().getTime() - 1 * 60 * this.mins); + }, + // Get time 24 hours ago from now + oneDayAgo() { + return new Date(this.now().getTime() - 24 * 60 * this.mins); + }, + // Method to get the messages from the features array + getMessagesFromFeatures(features) { + const selectedFields = features.map(item => ({ + pubtime: new Date(item.properties.pubtime + "Z"), + canonical_url: this.getCanonicalUrl(item.links), + filename: this.getFileName(this.getCanonicalUrl(item.links)) + // Add more fields as needed + })); + // sort by pubtime descending + return selectedFields.sort((a, b) => b.pubtime - a.pubtime); + }, // Builds a topic hierarchy dependent url and fetches the notifications - async apiCall() { - // If in TEST_MODE or API URL is not defined, just return. + async update_messages() { + console.log("Dataset selected: ", this.topicHierarchy); + // Check if TEST_MODE is set in .env file or if VITE_API_URL is not set if (import.meta.env.VITE_TEST_MODE === "true" || import.meta.env.VITE_API_URL == undefined) { - return; + console.log("TEST_MODE is enabled"); + // Use example data selected by user + let test_features = []; + if (this.topicHierarchy == "rou/rnimh/data/core/weather/surface-based-observations/synop") { + test_features = this.testMessageRomania; + } + else if (this.topicHierarchy == "mwi/mwi_met_centre/data/core/weather/surface-based-observations/synop") { + test_features = this.testMessageMalawi; + } + this.messages = this.getMessagesFromFeatures(test_features); } - + else { + // Use API to get data + await this.apiCall(); + } + }, + async apiCall() { const apiUrl = `${import.meta.env.VITE_API_URL}/collections/messages/items`; console.log("Fetching notifications from:", apiUrl); - try { const params = new URLSearchParams({ f: 'json', // Specify the response format as JSON data_id: `${this.topicHierarchy}%`, // Filter by data_id that starts with the provided topic hierarchy sortBy: '-datetime', // Sort by time in descending order - limit: 9999, // Limit the results to the last 9999 features - datetime: `${this.oneDayAgo.toISOString()}/${this.now.toISOString()}`, // Filter to last 24 hours + limit: 500, // Limit the results to the last 500 features + datetime: `${this.oneDayAgo().toISOString()}/${this.now().toISOString()}`, // Filter to last 24 hours }); // Make the HTTP GET request console.log("API request:", `${apiUrl}?${params}`) @@ -340,8 +523,7 @@ export default defineComponent({ else { const data = await response.json(); if (data.features) { - this.messages = data.features; - console.log("Messages:", this.messages); + this.messages = this.getMessagesFromFeatures(data.features); } else { console.error("API response does not contain features:", data); @@ -354,58 +536,24 @@ export default defineComponent({ }, // Method to get summary statistics of total published files in the // past hour and past 24 hours - async getSummary() { + updateSummary() { + console.log("Update summary statistics"); // Get the number of publish times from the last hour - const timesWithinHour = this.notificationData.publishTimes.filter(time => { - return time >= this.oneHourAgo; + const timesWithinHour = this.messages.filter(message => { + const publishTime = message.pubtime; + return publishTime >= this.oneHourAgo(); }).length; - // Get the number of publish times from the last 24 hours // (which is all of the publish times by the way we call the API) - const timesWithinDay = this.notificationData.publishTimes.length; - + const timesWithinDay = this.messages.length; // Update the summary statistics object this.summaryStats.totalFilesLastHour = timesWithinHour; this.summaryStats.totalFilesLastDay = timesWithinDay; }, - // Loads notification data of publish times and file urls - async getNotifications() { - // Check if TEST_MODE is set in .env file or if VITE_API_URL is not set - if (import.meta.env.VITE_TEST_MODE === "true" || import.meta.env.VITE_API_URL == undefined) { - console.log("TEST_MODE is enabled"); - console.log("Dataset selected: ", this.topicHierarchy); - // Use example data selected by user - if (this.topicHierarchy == "rou/rnimh/data/core/weather/surface-based-observations/synop") { - this.messages = this.testMessageSynoptic; - } - else if (this.topicHierarchy == "mwi/mwi_met_centre/data/core/weather/surface-based-observations/synop") { - this.messages = this.testMessageSurface; - } - } - else { - await this.apiCall(); - } - - this.notificationData = { - // Get the publish times of the notifications as an array - publishTimes: this.messages.map(item => - new Date(item.properties.pubtime)), - // Get the file urls of the notifications as an array - fileUrls: this.messages.map(item => { - const canonicalLink = item.links.find(link => link.rel === "canonical"); - // The file url is the href value associated with the canonical relation - return canonicalLink ? canonicalLink.href : null; - }) - } - - // Get summary statistics of notification data - await this.getSummary(); - - console.log("Publish times and URLs: ", this.notificationData); - }, + // Round a datetime to the nearest minute, used by updateChartData() roundToNearestMinute(time) { // Get minutes and seconds of the datetime - let minutes = time.getMinutes(); + let minutes = time.getUTCMinutes(); let seconds = time.getSeconds(); if (seconds >= 30) { @@ -418,123 +566,67 @@ export default defineComponent({ return time; }, - // Builds the timestamp count array used in the ApexCharts bar graph - async getTimestampCounts() { - // Reset the timestamp counts - this.timestampCounts = {}; - - // For each publish time, round to the nearest minute and update - // the count - this.notificationData["publishTimes"].forEach(time => { - - const roundedTime = this.roundToNearestMinute(time); - - // Update timestampCount object with this rounded publish time - if (this.timestampCounts[roundedTime]) { - // If the key already exists, add to the count - this.timestampCounts[roundedTime]++; - } - else { - // Otherwise begin the count at 1 - this.timestampCounts[roundedTime] = 1; + // updateChartData() counts the number of messages per minute and uses this + updateChartData() { + console.log("update chart data"); + // count messages per minutes and uses this create the chart data + const timestampCounts = {}; + // add 24hour ago, 0 to timestampCounts + timestampCounts[this.oneDayAgo()] = 0; + // add now, 0 to timestampCounts + timestampCounts[this.now()] = 0; + this.messages.forEach(message => { + const publishTime = new Date(message.pubtime); + const roundedTime = this.roundToNearestMinute(publishTime); + if (timestampCounts[roundedTime]) { + timestampCounts[roundedTime]++; + } else { + timestampCounts[roundedTime] = 1; } }); - console.log("Timestamp counts: ", this.timestampCounts) - }, - // Builds the wsiCounts object - async getWsiCounts() { - - // Reset the wsi counts - this.wsiCounts = {}; - - // Check if TEST_MODE is set in .env file or if VITE_API_URL is not set - if (import.meta.env.VITE_TEST_MODE === "true" || import.meta.env.VITE_API_URL == undefined) { - // Use example data selected by user - if (this.topicHierarchy == "rou/rnimh/data/core/weather/surface-based-observations/synop") { - this.messages = this.testMessageSynoptic; - } - else if (this.topicHierarchy == "mwi/mwi_met_centre/data/core/weather/surface-based-observations/synop") { - this.messages = this.testMessageSurface; - } - } - else { - await this.apiCall(); - } - // Group the messages based on 'wigos_station_identifier' add canonical_url property to each item - this.messages.forEach((item) => { - const canonicalLink = item.links.find(link => link.rel === "canonical"); - if (canonicalLink) { - item.canonical_url = canonicalLink.href; - } - const identifier = item.properties.wigos_station_identifier; - if (this.wsiCounts[identifier]) { - this.wsiCounts[identifier]++; - } - else { - this.wsiCounts[identifier] = 1 - } - }) + // Convert the timestampCounts object to an array of arrays + const chartData = Object.keys(timestampCounts).map(key => { + return [new Date(key).getTime(), timestampCounts[key]]; + }); - console.log("WSI counts: ", this.wsiCounts); - }, - // Initialise the data for ApexCharts series bar graph based on - // allNotifications - initChartData() { - // Creates a nested array structure of form [[timestamp1, count1],...] - const chartData = Object.entries(this.timestampCounts); + // sort chartData by time + chartData.sort((a, b) => a[0] - b[0]); + console.log("Chart data: ", chartData); - // Update the chartSeries data with the above - this.chartSeries[0].data = chartData; - }, - // Enables zoom functionality on bar graph - updateData: function (timeline) { - this.selectedZoom = timeline; - // Get current time const now = new Date().getTime(); + const twentyFourHoursAgo = now - (24 * 60 * this.mins); + this.chartOptions.xaxis.min = twentyFourHoursAgo; + this.chartOptions.xaxis.max = now; - // Depending on the button pressed, zoom the x-axis of the bar graph accordingly - switch (timeline) { - case 'one_hour': - this.$refs.chart.zoomX(now - (60 * this.mins), now); - break; - case 'three_hours': - this.$refs.chart.zoomX(now - (3 * 60 * this.mins), now); - break; - case 'six_hours': - this.$refs.chart.zoomX(now - (6 * 60 * this.mins), now); - break; - case 'twenty_four_hours': - this.$refs.chart.zoomX(now - (24 * 60 * this.mins), now); - break; - default: - } + this.chartSeries[0].data = chartData; }, - // Shows the HH:mm timestamp for the newest notifications + // Shows the time in a human readable format formatTime(timestamp) { - const hours = String(timestamp.getHours()).padStart(2, '0'); - const minutes = String(timestamp.getMinutes()).padStart(2, '0'); - return `${hours}:${minutes}`; + const year = timestamp.getUTCFullYear(); + const month = String(timestamp.getUTCMonth() + 1).padStart(2, '0'); + const day = String(timestamp.getUTCDate()).padStart(2, '0'); + const hours = String(timestamp.getUTCHours()).padStart(2, '0'); + const minutes = String(timestamp.getUTCMinutes()).padStart(2, '0'); + return `${year}/${month}/${day} ${hours}:${minutes} UTC`; }, // Gets the filename from the canonical href getFileName(url) { const urlParts = url.split('/'); return urlParts[urlParts.length - 1]; }, + getCanonicalUrl(links) { + const canonicalLink = links.find(link => link.rel === "canonical"); + if (canonicalLink) { + return canonicalLink.href; + } + return ''; + } }, mounted() { - this.apiCall(); - // Get notification data - this.getNotifications(); - // Get timestamp count data - this.getTimestampCounts(); - // Get WSI file count data - this.getWsiCounts(); - // Get summary statistics - this.getSummary(); - // Fill ApexCharts with latest data - this.initChartData(); + console.log("Mounted NotificationDashboard"); + this.update_messages(); } }); diff --git a/webapp/src/components/SynopForm.vue b/webapp/src/components/SynopForm.vue index af22b50..ab8ea2f 100644 --- a/webapp/src/components/SynopForm.vue +++ b/webapp/src/components/SynopForm.vue @@ -3,7 +3,14 @@ - Submit FM 12–XIV Ext. SYNOP Bulletin + + + +
+ Submit FM 12–XIV Ext. SYNOP Bulletin +
@@ -16,34 +23,48 @@

Month and year of the data

- + -

AAXX must be present for a valid SYNOP message +

AAXX must be present for a valid SYNOP + message

Delimiter (=) must be present for a valid SYNOP message

Raw FM 12 bulletin

- + - - + + + + + + +
+ +
- Submit - +
@@ -116,7 +137,7 @@ - - Output BUFR files: {{ result.files.length }} + Output BUFR files: {{ result.data_items.length }} - -
-
- {{ getFileName(file_url) }} -
+ + + - + +
+
+
+ {{ data_item.filename }} +
+
+ + +
+
+ + +
+
+
+
@@ -163,24 +207,6 @@ import DownloadButton from '@/components/DownloadButton.vue'; export default defineComponent({ name: 'RoleForm', - props: { - broker: { - type: String, - default: '' - }, - channel: { - type: String, - default: '' - }, - api: { - type: String, - default: 'api.opencdms.org' - }, - path: { - type: String, - default: '/processes/wis2box-synop-process/execution' - } - }, data() { // Default data values before reactivity return { @@ -193,9 +219,10 @@ export default defineComponent({ bulletin: "", // FM 12 data aaxxPresent: true, // Boolean to check if AAXX is in bulletin equalsPresent: true, // Boolean to check if = delimiter is in bulletin - hierarchyList: ["test1", "test2", "test3"], // List of topic hierarchies + topicList: ["test1", "test2", "test3"], // List of topic hierarchies // before they are obtained from discovery metadata - hierarchy: "", // Topic hierarchy selected by user + topic: "", // Topic hierarchy selected by user + token: "", // Execution token to be entered by user notificationsOnPending: true, // Realtime variable for if user has // selected notifications or not notificationsOn: true, // Variable that updates to the pending variable @@ -234,17 +261,15 @@ export default defineComponent({ this.equalsPresent = this.bulletin.includes('='); }, // Allows us to get the current topic hierarchies available - async fetchHierarchy() { + async fetchTopics() { const apiUrl = `${import.meta.env.VITE_API_URL}/collections/discovery-metadata/items?f=json`; - // check if TEST=True is set in .env file - console.log(import.meta.env); // check if TEST_MODE is set in .env file or if VITE_API_URL is not set if (import.meta.env.VITE_TEST_MODE === "true" || import.meta.env.VITE_API_URL == undefined) { console.log("TEST_MODE is enabled"); - this.hierachyList = ["test1", "test2", "test3"]; + this.topicList = ["test1", "test2", "test3"]; } else { - console.log("Fetching topic hierarchy from:", apiUrl); + //console.log("Fetching topic hierarchy from:", apiUrl); try { const response = await fetch(apiUrl); if (!response.ok) { @@ -252,14 +277,15 @@ export default defineComponent({ } else { const data = await response.json(); + // If the features object is in the data if (data.features) { // Use Array.map to create a new array of the topic hierarchies - this.hierarchyList = data.features.map(feature => { + this.topicList = data.features.map(feature => { if (feature.properties && feature.properties['wmo:topicHierarchy']) { return feature.properties['wmo:topicHierarchy'] } }); - console.log(this.hierarchyList) + console.log(this.topicList) } else { console.error("API response is not an object"); @@ -285,9 +311,15 @@ export default defineComponent({ "result": "Success", "messages transformed": 2, "messages published": 2, - "files": [ - "http://3.73.37.35/data/2023-12-17/wis/synop/test/WIGOS_0-20000-0-15015_20231217T120000.bufr4", - "http://3.73.37.35/data/2023-12-17/wis/synop/test/WIGOS_0-20000-0-15020_20231217T120000.bufr4" + "data_items": [ + { + "file_url": "http://3.127.235.197/data/2023-01-19/wis/synop/test/WIGOS_0-20000-0-64400_20230119T060000.bufr4", + "filename": "WIGOS_0-20000-0-64400_20230119T060000.bufr4" + }, + { + "data": "QlVGUgABgAQAABYAAAAAAAAAAAJuHgAH5wETBgAAAAALAAABgMGWx2AAAVMABOIAAANjQ0MDAAAAAAAAAAAAAAAIDIGxoaGBgAAAAAAAAAAAAAAAAAAAAPzimYBA/78kmTlBBU//////////////////////////////+dUnxn1P///////////26vbYOl////////////////////////////////////////////////////////////////AR////gJH///+T/x/+R/yf////////////7///v9f/////////////////////////////////+J/b/gAff2/4Dz/X/////////////////////////////////////7+kAH//v6QANnH////////////9+j//////////////v0f//////f//+/R/+////////////////////fo//////////////////3+oAP///////////////////8A3Nzc3", + "filename": "WIGOS_0-20000-0-64400_20230119T060000.bufr4" + } ], "warnings": [], "errors": [] @@ -300,10 +332,17 @@ export default defineComponent({ testPartialSuccessResult() { const testData = { "result": "Partial Success", - "messages transformed": 1, + "messages transformed": 2, "messages published": 1, - "files": [ - "http://3.73.37.35/data/2023-12-17/wis/synop/test/WIGOS_0-20000-0-15015_20231217T120000.bufr4" + "data_items": [ + { + "file_url": "http://3.127.235.197/data/2023-01-19/wis/synop/test/WIGOS_0-20000-0-64400_20230119T060000.bufr4", + "filename": "WIGOS_0-20000-0-64400_20230119T060000.bufr4" + }, + { + "data": "QlVGUgABgAQAABYAAAAAAAAAAAJuHgAH5wETBgAAAAALAAABgMGWx2AAAVMABOIAAANjQ0MDAAAAAAAAAAAAAAAIDIGxoaGBgAAAAAAAAAAAAAAAAAAAAPzimYBA/78kmTlBBU//////////////////////////////+dUnxn1P///////////26vbYOl////////////////////////////////////////////////////////////////AR////gJH///+T/x/+R/yf////////////7///v9f/////////////////////////////////+J/b/gAff2/4Dz/X/////////////////////////////////////7+kAH//v6QANnH////////////9+j//////////////v0f//////f//+/R/+////////////////////fo//////////////////3+oAP///////////////////8A3Nzc3", + "filename": "WIGOS_0-20000-0-64400_20230119T060000.bufr4" + } ], "warnings": [ "Missing station height for station 15090", @@ -320,7 +359,7 @@ export default defineComponent({ "result": "Failure", "messages transformed": 0, "messages published": 0, - "files": [], + "data_items": [], "warnings": [], "errors": [ "Error converting to BUFR: local variable 'messages' referenced before assignment", @@ -338,30 +377,38 @@ export default defineComponent({ year: this.date.year, // Year of data month: this.date.month + 1, // Month of data, +1 as JS starts // from 0 for months - channel: this.hierarchy, // Topic hierarchy + channel: this.topic, // Topic hierarchy notify: this.notificationsOn // Boolean for WIS2 notifications } }; - const synopUrl = `${import.meta.env.VITE_API_URL}/processes/wis2box-synop-process/execution` + const synopUrl = `${import.meta.env.VITE_API_URL}/processes/wis2box-synop2bufr/execution` - console.log(payload); - console.log(synopUrl); + //console.log(payload); + //console.log(synopUrl); this.input = payload; const response = await fetch(synopUrl, { method: 'POST', headers: { 'encode': 'json', - 'Content-Type': 'application/geo+json' + 'Content-Type': 'application/geo+json', + 'authorization': 'Bearer ' + this.token }, body: JSON.stringify(payload) }); if (!response.ok) { + let result; + if(response.status == 401) { + result = "Unauthorized, please provide a valid execution token" + } + else { + result = "API error" + } console.error('HTTP error', response.status); this.result = { - "result": "API error", + "result": result, "errors": [ synopUrl + " returned " + response.status ] @@ -369,8 +416,8 @@ export default defineComponent({ } else { const data = await response.json(); this.result = data; - console.log("Result:"); // TODO: Remove this line - console.log(this.result); // TODO: Remove this line + //console.log("Result:"); // TODO: Remove this line + //console.log(this.result); // TODO: Remove this line } }, // Method for when the user presses the submit button, including @@ -401,11 +448,6 @@ export default defineComponent({ // End loading animation this.loading = false; - }, - // Get filename from output BUFR files so it can be displayed on screen - getFileName(url) { - const urlParts = url.split('/'); - return urlParts[urlParts.length - 1]; } }, watch: { @@ -441,7 +483,7 @@ export default defineComponent({ DownloadButton }, mounted() { - this.fetchHierarchy(); + this.fetchTopics(); } }); @@ -452,6 +494,11 @@ export default defineComponent({ font-weight: 700; } +.small-title { + font-size: 1.1rem; + font-weight: 700; +} + .calendar-box { width: 250px; } @@ -516,5 +563,4 @@ export default defineComponent({ .divider-spacing { margin-top: 10px; -} - \ No newline at end of file +} \ No newline at end of file diff --git a/webapp/src/components/TopicSelector.vue b/webapp/src/components/TopicSelector.vue new file mode 100644 index 0000000..f8b07f9 --- /dev/null +++ b/webapp/src/components/TopicSelector.vue @@ -0,0 +1,8 @@ + diff --git a/webapp/src/layouts/default/AppBar.vue b/webapp/src/layouts/default/AppBar.vue index 572a4b5..1e997b9 100644 --- a/webapp/src/layouts/default/AppBar.vue +++ b/webapp/src/layouts/default/AppBar.vue @@ -1,28 +1,76 @@ diff --git a/webapp/src/router/index.js b/webapp/src/router/index.js index 3f175ba..34735d4 100644 --- a/webapp/src/router/index.js +++ b/webapp/src/router/index.js @@ -10,6 +10,12 @@ const routes = [ name: 'Home', component: () => import('@/views/Home.vue'), }, + { + path: 'csv2bufr_form', + name: 'CsvToBUFRForm', + component: () => import('@/views/CsvToBUFRForm.vue'), + meta: {title: 'wis2box - CSV to BUFR Form'} + }, { path: 'synop_form', name: 'SynopForm', diff --git a/webapp/src/views/CsvToBUFRForm.vue b/webapp/src/views/CsvToBUFRForm.vue new file mode 100644 index 0000000..1f2a7d6 --- /dev/null +++ b/webapp/src/views/CsvToBUFRForm.vue @@ -0,0 +1,7 @@ + + + diff --git a/webapp/src/views/Home.vue b/webapp/src/views/Home.vue index f685572..7434f64 100644 --- a/webapp/src/views/Home.vue +++ b/webapp/src/views/Home.vue @@ -1,6 +1,7 @@ \ No newline at end of file diff --git a/webapp/src/views/SynopForm.vue b/webapp/src/views/SynopForm.vue index 732f64f..c082d91 100644 --- a/webapp/src/views/SynopForm.vue +++ b/webapp/src/views/SynopForm.vue @@ -1,5 +1,5 @@