diff --git a/.env.production b/.env.production
index 5950b945..996d6bee 100644
--- a/.env.production
+++ b/.env.production
@@ -1,7 +1,20 @@
+# Application
VITE_ALERT_EDITOR=false
VITE_FORMULA_EDITOR=true
VITE_INSTRUMENT_CHART=true
VITE_CROSS_SECTION=true
VITE_DEVELOPMENT_BANNER=false
+VITE_REPORT_DOWNLOAD=false
+
+# API
VITE_API_URL=https://midas.sec.usace.army.mil/api
VITE_URL_BASE_PATH=/midas
+
+
+# EXTERNAL APIS
+VITE_CWMS_API_URL=https://cwms-data.usace.army.mil/cwms-data
+
+# Keycloak
+VITE_KC_URL=https://identity.sec.usace.army.mil/auth/
+VITE_KC_REALM=cwbi
+VITE_KC_CLIENT_ID=midas
diff --git a/.env.test b/.env.test
index 0783a221..6ed6f5c6 100644
--- a/.env.test
+++ b/.env.test
@@ -1,7 +1,19 @@
+# Application
VITE_ALERT_EDITOR=false
VITE_FORMULA_EDITOR=true
VITE_INSTRUMENT_CHART=true
VITE_CROSS_SECTION=true
VITE_DEVELOPMENT_BANNER=false
+VITE_REPORT_DOWNLOAD=false
+
+# API
VITE_API_URL=https://midas-test.cwbi.us/api
VITE_URL_BASE_PATH=/midas
+
+# EXTERNAL APIS
+VITE_CWMS_API_URL=https://cwms-data.usace.army.mil/cwms-data
+
+# Keycloak
+VITE_KC_URL=https://identity-test.cwbi.us/auth/
+VITE_KC_REALM=cwbi
+VITE_KC_CLIENT_ID=midas
diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml
index 595b3ceb..5ca8b4fb 100644
--- a/.github/workflows/develop.yml
+++ b/.github/workflows/develop.yml
@@ -17,8 +17,14 @@ jobs:
VITE_INSTRUMENT_CHART: true
VITE_CROSS_SECTION: true
VITE_DEVELOPMENT_BANNER: true
+ VITE_REPORT_DOWNLOAD: true
VITE_API_URL: https://develop-midas-api.rsgis.dev
+ VITE_CWMS_API_URL: https://cwms-data.usace.army.mil/cwms-data
VITE_URL_BASE_PATH: ''
+ VITE_KC_URL: https://identity-test.cwbi.us/auth/
+ VITE_KC_REALM: cwbi
+ VITE_KC_CLIENT_ID: midas
+ VITE_BASE_REDIRECT_URI: https://develop-midas.rsgis.dev/
runs-on: ubuntu-latest
strategy:
matrix:
diff --git a/package-lock.json b/package-lock.json
index 1046b8f9..cc567a4b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "hhd-ui",
- "version": "0.15.4",
+ "version": "0.17.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "hhd-ui",
- "version": "0.15.4",
+ "version": "0.17.0",
"dependencies": {
"@ag-grid-community/client-side-row-model": "^30.0.3",
"@ag-grid-community/core": "^30.0.3",
@@ -19,6 +19,7 @@
"@iconify/utils": "^2.1.22",
"@mui/icons-material": "^5.14.0",
"@mui/material": "^5.14.2",
+ "@tanstack/react-query": "^5.36.2",
"@tanstack/react-table": "^8.9.3",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
@@ -28,15 +29,18 @@
"date-fns": "^2.30.0",
"graceful-fs": "^4.2.11",
"internal-nav-helper": "^3.1.0",
+ "keycloak-js": "^25.0.2",
"lodash.debounce": "^4.0.8",
"lodash.isequal": "^4.5.0",
"luxon": "^3.3.0",
"money-clip": "^3.0.5",
+ "mui-color-input": "^2.0.3",
"ol": "^7.4.0",
"plotly.js": "^2.25.2",
"process": "^0.11.10",
"proj4": "^2.9.0",
"react": "^18.2.0",
+ "react-beautiful-dnd": "^13.1.1",
"react-color": "^2.19.3",
"react-csv": "^2.2.2",
"react-datepicker": "^4.16.0",
@@ -72,6 +76,7 @@
"rollup-plugin-visualizer": "^5.9.2",
"sass": "^1.63.6",
"source-map-explorer": "^2.5.3",
+ "typescript": "^5.4.5",
"vite": "^4.5.2",
"vite-plugin-checker": "^0.6.1",
"vite-plugin-svgr": "^3.2.0"
@@ -724,11 +729,11 @@
}
},
"node_modules/@babel/runtime": {
- "version": "7.22.6",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.6.tgz",
- "integrity": "sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==",
+ "version": "7.24.4",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.4.tgz",
+ "integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==",
"dependencies": {
- "regenerator-runtime": "^0.13.11"
+ "regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
@@ -792,6 +797,14 @@
"findup": "bin/findup.js"
}
},
+ "node_modules/@ctrl/tinycolor": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.1.0.tgz",
+ "integrity": "sha512-WyOx8cJQ+FQus4Mm4uPIZA64gbk3Wxh0so5Lcii0aJifqwoVOlfFtorjLE0Hen4OYyHZMXDWqMmaQemBhgxFRQ==",
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/@emotion/babel-plugin": {
"version": "11.11.0",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz",
@@ -933,70 +946,6 @@
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz",
"integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww=="
},
- "node_modules/@esbuild/android-arm": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.12.tgz",
- "integrity": "sha512-LIxaNIQfkFZbTLb4+cX7dozHlAbAshhFE5PKdro0l+FnCpx1GDJaQ2WMcqm+ToXKMt8p8Uojk/MFRuGyz3V5Sw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/android-arm64": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.12.tgz",
- "integrity": "sha512-BMAlczRqC/LUt2P97E4apTBbkvS9JTJnp2DKFbCwpZ8vBvXVbNdqmvzW/OsdtI/+mGr+apkkpqGM8WecLkPgrA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/android-x64": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.12.tgz",
- "integrity": "sha512-zU5MyluNsykf5cOJ0LZZZjgAHbhPJ1cWfdH1ZXVMXxVMhEV0VZiZXQdwBBVvmvbF28EizeK7obG9fs+fpmS0eQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/darwin-arm64": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.12.tgz",
- "integrity": "sha512-zUZMep7YONnp6954QOOwEBwFX9svlKd3ov6PkxKd53LGTHsp/gy7vHaPGhhjBmEpqXEXShi6dddjIkmd+NgMsA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=12"
- }
- },
"node_modules/@esbuild/darwin-x64": {
"version": "0.18.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.12.tgz",
@@ -1013,278 +962,6 @@
"node": ">=12"
}
},
- "node_modules/@esbuild/freebsd-arm64": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.12.tgz",
- "integrity": "sha512-GIIHtQXqgeOOqdG16a/A9N28GpkvjJnjYMhOnXVbn3EDJcoItdR58v/pGN31CHjyXDc8uCcRnFWmqaJt24AYJg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/freebsd-x64": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.12.tgz",
- "integrity": "sha512-zK0b9a1/0wZY+6FdOS3BpZcPc1kcx2G5yxxfEJtEUzVxI6n/FrC2Phsxj/YblPuBchhBZ/1wwn7AyEBUyNSa6g==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-arm": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.12.tgz",
- "integrity": "sha512-y75OijvrBE/1XRrXq1jtrJfG26eHeMoqLJ2dwQNwviwTuTtHGCojsDO6BJNF8gU+3jTn1KzJEMETytwsFSvc+Q==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-arm64": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.12.tgz",
- "integrity": "sha512-JKgG8Q/LL/9sw/iHHxQyVMoQYu3rU3+a5Z87DxC+wAu3engz+EmctIrV+FGOgI6gWG1z1+5nDDbXiRMGQZXqiw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-ia32": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.12.tgz",
- "integrity": "sha512-yoRIAqc0B4lDIAAEFEIu9ttTRFV84iuAl0KNCN6MhKLxNPfzwCBvEMgwco2f71GxmpBcTtn7KdErueZaM2rEvw==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-loong64": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.12.tgz",
- "integrity": "sha512-qYgt3dHPVvf/MgbIBpJ4Sup/yb9DAopZ3a2JgMpNKIHUpOdnJ2eHBo/aQdnd8dJ21X/+sS58wxHtA9lEazYtXQ==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-mips64el": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.12.tgz",
- "integrity": "sha512-wHphlMLK4ufNOONqukELfVIbnGQJrHJ/mxZMMrP2jYrPgCRZhOtf0kC4yAXBwnfmULimV1qt5UJJOw4Kh13Yfg==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-ppc64": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.12.tgz",
- "integrity": "sha512-TeN//1Ft20ZZW41+zDSdOI/Os1bEq5dbvBvYkberB7PHABbRcsteeoNVZFlI0YLpGdlBqohEpjrn06kv8heCJg==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-riscv64": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.12.tgz",
- "integrity": "sha512-AgUebVS4DoAblBgiB2ACQ/8l4eGE5aWBb8ZXtkXHiET9mbj7GuWt3OnsIW/zX+XHJt2RYJZctbQ2S/mDjbp0UA==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-s390x": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.12.tgz",
- "integrity": "sha512-dJ3Rb3Ei2u/ysSXd6pzleGtfDdc2MuzKt8qc6ls8vreP1G3B7HInX3i7gXS4BGeVd24pp0yqyS7bJ5NHaI9ing==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-x64": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.12.tgz",
- "integrity": "sha512-OrNJMGQbPaVyHHcDF8ybNSwu7TDOfX8NGpXCbetwOSP6txOJiWlgQnRymfC9ocR1S0Y5PW0Wb1mV6pUddqmvmQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/netbsd-x64": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.12.tgz",
- "integrity": "sha512-55FzVCAiwE9FK8wWeCRuvjazNRJ1QqLCYGZVB6E8RuQuTeStSwotpSW4xoRGwp3a1wUsaVCdYcj5LGCASVJmMg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/openbsd-x64": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.12.tgz",
- "integrity": "sha512-qnluf8rfb6Y5Lw2tirfK2quZOBbVqmwxut7GPCIJsM8lc4AEUj9L8y0YPdLaPK0TECt4IdyBdBD/KRFKorlK3g==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/sunos-x64": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.12.tgz",
- "integrity": "sha512-+RkKpVQR7bICjTOPUpkTBTaJ4TFqQBX5Ywyd/HSdDkQGn65VPkTsR/pL4AMvuMWy+wnXgIl4EY6q4mVpJal8Kg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/win32-arm64": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.12.tgz",
- "integrity": "sha512-GNHuciv0mFM7ouzsU0+AwY+7eV4Mgo5WnbhfDCQGtpvOtD1vbOiRjPYG6dhmMoFyBjj+pNqQu2X+7DKn0KQ/Gw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/win32-ia32": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.12.tgz",
- "integrity": "sha512-kR8cezhYipbbypGkaqCTWIeu4zID17gamC8YTPXYtcN3E5BhhtTnwKBn9I0PJur/T6UVwIEGYzkffNL0lFvxEw==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/win32-x64": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.12.tgz",
- "integrity": "sha512-O0UYQVkvfM/jO8a4OwoV0mAKSJw+mjWTAd1MJd/1FCX6uiMdLmMRPK/w6e9OQ0ob2WGxzIm9va/KG0Ja4zIOgg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@@ -1373,12 +1050,31 @@
"license": "MIT"
},
"node_modules/@floating-ui/dom": {
- "version": "1.0.4",
- "license": "MIT",
+ "version": "1.6.3",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.3.tgz",
+ "integrity": "sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==",
"dependencies": {
- "@floating-ui/core": "^1.0.1"
+ "@floating-ui/core": "^1.0.0",
+ "@floating-ui/utils": "^0.2.0"
}
},
+ "node_modules/@floating-ui/react-dom": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.8.tgz",
+ "integrity": "sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==",
+ "dependencies": {
+ "@floating-ui/dom": "^1.6.1"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz",
+ "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q=="
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
@@ -1624,25 +1320,24 @@
}
},
"node_modules/@mui/base": {
- "version": "5.0.0-beta.8",
- "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.8.tgz",
- "integrity": "sha512-b4vVjMZx5KzzEMf4arXKoeV5ZegAMOoPwoy1vfUBwhvXc2QtaaAyBp50U7OA2L06Leubc1A+lEp3eqwZoFn87g==",
- "dependencies": {
- "@babel/runtime": "^7.22.6",
- "@emotion/is-prop-valid": "^1.2.1",
- "@mui/types": "^7.2.4",
- "@mui/utils": "^5.14.1",
+ "version": "5.0.0-beta.40",
+ "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz",
+ "integrity": "sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==",
+ "dependencies": {
+ "@babel/runtime": "^7.23.9",
+ "@floating-ui/react-dom": "^2.0.8",
+ "@mui/types": "^7.2.14",
+ "@mui/utils": "^5.15.14",
"@popperjs/core": "^2.11.8",
- "clsx": "^1.2.1",
- "prop-types": "^15.8.1",
- "react-is": "^18.2.0"
+ "clsx": "^2.1.0",
+ "prop-types": "^15.8.1"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
- "url": "https://opencollective.com/mui"
+ "url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0",
@@ -1655,18 +1350,21 @@
}
}
},
- "node_modules/@mui/base/node_modules/react-is": {
- "version": "18.2.0",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
- "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
+ "node_modules/@mui/base/node_modules/clsx": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz",
+ "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==",
+ "engines": {
+ "node": ">=6"
+ }
},
"node_modules/@mui/core-downloads-tracker": {
- "version": "5.14.2",
- "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.14.2.tgz",
- "integrity": "sha512-x+c/MgDL1t/IIy5lDbMlrDouFG5DYZbl3DP4dbbuhlpPFBnE9glYwmJEee/orVHQpOPwLxCAIWQs+2DKSaBVWQ==",
+ "version": "5.15.14",
+ "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.14.tgz",
+ "integrity": "sha512-on75VMd0XqZfaQW+9pGjSNiqW+ghc5E2ZSLRBXwcXl/C4YzjfyjrLPhrEpKnR9Uym9KXBvxrhoHfPcczYHweyA==",
"funding": {
"type": "opencollective",
- "url": "https://opencollective.com/mui"
+ "url": "https://opencollective.com/mui-org"
}
},
"node_modules/@mui/icons-material": {
@@ -1695,19 +1393,19 @@
}
},
"node_modules/@mui/material": {
- "version": "5.14.2",
- "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.14.2.tgz",
- "integrity": "sha512-TgNR4/YRL11RifsnMWNhITNCkGJYVz20SCvVJBBoU5Y/KhUNSSJxjDpEB8VrnY+sUsV0NigLCkHZJglfsiS3Pw==",
- "dependencies": {
- "@babel/runtime": "^7.22.6",
- "@mui/base": "5.0.0-beta.8",
- "@mui/core-downloads-tracker": "^5.14.2",
- "@mui/system": "^5.14.1",
- "@mui/types": "^7.2.4",
- "@mui/utils": "^5.14.1",
- "@types/react-transition-group": "^4.4.6",
- "clsx": "^1.2.1",
- "csstype": "^3.1.2",
+ "version": "5.15.14",
+ "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.14.tgz",
+ "integrity": "sha512-kEbRw6fASdQ1SQ7LVdWR5OlWV3y7Y54ZxkLzd6LV5tmz+NpO3MJKZXSfgR0LHMP7meKsPiMm4AuzV0pXDpk/BQ==",
+ "dependencies": {
+ "@babel/runtime": "^7.23.9",
+ "@mui/base": "5.0.0-beta.40",
+ "@mui/core-downloads-tracker": "^5.15.14",
+ "@mui/system": "^5.15.14",
+ "@mui/types": "^7.2.14",
+ "@mui/utils": "^5.15.14",
+ "@types/react-transition-group": "^4.4.10",
+ "clsx": "^2.1.0",
+ "csstype": "^3.1.3",
"prop-types": "^15.8.1",
"react-is": "^18.2.0",
"react-transition-group": "^4.4.5"
@@ -1717,7 +1415,7 @@
},
"funding": {
"type": "opencollective",
- "url": "https://opencollective.com/mui"
+ "url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
@@ -1738,18 +1436,26 @@
}
}
},
+ "node_modules/@mui/material/node_modules/clsx": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz",
+ "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/@mui/material/node_modules/react-is": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
},
"node_modules/@mui/private-theming": {
- "version": "5.13.7",
- "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.13.7.tgz",
- "integrity": "sha512-qbSr+udcij5F9dKhGX7fEdx2drXchq7htLNr2Qg2Ma+WJ6q0ERlEqGSBiPiVDJkptcjeVL4DGmcf1wl5+vD4EA==",
+ "version": "5.15.14",
+ "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.14.tgz",
+ "integrity": "sha512-UH0EiZckOWcxiXLX3Jbb0K7rC8mxTr9L9l6QhOZxYc4r8FHUkefltV9VDGLrzCaWh30SQiJvAEd7djX3XXY6Xw==",
"dependencies": {
- "@babel/runtime": "^7.22.5",
- "@mui/utils": "^5.13.7",
+ "@babel/runtime": "^7.23.9",
+ "@mui/utils": "^5.15.14",
"prop-types": "^15.8.1"
},
"engines": {
@@ -1757,7 +1463,7 @@
},
"funding": {
"type": "opencollective",
- "url": "https://opencollective.com/mui"
+ "url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0",
@@ -1770,13 +1476,13 @@
}
},
"node_modules/@mui/styled-engine": {
- "version": "5.13.2",
- "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.13.2.tgz",
- "integrity": "sha512-VCYCU6xVtXOrIN8lcbuPmoG+u7FYuOERG++fpY74hPpEWkyFQG97F+/XfTQVYzlR2m7nPjnwVUgATcTCMEaMvw==",
+ "version": "5.15.14",
+ "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.14.tgz",
+ "integrity": "sha512-RILkuVD8gY6PvjZjqnWhz8fu68dVkqhM5+jYWfB5yhlSQKg+2rHkmEwm75XIeAqI3qwOndK6zELK5H6Zxn4NHw==",
"dependencies": {
- "@babel/runtime": "^7.21.0",
+ "@babel/runtime": "^7.23.9",
"@emotion/cache": "^11.11.0",
- "csstype": "^3.1.2",
+ "csstype": "^3.1.3",
"prop-types": "^15.8.1"
},
"engines": {
@@ -1784,7 +1490,7 @@
},
"funding": {
"type": "opencollective",
- "url": "https://opencollective.com/mui"
+ "url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.4.1",
@@ -1801,17 +1507,17 @@
}
},
"node_modules/@mui/system": {
- "version": "5.14.1",
- "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.14.1.tgz",
- "integrity": "sha512-u+xlsU34Jdkgx1CxmBnIC4Y08uPdVX5iEd3S/1dggDFtOGp+Lj8xmKRJAQ8PJOOJLOh8pDwaZx4AwXikL4l1QA==",
- "dependencies": {
- "@babel/runtime": "^7.22.6",
- "@mui/private-theming": "^5.13.7",
- "@mui/styled-engine": "^5.13.2",
- "@mui/types": "^7.2.4",
- "@mui/utils": "^5.14.1",
- "clsx": "^1.2.1",
- "csstype": "^3.1.2",
+ "version": "5.15.14",
+ "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.14.tgz",
+ "integrity": "sha512-auXLXzUaCSSOLqJXmsAaq7P96VPRXg2Rrz6OHNV7lr+kB8lobUF+/N84Vd9C4G/wvCXYPs5TYuuGBRhcGbiBGg==",
+ "dependencies": {
+ "@babel/runtime": "^7.23.9",
+ "@mui/private-theming": "^5.15.14",
+ "@mui/styled-engine": "^5.15.14",
+ "@mui/types": "^7.2.14",
+ "@mui/utils": "^5.15.14",
+ "clsx": "^2.1.0",
+ "csstype": "^3.1.3",
"prop-types": "^15.8.1"
},
"engines": {
@@ -1819,7 +1525,7 @@
},
"funding": {
"type": "opencollective",
- "url": "https://opencollective.com/mui"
+ "url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
@@ -1839,12 +1545,20 @@
}
}
},
+ "node_modules/@mui/system/node_modules/clsx": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz",
+ "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/@mui/types": {
- "version": "7.2.4",
- "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.4.tgz",
- "integrity": "sha512-LBcwa8rN84bKF+f5sDyku42w1NTxaPgPyYKODsh01U1fVstTClbUoSA96oyRBnSNyEiAVjKm6Gwx9vjR+xyqHA==",
+ "version": "7.2.14",
+ "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.14.tgz",
+ "integrity": "sha512-MZsBZ4q4HfzBsywtXgM1Ksj6HDThtiwmOKUXH1pKYISI9gAVXCNHNpo7TlGoGrBaYWZTdNoirIN7JsQcQUjmQQ==",
"peerDependencies": {
- "@types/react": "*"
+ "@types/react": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -1853,13 +1567,12 @@
}
},
"node_modules/@mui/utils": {
- "version": "5.14.1",
- "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.14.1.tgz",
- "integrity": "sha512-39KHKK2JeqRmuUcLDLwM+c2XfVC136C5/yUyQXmO2PVbOb2Bol4KxtkssEqCbTwg87PSCG3f1Tb0keRsK7cVGw==",
+ "version": "5.15.14",
+ "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.14.tgz",
+ "integrity": "sha512-0lF/7Hh/ezDv5X7Pry6enMsbYyGKjADzvHyo3Qrc/SSlTsQ1VkbDMbH0m2t3OR5iIVLwMoxwM7yGd+6FCMtTFA==",
"dependencies": {
- "@babel/runtime": "^7.22.6",
- "@types/prop-types": "^15.7.5",
- "@types/react-is": "^18.2.1",
+ "@babel/runtime": "^7.23.9",
+ "@types/prop-types": "^15.7.11",
"prop-types": "^15.8.1",
"react-is": "^18.2.0"
},
@@ -1868,10 +1581,16 @@
},
"funding": {
"type": "opencollective",
- "url": "https://opencollective.com/mui"
+ "url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
+ "@types/react": "^17.0.0 || ^18.0.0",
"react": "^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
}
},
"node_modules/@mui/utils/node_modules/react-is": {
@@ -2047,6 +1766,30 @@
"version": "0.24.51",
"license": "MIT"
},
+ "node_modules/@tanstack/query-core": {
+ "version": "5.36.1",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.36.1.tgz",
+ "integrity": "sha512-BteWYEPUcucEu3NBcDAgKuI4U25R9aPrHSP6YSf2NvaD2pSlIQTdqOfLRsxH9WdRYg7k0Uom35Uacb6nvbIMJg==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/react-query": {
+ "version": "5.36.2",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.36.2.tgz",
+ "integrity": "sha512-bHNa+5dead+j6SA8WVlEOPxcGfteVFgdyFTCFcxBgjnPf0fFpHUc7aNZBCnvmPXqy/BeQa9zTuU9ectb7i8ZXA==",
+ "dependencies": {
+ "@tanstack/query-core": "5.36.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0"
+ }
+ },
"node_modules/@tanstack/react-table": {
"version": "8.9.3",
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.9.3.tgz",
@@ -2226,6 +1969,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/hoist-non-react-statics": {
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz",
+ "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==",
+ "dependencies": {
+ "@types/react": "*",
+ "hoist-non-react-statics": "^3.3.0"
+ }
+ },
"node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.4",
"license": "MIT"
@@ -2312,8 +2064,9 @@
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
},
"node_modules/@types/prop-types": {
- "version": "15.7.5",
- "license": "MIT"
+ "version": "15.7.12",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
+ "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q=="
},
"node_modules/@types/react": {
"version": "18.2.14",
@@ -2333,18 +2086,21 @@
"@types/react": "*"
}
},
- "node_modules/@types/react-is": {
- "version": "18.2.1",
- "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-18.2.1.tgz",
- "integrity": "sha512-wyUkmaaSZEzFZivD8F2ftSyAfk6L+DfFliVj/mYdOXbVjRcS87fQJLTnhk6dRZPuJjI+9g6RZJO4PNCngUrmyw==",
+ "node_modules/@types/react-redux": {
+ "version": "7.1.33",
+ "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.33.tgz",
+ "integrity": "sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==",
"dependencies": {
- "@types/react": "*"
+ "@types/hoist-non-react-statics": "^3.3.0",
+ "@types/react": "*",
+ "hoist-non-react-statics": "^3.3.0",
+ "redux": "^4.0.0"
}
},
"node_modules/@types/react-transition-group": {
- "version": "4.4.6",
- "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.6.tgz",
- "integrity": "sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==",
+ "version": "4.4.10",
+ "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz",
+ "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==",
"dependencies": {
"@types/react": "*"
}
@@ -2711,10 +2467,12 @@
}
},
"node_modules/braces": {
- "version": "3.0.2",
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"license": "MIT",
"dependencies": {
- "fill-range": "^7.0.1"
+ "fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
@@ -2834,9 +2592,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001576",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001576.tgz",
- "integrity": "sha512-ff5BdakGe2P3SQsMsiqmt1Lc8221NR1VzHj5jXN5vBny9A6fpze94HiVV/n7XRosOlsShJcvMv5mdnpjOGCEgg==",
+ "version": "1.0.30001636",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001636.tgz",
+ "integrity": "sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==",
"dev": true,
"funding": [
{
@@ -3090,6 +2848,14 @@
"node": ">= 8"
}
},
+ "node_modules/css-box-model": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz",
+ "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==",
+ "dependencies": {
+ "tiny-invariant": "^1.0.6"
+ }
+ },
"node_modules/css-font": {
"version": "1.2.0",
"license": "MIT",
@@ -3146,9 +2912,9 @@
"license": "MIT"
},
"node_modules/csstype": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
- "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"node_modules/d": {
"version": "1.0.1",
@@ -3569,7 +3335,9 @@
"license": "ISC"
},
"node_modules/ejs": {
- "version": "3.1.8",
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
+ "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -3727,12 +3495,15 @@
}
},
"node_modules/es5-ext": {
- "version": "0.10.62",
+ "version": "0.10.64",
+ "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz",
+ "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==",
"hasInstallScript": true,
"license": "ISC",
"dependencies": {
"es6-iterator": "^2.0.3",
"es6-symbol": "^3.1.3",
+ "esniff": "^2.0.1",
"next-tick": "^1.1.0"
},
"engines": {
@@ -4123,10 +3894,31 @@
"engines": {
"node": ">=10"
},
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/esniff": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz",
+ "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==",
+ "license": "ISC",
+ "dependencies": {
+ "d": "^1.0.1",
+ "es5-ext": "^0.10.62",
+ "event-emitter": "^0.3.5",
+ "type": "^2.7.2"
+ },
+ "engines": {
+ "node": ">=0.10"
}
},
+ "node_modules/esniff/node_modules/type": {
+ "version": "2.7.3",
+ "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz",
+ "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==",
+ "license": "ISC"
+ },
"node_modules/espree": {
"version": "9.6.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.0.tgz",
@@ -4193,6 +3985,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/event-emitter": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
+ "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
+ "license": "MIT",
+ "dependencies": {
+ "d": "1",
+ "es5-ext": "~0.10.14"
+ }
+ },
"node_modules/events": {
"version": "3.3.0",
"license": "MIT",
@@ -4321,9 +4123,10 @@
"license": "MIT"
},
"node_modules/fast-loops": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/fast-loops/-/fast-loops-1.1.3.tgz",
- "integrity": "sha512-8EZzEP0eKkEEVX+drtd9mtuQ+/QrlfW/5MlwcwK5Nds6EkZ/tRzEexkzUY2mIssnAyVLT+TKHuRXmFNNXYUd6g=="
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/fast-loops/-/fast-loops-1.1.4.tgz",
+ "integrity": "sha512-8dbd3XWoKCTms18ize6JmQF1SFnnfj5s0B7rRry22EofgMu7B6LKHVh+XfFqFGsqnbH54xgeO83PzpKI+ODhlg==",
+ "license": "MIT"
},
"node_modules/fast-shallow-equal": {
"version": "1.0.0",
@@ -4382,7 +4185,9 @@
}
},
"node_modules/fill-range": {
- "version": "7.0.1",
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
@@ -5329,6 +5134,8 @@
},
"node_modules/is-number": {
"version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"license": "MIT",
"engines": {
"node": ">=0.12.0"
@@ -5676,6 +5483,11 @@
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz",
"integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ=="
},
+ "node_modules/js-sha256": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.11.0.tgz",
+ "integrity": "sha512-6xNlKayMZvds9h1Y1VWc0fQHQ82BxTXizWPEtEeGvmOUYpBRy4gbWroHLpzowe6xiQhHpelCQiE7HEdznyBL9Q=="
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"license": "MIT"
@@ -5761,10 +5573,28 @@
"node": ">=4.0"
}
},
+ "node_modules/jwt-decode": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
+ "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/kdbush": {
"version": "3.0.0",
"license": "ISC"
},
+ "node_modules/keycloak-js": {
+ "version": "25.0.2",
+ "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-25.0.2.tgz",
+ "integrity": "sha512-ACLf5O5PqzfDJwGqvLpqM0kflYWmyl3+T7M2C23gztJYccDxdfNP54+B9OkXz2GnDpLUId0ceoA+lbHw9t4Wng==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "js-sha256": "^0.11.0",
+ "jwt-decode": "^4.0.0"
+ }
+ },
"node_modules/kolorist": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",
@@ -6064,6 +5894,27 @@
"version": "2.1.2",
"license": "MIT"
},
+ "node_modules/mui-color-input": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/mui-color-input/-/mui-color-input-2.0.3.tgz",
+ "integrity": "sha512-rAd040qQ0Y+8dk4gE8kkCiJ/vCgA0j4vv1quJ43BfORTFE3uHarHj0xY1Vo9CPbojtx1f5vW+CjckYPRIZPIRg==",
+ "dependencies": {
+ "@ctrl/tinycolor": "^4.0.3"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.5.0",
+ "@emotion/styled": "^11.3.0",
+ "@mui/material": "^5.0.0",
+ "@types/react": "^18.0.0",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/mumath": {
"version": "3.3.4",
"license": "Unlicense",
@@ -6814,6 +6665,11 @@
"performance-now": "^2.1.0"
}
},
+ "node_modules/raf-schd": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
+ "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ=="
+ },
"node_modules/rbush": {
"version": "3.0.1",
"license": "MIT",
@@ -6831,6 +6687,29 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-beautiful-dnd": {
+ "version": "13.1.1",
+ "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz",
+ "integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==",
+ "dependencies": {
+ "@babel/runtime": "^7.9.2",
+ "css-box-model": "^1.2.0",
+ "memoize-one": "^5.1.1",
+ "raf-schd": "^4.0.2",
+ "react-redux": "^7.2.0",
+ "redux": "^4.0.4",
+ "use-memo-one": "^1.1.1"
+ },
+ "peerDependencies": {
+ "react": "^16.8.5 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/react-beautiful-dnd/node_modules/memoize-one": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
+ "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
+ },
"node_modules/react-color": {
"version": "2.19.3",
"license": "MIT",
@@ -6935,6 +6814,30 @@
"react-dom": "^16.8.0 || ^17 || ^18"
}
},
+ "node_modules/react-redux": {
+ "version": "7.2.9",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz",
+ "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==",
+ "dependencies": {
+ "@babel/runtime": "^7.15.4",
+ "@types/react-redux": "^7.1.20",
+ "hoist-non-react-statics": "^3.3.2",
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.7.2",
+ "react-is": "^17.0.2"
+ },
+ "peerDependencies": {
+ "react": "^16.8.3 || ^17 || ^18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-resizable-panels": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.0.7.tgz",
@@ -7090,6 +6993,14 @@
"node": ">=8"
}
},
+ "node_modules/redux": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
+ "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
+ "dependencies": {
+ "@babel/runtime": "^7.9.2"
+ }
+ },
"node_modules/redux-bundler": {
"version": "28.0.3",
"license": "MIT"
@@ -7103,9 +7014,9 @@
}
},
"node_modules/regenerator-runtime": {
- "version": "0.13.11",
- "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
- "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
+ "version": "0.14.1",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
+ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"node_modules/regexp.prototype.flags": {
"version": "1.4.3",
@@ -8127,8 +8038,7 @@
"node_modules/tiny-invariant": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz",
- "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==",
- "dev": true
+ "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw=="
},
"node_modules/tinycolor2": {
"version": "1.4.2",
@@ -8173,6 +8083,8 @@
},
"node_modules/to-regex-range": {
"version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
@@ -8278,17 +8190,16 @@
}
},
"node_modules/typescript": {
- "version": "4.8.4",
+ "version": "5.4.5",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
+ "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
"dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
- "node": ">=4.2.0"
+ "node": ">=14.17"
}
},
"node_modules/ufo": {
@@ -8399,6 +8310,14 @@
}
}
},
+ "node_modules/use-memo-one": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz",
+ "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/util": {
"version": "0.12.5",
"license": "MIT",
@@ -8415,10 +8334,11 @@
"license": "MIT"
},
"node_modules/vite": {
- "version": "4.5.2",
- "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz",
- "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==",
+ "version": "4.5.3",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz",
+ "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"esbuild": "^0.18.10",
"postcss": "^8.4.27",
@@ -9611,11 +9531,11 @@
}
},
"@babel/runtime": {
- "version": "7.22.6",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.6.tgz",
- "integrity": "sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==",
+ "version": "7.24.4",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.4.tgz",
+ "integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==",
"requires": {
- "regenerator-runtime": "^0.13.11"
+ "regenerator-runtime": "^0.14.0"
}
},
"@babel/template": {
@@ -9663,6 +9583,11 @@
"commander": "^2.15.1"
}
},
+ "@ctrl/tinycolor": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.1.0.tgz",
+ "integrity": "sha512-WyOx8cJQ+FQus4Mm4uPIZA64gbk3Wxh0so5Lcii0aJifqwoVOlfFtorjLE0Hen4OYyHZMXDWqMmaQemBhgxFRQ=="
+ },
"@emotion/babel-plugin": {
"version": "11.11.0",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz",
@@ -9784,34 +9709,6 @@
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz",
"integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww=="
},
- "@esbuild/android-arm": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.12.tgz",
- "integrity": "sha512-LIxaNIQfkFZbTLb4+cX7dozHlAbAshhFE5PKdro0l+FnCpx1GDJaQ2WMcqm+ToXKMt8p8Uojk/MFRuGyz3V5Sw==",
- "dev": true,
- "optional": true
- },
- "@esbuild/android-arm64": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.12.tgz",
- "integrity": "sha512-BMAlczRqC/LUt2P97E4apTBbkvS9JTJnp2DKFbCwpZ8vBvXVbNdqmvzW/OsdtI/+mGr+apkkpqGM8WecLkPgrA==",
- "dev": true,
- "optional": true
- },
- "@esbuild/android-x64": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.12.tgz",
- "integrity": "sha512-zU5MyluNsykf5cOJ0LZZZjgAHbhPJ1cWfdH1ZXVMXxVMhEV0VZiZXQdwBBVvmvbF28EizeK7obG9fs+fpmS0eQ==",
- "dev": true,
- "optional": true
- },
- "@esbuild/darwin-arm64": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.12.tgz",
- "integrity": "sha512-zUZMep7YONnp6954QOOwEBwFX9svlKd3ov6PkxKd53LGTHsp/gy7vHaPGhhjBmEpqXEXShi6dddjIkmd+NgMsA==",
- "dev": true,
- "optional": true
- },
"@esbuild/darwin-x64": {
"version": "0.18.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.12.tgz",
@@ -9819,125 +9716,6 @@
"dev": true,
"optional": true
},
- "@esbuild/freebsd-arm64": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.12.tgz",
- "integrity": "sha512-GIIHtQXqgeOOqdG16a/A9N28GpkvjJnjYMhOnXVbn3EDJcoItdR58v/pGN31CHjyXDc8uCcRnFWmqaJt24AYJg==",
- "dev": true,
- "optional": true
- },
- "@esbuild/freebsd-x64": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.12.tgz",
- "integrity": "sha512-zK0b9a1/0wZY+6FdOS3BpZcPc1kcx2G5yxxfEJtEUzVxI6n/FrC2Phsxj/YblPuBchhBZ/1wwn7AyEBUyNSa6g==",
- "dev": true,
- "optional": true
- },
- "@esbuild/linux-arm": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.12.tgz",
- "integrity": "sha512-y75OijvrBE/1XRrXq1jtrJfG26eHeMoqLJ2dwQNwviwTuTtHGCojsDO6BJNF8gU+3jTn1KzJEMETytwsFSvc+Q==",
- "dev": true,
- "optional": true
- },
- "@esbuild/linux-arm64": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.12.tgz",
- "integrity": "sha512-JKgG8Q/LL/9sw/iHHxQyVMoQYu3rU3+a5Z87DxC+wAu3engz+EmctIrV+FGOgI6gWG1z1+5nDDbXiRMGQZXqiw==",
- "dev": true,
- "optional": true
- },
- "@esbuild/linux-ia32": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.12.tgz",
- "integrity": "sha512-yoRIAqc0B4lDIAAEFEIu9ttTRFV84iuAl0KNCN6MhKLxNPfzwCBvEMgwco2f71GxmpBcTtn7KdErueZaM2rEvw==",
- "dev": true,
- "optional": true
- },
- "@esbuild/linux-loong64": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.12.tgz",
- "integrity": "sha512-qYgt3dHPVvf/MgbIBpJ4Sup/yb9DAopZ3a2JgMpNKIHUpOdnJ2eHBo/aQdnd8dJ21X/+sS58wxHtA9lEazYtXQ==",
- "dev": true,
- "optional": true
- },
- "@esbuild/linux-mips64el": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.12.tgz",
- "integrity": "sha512-wHphlMLK4ufNOONqukELfVIbnGQJrHJ/mxZMMrP2jYrPgCRZhOtf0kC4yAXBwnfmULimV1qt5UJJOw4Kh13Yfg==",
- "dev": true,
- "optional": true
- },
- "@esbuild/linux-ppc64": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.12.tgz",
- "integrity": "sha512-TeN//1Ft20ZZW41+zDSdOI/Os1bEq5dbvBvYkberB7PHABbRcsteeoNVZFlI0YLpGdlBqohEpjrn06kv8heCJg==",
- "dev": true,
- "optional": true
- },
- "@esbuild/linux-riscv64": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.12.tgz",
- "integrity": "sha512-AgUebVS4DoAblBgiB2ACQ/8l4eGE5aWBb8ZXtkXHiET9mbj7GuWt3OnsIW/zX+XHJt2RYJZctbQ2S/mDjbp0UA==",
- "dev": true,
- "optional": true
- },
- "@esbuild/linux-s390x": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.12.tgz",
- "integrity": "sha512-dJ3Rb3Ei2u/ysSXd6pzleGtfDdc2MuzKt8qc6ls8vreP1G3B7HInX3i7gXS4BGeVd24pp0yqyS7bJ5NHaI9ing==",
- "dev": true,
- "optional": true
- },
- "@esbuild/linux-x64": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.12.tgz",
- "integrity": "sha512-OrNJMGQbPaVyHHcDF8ybNSwu7TDOfX8NGpXCbetwOSP6txOJiWlgQnRymfC9ocR1S0Y5PW0Wb1mV6pUddqmvmQ==",
- "dev": true,
- "optional": true
- },
- "@esbuild/netbsd-x64": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.12.tgz",
- "integrity": "sha512-55FzVCAiwE9FK8wWeCRuvjazNRJ1QqLCYGZVB6E8RuQuTeStSwotpSW4xoRGwp3a1wUsaVCdYcj5LGCASVJmMg==",
- "dev": true,
- "optional": true
- },
- "@esbuild/openbsd-x64": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.12.tgz",
- "integrity": "sha512-qnluf8rfb6Y5Lw2tirfK2quZOBbVqmwxut7GPCIJsM8lc4AEUj9L8y0YPdLaPK0TECt4IdyBdBD/KRFKorlK3g==",
- "dev": true,
- "optional": true
- },
- "@esbuild/sunos-x64": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.12.tgz",
- "integrity": "sha512-+RkKpVQR7bICjTOPUpkTBTaJ4TFqQBX5Ywyd/HSdDkQGn65VPkTsR/pL4AMvuMWy+wnXgIl4EY6q4mVpJal8Kg==",
- "dev": true,
- "optional": true
- },
- "@esbuild/win32-arm64": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.12.tgz",
- "integrity": "sha512-GNHuciv0mFM7ouzsU0+AwY+7eV4Mgo5WnbhfDCQGtpvOtD1vbOiRjPYG6dhmMoFyBjj+pNqQu2X+7DKn0KQ/Gw==",
- "dev": true,
- "optional": true
- },
- "@esbuild/win32-ia32": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.12.tgz",
- "integrity": "sha512-kR8cezhYipbbypGkaqCTWIeu4zID17gamC8YTPXYtcN3E5BhhtTnwKBn9I0PJur/T6UVwIEGYzkffNL0lFvxEw==",
- "dev": true,
- "optional": true
- },
- "@esbuild/win32-x64": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.12.tgz",
- "integrity": "sha512-O0UYQVkvfM/jO8a4OwoV0mAKSJw+mjWTAd1MJd/1FCX6uiMdLmMRPK/w6e9OQ0ob2WGxzIm9va/KG0Ja4zIOgg==",
- "dev": true,
- "optional": true
- },
"@eslint-community/eslint-utils": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@@ -9997,11 +9775,27 @@
"version": "1.0.1"
},
"@floating-ui/dom": {
- "version": "1.0.4",
+ "version": "1.6.3",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.3.tgz",
+ "integrity": "sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==",
+ "requires": {
+ "@floating-ui/core": "^1.0.0",
+ "@floating-ui/utils": "^0.2.0"
+ }
+ },
+ "@floating-ui/react-dom": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.8.tgz",
+ "integrity": "sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==",
"requires": {
- "@floating-ui/core": "^1.0.1"
+ "@floating-ui/dom": "^1.6.1"
}
},
+ "@floating-ui/utils": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz",
+ "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q=="
+ },
"@humanwhocodes/config-array": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
@@ -10175,31 +9969,30 @@
"version": "3.1.0"
},
"@mui/base": {
- "version": "5.0.0-beta.8",
- "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.8.tgz",
- "integrity": "sha512-b4vVjMZx5KzzEMf4arXKoeV5ZegAMOoPwoy1vfUBwhvXc2QtaaAyBp50U7OA2L06Leubc1A+lEp3eqwZoFn87g==",
- "requires": {
- "@babel/runtime": "^7.22.6",
- "@emotion/is-prop-valid": "^1.2.1",
- "@mui/types": "^7.2.4",
- "@mui/utils": "^5.14.1",
+ "version": "5.0.0-beta.40",
+ "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz",
+ "integrity": "sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==",
+ "requires": {
+ "@babel/runtime": "^7.23.9",
+ "@floating-ui/react-dom": "^2.0.8",
+ "@mui/types": "^7.2.14",
+ "@mui/utils": "^5.15.14",
"@popperjs/core": "^2.11.8",
- "clsx": "^1.2.1",
- "prop-types": "^15.8.1",
- "react-is": "^18.2.0"
+ "clsx": "^2.1.0",
+ "prop-types": "^15.8.1"
},
"dependencies": {
- "react-is": {
- "version": "18.2.0",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
- "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
+ "clsx": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz",
+ "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg=="
}
}
},
"@mui/core-downloads-tracker": {
- "version": "5.14.2",
- "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.14.2.tgz",
- "integrity": "sha512-x+c/MgDL1t/IIy5lDbMlrDouFG5DYZbl3DP4dbbuhlpPFBnE9glYwmJEee/orVHQpOPwLxCAIWQs+2DKSaBVWQ=="
+ "version": "5.15.14",
+ "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.14.tgz",
+ "integrity": "sha512-on75VMd0XqZfaQW+9pGjSNiqW+ghc5E2ZSLRBXwcXl/C4YzjfyjrLPhrEpKnR9Uym9KXBvxrhoHfPcczYHweyA=="
},
"@mui/icons-material": {
"version": "5.14.0",
@@ -10210,24 +10003,29 @@
}
},
"@mui/material": {
- "version": "5.14.2",
- "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.14.2.tgz",
- "integrity": "sha512-TgNR4/YRL11RifsnMWNhITNCkGJYVz20SCvVJBBoU5Y/KhUNSSJxjDpEB8VrnY+sUsV0NigLCkHZJglfsiS3Pw==",
- "requires": {
- "@babel/runtime": "^7.22.6",
- "@mui/base": "5.0.0-beta.8",
- "@mui/core-downloads-tracker": "^5.14.2",
- "@mui/system": "^5.14.1",
- "@mui/types": "^7.2.4",
- "@mui/utils": "^5.14.1",
- "@types/react-transition-group": "^4.4.6",
- "clsx": "^1.2.1",
- "csstype": "^3.1.2",
+ "version": "5.15.14",
+ "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.14.tgz",
+ "integrity": "sha512-kEbRw6fASdQ1SQ7LVdWR5OlWV3y7Y54ZxkLzd6LV5tmz+NpO3MJKZXSfgR0LHMP7meKsPiMm4AuzV0pXDpk/BQ==",
+ "requires": {
+ "@babel/runtime": "^7.23.9",
+ "@mui/base": "5.0.0-beta.40",
+ "@mui/core-downloads-tracker": "^5.15.14",
+ "@mui/system": "^5.15.14",
+ "@mui/types": "^7.2.14",
+ "@mui/utils": "^5.15.14",
+ "@types/react-transition-group": "^4.4.10",
+ "clsx": "^2.1.0",
+ "csstype": "^3.1.3",
"prop-types": "^15.8.1",
"react-is": "^18.2.0",
"react-transition-group": "^4.4.5"
},
"dependencies": {
+ "clsx": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz",
+ "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg=="
+ },
"react-is": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
@@ -10236,55 +10034,61 @@
}
},
"@mui/private-theming": {
- "version": "5.13.7",
- "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.13.7.tgz",
- "integrity": "sha512-qbSr+udcij5F9dKhGX7fEdx2drXchq7htLNr2Qg2Ma+WJ6q0ERlEqGSBiPiVDJkptcjeVL4DGmcf1wl5+vD4EA==",
+ "version": "5.15.14",
+ "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.14.tgz",
+ "integrity": "sha512-UH0EiZckOWcxiXLX3Jbb0K7rC8mxTr9L9l6QhOZxYc4r8FHUkefltV9VDGLrzCaWh30SQiJvAEd7djX3XXY6Xw==",
"requires": {
- "@babel/runtime": "^7.22.5",
- "@mui/utils": "^5.13.7",
+ "@babel/runtime": "^7.23.9",
+ "@mui/utils": "^5.15.14",
"prop-types": "^15.8.1"
}
},
"@mui/styled-engine": {
- "version": "5.13.2",
- "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.13.2.tgz",
- "integrity": "sha512-VCYCU6xVtXOrIN8lcbuPmoG+u7FYuOERG++fpY74hPpEWkyFQG97F+/XfTQVYzlR2m7nPjnwVUgATcTCMEaMvw==",
+ "version": "5.15.14",
+ "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.14.tgz",
+ "integrity": "sha512-RILkuVD8gY6PvjZjqnWhz8fu68dVkqhM5+jYWfB5yhlSQKg+2rHkmEwm75XIeAqI3qwOndK6zELK5H6Zxn4NHw==",
"requires": {
- "@babel/runtime": "^7.21.0",
+ "@babel/runtime": "^7.23.9",
"@emotion/cache": "^11.11.0",
- "csstype": "^3.1.2",
+ "csstype": "^3.1.3",
"prop-types": "^15.8.1"
}
},
"@mui/system": {
- "version": "5.14.1",
- "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.14.1.tgz",
- "integrity": "sha512-u+xlsU34Jdkgx1CxmBnIC4Y08uPdVX5iEd3S/1dggDFtOGp+Lj8xmKRJAQ8PJOOJLOh8pDwaZx4AwXikL4l1QA==",
- "requires": {
- "@babel/runtime": "^7.22.6",
- "@mui/private-theming": "^5.13.7",
- "@mui/styled-engine": "^5.13.2",
- "@mui/types": "^7.2.4",
- "@mui/utils": "^5.14.1",
- "clsx": "^1.2.1",
- "csstype": "^3.1.2",
+ "version": "5.15.14",
+ "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.14.tgz",
+ "integrity": "sha512-auXLXzUaCSSOLqJXmsAaq7P96VPRXg2Rrz6OHNV7lr+kB8lobUF+/N84Vd9C4G/wvCXYPs5TYuuGBRhcGbiBGg==",
+ "requires": {
+ "@babel/runtime": "^7.23.9",
+ "@mui/private-theming": "^5.15.14",
+ "@mui/styled-engine": "^5.15.14",
+ "@mui/types": "^7.2.14",
+ "@mui/utils": "^5.15.14",
+ "clsx": "^2.1.0",
+ "csstype": "^3.1.3",
"prop-types": "^15.8.1"
+ },
+ "dependencies": {
+ "clsx": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz",
+ "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg=="
+ }
}
},
"@mui/types": {
- "version": "7.2.4",
- "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.4.tgz",
- "integrity": "sha512-LBcwa8rN84bKF+f5sDyku42w1NTxaPgPyYKODsh01U1fVstTClbUoSA96oyRBnSNyEiAVjKm6Gwx9vjR+xyqHA==",
+ "version": "7.2.14",
+ "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.14.tgz",
+ "integrity": "sha512-MZsBZ4q4HfzBsywtXgM1Ksj6HDThtiwmOKUXH1pKYISI9gAVXCNHNpo7TlGoGrBaYWZTdNoirIN7JsQcQUjmQQ==",
"requires": {}
},
"@mui/utils": {
- "version": "5.14.1",
- "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.14.1.tgz",
- "integrity": "sha512-39KHKK2JeqRmuUcLDLwM+c2XfVC136C5/yUyQXmO2PVbOb2Bol4KxtkssEqCbTwg87PSCG3f1Tb0keRsK7cVGw==",
+ "version": "5.15.14",
+ "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.14.tgz",
+ "integrity": "sha512-0lF/7Hh/ezDv5X7Pry6enMsbYyGKjADzvHyo3Qrc/SSlTsQ1VkbDMbH0m2t3OR5iIVLwMoxwM7yGd+6FCMtTFA==",
"requires": {
- "@babel/runtime": "^7.22.6",
- "@types/prop-types": "^15.7.5",
- "@types/react-is": "^18.2.1",
+ "@babel/runtime": "^7.23.9",
+ "@types/prop-types": "^15.7.11",
"prop-types": "^15.8.1",
"react-is": "^18.2.0"
},
@@ -10421,6 +10225,19 @@
"@sinclair/typebox": {
"version": "0.24.51"
},
+ "@tanstack/query-core": {
+ "version": "5.36.1",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.36.1.tgz",
+ "integrity": "sha512-BteWYEPUcucEu3NBcDAgKuI4U25R9aPrHSP6YSf2NvaD2pSlIQTdqOfLRsxH9WdRYg7k0Uom35Uacb6nvbIMJg=="
+ },
+ "@tanstack/react-query": {
+ "version": "5.36.2",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.36.2.tgz",
+ "integrity": "sha512-bHNa+5dead+j6SA8WVlEOPxcGfteVFgdyFTCFcxBgjnPf0fFpHUc7aNZBCnvmPXqy/BeQa9zTuU9ectb7i8ZXA==",
+ "requires": {
+ "@tanstack/query-core": "5.36.1"
+ }
+ },
"@tanstack/react-table": {
"version": "8.9.3",
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.9.3.tgz",
@@ -10535,6 +10352,15 @@
"version": "1.0.0",
"dev": true
},
+ "@types/hoist-non-react-statics": {
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz",
+ "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==",
+ "requires": {
+ "@types/react": "*",
+ "hoist-non-react-statics": "^3.3.0"
+ }
+ },
"@types/istanbul-lib-coverage": {
"version": "2.0.4"
},
@@ -10603,7 +10429,9 @@
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
},
"@types/prop-types": {
- "version": "15.7.5"
+ "version": "15.7.12",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
+ "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q=="
},
"@types/react": {
"version": "18.2.14",
@@ -10623,18 +10451,21 @@
"@types/react": "*"
}
},
- "@types/react-is": {
- "version": "18.2.1",
- "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-18.2.1.tgz",
- "integrity": "sha512-wyUkmaaSZEzFZivD8F2ftSyAfk6L+DfFliVj/mYdOXbVjRcS87fQJLTnhk6dRZPuJjI+9g6RZJO4PNCngUrmyw==",
+ "@types/react-redux": {
+ "version": "7.1.33",
+ "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.33.tgz",
+ "integrity": "sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==",
"requires": {
- "@types/react": "*"
+ "@types/hoist-non-react-statics": "^3.3.0",
+ "@types/react": "*",
+ "hoist-non-react-statics": "^3.3.0",
+ "redux": "^4.0.0"
}
},
"@types/react-transition-group": {
- "version": "4.4.6",
- "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.6.tgz",
- "integrity": "sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==",
+ "version": "4.4.10",
+ "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz",
+ "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==",
"requires": {
"@types/react": "*"
}
@@ -10879,9 +10710,11 @@
}
},
"braces": {
- "version": "3.0.2",
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"requires": {
- "fill-range": "^7.0.1"
+ "fill-range": "^7.1.1"
}
},
"browserslist": {
@@ -10934,9 +10767,9 @@
"dev": true
},
"caniuse-lite": {
- "version": "1.0.30001576",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001576.tgz",
- "integrity": "sha512-ff5BdakGe2P3SQsMsiqmt1Lc8221NR1VzHj5jXN5vBny9A6fpze94HiVV/n7XRosOlsShJcvMv5mdnpjOGCEgg==",
+ "version": "1.0.30001636",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001636.tgz",
+ "integrity": "sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==",
"dev": true
},
"canvas-fit": {
@@ -11111,6 +10944,14 @@
"which": "^2.0.1"
}
},
+ "css-box-model": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz",
+ "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==",
+ "requires": {
+ "tiny-invariant": "^1.0.6"
+ }
+ },
"css-font": {
"version": "1.2.0",
"requires": {
@@ -11158,9 +10999,9 @@
"version": "1.0.3"
},
"csstype": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
- "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"d": {
"version": "1.0.1",
@@ -11443,7 +11284,9 @@
"version": "2.2.4"
},
"ejs": {
- "version": "3.1.8",
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
+ "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
"dev": true,
"requires": {
"jake": "^10.8.5"
@@ -11563,10 +11406,13 @@
}
},
"es5-ext": {
- "version": "0.10.62",
+ "version": "0.10.64",
+ "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz",
+ "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==",
"requires": {
"es6-iterator": "^2.0.3",
"es6-symbol": "^3.1.3",
+ "esniff": "^2.0.1",
"next-tick": "^1.1.0"
}
},
@@ -11852,6 +11698,24 @@
"integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==",
"dev": true
},
+ "esniff": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz",
+ "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==",
+ "requires": {
+ "d": "^1.0.1",
+ "es5-ext": "^0.10.62",
+ "event-emitter": "^0.3.5",
+ "type": "^2.7.2"
+ },
+ "dependencies": {
+ "type": {
+ "version": "2.7.3",
+ "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz",
+ "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ=="
+ }
+ }
+ },
"espree": {
"version": "9.6.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.0.tgz",
@@ -11889,6 +11753,15 @@
"esutils": {
"version": "2.0.3"
},
+ "event-emitter": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
+ "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
+ "requires": {
+ "d": "1",
+ "es5-ext": "~0.10.14"
+ }
+ },
"events": {
"version": "3.3.0"
},
@@ -11984,9 +11857,9 @@
"version": "2.0.6"
},
"fast-loops": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/fast-loops/-/fast-loops-1.1.3.tgz",
- "integrity": "sha512-8EZzEP0eKkEEVX+drtd9mtuQ+/QrlfW/5MlwcwK5Nds6EkZ/tRzEexkzUY2mIssnAyVLT+TKHuRXmFNNXYUd6g=="
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/fast-loops/-/fast-loops-1.1.4.tgz",
+ "integrity": "sha512-8dbd3XWoKCTms18ize6JmQF1SFnnfj5s0B7rRry22EofgMu7B6LKHVh+XfFqFGsqnbH54xgeO83PzpKI+ODhlg=="
},
"fast-shallow-equal": {
"version": "1.0.0",
@@ -12036,7 +11909,9 @@
}
},
"fill-range": {
- "version": "7.0.1",
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"requires": {
"to-regex-range": "^5.0.1"
}
@@ -12654,7 +12529,9 @@
"dev": true
},
"is-number": {
- "version": "7.0.0"
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
},
"is-number-object": {
"version": "1.0.7",
@@ -12859,6 +12736,11 @@
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz",
"integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ=="
},
+ "js-sha256": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.11.0.tgz",
+ "integrity": "sha512-6xNlKayMZvds9h1Y1VWc0fQHQ82BxTXizWPEtEeGvmOUYpBRy4gbWroHLpzowe6xiQhHpelCQiE7HEdznyBL9Q=="
+ },
"js-tokens": {
"version": "4.0.0"
},
@@ -12916,9 +12798,23 @@
"object.assign": "^4.1.3"
}
},
+ "jwt-decode": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
+ "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA=="
+ },
"kdbush": {
"version": "3.0.0"
},
+ "keycloak-js": {
+ "version": "25.0.2",
+ "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-25.0.2.tgz",
+ "integrity": "sha512-ACLf5O5PqzfDJwGqvLpqM0kflYWmyl3+T7M2C23gztJYccDxdfNP54+B9OkXz2GnDpLUId0ceoA+lbHw9t4Wng==",
+ "requires": {
+ "js-sha256": "^0.11.0",
+ "jwt-decode": "^4.0.0"
+ }
+ },
"kolorist": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",
@@ -13135,6 +13031,14 @@
"ms": {
"version": "2.1.2"
},
+ "mui-color-input": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/mui-color-input/-/mui-color-input-2.0.3.tgz",
+ "integrity": "sha512-rAd040qQ0Y+8dk4gE8kkCiJ/vCgA0j4vv1quJ43BfORTFE3uHarHj0xY1Vo9CPbojtx1f5vW+CjckYPRIZPIRg==",
+ "requires": {
+ "@ctrl/tinycolor": "^4.0.3"
+ }
+ },
"mumath": {
"version": "3.3.4",
"requires": {
@@ -13630,6 +13534,11 @@
"performance-now": "^2.1.0"
}
},
+ "raf-schd": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
+ "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ=="
+ },
"rbush": {
"version": "3.0.1",
"requires": {
@@ -13642,6 +13551,27 @@
"loose-envify": "^1.1.0"
}
},
+ "react-beautiful-dnd": {
+ "version": "13.1.1",
+ "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz",
+ "integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==",
+ "requires": {
+ "@babel/runtime": "^7.9.2",
+ "css-box-model": "^1.2.0",
+ "memoize-one": "^5.1.1",
+ "raf-schd": "^4.0.2",
+ "react-redux": "^7.2.0",
+ "redux": "^4.0.4",
+ "use-memo-one": "^1.1.1"
+ },
+ "dependencies": {
+ "memoize-one": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
+ "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
+ }
+ }
+ },
"react-color": {
"version": "2.19.3",
"requires": {
@@ -13707,6 +13637,19 @@
"warning": "^4.0.2"
}
},
+ "react-redux": {
+ "version": "7.2.9",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz",
+ "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==",
+ "requires": {
+ "@babel/runtime": "^7.15.4",
+ "@types/react-redux": "^7.1.20",
+ "hoist-non-react-statics": "^3.3.2",
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.7.2",
+ "react-is": "^17.0.2"
+ }
+ },
"react-resizable-panels": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.0.7.tgz",
@@ -13824,6 +13767,14 @@
"strip-indent": "^3.0.0"
}
},
+ "redux": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
+ "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
+ "requires": {
+ "@babel/runtime": "^7.9.2"
+ }
+ },
"redux-bundler": {
"version": "28.0.3"
},
@@ -13832,9 +13783,9 @@
"requires": {}
},
"regenerator-runtime": {
- "version": "0.13.11",
- "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
- "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
+ "version": "0.14.1",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
+ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"regexp.prototype.flags": {
"version": "1.4.3",
@@ -14547,8 +14498,7 @@
"tiny-invariant": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz",
- "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==",
- "dev": true
+ "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw=="
},
"tinycolor2": {
"version": "1.4.2"
@@ -14576,6 +14526,8 @@
},
"to-regex-range": {
"version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"requires": {
"is-number": "^7.0.0"
}
@@ -14651,10 +14603,10 @@
}
},
"typescript": {
- "version": "4.8.4",
- "dev": true,
- "optional": true,
- "peer": true
+ "version": "5.4.5",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
+ "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
+ "dev": true
},
"ufo": {
"version": "1.4.0",
@@ -14724,6 +14676,12 @@
"version": "1.1.2",
"requires": {}
},
+ "use-memo-one": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz",
+ "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==",
+ "requires": {}
+ },
"util": {
"version": "0.12.5",
"requires": {
@@ -14738,9 +14696,9 @@
"version": "1.0.2"
},
"vite": {
- "version": "4.5.2",
- "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz",
- "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==",
+ "version": "4.5.3",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz",
+ "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==",
"dev": true,
"requires": {
"esbuild": "^0.18.10",
diff --git a/package.json b/package.json
index 33f0407a..37b61199 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "hhd-ui",
- "version": "0.15.7",
+ "version": "0.17.0",
"private": true,
"dependencies": {
"@ag-grid-community/client-side-row-model": "^30.0.3",
@@ -14,6 +14,7 @@
"@iconify/utils": "^2.1.22",
"@mui/icons-material": "^5.14.0",
"@mui/material": "^5.14.2",
+ "@tanstack/react-query": "^5.36.2",
"@tanstack/react-table": "^8.9.3",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
@@ -23,15 +24,18 @@
"date-fns": "^2.30.0",
"graceful-fs": "^4.2.11",
"internal-nav-helper": "^3.1.0",
+ "keycloak-js": "^25.0.2",
"lodash.debounce": "^4.0.8",
"lodash.isequal": "^4.5.0",
"luxon": "^3.3.0",
"money-clip": "^3.0.5",
+ "mui-color-input": "^2.0.3",
"ol": "^7.4.0",
"plotly.js": "^2.25.2",
"process": "^0.11.10",
"proj4": "^2.9.0",
"react": "^18.2.0",
+ "react-beautiful-dnd": "^13.1.1",
"react-color": "^2.19.3",
"react-csv": "^2.2.2",
"react-datepicker": "^4.16.0",
@@ -67,6 +71,7 @@
"rollup-plugin-visualizer": "^5.9.2",
"sass": "^1.63.6",
"source-map-explorer": "^2.5.3",
+ "typescript": "^5.4.5",
"vite": "^4.5.2",
"vite-plugin-checker": "^0.6.1",
"vite-plugin-svgr": "^3.2.0"
diff --git a/public/silent-check-sso.html b/public/silent-check-sso.html
new file mode 100644
index 00000000..128f00a3
--- /dev/null
+++ b/public/silent-check-sso.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app-bundles/batch-plot-configurations-bundle.js b/src/app-bundles/batch-plot-configurations-bundle.js
index 968b4c2a..b3c14806 100644
--- a/src/app-bundles/batch-plot-configurations-bundle.js
+++ b/src/app-bundles/batch-plot-configurations-bundle.js
@@ -1,14 +1,37 @@
-import createRestBundle from './create-rest-bundle';
import { createSelector } from 'redux-bundler';
+import createRestBundle from './create-rest-bundle';
+
+const getTimeseriesFromDisplay = (plotType, display, timeseries) => {
+ switch (plotType) {
+ case 'scatter-line':
+ return timeseries.filter((ts) =>
+ (display?.traces?.map(trace => trace.timeseries_id) || []).includes(ts.id)
+ );
+ case 'bullseye':
+ return timeseries.filter(ts =>
+ [display?.x_axis_timeseries_id, display?.y_axis_timeseries_id].includes(ts.id)
+ );
+ case 'contour':
+ return timeseries.filter(ts =>
+ display?.timeseries_ids?.includes(ts.id)
+ );
+ case 'profile':
+ return display?.instrument_id;
+ default:
+ throw new Error(`Invalid Plot Type: ${plotType}. Expected one of: ['scatter-line', 'bullseye', 'contour', 'profile']`);
+ }
+};
+
export default createRestBundle({
name: 'batchPlotConfigurations',
uid: 'id',
persist: false,
- getTemplate: '/projects/:projectId/plot_configurations',
- putTemplate: '/projects/:projectId/plot_configurations/{:item.id}',
+ getTemplate: '/projects/:projectId/plot_configs',
+ putTemplate: '/projects/:projectId/plot_configs/{:item.id}',
+ // @TODO: Remove the `postTemplate`
postTemplate: '/projects/:projectId/plot_configurations',
- deleteTemplate: '/projects/:projectId/plot_configurations/{:item.id}',
+ deleteTemplate: '/projects/:projectId/plot_configs/{:item.id}',
fetchActions: ['URL_UPDATED', 'PROJECTS_FETCH_FINISHED'],
forceFetchActions: ['BATCHPLOTCONFIGURATIONS_SAVE_FINISHED'],
urlParamSelectors: ['selectProjectsIdByRoute'],
@@ -29,6 +52,35 @@ export default createRestBundle({
store.doBatchPlotMapAddData();
},
+ /**
+ * Save plot plot config settings. note: formData payloads differ based on plotType.
+ *
+ * @param {string} plotType one of ['scatter-line', 'profile', 'contour', 'bullseye']
+ * @param {string} id Batch Plot Config Id
+ * @param {object} formData api-defined trace structure for related plotType
+ * @returns
+ */
+ doSaveBatchPlotConfiguration: (plotType, id = null, formData = {}) => ({ dispatch, store, apiPost, apiPut }) => {
+ const uriMap = {
+ 'scatter-line': 'scatter_line_plots',
+ 'profile': 'profile_plots',
+ 'contour': 'contour_plots',
+ 'bullseye': 'bullseye_plots',
+ };
+ const method = !id ? apiPost : apiPut;
+ const projectId = store.selectProjectsIdByRoute()?.projectId;
+ const uri = `/projects/${projectId}/plot_configs/${uriMap[plotType]}${!id ? '' : `/${id}`}`;
+
+ method(uri, formData, (err, _body) => {
+ if (err) {
+ dispatch({ type: 'BATCH_PLOT_CONFIGURATION_SAVE_ERROR', payload: err });
+ } else {
+ dispatch({ type: 'BATCH_PLOT_CONFIGURATION_SAVED' });
+ store.doBatchPlotConfigurationsFetch();
+ }
+ });
+ },
+
selectBatchPlotConfigurationsRaw: (state) => state.batchPlotConfigurations,
selectBatchPlotConfigurationsActiveId: (state) =>
state.batchPlotConfigurations._activeBatchPlotConfigurationId,
@@ -47,11 +99,12 @@ export default createRestBundle({
timeseries.length
) {
batchPlotConfigurations.forEach((config) => {
- const activeTS = timeseries.filter((ts) =>
- (config.timeseries_id || []).includes(ts.id)
- );
- instrumentMap[config.id] = instruments.filter((i) =>
- activeTS.some((ts) => ts.instrument_id === i.id)
+ const { plot_type, display } = config || {};
+ const activeTS = getTimeseriesFromDisplay(plot_type, display, timeseries);
+
+ instrumentMap[config.id] = plot_type === 'profile'
+ ? instruments.filter(i => i.id === activeTS)
+ : instruments.filter(i => activeTS.some((ts) => ts.instrument_id === i.id)
);
});
}
diff --git a/src/app-bundles/collection-group-bundle.js b/src/app-bundles/collection-group-bundle.js
index 52f93b0c..88e2d2d0 100644
--- a/src/app-bundles/collection-group-bundle.js
+++ b/src/app-bundles/collection-group-bundle.js
@@ -19,7 +19,7 @@ export default createRestBundle({
],
urlParamSelectors: ['selectProjectsIdByRoute'],
prefetch: (store) => {
- const hash = store.selectHash();
+ const hash = store.selectHashStripQuery();
const pathname = store.selectRelativePathname();
const whiteList = ['dashboard'];
diff --git a/src/app-bundles/create-auth-bundle.js b/src/app-bundles/create-auth-bundle.js
deleted file mode 100644
index f1bc2bf7..00000000
--- a/src/app-bundles/create-auth-bundle.js
+++ /dev/null
@@ -1,187 +0,0 @@
-import { createSelector } from 'redux-bundler';
-import xhr from 'xhr';
-
-const getTokenPart = function (token, part) {
- const splitToken = token.split('.');
- return splitToken[part];
-};
-
-const createAuthBundle = (opts) => {
- const defaults = {
- url: 'https://corpsmap-dev.sec.usace.army.mil/cwbi/auth/basic',
- name: 'auth',
- token: null,
- redirectOnLogout: null,
- verifyInterval: 1000 * 60,
- };
-
- const config = Object.assign({}, defaults, opts);
-
- if (opts.appId)
- config.url = `https://corpsmap-dev.sec.usace.army.mil/cwbi/goauth/token/${opts.appId}`;
-
- return {
- name: config.name,
-
- getReducer: () => {
- const initialState = {
- url: config.url,
- mockToken: config.token,
- token: config.token,
- error: null,
- mock: config.mock,
- shouldVerifyToken: true,
- redirectOnLogout: config.redirectOnLogout,
- };
-
- return (state = initialState, { type, payload }) => {
- switch (type) {
- case 'AUTH_LOGGED_IN':
- case 'AUTH_LOGGED_OUT':
- case 'AUTH_ERROR':
- case 'AUTH_VERIFY_TOKEN':
- return Object.assign({}, state, payload);
- default:
- return state;
- }
- };
- },
-
- doAuthLogin: () => ({ dispatch, store }) => {
- const isMock = store.selectAuthIsMocked();
- if (isMock) {
- const token = store.selectAuthTokenMockRaw();
- dispatch({
- type: 'AUTH_LOGGED_IN',
- payload: { token: token, error: null, shouldVerifyToken: true },
- });
- } else {
- const url = store.selectAuthUrl();
- //@todo move to fetch api at some point
- try {
- xhr(url, (err, response, body) => {
- if (err) {
- throw new Error(`Login Response not ok. ${err}`);
- } else {
- const token = typeof body === 'string' ? body : JSON.parse(body);
- dispatch({
- type: 'AUTH_LOGGED_IN',
- payload: { token: token, error: null, shouldVerifyToken: true },
- });
- }
- });
- } catch (err) {
- // eslint-disable-next-line no-console
- if (import.meta.env.DEV) console.error(err);
- dispatch({
- type: 'AUTH_ERROR',
- payload: { msg: 'Error Logging In', err: err },
- });
- }
- }
- },
-
- doAuthLogout: () => ({ dispatch, store }) => {
- if (store.selectAuthTokenRaw()) {
- dispatch({
- type: 'AUTH_LOGGED_OUT',
- payload: { token: null, error: null },
- });
- store.doRemoveProfile();
- const redirect = store.selectAuthRedirectOnLogout();
- if (redirect) store.doUpdateRelativeUrl(redirect);
- }
- },
-
- doAuthVerifyToken: () => ({ dispatch, store }) => {
- dispatch({
- type: 'AUTH_VERIFY_TOKEN',
- payload: { shouldVerifyToken: false },
- });
- const isExpired = store.selectAuthTokenIsExpired();
- if (isExpired) {
- store.doAuthLogout();
- } else {
- window.setTimeout(store.doAuthVerifyToken, config.verifyInterval);
- }
- },
-
- selectAuthRedirectOnLogout: (state) => state.auth.redirectOnLogout,
-
- selectAuthIsMocked: (state) => state.auth.mock,
-
- selectAuthUrl: (state) => state.auth.url,
-
- // select parts of the token itself
-
- selectAuthTokenRaw: (state) => state.auth.token,
-
- selectAuthTokenMockRaw: (state) => state.auth.mockToken,
-
- selectAuthTokenHeader: createSelector('selectAuthTokenRaw', (token) => {
- if (!token) return {};
- return JSON.parse(window.atob(getTokenPart(token, 0)));
- }),
-
- selectAuthTokenPayload: createSelector('selectAuthTokenRaw', (token) => {
- if (!token) return {};
- return JSON.parse(window.atob(getTokenPart(token, 1)));
- }),
-
- // select info about token expiration
-
- selectAuthTokenExp: createSelector('selectAuthTokenPayload', (payload) => {
- if (!Object.prototype.hasOwnProperty.call(payload, 'exp')) return null;
- return payload.exp;
- }),
-
- selectAuthTokenIsExpired: createSelector('selectAuthTokenExp', (exp) => {
- if (!exp) return true;
- return exp < Math.floor(Date.now() / 1000);
- }),
-
- // select parts of the payload
-
- selectAuthUsername: createSelector('selectAuthTokenPayload', (payload) => {
- if (!Object.prototype.hasOwnProperty.call(payload, 'name')) return null;
- return payload.name;
- }),
-
- selectAuthEdipi: createSelector('selectAuthTokenPayload', (payload) => {
- if (!Object.prototype.hasOwnProperty.call(payload, 'sub')) return null;
- return payload.sub;
- }),
-
- selectAuthRoles: createSelector('selectAuthTokenPayload', (payload) => {
- if (!Object.prototype.hasOwnProperty.call(payload, 'roles')) return [];
- return payload.roles;
- }),
-
- selectAuthGroups: createSelector('selectAuthRoles', (roles) => roles.map((role) => {
- const roleArr = role.split('.');
- return roleArr[0];
- })),
-
- selectAuthGroupRoles: createSelector('selectAuthRoles', (roles) => {
- const groupRoles = {};
- roles
- .map((role) => role.split('.'))
- .forEach((role) => {
- if (!Object.prototype.hasOwnProperty.call(groupRoles, role[0])) groupRoles[role[0]] = [];
- groupRoles[role[0]].push(role[1]);
- });
- return groupRoles;
- }),
-
- selectAuthIsLoggedIn: (state) => !!state.auth.token,
-
- reactAuthShouldVerifyToken: (state) => {
- if (state.auth.shouldVerifyToken)
- return { actionCreator: 'doAuthVerifyToken' };
- },
-
- persistActions: ['AUTH_LOGGED_IN', 'AUTH_LOGGED_OUT'],
- };
-};
-
-export default createAuthBundle;
diff --git a/src/app-bundles/create-jwt-api-bundle.js b/src/app-bundles/create-jwt-api-bundle.js
index 6782402b..fe708ca2 100644
--- a/src/app-bundles/create-jwt-api-bundle.js
+++ b/src/app-bundles/create-jwt-api-bundle.js
@@ -1,3 +1,5 @@
+import { getToken } from '../userService.ts';
+
const arrayIze = (thing) => !thing || Array.isArray(thing) ? thing : [thing];
const shouldSkipToken = (method, path, unless) => {
@@ -92,7 +94,6 @@ const createJwtApiBundle = (opts) => {
const defaults = {
name: 'api',
root: '',
- tokenSelector: 'selectAuthToken',
unless: null,
};
@@ -103,7 +104,6 @@ const createJwtApiBundle = (opts) => {
// selectors
const selectRoot = `select${uCaseName}Root`;
const selectUnless = `select${uCaseName}Unless`;
- const selectTokenSelector = `select${uCaseName}TokenSelector`;
return {
name: config.name,
@@ -112,7 +112,6 @@ const createJwtApiBundle = (opts) => {
const initialData = {
root: config.root,
unless: config.unless,
- tokenSelector: config.tokenSelector,
};
return (state = initialData) => state;
@@ -120,24 +119,25 @@ const createJwtApiBundle = (opts) => {
[selectRoot]: (state) => state[config.name].root,
[selectUnless]: (state) => state[config.name].unless,
- [selectTokenSelector]: (state) => state[config.name].tokenSelector,
getExtraArgs: (store) => {
const getCommonItems = () => ({
root: store[selectRoot](),
unless: store[selectUnless](),
- tokenSelector: store[selectTokenSelector](),
});
- const defaultHeaders = token => ({
- Authorization: `Bearer ${token}`,
- });
+ const defaultHeaders = token => (
+ token ? {
+ Authorization: `Bearer ${token}`,
+ } : {}
+ );
return {
apiFetch: (path, options = {}) => {
- const { root, unless, tokenSelector } = getCommonItems();
+ const { root, unless } = getCommonItems();
if (!shouldSkipToken(options.method, path, unless)) {
- const token = store[tokenSelector]();
+ const token = getToken();
+
if (!token) return null;
else {
options.headers = { ...defaultHeaders(token) };
@@ -147,13 +147,14 @@ const createJwtApiBundle = (opts) => {
},
apiGet: (path, callback) => {
- const { root, unless, tokenSelector } = getCommonItems();
+ const { root, unless } = getCommonItems();
const options = {
method: 'GET',
};
if (!shouldSkipToken(options.method, path, unless)) {
- const token = store[tokenSelector]();
+ const token = getToken();
+
if (!token) return null;
else {
options.headers = { ...defaultHeaders(token) };
@@ -163,7 +164,7 @@ const createJwtApiBundle = (opts) => {
},
apiPut: (path, payload, callback) => {
- const { root, unless, tokenSelector } = getCommonItems();
+ const { root, unless } = getCommonItems();
const options = {
method: 'PUT',
headers: {
@@ -171,7 +172,7 @@ const createJwtApiBundle = (opts) => {
},
};
if (!shouldSkipToken(options.method, path, unless)) {
- const token = store[tokenSelector]();
+ const token = getToken();
if (!token) return null;
else {
options.headers = {
@@ -187,7 +188,7 @@ const createJwtApiBundle = (opts) => {
},
apiPost: (path, payload, callback) => {
- const { root, unless, tokenSelector } = getCommonItems();
+ const { root, unless } = getCommonItems();
const options = {
method: 'POST',
headers: {
@@ -195,7 +196,7 @@ const createJwtApiBundle = (opts) => {
},
};
if (!shouldSkipToken(options.method, path, unless)) {
- const token = store[tokenSelector]();
+ const token = getToken();
if (!token) return null;
else {
options.headers = {
@@ -212,12 +213,12 @@ const createJwtApiBundle = (opts) => {
},
apiDelete: (path, callback) => {
- const { root, unless, tokenSelector } = getCommonItems();
+ const { root, unless } = getCommonItems();
const options = {
method: 'DELETE',
};
if (!shouldSkipToken(options.method, path, unless)) {
- const token = store[tokenSelector]();
+ const token = getToken();
if (!token) return null;
else {
options.headers = { ...defaultHeaders(token) };
diff --git a/src/app-bundles/create-rest-bundle.js b/src/app-bundles/create-rest-bundle.js
index d9314891..785023a9 100644
--- a/src/app-bundles/create-rest-bundle.js
+++ b/src/app-bundles/create-rest-bundle.js
@@ -33,70 +33,6 @@ const decorateUrlWithItem = (urlTemplate, item) => {
return url;
};
-/**
- * Check to see if a particular token part exists against a given value
- */
-function checkTokenPart(tokenRoles, val, idx) {
- let match = false;
- tokenRoles.forEach((tokenRole) => {
- const tokenPart = tokenRole.split('.')[idx];
- if (tokenPart === val) match = true;
- });
- return match;
-}
-
-/**
- * Check one array of roles against another array of roles accounting
- * for wildcards and org substitution
- */
-function checkRoles(roles, tokenRolesJoined, orgsActiveSlug) {
- let pass = false;
- for (let i = 0; i < roles.length; i++) {
- let role = roles[i];
- role = role.replace(
- ':ORG.',
- `${orgsActiveSlug ? orgsActiveSlug.toUpperCase() : ''}.`
- );
-
- // let super users through no matter what
- if (tokenRolesJoined.indexOf('APP.SYSADMIN') !== -1) {
- pass = true;
- break;
- }
-
- // first let's test if this role is in tokenRoles, if so, pass and move on
- if (tokenRolesJoined.indexOf(role) !== -1) {
- pass = true;
- break;
- }
-
- // ok, let's check to see if we have a wildcard
- if (role.indexOf('*') !== -1) {
- // if both parts are * then pass is true
- if (role === '*.*') {
- pass = true;
- break;
- }
-
- // otherwise we've got to check both parts separately
- const parts = role.split('.');
-
- // looks like we do, is it in the org position?
- if (parts[0] === '*') {
- // if so, check tokenRoles for the role
- if (checkTokenPart(tokenRolesJoined, parts[1], 1)) pass = true;
- if (pass) break;
- }
-
- // how about the role position?
- if (parts[1] === '*') {
- if (checkTokenPart(tokenRolesJoined, parts[0], 0)) pass = true;
- if (pass) break;
- }
- }
- }
- return pass;
-}
/**
* Main Bundle Creator export
@@ -183,9 +119,7 @@ const createRestBundle = (opts) => {
const selectForceFetch = `select${uCaseName}ForceFetch`;
const selectAbortReason = `select${uCaseName}AbortReason`;
const selectAllowRoles = `select${uCaseName}AllowRoles`;
- const selectIsAllowedRole = `select${uCaseName}IsAllowedRole`;
const selectDisallowRoles = `select${uCaseName}DisallowRoles`;
- const selectIsDisallowedRole = `select${uCaseName}IsDisallowedRole`;
const selectPageSize = `select${uCaseName}PageSize`;
const selectSortBy = `select${uCaseName}SortBy`;
const selectSortAsc = `select${uCaseName}SortAsc`;
@@ -274,19 +208,6 @@ const createRestBundle = (opts) => {
},
});
- const isAllowed = store[selectIsAllowedRole]();
- const isDisallowed = store[selectIsDisallowedRole]();
- if (!isAllowed || isDisallowed) {
- dispatch({
- type: actions.FETCH_ABORT,
- payload: {
- _isLoading: false,
- _abortReason: 'User is not allowed to run this query',
- },
- });
- return;
- }
-
const url = decorateUrlWithItem(store[selectGetUrl](), item);
let fetchCount = store[selectFetchCount]();
const isStale = store[selectIsStale]();
@@ -738,25 +659,9 @@ const createRestBundle = (opts) => {
),
[selectAllowRoles]: (state) => state[config.name]._allowRoles,
-
- [selectIsAllowedRole]: createSelector(
- selectAllowRoles,
- 'selectAuthRoles',
- checkRoles
- ),
-
[selectDisallowRoles]: (state) => state[config.name]._disallowRoles,
-
- [selectIsDisallowedRole]: createSelector(
- selectDisallowRoles,
- 'selectAuthRoles',
- checkRoles
- ),
-
[selectSortBy]: (state) => state[config.name]._sortBy,
-
[selectSortAsc]: (state) => state[config.name]._sortAsc,
-
[reactShouldFetch]: (state) => {
if (state[config.name]._shouldFetch) return { actionCreator: doFetch };
},
diff --git a/src/app-bundles/domains-bundle.js b/src/app-bundles/domains-bundle.js
index 8d8a876e..1ac45078 100644
--- a/src/app-bundles/domains-bundle.js
+++ b/src/app-bundles/domains-bundle.js
@@ -12,8 +12,8 @@ export default createRestBundle({
putTemplate: '/domains/{:item.id}',
postTemplate: '/domains',
deleteTemplate: '/domains/{:item.id}',
- fetchActions: ['URL_UPDATED', 'AUTH_LOGGED_IN'],
- forceFetchActions: ['EXPLOREMAP_INITIALIZE_START'],
+ fetchActions: ['URL_UPDATED'],
+ forceFetchActions: ['@@INIT'],
addons: {
selectDomainsItemsByGroup: createSelector('selectDomainsItems', (items) => {
if (!items) return null;
diff --git a/src/app-bundles/explore-data-bundle.js b/src/app-bundles/explore-data-bundle.js
index 45fe8062..b4c49d29 100644
--- a/src/app-bundles/explore-data-bundle.js
+++ b/src/app-bundles/explore-data-bundle.js
@@ -1,5 +1,6 @@
import { createSelector } from 'redux-bundler';
+import { getToken } from '../userService.ts';
import { seriesStyles } from '../common/helpers/utils';
const exploreDataBundle = {
@@ -65,14 +66,13 @@ const exploreDataBundle = {
dispatch({ type: 'EXPLORE_DATA_LOADING', payload: true });
const apiRoot = store.selectApiRoot();
- const token = store.selectAuthTokenRaw();
await fetch(`${apiRoot}/explorer?before=${before}&after=${after}`, {
method: 'POST',
mode: 'cors',
headers: {
'Content-Type': 'application/json',
- 'Authorization': 'Bearer ' + token,
+ 'Authorization': 'Bearer ' + getToken(),
},
body: JSON.stringify(instrumentIds),
})
@@ -89,7 +89,7 @@ const exploreDataBundle = {
mode: 'cors',
headers: {
'Content-Type': 'application/json',
- 'Authorization': 'Bearer ' + token,
+ 'Authorization': 'Bearer ' + getToken(),
},
body: JSON.stringify(instrumentIds),
})
diff --git a/src/app-bundles/home-data-bundle.js b/src/app-bundles/home-data-bundle.js
index c90f0317..e931c961 100644
--- a/src/app-bundles/home-data-bundle.js
+++ b/src/app-bundles/home-data-bundle.js
@@ -9,7 +9,7 @@ export default createRestBundle({
persist: true,
routeParam: null,
getTemplate: '/home',
- fetchActions: ['URL_UPDATED', 'AUTH_LOGGED_IN'],
+ fetchActions: ['URL_UPDATED'],
forceFetchActions: [],
prefetch: store => store.selectRelativePathname() === '/',
addons: {
@@ -26,7 +26,7 @@ export default createRestBundle({
}),
selectHashStripQuery: createSelector('selectHash', hash => {
- const queryRegex = /\?.*/g;
+ const queryRegex = /[?&].*/g;
return hash ? hash.replace(queryRegex, '') : null;
}),
diff --git a/src/app-bundles/inclinometer-measurements.js b/src/app-bundles/inclinometer-measurements.js
index 72e95b01..9dbf3fe3 100644
--- a/src/app-bundles/inclinometer-measurements.js
+++ b/src/app-bundles/inclinometer-measurements.js
@@ -23,7 +23,7 @@ export default createRestBundle({
],
mergeItems: true,
prefetch: (store) => {
- const hash = store.selectHash();
+ const hash = store.selectHashStripQuery();
const pathname = store.selectRelativePathname();
const whitelist = [];
diff --git a/src/app-bundles/index.js b/src/app-bundles/index.js
index 4891c014..5fa934b2 100644
--- a/src/app-bundles/index.js
+++ b/src/app-bundles/index.js
@@ -4,7 +4,6 @@ import {
createUrlBundle,
} from 'redux-bundler';
// Required change from @corpsmap/create-auth-bundle;
-import createAuthBundle from './create-auth-bundle';
// Required change from @corpsmap/create-jwt-api-bundle;
import createJwtApiBundle from './create-jwt-api-bundle';
import createUrlBasePathBundle from './create-url-base-path-bundle';
@@ -58,6 +57,7 @@ import projectionBundle from './projection-bundle';
import projectsBundle from './projects-bundle';
import qualityControl from './quality-control';
import rainfallBundle from './rainfall-bundle';
+import reportConfigurationsBundle from './report-configurations-bundle';
import routesBundle from './routes-bundle';
import submittalsBundle from './submittals-bundle';
import timeseriesBundle from './time-series-bundle';
@@ -65,43 +65,35 @@ import timeseriesMeasurementBundle from './time-series-measurements-bundle';
import uploadBundle from './upload-bundle';
import usersBundle from './users-bundle';
-// Mock Token User
-// const mockTokenPublic =
-// 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIwIiwibmFtZSI6IlVzZXIuVGVzdCIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoyMDAwMDAwMDAwLCJyb2xlcyI6W119._N-sAWgMhYsWhwIf44_SGSMGSgnnM8tntlswsBqjYDo';
-const mockTokenApplicationAdmin =
- 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyIiwibmFtZSI6IlVzZXIuQXBwbGljYXRpb25BZG1pbiIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoyMDAwMDAwMDAwLCJyb2xlcyI6W119.aKaDNBnuhQyXI6zvzn-dAg8SxJSP3mQEx5FTSmJbYog';
-// const mockTokenProjectAdmin =
-// 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIzIiwibmFtZSI6IlVzZXIuUHJvamVjdEFkbWluIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjIwMDAwMDAwMDAsInJvbGVzIjpbXX0.P2Cb6s3Kq0hHsfXEczFcUvpQuR8TTV88U4RDvcPabMM';
-// const mockTokenProjectMember =
-// 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0IiwibmFtZSI6IlVzZXIuUHJvamVjdE1lbWJlciIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoyMDAwMDAwMDAwLCJyb2xlcyI6W119.ujBvw9bCksuSbXGJreIpdXZcVIHtb8GhgviBTvrO9AQ';
-
-const jwtPaths = [
- '/members',
- '/datalogger',
+const inclusionPaths = [
+ '/instruments',
+ '/instrument_groups',
+ '/collection_groups',
+ '/domains',
+ '/timeseries',
];
+const exactPaths = [
+ '/home',
+ '/projects',
+ '/districts',
+]
+
export default composeBundles(
- createAuthBundle({
- appId: '07f1223f-f208-4b71-aa43-5d5f27cd8ed9',
- redirectOnLogout: '/',
- mock: import.meta.env.DEV ? true : false,
- token: import.meta.env.DEV ? mockTokenApplicationAdmin : null,
- }),
createJwtApiBundle({
root:
import.meta.env.DEV
? 'http://localhost:8080'
: import.meta.env.VITE_API_URL,
- tokenSelector: 'selectAuthTokenRaw',
unless: {
- // GET requests do not include token unless path starts with /my_ or includes any path in the jwtPaths array.
- // Need token to figure out who "me" is
+ // Example Scenario:
+ //
+ // GET requests do not include token unless path is in the nonTokenPaths array.
custom: ({ method, path }) => {
if (method === 'GET') {
- if (path.slice(0, 4) === '/my_' || jwtPaths.some(el => path.includes(el))) {
- return false;
+ if (inclusionPaths.some(el => path.includes(el)) || exactPaths.some(el => path === el)) {
+ return true;
}
- return true;
}
return false;
},
@@ -160,6 +152,7 @@ export default composeBundles(
projectsBundle,
qualityControl,
rainfallBundle,
+ reportConfigurationsBundle,
routesBundle,
submittalsBundle,
timeseriesBundle,
diff --git a/src/app-bundles/instrument-bundle.js b/src/app-bundles/instrument-bundle.js
index 525cdec5..155d71d2 100644
--- a/src/app-bundles/instrument-bundle.js
+++ b/src/app-bundles/instrument-bundle.js
@@ -30,7 +30,7 @@ export default createRestBundle({
],
urlParamSelectors: ['selectProjectsIdByRoute'],
prefetch: (store) => {
- const hash = store.selectHash();
+ const hash = store.selectHashStripQuery();
const pathname = store.selectRelativePathname();
const whiteList = [
'dashboard',
@@ -153,10 +153,19 @@ export default createRestBundle({
properties: { ...rest },
};
- const inStatusFilter = statusFilters.length ? statusFilters.includes(status) : true;
+ const inStatusFilter = () => {
+ if (statusFilters.length) {
+ if (statusFilters.includes('inactive')) {
+ return [...statusFilters, 'lost', 'destroyed', 'abandoned'].includes(status);
+
+ } else {
+ return statusFilters.includes(status);
+ }
+ } else return true;
+ };
const inTypeFilter = typeFilters.length ? typeFilters.includes(type_id) : true;
- return (inStatusFilter && inTypeFilter)
+ return (inStatusFilter() && inTypeFilter)
? feature
: null;
}).filter(e => e),
diff --git a/src/app-bundles/instrument-group-bundle.js b/src/app-bundles/instrument-group-bundle.js
index 15b7d132..cc5e0008 100644
--- a/src/app-bundles/instrument-group-bundle.js
+++ b/src/app-bundles/instrument-group-bundle.js
@@ -19,7 +19,7 @@ export default createRestBundle({
],
urlParamSelectors: ['selectProjectsIdByRoute'],
prefetch: (store) => {
- const hash = store.selectHash();
+ const hash = store.selectHashStripQuery();
const pathname = store.selectRelativePathname();
const whiteList = ['dashboard', 'explorer'];
diff --git a/src/app-bundles/instrument-sensors-bundle.js b/src/app-bundles/instrument-sensors-bundle.js
index 3efed25a..7e397648 100644
--- a/src/app-bundles/instrument-sensors-bundle.js
+++ b/src/app-bundles/instrument-sensors-bundle.js
@@ -31,11 +31,12 @@ export default {
selectInstrumentSensorsMeasurements: (state) => state.instrumentSensors.measurements,
selectInstrumentSensorsLastFetched: (state) => state.instrumentSensors._lastFetched,
- doFetchInstrumentSensorsById: (type) => ({ dispatch, store, apiGet }) => {
+ doFetchInstrumentSensorsById: (type, id = null) => ({ dispatch, store, apiGet }) => {
dispatch({ type: 'INSTRUMENT_SENSORS_BY_ID_FETCH_START' });
- const { instrumentId } = store.selectInstrumentsIdByRoute();
+ const { instrumentId } = store.selectInstrumentsIdByRoute() || {};
+ const uriId = id || instrumentId;
- const url = `/instruments/${type}/${instrumentId}/segments`;
+ const url = `/instruments/${type}/${uriId}/segments`;
apiGet(url, (err, body) => {
if (err) {
@@ -52,11 +53,13 @@ export default {
});
},
- doUpdateInstrumentSensor: (type, formData) => ({ dispatch, store, apiPut }) => {
+ doUpdateInstrumentSensor: (type, formData, id = null) => ({ dispatch, store, apiPut }) => {
dispatch({ type: 'INSTRUMENT_SENSOR_UPDATE_START' });
- const { instrumentId } = store.selectInstrumentsIdByRoute();
- const url = `/instruments/${type}/${instrumentId}/segments`;
+ const { instrumentId } = store.selectInstrumentsIdByRoute() || {};
+ const uriId = id || instrumentId;
+
+ const url = `/instruments/${type}/${uriId}/segments`;
apiPut(url, formData, (err, _body) => {
if (err) {
@@ -68,10 +71,12 @@ export default {
});
},
- doFetchInstrumentSensorMeasurements: (type, before, after) => ({ dispatch, store, apiGet }) => {
+ doFetchInstrumentSensorMeasurements: (type, before, after, id = null) => ({ dispatch, store, apiGet }) => {
dispatch({ type: 'SENSOR_MEASUREMENTS_FETCH_START' });
- const { instrumentId } = store.selectInstrumentsIdByRoute();
- const url = `/instruments/${type}/${instrumentId}/measurements?before=${before}&after=${after}`;
+ const { instrumentId } = store.selectInstrumentsIdByRoute() || {};
+ const uriId = id || instrumentId;
+
+ const url = `/instruments/${type}/${uriId}/measurements?before=${before}&after=${after}`;
apiGet(url, (err, body) => {
if (err) {
diff --git a/src/app-bundles/profile-bundle.js b/src/app-bundles/profile-bundle.js
index 06f12ca7..1415cf63 100644
--- a/src/app-bundles/profile-bundle.js
+++ b/src/app-bundles/profile-bundle.js
@@ -1,3 +1,4 @@
+import { isLoggedIn } from '../userService.ts';
import createRestBundle from './create-rest-bundle';
import { createSelector } from 'redux-bundler';
@@ -12,8 +13,8 @@ export default createRestBundle({
putTemplate: '/profiles/{:item.slug}',
postTemplate: '/profiles',
deleteTemplate: '',
- fetchActions: ['AUTH_LOGGED_IN'],
- forceFetchActions: ['PROFILE_SAVE_FINISHED'],
+ fetchActions: ['URL_UPDATED'],
+ forceFetchActions: ['PROFILE_SAVE_FINISHED', '@@INIT'],
reduceFurther: (state, { type, payload }) => {
switch (type) {
case 'PROFILE_REMOVE_PROFILE':
@@ -72,32 +73,15 @@ export default createRestBundle({
}, {});
}),
reactProfileExists: createSelector(
- 'selectAuthIsLoggedIn',
- 'selectRelativePathname',
- 'selectProfileIsLoading',
+ 'selectProfileFlags',
'selectProfileActive',
- (isLoggedIn, path, profileIsLoading, profile) => {
- if (isLoggedIn && !profileIsLoading) {
- if (!profile) {
- if (path !== '/signup')
- return {
- actionCreator: 'doUpdateRelativeUrl',
- args: ['/signup'],
- };
- }
- }
- }
- ),
- reactProfileCreatedRedirect: createSelector(
- 'selectProfileActive',
- 'selectAuthIsLoggedIn',
- 'selectRelativePathname',
- (profile, isLoggedIn, path) => {
- if (path === '/signup' && (profile || !isLoggedIn))
+ (flags, profile) => {
+ if (isLoggedIn() && !flags._isLoading && !profile) {
return {
- actionCreator: 'doUpdateRelativeUrl',
- args: ['/'],
+ actionCreator: 'doProfileSave',
+ args: [{}],
};
+ }
}
),
},
diff --git a/src/app-bundles/project-members-bundle.js b/src/app-bundles/project-members-bundle.js
index dc3535da..4bb52a07 100644
--- a/src/app-bundles/project-members-bundle.js
+++ b/src/app-bundles/project-members-bundle.js
@@ -15,7 +15,7 @@ export default createRestBundle({
'PROJECTMEMBERS_DELETE_USER_FINISHED',
],
prefetch: (store) => {
- const hash = store.selectHash();
+ const hash = store.selectHashStripQuery();
const whiteList = ['admin'];
return whiteList.includes(hash);
diff --git a/src/app-bundles/projects-bundle.js b/src/app-bundles/projects-bundle.js
index 12babdf8..98248236 100644
--- a/src/app-bundles/projects-bundle.js
+++ b/src/app-bundles/projects-bundle.js
@@ -12,7 +12,7 @@ export default createRestBundle({
putTemplate: '/projects/{:item.id}',
postTemplate: '/projects',
deleteTemplate: '/projects/{:item.id}',
- fetchActions: ['URL_UPDATED', 'AUTH_LOGGED_IN'],
+ fetchActions: ['URL_UPDATED'],
forceFetchActions: ['PROJECTS_SAVE_FINISHED', 'PROJECTS_DELETE_FINISHED'],
reduceFurther: (state, { type, payload }) => {
switch (type) {
diff --git a/src/app-bundles/rainfall-bundle.js b/src/app-bundles/rainfall-bundle.js
index ec27b2c2..cda09021 100644
--- a/src/app-bundles/rainfall-bundle.js
+++ b/src/app-bundles/rainfall-bundle.js
@@ -59,7 +59,7 @@ const rainfallBundle = {
x: [],
y: [],
xaxis: 'x',
- yaxis: 'y2',
+ yaxis: 'y3',
line: {
color: '#0062ff',
},
diff --git a/src/app-bundles/report-configurations-bundle.js b/src/app-bundles/report-configurations-bundle.js
new file mode 100644
index 00000000..f5d6bcb2
--- /dev/null
+++ b/src/app-bundles/report-configurations-bundle.js
@@ -0,0 +1,95 @@
+import { toast } from 'react-toastify';
+
+import { tUpdateError, tUpdateSuccess } from '../common/helpers/toast-helpers';
+
+export default {
+ name: 'reportConfigurations',
+ getReducer: () => {
+ const initialState = {
+ projectConfigs: [],
+ };
+
+ return (state = initialState, { type, payload }) => {
+ switch (type) {
+ case 'UPDATE_REPORT_CONFIGURATIONS':
+ return {
+ ...state,
+ projectConfigs: payload,
+ };
+ default:
+ return state;
+ }
+ };
+ },
+
+ selectReportConfigurationsRaw: state => state.reportConfigurations,
+ selectProjectReportConfigurations: state => state.reportConfigurations.projectConfigs,
+ selectProjectReportConfigurationsDownloads: state => state.reportConfigurations.downloads,
+
+ doFetchReportConfigurationsByProjectId: () => ({ dispatch, store, apiGet }) => {
+ const projectId = store.selectProjectsIdByRoute()?.projectId;
+ const uri = `/projects/${projectId}/report_configs`;
+
+ apiGet(uri, (err, body) => {
+ if (err) {
+ // eslint-disable-next-line no-console
+ console.log('todo', err);
+ } else {
+ dispatch({
+ type: 'UPDATE_REPORT_CONFIGURATIONS',
+ payload: body,
+ });
+ }
+ });
+ },
+
+ doCreateNewReportConfiguration: (data) => ({ store, apiPost }) => {
+ const toastId = toast.loading('Creating Report Configuration...');
+ const projectId = store.selectProjectsIdByRoute()?.projectId;
+ const uri = `/projects/${projectId}/report_configs`;
+
+ const body = {
+ ...data,
+ project_id: projectId,
+ };
+
+ apiPost(uri, body, (err, _body) => {
+ if (err) {
+ tUpdateError(toastId, "Failed to create Report Configuration");
+ } else {
+ tUpdateSuccess(toastId, "Successfully created Report Configuration");
+ store.doFetchReportConfigurationsByProjectId();
+ }
+ });
+ },
+
+ doUpdateReportConfiguration: (data) => ({ store, apiPut }) => {
+ const toastId = toast.loading('Updating Report Configuration...');
+ const projectId = store.selectProjectsIdByRoute()?.projectId;
+ const uri = `/projects/${projectId}/report_configs/${data?.id}`;
+
+ apiPut(uri, data, (err, _body) => {
+ if (err) {
+ tUpdateError(toastId, "Failed to update Report Configuration");
+ } else {
+ tUpdateSuccess(toastId, "Successfully updated Report Configuration");
+ store.doFetchReportConfigurationsByProjectId();
+ }
+ });
+ },
+
+ doDeleteReportConfiguration: ({ reportConfigurationId }) => ({ store, apiDelete }) => {
+ const projectId = store.selectProjectsIdByRoute()?.projectId;
+
+ const uri = `/projects/${projectId}/report_configs/${reportConfigurationId}`;
+
+ apiDelete(uri, (err, _body) => {
+ if (err) {
+ // eslint-disable-next-line no-console
+ console.log('todo', err);
+ } else {
+ store.doFetchReportConfigurationsByProjectId();
+ }
+ });
+ },
+};
diff --git a/src/app-bundles/routes-bundle.js b/src/app-bundles/routes-bundle.js
index b305c482..25b41992 100644
--- a/src/app-bundles/routes-bundle.js
+++ b/src/app-bundles/routes-bundle.js
@@ -6,11 +6,9 @@ import InstrumentGroup from '../app-pages/instrument-group';
import Help from '../app-pages/help/help';
import Home from '../app-pages/home/home';
import Instrument from '../app-pages/instrument/details';
-import Logout from '../app-pages/logout';
import NotFound from '../app-pages/404';
import Profile from '../app-pages/profile/userProfile';
import Project from '../app-pages/project';
-import SignUp from '../app-pages/signup/signup';
const base = import.meta.env.VITE_URL_BASE_PATH ?? ''
@@ -20,8 +18,6 @@ export default createRouteBundle(
[`${base}/`]: Home,
[`${base}/admin`]: AdminPage,
[`${base}/help`]: Help,
- [`${base}/logout`]: Logout,
- [`${base}/signup`]: SignUp,
[`${base}/profile`]: Profile,
[`${base}/not-found`]: NotFound,
[`${base}/:projectSlug`]: Project,
diff --git a/src/app-bundles/time-series-measurements-bundle.js b/src/app-bundles/time-series-measurements-bundle.js
index eea2c441..0ad16eef 100644
--- a/src/app-bundles/time-series-measurements-bundle.js
+++ b/src/app-bundles/time-series-measurements-bundle.js
@@ -27,7 +27,7 @@ export default createRestBundle({
],
mergeItems: true,
prefetch: (store) => {
- const hash = store.selectHash();
+ const hash = store.selectHashStripQuery();
const pathname = store.selectRelativePathname();
const whitelist = [];
@@ -44,8 +44,8 @@ export default createRestBundle({
dispatch({ type: 'TIMESERIES_FETCH_BY_ID_START', payload: {} });
const [after, before] = dateRange;
- const isoAfter = after ? after.toISOString() : afterDate;
- const isoBefore = before ? before.toISOString() : beforeDate;
+ const isoAfter = after ? new Date(after)?.toISOString() : afterDate;
+ const isoBefore = before ? new Date(before)?.toISOString() : beforeDate;
const url = `/timeseries/${timeseriesId}/measurements?after=${isoAfter}&before=${isoBefore}&threshold=${threshold}`;
const flags = store['selectTimeseriesMeasurementsFlags']();
diff --git a/src/app-bundles/upload-bundle.js b/src/app-bundles/upload-bundle.js
index e50507f3..96291525 100644
--- a/src/app-bundles/upload-bundle.js
+++ b/src/app-bundles/upload-bundle.js
@@ -396,7 +396,7 @@ const uploadBundle = {
: setAllTo[1];
} else {
// If field not mapped, set to null; if required field, push error
- const data = row[sourceKey] || fieldMap[key];
+ const data = row[sourceKey];
if (!data) {
parsedRow[key] = null;
if (config.required) parsedRow.errors.push(key);
diff --git a/src/app-components/chart/chart.jsx b/src/app-components/chart/chart.jsx
index f80e0b7c..a2cbad0a 100644
--- a/src/app-components/chart/chart.jsx
+++ b/src/app-components/chart/chart.jsx
@@ -76,8 +76,6 @@ const Chart = ({
style={{ width: '100%', height: '100%' }}
onInitialized={updateState}
onUpdate={updateState}
- // eslint-disable-next-line no-console
- onClick={(e) => console.log(e)}
/>
);
diff --git a/src/app-components/chart/minify-plotly.js b/src/app-components/chart/minify-plotly.js
index 4d88ab1e..436fe61a 100644
--- a/src/app-components/chart/minify-plotly.js
+++ b/src/app-components/chart/minify-plotly.js
@@ -3,6 +3,7 @@ import * as Bar from 'plotly.js/lib/bar';
import * as Pie from 'plotly.js/lib/pie';
import * as Surface from 'plotly.js/lib/surface';
import * as Scatter3D from 'plotly.js/lib/scatter3d';
+import * as Contour from 'plotly.js/lib/contour';
Plotly.register([
/*
@@ -11,6 +12,7 @@ Plotly.register([
List of available imports can be found here `node_modules/plotly.js/lib/index.js`
*/
Bar,
+ Contour,
Pie,
Surface,
Scatter3D,
diff --git a/src/app-components/domain-select.jsx b/src/app-components/domain-select.jsx
index 6af4b462..c04b2da6 100644
--- a/src/app-components/domain-select.jsx
+++ b/src/app-components/domain-select.jsx
@@ -10,13 +10,15 @@ export default connect(
useLabelAsDefault = false,
onChange,
domain,
+ label = '',
+ ...customProps
}) => {
const options = domainsItemsByGroup[domain]?.map(item => (
{ value: item.id, label: item.value }
)) || [];
return (
- <>
+
{!options || !options.length ? (
No Options...
) : (
@@ -28,12 +30,12 @@ export default connect(
const item = domainsItemsByGroup[domain]?.find(el => el.value === value?.label);
onChange(item);
}}
- renderInput={(params) => }
+ renderInput={(params) => }
options={options}
fullWidth
/>
)}
- >
+
);
}
);
diff --git a/src/app-components/hero/hero.jsx b/src/app-components/hero/hero.jsx
index e2c231c2..36b6bfe1 100644
--- a/src/app-components/hero/hero.jsx
+++ b/src/app-components/hero/hero.jsx
@@ -4,6 +4,7 @@ import { Timeline } from '@mui/icons-material';
import bg1 from '../../img/bg-1.jpg';
import cwbiLogo from '../../img/cwbi-logo.png';
import ggmLogo from '../../img/GGM-Logo.png';
+import { isLoggedIn } from '../../userService.ts';
import './hero.scss';
@@ -16,11 +17,29 @@ const Hero = () => (
window.open('https://cwbi.usace.army.mil/', '_blank')}
/>
+
+ UI Release Notes
+
+
+ API Release Notes
+
@@ -30,6 +49,9 @@ const Hero = () => (
Monitoring Instrumentation Data Acquisition System
+ {!isLoggedIn() &&
+ We've migrated to login.gov! Please sign in with an account associated with your government issued employee id (PIV/CAC). Your MIDAS profile will be linked upon login.
+
}
);
diff --git a/src/app-components/hero/hero.scss b/src/app-components/hero/hero.scss
index c5f6213b..2379690a 100644
--- a/src/app-components/hero/hero.scss
+++ b/src/app-components/hero/hero.scss
@@ -7,8 +7,9 @@
justify-content: space-between;
.logo-left {
- margin-top: 95px;
- margin-left: 15px;
+ margin-top: 108px;
+ margin-left: 35px;
+ text-align: center;
}
.logo-right {
@@ -16,4 +17,5 @@
margin-right: 25px;
text-align: center;
}
-}
\ No newline at end of file
+}
+
diff --git a/src/app-components/navigation/navBar.jsx b/src/app-components/navigation/navBar.jsx
index 6cd0e0b7..437ccc78 100644
--- a/src/app-components/navigation/navBar.jsx
+++ b/src/app-components/navigation/navBar.jsx
@@ -1,11 +1,14 @@
import React, { useState, useEffect } from 'react';
import { connect } from 'redux-bundler-react';
+import { AppBar, Box, Toolbar, Typography } from '@mui/material';
import { ChevronRight, Timeline } from '@mui/icons-material';
import DevBanner from './devBanner';
import NavItem from './navItem';
import ProfileMenu from './profileMenu';
-import { classArray } from '../../common/helpers/utils';
+import { useWindowSize } from 'react-use';
+
+import { doLogin, isLoggedIn } from '../../userService.ts';
import './navigation.scss';
@@ -18,95 +21,88 @@ const customTheme = {
theme: 'transparent',
hideBrand: true,
},
- '/signup': {
- brand: 'Home',
- },
};
const NavBar = connect(
- 'doAuthLogin',
- 'selectAuthIsLoggedIn',
'selectProjectsByRoute',
'selectRelativePathname',
({
- doAuthLogin,
- authIsLoggedIn,
projectsByRoute: project,
relativePathname: pathname,
}) => {
const [hideBrand, setHideBrand] = useState(false);
- const [brand, setBrand] = useState(null);
+ const [hideProject, setHideProject] = useState(false);
+ const [brand, setBrand] = useState('MIDAS');
const [theme, setTheme] = useState('primary');
+ const { width } = useWindowSize();
- const showDevBanner = import.meta.env.VITE_DEVELOPMENT_BANNER === 'true';
-
- const navClass = classArray([
- 'navbar',
- 'navbar-expand-lg',
- 'navbar-dark',
- theme !== 'transparent' ?
- showDevBanner ? 'fixed-top-banner' : 'fixed-top'
- : showDevBanner ? 'transparent-nav-banner' : '',
- `bg-${theme}`,
- ]);
+ const isTransparent = theme === 'transparent';
+ const showDevBanner = import.meta.env.VITE_DEVELOPMENT_BANNER === 'true';
useEffect(() => {
const { hideBrand, brand, theme } = customTheme[pathname] || {};
setHideBrand(hideBrand);
- setBrand(brand);
+ setBrand(brand || 'MIDAS');
setTheme(theme || 'primary');
}, [pathname]);
+ useEffect(() => {
+ if (width < 750) {
+ setHideProject(true);
+ } else {
+ setHideProject(false);
+ }
+ });
+
return (
<>
{showDevBanner && }
-
- {hideBrand ? null : (
-
-
-
-
- {brand || 'MIDAS'}
-
-
- {project && project.name && (
+
+
+
+ {!hideBrand && (
<>
-
-
- {project.name}
+
+
+ {brand}
+ {project && project.name && !hideProject && (
+ <>
+
+
+ {project.name}
+
+ >
+ )}
>
)}
-
- )}
-
-
-
-
-
-
-
+
+
{pathname === '/help' ? (
Home
) : (
Help
)}
-
- {authIsLoggedIn ? (
+
+ {isLoggedIn() ? (
) : (
- Login
+ Login
)}
-
-
-
-
+
+
+
+
>
);
}
diff --git a/src/app-components/navigation/navItem.jsx b/src/app-components/navigation/navItem.jsx
index 79434cd6..385a6c45 100644
--- a/src/app-components/navigation/navItem.jsx
+++ b/src/app-components/navigation/navItem.jsx
@@ -9,6 +9,7 @@ const NavItem = connect(
const cls = classArray([
'pointer',
'nav-item',
+ 'd-inline-block',
pathname.indexOf(href) !== -1 && href !== '/' && 'active',
]);
@@ -19,11 +20,11 @@ const NavItem = connect(
return !hidden ?
handler ? (
- {children}
+ {children}
) : (
-
+
{children}
diff --git a/src/app-components/navigation/navigation.scss b/src/app-components/navigation/navigation.scss
index 51f9f956..f54faaff 100644
--- a/src/app-components/navigation/navigation.scss
+++ b/src/app-components/navigation/navigation.scss
@@ -28,10 +28,7 @@
margin-top: -10px;
padding: 0 15px 5px 15px;
position: relative;
-
- // > {
- // flex: 1;
- // }
+ max-width: 100vw;
.secondary-nav-heading {
border-bottom: 1px solid #d6d7d8;
@@ -40,13 +37,13 @@
padding: 0 10px 4.5px;
position: absolute;
top: 0;
- width: 100vw;
+ min-width: 100vw;
+ max-width: 100vw;
z-index: -1;
}
.secondary-nav-page-content {
left: 0px;
- overflow: visible;
padding: 10px;
position: absolute;
top: 30px;
diff --git a/src/app-components/navigation/profileMenu.jsx b/src/app-components/navigation/profileMenu.jsx
index f2a61bcc..f32801ad 100644
--- a/src/app-components/navigation/profileMenu.jsx
+++ b/src/app-components/navigation/profileMenu.jsx
@@ -1,46 +1,65 @@
-import React from 'react';
+import React, { useState } from 'react';
import { connect } from 'redux-bundler-react';
+import { Box, Divider, Menu, MenuItem, Tooltip } from '@mui/material';
-import Dropdown from '../dropdown';
import RoleFilter from '../role-filter';
-
-const getInitials = (name = '') => {
- let initials = ['U', 'N'];
- let parts = name.split('.');
- if (parts[1] && parts[1][0]) initials[0] = parts[1][0];
- if (parts[0] && parts[0][0]) initials[1] = parts[0][0];
- return initials.join('');
-};
+import { doLogout, getInitials, getUsername } from '../../userService.ts';
const ProfileMenu = connect(
- 'selectAuthTokenPayload',
- ({ authTokenPayload: user }) => (
-
- )
+ {/* SITE ADMIN ONLY, NO Allowed Roles */}
+
+ navigateTo('/admin')}>Site Administration
+
+
+ {/* ALL Roles (Logged In) */}
+ navigateTo('/profile')}>My Profile
+ doLogout()} className='d-flex flex-column align-items-start'>
+ Logout
+ Currently logged in as {getUsername()}
+
+
+
+ );
+ },
);
export default ProfileMenu;
diff --git a/src/app-components/navigation/secondaryNavBar.jsx b/src/app-components/navigation/secondaryNavBar.jsx
index 563b811b..7cfc9e36 100644
--- a/src/app-components/navigation/secondaryNavBar.jsx
+++ b/src/app-components/navigation/secondaryNavBar.jsx
@@ -1,7 +1,25 @@
import React, { useEffect, useState } from 'react';
import { connect } from 'redux-bundler-react';
+import { Box, Tab, Tabs, tabsClasses } from '@mui/material';
-import TabContainer from '../tab/tabContainer';
+const CustomTabPanel = props => {
+ const { children, value, active, paddingTop } = props;
+
+ return (
+
+ {value === active && (
+
+ {children}
+
+ )}
+
+ );
+}
const SecondaryNavBar = connect(
'selectHashStripQuery',
@@ -9,43 +27,49 @@ const SecondaryNavBar = connect(
hashStripQuery,
navLinks = [],
}) => {
- const getIndex = () => navLinks.findIndex(elem => elem.uri == `#${hashStripQuery}`);
-
- const defaultTab = hashStripQuery ? getIndex() : () => {
+ const defaultTab = hashStripQuery ? `#${hashStripQuery}` : () => {
location.hash = navLinks[0].uri;
- return 0;
+ return navLinks[0].uri;
};
const [navTab, setNavTab] = useState(defaultTab);
- const [forceUpdateIncrement, setForceUpdateIncrement] = useState(0);
- const onTabChange = (_, index) => {
- location.hash = navLinks[index].uri;
- setNavTab(index);
+ const onTabChange = (_, newHash) => {
+ location.hash = newHash;
+ setNavTab(newHash);
};
+ // Handle External Hash Manipulation (eg: navbar)
useEffect(() => {
- if (navLinks[navTab]) {
- if (`#${hashStripQuery}` !== navLinks[navTab].uri) {
- setForceUpdateIncrement(forceUpdateIncrement + 1);
- }
- } else {
- location.assign('/not-found');
+ if (navTab !== `#${hashStripQuery}`) {
+ setNavTab(`#${hashStripQuery}`);
}
- }, [navLinks, hashStripQuery, navTab, setForceUpdateIncrement]);
+ }, [navTab, hashStripQuery, setNavTab]);
return (
-
+ <>
+
+
+ {navLinks.map(link => (
+
+ ))}
+
+
+ {navLinks.map(link => (
+
+ {link.content}
+
+ ))}
+ >
);
}
);
diff --git a/src/app-components/pageContent.jsx b/src/app-components/pageContent.jsx
index d74c32a9..04bf6676 100644
--- a/src/app-components/pageContent.jsx
+++ b/src/app-components/pageContent.jsx
@@ -4,6 +4,27 @@ import { connect } from 'redux-bundler-react';
import { classArray } from '../common/helpers/utils';
const hasDevBanner = import.meta.env.VITE_DEVELOPMENT_BANNER === 'true';
+// const blacklist = ['/', '/help'];
+
+// const PageContent = connect(
+// 'selectRelativePathname',
+// ({
+// relativePathname: pathname,
+// children,
+// }) => {
+// const pageClasses = classArray([
+// 'page-margin',
+// hasDevBanner && 'banner',
+// ]);
+
+// return (
+//
+// {children}
+//
+// );
+// }
+// );
+
const blacklist = ['/', '/help'];
const PageContent = connect(
diff --git a/src/app-components/profile-form.jsx b/src/app-components/profile-form.jsx
deleted file mode 100644
index c818607b..00000000
--- a/src/app-components/profile-form.jsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import React, {
- useState,
- forwardRef,
- useImperativeHandle,
- useEffect,
-} from 'react';
-
-export default forwardRef(({ item, onSave }, ref) => {
- const [username, setUsername] = useState((item && item.userName) || '');
- const [email, setEmail] = useState((item && item.email) || '');
- const [errors, setErrors] = useState(null);
-
- useEffect(() => {
- if (!username || !email) {
- setErrors('Username and Email are requred');
- } else {
- setErrors(null);
- }
- }, [username, email, setErrors]);
-
- const handleSave = () => {
- if (!errors) {
- onSave({
- id: (item && item.id) || null,
- username,
- email,
- });
- }
- };
-
- useImperativeHandle(ref, () => ({
- save: () => {
- handleSave();
- },
- }));
-
- return (
-
- );
-});
diff --git a/src/app-components/tab/tab.scss b/src/app-components/tab/tab.scss
index ba12b57b..c6fc80f5 100644
--- a/src/app-components/tab/tab.scss
+++ b/src/app-components/tab/tab.scss
@@ -1,4 +1,7 @@
.nav-item {
+ display: table-cell;
+ min-width: fit-content;
+ white-space: nowrap;
user-select: none;
&.active {
@@ -12,7 +15,9 @@
}
.nav-tabs {
+ display: table-row;
border-bottom: 1px solid #c8cacb !important;
+ overflow: hidden !important;
.nav-link.active {
border-color: #c8cacb #c8cacb #fff !important;
diff --git a/src/app-components/tab/tabContainer.jsx b/src/app-components/tab/tabContainer.jsx
index 517df11e..f0a1cf83 100644
--- a/src/app-components/tab/tabContainer.jsx
+++ b/src/app-components/tab/tabContainer.jsx
@@ -1,6 +1,8 @@
-import React, { useEffect, useState } from 'react';
+import React, { createRef, useEffect, useState } from 'react';
+import { useDeepCompareEffect } from 'react-use';
import TabItem from './tabItem';
+import useWindowDimensions from '../../customHooks/useWindowDimensions';
import './tab.scss';
@@ -24,8 +26,11 @@ const TabContainer = ({
changeTabDelay = 0,
...customProps
}) => {
+ const ref = createRef();
+ const [displayedTabs, setDisplayedTabs] = useState(tabs);
const [tabIndex, setTabIndex] = useState(defaultTab);
const [isDisabled, setIsDisabled] = useState(false);
+ const { width: windowWidth } = useWindowDimensions();
const changeTab = (title, index) => {
onTabChange(title, index);
@@ -39,10 +44,31 @@ const TabContainer = ({
}
}, [isDisabled, setIsDisabled]);
+ useDeepCompareEffect(() => {
+ const diff = tabs.length - displayedTabs.length;
+
+ if (ref.current.clientWidth > windowWidth) {
+ const clone = [...tabs];
+
+ clone.splice((tabs.length - diff) - 2, diff + 2);
+ setDisplayedTabs(clone);
+ } else {
+ if (diff && ((ref.current.clientWidth + 165) < windowWidth)) {
+ const clone = [...tabs];
+
+ if (diff !== 1) {
+ clone.splice(tabs.length - (diff - 1), diff - 1);
+ }
+
+ setDisplayedTabs(clone);
+ }
+ }
+ }, [ref, windowWidth, tabs, displayedTabs]);
+
return (
-
-
- {tabs.map((t, i) => (
+
+
+ {displayedTabs.map((t, i) => (
column.header,
- cell: val => column.render ? column.render(val?.row?.original) : val.getValue(),
+ cell: val => column.render ? column.render(val?.row?.original, val.getValue()) : val.getValue(),
enableSorting: !!column.isSortable,
enableColumnFilter: !!column.isFilterable,
})
diff --git a/src/app-pages/explorer/map-legend.jsx b/src/app-pages/explorer/map-legend.jsx
index 9756896b..d71116bf 100644
--- a/src/app-pages/explorer/map-legend.jsx
+++ b/src/app-pages/explorer/map-legend.jsx
@@ -7,7 +7,13 @@ import { IconButton } from '@mui/material';
import Card from '../../app-components/card';
import { classArray } from '../../common/helpers/utils';
-const STATUS_ORDER = ['Active', 'Lost', 'Inactive', 'Abandoned', 'Destroyed'];
+const STATUS_ORDER = [
+ 'Active',
+ // 'Lost',
+ 'Inactive',
+ // 'Abandoned',
+ // 'Destroyed',
+];
const getLegendItemClasses = (allItems = [], currentItem) => {
const isEmpty = allItems.length === 0;
@@ -57,7 +63,7 @@ export default connect(
value: s.value,
order: STATUS_ORDER.indexOf(title),
};
- }).sort((a, b) => (a.order > b.order ? 1 : -1));
+ }).filter(el => el.order !== -1).sort((a, b) => (a.order > b.order ? 1 : -1));
const currentTypes = instrument_type.filter(type => instruments.some(i => i.type_id === type.id));
diff --git a/src/app-pages/help/apiHelp.jsx b/src/app-pages/help/apiHelp.jsx
index 5b83cc77..60a04a8c 100644
--- a/src/app-pages/help/apiHelp.jsx
+++ b/src/app-pages/help/apiHelp.jsx
@@ -18,8 +18,8 @@ const ApiHelp = () => (
diff --git a/src/app-pages/home/project-list.jsx b/src/app-pages/home/project-list.jsx
index 87c02b85..ab921620 100644
--- a/src/app-pages/home/project-list.jsx
+++ b/src/app-pages/home/project-list.jsx
@@ -80,50 +80,48 @@ export default connect(
const [inputString, setInputString] = useState('');
const columnHelper = createColumnHelper();
- const columns = useMemo(
- () => [
- columnHelper.accessor('districtId', {
- header: 'District',
- id: 'districtId',
- enableColumnFilter: false,
- sortingFn: (a, b) => sortByDistrictName(a, b, districts),
- cell: (info) => (
- <>
- {info.row.getCanExpand() && (
-
- {info.row.getIsExpanded() ? (
-
- ) : (
-
- )}
-
- )}
- {mapDistrictName(info.getValue(), info.row.subRows.length, districts)}
- >
- ),
- }),
- columnHelper.accessor('title', {
- header: 'Project Name',
- id: 'title',
- enableColumnFilter: false,
- cell: (info) => (
- {info.getValue()}
- ),
- }),
- columnHelper.accessor('instrumentCount', {
- header: 'Instrument Count',
- id: 'instrumentCount',
- enableColumnFilter: false,
- enableSorting: false,
- }),
- columnHelper.accessor('instrumentGroupCount', {
- header: 'Instrument Group Count',
- id: 'instrumentGroupCount',
- enableColumnFilter: false,
- enableSorting: false,
- }),
- ],
- [districts]);
+ const columns = useMemo(() => [
+ columnHelper.accessor('districtId', {
+ header: 'District',
+ id: 'districtId',
+ enableColumnFilter: false,
+ sortingFn: (a, b) => sortByDistrictName(a, b, districts),
+ cell: (info) => (
+ <>
+ {info.row.getCanExpand() && (
+
+ {info.row.getIsExpanded() ? (
+
+ ) : (
+
+ )}
+
+ )}
+ {mapDistrictName(info.getValue(), info.row.subRows.length, districts)}
+ >
+ ),
+ }),
+ columnHelper.accessor('title', {
+ header: 'Project Name',
+ id: 'title',
+ enableColumnFilter: false,
+ cell: (info) => (
+ {info.getValue()}
+ ),
+ }),
+ columnHelper.accessor('instrumentCount', {
+ header: 'Instrument Count',
+ id: 'instrumentCount',
+ enableColumnFilter: false,
+ enableSorting: false,
+ }),
+ columnHelper.accessor('instrumentGroupCount', {
+ header: 'Instrument Group Count',
+ id: 'instrumentGroupCount',
+ enableColumnFilter: false,
+ enableSorting: false,
+ }),
+ ], [districts]);
const onChange = selected => {
const { value } = selected;
diff --git a/src/app-pages/instrument-group/instrument-group-form.jsx b/src/app-pages/instrument-group/instrument-group-form.jsx
index b5d5f92f..c9c3980f 100644
--- a/src/app-pages/instrument-group/instrument-group-form.jsx
+++ b/src/app-pages/instrument-group/instrument-group-form.jsx
@@ -79,7 +79,7 @@ export default connect(
saveIsSubmit
customClosingLogic
onCancel={() => doModalClose()}
- onDelete={handleDelete}
+ onDelete={item && item.id ? handleDelete : null}
/>
diff --git a/src/app-pages/instrument/cwms-timeseries/cwmsTimeseries.jsx b/src/app-pages/instrument/cwms-timeseries/cwmsTimeseries.jsx
new file mode 100644
index 00000000..0080cfc8
--- /dev/null
+++ b/src/app-pages/instrument/cwms-timeseries/cwmsTimeseries.jsx
@@ -0,0 +1,156 @@
+import React, { useRef, useState } from 'react';
+import ReactDatePicker from 'react-datepicker';
+import { AgGridReact } from '@ag-grid-community/react';
+import { Button } from '@mui/material';
+import { ClientSideRowModelModule } from '@ag-grid-community/client-side-row-model';
+import { connect } from 'redux-bundler-react';
+import { DateTime } from 'luxon';
+import { endOfDay, startOfDay, subDays } from 'date-fns';
+import { Icon } from '@iconify/react';
+
+import CwmsTimeseriesListItem from './cwmsTimeseriesListItem.jsx';
+import NewCwmsTimeseriesModal from './newCwmsTimeseries.jsx';
+import { useGetMidasCwmsTimeseries, useGetCwmsTimeseriesMeasurements } from '../../../app-services/collections/cwms-timeseries.ts';
+
+const CwmsTimeseries = connect(
+ 'doModalOpen',
+ 'selectProjectsIdByRoute',
+ 'selectInstrumentsByRoute',
+ ({
+ doModalOpen,
+ projectsIdByRoute: project,
+ instrumentsByRoute: instrument,
+ }) => {
+ const grid = useRef(null);
+
+ const { projectId } = project || {};
+ const { id } = instrument || {};
+
+ const [activeTimeseries, setActiveTimeseries] = useState(null);
+ const [dateRange, setDateRange] = useState([subDays(startOfDay(new Date()), 1), endOfDay(new Date())]);
+
+ const { data: midasTimeseries } = useGetMidasCwmsTimeseries({ projectId, instrumentId: id });
+ const { data: cwmsMeasurements, isLoading } = useGetCwmsTimeseriesMeasurements(
+ {
+ name: activeTimeseries?.cwms_timeseries_id,
+ office: activeTimeseries?.cwms_office_id,
+ begin: DateTime.fromJSDate(dateRange[0]).toISO(),
+ end: DateTime.fromJSDate(dateRange[1]).toISO(),
+ },
+ { enabled: !!activeTimeseries?.cwms_timeseries_id },
+ );
+
+ const { values = [] } = cwmsMeasurements || {};
+
+ const data = values.map(el => {
+ if (el?.length < 2) return null;
+
+ return {
+ timestamp: DateTime.fromMillis(el[0]).toFormat('D HH:mm:ss'),
+ value: el[1],
+ };
+ }).filter(e => e);
+
+ return (
+ <>
+
+
+ CWMS Timeseries are timeseries from the external source, Corps Water Management System (CWMS) , that are manually associated to instruments within the MIDAS
+ system. Use this panel to manage the connected timeseries and view the data associated with them. While connections can be made, the data it provides is strictly read-only.
+
+
+
+
+ doModalOpen(NewCwmsTimeseriesModal, { projectId, instrumentId: id })}
+ >
+ + New CWMS Timeseries
+
+
+
+ {!!activeTimeseries?.id && (
+
+
+ setDateRange(prev => [startOfDay(date), prev[1]])}
+ />
+
+
+ -
+
+
+ setDateRange(prev => [prev[0], endOfDay(date)])}
+ />
+
+
+ )}
+
+
+
+
+
+ {midasTimeseries?.length ? (
+ <>
+ {midasTimeseries.map(t => (
+ setActiveTimeseries(activeTimeseries?.id === t.id ? null : item)}
+ />
+ ))}
+ >
+ ) : (
+ No Timeseries Configured
+ )}
+
+
+
+ {isLoading ? (
+
+
+
+ ) : (
+
+ )}
+
+
+ >
+ );
+ },
+);
+
+export default CwmsTimeseries;
\ No newline at end of file
diff --git a/src/app-pages/instrument/cwms-timeseries/cwmsTimeseriesListItem.jsx b/src/app-pages/instrument/cwms-timeseries/cwmsTimeseriesListItem.jsx
new file mode 100644
index 00000000..6a5bde47
--- /dev/null
+++ b/src/app-pages/instrument/cwms-timeseries/cwmsTimeseriesListItem.jsx
@@ -0,0 +1,48 @@
+import React, { useRef } from 'react';
+import { connect } from 'redux-bundler-react';
+import { Edit } from '@mui/icons-material';
+
+import RoleFilter from '../../../app-components/role-filter';
+import { classnames } from '../../../common/helpers/utils';
+import NewCwmsTimeseriesModal from './newCwmsTimeseries';
+
+export default connect(
+ 'selectProjectsByRoute',
+ 'doModalOpen',
+ ({ projectsByRoute: project, doModalOpen, item, onClick, active }) => {
+ const li = useRef(null);
+ const itemClass = classnames({
+ 'pointer': true,
+ 'list-group-item': true,
+ active: active,
+ });
+
+ return (
+ item && (
+ {
+ if (e.currentTarget === li.current) onClick(item);
+ }}
+ >
+
+ {
+ doModalOpen(NewCwmsTimeseriesModal, { projectId: project.id, instrumentId: item.instrument_id, item, isEdit: true });
+ e.stopPropagation();
+ }}
+ >
+
+
+
+ {item.name}
+
+ {`${item.parameter} in ${item.unit}`}
+
+
+ )
+ );
+ }
+);
diff --git a/src/app-pages/instrument/cwms-timeseries/newCwmsTimeseries.jsx b/src/app-pages/instrument/cwms-timeseries/newCwmsTimeseries.jsx
new file mode 100644
index 00000000..1c969791
--- /dev/null
+++ b/src/app-pages/instrument/cwms-timeseries/newCwmsTimeseries.jsx
@@ -0,0 +1,152 @@
+import React, { useMemo, useState } from 'react';
+import { Autocomplete, TextField } from '@mui/material';
+import { connect } from 'redux-bundler-react';
+import { useQueryClient } from '@tanstack/react-query';
+
+import * as Modal from '../../../app-components/modal';
+import DomainSelect from '../../../app-components/domain-select.jsx';
+import { useGetCwmsOffices, useGetCwmsTimeseries, usePostMidasCwmsTimeseries, usePutMidasCwmsTimeseries } from '../../../app-services/collections/cwms-timeseries.ts';
+
+const generateOfficesOptions = cwmsOffices => {
+ if (!cwmsOffices?.length) return [];
+
+ return cwmsOffices.map(office => ({
+ label: `${office[`long-name`]} (${office?.name})`,
+ value: office?.name,
+ }));
+};
+
+const generateCwmsTimeseriesOptions = cwmsTimeseries => {
+ const { total, entries } = cwmsTimeseries || {};
+
+ if (!total || !entries) return [];
+
+ return entries.map(entry => ({
+ _extents: entry?.extents?.length ? entry.extents[0] : {},
+ label: entry?.name,
+ value: entry?.name,
+ }));
+};
+
+const NewCwmsTimeseriesModal = connect(
+ 'doModalClose',
+ 'doInstrumentTimeseriesDelete',
+ ({
+ doModalClose,
+ doInstrumentTimeseriesDelete,
+ projectId,
+ instrumentId,
+ item,
+ isEdit,
+ }) => {
+ const client = useQueryClient();
+ const midasCwmsTimeseriesCreator = usePostMidasCwmsTimeseries(client);
+ const midasCwmsTimeseriesUpdater = usePutMidasCwmsTimeseries(client);
+
+ const [selectedOffice, setSelectedOffice] = useState(isEdit ? item?.cwms_office_id : '');
+ const [selectedTimeseries, setSelectedTimeseries] = useState(isEdit ? item?.cwms_timeseries_id : '');
+ const [name, setName] = useState(isEdit ? item?.name : '');
+ const [parameterId, setParameterId] = useState(isEdit ? item?.parameter_id : '');
+ const [unitId, setUnitId] = useState(isEdit ? item?.unit_id : '');
+
+ const { data: cwmsOffices } = useGetCwmsOffices({});
+ const cwmsOfficesOptions = useMemo(() => generateOfficesOptions(cwmsOffices), [cwmsOffices]);
+
+ const { data: cwmsTimeseries } = useGetCwmsTimeseries({ office: selectedOffice?.value || selectedOffice }, {
+ enabled: !!selectedOffice?.value || !!selectedOffice,
+ });
+ const cwmsTimeseriesOptions = useMemo(() => generateCwmsTimeseriesOptions(cwmsTimeseries), [cwmsTimeseries]);
+
+ return (
+
+
+
+ Use the filters below to query for CWMS Timeseries:
+
+ opt.value === val || opt.value === val.value}
+ onChange={(_e, value) => setSelectedOffice(value)}
+ renderInput={(params) => }
+ options={cwmsOfficesOptions}
+ fullWidth
+ />
+ {selectedOffice && (
+ opt.value === val || opt.value === val.value}
+ onChange={(_e, value) => setSelectedTimeseries(value)}
+ renderInput={(params) => }
+ options={cwmsTimeseriesOptions}
+ fullWidth
+ />
+ )}
+ {selectedTimeseries && (
+ <>
+
+ The following fields will be used by MIDAS exclusively and will not change any data in regards to the CWMS timeseries:
+ setName(e.target.value)}
+ />
+ setParameterId(val?.id)}
+ domain='parameter'
+ label='Parameter'
+ />
+ setUnitId(val?.id)}
+ domain='unit'
+ label='Unit'
+ />
+ >
+ )}
+
+ doInstrumentTimeseriesDelete(item, () => {
+ client.invalidateQueries('midasCwmsTimeseries');
+ doModalClose();
+ }, true) : null}
+ saveIsDisabled={!selectedTimeseries || !name || !parameterId || !unitId}
+ saveText={isEdit ? 'Save Changes' : 'Add to Instrument'}
+ onSave={() => {
+ if (isEdit) {
+ midasCwmsTimeseriesUpdater.mutate({ projectId, instrumentId, timeseriesId: item?.id, body: {
+ instrument_id: instrumentId,
+ cwms_office_id: typeof selectedOffice === 'string' ? selectedOffice : selectedOffice.value,
+ cwms_timeseries_id: typeof selectedTimeseries === 'string' ? selectedTimeseries : selectedTimeseries.value,
+ cwms_extent_earliest_time: typeof selectedTimeseries === 'string' ? item?.cwms_extent_earliest_time : selectedTimeseries._extents['earliest-time'],
+ parameter_id: parameterId,
+ unit_id: unitId,
+ name,
+ }})
+ } else {
+ midasCwmsTimeseriesCreator.mutate({ projectId, instrumentId, body: [{
+ instrument_id: instrumentId,
+ cwms_office_id: selectedOffice.value,
+ cwms_timeseries_id: selectedTimeseries.value,
+ cwms_extent_earliest_time: selectedTimeseries._extents['earliest-time'],
+ parameter_id: parameterId,
+ unit_id: unitId,
+ name,
+ }]})
+ }
+ }}
+ />
+
+ );
+ },
+);
+
+export default NewCwmsTimeseriesModal;
diff --git a/src/app-pages/instrument/details.jsx b/src/app-pages/instrument/details.jsx
index 1d216388..6c6582da 100644
--- a/src/app-pages/instrument/details.jsx
+++ b/src/app-pages/instrument/details.jsx
@@ -179,19 +179,23 @@ export default connect(
-
- {isShapeArray && (
-
- )}
- {isIPI && (
-
-
+
+
- )}
+ {isShapeArray && (
+
+ )}
+ {isIPI && (
+
+ )}
+
diff --git a/src/app-pages/instrument/notes.jsx b/src/app-pages/instrument/notes.jsx
index bfeedf5b..40c47f44 100644
--- a/src/app-pages/instrument/notes.jsx
+++ b/src/app-pages/instrument/notes.jsx
@@ -105,22 +105,22 @@ const NoteItem = ({ note, editable, save }) => {
};
export default connect(
+ 'selectProfileActive',
'selectProjectsByRoute',
'selectInstrumentNotesItems',
- 'selectAuthEdipi',
'doInstrumentNotesSave',
'doInstrumentNotesDelete',
({
+ profileActive,
projectsByRoute: project,
instrumentNotesItems: notes,
- authEdipi,
doInstrumentNotesSave: save,
doInstrumentNotesDelete: del,
}) => {
const [isAdding, setIsAdding] = useState(false);
- const edipi = Number(authEdipi);
const sorted = notes.sort();
+ const { id } = profileActive || {};
return (
project && (
@@ -133,7 +133,7 @@ export default connect(
note={note}
save={save}
del={del}
- editable={edipi === note.creator_id}
+ editable={id === note.creator_id}
/>
)) : No Notes Added }
diff --git a/src/app-pages/instrument/settings.jsx b/src/app-pages/instrument/settings.jsx
index 788638e6..9e22d711 100644
--- a/src/app-pages/instrument/settings.jsx
+++ b/src/app-pages/instrument/settings.jsx
@@ -5,6 +5,7 @@ import AlertEditor from './alert/alert-editor';
import Card from '../../app-components/card';
import Chart from './chart/chart';
import Constants from './constants/constants';
+import CwmsTimeseries from './cwms-timeseries/cwmsTimeseries';
import FormulaEditor from './formula/formula';
import TabContainer from '../../app-components/tab';
import Timeseries from './timeseries/timeseries';
@@ -17,12 +18,14 @@ export default connect(
timeseriesMeasurementsItemsObject: measurements,
instrumentsByRoute: instrument,
}) => {
+ const { type, show_cwms_tab } = instrument || {};
+
// const alertsReady = import.meta.env.VITE_ALERT_EDITOR === 'true';
const alertsReady = false;
const forumlaReady = import.meta.env.VITE_FORMULA_EDITOR === 'true';
const chartReady = import.meta.env.VITE_INSTRUMENT_CHART === 'true';
- const isShapeArray = instrument?.type === 'SAA';
- const isIPI = instrument?.type === 'IPI';
+ const isShapeArray = type === 'SAA';
+ const isIPI = type === 'IPI';
const tabs = [
alertsReady && {
@@ -34,6 +37,9 @@ export default connect(
}, {
title: 'Timeseries',
content: ,
+ }, show_cwms_tab && {
+ title: 'CWMS Timeseries',
+ content: ,
}, (isShapeArray || isIPI) && {
title: 'Sensors',
content: ,
@@ -48,7 +54,11 @@ export default connect(
return (
-
+
);
}
diff --git a/src/app-pages/instrument/timeseries/timeseries-list-item.jsx b/src/app-pages/instrument/timeseries/timeseries-list-item.jsx
index 2a98023f..5263ad0d 100644
--- a/src/app-pages/instrument/timeseries/timeseries-list-item.jsx
+++ b/src/app-pages/instrument/timeseries/timeseries-list-item.jsx
@@ -28,8 +28,9 @@ export default connect(
{
+ onClick={e => {
doModalOpen(TimeseriesForm, { item: item, isEdit: true });
+ e.stopPropagation();
}}
>
diff --git a/src/app-pages/instrument/timeseries/timeseries.jsx b/src/app-pages/instrument/timeseries/timeseries.jsx
index 643599a9..78bee170 100644
--- a/src/app-pages/instrument/timeseries/timeseries.jsx
+++ b/src/app-pages/instrument/timeseries/timeseries.jsx
@@ -53,6 +53,8 @@ const getColumnDefs = (measurements, inclinometerMeasurements, activeTimeseries,
: [];
const keys = items && items.length ? Object.keys(items[0]) : [];
+
+ // TODO: SET COLUMN DEF MANAULLY
const columnDefs = [
{
headerName: '',
@@ -106,10 +108,10 @@ export default connect(
const [activeTimeseries, setActiveTimeseries] = useState(null);
const [isInclinometer, setIsInclinometer] = useState(false);
- // filter out any timeseries used for constants
+ // filter out any timeseries used for constants AND cwms timeseries
const actualSeries = timeseries.filter((ts) => (
ts.instrument_id === instrument.id &&
- instrument.constants.indexOf(ts.id) === -1
+ !['constant', 'cwms'].includes(ts.type)
));
const deleteSelectedRows = () => {
diff --git a/src/app-pages/profile/userProfile.css b/src/app-pages/profile/userProfile.css
index a8500ce3..cb155792 100644
--- a/src/app-pages/profile/userProfile.css
+++ b/src/app-pages/profile/userProfile.css
@@ -6,5 +6,5 @@
.limit-height {
max-height: 80vh;
overflow-x: auto;
- margin-bottom: 20px;
+ /* margin-bottom: 20px; */
}
diff --git a/src/app-pages/profile/userProfile.jsx b/src/app-pages/profile/userProfile.jsx
index 54bbaac1..da9c5637 100644
--- a/src/app-pages/profile/userProfile.jsx
+++ b/src/app-pages/profile/userProfile.jsx
@@ -4,6 +4,7 @@ import { formatDistance } from 'date-fns';
import Card from '../../app-components/card';
import TabContainer from '../../app-components/tab';
+import { getEmail, getFullName, getUsername } from '../../userService.ts';
import './userProfile.css';
@@ -57,9 +58,8 @@ const buildAlertContent = (alerts = [], onClick = () => {}) => {
const UserProfile = connect(
'selectProfileAlerts',
- 'selectAuthTokenPayload',
'doUpdateRelativeUrl',
- ({ profileAlerts: alerts, authTokenPayload: user, doUpdateRelativeUrl }) => {
+ ({ profileAlerts: alerts, doUpdateRelativeUrl }) => {
const tabs = [
{
title: 'Projects',
@@ -75,8 +75,12 @@ const UserProfile = connect(
<>
-
-
{user.name}
+
+ Currently logged in as
+
+
Username: {getUsername()}
+
Name: {getFullName()}
+
Email: {getEmail()}
diff --git a/src/app-pages/project/admin/index.jsx b/src/app-pages/project/admin/index.jsx
index 5acb53c9..0c5062b5 100644
--- a/src/app-pages/project/admin/index.jsx
+++ b/src/app-pages/project/admin/index.jsx
@@ -27,7 +27,7 @@ const AdminPage = connect(
return (
-
+
Current Project Members
diff --git a/src/app-pages/project/batch-plotting/batch-plotting.jsx b/src/app-pages/project/batch-plotting/batch-plotting.jsx
index 21eb9900..bce16db5 100644
--- a/src/app-pages/project/batch-plotting/batch-plotting.jsx
+++ b/src/app-pages/project/batch-plotting/batch-plotting.jsx
@@ -1,70 +1,129 @@
-import React from 'react';
+import React, { useState } from 'react';
import { connect } from 'redux-bundler-react';
import { Engineering } from '@mui/icons-material';
+import { Link } from '@mui/material';
+import { toast } from 'react-toastify';
+import { useQueries } from '@tanstack/react-query';
-import BatchPlotChart from './tab-content/batch-plot-chart';
+import BullseyePlot from './chart-content/bullseye-plot.jsx';
import Card from '../../../app-components/card';
+import ContourPlot from './chart-content/contour-plot.jsx';
import DataConfiguration from './components/data-configuration';
-import DepthChart from './tab-content/depth-chart';
import Map from '../../../app-components/classMap';
-import TabContainer from '../../../app-components/tab/tabContainer';
+import ProfilePlot from './chart-content/profile-plot.jsx';
+import ScatterLinePlot from './chart-content/scatter-line-plot.jsx';
+import { apiGet, buildQueryParams } from '../../../app-services/fetch-helpers.ts';
+import { downloadFinalReport, useGetReportStatus, useInitializeReportDownload } from '../../../app-services/collections/report-configuration-download.ts';
+import { extractTimeseriesFrom, PlotTypeText } from './helper.js';
+import { tUpdateSuccess } from '../../../common/helpers/toast-helpers';
import './batch-plotting.scss';
-const batchPlotContainsInclinometers = (activeId, items, timeseries, instruments, domains) => {
- const ret = {
- containsInclinometers: false,
- inclinometerTimeseriesIds: [],
- };
- if (!Object.keys(items).length || !activeId) return ret;
- const config = items[activeId];
- const { timeseries_id } = config || {};
-
- if (!timeseries_id) return ret;
-
- const { id: inclinometerTypeId } = domains['instrument_type'].find(el => el.value === 'Inclinometer');
-
- timeseries_id.forEach(id => {
- const { instrument_id } = timeseries.find(ts => ts.id === id) || {};
- const { type_id } = instruments.find(i => i.id === instrument_id) || {};
-
- if (type_id === inclinometerTypeId) {
- ret.containsInclinometers = true;
- ret.inclinometerTimeseriesIds = [...ret.inclinometerTimeseriesIds, id];
- }
- });
-
- return ret;
-};
-
const BatchPlotting = connect(
'doMapsInitialize',
'doMapsShutdown',
'selectMapsObject',
'selectHashQuery',
+ 'selectProjectsByRoute',
'selectBatchPlotConfigurationsActiveId',
'selectBatchPlotConfigurationsItemsObject',
'selectInstrumentTimeseriesItems',
- 'selectInstrumentsItems',
'selectDomainsItemsByGroup',
+ 'selectProjectReportConfigurations',
({
doMapsInitialize,
doMapsShutdown,
mapsObject,
hashQuery,
+ projectsByRoute: project,
batchPlotConfigurationsActiveId: batchPlotId,
batchPlotConfigurationsItemsObject: batchPlotItems,
instrumentTimeseriesItems: timeseries,
- instrumentsItems: instruments,
- domainsItemsByGroup: domains,
+ projectReportConfigurations: reportConfigs,
}) => {
+ const [toastId, setToastId] = useState(undefined);
+
const crossSectionReady = import.meta.env.VITE_CROSS_SECTION === 'true';
const userConfigId = hashQuery ? hashQuery['c'] : '';
+ const activeConfig = batchPlotItems[batchPlotId];
+ const { plot_type } = activeConfig || {};
+ const plotTimeseries = extractTimeseriesFrom(activeConfig, timeseries);
+ const cwmsTimeseries = plotTimeseries.filter(el => el.type === 'cwms');
+ const instrumentIds = cwmsTimeseries.map(t => t.instrument_id);
+
+ const { data: midasCwmsTimeseries } = useQueries({
+ queries: instrumentIds.map((id) => ({
+ queryKey: ['midasCwmsTimeseriesPlotting', id],
+ queryFn: () => {
+ const uri = `/projects/${projectId}/instruments/${id}/timeseries/cwms`;
+ return apiGet(uri);
+ },
+ staleTime: Infinity,
+ enabled: !!cwmsTimeseries.length,
+ })),
+ combine: (ret) => {
+ return {
+ data: ret.map((result) => result.data).flat(),
+ }
+ },
+ });
+
+ const cwmsTimeseriesIds = midasCwmsTimeseries.map(ts => ts?.cwms_timeseries_id);
+
+ const { data: cwmsTimeseriesMeasurements } = useQueries({
+ queries: cwmsTimeseriesIds.map(ts => ({
+ queryKey: ['cwmsTimeseriesMeasurementsPlotting', ts],
+ queryFn: () => {
+ const { cwms_timeseries_id, cwms_office_id, cwms_extent_earliest_time } = midasCwmsTimeseries.find(el => el.cwms_timeseries_id === ts);
+ const uri = `/timeseries${buildQueryParams({ name: cwms_timeseries_id, office: cwms_office_id, begin: cwms_extent_earliest_time })}`;
+ return apiGet(uri, 'CWMS');
+ },
+ staleTime: Infinity,
+ enabled: !!midasCwmsTimeseries.length,
+ })),
+ combine: (ret) => {
+ return {
+ data: ret.map((result) => result.data),
+ }
+ },
+ });
- const {
- containsInclinometers,
- inclinometerTimeseriesIds,
- } = batchPlotContainsInclinometers(batchPlotId, batchPlotItems, timeseries, instruments, domains);
+ const { id: projectId } = project;
+ const { data: jobDetails, mutate: initReportJobMutator } = useInitializeReportDownload(projectId);
+ const { report_config_id: reportConfigId, id: jobId } = jobDetails || {};
+
+ const { data: currentJob } = useGetReportStatus({ projectId, reportConfigId, jobId }, {
+ enabled: !!((projectId && reportConfigId && jobId) && (['INIT', 'IN_PROGRESS'].includes(jobDetails?.status))),
+ refetchInterval: 3000,
+ });
+ const { file_key, progress } = currentJob || {};
+
+ if (file_key && progress === 100) {
+ tUpdateSuccess(
+ toastId,
+ 'Your file is ready, click this message to open!',
+ {
+ autoClose: false,
+ onClose: () => {
+ downloadFinalReport({ projectId, reportConfigId, jobId });
+ },
+ }
+ );
+ }
+
+ const beginDownloadReportJob = id => {
+ const tId = toast.loading('Getting your report ready for download. This may take a minute...');
+ setToastId(tId);
+ initReportJobMutator(id);
+ };
+
+ const containedInReports = reportConfigs?.filter(cfg => cfg.plot_configs.some(el => el.id === batchPlotId)) || [];
+
+ const cwmsProps = {
+ hasCwmsData: !!cwmsTimeseries.length,
+ cwmsData: cwmsTimeseriesMeasurements,
+ midasCwmsTimeseries,
+ };
return (
<>
@@ -73,49 +132,62 @@ const BatchPlotting = connect(
-
-
-
+ {batchPlotId && (
+ <>
+
-
+ This Batch Plot Configuration is a part of the following reports. Click on the report name to download the report.
+ {/* or click on the Remove Button to remove the plot configuration from the correlated Report Configuration. */}
+
+ {containedInReports.length ? containedInReports.map(cfg => (
+ beginDownloadReportJob(cfg.id)}
+ >
+ {cfg.name}
+
+ )) : No Reports }
-
- {crossSectionReady && (
-
-
-
-
-
-
-
Currently Under Construction
-
-
-
+
+
+
+
+
+
+
+
+ {crossSectionReady && (
+
+
+
+
+
+
+
Currently Under Construction
+
+
+
+
+ )}
- )}
-
-
- ,
- },
- containsInclinometers && {
- title: 'Depth Based Plot',
- content:
- },
- ]}
- />
-
+
+
+
+ {plot_type === 'scatter-line' && }
+ {plot_type === 'profile' && }
+ {plot_type === 'contour' && }
+ {plot_type === 'bullseye' && }
+
+
+ >
+ )}
>
);
diff --git a/src/app-pages/project/batch-plotting/chart-content/_ipi-profile-plot.jsx b/src/app-pages/project/batch-plotting/chart-content/_ipi-profile-plot.jsx
new file mode 100644
index 00000000..1a850fc2
--- /dev/null
+++ b/src/app-pages/project/batch-plotting/chart-content/_ipi-profile-plot.jsx
@@ -0,0 +1,205 @@
+import React, { useState } from 'react';
+import ReactDatePicker from 'react-datepicker';
+import { addDays, subDays } from 'date-fns';
+import { Checkbox, FormControlLabel, Slider, Stack, Switch } from '@mui/material';
+import { connect } from 'redux-bundler-react';
+import { DateTime } from 'luxon';
+import { useDeepCompareEffect } from 'react-use';
+
+import Chart from '../../../../app-components/chart/chart';
+
+const colors = {
+ init: '#000000',
+};
+
+const config = {
+ repsonsive: true,
+ displaylogo: false,
+ displayModeBar: true,
+ scrollZoom: true,
+};
+
+const layout = (showTemperature, showIncremental) => ({
+ showlegend: true,
+ autosize: true,
+ height: 800,
+ rows: 1,
+ columns: showTemperature ? 2 : 1,
+ yaxis: {
+ domain: [0, 1],
+ anchor: 'x1',
+ autorange: 'reversed',
+ title: `Depth in Feet`,
+ },
+ xaxis: {
+ domain: [0, showTemperature ? 0.4 : 1],
+ anchor: 'y1',
+ title: `${showIncremental ? 'Incremental' : 'Cumulative'} Displacement`,
+ },
+ ...showTemperature && {
+ xaxis2: {
+ title: 'Temperature',
+ domain: [0.6, 1],
+ anchor: 'y2',
+ },
+ yaxis2: {
+ domain: [0, 1],
+ anchor: 'x2',
+ autorange: 'reversed',
+ }
+ },
+});
+
+const formatData = (measurements, indexes, showInitial, showTemperature, showIncremental) => {
+ if (!measurements.length) return {};
+
+ const timeIncrements = measurements.sort((a, b) => DateTime.fromISO(a.time).toMillis() - DateTime.fromISO(b.time).toMillis())
+ const relevantData = timeIncrements.slice(indexes[0], indexes[1] + 1);
+
+ const dataArray = [
+ ...(showInitial ? build2dTrace(timeIncrements[0], true, showTemperature, showIncremental).flat() : []),
+ ...relevantData.map(m => build2dTrace(m, false, showTemperature, showIncremental)).flat(),
+ ].filter(e => e);
+
+ return { dataArray, timeIncrements, relevantData };
+};
+
+const build2dTrace = (data, isInit, showTemperature, showIncremental) => {
+ if (!Object.keys(data).length) return {};
+
+ const { time, measurements } = data;
+
+ const x = [], xTemp = [], y = [];
+
+ measurements?.forEach(element => {
+ x.push(showIncremental ? (element?.inc_dev || 0) : (element?.cum_dev || 0));
+ xTemp.push(element?.temp);
+ y.push(element?.elevation || 0);
+ });
+
+ const localDateString = DateTime.fromISO(time).toLocaleString(DateTime.DATETIME_SHORT);
+ const common = {
+ y,
+ mode: 'markers+lines',
+ marker: { size: 5, color: isInit ? colors['init'] : undefined },
+ line: { width: 1 },
+ type: 'scatter',
+ };
+
+ return [{
+ ...common,
+ x,
+ name: isInit ? `Initial Displacement (${localDateString})` : `Displacement at ${localDateString}`,
+ hovertemplate: `
+
${localDateString}
+ Elevation: %{y}
+ ${showIncremental ? 'Incremental' : 'Cumulative'} Displacement: %{x}
+
+ `,
+ }, showTemperature ? {
+ ...common,
+ xTemp,
+ xaxis: 'x2',
+ yaxis: 'y2',
+ name: isInit ? `Initial Temperature (${localDateString})` : `Temperature at ${localDateString}`,
+ hovertemplate: `
+
${localDateString}
+ Elevation: %{y}
+ Temperature: %{x}
+
+ `,
+ } : {}];
+};
+
+const IpiProfilePlot = connect(
+ 'doFetchInstrumentSensorMeasurements',
+ 'selectInstrumentSensorsMeasurements',
+ ({
+ doFetchInstrumentSensorMeasurements,
+ instrumentSensorsMeasurements,
+ instrumentId,
+ }) => {
+ const [showTemperature, setShowTemperature] = useState(true);
+ const [showInitial, setShowInitial] = useState(false);
+ const [showIncremental, setShowIncremental] = useState(false);
+ const [sliderVal, setSliderVal] = useState([0, 0]);
+ const [dateRange, setDateRange] = useState([subDays(new Date(), 7), new Date()]);
+
+ const { dataArray = [], timeIncrements = [] } = formatData(instrumentSensorsMeasurements, sliderVal, showInitial, showTemperature, showIncremental);
+
+ useDeepCompareEffect(() => {
+ doFetchInstrumentSensorMeasurements('ipi', dateRange[1].toISOString(), dateRange[0].toISOString(), instrumentId);
+ }, [dateRange, instrumentId]);
+
+ return (
+ <>
+
+
+ Start Date
+ setDateRange([date, addDays(date, 7)])}
+ />
+
+
+ End Date
+ setDateRange([subDays(date, 7), date])}
+ />
+
+
+ setShowInitial(prev => !prev)} />}
+ label='Show Initial Displacement'
+ />
+
+
+ setShowTemperature(prev => !prev)} />}
+ label='Show Temperature'
+ />
+
+
+
+ Cumulative
+ setShowIncremental(prev => !prev)} />
+ Incremental
+
+
+
+
+
+
+ {DateTime.fromISO(instrumentSensorsMeasurements[val]?.time).toFormat('MMM dd, yyyy hh:mm:ss')} }
+ onChange={(_e, newVal) => setSliderVal(newVal)}
+ />
+
+
+ >
+ );
+ },
+);
+
+export default IpiProfilePlot;
diff --git a/src/app-pages/project/batch-plotting/chart-content/_saa-profile-plot.jsx b/src/app-pages/project/batch-plotting/chart-content/_saa-profile-plot.jsx
new file mode 100644
index 00000000..522664e6
--- /dev/null
+++ b/src/app-pages/project/batch-plotting/chart-content/_saa-profile-plot.jsx
@@ -0,0 +1,215 @@
+import React, { useState } from 'react';
+import { Slider } from '@mui/material';
+import { DateTime } from 'luxon';
+import { Icon } from '@iconify/react';
+
+import Chart from '../../../../app-components/chart/chart';
+import { useGetMeasurementsByInstrumentType } from '../../../../app-services/collections/instrument-timeseries-measurements.ts';
+
+const colors = [
+ '#800000',
+ '#000075',
+ '#e6194B',
+ '#3cb44b',
+ '#911eb4',
+ '#fabed4',
+];
+
+const formatData = (data = [], indexes = [], _isMetric = false) => {
+ if (!data.length) return {};
+
+ const depthIncrements = data.map((datum, i) => {
+ const { time, measurements = [] } = datum;
+
+ const valueDisplacement = measurements.sort((a, b) => b.elevation - a.elevation);
+
+ return { time, valueDisplacement, colorIndex: i };
+ }).sort((a, b) => DateTime.fromISO(a.time).toMillis() - DateTime.fromISO(b.time).toMillis());
+
+ const relevantData = depthIncrements.slice(indexes[0], indexes[1] + 1);
+ const dataArray = [];
+
+ for (let i = 0; i < relevantData.length; i++) {
+ const { valueDisplacement, time, colorIndex } = relevantData[i];
+
+ dataArray.push(
+ valueDisplacement.reduce((accum, current, ind) => {
+ const { elevation, x_increment, y_increment, } = current;
+
+ if (ind === 0) {
+ accum.nDepth.push(elevation);
+ accum.aIncrement.push(x_increment);
+ accum.bIncrement.push(y_increment);
+ accum.time = DateTime.fromISO(time).toFormat('MMM dd, yyyy hh:mm:ss');
+ accum.colorIndex = colorIndex;
+ } else {
+ accum.nDepth.push(elevation);
+ accum.aIncrement.push(accum.aIncrement[ind - 1] + x_increment);
+ accum.bIncrement.push(accum.bIncrement[ind - 1] + y_increment);
+ accum.time = DateTime.fromISO(time).toFormat('MMM dd, yyyy hh:mm:ss');
+ accum.colorIndex = colorIndex;
+ }
+
+ return accum;
+ }, {
+ nDepth: [],
+ aIncrement: [],
+ bIncrement: [],
+ time: '',
+ colorIndex: '',
+ })
+ )
+ }
+
+ return { depthIncrements, dataArray, relevantData };
+};
+
+const build3dTraces = (dataArray, unit) => dataArray.map(data => (
+ {
+ x: data.aIncrement,
+ y: data.bIncrement,
+ z: data.nDepth,
+ mode: 'markers+lines',
+ marker: { size: 3, color: colors[data.colorIndex % colors.length] },
+ line: { width: 1 },
+ type: 'scatter3d',
+ name: `${data.time} Cumulative Displacement (in ${unit})`,
+ }
+
+ // If client wants A and B Displacement on the 3-D plot, add these back in and adjust function to a forEach using push logic.
+ // , {
+ // x: data.aIncrement,
+ // y: new Array(data.bIncrement.length).fill(0),
+ // z: data.nDepth,
+ // mode: 'markers+lines',
+ // marker: { size: 5, color: 'green' },
+ // type: 'scatter3d',
+ // name: `A Displacement (in ${unit})`,
+ // }, {
+ // x: new Array(data.aIncrement.length).fill(0),
+ // y: data.bIncrement,
+ // z: data.nDepth,
+ // mode: 'markers+lines',
+ // marker: { size: 5, color: 'orange' },
+ // type: 'scatter3d',
+ // name: `B Displacement (in ${unit})`
+ // }
+));
+
+const build2dTrace = (dataArray, key, unit) => dataArray.map(data => (
+ {
+ x: data[key],
+ y: data.nDepth,
+ mode: 'markers+lines',
+ marker: { size: 5, color: colors[data.colorIndex % colors.length] },
+ line: { width: 1 },
+ type: 'scatter',
+ name: `${key} Displacement (in ${unit})`,
+ hovertemplate: `
+
${data.time}
+ Depth: %{y}
+ Displacement: %{x}
+
+ `,
+ }
+));
+
+const SaaProfilePlot = ({
+ instrumentId,
+ instrumentType,
+}) => {
+ const [sliderVal, setSliderVal] = useState([0, 0]);
+
+ const { data: measuremnts, isLoading } = useGetMeasurementsByInstrumentType({ instrumentId, instrumentType });
+
+ const isMetric = false;
+ const unit = isMetric ? 'mm' : 'inches';
+ const { dataArray = [], depthIncrements = [] } = formatData(measuremnts, sliderVal, isMetric);
+
+ const config = {
+ repsonsive: true,
+ displaylogo: false,
+ displayModeBar: true,
+ scrollZoom: true,
+ };
+
+ const layout3d = {
+ autosize: true,
+ height: 800,
+ scene: {
+ xaxis: { title: `A-Displacement (in ${unit})` },
+ yaxis: { title: `B-Displacement (in ${unit})` },
+ zaxis: { title: 'Depth', autorange: 'reversed' },
+ },
+ legend: {
+ 'orientation': 'h',
+ },
+ };
+
+ const layoutTall = (key) => ({
+ showlegend: false,
+ autosize: true,
+ height: 800,
+ yaxis: {
+ autorange: 'reversed',
+ title: `Depth in Feet`,
+ },
+ xaxis: {
+ title: `${key}-Displacement in ${unit}`,
+ },
+ });
+
+ const incrementData = build3dTraces(dataArray, unit);
+
+ return (
+ <>
+ {isLoading ? (
+
+
+
+ ) : (
+ <>
+
+
+
+ {DateTime.fromISO(depthIncrements[val]?.time).toFormat('MMM dd, yyyy hh:mm:ss')} }
+ onChange={(_e, newVal) => setSliderVal(newVal)}
+ />
+
+
+ >
+ )}
+ >
+ );
+};
+
+export default SaaProfilePlot;
diff --git a/src/app-pages/project/batch-plotting/chart-content/bullseye-plot.jsx b/src/app-pages/project/batch-plotting/chart-content/bullseye-plot.jsx
new file mode 100644
index 00000000..0285b1a7
--- /dev/null
+++ b/src/app-pages/project/batch-plotting/chart-content/bullseye-plot.jsx
@@ -0,0 +1,132 @@
+import React, { useState } from 'react';
+import { connect } from 'redux-bundler-react';
+
+import BatchPlotChartSettings from '../components/batch-plot-chart-settings.jsx';
+import Chart from '../../../../app-components/chart/chart.jsx';
+import { useGetBullseyeMeasurements } from '../../../../app-services/collections/instrument-timeseries-measurements.ts';
+import { determineDateRange } from '../helper.js';
+import { DateTime } from 'luxon';
+
+const generateCircle = (maxValue, scalar, isLight) => {
+ const scaled = maxValue * scalar;
+
+ return {
+ type: 'circle',
+ xref: 'x',
+ yref: 'y',
+ x0: -scaled,
+ y0: -scaled,
+ x1: scaled,
+ y1: scaled,
+ opacity: 0.6,
+ line: {
+ width: 2,
+ color: isLight ? 'gray' : 'black',
+ },
+ };
+};
+
+const generateBullseyeData = (_plotConfig, measurements = []) => {
+ const x = measurements.map(el => el.x);
+ const y = measurements.map(el => el.y);
+
+ const xMax = Math.max(...x);
+ const yMax = Math.max(...y);
+ const totalMax = Math.max(xMax, yMax);
+
+ return {
+ data: [
+ {
+ mode: 'lines+markers',
+ type: 'scatter',
+ x,
+ y,
+ },
+ ],
+ totalMax,
+ };
+};
+
+const BullseyePlot = connect(
+ 'doSaveBatchPlotConfiguration',
+ ({
+ doSaveBatchPlotConfiguration,
+ plotConfig,
+ }) => {
+ const { auto_range, date_range, display, show_masked, show_comments, show_nonvalidated, plot_type, id, project_id } = plotConfig || {};
+
+ const [chartSettings, setChartSettings] = useState({ auto_range, display, show_masked, show_comments, show_nonvalidated, date_range, plot_type });
+ const [dateRange, setDateRange] = useState(determineDateRange(date_range));
+ const [threshold, setThreshold] = useState(3000);
+
+ const { data: measurements } = useGetBullseyeMeasurements({
+ projectId: project_id,
+ plotConfigId: id,
+ before: DateTime.fromJSDate(dateRange[1]).toISO(),
+ after: DateTime.fromJSDate(dateRange[0]).toISO(),
+ });
+ const { data, totalMax } = generateBullseyeData(plotConfig, measurements);
+
+ const config = {
+ repsonsive: true,
+ displaylogo: false,
+ displayModeBar: true,
+ scrollZoom: true,
+ };
+
+ const layout = {
+ showlegend: false,
+ height: 800,
+ width: 800,
+ yaxis: {
+ showgrid: true,
+ title: `<-- South | North -->`,
+ },
+ xaxis: {
+ showgrid: true,
+ title: `<-- West | East -->`,
+ },
+ shapes: [
+ generateCircle(totalMax, 0.25, true),
+ generateCircle(totalMax, 0.5, false),
+ generateCircle(totalMax, 0.75, true),
+ generateCircle(totalMax, 1, false),
+ generateCircle(totalMax, 1.25, true),
+ ],
+ };
+
+ const savePlotSettings = (params) => {
+ doSaveBatchPlotConfiguration(plot_type, id, { ...params });
+ };
+
+ return (
+ <>
+
+
+
+ {chartSettings ? (
+ <>
+
+
+ >
+ ) : null}
+ >
+ );
+ },
+);
+
+export default BullseyePlot;
diff --git a/src/app-pages/project/batch-plotting/chart-content/contour-plot.jsx b/src/app-pages/project/batch-plotting/chart-content/contour-plot.jsx
new file mode 100644
index 00000000..eb2b3aaf
--- /dev/null
+++ b/src/app-pages/project/batch-plotting/chart-content/contour-plot.jsx
@@ -0,0 +1,162 @@
+import React, { useMemo, useState } from 'react';
+import ReactDatePicker from 'react-datepicker';
+import { addDays, subDays } from 'date-fns';
+import { Autocomplete, TextField } from '@mui/material';
+import { connect } from 'redux-bundler-react';
+import { DateTime } from 'luxon';
+import { formatMeasurementOptions, formatMeasurementTimestamp, generatePayloadForType } from '../helper.js';
+import { Icon } from '@iconify/react';
+import { useDeepCompareEffect } from 'react-use';
+
+import Chart from '../../../../app-components/chart/chart';
+import { useGetContourMeasurements, useGetMeasurementTimestamps } from '../../../../app-services/collections/instrument-timeseries-measurements.ts';
+
+const generateContourData = (display, measurements) => {
+ const { time, show_labels, contour_smoothing, gradient_smoothing } = display || {};
+ if (!time) return [];
+
+ return [{
+ ...measurements,
+ connectgaps: true,
+ type: 'contour',
+ contours: {
+ line: {
+ smoothing: contour_smoothing ? 0.9 : 0,
+ },
+ ...gradient_smoothing && { coloring: 'heatmap' },
+ showlabels: show_labels,
+ labelfont: {
+ family: 'Raleway',
+ size: 12,
+ color: 'white',
+ },
+ },
+ }];
+};
+
+const ContourPlot = connect(
+ 'doSaveBatchPlotConfiguration',
+ ({
+ doSaveBatchPlotConfiguration,
+ plotConfig,
+ }) => {
+ const { project_id, display, id, plot_type, name } = plotConfig || {};
+ const { time } = display || {};
+
+ const [selectedMeasurement, setSelectedMeasurement] = useState(time ? formatMeasurementTimestamp(time) : '');
+ const [dateRange, setDateRange] = useState([subDays(Date.now(), 7), new Date()]);
+
+ const { data: measurements, isLoading } = useGetContourMeasurements({ projectId: project_id, plotConfigId: id, time: time });
+ const { data: timestamps } = useGetMeasurementTimestamps({
+ projectId: project_id,
+ plotConfigId: id,
+ before: DateTime.fromJSDate(dateRange[1]).toISO(),
+ after: DateTime.fromJSDate(dateRange[0]).toISO(),
+ });
+
+ const measurementOptions = useMemo(() => formatMeasurementOptions(timestamps?.length ? timestamps : []), [timestamps]);
+
+ const config = {
+ repsonsive: true,
+ displaylogo: false,
+ displayModeBar: true,
+ scrollZoom: true,
+ };
+
+ const layout = {
+ showlegend: false,
+ autosize: true,
+ height: 600,
+ yaxis: {
+ title: `Latitude`,
+ },
+ xaxis: {
+ title: `Longitude`,
+ },
+ };
+
+ const data = generateContourData(display, measurements);
+
+ useDeepCompareEffect(() => {
+ if (selectedMeasurement) {
+ doSaveBatchPlotConfiguration(
+ plot_type,
+ id,
+ generatePayloadForType(
+ plot_type,
+ name,
+ {
+ ...display,
+ time: selectedMeasurement._original,
+ },
+ project_id
+ )
+ );
+ }
+ }, [selectedMeasurement]);
+
+ return (
+ <>
+ {isLoading ? (
+
+
+
+ ) : (
+ <>
+ {id && (
+ <>
+
+
+ Start Date
+ setDateRange([date, addDays(date, 7)])}
+ />
+
+
+ End Date
+ setDateRange([subDays(date, 7), date])}
+ />
+
+
+
setSelectedMeasurement(value)}
+ options={measurementOptions}
+ isOptionEqualToValue={(opt, val) => opt._original === val._original}
+ renderInput={(params) => (
+
+ )}
+ />
+
+ >
+ )}
+
+ >
+ )}
+ >
+ );
+ },
+);
+
+export default ContourPlot;
diff --git a/src/app-pages/project/batch-plotting/chart-content/profile-plot.jsx b/src/app-pages/project/batch-plotting/chart-content/profile-plot.jsx
new file mode 100644
index 00000000..9dbebfdd
--- /dev/null
+++ b/src/app-pages/project/batch-plotting/chart-content/profile-plot.jsx
@@ -0,0 +1,24 @@
+import React from 'react';
+
+import SaaProfilePlot from './_saa-profile-plot';
+import IpiProfilePlot from './_ipi-profile-plot';
+
+const ProfilePlot = ({
+ plotConfig,
+}) => {
+ const { display } = plotConfig || {};
+ const { instrument_id, instrument_type } = display || {};
+
+ return (
+ <>
+ {instrument_type === 'SAA' && (
+
+ )}
+ {instrument_type === 'IPI' && (
+
+ )}
+ >
+ );
+};
+
+export default ProfilePlot;
diff --git a/src/app-pages/project/batch-plotting/chart-content/scatter-line-plot.jsx b/src/app-pages/project/batch-plotting/chart-content/scatter-line-plot.jsx
new file mode 100644
index 00000000..05914de1
--- /dev/null
+++ b/src/app-pages/project/batch-plotting/chart-content/scatter-line-plot.jsx
@@ -0,0 +1,159 @@
+import React, { useEffect, useMemo, useState } from 'react';
+import { connect } from 'redux-bundler-react';
+import { subDays } from 'date-fns';
+import { useDeepCompareEffect } from 'react-use';
+
+import BatchPlotChartSettings from '../components/batch-plot-chart-settings';
+import Chart from '../../../../app-components/chart/chart';
+import ChartErrors from '../components/batch-plot-errors';
+import { determineDateRange, generateNewChartData } from '../helper';
+
+const ScatterLinePlot = connect(
+ // 'doPrintSetData',
+ 'doTimeseriesMeasurementsFetchById',
+ 'doSaveBatchPlotConfiguration',
+ 'selectBatchPlotConfigurationsActiveId',
+ 'selectBatchPlotConfigurationsItemsObject',
+ 'selectTimeseriesMeasurementsItems',
+ 'selectInstrumentTimeseriesItems',
+ ({
+ // doPrintSetData,
+ doTimeseriesMeasurementsFetchById,
+ doSaveBatchPlotConfiguration,
+ batchPlotConfigurationsActiveId: activeId,
+ batchPlotConfigurationsItemsObject: batchPlotConfigs,
+ timeseriesMeasurementsItems: timeseriesMeasurements,
+ instrumentTimeseriesItems: timeseries,
+ plotConfig,
+ hasCwmsData,
+ cwmsData,
+ midasCwmsTimeseries,
+ }) => {
+ const { auto_range, date_range, display, show_masked, show_comments, show_nonvalidated, plot_type, id } = plotConfig || {};
+
+ const plotTimeseriesIds = display?.traces?.map(el => el.timeseries_id) || [];
+ const plotTimeseries = timeseries.filter(ts => plotTimeseriesIds.includes(ts.id));
+ const plotMeasurements = plotTimeseriesIds.map(id => timeseriesMeasurements.find(elem => elem.timeseries_id === id));
+
+ const [dateRange, setDateRange] = useState([subDays(new Date(), 365), new Date()]);
+ const [threshold, setThreshold] = useState(3000);
+ const [chartSettings, setChartSettings] = useState({ auto_range, display, show_masked, show_comments, show_nonvalidated, date_range, plot_type });
+
+ const chartData = useMemo(() => generateNewChartData(plotMeasurements, plotTimeseries, chartSettings, plotConfig, hasCwmsData, cwmsData, midasCwmsTimeseries), [plotMeasurements, activeId]);
+ const withPrecipitation = plotTimeseries.some(ts => ts.parameter === 'precipitation');
+ const layout = {
+ xaxis: {
+ autorange: chartSettings?.auto_range,
+ range: dateRange,
+ title: 'Date',
+ showline: true,
+ mirror: true,
+ },
+ yaxis: {
+ title: display?.layout?.y_axis_title || 'Measurement',
+ showline: true,
+ mirror: true,
+ domain: [0, withPrecipitation ? 0.66 : 1],
+ },
+ ...(display?.layout?.y2_axis_title && {
+ yaxis2: {
+ title: display?.layout?.y2_axis_title,
+ showline: true,
+ side: 'right',
+ overlaying: 'y1',
+ domain: [0, withPrecipitation ? 0.66 : 1],
+ }
+ }),
+ ...(withPrecipitation && {
+ yaxis3: {
+ title: 'Rainfall',
+ autorange: 'reversed',
+ showline: true,
+ mirror: true,
+ domain: [0.66, 1],
+ },
+ }),
+ shapes: display?.layout?.custom_shapes?.map(shape =>
+ shape.enabled ? {
+ type: 'line',
+ x0: dateRange[0],
+ x1: dateRange[1],
+ y0: shape.data_point,
+ y1: shape.data_point,
+ line: {
+ color: shape.color,
+ width: 3,
+ },
+ } : false).filter(e => e) || [],
+ autosize: true,
+ dragmode: 'pan',
+ height: 600,
+ };
+
+ const savePlotSettings = (params) => {
+ plotTimeseriesIds.forEach(id => doTimeseriesMeasurementsFetchById({ timeseriesId: id, dateRange, threshold }));
+ doSaveBatchPlotConfiguration(plot_type, id, { ...params });
+ };
+
+ useEffect(() => {
+ const currentConfig = batchPlotConfigs[activeId];
+ const { threshold, date_range, auto_range, show_comments, show_nonvalidated, show_masked } = currentConfig;
+
+ if (activeId) {
+ setThreshold(threshold);
+ setDateRange(determineDateRange(date_range));
+ setChartSettings({ auto_range, date_range, show_comments, show_nonvalidated, show_masked, plot_type });
+ }
+ }, [activeId]);
+
+ /** Fetches All Timeseries Measurements used by the plot */
+ useDeepCompareEffect(() => {
+ const cfg = plotConfig || {};
+
+ cfg?.display?.traces?.forEach(trace => {
+ const { timeseries_id } = trace;
+
+ doTimeseriesMeasurementsFetchById({ timeseriesId: timeseries_id, dateRange, threshold })
+ });
+ }, [plotConfig, dateRange, threshold]);
+
+ return (
+ <>
+
+
+ {chartSettings ? (
+ <>
+
+
+ >
+ ) : null}
+ >
+ );
+ }
+);
+
+export default ScatterLinePlot;
diff --git a/src/app-pages/project/batch-plotting/components/batch-plot-chart-settings.jsx b/src/app-pages/project/batch-plotting/components/batch-plot-chart-settings.jsx
index 7dafcdb9..0cb396df 100644
--- a/src/app-pages/project/batch-plotting/components/batch-plot-chart-settings.jsx
+++ b/src/app-pages/project/batch-plotting/components/batch-plot-chart-settings.jsx
@@ -1,24 +1,25 @@
-import React, { useState } from 'react';
+import React, { useEffect, useState } from 'react';
import DatePicker from 'react-datepicker';
-import { DateTime } from 'luxon';
-import { subDays, startOfDay } from 'date-fns';
+import { connect } from 'redux-bundler-react';
import { CSVLink } from 'react-csv';
+import { DateTime } from 'luxon';
import { Slider } from '@mui/material';
import { toast } from 'react-toastify';
+import BatchPlotAdvancedSettings from '../modals/BatchPlotAdvancedSettings';
import Button from '../../../../app-components/button';
import HelperTooltip from '../../../../app-components/helper-tooltip';
-import PrintButton from './print-button';
-
-const dateAgo = days => subDays(new Date(), days);
+import { determineDateRange } from '../helper';
+// import PrintButton from './print-button';
+// TODO
const customDateFormat = (fromTime, endTime) => {
const fromISO = fromTime.toISOString();
const endISO = endTime.toISOString();
- const fromDate = DateTime.fromISO(fromISO).toFormat('MM/dd/yyyy');
- const endDate = DateTime.fromISO(endISO).toFormat('MM/dd/yyyy');
+ const fromDate = DateTime.fromISO(fromISO).toFormat('yyyy-MM-dd');
+ const endDate = DateTime.fromISO(endISO).toFormat('yyyy-MM-dd');
- return `${fromDate} - ${endDate}`;
+ return `${fromDate} ${endDate}`;
};
// @TODO - this function is atrocious. fix it.
@@ -48,262 +49,265 @@ const generatePlottedCSV = async (chartData = [], setCsvData) => {
setCsvData(res);
};
-const BatchPlotChartSettings = ({
- chartSettings,
- setChartSettings,
- dateRange = [],
- setDateRange,
- threshold = 3000,
- setThreshold,
- savePlotSettings,
- chartData,
-}) => {
- const [fromTime, endTime] = dateRange;
- const [csvData, setCsvData] = useState([]);
- const [activeButton, setActiveButton] = useState('1 year');
- const { auto_range, show_comments, show_masked, show_nonvalidated } = chartSettings;
+const BatchPlotChartSettings = connect(
+ 'doModalOpen',
+ ({
+ doModalOpen,
+ plotConfig,
+ chartSettings,
+ setChartSettings,
+ dateRange = [],
+ setDateRange,
+ threshold = 3000,
+ setThreshold,
+ savePlotSettings,
+ chartData = {},
+ }) => {
+ const [fromTime, endTime] = dateRange;
+ const { date_range, auto_range, show_comments, show_masked, show_nonvalidated, plot_type } = chartSettings;
- const alterRange = (daysAgo) => {
- setDateRange([startOfDay(dateAgo(daysAgo)), new Date()]);
- };
+ const [currentThreshold, setCurrentThreshold] = useState(threshold);
+ const [csvData, setCsvData] = useState([]);
+ const [activeButton, setActiveButton] = useState(['lifetime', '5 years', '1 year', '1 month'].includes(date_range) ? date_range : 'Custom');
- const calcLifetime = () => {
- setDateRange([new Date(0), new Date()]);
- };
+ const isDisplayAllActive = () => show_comments && show_masked && show_nonvalidated;
- const isDisplayAllActive = () => show_comments && show_masked && show_nonvalidated;
+ const handleDateChangeRaw = (e) => {
+ e.preventDefault();
+ };
- const handleDateChangeRaw = (e) => {
- e.preventDefault();
- };
+ useEffect(() => {
+ setDateRange(determineDateRange(activeButton === 'Custom' ? date_range : activeButton));
+ }, [activeButton]);
- return (
-
-
Plot Settings:
-
-
-
-
- {
- setActiveButton('Lifetime');
- calcLifetime();
- }}
- />
- {
- setActiveButton('5 years');
- alterRange(1825);
- }}
- />
- {
- setActiveButton('1 year');
- alterRange(365);
- }}
- />
- {
- setActiveButton('1 month');
- alterRange(30);
- }}
- />
- setActiveButton('Custom')}
- />
-
- {activeButton === 'Custom' && (
-
setDateRange(update)}
- onChangeRaw={handleDateChangeRaw}
- showMonthDropdown
- showYearDropdown
- dateFormat='MMMM d, yyyy'
- />
- )}
-
-
- Display Point Threshold:
-
- The Display Point Threshold value determines the number of data points to downsample the plot to.
- The higher this value is, the more accurate the data will be to actual and similarly, the lower the
- this value is, the less accurate it will be to actual. To turn off downsampling and use all data points,
- set the Display Point Threshold to 0 . The number of data points will considerably change the
- loading time of the plot, the lower the value the faster it will load.
-
- It is recommended to only use a high value, or 0, if your date range is small or need extremely
- accurate data representation.
-
- )}
+ return (
+
+
Plot Settings:
+ {/*
*/}
+
+
+
+ setActiveButton('lifetime')}
/>
-
- setThreshold(newVal)}
- />
-
-
-
-
- setChartSettings({ ...chartSettings, auto_range: !auto_range})}
- onChange={() => {}}
- />
- Auto-range
-
- setActiveButton('5 years')}
+ />
+ setActiveButton('1 year')}
+ />
+ setActiveButton('1 month')}
+ />
+ setActiveButton('Custom')}
+ />
+
+ {activeButton === 'Custom' && (
+
setDateRange(update)}
+ onChangeRaw={handleDateChangeRaw}
+ showMonthDropdown
+ showYearDropdown
+ dateFormat='MMMM d, yyyy'
+ />
+ )}
+
- Selecting this option will allow the plot to
- attempt a 'best-fit' view for the data selected.
+ Display Point Threshold:
+
+ The Display Point Threshold value determines the number of data points to downsample the plot to.
+ The higher this value is, the more accurate the data will be to actual and similarly, the lower the
+ this value is, the less accurate it will be to actual. To turn off downsampling and use all data points,
+ set the Display Point Threshold to 0 . The number of data points will considerably change the
+ loading time of the plot, the lower the value the faster it will load.
+
+ It is not recommended to use a high value, or 0. Only use these options if your date range is small or need extremely
+ accurate data representation.
+
+ )}
+ />
- }
- />
-
-
- setChartSettings({
- ...chartSettings,
- show_masked: !isDisplayAllActive(),
- show_nonvalidated: !isDisplayAllActive(),
- show_comments: !isDisplayAllActive(),
- threshold,
- })}
- onChange={() => {}}
- />
- Display All Data
-
-
- setChartSettings({
- ...chartSettings,
- show_masked: !show_masked
- })}
- onChange={() => {}}
- />
- Show Masked Data
-
-
- setChartSettings({
- ...chartSettings,
- show_nonvalidated: !show_nonvalidated
- })}
- onChange={() => {}}
- />
- Show Non-Validated
-
-
- setChartSettings({
- ...chartSettings,
- show_comments: !show_comments
- })}
- onChange={() => {}}
+ setThreshold(newVal)}
+ onChange={(_e, newVal) => setCurrentThreshold(newVal)}
+ />
+
+
+
+
+ setChartSettings({ ...chartSettings, auto_range: !auto_range})}
+ onChange={() => {}}
+ />
+ Auto-range
+
+
+ Selecting this option will allow the plot to
+ attempt a 'best-fit' view for the data selected.
+
+ }
/>
- Show Comments
-
+
+
+ setChartSettings({
+ ...chartSettings,
+ show_masked: !isDisplayAllActive(),
+ show_nonvalidated: !isDisplayAllActive(),
+ show_comments: !isDisplayAllActive(),
+ threshold,
+ })}
+ onChange={() => {}}
+ />
+ Display All Data
+
+
+ setChartSettings({
+ ...chartSettings,
+ show_masked: !show_masked
+ })}
+ onChange={() => {}}
+ />
+ Show Masked Data
+
+
+ setChartSettings({
+ ...chartSettings,
+ show_nonvalidated: !show_nonvalidated
+ })}
+ onChange={() => {}}
+ />
+ Show Non-Validated
+
+
+ setChartSettings({
+ ...chartSettings,
+ show_comments: !show_comments
+ })}
+ onChange={() => {}}
+ />
+ Show Comments
+
+
-
-
- savePlotSettings({
- ...chartSettings,
- date_range: activeButton === 'Custom' ? customDateFormat(fromTime, endTime) : activeButton,
- })}
- />
- {
- toast.promise(
- Promise.resolve(generatePlottedCSV(chartData, setCsvData)),
- {
- pending: {
- render: () => 'Generating CSV file. Please wait...',
- },
- success: {
- render: () => {
- done();
- return 'Your file is ready!';
- }
- },
- error: {
- render: ({ data }) => {
- done(false);
- return `Failed to generate file... \n${data?.message}`;
- }
- },
- }
- )
- }}
- >
+
savePlotSettings({
+ ...plotConfig,
+ ...chartSettings,
+ date_range: activeButton === 'Custom' ? customDateFormat(fromTime, endTime) : activeButton,
+ })}
/>
-
-
- );
-};
+
{
+ toast.promise(
+ Promise.resolve(generatePlottedCSV(chartData, setCsvData)),
+ {
+ pending: {
+ render: () => 'Generating CSV file. Please wait...',
+ },
+ success: {
+ render: () => {
+ done();
+ return 'Your file is ready!';
+ }
+ },
+ error: {
+ render: ({ data }) => {
+ done(false);
+ return `Failed to generate file... \n${data?.message}`;
+ }
+ },
+ }
+ )
+ }}
+ >
+
+
+ {plot_type === 'scatter-line' && (
+
doModalOpen(BatchPlotAdvancedSettings, { chartData, plotConfig }, 'lg')}
+ />
+ )}
+
+ );
+ }
+);
export default BatchPlotChartSettings;
diff --git a/src/app-pages/project/batch-plotting/components/batch-plot-errors.jsx b/src/app-pages/project/batch-plotting/components/batch-plot-errors.jsx
index fd71cd88..0037e652 100644
--- a/src/app-pages/project/batch-plotting/components/batch-plot-errors.jsx
+++ b/src/app-pages/project/batch-plotting/components/batch-plot-errors.jsx
@@ -13,7 +13,7 @@ const BatchPlotErrors = ({
if (!found || found?.x?.length) return;
const ts = timeseries.find(data => data.id === el);
- const {instrument, name, unit } = ts;
+ const { instrument, name, unit } = ts || {};
emptyDataSets.push(`${instrument} - ${name} (${unit})`);
});
diff --git a/src/app-pages/project/batch-plotting/components/configuration-panel.jsx b/src/app-pages/project/batch-plotting/components/configuration-panel.jsx
deleted file mode 100644
index 0df8acba..00000000
--- a/src/app-pages/project/batch-plotting/components/configuration-panel.jsx
+++ /dev/null
@@ -1,127 +0,0 @@
-import React, { useState, useEffect, useMemo } from 'react';
-import { connect } from 'redux-bundler-react';
-
-import Button from '../../../../app-components/button';
-import MultiSelect from '../../../../app-components/multi-select';
-
-const formatOptions = timeseries => (
- timeseries.map(ts => ({
- text: `${ts.instrument} - ${ts.name}`,
- value: ts.id,
- })).sort((a, b) => (
- a.text < b.text
- ? -1
- : a.text > b.text
- ? 1
- : 0
- ))
-);
-
-const configNameExists = (newConfigName = '', currentConfigurations = []) => {
- const found = currentConfigurations.find(elem => (
- newConfigName.trim() === elem.text.trim()
- ));
-
- return !!found;
-};
-
-const ConfigurationPanel = connect(
- 'doBatchPlotConfigurationsSave',
- 'selectBatchPlotConfigurationsItemsObject',
- 'selectBatchPlotConfigurationsActiveId',
- 'selectInstrumentTimeseriesItems',
- 'selectProjectsIdByRoute',
- ({
- doBatchPlotConfigurationsSave,
- batchPlotConfigurationsItemsObject,
- batchPlotConfigurationsActiveId,
- instrumentTimeseriesItems: instrumentTimeseries,
- projectsIdByRoute: project,
- isOpen,
- isEditMode,
- configurations,
- closePanel,
- }) => {
- if (!isOpen) return null;
-
- const currentItem = batchPlotConfigurationsItemsObject[batchPlotConfigurationsActiveId];
- const timeseries = useMemo(() => formatOptions(instrumentTimeseries), [instrumentTimeseries]);
- const [selectedTimeseries, setSelectedTimeseries] = useState((currentItem && isEditMode) && currentItem.timeseries_id || []);
- const [newConfigName, setNewConfigName] = useState((currentItem && isEditMode) && currentItem.name || '');
- const [inputError, setInputError] = useState('');
-
- const handleSave = () => {
- if (!newConfigName || !newConfigName.trim()) {
- setInputError('Please provide a configuration name.');
- } else if (configNameExists(newConfigName, configurations) && !isEditMode) {
- setInputError('Configuration name already exists. Please use a different name.');
- } else {
- doBatchPlotConfigurationsSave({
- ...isEditMode && { id: batchPlotConfigurationsActiveId },
- name: newConfigName.trim(),
- timeseries_id: selectedTimeseries,
- project_id: project.projectId,
- });
- closePanel();
- }
- };
-
- useEffect(() => {
- if (!isOpen) {
- setNewConfigName('');
- setSelectedTimeseries([]);
- setInputError('');
- }
- }, [isOpen, setNewConfigName, setSelectedTimeseries, setInputError]);
-
- return (
-
-
-
{
- if (inputError) setInputError('');
- setNewConfigName(e.target.value);
- }}
- />
- {inputError && (
-
- {inputError}
-
- )}
-
-
- closePanel()}
- />
-
-
-
- setSelectedTimeseries(val)}
- initialValues={selectedTimeseries}
- />
-
-
- );
- }
-);
-
-export default ConfigurationPanel;
diff --git a/src/app-pages/project/batch-plotting/components/data-configuration.jsx b/src/app-pages/project/batch-plotting/components/data-configuration.jsx
index d6f25b84..7216256b 100644
--- a/src/app-pages/project/batch-plotting/components/data-configuration.jsx
+++ b/src/app-pages/project/batch-plotting/components/data-configuration.jsx
@@ -1,22 +1,52 @@
-import React, { useEffect, useState } from 'react';
+import React, { useEffect } from 'react';
import { connect } from 'redux-bundler-react';
+import { Autocomplete, TextField } from '@mui/material';
import { Delete, Edit } from '@mui/icons-material';
import Button from '../../../../app-components/button';
-import ConfigurationPanel from './configuration-panel';
+import BatchPlotConfigurationModal from '../modals/BatchPlotConfiguration';
import DeleteButton from '../../../../app-components/delete-confirm';
-import Select from '../../../../app-components/select';
import usePrevious from '../../../../customHooks/usePrevious';
import '../batch-plotting.scss';
+import { PlotTypeText } from '../helper';
+
+// const groupBatchPlotConfigurations = batchPlotConfigurationsItems => {
+// return batchPlotConfigurationsItems.reduce((accum, current) => {
+// const { name, id, plot_type = 'Scatter Line' } = current;
+
+// const groupIndex = accum.findIndex(el => el.groupName === plot_type);
+// const elem = accum[groupIndex];
+// const newElem = {
+// name,
+// id,
+// };
+
+// if (groupIndex >= 0) {
+// accum.splice(groupIndex, 1, {
+// groupName: plot_type,
+// plotConfigurations: [...elem.plotConfigurations, newElem],
+// })
+// } else {
+// accum.push({
+// groupName: plot_type,
+// plotConfigurations: [newElem],
+// });
+// }
+
+// return accum;
+// }, []);
+// };
const DataConfiguration = connect(
+ 'doModalOpen',
'selectBatchPlotConfigurationsItems',
'selectBatchPlotConfigurationsItemsObject',
'selectBatchPlotConfigurationsActiveId',
'doBatchPlotConfigurationsDelete',
'doBatchPlotConfigurationsSetActiveId',
({
+ doModalOpen,
batchPlotConfigurationsItems,
batchPlotConfigurationsItemsObject,
batchPlotConfigurationsActiveId,
@@ -24,105 +54,86 @@ const DataConfiguration = connect(
doBatchPlotConfigurationsSetActiveId,
initialConfigurationId,
}) => {
- const [isPanelOpen, setIsPanelOpen] = useState(false);
- const [isEditMode, setIsEditMode] = useState(false);
const previousConfigId = usePrevious(batchPlotConfigurationsActiveId);
- const configurations = batchPlotConfigurationsItems.map(config => ({
- text: config.name,
- value: config.id,
- })).sort((a, b) => a.text.localeCompare(b.text));
-
- const handleEditClick = () => {
- const currentItem = batchPlotConfigurationsItemsObject[batchPlotConfigurationsActiveId];
-
- if (currentItem) {
- setIsEditMode(true);
- setIsPanelOpen(true);
- }
- };
-
const handleDeleteClick = () => {
const currentItem = batchPlotConfigurationsItemsObject[batchPlotConfigurationsActiveId];
if (currentItem) {
doBatchPlotConfigurationsDelete(currentItem);
+ doBatchPlotConfigurationsSetActiveId('');
}
};
- const handleNewClick = () => {
- setIsEditMode(false);
- setIsPanelOpen(true);
- };
-
const handleSelectChange = val => {
if (val && val !== previousConfigId) {
- doBatchPlotConfigurationsSetActiveId(val);
+ doBatchPlotConfigurationsSetActiveId(val.id);
+ } else if (!val) {
+ doBatchPlotConfigurationsSetActiveId('');
}
};
- // Set active id to empty before we start. makes sure certain actions aren't available to the user that shouldn't be
- useEffect(() => {
- doBatchPlotConfigurationsSetActiveId('');
- }, [doBatchPlotConfigurationsSetActiveId]);
-
useEffect(() => {
- if (initialConfigurationId) {
- doBatchPlotConfigurationsSetActiveId(initialConfigurationId);
- }
- }, [initialConfigurationId, doBatchPlotConfigurationsSetActiveId]);
+ const toSet = initialConfigurationId ? initialConfigurationId : '';
+ doBatchPlotConfigurationsSetActiveId(toSet);
+ }, [doBatchPlotConfigurationsSetActiveId, initialConfigurationId]);
return (
-
- {batchPlotConfigurationsActiveId && (
- <>
-
}
- handleClick={() => handleEditClick()}
- />
-
}
- handleDelete={() => handleDeleteClick()}
- />
- >
- )}
-
handleNewClick()}
+ options={batchPlotConfigurationsItems.sort((a, b) => -b.name.localeCompare(a.name))}
+ isOptionEqualToValue={(opt, val) => val ? opt?.id === val?.id : false}
+ groupBy={option => PlotTypeText[option.plot_type]}
+ onChange={(_e, val) => handleSelectChange(val)}
+ getOptionLabel={option => option.name}
+ sx={{ width: 350, display: 'inline-block' }}
+ renderInput={(params) => (
+
+ )}
/>
+
+ {batchPlotConfigurationsActiveId && (
+ <>
+ }
+ handleClick={() => doModalOpen(BatchPlotConfigurationModal, { isEdit: true, initConfig: batchPlotConfigurationsItemsObject[batchPlotConfigurationsActiveId] })}
+ />
+ }
+ handleDelete={() => handleDeleteClick()}
+ />
+ >
+ )}
+ doModalOpen(BatchPlotConfigurationModal)}
+ />
+
-
setIsPanelOpen(false)}
- />
);
}
diff --git a/src/app-pages/project/batch-plotting/helper.js b/src/app-pages/project/batch-plotting/helper.js
index 9f938da1..3dea2a01 100644
--- a/src/app-pages/project/batch-plotting/helper.js
+++ b/src/app-pages/project/batch-plotting/helper.js
@@ -1,19 +1,123 @@
-const getStyle = (_index) => ({
- type: 'scatter',
- mode: 'lines+markers',
- marker: {
- size: 8,
- },
- line: {
- width: 2,
- },
-});
+import { startOfDay, subDays } from 'date-fns';
+import { DateTime } from 'luxon';
+
+export const PlotTypeText = {
+ 'scatter-line': 'Scatter Line Plot',
+ 'profile': 'Depth Profile Plot',
+ 'contour': 'Contour Plot',
+ 'bullseye': 'Bullseye Plot',
+};
+
+const defaultPlotConfigOptions = {
+ show_comments: true,
+ show_masked: true,
+ show_nonvalidated: true,
+ auto_range: false,
+ threshold: 3000,
+ date_range: '1 year',
+};
+
+const getStyle = (trace) => {
+ const { color, line_style, show_markers, width, y_axis } = trace;
+ const lineColor = color === '#ffffff' ? null : color;
+
+ return {
+ type: 'scattergl',
+ mode: show_markers ? 'lines+markers' : 'lines',
+ showlegend: false,
+ yaxis: y_axis,
+ line: {
+ dash: line_style,
+ color: lineColor,
+ width: width,
+ },
+ marker: {
+ size: Number(width) ? (Number(width) * 2) + 3 : 5,
+ color: lineColor,
+ },
+ };
+};
+
+const dateAgo = days => startOfDay(subDays(new Date(), days));
+
+export const determineDateRange = date_range => {
+ switch (date_range) {
+ case 'lifetime':
+ return [new Date(0), new Date()];
+ case '5 years':
+ return [dateAgo(1825), new Date()];
+ case '1 month':
+ return [dateAgo(30), new Date()];
+ case '1 year':
+ return [dateAgo(365), new Date()];
+ default:
+ return date_range.split(' ').map(date => DateTime.fromFormat(date, 'yyyy-MM-dd').toJSDate());
+ };
+};
+
+export const extractTimeseriesFrom = (plotConfig = {}, timeseries = []) => {
+ if (!Object.keys(plotConfig).length || !timeseries.length) return [];
+
+ const { plot_type, display } = plotConfig;
+
+ switch (plot_type) {
+ case 'scatter-line': {
+ const { traces = [] } = display || {};
+ const tsIds = traces.map(t => t.timeseries_id);
+
+ return timeseries.filter(ts => tsIds.includes(ts.id));
+ }
+ case 'profile':
+ return [];
+ case 'contour':
+ return [];
+ case 'bullseye':
+ return [];
+ default:
+ return [];
+ }
+};
+
+const buildCwmsTraces = (cwmsData = [], cwmsTimeseries = [], plotConfig) => {
+ if (!cwmsData.length || !cwmsTimeseries.length) return [];
+
+ const data = cwmsData.filter(e => e);
+
+ if (!data.length) return [];
+ const { traces } = plotConfig?.display || {};
+
+ return traces.map(trace => {
+ const { timeseries_id } = trace;
+ const { cwms_timeseries_id, instrument, name, unit } = cwmsTimeseries.find(el => el.id === timeseries_id) || {};
+ const { values = [] } = cwmsData.find(el => el?.name === cwms_timeseries_id) || {};
+
+ const plotData = values.map(el => {
+ if (el?.length < 2) return null;
-export const generateNewChartData = (measurements, timeseries, chartSettings) => {
+ return {
+ x: DateTime.fromMillis(el[0]).toISO(),
+ y: el[1],
+ };
+ }).filter(e => e);
+
+ return {
+ ...getStyle(trace),
+ x: plotData.map(d => d.x),
+ y: plotData.map(d => d.y),
+ name: `${instrument} - ${name} (${unit})` || '',
+ showlegend: true,
+ hoverinfo: 'x+y+text',
+ timeseriesId: timeseries_id,
+ }
+ });
+};
+
+export const generateNewChartData = (measurements, timeseries, chartSettings, plotConfig, hasCwmsData = false, cwmsData = [], midasCwmsTimeseries = []) => {
const { show_comments, show_masked, show_nonvalidated } = chartSettings || {};
+ const { traces } = plotConfig?.display || {};
if (measurements.length && timeseries.length) {
- const data = measurements.map((elem, index) => {
+ const data = measurements.map(elem => {
if (elem && timeseries.length) {
const { items, timeseries_id } = elem;
@@ -24,7 +128,13 @@ export const generateNewChartData = (measurements, timeseries, chartSettings) =>
name,
unit,
parameter,
- } = timeseries.find(t => t.id === timeseries_id);
+ } = timeseries.find(t => t.id === timeseries_id) || {};
+
+ if (!instrument) {
+ // eslint-disable-next-line no-console
+ console.error('Error: timeseries id (%s) does not exist in `timeseries` array.', String(timeseries_id));
+ return;
+ }
const filteredItems = items.filter(item => {
const { masked, validated } = item;
@@ -49,22 +159,24 @@ export const generateNewChartData = (measurements, timeseries, chartSettings) =>
{ x: [], y: [], hovertext: [] }
);
+
+ const trace = traces.find(el => el.timeseries_id === timeseries_id);
+
return parameter === 'precipitation' ? {
+ ...getStyle(trace),
x: x,
y: y,
type: 'bar',
- yaxis: 'y2',
+ yaxis: 'y3',
name: `${instrument} - ${name} (${unit})` || '',
hovertext: show_comments ? hovertext : [],
hoverinfo: 'x+y+text',
showlegend: true,
timeseriesId: timeseries_id,
} : {
- ...getStyle(index),
+ ...getStyle(trace),
x: x,
y: y,
- type: 'scattergl',
- mode: 'lines',
name: `${instrument} - ${name} (${unit})` || '',
showlegend: true,
hovertext: show_comments ? hovertext : [],
@@ -74,32 +186,81 @@ export const generateNewChartData = (measurements, timeseries, chartSettings) =>
}
}).filter(e => e);
+ if (hasCwmsData) {
+ data.push(...buildCwmsTraces(cwmsData, midasCwmsTimeseries, plotConfig));
+ }
+
return data || [];
}
return [];
};
+export const generatePayloadForType = (plotType, configName, display, projectId) => {
+ switch (plotType) {
+ case 'scatter-line':
+ case 'profile':
+ case 'contour':
+ case 'bullseye':
+ return _plotPayload(configName, display, projectId);
+ default:
+ throw new Error(`Invalid Plot Type: ${plotType}. Expected one of ['scatter-line', 'profile', 'contour', 'bullseye'].`);
+ }
+};
+
+const _plotPayload = (configName, display, projectId) => ({
+ name: configName.trim(),
+ display,
+ project_id: projectId,
+ ...defaultPlotConfigOptions,
+});
+
+export const formatTimeseriesOptions = (timeseries = []) => (
+ timeseries.map(ts => ({
+ label: `${ts.instrument} - ${ts.name}`,
+ value: ts.id,
+ instrumentName: ts.instrument,
+ }))
+);
+
+export const formatTimeseriesId = (id, timeseries) => {
+ const ts = timeseries.find(t => t.id === id);
+
+ return ts ? {
+ label: `${ts.instrument} - ${ts.name}`,
+ value: id,
+ instrumentName: ts.instrument,
+ } : null;
+};
+
+export const formatInstrumentOptions = instruments => (
+ instruments.map(i => ({
+ label: i.name,
+ value: i.id,
+ }))
+);
-/*
+export const formatInstrument = (id, instruments) => {
+ const i = instruments.find(i => i.id === id);
-Could instead check for first date that is above the lower range and splice out all lower
-and check for first data lower than upper range and splice all higher
+ return i ? {
+ label: i.name,
+ value: i.id,
+ } : null;
+};
-assumption: sorted by dates
+export const formatMeasurementOptions = (timestamps = []) => {
+ const sorted = timestamps?.sort((a, b) => DateTime.fromISO(a).toMillis() - DateTime.fromISO(b).toMillis());
-*/
-// export const limitDatabyDateRange = (datedData = [], dateRange) => {
-// if (datedData[0] && datedData[0].x && datedData[0].y) {
-// for (let i = 0; i < datedData[0].x.length; i++) {
-// const tempDate = new Date(datedData[0].x[i]);
-// if (tempDate > dateRange[1] || tempDate < dateRange[0]) {
-// datedData[0].x.splice(i, 1);
-// datedData[0].y.splice(i, 1);
-// i--;
-// }
-// }
-// }
+ return sorted?.map(m => ({
+ _original: m,
+ label: DateTime.fromISO(m).toLocaleString(DateTime.DATETIME_MED_WITH_SECONDS),
+ value: new Date(m),
+ }));
+};
-// return datedData;
-// };
+export const formatMeasurementTimestamp = time => ({
+ _original: time,
+ label: DateTime.fromISO(time).toLocaleString(DateTime.DATETIME_MED_WITH_SECONDS),
+ value: new Date(time),
+});
diff --git a/src/app-pages/project/batch-plotting/modals/BatchPlotAdvancedSettings.jsx b/src/app-pages/project/batch-plotting/modals/BatchPlotAdvancedSettings.jsx
new file mode 100644
index 00000000..76e587c9
--- /dev/null
+++ b/src/app-pages/project/batch-plotting/modals/BatchPlotAdvancedSettings.jsx
@@ -0,0 +1,77 @@
+import React from 'react';
+import { connect } from 'redux-bundler-react';
+
+import * as Modal from '../../../../app-components/modal';
+import Accordion from '../../../../app-components/accordion';
+import Thresholds from './components/Thresholds';
+import LegendOrder from './components/LegendOrder';
+import SecondaryAxis from './components/SecondaryAxis';
+import PlotSymbology from './components/PlotSymbology';
+
+const BatchPlotAdvancedSettings = connect(
+ 'doSaveBatchPlotConfiguration',
+ ({
+ doSaveBatchPlotConfiguration,
+ plotConfig = {},
+ chartData = [],
+ }) => (
+
+
+
+
+
+
+ {/* Show list of thresholds | display on/off | edit/delete options */}
+
+
+
+
+
+ {/* Secondary Y-Axis: Name | Parameter to fill | on/off */}
+
+
+
+
+
+ {/*
+ Show list of all instruments/parameters. each needs options:
+ color | line-type | marker
+
+ Possibly break down by automatic/manually uploaded data?
+ */}
+
+
+
+
+
+ {/* Ability To Change the order of the timeseries in the legend */}
+
+
+
+
+
+
+
+ )
+);
+
+export default BatchPlotAdvancedSettings;
diff --git a/src/app-pages/project/batch-plotting/modals/BatchPlotConfiguration.jsx b/src/app-pages/project/batch-plotting/modals/BatchPlotConfiguration.jsx
new file mode 100644
index 00000000..a5dbb843
--- /dev/null
+++ b/src/app-pages/project/batch-plotting/modals/BatchPlotConfiguration.jsx
@@ -0,0 +1,112 @@
+import React, { useState } from 'react';
+import { Autocomplete, TextField } from '@mui/material';
+import { connect } from 'redux-bundler-react';
+
+import * as Modal from '../../../../app-components/modal';
+import ScatterLineDisplay from './components/_scatterLineDisplay';
+import ProfileDisplay from './components/_profileDisplay';
+import ContourDisplay from './components/_contourDisplay';
+import BullseyeDisplay from './components/_bullseyeDisplay';
+import { generatePayloadForType } from '../helper';
+
+const BatchPlotConfigurationModal = connect(
+ 'doSaveBatchPlotConfiguration',
+ 'selectProjectsIdByRoute',
+ ({
+ doSaveBatchPlotConfiguration,
+ projectsIdByRoute: project,
+ isEdit = false,
+ initConfig = {},
+ }) => {
+ const title = `${isEdit ? 'Edit' : 'New'} Batch Plot Configuration`;
+
+ const { id, name, plot_type, display = {} } = initConfig;
+
+ const [inputError, setInputError] = useState('');
+ const [newConfigName, setNewConfigName] = useState(name ?? '');
+ const [plotType, setPlotType] = useState(plot_type || null);
+ const [newDisplay, setNewDisplay] = useState(display || null);
+ const [isDisplayValid, setIsDisplayValid] = useState(false);
+
+ const handleChange = (display, isValid) => {
+ setNewDisplay(display);
+ setIsDisplayValid(isValid)
+ };
+
+ return (
+
+
+
+ {
+ if (inputError) setInputError('');
+ setNewConfigName(e.target.value);
+ }}
+ />
+ setPlotType(val.value)}
+ isOptionEqualToValue={(opt, val) => val ? opt?.value === val : false}
+ options={[
+ { value: 'scatter-line', label: 'Scatter Line' },
+ { value: 'profile', label: 'Depth Profile' },
+ { value: 'contour', label: 'Contour' },
+ { value: 'bullseye', label: 'Bullseye' },
+ ]}
+ renderInput={(params) => (
+
+ )}
+ />
+ {plotType === 'scatter-line' && (
+
+ )}
+ {plotType === 'profile' && (
+
+ )}
+ {plotType === 'contour' && (
+
+ )}
+ {plotType === 'bullseye' && (
+
+ )}
+
+ doSaveBatchPlotConfiguration(plotType, id, generatePayloadForType(plotType, newConfigName, newDisplay, project.projectId))}
+ />
+
+ );
+ },
+);
+
+export default BatchPlotConfigurationModal;
diff --git a/src/app-pages/project/batch-plotting/modals/components/LegendOrder.jsx b/src/app-pages/project/batch-plotting/modals/components/LegendOrder.jsx
new file mode 100644
index 00000000..90cc2d5c
--- /dev/null
+++ b/src/app-pages/project/batch-plotting/modals/components/LegendOrder.jsx
@@ -0,0 +1,115 @@
+import React, { useState } from 'react';
+import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
+import { Button, List, ListItem } from '@mui/material';
+
+const LegendOrder = ({
+ chartData,
+ plotConfig,
+ doSaveBatchPlotConfiguration,
+}) => {
+ const { display, plot_type, id } = plotConfig;
+
+ const [items, setItems] = useState(chartData.map(el => ({
+ name: el.name,
+ id: el.timeseriesId,
+ })));
+
+ const handleDragEnd = (result) => {
+ const { source, destination } = result;
+ const { index: sourceIndex } = source;
+ const { index: destinationIndex } = destination;
+
+ const clone = [...items];
+ const movedItem = clone.splice(sourceIndex, 1);
+ const newItems = [];
+
+ clone.forEach((el, index) => {
+ if (index === destinationIndex) {
+ newItems.push(movedItem[0]);
+ }
+ newItems.push(el);
+ });
+
+ if (items.length !== newItems.length) {
+ newItems.push(movedItem[0]);
+ }
+
+ setItems(newItems);
+ };
+
+ const saveLegendOrder = () => {
+ const traces = display.traces;
+ const newTraces = traces.map(trace => {
+ const newOrder = items.findIndex(el => el.id === trace.timeseries_id);
+
+ return {
+ ...trace,
+ trace_order: newOrder,
+ };
+ })
+
+ doSaveBatchPlotConfiguration(
+ plot_type,
+ id,
+ {
+ ...plotConfig,
+ display: {
+ ...display,
+ traces: newTraces,
+ }
+ }
+ );
+ };
+
+ return (
+ <>
+
+ Drag and Drop the timeseries items below to the order you'd like the plot legend to appear in.
+ Note: This will also effect the rendering of the plot, where items lower on the list are rendered last and will be the front-most trace.
+
+
+
+ {(provided) => (
+
+ {items.map((item, index) => (
+
+ {(provided) => (
+
+ {item.name}
+
+ )}
+
+ ))}
+ {provided.placeholder}
+
+ )}
+
+
+
saveLegendOrder()}
+ >
+ Save Legend Order
+
+ >
+ );
+};
+
+export default LegendOrder;
diff --git a/src/app-pages/project/batch-plotting/modals/components/PlotSymbology.jsx b/src/app-pages/project/batch-plotting/modals/components/PlotSymbology.jsx
new file mode 100644
index 00000000..bf4fb17d
--- /dev/null
+++ b/src/app-pages/project/batch-plotting/modals/components/PlotSymbology.jsx
@@ -0,0 +1,242 @@
+import React, { useEffect, useState } from 'react';
+import createPlotlyComponent from 'react-plotly.js/factory';
+import { Checkbox, Select, MenuItem, FormControl, FormControlLabel, Input, InputLabel, Button } from '@mui/material';
+import { useDeepCompareEffect } from 'react-use';
+import { MuiColorInput } from 'mui-color-input';
+
+import Plotly from '../../../../../app-components/chart/minify-plotly';
+
+const Plot = createPlotlyComponent(Plotly);
+
+const TimeseriesSettings = ({
+ chartData,
+ selected,
+ setSettings,
+}) => {
+ const currentSettings = chartData.find(el => el.timeseriesId === selected);
+ const { line, mode } = currentSettings || {};
+
+ const [selectedStyles, setSelectedStyles] = useState({
+ dash: line?.dash || 'solid',
+ color: line?.color || '',
+ width: line?.width || 3,
+ markers: mode === 'lines+markers',
+ });
+
+ useEffect(() => {
+ setSelectedStyles({
+ dash: line?.dash || 'solid',
+ color: line?.color || '',
+ width: line?.width || 3,
+ markers: mode === 'lines+markers',
+ });
+ }, [selected]);
+
+ useDeepCompareEffect(() => {
+ setSettings(selectedStyles);
+ }, [selectedStyles]);
+
+ return (
+
+
+
+ Line Style
+ setSelectedStyles(prev => ({ ...prev, dash: e.target.value }))}
+ >
+ Solid
+ Dotted
+ Dash-Dotted
+
+
+
+
+
+ setSelectedStyles(prev => ({ ...prev, color: val }))}
+ />
+ setSelectedStyles(prev => ({ ...prev, color: e.target.value }))}
+ />
+
+
+
+
+ Line Width
+ setSelectedStyles(prev => ({ ...prev, width: e.target.value }))}
+ />
+
+
+
+
+ setSelectedStyles(prev => ({ ...prev, markers: checked }))}
+ />
+ )}
+ />
+
+
+
+ );
+};
+
+const generateData = settings => {
+ const { dash, width, color, markers } = settings || {};
+
+ return [{
+ x: [1, 2, 3, 4],
+ y: [2, 1, 4, 3],
+ type: 'scattergl',
+ mode: markers ? 'lines+markers' : 'lines',
+ showlegend: false,
+ line: {
+ dash: dash,
+ color: color === '#ffffff' ? null : color,
+ width: width,
+ },
+ marker: {
+ size: Number(width) ? (Number(width) * 2) + 3 : 5,
+ color: color === '#ffffff' ? null : color,
+ },
+ }]
+};
+
+const ExamplePlot = ({ settings }) => {
+ const [data, setData] = useState(generateData(settings));
+
+ useDeepCompareEffect(() => {
+ setData(generateData(settings));
+ }, [settings]);
+
+ return (
+
+ );
+};
+
+const PlotSymbology = ({
+ plotConfig,
+ chartData,
+ doSaveBatchPlotConfiguration,
+}) => {
+ const { display, plot_type, id } = plotConfig;
+ const [settings, setSettings] = useState({});
+ const [selectedTs, setSelectedTs] = useState(undefined);
+
+ const items = chartData.map(el => ({
+ name: el.name,
+ id: el.timeseriesId,
+ }));
+
+ const saveTraceSettings = () => {
+ const traces = display?.traces || [];
+ const index = traces.findIndex(trace => trace.timeseries_id === selectedTs);
+ const newTrace = {
+ ...index !== -1 ? traces[index] : {},
+ color: settings.color,
+ show_markers: settings.markers,
+ width: Number(settings.width),
+ line_style: settings.dash,
+ };
+
+ traces.splice(index, 1, newTrace)
+
+ doSaveBatchPlotConfiguration(
+ plot_type,
+ id,
+ {
+ ...plotConfig,
+ display: {
+ ...display,
+ traces,
+ },
+ }
+ );
+ };
+
+ return (
+ <>
+
+ Timeseries
+ {
+ setSettings({});
+ setSelectedTs(e.target.value)
+ }}
+ >
+ {items.map(el => (
+ {el.name}
+ ))}
+
+
+ {selectedTs && (
+ <>
+
+
Use the settings below to customize the line style for the selected timeseries. Setting line color to white/#ffffff will allow the plot to use a semi-random generated color.
+
+
saveTraceSettings(selectedTs)}
+ >
+ Save Line Settings
+
+
+
+ >
+ )}
+ >
+ )
+};
+
+export default PlotSymbology;
\ No newline at end of file
diff --git a/src/app-pages/project/batch-plotting/modals/components/SecondaryAxis.jsx b/src/app-pages/project/batch-plotting/modals/components/SecondaryAxis.jsx
new file mode 100644
index 00000000..31733eb6
--- /dev/null
+++ b/src/app-pages/project/batch-plotting/modals/components/SecondaryAxis.jsx
@@ -0,0 +1,125 @@
+import React, { useState } from 'react';
+import { Button, FormControl, Input, InputLabel, MenuItem, Select } from '@mui/material';
+import { connect } from 'redux-bundler-react';
+
+const generateTimeseriesOptions = (timeseriesIds = [], timeseries = []) => {
+ const options = [];
+
+ timeseriesIds.forEach(id => {
+ const found = timeseries.find(el => el.id === id);
+
+ if (found) options.push({
+ value: id,
+ label: `${found?.instrument} - ${found?.name} (${found?.parameter})`,
+ });
+ });
+
+ return options;
+};
+
+const SecondaryAxis = connect(
+ 'selectInstrumentTimeseriesItems',
+ ({
+ instrumentTimeseriesItems: timeseries,
+ chartData,
+ plotConfig,
+ doSaveBatchPlotConfiguration,
+ }) => {
+ const { display, plot_type, id } = plotConfig || {};
+
+ const [axisSettings, setAxisSettings] = useState({
+ primaryTitle: display?.layout?.y_axis_title || 'Measurement',
+ secondaryTitle: display?.layout?.y2_axis_title,
+ timeseriesIds: display?.traces?.filter(trace => trace.y_axis === 'y2').map(el => el.timeseries_id) || [],
+ });
+ const chartTimeseriesIds = chartData?.map(d => d.timeseriesId);
+ const timeseriesOptions = generateTimeseriesOptions(chartTimeseriesIds, timeseries);
+
+ const saveSecondaryAxisSettings = () => {
+ const { primaryTitle, secondaryTitle, timeseriesIds } = axisSettings;
+
+ const newTraces = display.traces.map(trace => {
+ let axis = 'y1';
+ if (timeseriesIds.includes(trace.timeseries_id)) {
+ axis = 'y2';
+ }
+ return {
+ ...trace,
+ y_axis: axis,
+ };
+ });
+
+ doSaveBatchPlotConfiguration(
+ plot_type,
+ id,
+ {
+ ...plotConfig,
+ display: {
+ ...display,
+ layout: {
+ ...display.layout,
+ y_axis_title: primaryTitle,
+ y2_axis_title: secondaryTitle,
+ },
+ traces: newTraces,
+ },
+ }
+ );
+ };
+
+ return (
+ <>
+
+ Primary Axis Title
+ setAxisSettings(prev => ({ ...prev, primaryTitle: e.target.value }))}
+ />
+
+
+
+ Fill in the information below to apply timeseries to a secondary axis on the batch plot. If no timeseries are provided, the secondary axis will not be displayed on the plot.
+
+
+ Secondary Axis Title
+ setAxisSettings(prev => ({ ...prev, secondaryTitle: e.target.value }))}
+ />
+
+
+ Timeseries
+ setAxisSettings(prev => ({ ...prev, timeseriesIds: e.target.value }))}
+ >
+ {timeseriesOptions?.map(option => {
+ const { value, label } = option;
+
+ return {label} ;
+ })}
+
+
+
saveSecondaryAxisSettings()}
+ >
+ Save Axis Settings
+
+ >
+ );
+ },
+);
+
+export default SecondaryAxis;
diff --git a/src/app-pages/project/batch-plotting/modals/components/Thresholds.jsx b/src/app-pages/project/batch-plotting/modals/components/Thresholds.jsx
new file mode 100644
index 00000000..98e22275
--- /dev/null
+++ b/src/app-pages/project/batch-plotting/modals/components/Thresholds.jsx
@@ -0,0 +1,199 @@
+import React, { useState } from 'react';
+import { Button, Checkbox, IconButton, Input } from '@mui/material';
+import { Check, DeleteOutline } from '@mui/icons-material';
+import { MuiColorInput } from 'mui-color-input';
+
+const newThreshold = {
+ name: 'Threshold',
+ data_point: 0,
+ enabled: true,
+ color: '#000000',
+};
+
+const Thresholds = ({
+ plotConfig,
+ doSaveBatchPlotConfiguration,
+}) => {
+ const { display, plot_type, id } = plotConfig || {};
+ const currentShapes = display?.layout?.custom_shapes || [];
+ const [workingData, setWorkingData] = useState({});
+ const [key, setKey] = useState(0);
+
+ const saveThreshold = (threshold) => {
+ const shapes = display?.layout?.custom_shapes || [];
+
+ if (!threshold) shapes.push(newThreshold);
+ else {
+ const workingShape = shapes[threshold.index];
+ shapes.splice(threshold.index, 1, {
+ ...workingShape,
+ ...threshold,
+ })
+ }
+
+ doSaveBatchPlotConfiguration(
+ plot_type,
+ id,
+ {
+ ...plotConfig,
+ display: {
+ ...display,
+ layout: {
+ ...display.layout,
+ custom_shapes: shapes,
+ },
+ },
+ }
+ );
+
+ if (threshold) {
+ setWorkingData(prev => ({ ...prev, [threshold.index]: undefined }))
+ }
+
+ setKey(prev => prev + 1);
+ };
+
+ const deleteThreshold = index => {
+ const shapes = display?.layout?.custom_shapes || [];
+ shapes.splice(index, 1);
+
+ doSaveBatchPlotConfiguration(
+ plot_type,
+ id,
+ {
+ ...plotConfig,
+ display: {
+ ...plotConfig.display,
+ layout: {
+ ...plotConfig.display.layout,
+ custom_shapes: shapes,
+ },
+ },
+ }
+ );
+
+ setKey(prev => prev + 1);
+ };
+
+ return (
+
+ {currentShapes.length ? (
+ <>
+
+ Below is this list of Thresholds for the current plot, including generated thresholds from alerts and manually entered thresholds.
+ Each manual threshold's name and value can be edited, disabled for display, or removed completely.
+
+
+
+ Name
+
+
+ Value
+
+
+ Color
+
+
+ Enabled
+
+
+
+
+ {currentShapes.map((shape, index) => (
+ <>
+
+
+ {
+ setWorkingData(prev => ({ ...prev, [index]: { ...prev[index], name: e.target.value }}));
+ }}
+ />
+
+
+ {
+ setWorkingData(prev => ({ ...prev, [index]: { ...prev[index], data_point: Number(e.target.value) }}));
+ }}
+ />
+
+
+ {
+ setWorkingData(prev => ({ ...prev, [index]: { ...prev[index], color: val }}));
+ }}
+ />
+ {
+ setWorkingData(prev => ({ ...prev, [index]: { ...prev[index], color: val }}));
+ }}
+ />
+
+
+ {
+ setWorkingData(prev => ({ ...prev, [index]: { ...prev[index], enabled: checked }}));
+ }}
+ />
+
+
+ {
+ saveThreshold({
+ ...workingData[index],
+ index,
+ });
+ }}
+ >
+
+
+ deleteThreshold(index)}
+ >
+
+
+
+
+
+ >
+ ))}
+ >
+ ) : (
+
+ No thresholds set. Click the Add Threshold button to create one.
+
+ )}
+
saveThreshold()}
+ >
+ Add Threshold
+
+
+ )
+}
+
+export default Thresholds;
diff --git a/src/app-pages/project/batch-plotting/modals/components/_bullseyeDisplay.jsx b/src/app-pages/project/batch-plotting/modals/components/_bullseyeDisplay.jsx
new file mode 100644
index 00000000..56edebe1
--- /dev/null
+++ b/src/app-pages/project/batch-plotting/modals/components/_bullseyeDisplay.jsx
@@ -0,0 +1,77 @@
+import React, { useMemo, useState } from 'react';
+import { Autocomplete, TextField } from '@mui/material';
+import { connect } from 'redux-bundler-react';
+
+import { formatTimeseriesOptions, formatTimeseriesId } from '../../helper';
+import { useDeepCompareEffect } from 'react-use';
+
+const formatDisplay = (xTimeseries, yTimeseries, display) => {
+ return {
+ ...display,
+ x_axis_timeseries_id: xTimeseries?.value,
+ y_axis_timeseries_id: yTimeseries?.value,
+ };
+};
+
+const BullseyeDisplay = connect(
+ 'selectInstrumentTimeseriesItems',
+ ({
+ instrumentTimeseriesItems: timeseries,
+ display,
+ onChange,
+ }) => {
+ const { x_axis_timeseries_id, y_axis_timeseries_id } = display || {};
+ const options = useMemo(() => formatTimeseriesOptions(timeseries).sort((a, b) => -b.label.localeCompare(a.label)), [timeseries]);
+
+ const [xTimeseries, setXTimeseries] = useState(formatTimeseriesId(x_axis_timeseries_id, timeseries));
+ const [yTimeseries, setYTimeseries] = useState(formatTimeseriesId(y_axis_timeseries_id, timeseries));
+
+ useDeepCompareEffect(() => {
+ const newDisplay = formatDisplay(xTimeseries, yTimeseries, display);
+ onChange(newDisplay, (!!xTimeseries && !!yTimeseries));
+ }, [xTimeseries, yTimeseries, onChange]);
+
+ return (
+ <>
+
opt.instrumentName}
+ getOptionLabel={opt => opt.label}
+ options={options}
+ value={xTimeseries}
+ isOptionEqualToValue={(opt, val) => val ? opt?.value === val?.value : false}
+ onChange={(e, val) => setXTimeseries(val)}
+ renderInput={(params) => (
+
+ )}
+ />
+ opt.instrumentName}
+ getOptionLabel={opt => opt.label}
+ options={options}
+ value={yTimeseries}
+ isOptionEqualToValue={(opt, val) => val ? opt?.value === val?.value : false}
+ onChange={(e, val) => setYTimeseries(val)}
+ renderInput={(params) => (
+
+ )}
+ />
+ >
+ );
+ },
+);
+
+export default BullseyeDisplay;
diff --git a/src/app-pages/project/batch-plotting/modals/components/_contourDisplay.jsx b/src/app-pages/project/batch-plotting/modals/components/_contourDisplay.jsx
new file mode 100644
index 00000000..1dfafbb4
--- /dev/null
+++ b/src/app-pages/project/batch-plotting/modals/components/_contourDisplay.jsx
@@ -0,0 +1,141 @@
+import React, { useMemo, useState } from 'react';
+import isEqual from 'lodash.isequal';
+import { Autocomplete, Checkbox, FormControl, FormControlLabel, FormGroup, InputLabel, MenuItem, Select, TextField } from '@mui/material';
+import { connect } from 'redux-bundler-react';
+import { useDeepCompareEffect } from 'react-use';
+
+import { formatTimeseriesOptions, formatTimeseriesId } from '../../helper';
+
+const formatDisplay = (oldDisplay, newOptions) => {
+ const {
+ selectedDuration,
+ isContourSmoothing,
+ isGradientSmoothing,
+ isShowLabels,
+ selectedTimeseries,
+ } = newOptions;
+
+ return {
+ ...oldDisplay,
+ timeseries_ids: selectedTimeseries?.map(t => t.value),
+ show_labels: isShowLabels,
+ contour_smoothing: isContourSmoothing,
+ gradient_smoothing: isGradientSmoothing,
+ locf_backfill: selectedDuration,
+ };
+};
+
+const ContourDisplay = connect(
+ 'selectInstrumentTimeseriesItems',
+ ({
+ instrumentTimeseriesItems: timeseries,
+ display,
+ onChange,
+ }) => {
+ const { contour_smoothing, gradient_smoothing, show_labels, locf_backfill, timeseries_ids } = display || {};
+
+ const [selectedDuration, setSelectedDuration] = useState(locf_backfill || '');
+ const [isContourSmoothing, setIsContourSmoothing] = useState(contour_smoothing || false);
+ const [isGradientSmoothing, setIsGradientSmoothing] = useState(gradient_smoothing || false);
+ const [isShowLabels, setIsShowLabels] = useState(show_labels || false);
+ const [selectedTimeseries, setSelectedTimeseries] = useState(timeseries_ids ? timeseries_ids.map(id => formatTimeseriesId(id, timeseries)) : []);
+
+ const timeseriesOptions = useMemo(() => formatTimeseriesOptions(timeseries).sort((a, b) => -b.label.localeCompare(a.label)), [timeseries]);
+
+ useDeepCompareEffect(() => {
+ const newDisplay = formatDisplay(display, {
+ selectedDuration,
+ isContourSmoothing,
+ isGradientSmoothing,
+ isShowLabels,
+ selectedTimeseries,
+ });
+
+ if (!isEqual(newDisplay, display)) {
+ onChange(newDisplay, (!!selectedDuration && !!selectedTimeseries?.length));
+ }
+ }, [selectedDuration, selectedTimeseries, isShowLabels, isContourSmoothing, isGradientSmoothing, onChange]);
+
+ return (
+ <>
+
+ opt.instrumentName}
+ getOptionLabel={option => option.label}
+ options={timeseriesOptions}
+ value={selectedTimeseries}
+ isOptionEqualToValue={(opt, val) => val ? opt?.value === val.value : false}
+ onChange={(_e, val) => setSelectedTimeseries(val)}
+ renderInput={(params) => (
+
+ )}
+ />
+
+ Duration
+ setSelectedDuration(e.target.value)}
+ >
+ 15 Minutes
+ 30 Minutes
+ 1 Hour
+ 12 Hours
+ 1 Day
+ 1 Week
+ 1 Month
+ Any
+
+
+
+ setIsContourSmoothing(checked)}
+ checked={isContourSmoothing}
+ />
+ )}
+ />
+ setIsGradientSmoothing(checked)}
+ checked={isGradientSmoothing}
+ />
+ )}
+ />
+ setIsShowLabels(checked)}
+ checked={isShowLabels}
+ />
+ )}
+ />
+
+ >
+ );
+ },
+);
+
+export default ContourDisplay;
diff --git a/src/app-pages/project/batch-plotting/modals/components/_profileDisplay.jsx b/src/app-pages/project/batch-plotting/modals/components/_profileDisplay.jsx
new file mode 100644
index 00000000..07c29da8
--- /dev/null
+++ b/src/app-pages/project/batch-plotting/modals/components/_profileDisplay.jsx
@@ -0,0 +1,46 @@
+import React, { useEffect, useMemo, useState } from 'react';
+import { Autocomplete, TextField } from '@mui/material';
+import { connect } from 'redux-bundler-react';
+import { formatInstrument, formatInstrumentOptions } from '../../helper';
+
+const ProfileDisplay = connect(
+ 'selectInstrumentsItems',
+ ({
+ instrumentsItems: instruments,
+ display,
+ onChange,
+ }) => {
+ const { instrument_id } = display || {};
+ const [newInstrument, setNewInstrument] = useState(formatInstrument(instrument_id, instruments));
+
+ const instrumentOptions = useMemo(() => formatInstrumentOptions(instruments), [instruments]);
+
+ useEffect(() => {
+ onChange({
+ instrument_id: newInstrument,
+ }, !!newInstrument);
+ }, [newInstrument, onChange]);
+
+ return (
+ opt.label}
+ options={instrumentOptions}
+ value={newInstrument}
+ isOptionEqualToValue={(opt, val) => val ? opt?.value === val?.value : false}
+ onChange={(e, val) => setNewInstrument(val)}
+ renderInput={(params) => (
+
+ )}
+ />
+ );
+ },
+);
+
+export default ProfileDisplay;
diff --git a/src/app-pages/project/batch-plotting/modals/components/_scatterLineDisplay.jsx b/src/app-pages/project/batch-plotting/modals/components/_scatterLineDisplay.jsx
new file mode 100644
index 00000000..53b652b1
--- /dev/null
+++ b/src/app-pages/project/batch-plotting/modals/components/_scatterLineDisplay.jsx
@@ -0,0 +1,66 @@
+import React, { useEffect, useMemo, useState } from 'react';
+import { Autocomplete, TextField } from '@mui/material';
+import { connect } from 'redux-bundler-react';
+import { formatTimeseriesOptions, formatTimeseriesId } from '../../helper';
+
+const formatDisplay = (timeseries, display) => {
+ return {
+ ...display,
+ traces: timeseries.map((ts, index) => ({
+ timeseries_id: ts.value,
+ color: '#ffffff',
+ trace_order: index,
+ y_axis: 'y1',
+ line_style: 'solid',
+ show_markers: true,
+ width: 3,
+ }))
+ };
+};
+
+const ScatterLineDisplay = connect(
+ 'selectInstrumentTimeseriesItems',
+ ({
+ instrumentTimeseriesItems: instrumentTimeseries,
+ display,
+ onChange = () => {},
+ }) => {
+ const { traces = [] } = display || {};
+ const timeseries = useMemo(() => formatTimeseriesOptions(instrumentTimeseries), [instrumentTimeseries]);
+
+ const [timeseriesInput, setTimeseriesInput] = useState('');
+ const [selectedTimeseries, setSelectedTimeseries] = useState(traces.length ? traces.map(trace => formatTimeseriesId(trace.timeseries_id, instrumentTimeseries)) : []);
+
+ useEffect(() => {
+ const newDisplay = formatDisplay(selectedTimeseries, display);
+ onChange(newDisplay, !!selectedTimeseries.length);
+ }, [selectedTimeseries, formatDisplay, onChange]);
+
+ return (
+ opt.instrumentName}
+ getOptionLabel={option => option.label}
+ options={timeseries.sort((a, b) => -b.label.localeCompare(a.label))}
+ value={selectedTimeseries}
+ isOptionEqualToValue={(opt, val) => val ? opt?.value === val.value : false}
+ onChange={(e, val) => setSelectedTimeseries(val)}
+ renderInput={(params) => (
+ setTimeseriesInput(e.target.value)}
+ variant='outlined'
+ label='Timeseries'
+ />
+ )}
+ />
+ );
+ },
+);
+
+export default ScatterLineDisplay;
diff --git a/src/app-pages/project/batch-plotting/tab-content/batch-plot-chart.jsx b/src/app-pages/project/batch-plotting/tab-content/batch-plot-chart.jsx
deleted file mode 100644
index 3f143b7a..00000000
--- a/src/app-pages/project/batch-plotting/tab-content/batch-plot-chart.jsx
+++ /dev/null
@@ -1,155 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import { connect } from 'redux-bundler-react';
-import { subDays } from 'date-fns';
-
-import Chart from '../../../../app-components/chart/chart';
-import ChartErrors from '../components/batch-plot-errors';
-import ChartSettings from '../components/batch-plot-chart-settings';
-import { generateNewChartData } from '../helper';
-
-const BatchPlotChart = connect(
- 'doPrintSetData',
- 'doInstrumentTimeseriesSetActiveId',
- 'doTimeseriesMeasurementsFetchById',
- 'doBatchPlotConfigurationsSave',
- 'selectBatchPlotConfigurationsActiveId',
- 'selectBatchPlotConfigurationsItemsObject',
- 'selectTimeseriesMeasurementsItems',
- 'selectInstrumentTimeseriesItems',
- ({
- doPrintSetData,
- doInstrumentTimeseriesSetActiveId,
- doTimeseriesMeasurementsFetchById,
- doBatchPlotConfigurationsSave,
- batchPlotConfigurationsActiveId,
- batchPlotConfigurationsItemsObject,
- timeseriesMeasurementsItems,
- instrumentTimeseriesItems,
- }) => {
- const [timeseriesIds, setTimeseriesId] = useState([]);
- const [measurements, setMeasurements] = useState([]);
- const [chartData, setChartData] = useState([]);
- const [dateRange, setDateRange] = useState([subDays(new Date(), 365), new Date()]);
- const [threshold, setThreshold] = useState(3000);
- const [withPrecipitation, setWithPrecipitation] = useState(false);
- const [chartSettings, setChartSettings] = useState({ auto_range: false });
-
- const layout = {
- xaxis: {
- autorange: chartSettings?.auto_range,
- range: dateRange,
- title: 'Date',
- showline: true,
- mirror: true,
- },
- yaxis: {
- title: 'Measurement',
- showline: true,
- mirror: true,
- domain: [0, withPrecipitation ? 0.66 : 1],
- },
- ...(withPrecipitation && {
- yaxis2: {
- title: 'Rainfall',
- autorange: 'reversed',
- showline: true,
- mirror: true,
- domain: [0.66, 1],
- },
- }),
- autosize: true,
- dragmode: 'pan',
- height: 600,
- };
-
- const savePlotSettings = (params) => {
- timeseriesIds.forEach(id => doTimeseriesMeasurementsFetchById({ timeseriesId: id, dateRange, threshold }));
- doBatchPlotConfigurationsSave(...params);
- };
-
- /** Load specific timeseries ids into state when new configurations are loaded */
- useEffect(() => {
- const config = batchPlotConfigurationsItemsObject[batchPlotConfigurationsActiveId];
- setTimeseriesId((config || {}).timeseries_id || []);
- setChartSettings(config);
- }, [
- batchPlotConfigurationsActiveId,
- batchPlotConfigurationsItemsObject,
- setTimeseriesId,
- setChartSettings,
- ]);
-
- /** Fetch the timeseries measurements in regards to date range */
- useEffect(() => {
- // @TODO: new endpoint: fetch -> batchPlotMeasurements(plotConfigId);
- timeseriesIds.forEach(id => doTimeseriesMeasurementsFetchById({ timeseriesId: id, dateRange, threshold }));
- }, [timeseriesIds, doInstrumentTimeseriesSetActiveId]);
-
- /** Extract specific measurements from the store that relate to our set timeseries */
- useEffect(() => {
- const measurementItems = timeseriesIds.map(id =>
- timeseriesMeasurementsItems.find(elem => elem.timeseries_id === id)
- );
-
- setMeasurements(measurementItems);
- }, [timeseriesIds, timeseriesMeasurementsItems, setMeasurements]);
-
- /** When we get new measurements, update chart data */
- useEffect(() => {
- const newData = generateNewChartData(measurements, instrumentTimeseriesItems, chartSettings);
-
- setChartData(newData);
- }, [measurements, instrumentTimeseriesItems, withPrecipitation, chartSettings]);
-
- /** When chart data changes, see if there is precip data to adjust plot */
- useEffect(() => {
- if (
- chartData.length &&
- chartData.find(elem => elem?.yaxis === 'y2')
- ) {
- setWithPrecipitation(true);
- } else {
- setWithPrecipitation(false);
- }
- doPrintSetData(chartData, layout);
- }, [chartData, setWithPrecipitation]);
-
- return (
- <>
-
-
- {chartSettings ? (
- <>
-
-
- >
- ) : null}
- >
- );
- }
-);
-
-export default BatchPlotChart;
diff --git a/src/app-pages/project/batch-plotting/tab-content/depth-chart.jsx b/src/app-pages/project/batch-plotting/tab-content/depth-chart.jsx
deleted file mode 100644
index d6e21fda..00000000
--- a/src/app-pages/project/batch-plotting/tab-content/depth-chart.jsx
+++ /dev/null
@@ -1,219 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import { connect } from 'redux-bundler-react';
-import { Slider } from '@mui/material';
-import { DateTime } from 'luxon';
-
-import Chart from '../../../../app-components/chart/chart';
-
-const colors = [
- '#800000',
- '#000075',
- '#e6194B',
- '#3cb44b',
- '#911eb4',
- '#fabed4',
-];
-
-const formatData = (data = [], indexes = []) => {
- const inclinometerIds = Object.keys(data);
- if (!inclinometerIds.length) return {};
-
- const workingData = data[inclinometerIds[0]].inclinometers;
-
- const depthIncrements = workingData.map((datum, i) => {
- const { time, values } = datum;
-
- const valueDisplacement = values.sort((a, b) => b.nDepth - a.nDepth);
-
- return { time, valueDisplacement, colorIndex: i };
- }).sort((a, b) => DateTime.fromISO(a.time).toMillis() - DateTime.fromISO(b.time).toMillis());
-
- const relevantData = depthIncrements.slice(indexes[0], indexes[1] + 1);
- const dataArray = [];
-
- for (let i = 0; i < relevantData.length; i++) {
- const { valueDisplacement, time, colorIndex } = relevantData[i];
-
- dataArray.push(
- valueDisplacement.reduce((accum, current, ind) => {
- const { depth, aIncrement, bIncrement } = current;
-
- if (ind === 0) {
- accum.nDepth.push(depth);
- accum.aIncrement.push(aIncrement);
- accum.bIncrement.push(bIncrement);
- accum.time = DateTime.fromISO(time).toFormat('MMM dd, yyyy hh:mm:ss');
- accum.colorIndex = colorIndex;
- } else {
- accum.nDepth.push(depth);
- accum.aIncrement.push(accum.aIncrement[ind - 1] + aIncrement);
- accum.bIncrement.push(accum.bIncrement[ind - 1] + bIncrement);
- accum.time = DateTime.fromISO(time).toFormat('MMM dd, yyyy hh:mm:ss');
- accum.colorIndex = colorIndex;
- }
-
- return accum;
- }, {
- nDepth: [],
- aIncrement: [],
- bIncrement: [],
- time: '',
- colorIndex: '',
- })
- )
- }
-
- return { depthIncrements, dataArray, relevantData };
-};
-
-const build3dTraces = (dataArray, unit) => dataArray.map(data => (
- {
- x: data.aIncrement,
- y: data.bIncrement,
- z: data.nDepth,
- mode: 'markers+lines',
- marker: { size: 3, color: colors[data.colorIndex % colors.length] },
- line: { width: 1 },
- type: 'scatter3d',
- name: `${data.time} Cumulative Displacement (in ${unit})`,
- }
-
- // If client wants A and B Displacement on the 3-D plot, add these back in and adjust function to a forEach using push logic.
- // , {
- // x: data.aIncrement,
- // y: new Array(data.bIncrement.length).fill(0),
- // z: data.nDepth,
- // mode: 'markers+lines',
- // marker: { size: 5, color: 'green' },
- // type: 'scatter3d',
- // name: `A Displacement (in ${unit})`,
- // }, {
- // x: new Array(data.aIncrement.length).fill(0),
- // y: data.bIncrement,
- // z: data.nDepth,
- // mode: 'markers+lines',
- // marker: { size: 5, color: 'orange' },
- // type: 'scatter3d',
- // name: `B Displacement (in ${unit})`
- // }
-));
-
-const build2dTrace = (dataArray, key, unit) => dataArray.map(data => (
- {
- x: data[key],
- y: data.nDepth,
- mode: 'markers+lines',
- marker: { size: 5, color: colors[data.colorIndex % colors.length] },
- line: { width: 1 },
- type: 'scatter',
- name: `${key} Displacement (in ${unit})`,
- hovertemplate: `
- ${data.time}
- Depth: %{y}
- Displacement: %{x}
-
- `,
- }
-));
-
-const DepthChart = connect(
- 'doFetchInclinometerMeasurementsByTimeseriesId',
- 'selectCurrentInclinometerMeasurements',
- ({
- doFetchInclinometerMeasurementsByTimeseriesId,
- currentInclinometerMeasurements,
- inclinometerTimeseriesIds,
- }) => {
- const [sliderVal, setSliderVal] = useState([0, 0]);
-
- useEffect(() => {
- inclinometerTimeseriesIds.forEach(id => {
- doFetchInclinometerMeasurementsByTimeseriesId(id);
- });
- }, [inclinometerTimeseriesIds, doFetchInclinometerMeasurementsByTimeseriesId]);
-
- const inclinometerIds = Object.keys(currentInclinometerMeasurements || {});
- const isMetric = false;
- const unit = isMetric ? 'mm' : 'inches';
- const { dataArray = [], depthIncrements = [] } = formatData(currentInclinometerMeasurements, sliderVal, isMetric);
-
- const config = {
- repsonsive: true,
- displaylogo: false,
- displayModeBar: true,
- scrollZoom: true,
- };
-
- const layout3d = {
- autosize: true,
- height: 800,
- scene: {
- xaxis: { title: `A-Displacement (in ${unit})` },
- yaxis: { title: `B-Displacement (in ${unit})` },
- zaxis: { title: 'Depth', autorange: 'reversed' },
- },
- legend: {
- 'orientation': 'h',
- },
- };
-
- const layoutTall = (key) => ({
- showlegend: false,
- autosize: true,
- height: 800,
- yaxis: {
- autorange: 'reversed',
- title: `Depth in Feet`,
- },
- xaxis: {
- title: `${key}-Displacement in ${unit}`,
- },
- });
-
- const incrementData = build3dTraces(dataArray, unit);
-
- return inclinometerIds.length ? (
- <>
-
-
-
- {DateTime.fromISO(depthIncrements[val]?.time).toFormat('MMM dd, yyyy hh:mm:ss')} }
- onChange={(_e, newVal) => setSliderVal(newVal)}
- />
-
-
- >
- ) : Loading Chart Data.... ;
- },
-);
-
-export default DepthChart;
diff --git a/src/app-pages/project/dashboard/cards/batchPlotCard.jsx b/src/app-pages/project/dashboard/cards/batchPlotCard.jsx
index 2d1c4b96..ae8ec9ef 100644
--- a/src/app-pages/project/dashboard/cards/batchPlotCard.jsx
+++ b/src/app-pages/project/dashboard/cards/batchPlotCard.jsx
@@ -28,10 +28,8 @@ const BatchPlotCard = connect(
{plots.length ? (
-
- {plots.map((plot, i) => {
- if (i < 5) return createReportRow(project, plot);
- })}
+
+ {plots.map((plot, _i) => createReportRow(project, plot))}
) : (
diff --git a/src/app-pages/project/dashboard/cards/instrumentStatusCard.jsx b/src/app-pages/project/dashboard/cards/instrumentStatusCard.jsx
index 0d5b1f59..b23fcb8f 100644
--- a/src/app-pages/project/dashboard/cards/instrumentStatusCard.jsx
+++ b/src/app-pages/project/dashboard/cards/instrumentStatusCard.jsx
@@ -22,6 +22,8 @@ const InstrumentStatusCard = connect(
const instrumentsByStatus = reduceInstrumentsByStatus(instruments);
const dataValues = Object.values(instrumentsByStatus);
+
+ // NOTE: Possibly remove yellow -> gray
return (
diff --git a/src/app-pages/project/dashboard/cards/reportConfigsCard.jsx b/src/app-pages/project/dashboard/cards/reportConfigsCard.jsx
new file mode 100644
index 00000000..37c92f4d
--- /dev/null
+++ b/src/app-pages/project/dashboard/cards/reportConfigsCard.jsx
@@ -0,0 +1,154 @@
+import React, { useState } from 'react';
+import { connect } from 'redux-bundler-react';
+import { Download, Menu } from '@mui/icons-material';
+import { toast } from 'react-toastify';
+
+import Badge from '../../../../app-components/badge';
+import Button from '../../../../app-components/button';
+import Card from '../../../../app-components/card';
+import Dropdown from '../../../../app-components/dropdown';
+import NewReportConfigContent from '../modals/newReportConfigContent';
+import RoleFilter from '../../../../app-components/role-filter';
+import Table from '../../../../app-components/table';
+import { downloadFinalReport, useGetReportStatus, useInitializeReportDownload } from '../../../../app-services/collections/report-configuration-download.ts';
+import { tUpdateSuccess } from '../../../../common/helpers/toast-helpers.js';
+
+import '../../project.scss';
+
+const downloadEnabled = import.meta.env.VITE_REPORT_DOWNLOAD === 'true';
+
+const NoReportConfigs = ({ doModalOpen, project }) => (
+
+ No Report Configurations in this project.
+
+
+
+ Click the button below to add a new Report Configuration.
+
+ doModalOpen(NewReportConfigContent, {}, 'lg')}
+ />
+
+
+);
+
+const ReportConfigsCard = connect(
+ 'doModalOpen',
+ 'selectProjectReportConfigurations',
+ 'selectProjectsByRoute',
+ ({
+ doModalOpen,
+ projectReportConfigurations: reportConfigs,
+ projectsByRoute: project,
+ }) => {
+ const [toastId, setToastId] = useState(undefined);
+
+ const { id: projectId } = project;
+ const { data: jobDetails, mutate: initReportJobMutator } = useInitializeReportDownload(projectId);
+ const { report_config_id: reportConfigId, id: jobId } = jobDetails || {};
+
+ const { data: currentJob } = useGetReportStatus({ projectId, reportConfigId, jobId }, {
+ enabled: !!((projectId && reportConfigId && jobId) && (['INIT', 'IN_PROGRESS'].includes(jobDetails?.status))),
+ refetchInterval: 3000,
+ });
+ const { file_key, progress } = currentJob || {};
+
+ if (file_key && progress === 100) {
+ tUpdateSuccess(
+ toastId,
+ 'Your file is ready, click this message to open!',
+ {
+ autoClose: false,
+ onClose: () => {
+ downloadFinalReport({ projectId, reportConfigId, jobId });
+ }
+ }
+ );
+ }
+
+ const beginDownloadReportJob = id => {
+ const tId = toast.loading('Getting your report ready for download. This may take a minute...');
+ setToastId(tId);
+ initReportJobMutator(id);
+ };
+
+ return (
+
+
+
+
+ Report Configurations
+
+
+
+ }
+ buttonClasses={['m-0', 'p-0']}
+ menuClasses={['dropdown-menu-right']}
+ >
+ doModalOpen(NewReportConfigContent, {}, 'lg')}>
+ Create Report Configuation
+
+
+
+
+
+
+ {reportConfigs.length ? (
+ (
+ doModalOpen(NewReportConfigContent, { isEdit: true, data: row }, 'lg' )}
+ />
+ ),
+ }, {
+ key: 'creator_username',
+ header: 'Created By',
+ isSortable: true,
+ }, {
+ key: 'actions',
+ header: '',
+ render: (row) => (
+ <>
+ {downloadEnabled ? (
+ }
+ handleClick={() => {
+ const { id } = row;
+ beginDownloadReportJob(id);
+ }}
+ />
+ ) : null}
+ >
+ ),
+ }]}
+ />
+ ) : }
+
+
+ );
+ },
+);
+
+export default ReportConfigsCard;
diff --git a/src/app-pages/project/dashboard/index.jsx b/src/app-pages/project/dashboard/index.jsx
index b11c14e2..379150ce 100644
--- a/src/app-pages/project/dashboard/index.jsx
+++ b/src/app-pages/project/dashboard/index.jsx
@@ -8,14 +8,17 @@ import InstrumentGroupCard from './cards/instrumentGroupCard';
import InstrumentStatusCard from './cards/instrumentStatusCard';
import InstrumentTypeCard from './cards/instrumentTypeCard';
import { isUserAllowed } from '../../../app-components/role-filter';
+import ReportConfigsCard from './cards/reportConfigsCard';
const ProjectDashboard = connect(
'doFetchDataLoggersByProjectId',
+ 'doFetchReportConfigurationsByProjectId',
'selectProfileRolesObject',
'selectProfileIsAdmin',
'selectProjectsByRoute',
({
doFetchDataLoggersByProjectId,
+ doFetchReportConfigurationsByProjectId,
profileRolesObject,
profileIsAdmin,
projectsByRoute: project,
@@ -24,17 +27,22 @@ const ProjectDashboard = connect(
useEffect(() => {
doFetchDataLoggersByProjectId();
- }, [doFetchDataLoggersByProjectId]);
+ doFetchReportConfigurationsByProjectId();
+ }, [doFetchDataLoggersByProjectId, doFetchReportConfigurationsByProjectId]);
return (
project ? (
-
+
{isUserAllowed(profileRolesObject, profileIsAdmin, [`${slug.toUpperCase()}.*`])
- ?
- : null
+ ? (
+ <>
+
+
+ >
+ ) : null
}
diff --git a/src/app-pages/project/dashboard/modals/newReportConfigContent.jsx b/src/app-pages/project/dashboard/modals/newReportConfigContent.jsx
new file mode 100644
index 00000000..d1c79d86
--- /dev/null
+++ b/src/app-pages/project/dashboard/modals/newReportConfigContent.jsx
@@ -0,0 +1,277 @@
+import React, { useState } from 'react';
+import { connect } from 'redux-bundler-react';
+import { FormControl, Input, InputLabel, MenuItem, Select, Switch } from '@mui/material';
+import ReactDatePicker from 'react-datepicker';
+
+import * as Modal from '../../../../app-components/modal';
+import Card from '../../../../app-components/card';
+import { DateTime } from 'luxon';
+
+const timePatterns = ['', 'Custom', '1 month', '1 year', '5 years', 'Lifetime'];
+
+const requiredFields = {
+ name: '',
+ description: '',
+ plot_configs: [],
+};
+
+const initFormState = (initValue = {}) => {
+ const {
+ name = '',
+ description = '',
+ plot_configs = [],
+ global_overrides = {
+ date_range: {
+ enabled: false,
+ value: '',
+ customStart: null, // YYYY-MM-DD YYYY-MM-DD
+ customEnd: null,
+ },
+ show_masked: {
+ enabled: false,
+ value: true,
+ },
+ show_nonvalidated: {
+ enabled: false,
+ value: true,
+ }
+ },
+ } = initValue;
+
+ if (!timePatterns.includes(global_overrides.date_range.value)) {
+ const [start, end] = String(global_overrides.date_range.value).split(' ');
+ global_overrides.date_range.value = 'Custom';
+ global_overrides.date_range.customStart = start;
+ global_overrides.date_range.customEnd = end;
+ }
+
+ return {
+ name,
+ description,
+ plot_configs: plot_configs.map(el => el.id),
+ global_overrides,
+ };
+};
+
+const setGlobalOverrides = (prev, key, subkey, value) => ({
+ ...prev,
+ global_overrides: {
+ ...prev.global_overrides,
+ [key]: {
+ ...prev.global_overrides[key],
+ [subkey]: value,
+ },
+ },
+})
+
+const NewReportConfigContent = connect(
+ 'doCreateNewReportConfiguration',
+ 'doUpdateReportConfiguration',
+ 'selectBatchPlotConfigurationsItems',
+ ({
+ doCreateNewReportConfiguration,
+ doUpdateReportConfiguration,
+ batchPlotConfigurationsItems: plots,
+ isEdit = false,
+ data = {},
+ }) => {
+ const [formState, setFormState] = useState(initFormState(isEdit ? data : {}));
+
+ const handleInputChange = (key, val) => {
+ setFormState(prev => ({ ...prev, [key]: val }));
+ };
+
+ const isSaveEnabled = () => {
+ const requiredKeys = Object.keys(requiredFields);
+
+ return requiredKeys.every(key => {
+ if (Array.isArray(formState[key])) {
+ return formState[key].length > 0;
+ } else {
+ return !!formState[key];
+ }
+ });
+ };
+
+ const saveReportConfig = () => {
+ if (isEdit) {
+ doUpdateReportConfiguration({
+ id: data.id,
+ ...formState,
+ plot_configs: formState.plot_configs?.map(c => plots.find(plot => plot.id === c)),
+ });
+ } else {
+ doCreateNewReportConfiguration({
+ ...formState,
+ plot_configs: formState.plot_configs?.map(c => plots.find(plot => plot.id === c)),
+ });
+ }
+ };
+
+ return (
+
+
+
+
+ Report Name
+ handleInputChange('name', e.target.value)}
+ />
+
+
+ Description
+ handleInputChange('description', e.target.value)}
+ />
+
+
+ Batch Plot Configurations
+ handleInputChange('plot_configs', e.target.value)}
+ >
+ {plots?.map(el => {
+ const { id, name } = el;
+ return (
+ {name}
+ );
+ })}
+
+
+
+
+
+
+
+ Date Range
+
setFormState(prev => setGlobalOverrides(prev, 'date_range', 'enabled', isChecked))}
+ />
+
+ Presets
+ setFormState(prev => setGlobalOverrides(prev, 'date_range', 'value', e.target.value))}
+ sx={{ width: '250px' }}
+ >
+ Custom
+ One (1) Month
+ One (1) Year
+ Five (5) Years
+ Lifetime
+
+
+ {formState?.global_overrides?.date_range?.value === 'Custom' && (
+
+
+ Start Date
+ setFormState(prev => setGlobalOverrides(prev, 'date_range', 'customStart', DateTime.fromJSDate(date).toFormat('yyyy-MM-dd')))}
+ />
+
+
+ End Date
+ setFormState(prev => setGlobalOverrides(prev, 'date_range', 'customEnd', DateTime.fromJSDate(date).toFormat('yyyy-MM-dd')))}
+ />
+
+
+ )}
+
+
+ Masked Values
+ setFormState(prev => setGlobalOverrides(prev, 'show_masked', 'enabled', isChecked))}
+ />
+
+ Select Display Option
+ setFormState(prev => setGlobalOverrides(prev, 'show_masked', 'value', Boolean(e.target.value)))}
+ sx={{ width: '250px' }}
+ >
+ Hide Masked Values
+ Show Masked Values
+
+
+
+ Non-Validated Values
+ setFormState(prev => setGlobalOverrides(prev, 'show_nonvalidated', 'enabled', isChecked))}
+ />
+
+ Select Display Option
+ setFormState(prev => setGlobalOverrides(prev, 'show_nonvalidated', 'value', Boolean(e.target.value)))}
+ sx={{ width: '250px' }}
+ >
+ Hide Non-Validated Values
+ Show Non-Validated Values
+
+
+
+
+
+
+
+ );
+ },
+);
+
+export default NewReportConfigContent;
diff --git a/src/app-pages/project/data-loggers/modals/editMappingRowModal.jsx b/src/app-pages/project/data-loggers/modals/editMappingRowModal.jsx
index 6539a322..954af06f 100644
--- a/src/app-pages/project/data-loggers/modals/editMappingRowModal.jsx
+++ b/src/app-pages/project/data-loggers/modals/editMappingRowModal.jsx
@@ -15,6 +15,7 @@ const generateTimeseriesOptions = (timeseries = {}, selectedInstrument) => {
const tIds = Object.keys(timeseries);
return tIds.map(t => {
+ if (timeseries[t].is_computed) return null;
if (timeseries[t].instrument_id === selectedInstrument) {
return ({
label: timeseries[t].name,
diff --git a/src/app-pages/project/index.jsx b/src/app-pages/project/index.jsx
index 14b5c3e4..c58133f2 100644
--- a/src/app-pages/project/index.jsx
+++ b/src/app-pages/project/index.jsx
@@ -49,7 +49,7 @@ const Project = connect(
}, {
title:
} />,
content:
,
- paddingSmall: true,
+ paddingSmall: false,
uri: '#explorer',
}, {
title:
} />,
@@ -64,7 +64,7 @@ const Project = connect(
paddingSmall: true,
uri: '#uploader',
}, {
- title:
} />,
+ title:
} />,
content:
,
paddingSmall: true,
uri: '#batch-plotting',
diff --git a/src/app-pages/signup/signup.jsx b/src/app-pages/signup/signup.jsx
deleted file mode 100644
index fb922b3b..00000000
--- a/src/app-pages/signup/signup.jsx
+++ /dev/null
@@ -1,81 +0,0 @@
-import React, { useRef } from 'react';
-import { connect } from 'redux-bundler-react';
-
-import Card from '../../app-components/card';
-import ProfileForm from '../../app-components/profile-form';
-
-export default connect(
- 'doProfileSave',
- 'doUpdateRelativeUrl',
- 'selectAuthIsLoggedIn',
- 'selectProfileActive',
- ({
- doProfileSave,
- doUpdateRelativeUrl,
- authIsLoggedIn: isLoggedIn,
- profileActive: profile,
- }) => {
- // If user already has a profile or is not logged in,
- // i.e. navigated directly to "/signup", redirect them back to home.
- if (profile || !isLoggedIn) {
- doUpdateRelativeUrl('/');
- }
-
- const form = useRef();
- const handleSave = () => {
- if (form.current) {
- form.current.save();
- doUpdateRelativeUrl('/');
- }
- };
-
- return (
-
-
-
-
-
- Create your profile to continue
-
-
- This information will be used to display your identity
- associated with changes you make within this system and to
- facilitate sending notifications. We wont send you any
- notifications unless you or an admin subscribe to alerts.
-
-
-
-
-
-
-
-
-
- );
- }
-);
diff --git a/src/app-services/collections/cwms-timeseries.ts b/src/app-services/collections/cwms-timeseries.ts
new file mode 100644
index 00000000..4f6f2d57
--- /dev/null
+++ b/src/app-services/collections/cwms-timeseries.ts
@@ -0,0 +1,113 @@
+import { QueryClient, useMutation, useQuery } from '@tanstack/react-query';
+
+import { apiGet, apiPost, apiPut, buildQueryParams } from '../fetch-helpers';
+
+type CwmsTimeseries = {
+ name: string,
+ instrument_id: string,
+ unit_id: string,
+ parameter_id: string,
+ cwms_office_id: string,
+ cwms_timeseries_id: string,
+ cwms_extent_earliest_time: string,
+};
+
+interface OfficeParams {
+ like?: string,
+ hasData?: boolean,
+}
+
+interface CwmsTimeseriesParams {
+ office: string;
+}
+
+interface CwmsTimeseriesMeasurementParams {
+ name: string,
+ office?: string,
+ page?: string,
+ begin?: string,
+ end?: string,
+}
+
+interface MidasCwmsPostTimeseriesParams {
+ projectId: string,
+ instrumentId: string,
+ body: CwmsTimeseries[],
+}
+
+interface MidasCwmsPutTimeseriesParams {
+ projectId: string,
+ instrumentId: string,
+ timeseriesId: string,
+ body: CwmsTimeseries,
+}
+
+export const useGetCwmsOffices = ({ hasData = true }: OfficeParams, opts: ClientQueryOptions) => {
+ const uri = `/offices${buildQueryParams({ hasData })}`;
+
+ return useQuery({
+ queryKey: [`cwmsOffices`, hasData],
+ queryFn: () => apiGet(uri, 'CWMS'),
+ ...opts,
+ });
+};
+
+export const useGetCwmsTimeseries = ({ office }: CwmsTimeseriesParams, opts: ClientQueryOptions) => {
+ const uri = `/catalog/TIMESERIES?office=${office}&page-size=5000`;
+
+ return useQuery({
+ queryKey: [`cwmsTimeseries`, office],
+ queryFn: () => apiGet(uri, 'CWMS'),
+ ...opts,
+ });
+};
+
+export const useGetCwmsTimeseriesMeasurements = ({ name, office, page, begin, end }: CwmsTimeseriesMeasurementParams, opts: ClientQueryOptions) => {
+ const uri = `/timeseries${buildQueryParams({ name, office, page, begin, end })}`;
+
+ return useQuery({
+ queryKey: [`cwmsTimeseriesMeasurements`, name, office, page, begin, end],
+ queryFn: () => apiGet(uri, 'CWMS'),
+ ...opts,
+ });
+};
+
+export const useGetMidasCwmsTimeseries = ({ projectId, instrumentId }: MidasCwmsPostTimeseriesParams, opts: ClientQueryOptions) => {
+ const uri = `/projects/${projectId}/instruments/${instrumentId}/timeseries/cwms`;
+
+ return useQuery({
+ queryKey: [`midasCwmsTimeseries`, projectId, instrumentId],
+ queryFn: () => apiGet(uri),
+ ...opts,
+ });
+};
+
+export const usePostMidasCwmsTimeseries = (client: QueryClient) => {
+ return useMutation({
+ mutationFn: ({ projectId, instrumentId, body }: MidasCwmsPostTimeseriesParams) => {
+ const uri = `/projects/${projectId}/instruments/${instrumentId}/timeseries/cwms`;
+
+ return apiPost(uri, body);
+ },
+ onSuccess: (_body, _, _ctx) => {
+ client.invalidateQueries({
+ queryKey: ['midasCwmsTimeseries'],
+ })
+ },
+ });
+};
+
+export const usePutMidasCwmsTimeseries = (client: QueryClient) => {
+ return useMutation({
+ mutationFn: ({ projectId, instrumentId, timeseriesId, body }: MidasCwmsPutTimeseriesParams) => {
+ const uri = `/projects/${projectId}/instruments/${instrumentId}/timeseries/cwms/${timeseriesId}`;
+
+ return apiPut(uri, body);
+ },
+ onSuccess: (_body, _, _ctx) => {
+ client.invalidateQueries({
+ queryKey: ['midasCwmsTimeseries'],
+ })
+ },
+ });
+};
diff --git a/src/app-services/collections/instrument-timeseries-measurements.ts b/src/app-services/collections/instrument-timeseries-measurements.ts
new file mode 100644
index 00000000..d89879a9
--- /dev/null
+++ b/src/app-services/collections/instrument-timeseries-measurements.ts
@@ -0,0 +1,67 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { apiGet } from '../fetch-helpers';
+
+interface InstrumentParams {
+ instrumentId: string,
+ instrumentType: string,
+}
+
+interface BullseyeParams {
+ projectId: string,
+ plotConfigId: string,
+}
+
+interface ContourParams extends BullseyeParams {
+ time: string,
+}
+
+interface TimestampParams extends BullseyeParams {
+ before?: string,
+ after?: string,
+}
+
+export const useGetContourMeasurements = ({ projectId, plotConfigId, time }: ContourParams, opts: ClientQueryOptions) => {
+ const uri = `/projects/${projectId}/plot_configs/contour_plots/${plotConfigId}/measurements?time=${time}`;
+
+ return useQuery({
+ queryKey: [`contourMeasurements`, projectId, plotConfigId, time],
+ queryFn: () => apiGet(uri),
+ ...opts,
+ });
+};
+
+export const useGetBullseyeMeasurements = ({ projectId, plotConfigId, before, after }: TimestampParams, opts: ClientQueryOptions) => {
+ const uri = `/projects/${projectId}/plot_configs/bullseye_plots/${plotConfigId}/measurements?before=${before}&after=${after}`;
+
+ return useQuery({
+ queryKey: [`contourMeasurements`, projectId, plotConfigId, before, after],
+ queryFn: () => apiGet(uri),
+ ...opts,
+ });
+};
+
+export const useGetMeasurementsByInstrumentType = ({ instrumentId, instrumentType }: InstrumentParams, opts: ClientQueryOptions) => {
+ const typeMap = {
+ 'SAA': 'saa',
+ 'IPI': 'ipi',
+ } as Record
;
+
+ const uri = `/instruments/${typeMap[instrumentType]}/${instrumentId}/measurements`;
+
+ return useQuery({
+ queryKey: [`instrumentMeasurementsType`, instrumentId, instrumentType],
+ queryFn: () => apiGet(uri),
+ ...opts,
+ });
+};
+
+export const useGetMeasurementTimestamps = ({ projectId, plotConfigId, before, after }: TimestampParams, opts: ClientQueryOptions) => {
+ const uri = `/projects/${projectId}/plot_configs/contour_plots/${plotConfigId}/times?before=${before}&after=${after}`;
+
+ return useQuery({
+ queryKey: [`measurementTimestamps`, projectId, plotConfigId, before, after],
+ queryFn: () => apiGet(uri),
+ ...opts,
+ });
+};
diff --git a/src/app-services/collections/report-configuration-download.ts b/src/app-services/collections/report-configuration-download.ts
new file mode 100644
index 00000000..4e9f5354
--- /dev/null
+++ b/src/app-services/collections/report-configuration-download.ts
@@ -0,0 +1,57 @@
+import { useMutation, useQuery } from '@tanstack/react-query';
+import { toast } from 'react-toastify';
+
+import { apiGet, apiGetBlob, apiPost } from '../fetch-helpers';
+
+interface QueryParams {
+ projectId: string,
+ reportConfigId: string,
+ jobId: string,
+}
+
+export const downloadFinalReport = ({ projectId, reportConfigId, jobId }: QueryParams) => {
+ const uri = `/projects/${projectId}/report_configs/${reportConfigId}/jobs/${jobId}/downloads`;
+
+ apiGetBlob(uri)
+ .then((file) => {
+ const fileUrl = file ? URL.createObjectURL(file) : '';
+ window.open(fileUrl, '_blank');
+ window.URL.revokeObjectURL(fileUrl);
+ return fileUrl;
+ })
+ .catch(err => console.error(err));
+};
+
+export const useGetReportStatus = ({ projectId, reportConfigId, jobId }: QueryParams, opts: ClientQueryOptions) => {
+ const uri = `/projects/${projectId}/report_configs/${reportConfigId}/jobs/${jobId}`;
+
+ return useQuery({
+ queryKey: [`reportStatus`, reportConfigId, jobId],
+ queryFn: () => apiGet(uri),
+ ...opts,
+ });
+};
+
+export const useInitializeReportDownload = (projectId: string, toastId: string) => {
+ return useMutation({
+ mutationFn: (reportConfigId: string) => {
+ const uri = `/projects/${projectId}/report_configs/${reportConfigId}/jobs`;
+
+ return apiPost(uri);
+ },
+ onError: (err, __, _ctx) => {
+ toast.update(toastId, {
+ render: 'Failed to initialize report job. Please try again later.',
+ type: 'error',
+ isLoading: false,
+ autoClose: 5000,
+ closeOnClick: true,
+ draggable: true,
+ })
+ return { error: 'Failed to Initialize Job', _raw: err };
+ },
+ onSuccess: (body, _, _ctx) => {
+ return body;
+ },
+ });
+};
diff --git a/src/app-services/fetch-helpers.ts b/src/app-services/fetch-helpers.ts
new file mode 100644
index 00000000..a634393c
--- /dev/null
+++ b/src/app-services/fetch-helpers.ts
@@ -0,0 +1,159 @@
+import { getToken } from '../userService';
+
+type ApiSource = 'MIDAS' | 'CWMS';
+
+const API_ROOT = import.meta.env.VITE_API_URL as string;
+const CWMS_ROOT = import.meta.env.VITE_CWMS_API_URL as string;
+
+const roots = {
+ 'MIDAS': API_ROOT,
+ 'CWMS': CWMS_ROOT,
+};
+
+interface OptsType extends RequestInit {
+ isFormData?: boolean;
+}
+
+interface CommonItems {
+ root: string;
+}
+
+export const buildQueryParams = (params: Record) => {
+ const keys = Object.keys(params);
+ const mapped = keys.map(key => {
+ if (!key || params[key] === undefined) return null;
+ return `${key}=${params[key]}`;
+ }).filter(e => e);
+
+ return `?${mapped.join('&')}`;
+};
+
+export const commonFetch = async (root: string, path: string, options: OptsType): Promise => {
+ const res = await fetch(`${root}${path}`, options);
+
+ if (res.status === 204) return {} as JSON;
+ else if (!res.ok) throw new Error(res.statusText);
+
+ return (await res.json()) as JSON;
+};
+
+const getCommonItems = (source: ApiSource = 'MIDAS'): CommonItems => {
+ return ({
+ root: roots[source],
+ });
+};
+
+const defaultHeaders = (source: ApiSource = 'MIDAS') => ({
+ Authorization: source !== 'MIDAS' ? '' : `Bearer ${getToken()}`,
+ ...source === 'CWMS' && {
+ accept: 'application/json;version=2',
+ },
+});
+
+export const apiFetch = (path: string, options: OptsType) => {
+ const { root } = getCommonItems();
+
+ options.headers = { ...defaultHeaders() };
+ return fetch(`${root}${path}`, options);
+};
+
+export const apiGet = (path: string, source?: ApiSource) => {
+ const { root } = getCommonItems(source);
+ const options = { method: 'GET' } as OptsType;
+
+ options.headers = { ...defaultHeaders(source) };
+ return commonFetch(root, path, options);
+};
+
+export const apiGetBlob = async (path: string) => {
+ const { root } = getCommonItems();
+ const options = { method: 'GET' } as OptsType;
+
+ options.headers = { ...defaultHeaders() };
+ const response = await fetch(`${root}${path}`, options);
+
+ return response.blob();
+};
+
+export const apiPut = (path: string, payload: JSON | Record) => {
+ const { root } = getCommonItems();
+ const options = {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ } as OptsType;
+
+ options.headers = {
+ ...options.headers,
+ ...defaultHeaders(),
+ };
+
+ if (payload) {
+ options.body = JSON.stringify(payload);
+ }
+
+ return commonFetch(root, path, options);
+};
+
+/**
+ * Function to send a POST request to the api.
+ * @param {String} path - The URI of the request, will be appended to base path.
+ * @param {JSON | FormData | any[]} payload - The payload of the request. If FormData, request will be sent with Content-Type: multipart/form-data
+ * @param {OptsType} opts - Options provided to the function to alter the request.
+ * @returns
+ */
+export const apiPost = (path: string, payload?: JSON | FormData | any[], opts?: OptsType) => {
+ const { root } = getCommonItems();
+ const { isFormData } = opts || {};
+
+ const options = {
+ method: 'POST',
+ headers: {
+ ...(!isFormData && { 'Content-Type': 'application/json' }),
+ },
+ } as OptsType;
+
+ options.headers = {
+ ...options.headers,
+ ...defaultHeaders(),
+ };
+
+ if (payload) {
+ options.body = (isFormData ? payload : JSON.stringify(payload)) as BodyInit;
+ }
+
+ return commonFetch(root, path, options);
+};
+
+export const apiPatch = (path: string, payload: JSON | FormData | Record) => {
+ const { root } = getCommonItems();
+ const options = {
+ method: 'PATCH',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ } as OptsType;
+
+ options.headers = {
+ ...options.headers,
+ ...defaultHeaders(),
+ };
+
+ if (payload) {
+ options.body = JSON.stringify(payload);
+ }
+
+ return commonFetch(root, path, options);
+};
+
+export const apiDelete = (path: string) => {
+ const { root } = getCommonItems();
+ const options = {
+ method: 'DELETE',
+ } as OptsType;
+
+ options.headers = { ...defaultHeaders() };
+
+ return commonFetch(root, path, options);
+};
diff --git a/src/common/forms/collection-group-form.jsx b/src/common/forms/collection-group-form.jsx
index 7838f24d..bbca1ca9 100644
--- a/src/common/forms/collection-group-form.jsx
+++ b/src/common/forms/collection-group-form.jsx
@@ -63,7 +63,7 @@ export default connect(
saveIsSubmit
customClosingLogic
onCancel={() => doModalClose()}
- onDelete={handleDelete}
+ onDelete={item && item.id ? handleDelete : null}
/>
diff --git a/src/common/forms/instrument-form.jsx b/src/common/forms/instrument-form.jsx
index bcf29c2e..81d93b6f 100644
--- a/src/common/forms/instrument-form.jsx
+++ b/src/common/forms/instrument-form.jsx
@@ -51,6 +51,7 @@ export default connect(
isEdit = true,
}) => {
const [name, setName] = useState(item?.name || '');
+ const [showCwms, setShowCwms] = useState(item?.show_cwms_tab || '');
const [type_id, setTypeId] = useState(item?.type_id || '');
const [station, setStation] = useState(item?.station || '');
const [offset, setOffset] = useState(item?.offset || '');
@@ -125,6 +126,7 @@ export default connect(
type_id: type_id ?? item.type_id,
status_id: status_id ?? item.status_id,
status_time,
+ show_cwms_tab: showCwms,
opts,
station:
station === null || station === ''
@@ -209,6 +211,15 @@ export default connect(
placeholder='Name'
/>
+
+ setShowCwms(e.target.checked)}
+ />
+ Contains CWMS Data
+
Type
setTypeId(val?.id)} domain='instrument_type' />
diff --git a/src/css/index.scss b/src/css/index.scss
index a5c79dcd..5375249c 100644
--- a/src/css/index.scss
+++ b/src/css/index.scss
@@ -54,14 +54,16 @@
.card-header {
background-color: #f7f7f7 !important;
- z-index: 2;
+ display: block !important;
+ width: 100vw;
+ padding: 7px;
}
.page-margin {
- margin-top: 98px;
+ margin-top: 73px;
&.banner {
- margin-top: 130px
+ margin-top: 105px;
}
}
diff --git a/src/customHooks/useWindowDimensions.js b/src/customHooks/useWindowDimensions.js
new file mode 100644
index 00000000..4f9a29b8
--- /dev/null
+++ b/src/customHooks/useWindowDimensions.js
@@ -0,0 +1,24 @@
+import { useState, useEffect } from 'react';
+
+const getWindowDimensions = () => {
+ const { innerWidth: width, innerHeight: height } = window;
+
+ return { width, height };
+}
+
+const useWindowDimensions = () => {
+ const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions());
+
+ useEffect(() => {
+ const handleResize = () => {
+ setWindowDimensions(getWindowDimensions());
+ }
+
+ window.addEventListener('resize', handleResize);
+ return () => window.removeEventListener('resize', handleResize);
+ }, []);
+
+ return windowDimensions;
+};
+
+export default useWindowDimensions;
diff --git a/src/index.jsx b/src/index.jsx
index a5d917a1..f5794d3c 100644
--- a/src/index.jsx
+++ b/src/index.jsx
@@ -2,24 +2,32 @@ import React from 'react';
import { createRoot } from 'react-dom/client';
import { Provider } from 'redux-bundler-react';
import { getNavHelper } from 'internal-nav-helper';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import App from './App';
import cache from './common/helpers/cache';
import getStore from './app-bundles';
+import { initKeycloak } from './userService.ts';
const container = document.getElementById('root');
const root = createRoot(container);
+const queryClient = new QueryClient();
cache.getAll().then((initialData) => {
- const store = getStore(initialData);
+ const renderApp = () => {
+ const store = getStore(initialData);
+ if (import.meta.env.DEV) window.store = store;
- if (import.meta.env.DEV) window.store = store;
+ root.render(
+
+
+
+
+
+ )
+ };
- root.render(
-
-
-
- );
+ initKeycloak(renderApp);
});
diff --git a/src/upload-parsers/timeseries_measurements.js b/src/upload-parsers/timeseries_measurements.js
index 82abcf4c..10893f45 100644
--- a/src/upload-parsers/timeseries_measurements.js
+++ b/src/upload-parsers/timeseries_measurements.js
@@ -8,7 +8,7 @@ const timeseriesMeasurementParser = {
prePostFilter: (data) => (
/** this will work for single timeseries_id, needs to be updated to allow for multiple */
data.reduce((accum, current) => {
- const { timeseries_id, time, value, masked, validated, annotation } = current;
+ const { timeseries_id, time, value, masked, validated, annotation = '' } = current;
return ({
...accum,
@@ -61,7 +61,7 @@ const timeseriesMeasurementParser = {
label: 'Masked',
type: 'boolean',
required: false,
- parse: val => val === 'true' || val === 'T' || val ==='Y',
+ parse: val => val === 'true' || val === 'T' || val === 'Y',
validate: val => !!val,
helpText: 'Boolean value of whether the measurement should be masked (Optional, default to false)',
},
@@ -69,7 +69,7 @@ const timeseriesMeasurementParser = {
label: 'Validated',
type: 'boolean',
required: false,
- parse: val => val === 'true' || val === 'T' || val ==='Y',
+ parse: val => val === 'true' || val === 'T' || val === 'Y',
validate: val => !!val,
helpText: 'Boolean value of whether the measurement is already validated (Optional, default to false)',
},
@@ -78,7 +78,7 @@ const timeseriesMeasurementParser = {
type: 'string',
required: false,
parse: val => val,
- validate: val => !!val,
+ validate: val => !!val || val === '',
helpText: 'String note to be associated with the measurement (Optional, can be empty)',
},
},
diff --git a/src/userService.ts b/src/userService.ts
new file mode 100644
index 00000000..2183e4d9
--- /dev/null
+++ b/src/userService.ts
@@ -0,0 +1,65 @@
+import Keycloak from 'keycloak-js';
+
+const _kc = new Keycloak({
+ url: import.meta.env.VITE_KC_URL,
+ realm: import.meta.env.VITE_KC_REALM,
+ clientId: import.meta.env.VITE_KC_CLIENT_ID,
+});
+
+const basePath = import.meta.env.VITE_URL_BASE_PATH ?? ''
+
+export const initKeycloak = (renderAppCallback = () => {}) => {
+ _kc
+ .init({
+ onLoad: 'check-sso',
+ silentCheckSsoRedirectUri:
+ `${window.location.origin}${basePath}/silent-check-sso.html`,
+ pkceMethod: 'S256',
+ adapter: 'default',
+ })
+ .then(() => {
+ renderAppCallback();
+ })
+ .catch(e => console.error('test error on init: ', e)); // eslint-disable no-console
+};
+
+const loginOptions = import.meta.env.DEV ? undefined : { idpHint: 'login.gov' };
+
+export const doLogin = () => _kc.login(loginOptions);
+export const doLogout = () => _kc.logout({
+ redirectUri: import.meta.env.VITE_BASE_REDIRECT_URI,
+});
+export const getToken = () => _kc.token;
+export const isLoggedIn = () => !!_kc.token;
+export const updateToken = (successCallback: () => {}) =>
+ _kc.updateToken(5).then(successCallback).catch(doLogin);
+export const getUsername = () => _kc.tokenParsed?.preferred_username;
+export const getInitials = () => {
+ const nameParts = (_kc.tokenParsed?.name || _kc.tokenParsed?.preferred_username).split(' ') as string[];
+ const uppers = nameParts.map(name => name?.length ? name[0].toUpperCase() : '').filter(e => e);
+
+ return uppers.join('');
+};
+export const getFullName = () => _kc.tokenParsed?.name;
+export const getFirstName = () => _kc.tokenParsed?.given_name;
+export const getEmail = () => _kc.tokenParsed?.email;
+
+_kc.onTokenExpired = () => {
+ _kc.updateToken(50);
+};
+
+_kc.onAuthError = () => {
+ try {
+ doLogout();
+ } catch (err) {
+ console.error(err);
+ }
+};
+
+_kc.onAuthRefreshError = () => {
+ try {
+ doLogout();
+ } catch (err) {
+ console.error(err);
+ }
+};
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 00000000..73db2df4
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "sourceMap": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "types": ["vite/client"],
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/tsconfig.node.json b/tsconfig.node.json
new file mode 100644
index 00000000..42872c59
--- /dev/null
+++ b/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/tsconfig.vite.json b/tsconfig.vite.json
new file mode 100644
index 00000000..87a55c36
--- /dev/null
+++ b/tsconfig.vite.json
@@ -0,0 +1,7 @@
+{
+ "include": ["vite.config.*"],
+ "compilerOptions": {
+ "composite": true,
+ "types": ["node", "vite/client"],
+ }
+}
\ No newline at end of file