diff --git a/docker-compose.yml b/docker-compose.yml
index b215b137d..cb03956f9 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -16,8 +16,8 @@ services:
- POSTGRES_PASSWORD=pleaseChange # default postgres password that should be changed for security.
volumes:
- ./postgres-data:/var/lib/postgresql/data/pgdata
- #ports:
- # - "5432:5432"
+ # ports:
+ # - "5432:5432"
# Uncomment the above lines to enable access to the PostgreSQL server
# from the host machine.
# Web service runs Node
diff --git a/package-lock.json b/package-lock.json
index f3f60960b..fa2f0736d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -47,6 +47,8 @@
"moment-timezone": "~0.5.34",
"morgan": "~1.9.0",
"multer": "~1.3.1",
+ "ngraph.graph": "~20.0.0",
+ "ngraph.path": "~1.4.0",
"node-polyfill-webpack-plugin": "~1.1.4",
"nodemailer": "~6.6.1",
"package-lock": "~1.0.3",
@@ -57,6 +59,7 @@
"query-string": "~7.0.1",
"rc-slider": "~8.6.6",
"react": "~17.0.2",
+ "react-bootstrap": "~2.2.3",
"react-dom": "~17.0.2",
"react-dropzone": "~12.0.4",
"react-intl": "~5.22.0",
@@ -2527,6 +2530,26 @@
"pick-by-alias": "^1.2.0"
}
},
+ "node_modules/@popperjs/core": {
+ "version": "2.11.5",
+ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.5.tgz",
+ "integrity": "sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/popperjs"
+ }
+ },
+ "node_modules/@react-aria/ssr": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.1.2.tgz",
+ "integrity": "sha512-amXY11ImpokvkTMeKRHjsSsG7v1yzzs6yeqArCyBIk60J3Yhgxwx9Cah+Uu/804ATFwqzN22AXIo7SdtIaMP+g==",
+ "dependencies": {
+ "@babel/runtime": "^7.6.2"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1"
+ }
+ },
"node_modules/@redux-devtools/core": {
"version": "3.11.0",
"resolved": "https://registry.npmjs.org/@redux-devtools/core/-/core-3.11.0.tgz",
@@ -2590,6 +2613,54 @@
"redux": "^3.4.0 || ^4.0.0"
}
},
+ "node_modules/@restart/hooks": {
+ "version": "0.4.7",
+ "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.7.tgz",
+ "integrity": "sha512-ZbjlEHcG+FQtpDPHd7i4FzNNvJf2enAwZfJbpM8CW7BhmOAbsHpZe3tsHwfQUrBuyrxWqPYp2x5UMnilWcY22A==",
+ "dependencies": {
+ "dequal": "^2.0.2"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ }
+ },
+ "node_modules/@restart/ui": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@restart/ui/-/ui-1.2.0.tgz",
+ "integrity": "sha512-oIh2t3tG8drZtZ9SlaV5CY6wGsUViHk8ZajjhcI+74IQHyWy+AnxDv8rJR5wVgsgcgrPBUvGNkC1AEdcGNPaLQ==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.16",
+ "@popperjs/core": "^2.10.1",
+ "@react-aria/ssr": "^3.0.1",
+ "@restart/hooks": "^0.4.0",
+ "@types/warning": "^3.0.0",
+ "dequal": "^2.0.2",
+ "dom-helpers": "^5.2.0",
+ "uncontrollable": "^7.2.1",
+ "warning": "^4.0.3"
+ },
+ "peerDependencies": {
+ "react": ">=16.14.0",
+ "react-dom": ">=16.14.0"
+ }
+ },
+ "node_modules/@restart/ui/node_modules/dom-helpers": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+ "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+ "dependencies": {
+ "@babel/runtime": "^7.8.7",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@restart/ui/node_modules/warning": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
+ "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
+ "dependencies": {
+ "loose-envify": "^1.0.0"
+ }
+ },
"node_modules/@sindresorhus/is": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
@@ -2888,6 +2959,11 @@
"@types/node": "*"
}
},
+ "node_modules/@types/invariant": {
+ "version": "2.2.35",
+ "resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.35.tgz",
+ "integrity": "sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg=="
+ },
"node_modules/@types/json-schema": {
"version": "7.0.9",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",
@@ -3078,6 +3154,11 @@
"@types/node": "*"
}
},
+ "node_modules/@types/warning": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz",
+ "integrity": "sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI="
+ },
"node_modules/@types/ws": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.2.2.tgz",
@@ -5010,9 +5091,9 @@
"integrity": "sha1-ZqDmQBGBbjcZaCj9yMjBRzEshjQ="
},
"node_modules/classnames": {
- "version": "2.2.6",
- "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
- "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q=="
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
+ "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
},
"node_modules/clean-stack": {
"version": "2.2.0",
@@ -6053,6 +6134,14 @@
"node": ">= 0.6"
}
},
+ "node_modules/dequal": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.2.tgz",
+ "integrity": "sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/des.js": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz",
@@ -10430,6 +10519,24 @@
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
"integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw="
},
+ "node_modules/ngraph.events": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ngraph.events/-/ngraph.events-1.2.1.tgz",
+ "integrity": "sha512-D4C+nXH/RFxioGXQdHu8ELDtC6EaCiNsZtih0IvyGN81OZSUby4jXoJ5+RNWasfsd0FnKxxpAROyUMzw64QNsw=="
+ },
+ "node_modules/ngraph.graph": {
+ "version": "20.0.0",
+ "resolved": "https://registry.npmjs.org/ngraph.graph/-/ngraph.graph-20.0.0.tgz",
+ "integrity": "sha512-tJqmik6U5geNDSbmTSwm4R6coTMDbkfFFHD8wdeSJtKU/cxIWFsKtXuwMva/wTk6tQQl1C2//lrzmwfPJXAXHw==",
+ "dependencies": {
+ "ngraph.events": "^1.2.1"
+ }
+ },
+ "node_modules/ngraph.path": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/ngraph.path/-/ngraph.path-1.4.0.tgz",
+ "integrity": "sha512-yJZay4tP0wcjqkkf8zlMQ/T+JOgU+EWfdE4w4TG8OS94B12J/+Z44UOYxVJErE8E6/wFunX1hMZEB1/GHsBYHg=="
+ },
"node_modules/nise": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/nise/-/nise-5.1.0.tgz",
@@ -11567,6 +11674,26 @@
"react-is": "^16.8.1"
}
},
+ "node_modules/prop-types-extra": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz",
+ "integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==",
+ "dependencies": {
+ "react-is": "^16.3.2",
+ "warning": "^4.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=0.14.0"
+ }
+ },
+ "node_modules/prop-types-extra/node_modules/warning": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
+ "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
+ "dependencies": {
+ "loose-envify": "^1.0.0"
+ }
+ },
"node_modules/prop-types/node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -11909,6 +12036,83 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-bootstrap": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.2.3.tgz",
+ "integrity": "sha512-gXsAEBdDUHnOpJ2C+DDQ4mFt7tN6u6qWnTH3tqiE9jUvV6gGY8uHFp0iGBsM+yjrBwmR6bqCBFh8Z82aQj1LSw==",
+ "dependencies": {
+ "@babel/runtime": "^7.17.2",
+ "@restart/hooks": "^0.4.6",
+ "@restart/ui": "^1.2.0",
+ "@types/invariant": "^2.2.35",
+ "@types/prop-types": "^15.7.4",
+ "@types/react": ">=16.14.8",
+ "@types/react-transition-group": "^4.4.4",
+ "@types/warning": "^3.0.0",
+ "classnames": "^2.3.1",
+ "dom-helpers": "^5.2.1",
+ "invariant": "^2.2.4",
+ "prop-types": "^15.8.1",
+ "prop-types-extra": "^1.1.0",
+ "react-transition-group": "^4.4.2",
+ "uncontrollable": "^7.2.1",
+ "warning": "^4.0.3"
+ },
+ "peerDependencies": {
+ "react": ">=16.14.0",
+ "react-dom": ">=16.14.0"
+ }
+ },
+ "node_modules/react-bootstrap/node_modules/dom-helpers": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+ "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+ "dependencies": {
+ "@babel/runtime": "^7.8.7",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/react-bootstrap/node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-bootstrap/node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
+ "node_modules/react-bootstrap/node_modules/react-transition-group": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz",
+ "integrity": "sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==",
+ "dependencies": {
+ "@babel/runtime": "^7.5.5",
+ "dom-helpers": "^5.0.1",
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.6.2"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0",
+ "react-dom": ">=16.6.0"
+ }
+ },
+ "node_modules/react-bootstrap/node_modules/warning": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
+ "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
+ "dependencies": {
+ "loose-envify": "^1.0.0"
+ }
+ },
"node_modules/react-dom": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
@@ -14472,6 +14676,20 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/uncontrollable": {
+ "version": "7.2.1",
+ "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz",
+ "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==",
+ "dependencies": {
+ "@babel/runtime": "^7.6.3",
+ "@types/react": ">=16.9.11",
+ "invariant": "^2.2.4",
+ "react-lifecycles-compat": "^3.0.4"
+ },
+ "peerDependencies": {
+ "react": ">=15.0.0"
+ }
+ },
"node_modules/undefsafe": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
@@ -17776,6 +17994,19 @@
"pick-by-alias": "^1.2.0"
}
},
+ "@popperjs/core": {
+ "version": "2.11.5",
+ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.5.tgz",
+ "integrity": "sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw=="
+ },
+ "@react-aria/ssr": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.1.2.tgz",
+ "integrity": "sha512-amXY11ImpokvkTMeKRHjsSsG7v1yzzs6yeqArCyBIk60J3Yhgxwx9Cah+Uu/804ATFwqzN22AXIo7SdtIaMP+g==",
+ "requires": {
+ "@babel/runtime": "^7.6.2"
+ }
+ },
"@redux-devtools/core": {
"version": "3.11.0",
"resolved": "https://registry.npmjs.org/@redux-devtools/core/-/core-3.11.0.tgz",
@@ -17827,6 +18058,49 @@
"lodash": "^4.17.21"
}
},
+ "@restart/hooks": {
+ "version": "0.4.7",
+ "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.7.tgz",
+ "integrity": "sha512-ZbjlEHcG+FQtpDPHd7i4FzNNvJf2enAwZfJbpM8CW7BhmOAbsHpZe3tsHwfQUrBuyrxWqPYp2x5UMnilWcY22A==",
+ "requires": {
+ "dequal": "^2.0.2"
+ }
+ },
+ "@restart/ui": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@restart/ui/-/ui-1.2.0.tgz",
+ "integrity": "sha512-oIh2t3tG8drZtZ9SlaV5CY6wGsUViHk8ZajjhcI+74IQHyWy+AnxDv8rJR5wVgsgcgrPBUvGNkC1AEdcGNPaLQ==",
+ "requires": {
+ "@babel/runtime": "^7.13.16",
+ "@popperjs/core": "^2.10.1",
+ "@react-aria/ssr": "^3.0.1",
+ "@restart/hooks": "^0.4.0",
+ "@types/warning": "^3.0.0",
+ "dequal": "^2.0.2",
+ "dom-helpers": "^5.2.0",
+ "uncontrollable": "^7.2.1",
+ "warning": "^4.0.3"
+ },
+ "dependencies": {
+ "dom-helpers": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+ "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+ "requires": {
+ "@babel/runtime": "^7.8.7",
+ "csstype": "^3.0.2"
+ }
+ },
+ "warning": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
+ "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
+ "requires": {
+ "loose-envify": "^1.0.0"
+ }
+ }
+ }
+ },
"@sindresorhus/is": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
@@ -18102,6 +18376,11 @@
"@types/node": "*"
}
},
+ "@types/invariant": {
+ "version": "2.2.35",
+ "resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.35.tgz",
+ "integrity": "sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg=="
+ },
"@types/json-schema": {
"version": "7.0.9",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",
@@ -18291,6 +18570,11 @@
"@types/node": "*"
}
},
+ "@types/warning": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz",
+ "integrity": "sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI="
+ },
"@types/ws": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.2.2.tgz",
@@ -19768,9 +20052,9 @@
"integrity": "sha1-ZqDmQBGBbjcZaCj9yMjBRzEshjQ="
},
"classnames": {
- "version": "2.2.6",
- "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
- "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q=="
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
+ "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
},
"clean-stack": {
"version": "2.2.0",
@@ -20646,6 +20930,11 @@
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
},
+ "dequal": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.2.tgz",
+ "integrity": "sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug=="
+ },
"des.js": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz",
@@ -24956,6 +25245,25 @@
}
}
},
+ "prop-types-extra": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz",
+ "integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==",
+ "requires": {
+ "react-is": "^16.3.2",
+ "warning": "^4.0.0"
+ },
+ "dependencies": {
+ "warning": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
+ "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
+ "requires": {
+ "loose-envify": "^1.0.0"
+ }
+ }
+ }
+ },
"protocol-buffers-schema": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
@@ -25248,6 +25556,74 @@
}
}
},
+ "react-bootstrap": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.2.3.tgz",
+ "integrity": "sha512-gXsAEBdDUHnOpJ2C+DDQ4mFt7tN6u6qWnTH3tqiE9jUvV6gGY8uHFp0iGBsM+yjrBwmR6bqCBFh8Z82aQj1LSw==",
+ "requires": {
+ "@babel/runtime": "^7.17.2",
+ "@restart/hooks": "^0.4.6",
+ "@restart/ui": "^1.2.0",
+ "@types/invariant": "^2.2.35",
+ "@types/prop-types": "^15.7.4",
+ "@types/react": ">=16.14.8",
+ "@types/react-transition-group": "^4.4.4",
+ "@types/warning": "^3.0.0",
+ "classnames": "^2.3.1",
+ "dom-helpers": "^5.2.1",
+ "invariant": "^2.2.4",
+ "prop-types": "^15.8.1",
+ "prop-types-extra": "^1.1.0",
+ "react-transition-group": "^4.4.2",
+ "uncontrollable": "^7.2.1",
+ "warning": "^4.0.3"
+ },
+ "dependencies": {
+ "dom-helpers": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+ "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+ "requires": {
+ "@babel/runtime": "^7.8.7",
+ "csstype": "^3.0.2"
+ }
+ },
+ "object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
+ },
+ "prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "requires": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
+ "react-transition-group": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz",
+ "integrity": "sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==",
+ "requires": {
+ "@babel/runtime": "^7.5.5",
+ "dom-helpers": "^5.0.1",
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.6.2"
+ }
+ },
+ "warning": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
+ "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
+ "requires": {
+ "loose-envify": "^1.0.0"
+ }
+ }
+ }
+ },
"react-dom": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
@@ -27279,6 +27655,17 @@
"which-boxed-primitive": "^1.0.2"
}
},
+ "uncontrollable": {
+ "version": "7.2.1",
+ "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz",
+ "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==",
+ "requires": {
+ "@babel/runtime": "^7.6.3",
+ "@types/react": ">=16.9.11",
+ "invariant": "^2.2.4",
+ "react-lifecycles-compat": "^3.0.4"
+ }
+ },
"undefsafe": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
diff --git a/package.json b/package.json
index 7467f73cd..7e4e0d9be 100644
--- a/package.json
+++ b/package.json
@@ -29,6 +29,7 @@
"refreshAllReadingViews": "node -e 'require(\"./src/server/services/refreshAllReadingViews\").refreshAllReadingViews()'",
"refreshDailyReadingViews": "node -e 'require(\"./src/server/services/refreshReadingViews\").refreshReadingViews()'",
"refreshHourlyReadingViews": "node -e 'require(\"./src/server/services/refreshHourlyReadingViews\").refreshHourlyReadingViews()'",
+ "updateCikAndViews": "node -e 'require(\"./src/server/services/graph/redoCik.js\").updateCikAndViews()'",
"obvius:showConfigfiles": "node ./src/server/services/obvius/showConfigfiles.js",
"obvius:purgeConfigfiles": "node ./src/server/services/obvius/purgeConfigfiles.js",
"generateFourDayTestingData": "node -e 'require(\"./src/server/data/automatedTestingData\").generateFourDayTestingData()'",
@@ -40,9 +41,7 @@
"generateSineSquaredTestingData": "node -e 'require(\"./src/server/data/automatedTestingData\").generateSineSquaredTestingData(2.5)'",
"generateCosineSquaredTestingData": "node -e 'require(\"./src/server/data/automatedTestingData\").generateCosineSquaredTestingData(2.5)'",
"generateTestingData": "node -e 'require(\"./src/server/data/automatedTestingData\").generateTestingData()'",
- "testData": "./src/scripts/testData.sh",
- "insertSpecialUnitsAndConversions": "node -e 'require(\"./src/server/data/automatedTestingData.js\").insertSpecialUnitsAndConversions()'",
- "insertSpecialMeters": "node -e 'require(\"./src/server/data/automatedTestingData.js\").insertSpecialMeters()'"
+ "testData": "node -e 'require(\"./src/server/data/automatedTestingData.js\").insertSpecialUnitsConversionsMeters()'"
},
"nodemonConfig": {
"watch": [
@@ -113,6 +112,7 @@
"query-string": "~7.0.1",
"rc-slider": "~8.6.6",
"react": "~17.0.2",
+ "react-bootstrap": "~2.2.3",
"react-dom": "~17.0.2",
"react-dropzone": "~12.0.4",
"react-intl": "~5.22.0",
diff --git a/src/client/app/actions/graph.ts b/src/client/app/actions/graph.ts
index c41b8cd65..bd78fcfb7 100644
--- a/src/client/app/actions/graph.ts
+++ b/src/client/app/actions/graph.ts
@@ -41,6 +41,10 @@ export function updateBarDuration(barDuration: moment.Duration): t.UpdateBarDura
return { type: ActionType.UpdateBarDuration, barDuration };
}
+export function updateLineGraphRate(lineGraphRate: t.LineGraphRate) {
+ return { type: ActionType.UpdateLineGraphRate, lineGraphRate }
+}
+
export function setHotlinked(hotlinked: boolean): t.SetHotlinked {
return { type: ActionType.SetHotlinked, hotlinked };
}
diff --git a/src/client/app/actions/groups.ts b/src/client/app/actions/groups.ts
index 942f165ee..e3b987c99 100644
--- a/src/client/app/actions/groups.ts
+++ b/src/client/app/actions/groups.ts
@@ -24,7 +24,7 @@ function requestGroupChildren(groupID: number): t.RequestGroupChildrenAction {
return { type: ActionType.RequestGroupChildren, groupID };
}
-function receiveGroupChildren(groupID: number, data: {meters: number[], groups: number[], deepMeters: number[]}): t.ReceiveGroupChildrenAction {
+function receiveGroupChildren(groupID: number, data: { meters: number[], groups: number[], deepMeters: number[] }): t.ReceiveGroupChildrenAction {
return { type: ActionType.ReceiveGroupChildren, groupID, data };
}
@@ -321,7 +321,8 @@ export function submitGroupInEditingIfNeeded() {
gps: rawGroup.gps,
displayable: rawGroup.displayable,
note: rawGroup.note,
- area: rawGroup.area
+ area: rawGroup.area,
+ defaultGraphicUnit: rawGroup.defaultGraphicUnit
};
if (creatingNewGroup(getState())) {
return dispatch(submitNewGroup(group));
diff --git a/src/client/app/actions/meters.ts b/src/client/app/actions/meters.ts
index 9dc6dfd5c..53b12a7dd 100644
--- a/src/client/app/actions/meters.ts
+++ b/src/client/app/actions/meters.ts
@@ -1,97 +1,117 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-import * as _ from 'lodash';
-import { ActionType, Dispatch, GetState, Thunk } from '../types/redux/actions';
-import { MeterMetadata } from '../types/redux/meters';
-import { State } from '../types/redux/state';
+import { ActionType, Thunk, Dispatch, GetState } from '../types/redux/actions';
+import { showSuccessNotification, showErrorNotification } from '../utils/notifications';
+import translate from '../utils/translate';
import * as t from '../types/redux/meters';
-import { NamedIDItem } from '../types/items';
import { metersApi } from '../utils/api';
-import { showErrorNotification } from '../utils/notifications';
-import translate from '../utils/translate';
export function requestMetersDetails(): t.RequestMetersDetailsAction {
return { type: ActionType.RequestMetersDetails };
}
-export function receiveMetersDetails(data: NamedIDItem[]): t.ReceiveMetersDetailsAction {
+export function receiveMetersDetails(data: t.MeterData[]): t.ReceiveMetersDetailsAction {
return { type: ActionType.ReceiveMetersDetails, data };
}
-export function changeDisplayedMeters(meters: number[]): t.ChangeDisplayedMetersAction {
- return { type: ActionType.ChangeDisplayedMeters, selectedMeters: meters};
+export function fetchMetersDetails(): Thunk {
+ return async (dispatch: Dispatch, getState: GetState) => {
+ // ensure a fetch is not currently happening
+ if (!getState().meters.isFetching)
+ {
+ // set isFetching to true
+ dispatch(requestMetersDetails());
+ // attempt to retrieve meters details from database
+ const meters = await metersApi.getMetersDetails();
+ // update the state with the meters details and set isFetching to false
+ dispatch(receiveMetersDetails(meters));
+ // If this is the first fetch, inform the store that the first fetch has been made
+ if (!getState().meters.hasBeenFetchedOnce)
+ {
+ dispatch(confirmMetersFetchedOnce());
+ }
+ }
+ }
}
-export function editMeterDetails(meter: MeterMetadata): t.EditMeterDetailsAction {
- return { type: ActionType.EditMeterDetails, meter };
+export function changeDisplayedMeters(meters: number[]): t.ChangeDisplayedMetersAction {
+ return { type: ActionType.ChangeDisplayedMeters, selectedMeters: meters };
}
-export function submitMeterEdits(meter: number): t.SubmitEditedMeterAction {
- return { type: ActionType.SubmitEditedMeter, meter };
+// Pushes meterId onto submitting meters state array
+export function submitMeterEdits(meterId: number): t.SubmitEditedMeterAction {
+ return { type: ActionType.SubmitEditedMeter, meterId };
}
-export function confirmMeterEdits(meter: number): t.ConfirmEditedMeterAction {
- return { type: ActionType.ConfirmEditedMeter, meter};
+export function confirmMeterEdits(editedMeter: t.MeterData): t.ConfirmEditedMeterAction {
+ return { type: ActionType.ConfirmEditedMeter, editedMeter };
}
-export function fetchMetersDetails(): Thunk {
- return async (dispatch: Dispatch) => {
- dispatch(requestMetersDetails());
- const metersDetails = await metersApi.details();
- dispatch(receiveMetersDetails(metersDetails));
- };
+export function deleteSubmittedMeter(meterId: number): t.DeleteSubmittedMeterAction {
+ return {type: ActionType.DeleteSubmittedMeter, meterId}
}
-export function submitEditedMeters(): Thunk {
- return async (dispatch: Dispatch, getState: GetState) => {
- Object.keys(getState().meters.editedMeters).forEach(meterIdS => {
- const meterId = parseInt(meterIdS);
- if (getState().meters.submitting.indexOf(meterId) === -1) {
- dispatch(submitEditedMeter(meterId));
- }
- });
- };
+export function confirmMetersFetchedOnce(): t.ConfirmMetersFetchedOnceAction {
+ return { type: ActionType.ConfirmMetersFetchedOnce };
}
-export function submitEditedMeter(meterId: number): Thunk {
- return async (dispatch: Dispatch, getState: GetState) => {
- const submittingMeter = getState().meters.editedMeters[meterId];
- dispatch(submitMeterEdits(meterId));
- try {
- await metersApi.edit(submittingMeter);
- dispatch(confirmMeterEdits(meterId));
- } catch (err) {
- showErrorNotification(translate('failed.to.edit.meter'));
+// Fetch the meters details from the database if they have not already been fetched once
+export function fetchMetersDetailsIfNeeded(): Thunk {
+ return (dispatch: Dispatch, getState: GetState) => {
+ // If meters have not been fetched once, return the fetchMeterDetails function
+ if (!getState().meters.hasBeenFetchedOnce)
+ {
+ return dispatch(fetchMetersDetails());
}
+ // If meters have already been fetched, return a resolved promise
+ return Promise.resolve();
};
}
-/**
- * Remove all the meters in editing without submitting them
- */
-export function confirmEditedMeters(): Thunk {
+export function submitEditedMeter(editedMeter: t.MeterData): Thunk {
return async (dispatch: Dispatch, getState: GetState) => {
- Object.keys(getState().meters.editedMeters).forEach(meterIdS => {
- const meterId = parseInt(meterIdS);
- dispatch(confirmMeterEdits(meterId));
- });
- }
-}
+ // check if meterData is already submitting (indexOf returns -1 if item does not exist in array)
+ if (getState().meters.submitting.indexOf(editedMeter.id) === -1) {
+ // Inform the store we are about to edit the passed in meter
+ // Pushes meterId of the meterData to submit onto the submitting state array
+ dispatch(submitMeterEdits(editedMeter.id));
-/**
- * @param {State} state
- */
-function shouldFetchMetersDetails(state: State): boolean {
- return !state.meters.isFetching && _.size(state.meters.byMeterID) === 0;
-}
-
-export function fetchMetersDetailsIfNeeded(alwaysFetch?: boolean): Thunk {
- return (dispatch: Dispatch, getState: GetState) => {
- if (alwaysFetch || shouldFetchMetersDetails(getState())) {
- return dispatch(fetchMetersDetails());
+ // Attempt to edit the meter in the database
+ try {
+ // posts the edited meterData to the meters API
+ await metersApi.edit(editedMeter);
+ // Clear meter Id from submitting state array
+ dispatch(deleteSubmittedMeter(editedMeter.id));
+ // Update the store with our new edits
+ dispatch(confirmMeterEdits(editedMeter));
+ // Success!
+ showSuccessNotification(translate('meter.successfully.edited.meter'));
+ } catch (err) {
+ // Failure! ):
+ showErrorNotification(translate('meter.failed.to.edit.meter'));
+ // Clear our changes from to the submitting meters state
+ // We must do this in case fetch failed to keep the store in sync with the database
+ dispatch(deleteSubmittedMeter(editedMeter.id));
+ }
}
- return Promise.resolve();
};
}
+
+// Add meter to database
+export function addMeter(meter: t.MeterData): Thunk {
+ return async (dispatch: Dispatch) => {
+ try {
+ // Attempt to add meter to database
+ await metersApi.addMeter(meter);
+ // Update the meters state from the database on a successful call
+ // In the future, getting rid of this database fetch and updating the store on a successful API call would make the page faster
+ // However, since the database currently assigns the id to the MeterData
+ dispatch(fetchMetersDetails());
+ showSuccessNotification(translate('meter.successfully.create.meter'));
+ } catch (err) {
+ showErrorNotification(translate('meter.failed.to.create.meter'));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/client/app/actions/units.ts b/src/client/app/actions/units.ts
index 32230c60f..3b715da63 100644
--- a/src/client/app/actions/units.ts
+++ b/src/client/app/actions/units.ts
@@ -3,7 +3,8 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { ActionType, Thunk, Dispatch, GetState } from '../types/redux/actions';
-import { State } from '../types/redux/state';
+import { showSuccessNotification, showErrorNotification } from '../utils/notifications';
+import translate from '../utils/translate';
import * as t from '../types/redux/units';
import { unitsApi } from '../utils/api';
@@ -16,22 +17,95 @@ export function receiveUnitsDetails(data: t.UnitData[]): t.ReceiveUnitsDetailsAc
}
export function fetchUnitsDetails(): Thunk {
- return async (dispatch: Dispatch) => {
- dispatch(requestUnitsDetails());
- const units = await unitsApi.getUnitsDetails();
- dispatch(receiveUnitsDetails(units));
+ return async (dispatch: Dispatch, getState: GetState) => {
+ // ensure a fetch is not currently happening
+ if (!getState().units.isFetching) {
+ // set isFetching to true
+ dispatch(requestUnitsDetails());
+ // attempt to retrieve units details from database
+ const units = await unitsApi.getUnitsDetails();
+ // update the state with the units details and set isFetching to false
+ dispatch(receiveUnitsDetails(units));
+ // If this is the first fetch, inform the store that the first fetch has been made
+ if (!getState().units.hasBeenFetchedOnce) {
+ dispatch(confirmUnitsFetchedOnce());
+ }
+ }
}
}
-function shouldFetchUnitsDetails(state: State): boolean {
- return !state.units.isFetching;
+export function changeDisplayedUnits(units: number[]): t.ChangeDisplayedUnitsAction {
+ return { type: ActionType.ChangeDisplayedUnits, selectedUnits: units };
+}
+
+// Pushes unitId onto submitting units state array
+export function submitUnitEdits(unitId: number): t.SubmitEditedUnitAction {
+ return { type: ActionType.SubmitEditedUnit, unitId };
+}
+
+export function confirmUnitEdits(editedUnit: t.UnitData): t.ConfirmEditedUnitAction {
+ return { type: ActionType.ConfirmEditedUnit, editedUnit };
+}
+
+export function deleteSubmittedUnit(unitId: number): t.DeleteSubmittedUnitAction {
+ return { type: ActionType.DeleteSubmittedUnit, unitId }
+}
+
+export function confirmUnitsFetchedOnce(): t.ConfirmUnitsFetchedOnceAction {
+ return { type: ActionType.ConfirmUnitsFetchedOnce };
}
+// Fetch the units details from the database if they have not already been fetched once
export function fetchUnitsDetailsIfNeeded(): Thunk {
return (dispatch: Dispatch, getState: GetState) => {
- if (shouldFetchUnitsDetails(getState())) {
+ // If units have not been fetched once, return the fetchUnitDetails function
+ if (!getState().units.hasBeenFetchedOnce) {
return dispatch(fetchUnitsDetails());
}
+ // If units have already been fetched, return a resolved promise
return Promise.resolve();
};
}
+
+export function submitEditedUnit(editedUnit: t.UnitData): Thunk {
+ return async (dispatch: Dispatch, getState: GetState) => {
+ // check if unitData is already submitting (indexOf returns -1 if item does not exist in array)
+ if (getState().units.submitting.indexOf(editedUnit.id) === -1) {
+ // Inform the store we are about to edit the passed in unit
+ // Pushes unitId of the unitData to submit onto the submitting state array
+ dispatch(submitUnitEdits(editedUnit.id));
+
+ // Attempt to edit the unit in the database
+ try {
+ // posts the edited unitData to the units API
+ await unitsApi.edit(editedUnit);
+ // Update the store with our new edits
+ dispatch(confirmUnitEdits(editedUnit));
+ // Success!
+ showSuccessNotification(translate('unit.successfully.edited.unit'));
+ } catch (err) {
+ // Failure! ):
+ showErrorNotification(translate('unit.failed.to.edit.unit'));
+ }
+ // Clear unit Id from submitting state array
+ dispatch(deleteSubmittedUnit(editedUnit.id));
+ }
+ };
+}
+
+// Add unit to database
+export function addUnit(unit: t.UnitData): Thunk {
+ return async (dispatch: Dispatch) => {
+ try {
+ // Attempt to add unit to database
+ await unitsApi.addUnit(unit);
+ // Update the units state from the database on a successful call
+ // In the future, getting rid of this database fetch and updating the store on a successful API call would make the page faster
+ // However, since the database currently assigns the id to the UnitData we need to fetch
+ dispatch(fetchUnitsDetails());
+ showSuccessNotification(translate('unit.successfully.create.unit'));
+ } catch (err) {
+ showErrorNotification(translate('unit.failed.to.create.unit'));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/client/app/components/ChartDataSelectComponent.tsx b/src/client/app/components/ChartDataSelectComponent.tsx
index a6affd6e4..e505527ab 100644
--- a/src/client/app/components/ChartDataSelectComponent.tsx
+++ b/src/client/app/components/ChartDataSelectComponent.tsx
@@ -16,10 +16,16 @@ import {
CartesianPoint, Dimensions, normalizeImageDimensions, calculateScaleFromEndpoints,
itemDisplayableOnMap, itemMapInfoOk, gpsToUserGrid
} from '../utils/calibration';
-import { changeSelectedGroups, changeSelectedMeters, changeSelectedUnit } from '../actions/graph';
+import {
+ changeSelectedGroups, changeSelectedMeters, changeSelectedUnit, updateSelectedMeters,
+ updateSelectedGroups, updateSelectedUnit
+} from '../actions/graph';
import { DisplayableType, UnitData, UnitType } from '../types/redux/units'
-import { metersInGroup, setIntersect, unitsCompatibleWithMeters } from '../utils/determineCompatibleUnits';
+import { metersInGroup, unitsCompatibleWithMeters } from '../utils/determineCompatibleUnits';
import { Dispatch } from '../types/redux/actions';
+import { UnitsState } from '../types/redux/units';
+import { MetersState } from 'types/redux/meters';
+import { GroupsState } from 'types/redux/groups';
/**
* A component which allows the user to select which data should be displayed on the chart.
@@ -47,10 +53,8 @@ export default function ChartDataSelectComponent() {
const allGroups = state.groups.byGroupID;
// Map information about meters, groups and units into a format the component can display.
- const sortedMeters = _.sortBy(_.values(allMeters).map(meter =>
- ({ value: meter.id, label: meter.name.trim(), isDisabled: false } as SelectOption)), 'label');
- const sortedGroups = _.sortBy(_.values(allGroups).map(group =>
- ({ value: group.id, label: group.name.trim(), isDisabled: false } as SelectOption)), 'label');
+ const sortedMeters = getMeterCompatibilityForDropdown(state);
+ const sortedGroups = getGroupCompatibilityForDropdown(state);
const sortedUnits = getUnitCompatibilityForDropdown(state);
//Map information about the currently selected meters into a format the component can display.
@@ -137,13 +141,7 @@ export default function ChartDataSelectComponent() {
// The selectedUnit becomes the unit of the meter selected. Note is should always be set (not -99) since
// those meters should not have been visible. The only exception is if there are no selected meters but
// then this loop does not run. The loop is assumed to only run once in this case.
- // TODO this really should only be for debugging??
- if (state.graph.selectedMeters.length != 1) {
- console.log('9000 state.graph.selectedMeters length is not one but ' + state.graph.selectedMeters.length);
- }
-
- // TODO is it possible the unit of the meter is not set in state? Seems not if can select??
- state.graph.selectedUnit = state.meters.byMeterID[meterID].defaultGraphicUnit;
+ dispatch(changeSelectedUnit(state.meters.byMeterID[meterID].defaultGraphicUnit));
}
selectedMeters.push({
// For meters we display the identifier.
@@ -152,33 +150,22 @@ export default function ChartDataSelectComponent() {
isDisabled: false
} as SelectOption)
}
- // }
- // }
});
const selectedGroups: SelectOption[] = [];
state.graph.selectedGroups.forEach(groupID => {
- // TODO For now you cannot graph a group unit you have a graphing unit
- if (!(disableGroups.includes(groupID)) && state.graph.selectedUnit != -99) {
- // TODO use this code once group has default graphic unit
- // if (!(disableGroups.includes(groupID))) {
- // // If the selected unit is -99 then there is not graphic unit yet. In this case you can only select a
- // // group that has a default graphic unit because that will become the selected unit. This should only
- // // happen if no meter or group is yet selected.
- // if (state.graph.selectedUnit == -99) {
- // // If no unit is set then this should always be the first group (or meter) selected.
- // // The selectedUnit becomes the unit of the group selected. Note is should always be set (not -99) since
- // // those groups should not have been visible. The only exception is if there are no selected groups but
- // // then this loop does not run. The loop is assumed to only run once in this case.
- // // TODO this really should only be for debugging??
- // if (state.graph.selectedGroups.length != 1) {
- // console.log('9100 state.graph.selectedGroups length is not one but ' + state.graph.selectedGroups.length);
- // }
-
- // // TODO is it possible the unit of the meter is not set in state? Seems not if can select??
- // // TODO group state does not yet have default graphic unit so must wait to do this.
- // state.graph.selectedUnit = state.groups.byGroupID[groupID].defaultGraphicUnit;
- // }
+ // Don't include disabled groups.
+ if (!(disableGroups.includes(groupID))) {
+ // If the selected unit is -99 then there is no graphic unit yet. In this case you can only select a
+ // group that has a default graphic unit because that will become the selected unit. This should only
+ // happen if no meter or group is yet selected.
+ if (state.graph.selectedUnit == -99) {
+ // If no unit is set then this should always be the first group (or meter) selected.
+ // The selectedUnit becomes the unit of the group selected. Note is should always be set (not -99) since
+ // those groups should not have been visible. The only exception is if there are no selected groups but
+ // then this loop does not run. The loop is assumed to only run once in this case.
+ state.graph.selectedUnit = state.groups.byGroupID[groupID].defaultGraphicUnit;
+ }
selectedGroups.push({
// For groups we display the name since no identifier.
label: state.groups.byGroupID[groupID] ? state.groups.byGroupID[groupID].name : '',
@@ -254,17 +241,20 @@ export default function ChartDataSelectComponent() {
selectedOptions={dataProps.selectedUnit}
placeholder={intl.formatMessage(messages.selectUnit)}
onValuesChange={(newSelectedUnitOptions: SelectOption[]) => {
- let newUnit;
+ // TODO I don't quite understand why the component results in an array of size 2 when updating state
+ // For now I have hardcoded a fix that allows units to be selected over other units without clicking the x button
if (newSelectedUnitOptions.length === 0) {
- // The unit was unselected so no new one. Thus, set to -99 since no unit.
- // TODO This causes an error by trying to get data before the value is changed again.??
- // TODO need to clear all meters/groups in this case.
- // TODO nice if warned user about to clear all meters/groups.
- newUnit = -99;
- } else {
- newUnit = newSelectedUnitOptions[0].value;
+ // Update the selected meters and groups to empty to avoid graphing errors
+ // The update selected meters/groups functions are essentially the same as the change functions
+ // However, they do not attempt to graph.
+ dispatch(updateSelectedGroups([]));
+ dispatch(updateSelectedMeters([]));
+ dispatch(updateSelectedUnit(-99));
}
- dispatch(changeSelectedUnit(newUnit));
+ else if (newSelectedUnitOptions.length === 1) { dispatch(changeSelectedUnit(newSelectedUnitOptions[0].value)); }
+ else if (newSelectedUnitOptions.length > 1) { dispatch(changeSelectedUnit(newSelectedUnitOptions[1].value)); }
+ // This should not happen
+ else { dispatch(changeSelectedUnit(-99)); }
}}
/>
@@ -278,50 +268,40 @@ export default function ChartDataSelectComponent() {
* @param {State} state - current redux state
* @return {SelectOption[]} an array of SelectOption
*/
-export function getUnitCompatibilityForDropdown(state: State) {
+function getUnitCompatibilityForDropdown(state: State) {
+
// Holds all units that are compatible with selected meters/groups
const compatibleUnits = new Set();
// Holds all units that are not compatible with selected meters/groups
const incompatibleUnits = new Set();
- if (state.graph.selectedUnit === -99) {
+
+ // Holds all selected meters, including those retrieved from groups
+ const allSelectedMeters = new Set();
+
+ // Get for all meters
+ state.graph.selectedMeters.forEach(meter => {
+ allSelectedMeters.add(meter);
+ });
+ // Get for all groups
+ state.graph.selectedGroups.forEach(group => {
+ // Get for all deep meters in group
+ metersInGroup(group).forEach(meter => {
+ allSelectedMeters.add(meter);
+ });
+ });
+
+ if (allSelectedMeters.size == 0) {
+ // No meters/groups are selected. This includes the case where the selectedUnit is -99.
// Every unit is okay/compatible in this case so skip the work needed below.
- // Can only show unit types (not meters) and only displayable ones.
- // is either all (not logged in as admin) or admin
+ // Filter the units to be displayed by user status and displayable type
getVisibleUnitOrSuffixState(state).forEach(unit => {
compatibleUnits.add(unit.id);
- })
+ });
} else {
// Some meter or group is selected
- // Holds the units compatible with the meters/groups selected.
- // The first meter or group processed is different since intersection with empty set is empty.
- let first = true;
- let units = new Set();
- const M = new Set();
- // Get for all meters
- state.graph.selectedMeters.forEach(meter => {
- M.add(meter);
- const newUnits = unitsCompatibleWithMeters(M)
- if (first) {
- // First meter/group so all its units are acceptable at this point
- units = newUnits;
- first = false;
- } else {
- // Do intersection of compatible units so far with ones for this meters
- units = setIntersect(units, newUnits);
- }
- })
- // Get for all groups
- state.graph.selectedGroups.forEach(async group => {
- const newUnits = unitsCompatibleWithMeters(await metersInGroup(group));
- if (first) {
- // First meter/group so all its units are acceptable at this point
- units = newUnits;
- first = false;
- } else {
- // Do intersection of compatible units so far with ones for this meters
- units = setIntersect(newUnits, units);
- }
- })
+ // Retrieve set of units compatible with list of selected meters and/or groups
+ const units = unitsCompatibleWithMeters(allSelectedMeters);
+
// Loop over all units (they must be of type unit or suffix - case 1)
getVisibleUnitOrSuffixState(state).forEach(o => {
// Control displayable ones (case 2)
@@ -332,50 +312,237 @@ export function getUnitCompatibilityForDropdown(state: State) {
// Should show as incompatible (case 4)
incompatibleUnits.add(o.id);
}
- })
+ });
}
// Ready to display unit. Put selectable ones before unselectable ones.
- const finalUnits = getUnitCompatibility(compatibleUnits, incompatibleUnits, state);
+ const finalUnits = getSelectOptionsByItem(compatibleUnits, incompatibleUnits, state.units);
return finalUnits;
}
+// NOTE: getMeterCompatibilityForDropdown and getGroupCompatibilityForDropdown are essentially the same function.
+// Keeping them separate for now for readability, perhaps they can be consolidated in the future
+
+/**
+ * Determines the compatibility of meters in the redux state for display in dropdown
+ * @param {State} state - current redux state
+ * @return {SelectOption[]} an array of SelectOption
+ */
+export function getMeterCompatibilityForDropdown(state: State) {
+ // Holds all meters visible to the user
+ const visibleMeters = new Set();
+
+ // Get all the meters that this user can see.
+ if (state.currentUser.profile?.role === 'admin') {
+ // Can see all meters
+ Object.values(state.meters.byMeterID).forEach(meter => {
+ visibleMeters.add(meter.id);
+ });
+ }
+ else {
+ // Regular user or not logged in so only add displayable meters
+ Object.values(state.meters.byMeterID).forEach(meter => {
+ if (meter.displayable) {
+ visibleMeters.add(meter.id);
+ }
+ });
+ }
+
+ // meters that can graph
+ const compatibleMeters = new Set();
+ // meters that cannot graph.
+ const incompatibleMeters = new Set();
+
+ if (state.graph.selectedUnit === -99) {
+ // No unit is selected then no meter/group should be selected.
+ // In this case, every meter is valid (provided it has a default graphic unit)
+ // If the meter has a default graphic unit set then it can graph, otherwise it cannot.
+ visibleMeters.forEach(meterId => {
+ if (state.meters.byMeterID[meterId].defaultGraphicUnit === -99) {
+ //Default graphic unit is not set
+ incompatibleMeters.add(meterId);
+ }
+ else {
+ //Default graphic unit is set
+ compatibleMeters.add(meterId);
+ }
+ });
+ }
+ else {
+ // A unit is selected
+ // For each meter get all of its compatible units
+ // Then, check if the selected unit exists in that set of compatible units
+ visibleMeters.forEach(meterId => {
+ // Get the set of units compatible with the current meter
+ const compatibleUnits = unitsCompatibleWithMeters(new Set([meterId]));
+ if (compatibleUnits.has(state.graph.selectedUnit)) {
+ // The selected unit is part of the set of compatible units with this meter
+ compatibleMeters.add(meterId);
+ }
+ else {
+ // The selected unit is not part of the compatible units set for this meter
+ incompatibleMeters.add(meterId);
+ }
+ });
+ }
+
+ // Retrieve select options from meter sets
+ const finalMeters = getSelectOptionsByItem(compatibleMeters, incompatibleMeters, state.meters);
+ return finalMeters;
+}
+
+/**
+ * Determines the compatibility of group in the redux state for display in dropdown
+ * @param {State} state - current redux state
+ * @return {SelectOption[]} an array of SelectOption
+ */
+export function getGroupCompatibilityForDropdown(state: State) {
+ // Holds all groups visible to the user
+ const visibleGroup = new Set();
+
+ // Get all the groups that this user can see.
+ if (state.currentUser.profile?.role === 'admin') {
+ // Can see all groups
+ Object.values(state.groups.byGroupID).forEach(group => {
+ visibleGroup.add(group.id);
+ });
+ }
+ else {
+ // Regular user or not logged in so only add displayable groups
+ Object.values(state.groups.byGroupID).forEach(group => {
+ if (group.displayable) {
+ visibleGroup.add(group.id);
+ }
+ });
+ }
+
+ // groups that can graph
+ const compatibleGroups = new Set();
+ // groups that cannot graph.
+ const incompatibleGroups = new Set();
+
+ if (state.graph.selectedUnit === -99) {
+ // If no unit is selected then no meter/group should be selected.
+ // In this case, every group is valid (provided it has a default graphic unit)
+ // If the group has a default graphic unit set then it can graph, otherwise it cannot.
+ visibleGroup.forEach(groupId => {
+ if (state.groups.byGroupID[groupId].defaultGraphicUnit === -99) {
+ //Default graphic unit is not set
+ incompatibleGroups.add(groupId);
+ }
+ else {
+ //Default graphic unit is set
+ compatibleGroups.add(groupId);
+ }
+ });
+ }
+ else {
+ // A unit is selected
+ // For each group get all of its compatible units
+ // Then, check if the selected unit exists in that set of compatible units
+ visibleGroup.forEach(groupId => {
+ // Get the set of units compatible with the current group (through its deepMeters attribute)
+ const compatibleUnits = unitsCompatibleWithMeters(metersInGroup(groupId));
+ if (compatibleUnits.has(state.graph.selectedUnit)) {
+ // The selected unit is part of the set of compatible units with this group
+ compatibleGroups.add(groupId);
+ }
+ else {
+ // The selected unit is not part of the compatible units set for this group
+ incompatibleGroups.add(groupId);
+ }
+ });
+ }
+
+ // Retrieve select options from group sets
+ const finalGroups = getSelectOptionsByItem(compatibleGroups, incompatibleGroups, state.groups);
+ return finalGroups;
+}
+
/**
- * Filters all units that are of type meter or displayable type none from the redux state.
+ * Filters all units that are of type meter or displayable type none from the redux state, as well as admin only units if the user is not an admin.
* @param {State} state - current redux state
* @return {UnitData[]} an array of UnitData
*/
export function getVisibleUnitOrSuffixState(state: State) {
- const visibleUnitsOrSuffixes = _.filter(state.units.units, function (o: UnitData) {
- return o.typeOfUnit != UnitType.meter && o.displayable != DisplayableType.none;
- })
+ let visibleUnitsOrSuffixes;
+ if (state.currentUser.profile?.role === 'admin') {
+ // User is an admin, allow all units to be seen
+ visibleUnitsOrSuffixes = _.filter(state.units.units, (o: UnitData) => {
+ return o.typeOfUnit != UnitType.meter && o.displayable != DisplayableType.none;
+ });
+ }
+ else {
+ // User is not an admin, do not allow for admin units to be seen
+ visibleUnitsOrSuffixes = _.filter(state.units.units, (o: UnitData) => {
+ return o.typeOfUnit != UnitType.meter && o.displayable == DisplayableType.all;
+ });
+ }
return visibleUnitsOrSuffixes;
}
/**
- * Sets visibility of SelectOptions for dropdown. Determined by which set they are contained in
- * @param {State} state - current redux state
- * @param {Set} compatibleUnits - units that are compatible with current selected unit
- * @param {Set} incompatibleUnits - units that are not compatible with current selected unit
+ * Returns a set of SelectOptions based on the type of state passed in and sets the visibility.
+ * Visibility is determined by which set the items are contained in.
+ * @param {State} state - current redux state, must be one of UnitsState, MetersState, or GroupsState
+ * @param {Set} compatibleItems - items that are compatible with current selected options
+ * @param {Set} incompatibleItems - units that are not compatible with current selected options
* @return {SelectOption[]} an array of SelectOption
*/
-function getUnitCompatibility(compatibleUnits: Set, incompatibleUnits: Set, state: State) {
- const finalUnits: SelectOption[] = [];
- compatibleUnits.forEach(unit => {
- finalUnits.push({
- value: unit,
- label: state.units.units[unit].identifier,
+function getSelectOptionsByItem(compatibleItems: Set, incompatibleItems: Set, state: UnitsState | MetersState | GroupsState) {
+ // Holds the label of the select item, set dynamically according to the type of item passed in
+ let label = '';
+
+ //The final list of select options to be displayed
+ const finalItems: SelectOption[] = [];
+
+ //Loop over each itemId and create an activated select option
+ compatibleItems.forEach(itemId => {
+ // Perhaps in the future this can be done differently
+ // Loop over the state type to see what state was passed in (units, meter, group, etc)
+ // Set the label correctly based on the type of state
+ // If this is converted to a switch statement the instanceOf function needs to be called twice
+ // Once for the initial state type check, again because the interpreter (for some reason) needs to be sure that the property exists in the object
+ // If else statements do not suffer from this
+ if (instanceOfUnitsState(state)) {
+ label = state.units[itemId].identifier;
+ }
+ else if (instanceOfMetersState(state)) {
+ label = state.byMeterID[itemId].name;
+ }
+ else if (instanceOfGroupsState(state)) {
+ label = state.byGroupID[itemId].name;
+ }
+ else { label = ''; }
+ finalItems.push({
+ value: itemId,
+ label: label,
isDisabled: false
} as SelectOption
- )
- })
- incompatibleUnits.forEach(unit => {
- finalUnits.push({
- value: unit,
- label: state.units.units[unit].identifier,
+ );
+ });
+ //Loop over each itemId and create a disabled select option
+ incompatibleItems.forEach(itemId => {
+ if (instanceOfUnitsState(state)) {
+ label = state.units[itemId].identifier;
+ }
+ else if (instanceOfMetersState(state)) {
+ label = state.byMeterID[itemId].name;
+ }
+ else if (instanceOfGroupsState(state)) {
+ label = state.byGroupID[itemId].name;
+ }
+ else { label = ''; }
+ finalItems.push({
+ value: itemId,
+ label: label,
isDisabled: true
} as SelectOption
- )
+ );
})
- return _.sortBy(_.sortBy(finalUnits, unit => unit.label.toLowerCase(), 'asc'), unit => unit.isDisabled, 'asc');
+ return _.sortBy(_.sortBy(finalItems, item => item.label.toLowerCase(), 'asc'), item => item.isDisabled, 'asc');
}
-// }
\ No newline at end of file
+
+// Helper functions to determine what type of state was passed in
+function instanceOfUnitsState(state: any): state is UnitsState { return 'units' in state; }
+function instanceOfMetersState(state: any): state is MetersState { return 'byMeterID' in state; }
+function instanceOfGroupsState(state: any): state is GroupsState { return 'byGroupID' in state; }
diff --git a/src/client/app/components/GraphicRateMenuComponent.tsx b/src/client/app/components/GraphicRateMenuComponent.tsx
new file mode 100644
index 000000000..81f37631a
--- /dev/null
+++ b/src/client/app/components/GraphicRateMenuComponent.tsx
@@ -0,0 +1,60 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+* License, v. 2.0. If a copy of the MPL was not distributed with this
+* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { State } from '../types/redux/state';
+import { useDispatch, useSelector } from 'react-redux';
+import { SelectOption } from '../types/items';
+import Select from 'react-select';
+import translate from '../utils/translate';
+import { updateLineGraphRate } from '../actions/graph'
+import { LineGraphRate, LineGraphRates } from '../types/redux/graph';
+
+export default function GraphicRateMenuComponent() {
+ const dispatch = useDispatch();
+
+ // Graph state
+ const graphState = useSelector((state: State) => state.graph);
+
+ // Array of select options created from the rates
+ const rateOptions: SelectOption[] = [];
+
+ //Loop over our rates object to create the selects for the dropdown
+ Object.entries(LineGraphRates).forEach(([rateKey, rateValue]) => {
+ rateOptions.push({
+ label: translate(rateKey),
+ value: rateValue,
+ labelIdForTranslate: rateKey
+ } as SelectOption);
+ });
+
+ const labelStyle: React.CSSProperties = {
+ fontWeight: 'bold',
+ margin: 0
+ };
+
+ return (
+
+ {
+ graphState.chartToRender == 'line' &&
+
+
:
+ { /* On change update the line graph rate in the store after a null check */}
+
+ }
+
+ );
+}
diff --git a/src/client/app/components/HeaderButtonsComponent.tsx b/src/client/app/components/HeaderButtonsComponent.tsx
index 97cba1e9e..b40b9b8c0 100644
--- a/src/client/app/components/HeaderButtonsComponent.tsx
+++ b/src/client/app/components/HeaderButtonsComponent.tsx
@@ -44,6 +44,7 @@ export default class HeaderButtonsComponent extends React.Component
+
diff --git a/src/client/app/components/RouteComponent.tsx b/src/client/app/components/RouteComponent.tsx
index e7fcf8825..28312c91c 100644
--- a/src/client/app/components/RouteComponent.tsx
+++ b/src/client/app/components/RouteComponent.tsx
@@ -24,7 +24,6 @@ import { validateComparePeriod, validateSortingOrder } from '../utils/calculateC
import EditGroupsContainer from '../containers/groups/EditGroupsContainer';
import CreateGroupContainer from '../containers/groups/CreateGroupContainer';
import GroupsDetailContainer from '../containers/groups/GroupsDetailContainer';
-import MetersDetailContainer from '../containers/meters/MetersDetailContainer';
import UsersDetailContainer from '../containers/admin/UsersDetailContainer';
import CreateUserContainer from '../containers/admin/CreateUserContainer';
import { TimeInterval } from '../../../common/TimeInterval';
@@ -33,6 +32,8 @@ import MapCalibrationContainer from '../containers/maps/MapCalibrationContainer'
import UploadCSVContainer from '../containers/csv/UploadCSVContainer';
import { UserRole } from '../types/items';
import { hasPermissions } from '../utils/hasPermissions';
+import UnitsDetailComponent from './unit/UnitsDetailComponent';
+import MetersDetailComponent from './meters/MetersDetailComponent';
import * as queryString from 'query-string';
interface RouteProps {
@@ -243,7 +244,7 @@ export default class RouteComponent extends React.Component {
this.requireAuth(AdminComponent())}/>
this.requireRole(UserRole.CSV, )}/>
this.checkAuth()}/>
- this.checkAuth()}/>
+ this.checkAuth()}/>
this.linkToGraph(, location.search)}/>
this.requireAuth()}/>
this.requireAuth()}/>
@@ -251,6 +252,7 @@ export default class RouteComponent extends React.Component {
this.requireAuth()}/>
this.requireAuth()}/>
this.requireAuth( []}/>)}/>
+ this.requireAuth()}/>
diff --git a/src/client/app/components/TooltipHelpComponent.tsx b/src/client/app/components/TooltipHelpComponent.tsx
index b76054a22..bb87c52ad 100644
--- a/src/client/app/components/TooltipHelpComponent.tsx
+++ b/src/client/app/components/TooltipHelpComponent.tsx
@@ -41,6 +41,9 @@ export default class TooltipHelpComponent extends React.Component
-
+
diff --git a/src/client/app/components/UIOptionsComponent.tsx b/src/client/app/components/UIOptionsComponent.tsx
index c5456f1b9..111134f58 100644
--- a/src/client/app/components/UIOptionsComponent.tsx
+++ b/src/client/app/components/UIOptionsComponent.tsx
@@ -19,6 +19,7 @@ import TooltipMarkerComponent from './TooltipMarkerComponent';
import 'rc-slider/assets/index.css';
import MapChartSelectComponent from './MapChartSelectComponent';
import ReactTooltip from 'react-tooltip';
+import GraphicRateMenuComponent from './GraphicRateMenuComponent';
const Slider = createSliderWithTooltip(sliderWithoutTooltips);
@@ -86,6 +87,7 @@ class UIOptionsComponent extends React.Component
+
{/* Controls specific to the bar chart. */}
{this.props.chartToRender === ChartTypes.bar &&
diff --git a/src/client/app/components/meters/CreateMeterModalComponent.tsx b/src/client/app/components/meters/CreateMeterModalComponent.tsx
new file mode 100644
index 000000000..a390c759d
--- /dev/null
+++ b/src/client/app/components/meters/CreateMeterModalComponent.tsx
@@ -0,0 +1,559 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+* License, v. 2.0. If a copy of the MPL was not distributed with this
+* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+import * as React from 'react';
+import { Modal, Button } from 'react-bootstrap';
+import { Input } from 'reactstrap';
+import { FormattedMessage } from 'react-intl';
+import translate from '../../utils/translate';
+import '../../styles/Modal.unit.css';
+import { MeterType } from '../../types/redux/meters';
+import { useDispatch } from 'react-redux';
+import { addMeter } from '../../actions/meters';
+import { useState } from 'react';
+import { TrueFalseType } from '../../types/items';
+
+export default function CreateMeterModalComponent() {
+
+ const dispatch = useDispatch();
+
+ const defaultValues = {
+ id: -99,
+ identifier : '',
+ name : '',
+ area : 0,
+ enabled : true,
+ displayable : true,
+ meterType : MeterType.other,
+ url : '',
+ timeZone : '',
+ gps : 'latitude, longitude',
+ unitId : 0,
+ defaultGraphicUnit : 0,
+ note : '',
+ cumulative : false,
+ cumulativeReset : false,
+ cumulativeResetStart : '',
+ cumulativeResetEnd : '',
+ endOnlyTime : false,
+ reading : 0,
+ readingGap : 0,
+ readingVariation : 0,
+ readingDuplication : 0,
+ timeSort : false,
+ startTimestamp : '',
+ endTimestamp : ''
+ }
+
+ /* State */
+ // We can definitely sacrifice readability here (and in the render) to consolidate these into a single function if need be
+ // NOTE a lot of this is copied from the MeterModalEditComponent, in the future we could make a single component to handle all edit pages if need be
+ // TODO Katherine with Delaney help are going to try to consolidate to create reusable functions.
+
+ // Modal show
+ const [showModal, setShowModal] = useState(false);
+ const handleClose = () => {
+ setShowModal(false);
+ resetState();
+ };
+ const handleShow = () => setShowModal(true);
+
+ // id
+ const [id, setId] = useState(defaultValues.id);
+ const handleIdChange = (e: React.ChangeEvent) => {
+ setId(Number(e.target.value));
+ }
+
+ // identifier
+ const [identifier, setIdentifier] = useState(defaultValues.identifier);
+ const handleIdentifierChange = (e: React.ChangeEvent) => {
+ setIdentifier(e.target.value);
+ }
+
+ // name
+ const [name, setName] = useState(defaultValues.name);
+ const handleNameChange = (e: React.ChangeEvent) => {
+ setName(e.target.value);
+ }
+
+ // area
+ const [area, setArea] = useState(defaultValues.area);
+ const handleAreaChange = (e: React.ChangeEvent) => {
+ setArea(Number(e.target.value));
+ }
+
+ // enabled
+ const [enabled, setEnabled] = useState(defaultValues.enabled);
+ const handleEnabledChange = (e: React.ChangeEvent) => {
+ setEnabled(JSON.parse(e.target.value));
+ }
+
+ // displayable
+ const [displayable, setDisplayable] = useState(defaultValues.displayable);
+ const handleDisplayableChange = (e: React.ChangeEvent) => {
+ setDisplayable(JSON.parse(e.target.value));
+ }
+
+ // meterType
+ const [meterType, setMeterType] = useState(defaultValues.meterType? `${defaultValues.meterType}` : '');
+ const handleMeterTypeChange = (e: React.ChangeEvent) => {
+ setMeterType(e.target.value);
+ }
+
+ // URL
+ const [url, setUrl] = useState(defaultValues.url);
+ const handleUrlChange = (e: React.ChangeEvent) => {
+ setUrl(e.target.value);
+ }
+
+ // timezone
+ const [timeZone, setTimeZone] = useState(defaultValues.timeZone);
+ const handleTimeZoneChange = (e: React.ChangeEvent) => {
+ setTimeZone(e.target.value);
+ }
+
+ // GPS
+ const [gps, setGps] = useState(defaultValues.gps);
+ const handleGpsChange = (e: React.ChangeEvent) => {
+ setGps(e.target.value);
+ }
+
+ // unitID
+ const [unitId, setUnitID] = useState(defaultValues.unitId);
+ const handleUnitIDChange = (e: React.ChangeEvent) => {
+ setUnitID(Number(e.target.value));
+ }
+
+ // defaultGraphicUnit
+ const [defaultGraphicUnit, setDefaultGraphicUnit] = useState(defaultValues.defaultGraphicUnit);
+ const handleDefaultGraphicUnitChange = (e: React.ChangeEvent) => {
+ setDefaultGraphicUnit(Number(e.target.value));
+ }
+
+ // note
+ const [note, setNote] = useState(defaultValues.note);
+ const handleNoteChange = (e: React.ChangeEvent) => {
+ setNote(e.target.value);
+ }
+
+ // cumulative
+ const [cumulative, setCumulative] = useState(defaultValues.cumulative);
+ const handleCumulativeChange = (e: React.ChangeEvent) => {
+ setCumulative(JSON.parse(e.target.value));
+ }
+
+ // cumulativeReset
+ const [cumulativeReset, setCumulativeReset] = useState(defaultValues.cumulativeReset);
+ const handleCumulativeResetChange = (e: React.ChangeEvent) => {
+ setCumulativeReset(JSON.parse(e.target.value));
+ }
+
+ // cumulativeResetStart
+ const [cumulativeResetStart, setCumulativeResetStart] = useState(defaultValues.cumulativeResetStart);
+ const handleCumulativeResetStartChange = (e: React.ChangeEvent) => {
+ setCumulativeResetStart(e.target.value);
+ }
+
+ // cumulativeResetEnd
+ const [cumulativeResetEnd, setCumulativeResetEnd] = useState(defaultValues.cumulativeResetEnd);
+ const handleCumulativeResetEndChange = (e: React.ChangeEvent) => {
+ setCumulativeResetEnd(e.target.value);
+ }
+
+ // endOnlyTime
+ const [endOnlyTime, setEndOnlyTime] = useState(defaultValues.endOnlyTime);
+ const handleEndOnlyTimeChange = (e: React.ChangeEvent) => {
+ setEndOnlyTime(JSON.parse(e.target.value));
+ }
+
+ // reading
+ const [reading, setReading] = useState(defaultValues.reading);
+ const handleReadingChange = (e: React.ChangeEvent) => {
+ setReading(Number(e.target.value));
+ }
+
+ // readingGap
+ const [readingGap, setReadingGap] = useState(defaultValues.readingGap);
+ const handleReadingGapChange = (e: React.ChangeEvent) => {
+ setReadingGap(Number(e.target.value));
+ }
+
+ // readingVariation
+ const [readingVariation, setReadingVariation] = useState(defaultValues.readingVariation);
+ const handleReadingVariationChange = (e: React.ChangeEvent) => {
+ setReadingVariation(Number(e.target.value));
+ }
+
+ // readingDuplication
+ const [readingDuplication, setReadingDuplication] = useState(defaultValues.readingDuplication);
+ const handleReadingDuplicationChange = (e: React.ChangeEvent) => {
+ setReadingDuplication(Number(e.target.value));
+ }
+
+ // timeSort
+ const [timeSort, setTimeSort] = useState(defaultValues.timeSort);
+ const handleTimeSortChange = (e: React.ChangeEvent) => {
+ setTimeSort(JSON.parse(e.target.value));
+ }
+
+ // startTimestamp
+ const [startTimestamp, setStartTimestamp] = useState(defaultValues.startTimestamp);
+ const handleStartTimestampChange = (e: React.ChangeEvent) => {
+ setStartTimestamp(e.target.value);
+ }
+
+ // endTimestamp
+ const [endTimestamp, setEndTimestamp] = useState(defaultValues.endTimestamp);
+ const handleEndTimestampChange = (e: React.ChangeEvent) => {
+ setEndTimestamp(e.target.value);
+ }
+
+
+ /* End State */
+
+ // Reset the state to default values
+ // This would also benefit from a single state changing function for all state
+ const resetState = () => {
+ setName(defaultValues.name);
+ setIdentifier(defaultValues.identifier);
+ }
+
+ // Unlike edit, we decided to discard and inputs when you choose to leave the page. The reasoning is
+ // that create starts from an empty template.
+
+ // Submit
+ const handleSubmit = () => {
+
+ // Close modal first to avoid repeat clicks
+ setShowModal(false);
+
+ // New Meter object, overwrite all unchanged props with state
+ const newMeter = {
+ ...defaultValues,
+ id,
+ identifier,
+ name,
+ area,
+ enabled,
+ displayable,
+ meterType,
+ url,
+ timeZone,
+ gps,
+ unitId,
+ defaultGraphicUnit,
+ note,
+ cumulative,
+ cumulativeReset,
+ cumulativeResetStart,
+ cumulativeResetEnd,
+ endOnlyTime,
+ reading,
+ readingGap,
+ readingVariation,
+ readingDuplication,
+ timeSort,
+ startTimestamp,
+ endTimestamp
+
+ }
+
+ // Set default identifier as name if left blank
+ newMeter.identifier = (!newMeter.identifier || newMeter.identifier.length === 0) ? newMeter.name : newMeter.identifier;
+
+
+ // Add the new Meter and update the store
+ dispatch(addMeter(newMeter));
+
+ resetState();
+ };
+
+ const formInputStyle: React.CSSProperties = {
+ paddingBottom: '5px'
+ }
+
+ const tableStyle: React.CSSProperties = {
+ width: '100%'
+ };
+
+
+ return (
+ <>
+ {/* Show modal button */}
+
+
+
+
+
+
+ {/* when any of the Meter are changed call one of the functions. */}
+
+
+
+
+ {/* Hides the modal */}
+
+ {/* On click calls the function handleSaveChanges in this component */}
+
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/src/client/app/components/meters/MeterViewComponent.tsx b/src/client/app/components/meters/MeterViewComponent.tsx
index 9a0c93b7a..623882858 100644
--- a/src/client/app/components/meters/MeterViewComponent.tsx
+++ b/src/client/app/components/meters/MeterViewComponent.tsx
@@ -1,344 +1,51 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import * as React from 'react';
import { Button } from 'reactstrap';
-import { FormattedMessage, injectIntl, WrappedComponentProps } from 'react-intl';
-import { MeterMetadata, EditMeterDetailsAction } from '../../types/redux/meters';
-import { GPSPoint, isValidGPSInput } from '../../utils/calibration';
-import TimeZoneSelect from '../TimeZoneSelect';
-import { updateUnsavedChanges } from '../../actions/unsavedWarning';
-import { fetchMetersDetails, submitEditedMeters, confirmEditedMeters } from '../../actions/meters';
-import store from '../../index';
-
-interface MeterViewProps {
- // The ID of the meter to be displayed
- id: number;
- // The meter metadata being displayed by this row
- meter: MeterMetadata;
- isEdited: boolean;
- isSubmitting: boolean;
- loggedInAsAdmin: boolean;
- // The function used to dispatch the action to edit meter details
- editMeterDetails(meter: MeterMetadata): EditMeterDetailsAction;
- log(level: string, message: string): any;
-}
-
-interface MeterViewState {
- gpsFocus: boolean;
- gpsInput: string;
- identifierFocus: boolean;
- identifierInput: string;
-}
-
-type MeterViewPropsWithIntl = MeterViewProps & WrappedComponentProps;
-
-class MeterViewComponent extends React.Component {
- constructor(props: MeterViewPropsWithIntl) {
- super(props);
- this.state = {
- gpsFocus: false,
- gpsInput: (this.props.meter.gps) ? `${this.props.meter.gps.latitude},${this.props.meter.gps.longitude}` : '',
- identifierFocus: false,
- identifierInput: this.props.meter.identifier
- };
- this.toggleMeterDisplayable = this.toggleMeterDisplayable.bind(this);
- this.toggleMeterEnabled = this.toggleMeterEnabled.bind(this);
- this.toggleGPSInput = this.toggleGPSInput.bind(this);
- this.handleGPSChange = this.handleGPSChange.bind(this);
- this.changeTimeZone = this.changeTimeZone.bind(this);
- this.toggleIdentifierInput = this.toggleIdentifierInput.bind(this);
- this.handleIdentifierChange = this.handleIdentifierChange.bind(this);
- }
-
- public render() {
- const loggedInAsAdmin = this.props.loggedInAsAdmin;
- return (
-