From 578baed6014f613aa09f3d28621256ff2be9704f Mon Sep 17 00:00:00 2001 From: Artur Dudnik Date: Tue, 7 Jan 2020 22:02:56 +0300 Subject: [PATCH] upgrade eslint, add eslint --fix on save #67 --- .eslintignore | 9 +- .eslintrc | 165 +++++- .flowconfig | 19 + babel.config.js | 14 +- dev.server.js | 26 +- jest.config.js | 9 +- jsconfig.json | 11 + package.json | 23 +- src/client/client.js | 22 + src/client/components/ConfirmationDialog.js | 31 +- .../components/FilterView/CustomMarkers.js | 100 ++-- .../components/FilterView/DeleteDeviceLink.js | 38 +- .../components/FilterView/DeviceField.js | 32 +- src/client/components/FilterView/OrgField.js | 100 ++-- src/client/components/FilterView/Style.js | 17 +- src/client/components/FilterView/index.js | 152 ++--- src/client/components/HeaderView.js | 35 +- src/client/components/ListView.js | 180 +++--- src/client/components/LoadingIndicator.js | 35 +- src/client/components/LocationView.js | 75 +-- src/client/components/MapView.js | 546 +++++++++--------- src/client/components/MarkerClusterer.js | 3 +- .../components/RemoveAnimationProvider.js | 12 +- src/client/components/TabPanel.js | 4 +- src/client/components/TooManyPointsWarning.js | 29 +- src/client/components/Viewport.js | 85 +-- src/client/components/ViewportStyle.js | 28 +- src/client/components/WatchModeWarning.js | 12 +- src/client/components/WrappedViewport.js | 8 +- src/client/constants.js | 1 - src/client/globalBus.js | 21 +- src/client/main.js | 67 +-- src/client/reducer/dashboard/index.js | 500 ++++++++-------- src/client/reducer/index.js | 5 +- src/client/reducer/state.js | 2 + src/client/storage.js | 61 +- src/client/store.js | 6 +- src/client/utils/GA.js | 8 +- src/client/utils/cloneState.js | 7 +- src/client/utils/formatDate.js | 2 +- src/server/database/CompanyModel.js | 7 +- src/server/database/DeviceModel.js | 10 +- src/server/database/LocationDefinition.js | 4 +- src/server/database/LocationModel.js | 31 +- src/server/database/define-sequelize-db.js | 6 +- src/server/database/initializeDatabase.js | 20 +- src/server/database/migrate.js | 27 +- src/server/index.js | 40 +- src/server/libs/RNCrypto.js | 24 +- src/server/libs/jwt.js | 14 +- src/server/libs/utils.js | 37 +- src/server/models/Device.js | 54 +- src/server/models/Location.js | 149 ++--- src/server/models/Org.js | 16 +- src/server/routes/api-v2.js | 203 ++++--- src/server/routes/site-api.js | 85 ++- src/server/routes/tests.js | 30 +- tests/api.test.js | 105 ++-- tests/data.js | 40 +- tests/site-api.test.js | 60 +- webpack.config.js | 28 +- 61 files changed, 1942 insertions(+), 1548 deletions(-) create mode 100644 jsconfig.json create mode 100644 src/client/client.js diff --git a/.eslintignore b/.eslintignore index f813452..cdc9738 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,6 +1,3 @@ -blueprints/**/files/** -coverage/** -node_modules/** -dist/** -src/index.html -**/flow_validate_* +coverage/* +node_modules/* +dist/* diff --git a/.eslintrc b/.eslintrc index 9828da5..7e20ae6 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,7 +1,8 @@ { "parser": "babel-eslint", "extends": [ - "standard", + "airbnb", + "eslint:recommended", "plugin:react/recommended", "plugin:flowtype/recommended" ], @@ -10,47 +11,122 @@ "react", "promise", "immutable", - "flowtype", + "flowtype" ], "env": { - "browser" : true, + "browser": true, "jest": true }, "globals": { - "__line" : false, - "__DEV__" : false, - "__TEST__" : false, - "__PROD__" : false, - "__COVERAGE__" : false, + "__line": false, + "__DEV__": false, + "__TEST__": false, + "__PROD__": false, + "__COVERAGE__": false, "$PropertyType": false, "$Shape": false, - "google": false - }, - "settings": { - "flowtype": { - "onlyFilesWithFlowAnnotation": true - }, - "react": { - "version": "detect" - } + "google": false, + "history": false }, "rules": { - "no-use-before-deinfe" : 0, - "comma-dangle" : ["error", "always-multiline"], - "key-spacing" : 0, - "semi" : [2, "always"], - "jsx-quotes" : [2, "prefer-single"], - "max-len" : [2, 120, 2], - "object-curly-spacing" : [2, "always"], - + "import/default": 0, + "import/named": 0, + "import/namespace": 0, + "import/no-named-as-default-member": 0, + "import/no-named-as-default": 0, + "import/order": [ + "error", + { + "groups": [ + [ + "builtin", + "external" + ], + [ + "internal", + "index" + ], + [ + "parent" + ], + [ + "sibling" + ] + ], + "newlines-between": "always-and-inside-groups" + } + ], + "no-plusplus": 0, + "react/jsx-props-no-spreading": 0, + "no-return-assign": 0, + "arrow-parens": ["error", "as-needed"], + "space-before-function-paren": 0, + "lines-between-class-members": 0, + "no-nested-ternary": 0, + "operator-linebreak": [ + "error", + "after", + { + "overrides": { + "?": "before", + ":": "before" + } + } + ], + "no-use-before-deinfe": 0, + "comma-dangle": [ + "error", + "always-multiline" + ], + "key-spacing": 0, + "semi": [ + 2, + "always" + ], + "jsx-quotes": [ + 2, + "prefer-single" + ], + "max-len": [ + 2, + 120, + 2 + ], + "object-curly-spacing": [ + 2, + "always" + ], + "object-curly-newline": [ + "error", + { + "ObjectExpression": { + "multiline": true, + "minProperties": 3 + }, + "ObjectPattern": { + "multiline": true, + "minProperties": 3 + }, + "ImportDeclaration": { + "multiline": true, + "minProperties": 3 + }, + "ExportDeclaration": { + "multiline": true, + "minProperties": 3 + } + } + ], + "no-unused-expressions": 0, + "no-bitwise": 0, + "react/require-default-props": 0, + "react/jsx-filename-extension": 0, + "react/destructuring-assignment": 1, "react/no-unused-prop-types": 0, //We use flow "react/prop-types": 0, //We use flow - /* "immutable/no-let": 2, */ /* "immutable/no-mutation": 2, */ /* "immutable/no-this": 2, */ - - "flowtype/boolean-style": [ 2, "boolean" @@ -100,5 +176,36 @@ ], "flowtype/use-flow-type": 1, "flowtype/valid-syntax": 1 + }, + "settings": { + "flowtype": { + "onlyFilesWithFlowAnnotation": true + }, + "react": { + "version": "detect" + }, + "import/parser": "babel-eslint", + "import/resolver": { + "node": { + "moduleDirectory": [ + "src/server", + "src/client", + "./", + "node_modules" + ], + "paths": [ + "src/server", + "src/client", + "./", + "node_modules" + ] + }, + }, + "import/extensions": [ + ".js" + ], + "import/ignore": [ + "\\.(scss|less|css)$" + ] } -} +} \ No newline at end of file diff --git a/.flowconfig b/.flowconfig index bf9a5ff..55ce23d 100644 --- a/.flowconfig +++ b/.flowconfig @@ -1,11 +1,30 @@ [ignore] .*/compiled + [include] [libs] [options] module.name_mapper='^~/\(.*\)$' -> '/src/client/\1' +module.name_mapper='^{rootPathPrefix}/\(.*\)$' -> '/{rootPathSuffix}/\1' module.name_mapper='^.*\.css$' -> 'css-module-flow' module.name_mapper='^.*\.scss$' -> 'css-module-flow' esproposal.decorators=ignore +module.system.node.resolve_dirname=node_modules +module.system.node.resolve_dirname=src/client +module.system.node.resolve_dirname=src/server +module.system=haste +munge_underscores=false +esproposal.class_static_fields=enable +esproposal.class_instance_fields=enable + +suppress_type=$FlowIssue +suppress_type=$FlowFixMe +suppress_type=$FixMe +suppress_type=$FlowExpectedError + +suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(3[0-3]\\|[1-2][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*www[a-z,_]*\\)?)\\) +suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(3[0-3]\\|[1-2][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*www[a-z,_]*\\)?)\\)?:? #[0-9]+ +suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy +suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError \ No newline at end of file diff --git a/babel.config.js b/babel.config.js index 2632cdb..129f299 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,22 +1,16 @@ module.exports = { presets: ['@babel/preset-flow', '@babel/preset-env', '@babel/preset-react'], plugins: [ - ['@babel/plugin-proposal-decorators', { 'legacy': true }], + ['@babel/plugin-proposal-decorators', { legacy: true }], 'transform-function-bind', '@babel/plugin-proposal-export-namespace-from', '@babel/plugin-proposal-class-properties', '@babel/plugin-proposal-export-default-from', ['babel-plugin-root-import', { - 'paths': [ - { 'rootPathSuffix': 'src/client' }, + paths: [ + { rootPathSuffix: 'src/client', rootPathPrefix: '' }, ], }], ], - env: { - development: { - plugins: [ - 'react-hot-loader/babel', - ], - }, - }, + env: { development: { plugins: ['react-hot-loader/babel'] } }, }; diff --git a/dev.server.js b/dev.server.js index abd2b99..2635c87 100644 --- a/dev.server.js +++ b/dev.server.js @@ -13,9 +13,8 @@ const app = express(); const compiler = webpack(webpackConfig); const make = (apiAddress) => { const proxy = apiAddress - ? httpProxy.createProxyServer({ - target: apiAddress, - }) : httpProxy.createProxyServer(); + ? httpProxy.createProxyServer({ target: apiAddress }) + : httpProxy.createProxyServer(); proxy.on('error', (error, req, res) => { if (error.code !== 'ECONNRESET') { @@ -34,9 +33,7 @@ const register = (app, proxy, path, apiAddress) => { console.log(`Server ${app.name} will proxy ${path} to ${apiAddress}`); app.use(path, (req, res) => { - proxy.web(req, res, { - target: apiAddress, - }); + proxy.web(req, res, { target: apiAddress }); }); }; const middleware = [ @@ -44,9 +41,7 @@ const middleware = [ port: devPort, contentBase: path.join(__dirname, 'src', 'client'), hot: true, - stats: { - colors: true, - }, + stats: { colors: true }, compress: true, }), webpackHotMiddleware(compiler, { @@ -59,9 +54,7 @@ const middleware = [ app.use(middleware); -[ - { address: `http://localhost:${port}/api`, path: '/api' }, -].forEach(cfg => { +[{ address: `http://localhost:${port}/api`, path: '/api' }].forEach((cfg) => { const proxy = make(cfg.address); app.on('stop', () => proxy.close()); register(app, proxy, cfg.path, cfg.address); @@ -83,8 +76,7 @@ app.listen(devPort, () => { console.log('Developer Server | port: %s', devPort); }); -process - .on('dev server Uncaught Exception', (err) => { - // eslint-disable-next-line no-console - console.error(' Exception %s: ', err.message, err.stack); - }); +process.on('dev server Uncaught Exception', (err) => { + // eslint-disable-next-line no-console + console.error(' Exception %s: ', err.message, err.stack); +}); diff --git a/jest.config.js b/jest.config.js index 7f366d2..3cbe567 100644 --- a/jest.config.js +++ b/jest.config.js @@ -6,17 +6,12 @@ module.exports = { ...defaults.transform, '^.+\\.[t|j]sx?$': '/jest.transform.js', }, - moduleFileExtensions: [ - 'js', - 'jsx', - ], + moduleFileExtensions: ['js', 'jsx'], testEnvironment: 'node', coveragePathIgnorePatterns: [].concat( defaults.coveragePathIgnorePatterns, [] ), - setupFiles: [ - '/jest.init.js', - ], + setupFiles: ['/jest.init.js'], verbose: true, }; diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..2075591 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES6", + "module": "commonjs", + "experimentalDecorators": true + }, + "exclude": [ + "/node_modules/", + "node_modules" + ] +} \ No newline at end of file diff --git a/package.json b/package.json index 83a8d4a..7c2c84c 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "A field-testing/analysis server & web-app for the Background Geolocation plugin", "main": "index.js", "scripts": { - "lint": "eslint src/ && flow", + "lint": "eslint src/**/*.js --fix && flow", "build": "NODE_ENV=production webpack", "start": "better-npm-run start", "dev": "better-npm-run dev", @@ -24,14 +24,14 @@ "command": "concurrently --kill-others \"BABEL_ENV=node node --inspect=9229 ./bin/server.js\" \"BABEL_ENV=webpack node ./bin/dev.js\"", "env": { "TZ": "UTC", - "NODE_PATH": "./src/server/" + "NODE_PATH": "./src/client/" } }, "start": { "command": "node ./bin/server.js", "env": { "TZ": "UTC", - "NODE_PATH": "./src/server/", + "NODE_PATH": "./src/client/", "DYNO": "1", "NODE_ENV": "production", "NPM_CONFIG_PRODUCTION": "true" @@ -39,9 +39,8 @@ } }, "dependencies": { - "@babel/core": "^7.7.2", - "@babel/preset-env": "^7.7.1", "@babel/cli": "^7.6.0", + "@babel/core": "^7.7.2", "@babel/node": "^7.6.1", "@babel/plugin-proposal-class-properties": "^7.5.5", "@babel/plugin-proposal-decorators": "^7.4.4", @@ -49,6 +48,7 @@ "@babel/plugin-proposal-export-namespace-from": "^7.5.2", "@babel/plugin-transform-regenerator": "^7.4.5", "@babel/polyfill": "^7.4.4", + "@babel/preset-env": "^7.7.1", "@babel/preset-flow": "^7.0.0", "@babel/preset-react": "^7.0.0", "@babel/register": "^7.5.5", @@ -56,9 +56,10 @@ "@material-ui/core": "^4.4.1", "@material-ui/icons": "^4.4.1", "@material-ui/pickers": "^3.2.5", + "@material-ui/styles": "^4.8.2", + "@hot-loader/react-dom": "^16.8.6", "babel-loader": "^8.0.6", "babel-node": "0.0.1-security", - "babel-plugin-root-import": "^6.4.1", "babel-plugin-transform-flow-strip-types": "^6.22.0", "babel-plugin-transform-function-bind": "^6.22.0", "babel-plugin-transform-require-ignore": "0.0.2", @@ -111,14 +112,16 @@ "copy-webpack-plugin": "^5.0.4", "css-loader": "^0.26.1", "ejs-loader": "^0.3.0", - "eslint": "^4.0.0", + "eslint": "^6.8.0", + "eslint-config-airbnb": "^18.0.1", "eslint-config-standard": "^10.2.1", "eslint-config-standard-react": "^5.0.0", "eslint-loader": "^1.8.0", "eslint-plugin-babel": "^4.1.1", "eslint-plugin-flowtype": "^2.34.0", "eslint-plugin-immutable": "^1.0.0", - "eslint-plugin-import": "^2.3.0", + "eslint-plugin-import": "^2.19.1", + "eslint-plugin-jsx-a11y": "^6.2.3", "eslint-plugin-node": "^5.0.0", "eslint-plugin-promise": "^3.5.0", "eslint-plugin-react": "^7.1.0", @@ -136,9 +139,7 @@ "postcss-mixins": "^6.2.2", "postcss-modules-values": "^3.0.0", "postcss-preset-env": "^5.3.0", - "prettier-eslint": "^6.3.0", - "prettier-eslint-cli": "^4.1.1", - "react-hot-loader": "^3.0.0-beta.6", + "react-hot-loader": "^4.6.3", "sqlite3": "~4.0.6", "style-loader": "^0.13.1", "uglifyjs-webpack-plugin": "^2.2.0", diff --git a/src/client/client.js b/src/client/client.js new file mode 100644 index 0000000..71d6959 --- /dev/null +++ b/src/client/client.js @@ -0,0 +1,22 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import { + BrowserRouter as Router, + Route, + Switch, +} from 'react-router-dom'; + +import WrappedViewport from './components/WrappedViewport'; + +const App = ({ store }) => ( + + + + + + + + +); + +export default App; diff --git a/src/client/components/ConfirmationDialog.js b/src/client/components/ConfirmationDialog.js index 21ea399..f473f65 100644 --- a/src/client/components/ConfirmationDialog.js +++ b/src/client/components/ConfirmationDialog.js @@ -14,19 +14,22 @@ export type Result = {| value: any |}; type Props = {| children: any, - onOpen?: () => {}, onClose: (?Result) => {}, - dlgContentProps: any, + open: boolean, title: any, - value: any, + + dlgContentProps?: any, + onOpen?: (Result) => {}, + onEntering?: () => {}, + value?: any, |}; -export default function ConfirmationDialog ({ +export default function ConfirmationDialog({ onClose, - value, open, children, - onOpen, + value = undefined, + onOpen = undefined, title = 'Confirm?', dlgContentProps = {}, ...other @@ -57,10 +60,18 @@ export default function ConfirmationDialog ({ > {title} - - {typeof children === 'string' - ? {children} - : children} + + {typeof children === 'string' ? ( + + {children} + + ) : ( + children + )} ); } -}; +} -export default connect( - undefined, - { - onAddTestMarker: (data) => ({ - type: 'ADD_TEST_MARKER', - value: { data }, - }), - })(CustomMarkers); +export default connect(undefined, { + onAddTestMarker: data => ({ + type: 'ADD_TEST_MARKER', + value: { data }, + }), +})(CustomMarkers); diff --git a/src/client/components/FilterView/DeleteDeviceLink.js b/src/client/components/FilterView/DeleteDeviceLink.js index 8980a91..dd0fbc9 100644 --- a/src/client/components/FilterView/DeleteDeviceLink.js +++ b/src/client/components/FilterView/DeleteDeviceLink.js @@ -7,16 +7,21 @@ import Radio from '@material-ui/core/Radio'; import FormControlLabel from '@material-ui/core/FormControlLabel'; import { connect } from 'react-redux'; import format from 'date-fns/format'; + +import { type GlobalState } from 'reducer/state'; +import { deleteActiveDevice } from 'reducer/dashboard'; + import ConfirmationDialog, { type Result } from '../ConfirmationDialog'; import RemoveAnimationProvider from '../RemoveAnimationProvider'; -import { type GlobalState } from '~/reducer/state'; -import { deleteActiveDevice } from '~/reducer/dashboard'; + type StateProps = {| isVisible: boolean, + startDate: Date, + endDate: Date, |}; type DispatchProps = {| - deleteDevice: () => any, + deleteDevice: (filter?: { startDate: Date, endDate: Date }) => any, |}; type Props = {| ...StateProps, ...DispatchProps |}; const style = { @@ -27,7 +32,12 @@ const style = { textTransform: 'none', // float: 'right', }; -const DeleteDeviceLink = ({ isVisible, startDate, endDate, deleteDevice }: Props) => { +const DeleteDeviceLink = ({ + isVisible, + startDate, + endDate, + deleteDevice, +}: Props) => { const [open, setOpen] = React.useState(false); const [value, setValue] = React.useState('true'); const radioGroupRef = React.useRef(null); @@ -42,7 +52,7 @@ const DeleteDeviceLink = ({ isVisible, startDate, endDate, deleteDevice }: Props } }; - const onClose = (result: Result) => { + const onClose = (result?: Result) => { setOpen(false); result && deleteDevice(value ? { startDate, endDate } : undefined); }; @@ -50,17 +60,12 @@ const DeleteDeviceLink = ({ isVisible, startDate, endDate, deleteDevice }: Props e.preventDefault(); setOpen(true); }; - const handleChange = (e: Event) => { + const handleChange = (e: { target: HTMLInputElement }) => { setValue(e.target.value); }; return [ - , } - label={`between ${format(new Date(startDate), 'MM-dd')} and ${format(new Date(endDate), 'MM-dd')}`} + label={`between ${format( + new Date(startDate), + 'MM-dd', + )} and ${format(new Date(endDate), 'MM-dd')}`} /> @@ -96,8 +104,6 @@ const mapStateToProps = (state: GlobalState): StateProps => ({ endDate: state.dashboard.endDate, }); -const mapDispatchToProps: DispatchProps = { - deleteDevice: deleteActiveDevice, -}; +const mapDispatchToProps: DispatchProps = { deleteDevice: deleteActiveDevice }; export default connect(mapStateToProps, mapDispatchToProps)(DeleteDeviceLink); diff --git a/src/client/components/FilterView/DeviceField.js b/src/client/components/FilterView/DeviceField.js index 8735fbe..46af30c 100644 --- a/src/client/components/FilterView/DeviceField.js +++ b/src/client/components/FilterView/DeviceField.js @@ -1,7 +1,10 @@ // @flow import React from 'react'; -import { Select, TextField, MenuItem } from '@material-ui/core'; -import type { Source, MaterialInputElement } from '~/reducer/dashboard'; +import Select from '@material-ui/core/Select'; +import TextField from '@material-ui/core/TextField'; +import MenuItem from '@material-ui/core/MenuItem'; + +import type { Source, MaterialInputElement } from 'reducer/dashboard'; type Props = { onChange: (value: string) => any, @@ -12,9 +15,13 @@ type Props = { const flex = { display: 'flex' }; -const DeviceField = ({ onChange, source, hasData, value }: Props) => { +const DeviceField = ({ + onChange, source, hasData, value, +}: Props) => { const entry = !!source && source.find((x: Source) => x.value === value); - const text = !entry ? 'No device present' : entry.label; + const text = !entry + ? 'No device present' + : entry.label; const handleChange = (e: MaterialInputElement) => onChange(e.target.value); return source.length > 1 ? ( @@ -22,16 +29,19 @@ const DeviceField = ({ onChange, source, hasData, value }: Props) => { autoWidth style={flex} label='Device' - onChange={handleChange} value={'' + (value || '')} + onChange={handleChange} + value={`${value || ''}`} > - {source.map((x: Source) => {x.label})} + {source.map((x: Source) => ( + + {x.label} + + ))} ) - : ( - hasData - ? - : - ); + : hasData + ? + : ; }; export default DeviceField; diff --git a/src/client/components/FilterView/OrgField.js b/src/client/components/FilterView/OrgField.js index b9512a1..58f3d9d 100644 --- a/src/client/components/FilterView/OrgField.js +++ b/src/client/components/FilterView/OrgField.js @@ -1,6 +1,7 @@ // @flow import React from 'react'; -import { Select, MenuItem } from '@material-ui/core'; +import MenuItem from '@material-ui/core/MenuItem'; +import Select from '@material-ui/core/Select'; import Button from '@material-ui/core/Button'; import Dialog from '@material-ui/core/Dialog'; import IconButton from '@material-ui/core/IconButton'; @@ -22,8 +23,10 @@ import Grid from 'react-virtualized/dist/commonjs/Grid'; import DeviceUnknownIcon from '@material-ui/icons/DeviceUnknown'; import CloseRounded from '@material-ui/icons/CloseRounded'; import clx from 'classnames'; + +import type { Source, MaterialInputElement } from 'reducer/dashboard'; + import RemoveAnimationProvider from '../RemoveAnimationProvider'; -import type { Source, MaterialInputElement } from '~/reducer/dashboard'; type Props = { onChange: (value: string) => any, @@ -33,8 +36,13 @@ type Props = { }; // const theme = useTheme(); const flex = { display: 'flex' }; -const contentStyle = { minHeight: 400, position: 'relative', display: 'flex', flexDirection: 'column' }; -const containerStyle = { flex: '1 auto', 'overflowY': 'auto' }; +const contentStyle = { + minHeight: 400, + position: 'relative', + display: 'flex', + flexDirection: 'column', +}; +const containerStyle = { flex: '1 auto', overflowY: 'auto' }; const styles = (theme: any) => ({ closeButton: { position: 'absolute', @@ -55,9 +63,12 @@ const styles = (theme: any) => ({ }); const OrgField = withStyles(styles)((props: Props) => { - const { onChange, value, source: s, fullScreen, classes } = props; + const { + onChange, value, source: s, fullScreen, classes, + } = props; const [dialogOpen, setOpen] = React.useState(false); const [filter, setFilter] = React.useState(''); + const isLong = s.length > 10; const handleChange = (ev: Event) => { setFilter(ev.target.value); }; @@ -71,38 +82,31 @@ const OrgField = withStyles(styles)((props: Props) => { } setOpen(true); }; + const val = filter.toLowerCase(); + const source = val + ? s.filter((x: Source) => x.label && !!~x.label.toLowerCase().indexOf(val)) + : s; const handleOk = React.useCallback((index: number) => { setOpen(false); !!source.length && onChange(source[index].value); }); - const rowRenderer = ({ columnIndex, key, rowIndex, style }: any) => { - return ( - handleOk(rowIndex)} - className={clx( - classes.item, - 'list-row-item', - { [classes.selected]: value === source[rowIndex].value }, - )} - style={style} - > - - - - - {source[rowIndex].label} - - - ); - }; - const isLong = s.length > 10; + const rowRenderer = ({ + key, rowIndex, style, + }: any) => ( + handleOk(rowIndex)} + className={clx(classes.item, 'list-row-item', { [classes.selected]: value === source[rowIndex].value })} + style={style} + > + + + + {source[rowIndex].label} + + ); const contentRef = React.createRef(); - const val = filter.toLowerCase(); - const source = val - ? s.filter((x: Source) => x.label && !!~x.label.toLowerCase().indexOf(val)) - : s; if (!s.length) { return null; } @@ -118,7 +122,11 @@ const OrgField = withStyles(styles)((props: Props) => { onChange={({ target }: MaterialInputElement) => onChange(target.value)} value={value} > - {s.map((x: Source) => ({x.label}))} + {s.map((x: Source) => ( + + {x.label} + + ))} , { > Org selector - + - + Company Filter @@ -155,7 +164,7 @@ const OrgField = withStyles(styles)((props: Props) => { margin='dense' value={filter} onChange={handleChange} - endAdornment={ + endAdornment={( { - } + )} />
- - {({ isScrolling, registerChild, onChildScroll, scrollTop }: any) => ( + + {({ + isScrolling, + registerChild, + onChildScroll, + scrollTop, + }: any) => ( {({ width, height }: any) => (
diff --git a/src/client/components/FilterView/Style.js b/src/client/components/FilterView/Style.js index 6117dd9..c4933eb 100644 --- a/src/client/components/FilterView/Style.js +++ b/src/client/components/FilterView/Style.js @@ -1,23 +1,14 @@ import { makeStyles } from '@material-ui/core/styles'; const useStyles = makeStyles(theme => ({ - cardsContainer: { - padding: 5, - }, - header: { - // backgroundColor: theme.palette.primary.dark, - // color: theme.palette.primary.contrastText, - }, - appBar: { - backgroundColor: theme.palette.primary.dark, - }, + cardsContainer: { padding: 5 }, + header: {}, + appBar: { backgroundColor: theme.palette.primary.dark }, paddingRow: { marginTop: 10, // marginBottom: 10, }, - relative: { - position: 'relative', - }, + relative: { position: 'relative' }, switch: { margin: 0, display: 'flex', diff --git a/src/client/components/FilterView/index.js b/src/client/components/FilterView/index.js index 85c6bf8..ca001c2 100644 --- a/src/client/components/FilterView/index.js +++ b/src/client/components/FilterView/index.js @@ -1,59 +1,58 @@ // @flow -import React from 'react'; -import { connect } from 'react-redux'; import DateFnsUtils from '@date-io/date-fns'; -import { - AppBar, - Button, - Card, - CardContent, - CardHeader, - Checkbox, - FormControlLabel, - IconButton, - Switch, - TextField, - Toolbar, - Typography, - useTheme, -} from '@material-ui/core'; -import { - ChevronLeft as ChevronLeftIcon, - ChevronRight as ChevronRightIcon, - Refresh as RefreshIcon, -} from '@material-ui/icons'; +import AppBar from '@material-ui/core/AppBar'; +import IconButton from '@material-ui/core/IconButton'; +import Toolbar from '@material-ui/core/Toolbar'; +import Typography from '@material-ui/core/Typography'; +import Button from '@material-ui/core/Button'; +import Card from '@material-ui/core/Card'; +import CardContent from '@material-ui/core/CardContent'; +import CardHeader from '@material-ui/core/CardHeader'; +import Checkbox from '@material-ui/core/Checkbox'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import Switch from '@material-ui/core/Switch'; +import TextField from '@material-ui/core/TextField'; +import useMediaQuery from '@material-ui/core/useMediaQuery'; +import { useTheme } from '@material-ui/core/styles'; +import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'; +import ChevronRightIcon from '@material-ui/icons/ChevronRight'; +import RefreshIcon from '@material-ui/icons/Refresh'; import { DatePicker, MuiPickersUtilsProvider, TimePicker, } from '@material-ui/pickers'; -import useMediaQuery from '@material-ui/core/useMediaQuery'; +import React from 'react'; +import { connect } from 'react-redux'; -import useStyles from './Style'; -import formatDate from '~/utils/formatDate'; -import DeviceField from './DeviceField'; -import RemoveAnimationProvider from '../RemoveAnimationProvider'; -import DeleteDeviceLink from './DeleteDeviceLink'; -import OrgField from './OrgField'; -import CustomMarkers from './CustomMarkers'; -import { type GlobalState } from '~/reducer/state'; import { - type OrgToken, - type Device, - type MaterialInputElement, - type Source, - changeOrgToken, changeDeviceId, changeEnableClustering, changeEndDate, changeIsWatching, changeMaxMarkers, + changeOrgToken, changeShowGeofenceHits, changeShowMarkers, changeShowPolyline, changeStartDate, + type Device, + type MaterialInputElement, + type OrgToken, reload, -} from '~/reducer/dashboard'; + type Source, +} from 'reducer/dashboard'; +import { type GlobalState } from 'reducer/state'; +import formatDate from 'utils/formatDate'; + +import RemoveAnimationProvider from '../RemoveAnimationProvider'; + +import CustomMarkers from './CustomMarkers'; +import DeleteDeviceLink from './DeleteDeviceLink'; +import DeviceField from './DeviceField'; +import OrgField from './OrgField'; +import useStyles from './Style'; + const cardMargins = { marginBottom: '10px' }; type StateProps = {| companyId: string, @@ -88,7 +87,7 @@ type Props = {| ...DispatchProps, setOpen: (open: boolean) => any, |}; -const FilterView = function ({ +const FilterView = ({ companyId, orgTokens, deviceId, @@ -114,7 +113,7 @@ const FilterView = function ({ showMarkers, showPolyline, startDate, -}: Props): React$Element { +}: Props): React$Element => { const theme = useTheme(); const classes = useStyles(); const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); @@ -122,14 +121,19 @@ const FilterView = function ({
- + - - Filter - + Filter! setOpen(false)}> - {theme.direction === 'ltr' ? : } + {theme.direction === 'ltr' + ? + : } @@ -145,7 +149,12 @@ const FilterView = function ({ source={orgTokens} value={companyId} /> - +
@@ -184,14 +193,20 @@ const FilterView = function ({ value={endDate} />
- onChangeIsWatching(e.target.checked)} style={{ flex: 1 }} /> - } + )} label='Watch mode' /> @@ -263,26 +278,27 @@ const FilterView = function ({ ); }; -const mapStateToProps = function (state: GlobalState): StateProps { - return { - deviceId: '' + state.dashboard.deviceId, - companyId: '' + state.dashboard.companyId, - enableClustering: state.dashboard.enableClustering, - startDate: state.dashboard.startDate, - endDate: state.dashboard.endDate, - devices: state.dashboard.devices.map((device: Device) => ({ value: '' + device.id, label: device.name })), - orgTokens: state.dashboard.orgTokens.map((orgToken: OrgToken) => ({ - value: '' + orgToken.id, - label: orgToken.name, - })), - hasData: state.dashboard.hasData, - isWatching: state.dashboard.isWatching, - showGeofenceHits: state.dashboard.showGeofenceHits, - showPolyline: state.dashboard.showPolyline, - showMarkers: state.dashboard.showMarkers, - maxMarkers: state.dashboard.maxMarkers, - }; -}; +const mapStateToProps = (state: GlobalState): StateProps => ({ + deviceId: `${state.dashboard.deviceId}`, + companyId: `${state.dashboard.companyId}`, + enableClustering: state.dashboard.enableClustering, + startDate: state.dashboard.startDate, + endDate: state.dashboard.endDate, + devices: state.dashboard.devices.map((device: Device) => ({ + value: `${device.id}`, + label: device.name, + })), + orgTokens: state.dashboard.orgTokens.map((orgToken: OrgToken) => ({ + value: `${orgToken.id}`, + label: orgToken.name, + })), + hasData: state.dashboard.hasData, + isWatching: state.dashboard.isWatching, + showGeofenceHits: state.dashboard.showGeofenceHits, + showPolyline: state.dashboard.showPolyline, + showMarkers: state.dashboard.showMarkers, + maxMarkers: state.dashboard.maxMarkers, +}); const mapDispatchToProps: DispatchProps = { onReload: reload, diff --git a/src/client/components/HeaderView.js b/src/client/components/HeaderView.js index a4d5b15..9424259 100644 --- a/src/client/components/HeaderView.js +++ b/src/client/components/HeaderView.js @@ -1,17 +1,16 @@ // @flow -import React from 'react'; -import clsx from 'classnames'; -import { - AppBar, - Toolbar, - Link, - Typography, - IconButton, -} from '@material-ui/core'; +import AppBar from '@material-ui/core/AppBar'; +import IconButton from '@material-ui/core/IconButton'; +import Link from '@material-ui/core/Link'; +import Toolbar from '@material-ui/core/Toolbar'; +import Typography from '@material-ui/core/Typography'; import MenuIcon from '@material-ui/icons/Menu'; +import clsx from 'classnames'; +import React from 'react'; + import logo from '../assets/images/transistor-logo.svg'; -const style = { 'justifyContent': 'space-between' }; +const style = { justifyContent: 'space-between' }; const margin = { marginRight: '-20px' }; type Props = {| classes: {| @@ -28,7 +27,9 @@ type Props = {| location: any, |}; -const HeaderView = ({ classes, open, setOpen, children, location }: Props) => +const HeaderView = ({ + classes, open, setOpen, children, location, +}: Props) => ( setOpen(true)} + color='inherit' + onClick={() => setOpen(true)} aria-label='menu' > - - Background Geolocation Console - + Background Geolocation Console - + {children} - ; + +); export default HeaderView; diff --git a/src/client/components/ListView.js b/src/client/components/ListView.js index 0769925..a7d58f9 100644 --- a/src/client/components/ListView.js +++ b/src/client/components/ListView.js @@ -5,11 +5,16 @@ import { connect } from 'react-redux'; import format from 'date-fns/format'; import { List, AutoSizer } from 'react-virtualized'; import classNames from 'classnames'; -import { type Location, setSelectedLocation } from '~/reducer/dashboard'; -import { type GlobalState } from '~/reducer/state'; import { createSelector } from 'reselect'; -import { changeTabBus, type ChangeTabPayload, scrollToRowBus, type ScrollToRowPayload } from '~/globalBus'; -import Styles from '~/assets/styles/app.css'; + +import { type Location, setSelectedLocation } from 'reducer/dashboard'; +import { type GlobalState } from 'reducer/state'; +import { + changeTabBus, + scrollToRowBus, + type ScrollToRowPayload, +} from 'globalBus'; +import Styles from 'assets/styles/app.css'; type LocationRow = {| accuracy: number, @@ -19,7 +24,7 @@ type LocationRow = {| company_id: number, coordinate: string, created_at: string, - device_id: string, + device_id: number, event: string, is_moving: string, odometer: number, @@ -29,8 +34,8 @@ type LocationRow = {| |}; type StateProps = {| locations: LocationRow[], - selectedLocationId: string, - isActiveTab: boolean, + selectedLocationId: string, + isActiveTab: boolean, |}; type DispatchProps = {| @@ -43,25 +48,27 @@ const getRowData = (location: Location): LocationRow => { let event = location.event || ''; switch (location.event) { case 'geofence': - event = location.event + ': ' + ( + event = `${location.event + }: ${ location.geofence - ? location.geofence.action + ' ' + location.geofence.identifier - : 'empty' - ); + ? `${location.geofence.action} ${location.geofence.identifier}` + : 'empty'}`; break; + default: } return { uuid: location.uuid, device_id: +location.device_id, company_id: location.company_id, - coordinate: location.latitude.toFixed(6) + ', ' + location.longitude.toFixed(6), + coordinate: + `${location.latitude.toFixed(6)}, ${location.longitude.toFixed(6)}`, recorded_at: format(new Date(location.recorded_at), 'MM-dd HH:mm:ss:SSS'), created_at: format(new Date(location.created_at), 'MM-dd HH:mm:ss:SSS'), is_moving: location.is_moving ? 'true' : 'false', accuracy: location.accuracy, speed: location.speed, odometer: location.odometer, - event: event, + event, activity: `${location.activity_type} (${location.activity_confidence}%)`, battery_level: `${(location.battery_level * 100).toFixed(0)}%`, battery_is_charging: location.battery_is_charging, @@ -69,33 +76,28 @@ const getRowData = (location: Location): LocationRow => { }; class ListView extends React.PureComponent { - props: Props; list: any; + postponedScrollToRowPayload: ?ScrollToRowPayload = null; - componentDidMount () { + componentDidMount() { scrollToRowBus.subscribe(this.scrollToRow); changeTabBus.subscribe(this.changeTab); } - componentWillUnmount () { + componentWillUnmount() { scrollToRowBus.unsubscribe(this.scrollToRow); changeTabBus.unsubscribe(this.changeTab); } - changeTab = (payload: ChangeTabPayload) => { - if (this.props.isActiveTab) { + changeTab = () => { + const { isActiveTab } = this.props; + + if (isActiveTab) { setTimeout(() => this.scrollToRowIfPostponed(), 1); } }; - scrollToRowIfPostponed () { - if (this.postponedScrollToRowPayload) { - this.scrollToRow(this.postponedScrollToRowPayload); - this.postponedScrollToRowPayload = null; - } - } - // scrolling to the specified location // if the tab is not active - postpone till tab becomes active scrollToRow = ({ locationId }: ScrollToRowPayload) => { @@ -110,76 +112,78 @@ class ListView extends React.PureComponent { } }; - rowRenderer = ({ index, isScrolling, isVisible, key, parent, style }: any) => { - const item = this.props.locations[index]; + rowRenderer = ({ + index, + key, + style, + }: any) => { + const { + onRowSelect, locations, selectedLocationId, + } = this.props; + const item = locations[index]; return (
this.props.onRowSelect(item.uuid)} + onClick={() => onRowSelect(item.uuid)} > - - {item.uuid} - + {item.uuid} - - {item.recorded_at} - + {item.recorded_at} - - {item.created_at} - + {item.created_at} - - {item.coordinate} - + {item.coordinate} - - {item.accuracy} - + {item.accuracy} - - {item.speed} - + {item.speed} - - {item.odometer} - + {item.odometer} - - {item.event} - + {item.event} - - {item.is_moving} - + {item.is_moving} - - {item.activity} - + {item.activity} - - - {item.battery_level} - + + {item.battery_level}
); }; - render () { + + scrollToRowIfPostponed() { + if (this.postponedScrollToRowPayload) { + this.scrollToRow(this.postponedScrollToRowPayload); + this.postponedScrollToRowPayload = null; + } + } + + render() { + const { locations } = this.props; setTimeout(() => this.list && this.list.forceUpdateGrid(), 1); return (
@@ -198,17 +202,18 @@ class ListView extends React.PureComponent {
- {({ width, height }: { width: number, height: number }) => + {({ height }: { width: number, height: number }) => ( | null) => (this.list = this.list || list)} width={1260} height={height} - rowCount={this.props.locations.length} + rowCount={locations.length} rowHeight={48} rowRenderer={this.rowRenderer} - />} + /> + )}
@@ -221,16 +226,25 @@ type LocationArgs = { currentLocation: ?Location, locations: Location[], }; -const getLocationsSource = function ({ locations, currentLocation, isWatching }: LocationArgs) { +const getLocationsSource = ({ + locations, + currentLocation, + isWatching, +}: LocationArgs) => { if (isWatching) { return currentLocation ? [currentLocation] : []; - } else { - return locations; } + return locations; }; -const getLocations = function ({ locations, currentLocation, isWatching }: LocationArgs) { - const source = getLocationsSource({ locations, currentLocation, isWatching }); +const getLocations = ({ + locations, + currentLocation, + isWatching, +}: LocationArgs) => { + const source = getLocationsSource({ + locations, currentLocation, isWatching, + }); return source.map(getRowData); }; @@ -242,19 +256,19 @@ const getLocationsSelector = createSelector( isWatching: state.dashboard.isWatching, }), ], - ({ locations, currentLocation, isWatching }: LocationArgs) => getLocations({ locations, currentLocation, isWatching }) + ({ + locations, currentLocation, isWatching, + }: LocationArgs) => getLocations({ + locations, currentLocation, isWatching, + }), ); -const mapStateToProps = function (state: GlobalState): StateProps { - return { - locations: getLocationsSelector(state), - selectedLocationId: state.dashboard.selectedLocationId, - isActiveTab: state.dashboard.activeTab === 'list', - }; -}; +const mapStateToProps = (state: GlobalState): StateProps => ({ + locations: getLocationsSelector(state), + selectedLocationId: state.dashboard.selectedLocationId, + isActiveTab: state.dashboard.activeTab === 'list', +}); -const mapDispatchToProps: DispatchProps = { - onRowSelect: setSelectedLocation, -}; +const mapDispatchToProps: DispatchProps = { onRowSelect: setSelectedLocation }; export default connect(mapStateToProps, mapDispatchToProps)(ListView); diff --git a/src/client/components/LoadingIndicator.js b/src/client/components/LoadingIndicator.js index 21b2b65..cbb3771 100644 --- a/src/client/components/LoadingIndicator.js +++ b/src/client/components/LoadingIndicator.js @@ -1,11 +1,10 @@ // @flow import React from 'react'; import { connect } from 'react-redux'; -import { - Fade, - LinearProgress, -} from '@material-ui/core'; -import { type GlobalState } from '~/reducer/state'; +import Fade from '@material-ui/core/Fade'; +import LinearProgress from '@material-ui/core/LinearProgress'; + +import { type GlobalState } from 'reducer/state'; const style = { height: 10, @@ -19,19 +18,17 @@ const style = { type Props = {| isLoading: boolean, |}; -const LoadingIndicator = ({ isLoading }: Props) => (
- - - -
); +const LoadingIndicator = ({ isLoading }: Props) => ( +
+ + + +
+); -const mapStateToProps = (state: GlobalState) => ({ - isLoading: state.dashboard.isLoading, -}); +const mapStateToProps = (state: GlobalState) => ({ isLoading: state.dashboard.isLoading }); export default connect(mapStateToProps)(LoadingIndicator); diff --git a/src/client/components/LocationView.js b/src/client/components/LocationView.js index 0d96793..c009445 100644 --- a/src/client/components/LocationView.js +++ b/src/client/components/LocationView.js @@ -2,21 +2,17 @@ import React from 'react'; import { createSelector } from 'reselect'; import find from 'lodash/find'; -import { - AppBar, - IconButton, - Toolbar, - Typography, - useTheme, -} from '@material-ui/core'; -import { - ChevronLeft as ChevronLeftIcon, - ChevronRight as ChevronRightIcon, -} from '@material-ui/icons'; - +import AppBar from '@material-ui/core/AppBar'; +import IconButton from '@material-ui/core/IconButton'; +import Toolbar from '@material-ui/core/Toolbar'; +import Typography from '@material-ui/core/Typography'; +import { useTheme } from '@material-ui/core/styles'; +import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'; +import ChevronRightIcon from '@material-ui/icons/ChevronRight'; import { connect } from 'react-redux'; -import { type Location, unselectLocation } from '~/reducer/dashboard'; -import { type GlobalState } from '~/reducer/state'; + +import { type Location, unselectLocation } from 'reducer/dashboard'; +import { type GlobalState } from 'reducer/state'; type StateProps = {| location: ?Location, @@ -30,32 +26,39 @@ type Props = {| ...DispatchProps, classes: {| appBar: string, - appBarShift: string, - appBarWithLocationShift: string, - appBarBothShift: string, - menuButton: string, - hide: string, - locationContainer: string, + appBarShift: string, + appBarWithLocationShift: string, + appBarBothShift: string, + menuButton: string, + hide: string, + locationContainer: string, |}, |}; -const LocationView = ({ location, onClose, classes }: Props) => (location && ( +const LocationView = ({ + location, onClose, classes, +}: Props) => (location && (
- - Location - + Location - {useTheme().direction === 'ltr' ? : } + {useTheme().direction === 'ltr' ? ( + + ) : ( + + )}
-
{JSON.stringify(location, null, 2)}
+
+        {JSON.stringify(location, null, 2)}
+      
-)) || ''; +)) || + ''; type LocationArgs = { isWatching: boolean, @@ -73,17 +76,15 @@ export const getLocation = createSelector( selectedLocationId: state.dashboard.selectedLocationId, }), ], - ({ isWatching, currentLocation, locations, selectedLocationId }: LocationArgs) => - isWatching - ? currentLocation - : find(locations, { uuid: selectedLocationId }) + ({ + isWatching, + currentLocation, + locations, + selectedLocationId, + }: LocationArgs) => (isWatching ? currentLocation : find(locations, { uuid: selectedLocationId })), ); -const mapStateToProps = (state: GlobalState): StateProps => ({ - location: getLocation(state), -}); -const mapDispatchToProps: DispatchProps = { - onClose: unselectLocation, -}; +const mapStateToProps = (state: GlobalState): StateProps => ({ location: getLocation(state) }); +const mapDispatchToProps: DispatchProps = { onClose: unselectLocation }; export default connect(mapStateToProps, mapDispatchToProps)(LocationView); diff --git a/src/client/components/MapView.js b/src/client/components/MapView.js index c7d213d..bef44cd 100644 --- a/src/client/components/MapView.js +++ b/src/client/components/MapView.js @@ -1,17 +1,21 @@ +/* eslint-disable no-console */ // @flow /* eslint-disable camelcase */ import React, { Component } from 'react'; import { createSelector } from 'reselect'; - +import GoogleMap from 'google-map-react'; import { connect } from 'react-redux'; -import { type Location, type Marker, clickMarker } from '~/reducer/dashboard'; -import { type GlobalState } from '~/reducer/state'; -import GoogleMap from 'google-map-react'; +import { + type Location, type Marker, clickMarker, +} from 'reducer/dashboard'; +import { type GlobalState } from 'reducer/state'; +import { + changeTabBus, fitBoundsBus, type FitBoundsPayload, +} from 'globalBus'; -import { COLORS, MAX_POINTS } from '~/constants'; -import { changeTabBus, type ChangeTabPayload, fitBoundsBus, type FitBoundsPayload } from '~/globalBus'; +import { COLORS, MAX_POINTS } from '../constants'; import MarkerClusterer from './MarkerClusterer'; @@ -43,11 +47,10 @@ type Props = {| type MapState = {| center: {| lat: number, lng: number |}, - zoom: number, + zoom?: number, |}; class MapView extends Component { - props: Props; previousLocations: Location[] = []; motionChangePolylines: any = []; selectedMarker: any = null; @@ -58,10 +61,6 @@ class MapView extends Component { polyline: any = null; currentLocationMarker: any = null; locationAccuracyCircle: any = null; - state: MapState = { - center: { lat: -25.363882, lng: 131.044922 }, - zoom: 18, - }; updateFlags = { needsMarkersRedraw: true, needsTestMarkersRedraw: true, @@ -71,6 +70,15 @@ class MapView extends Component { }; postponedFitBoundsPayload: ?FitBoundsPayload = null; + constructor(props: Props, context: any) { + super(props, context); + + this.state = { + center: { lat: -25.363882, lng: 131.044922 }, + // zoom: 18, + }; + } + componentDidMount () { fitBoundsBus.subscribe(this.fitBounds); changeTabBus.subscribe(this.changeTab); @@ -81,34 +89,29 @@ class MapView extends Component { changeTabBus.unsubscribe(this.changeTab); } - changeTab = (payload: ChangeTabPayload) => { - if (this.props.isActiveTab) { + changeTab = () => { + const { isActiveTab } = this.props; + if (isActiveTab) { setTimeout(() => this.fitBoundsIfPostponed(), 1); } }; - fitBoundsIfPostponed () { - if (this.postponedFitBoundsPayload) { - this.fitBounds(this.postponedFitBoundsPayload); - this.postponedFitBoundsPayload = null; - } - } - // Fit Bounds, postpone if gmap is not ready, also postpone if tab is not active fitBounds = (payload: FitBoundsPayload) => { - if (!this.props.isActiveTab) { + const { isActiveTab, locations } = this.props; + if (!isActiveTab) { this.postponedFitBoundsPayload = payload; return; } if (this.gmap) { - if (this.props.locations.length > 1) { + if (locations.length > 1) { const bounds = new google.maps.LatLngBounds(); - this.props.locations.forEach(function (location: Location) { + locations.forEach((location: Location) => { bounds.extend(new google.maps.LatLng(location.latitude, location.longitude)); }); this.gmap.fitBounds(bounds); - } else if (this.props.locations.length === 1) { - const location = this.props.locations[0]; + } else if (locations.length === 1) { + const [location] = locations; this.gmap.setCenter(new google.maps.LatLng(location.latitude, location.longitude)); } } else { @@ -116,7 +119,7 @@ class MapView extends Component { } }; - onBoundChange = (e: any) => { + onBoundChange = () => { console.time('onBoundChange'); const bound = this.gmap.getBounds(); this.markers @@ -137,7 +140,7 @@ class MapView extends Component { onMapLoaded = (event: any) => { this.gmap = event.map; // Route polyline - let seq = { + const seq = { repeat: '50px', icon: { path: google.maps.SymbolPath.FORWARD_OPEN_ARROW, @@ -194,6 +197,70 @@ class MapView extends Component { cluster.remove(); }; + + buildLocationIcon = (location: Location, options: any = {}) => { + let anchor; + let fillColor = COLORS.polyline_color; + let scale = options.scale || 2; + let path = google.maps.SymbolPath.FORWARD_CLOSED_ARROW; + + if (location.geofence) { + path = google.maps.SymbolPath.FORWARD_CLOSED_ARROW; + anchor = new google.maps.Point(0, 2.6); + scale = 3; + switch (location.geofence.action) { + case 'ENTER': + fillColor = COLORS.green; + break; + case 'EXIT': + fillColor = COLORS.red; + break; + case 'DWELL': + fillColor = COLORS.gold; + break; + default: + } + } + let fillOpacity = 1; + + if (location.event === 'motionchange') { + if (!location.is_moving) { + anchor = undefined; + path = google.maps.SymbolPath.CIRCLE; + scale = 10; + fillOpacity = 0.7; + fillColor = COLORS.red; + } else { + path = google.maps.SymbolPath.FORWARD_OPEN_ARROW; + fillColor = COLORS.green; + scale = 3; + fillOpacity = 1; + } + } + if (options.selected) { + scale *= 2; + } + + return { + path, + rotation: location.heading, + scale, + anchor, + fillColor: options.fillColor || fillColor, + fillOpacity: options.fillOpacity || fillOpacity, + strokeColor: options.strokeColor || COLORS.black, + strokeWeight: options.strokeWeight || 1, + strokeOpacity: options.strokeOpacity || 1, + }; + } + + fitBoundsIfPostponed () { + if (this.postponedFitBoundsPayload) { + this.fitBounds(this.postponedFitBoundsPayload); + this.postponedFitBoundsPayload = null; + } + } + cleanClustering () { !!this.markerCluster && this.markerCluster.clearMarkers(); } @@ -219,17 +286,17 @@ class MapView extends Component { minimumClusterSize: 7, gridSize: 33, imagePath: '/images/m', - } + }, ); google.maps.event.addListener(this.markerCluster, 'click', this.onClusterClick); console.timeEnd('clustering'); - }; + } // ensures that selected location is properly displayed // previous marker is set to default icon, new marker or nothing is set to // selected icon updateSelectedLocation () { - const location = this.props.selectedLocation; + const { selectedLocation: location } = this.props; if (this.selectedMarker) { this.selectedMarker.setIcon(this.buildLocationIcon(this.selectedMarker.location)); this.selectedMarker.setZIndex(1); @@ -238,13 +305,9 @@ class MapView extends Component { this.selectedMarker = null; return; } - let marker = this.markers.find((marker: any) => { - return marker.location.uuid === location.uuid; - }); + let marker = this.markers.find((x: any) => x.location.uuid === location.uuid); if (!marker) { - marker = this.geofenceHitMarkers.find((marker: any) => { - return marker.location && marker.location.uuid === location.uuid; - }); + marker = this.geofenceHitMarkers.find((x: any) => x.location && marker.location.uuid === location.uuid); } if (marker) { this.selectedMarker = marker; @@ -255,155 +318,14 @@ class MapView extends Component { strokeColor: COLORS.red, strokeWeight: 2, selected: true, - }) + }), ); } } - renderMarkers () { - console.time('renderMarkers'); - const { - currentLocation, - isWatching, - locations, - showGeofenceHits, - showMarkers, - showPolyline, - testMarkers, - } = this.props; - - // if locations have not changed - do not clear markers - // just update current location, selected location and handle visibility of markers - if (this.updateFlags.needsTestMarkersRedraw && testMarkers.length) { - this.renderTestMarkers(testMarkers); - } - if (this.updateFlags.needsMarkersRedraw) { - this.clearMarkers(); - this.cleanClustering(); - - const length = locations.length; - console.info('draw markers: ' + length); - - this.polyline.setMap(showPolyline ? this.gmap : null); - - let motionChangePosition = null; - let searchingForMotionChange = false; - - // Iterate in reverse order to create polyline points from oldest->latest. - // We DO NOT want this.props.locations.reverse()!!! - for (var n = length - 1; n > 0; n--) { - let location = locations[n]; - let latLng = new google.maps.LatLng(location.latitude, location.longitude); - if (location.geofence) { - this.buildGeofenceMarker(location, { - map: showGeofenceHits ? this.gmap : null, - }); - } else { - let marker = this.buildLocationMarker(location, { - map: showMarkers ? this.gmap : null, - }); - this.markers.push(marker); - } - this.polyline.getPath().push(latLng); - - if (location.event === 'motionchange') { - if (!location.is_moving) { - searchingForMotionChange = true; - motionChangePosition = latLng; - } else if (searchingForMotionChange) { - searchingForMotionChange = false; - this.motionChangePolylines.push(this.buildMotionChangePolyline(motionChangePosition, latLng)); - } - } - } - this.clustering(); - } else { - // keep existing markers - just update their visibility - console.time('renderMarkers: Visibility'); - if (this.updateFlags.needsShowMarkersUpdate) { - this.markers.forEach((marker: any) => { - marker.setMap(showMarkers ? this.gmap : null); - }); - } - if (this.updateFlags.needsShowPolylineUpdate) { - this.polyline.setMap(showPolyline ? this.gmap : null); - this.motionChangePolylines.forEach((polyline: any) => { - polyline.setMap(showPolyline ? this.gmap : null); - }); - } - if (this.updateFlags.needsShowGeofenceHitsUpdate) { - this.geofenceHitMarkers.forEach((marker: any) => { - marker.setMap(showGeofenceHits ? this.gmap : null); - }); - } - console.timeEnd('renderMarkers: Visibility'); - } - // handle current location - if (isWatching && currentLocation) { - console.time('renderMarkers: Current Location'); - let latLng = new google.maps.LatLng(currentLocation.latitude, currentLocation.longitude); - this.gmap.setCenter(latLng); - this.currentLocationMarker.setMap(this.gmap); - this.locationAccuracyCircle.setMap(this.gmap); - this.currentLocationMarker.setPosition(latLng); - this.locationAccuracyCircle.setCenter(latLng); - this.locationAccuracyCircle.setRadius(currentLocation.accuracy); - console.timeEnd('renderMarkers: Current Location'); - } else { - this.currentLocationMarker.setMap(null); - this.locationAccuracyCircle.setMap(null); - } - // draw selectedMarker - console.time('renderMarkers: Selected Location'); - this.updateSelectedLocation(); - console.timeEnd('renderMarkers: Selected Location'); - console.timeEnd('renderMarkers'); - } - - /** - * Render manually added test markers - { - type: 'location|geofence' - position: { - lat: Float, - lng: Float - }, - radius: Number (present only when type: "geofence") - } - */ - renderTestMarkers (testMarkers: any) { - // 37.33313411,-122.05283635 - for (let n = 0, len = testMarkers.length; n < len; n++) { - let record = testMarkers[n]; - if (record.type === 'location') { - // eslint-disable-next-line no-new - new google.maps.Marker({ - position: record.position, - map: this.gmap, - label: record.label, - }); - } else if (record.type === 'geofence') { - // eslint-disable-next-line no-new - new google.maps.Circle({ - zIndex: 2000, - fillOpacity: 0, - strokeColor: '#ff0000', - strokeWeight: 1, - strokeOpacity: 1, - radius: record.radius, - center: record.position, - map: this.gmap, - }); - } - } - // arbitrarily center on first marker. - let first = testMarkers[0]; - this.gmap.setCenter(first.position); - } - buildMotionChangePolyline (stationaryPosition: any, movingPosition: any) { const { showPolyline } = this.props; - let seq = { + const seq = { repeat: '25px', icon: { path: google.maps.SymbolPath.FORWARD_OPEN_ARROW, @@ -429,7 +351,7 @@ class MapView extends Component { } buildGeofenceMarker (location: Location, options: any) { - let geofence = location.geofence; + const { geofence } = location; let circle = this.geofenceMarkers[geofence.identifier]; if (!circle) { let center; @@ -450,14 +372,14 @@ class MapView extends Component { strokeColor: COLORS.black, strokeWeight: 1, strokeOpacity: 1, - radius: radius, - center: center, + radius, + center, map: options.map, }); this.geofenceMarkers[geofence.identifier] = circle; this.geofenceHitMarkers.push(circle); } - var color; + let color; if (geofence.action === 'ENTER') { color = COLORS.green; } else if (geofence.action === 'DWELL') { @@ -465,13 +387,13 @@ class MapView extends Component { } else { color = COLORS.red; } - let circleLatLng = circle.getCenter(); - let locationLatLng = new google.maps.LatLng(location.latitude, location.longitude); + const circleLatLng = circle.getCenter(); + const locationLatLng = new google.maps.LatLng(location.latitude, location.longitude); const heading = google.maps.geometry.spherical.computeHeading(circleLatLng, locationLatLng); - let circleEdgeLatLng = google.maps.geometry.spherical.computeOffset(circleLatLng, circle.getRadius(), heading); + const circleEdgeLatLng = google.maps.geometry.spherical.computeOffset(circleLatLng, circle.getRadius(), heading); - var geofenceEdgeMarker = new google.maps.Marker({ + const geofenceEdgeMarker = new google.maps.Marker({ zIndex: 2000, icon: { path: google.maps.SymbolPath.CIRCLE, @@ -487,7 +409,7 @@ class MapView extends Component { }); this.geofenceHitMarkers.push(geofenceEdgeMarker); - var locationMarker = this.buildLocationMarker(location, { + const locationMarker = this.buildLocationMarker(location, { showHeading: true, zIndex: 2000, map: options.map, @@ -495,7 +417,7 @@ class MapView extends Component { }); this.geofenceHitMarkers.push(locationMarker); - var polyline = new google.maps.Polyline({ + const polyline = new google.maps.Polyline({ map: options.map, zIndex: 2000, strokeColor: COLORS.black, @@ -507,14 +429,13 @@ class MapView extends Component { } // Build a bread-crumb location marker. - buildLocationMarker (location: Location, options: any) { + buildLocationMarker (location: Location, options: any = {}) { const { onSelectLocation } = this.props; - options = options || {}; - let zIndex = options.zIndex || 1; - let marker = new google.maps.Marker({ - zIndex: zIndex, + const zIndex = options.zIndex || 1; + const marker = new google.maps.Marker({ + zIndex, icon: this.buildLocationIcon(location, options), - location: location, + location, map: options.map, position: new google.maps.LatLng(location.latitude, location.longitude), }); @@ -523,62 +444,6 @@ class MapView extends Component { return marker; } - buildLocationIcon (location: Location, options: any) { - options = options || {}; - let anchor; - let fillColor = COLORS.polyline_color; - let scale = options.scale || 2; - let path = google.maps.SymbolPath.FORWARD_CLOSED_ARROW; - - if (location.geofence) { - path = google.maps.SymbolPath.FORWARD_CLOSED_ARROW; - anchor = new google.maps.Point(0, 2.6); - scale = 3; - switch (location.geofence.action) { - case 'ENTER': - fillColor = COLORS.green; - break; - case 'EXIT': - fillColor = COLORS.red; - break; - case 'DWELL': - fillColor = COLORS.gold; - break; - } - } - let fillOpacity = 1; - - if (location.event === 'motionchange') { - if (!location.is_moving) { - anchor = undefined; - path = google.maps.SymbolPath.CIRCLE; - scale = 10; - fillOpacity = 0.7; - fillColor = COLORS.red; - } else { - path = google.maps.SymbolPath.FORWARD_OPEN_ARROW; - fillColor = COLORS.green; - scale = 3; - fillOpacity = 1; - } - } - if (options.selected) { - scale *= 2; - } - - return { - path: path, - rotation: location.heading, - scale: scale, - anchor: anchor, - fillColor: options.fillColor || fillColor, - fillOpacity: options.fillOpacity || fillOpacity, - strokeColor: options.strokeColor || COLORS.black, - strokeWeight: options.strokeWeight || 1, - strokeOpacity: options.strokeOpacity || 1, - }; - } - clearMarkers () { this.markers.forEach((marker: Marker) => { google.maps.event.clearInstanceListeners(marker); @@ -600,17 +465,20 @@ class MapView extends Component { } UNSAFE_shouldComponentUpdate (nestState: StateProps, nextProps: Props) { + const { + locations, testMarkers, showMarkers, enableClustering, showPolyline, showGeofenceHits, + } = this.props; // If the map was rendered - decide how we can only partially update markers // to significantly speed up the update // const previous = this.updateFlags; if (this.gmap) { this.updateFlags = { - needsMarkersRedraw: nextProps.locations !== this.props.locations, - needsTestMarkersRedraw: nextProps.testMarkers !== this.props.testMarkers, - needsShowMarkersUpdate: nextProps.showMarkers !== this.props.showMarkers || - nextProps.enableClustering !== this.props.enableClustering, - needsShowPolylineUpdate: nextProps.showPolyline !== this.props.showPolyline, - needsShowGeofenceHitsUpdate: nextProps.showGeofenceHits !== this.props.showGeofenceHits, + needsMarkersRedraw: nextProps.locations !== locations, + needsTestMarkersRedraw: nextProps.testMarkers !== testMarkers, + needsShowMarkersUpdate: nextProps.showMarkers !== showMarkers || + nextProps.enableClustering !== enableClustering, + needsShowPolylineUpdate: nextProps.showPolyline !== showPolyline, + needsShowGeofenceHitsUpdate: nextProps.showGeofenceHits !== showGeofenceHits, }; const result = Object.keys(this.updateFlags) .find((x: string) => !!this.updateFlags[x]); @@ -619,26 +487,167 @@ class MapView extends Component { return false; } + + renderMarkers () { + console.time('renderMarkers'); + const { + currentLocation, + isWatching, + locations, + showGeofenceHits, + showMarkers, + showPolyline, + testMarkers, + } = this.props; + + // if locations have not changed - do not clear markers + // just update current location, selected location and handle visibility of markers + if (this.updateFlags.needsTestMarkersRedraw && testMarkers.length) { + this.renderTestMarkers(testMarkers); + } + if (this.updateFlags.needsMarkersRedraw) { + this.clearMarkers(); + this.cleanClustering(); + + const { length } = locations; + console.info(`draw markers: ${length}`); + + this.polyline.setMap(showPolyline ? this.gmap : null); + + let motionChangePosition = null; + let searchingForMotionChange = false; + + // Iterate in reverse order to create polyline points from oldest->latest. + // We DO NOT want this.props.locations.reverse()!!! + for (let n = length - 1; n > 0; n--) { + const location = locations[n]; + const latLng = new google.maps.LatLng(location.latitude, location.longitude); + if (location.geofence) { + this.buildGeofenceMarker(location, { map: showGeofenceHits ? this.gmap : null }); + } else { + const marker = this.buildLocationMarker(location, { map: showMarkers ? this.gmap : null }); + this.markers.push(marker); + } + this.polyline.getPath().push(latLng); + + if (location.event === 'motionchange') { + if (!location.is_moving) { + searchingForMotionChange = true; + motionChangePosition = latLng; + } else if (searchingForMotionChange) { + searchingForMotionChange = false; + this.motionChangePolylines.push(this.buildMotionChangePolyline(motionChangePosition, latLng)); + } + } + } + this.clustering(); + } else { + // keep existing markers - just update their visibility + console.time('renderMarkers: Visibility'); + if (this.updateFlags.needsShowMarkersUpdate) { + this.markers.forEach((marker: any) => { + marker.setMap(showMarkers ? this.gmap : null); + }); + } + if (this.updateFlags.needsShowPolylineUpdate) { + this.polyline.setMap(showPolyline ? this.gmap : null); + this.motionChangePolylines.forEach((polyline: any) => { + polyline.setMap(showPolyline ? this.gmap : null); + }); + } + if (this.updateFlags.needsShowGeofenceHitsUpdate) { + this.geofenceHitMarkers.forEach((marker: any) => { + marker.setMap(showGeofenceHits ? this.gmap : null); + }); + } + console.timeEnd('renderMarkers: Visibility'); + } + // handle current location + if (isWatching && currentLocation) { + console.time('renderMarkers: Current Location'); + const latLng = new google.maps.LatLng(currentLocation.latitude, currentLocation.longitude); + this.gmap.setCenter(latLng); + this.currentLocationMarker.setMap(this.gmap); + this.locationAccuracyCircle.setMap(this.gmap); + this.currentLocationMarker.setPosition(latLng); + this.locationAccuracyCircle.setCenter(latLng); + this.locationAccuracyCircle.setRadius(currentLocation.accuracy); + console.timeEnd('renderMarkers: Current Location'); + } else { + this.currentLocationMarker.setMap(null); + this.locationAccuracyCircle.setMap(null); + } + // draw selectedMarker + console.time('renderMarkers: Selected Location'); + this.updateSelectedLocation(); + console.timeEnd('renderMarkers: Selected Location'); + console.timeEnd('renderMarkers'); + } + + /** + * Render manually added test markers + { + type: 'location|geofence' + position: { + lat: Float, + lng: Float + }, + radius: Number (present only when type: "geofence") + } + */ + renderTestMarkers (testMarkers: any) { + // 37.33313411,-122.05283635 + for (let n = 0, len = testMarkers.length; n < len; n++) { + const record = testMarkers[n]; + if (record.type === 'location') { + // eslint-disable-next-line no-new + new google.maps.Marker({ + position: record.position, + map: this.gmap, + label: record.label, + }); + } else if (record.type === 'geofence') { + // eslint-disable-next-line no-new + new google.maps.Circle({ + zIndex: 2000, + fillOpacity: 0, + strokeColor: '#ff0000', + strokeWeight: 1, + strokeOpacity: 1, + radius: record.radius, + center: record.position, + map: this.gmap, + }); + } + } + // arbitrarily center on first marker. + const first = testMarkers[0]; + this.gmap.setCenter(first.position); + } + render () { + const { isActiveTab } = this.props; + const { center } = this.state; + if (this.gmap) { this.renderMarkers(); } // protects us from rendering the google map while the tab is not active // because of display: none this leads to improper size calculations, so // later FitToBounds or setMapCenter do not work - if (!this.props.isActiveTab && !this.gmap) { + if (!isActiveTab && !this.gmap) { return null; } return ( @@ -661,11 +670,7 @@ const selectedLocationSelector = createSelector( locations.find((x: Location) => x.uuid === selectedLocationId), ); -const nthItem = function (n: number) { - return function (candidate: Location, index: number) { - return candidate.event || index % n === 0; - }; -}; +const nthItem = (n: number) => (candidate: Location, index: number) => candidate.event || index % n === 0; const filteredLocationSelector = createSelector( [ (state: GlobalState) => ({ @@ -673,11 +678,12 @@ const filteredLocationSelector = createSelector( length: state.dashboard.locations.length, }), ], - ({ locations, length }: { locations: Location[], length: number }) => + ({ locations, length }: { locations: Location[], length: number }) => ( length < MAX_POINTS ? locations : locations.filter(nthItem(Math.floor(length / MAX_POINTS) + 1)) + ), ); -const mapStateToProps = function (state: GlobalState) { +const mapStateToProps = (state: GlobalState) => { const { dashboard } = state; return { locations: filteredLocationSelector(state), @@ -693,8 +699,6 @@ const mapStateToProps = function (state: GlobalState) { }; }; -const mapDispatchToProps = { - onSelectLocation: clickMarker, -}; +const mapDispatchToProps = { onSelectLocation: clickMarker }; export default connect(mapStateToProps, mapDispatchToProps)(MapView); diff --git a/src/client/components/MarkerClusterer.js b/src/client/components/MarkerClusterer.js index 1234c30..ef82979 100644 --- a/src/client/components/MarkerClusterer.js +++ b/src/client/components/MarkerClusterer.js @@ -1,5 +1,4 @@ -/* eslint-disable max-len */ -/* eslint-disable camelcase */ +/* eslint-disable */ /** * @name MarkerClustererPlus for Google Maps V3 * @author Gary Little diff --git a/src/client/components/RemoveAnimationProvider.js b/src/client/components/RemoveAnimationProvider.js index d894772..1cebb69 100644 --- a/src/client/components/RemoveAnimationProvider.js +++ b/src/client/components/RemoveAnimationProvider.js @@ -1,21 +1,15 @@ // @flow import React from 'react'; import { ThemeProvider } from '@material-ui/styles'; -import { createMuiTheme } from '@material-ui/core'; +import { createMuiTheme } from '@material-ui/core/styles'; -export const removeAnimationTheme = createMuiTheme({ - transitions: { - create: () => 'none', - }, -}); +export const removeAnimationTheme = createMuiTheme({ transitions: { create: () => 'none' } }); type Props = {| children: any, |}; const RemoveAnimationProvider = ({ children }: Props) => ( - - {children} - + {children} ); export default RemoveAnimationProvider; diff --git a/src/client/components/TabPanel.js b/src/client/components/TabPanel.js index 6bd9f80..13f6fb9 100644 --- a/src/client/components/TabPanel.js +++ b/src/client/components/TabPanel.js @@ -9,7 +9,9 @@ type Props = {| className: any, |}; -const TabPanel = ({ children, value, index, className }: Props) => ( +const TabPanel = ({ + children, value, index, className, +}: Props) => ( +const TooManyPointsWarning = ({ + isVisible, maxPoints, pointsCount, +}: Props) => (
fontWeight: 'bold', }} > - Map contains {pointsCount} points! Only {maxPoints} are shown for performance reason! -
; + Map contains + {' '} + {pointsCount} + {' '} + points! Only + {' '} + {maxPoints} + {' '} + are shown for + performance reason! +
+); const mapStateToProps = (state: GlobalState) => ({ - isVisible: state.dashboard.locations.length > MAX_POINTS && state.dashboard.activeTab === 'map', + isVisible: + state.dashboard.locations.length > MAX_POINTS && + state.dashboard.activeTab === 'map', maxPoints: MAX_POINTS, pointsCount: state.dashboard.locations.length, }); diff --git a/src/client/components/Viewport.js b/src/client/components/Viewport.js index 0192777..1884df3 100644 --- a/src/client/components/Viewport.js +++ b/src/client/components/Viewport.js @@ -1,12 +1,18 @@ // @flow import React, { useState } from 'react'; import clsx from 'classnames'; +import CssBaseline from '@material-ui/core/CssBaseline'; +import Drawer from '@material-ui/core/Drawer'; +import Tab from '@material-ui/core/Tab'; +import Tabs from '@material-ui/core/Tabs'; +import { connect } from 'react-redux'; + +import type { GlobalState } from 'reducer/state'; import { - CssBaseline, - Drawer, - Tab, - Tabs, -} from '@material-ui/core'; + changeActiveTab, + type Tab as TabType, + type Location, +} from 'reducer/dashboard'; import HeaderView from './HeaderView'; import FilterView from './FilterView'; @@ -18,14 +24,11 @@ import LoadingIndicator from './LoadingIndicator'; import WatchModeWarning from './WatchModeWarning'; import useStyles from './ViewportStyle'; import TooManyPointsWarning from './TooManyPointsWarning'; -import { connect } from 'react-redux'; -import type { GlobalState } from '~/reducer/state'; -import { changeActiveTab, type Tab as TabType, type Location } from '~/reducer/dashboard'; type StateProps = {| isLocationSelected: boolean, - activeTabIndex: 0 | 1, - location: ?Location, + activeTabIndex: 0 | 1, + location: ?Location, |}; type DispatchProps = {| onChangeActiveTab: (tab: TabType) => any, @@ -35,7 +38,7 @@ type Props = {| ...StateProps, ...DispatchProps, |}; -const Viewport = ({ isLocationSelected, activeTabIndex, location }: Props) => { +const Viewport = ({ activeTabIndex, location }: Props) => { const [tabIndex, setTabIndex] = useState(activeTabIndex); const [open, setOpen] = React.useState(true); const classes = useStyles(); @@ -43,8 +46,17 @@ const Viewport = ({ isLocationSelected, activeTabIndex, location }: Props) => { return (
- - setTabIndex(index)}> + + setTabIndex(index)} + > @@ -54,34 +66,29 @@ const Viewport = ({ isLocationSelected, activeTabIndex, location }: Props) => { variant='persistent' anchor='left' open={open} - classes={{ - paper: classes.drawerPaper, - }} + classes={{ paper: classes.drawerPaper }} >
- + @@ -92,9 +99,7 @@ const Viewport = ({ isLocationSelected, activeTabIndex, location }: Props) => { variant='persistent' anchor='right' open={!!location} - classes={{ - paper: classes.drawerLocationPaper, - }} + classes={{ paper: classes.drawerLocationPaper }} > @@ -102,14 +107,10 @@ const Viewport = ({ isLocationSelected, activeTabIndex, location }: Props) => { ); }; -const mapStateToProps = function (state: GlobalState): StateProps { - return { - isLocationSelected: !!state.dashboard.selectedLocationId, - activeTabIndex: state.dashboard.activeTab === 'map' ? 0 : 1, - location: getLocation(state), - }; -}; -const mapDispatchToProps: DispatchProps = { - onChangeActiveTab: changeActiveTab, -}; +const mapStateToProps = (state: GlobalState): StateProps => ({ + isLocationSelected: !!state.dashboard.selectedLocationId, + activeTabIndex: state.dashboard.activeTab === 'map' ? 0 : 1, + location: getLocation(state), +}); +const mapDispatchToProps: DispatchProps = { onChangeActiveTab: changeActiveTab }; export default connect(mapStateToProps, mapDispatchToProps)(Viewport); diff --git a/src/client/components/ViewportStyle.js b/src/client/components/ViewportStyle.js index 5131ef7..c7b4ef4 100644 --- a/src/client/components/ViewportStyle.js +++ b/src/client/components/ViewportStyle.js @@ -56,15 +56,9 @@ const useStyles = makeStyles(theme => ({ right: 0, bottom: 0, }, - tabs: { - backgroundColor: theme.palette.primary.main, - }, - menuButton: { - marginRight: theme.spacing(2), - }, - hide: { - display: 'none', - }, + tabs: { backgroundColor: theme.palette.primary.main }, + menuButton: { marginRight: theme.spacing(2) }, + hide: { display: 'none' }, drawer: { width: drawerWidth, flexShrink: 0, @@ -73,12 +67,8 @@ const useStyles = makeStyles(theme => ({ width: locationDrawlerWidth, flexShrink: 0, }, - drawerPaper: { - width: drawerWidth, - }, - drawerLocationPaper: { - width: locationDrawlerWidth, - }, + drawerPaper: { width: drawerWidth }, + drawerLocationPaper: { width: locationDrawlerWidth }, drawerHeader: { display: 'flex', alignItems: 'center', @@ -86,12 +76,8 @@ const useStyles = makeStyles(theme => ({ ...theme.mixins.toolbar, justifyContent: 'space-between', }, - overflowAuto: { - overflow: 'auto', - }, - whiteBackground: { - backgroundColor: 'rgb(255, 255, 255)', - }, + overflowAuto: { overflow: 'auto' }, + whiteBackground: { backgroundColor: 'rgb(255, 255, 255)' }, content: { position: 'relative', flexGrow: 1, diff --git a/src/client/components/WatchModeWarning.js b/src/client/components/WatchModeWarning.js index a0c2470..b25f94f 100644 --- a/src/client/components/WatchModeWarning.js +++ b/src/client/components/WatchModeWarning.js @@ -1,12 +1,13 @@ // @flow import React from 'react'; import { connect } from 'react-redux'; -import { type GlobalState } from '~/reducer/state'; + +import { type GlobalState } from 'reducer/state'; type Props = {| isWatching: boolean, |}; -const WatchModeWarning = ({ isWatching }: Props) => +const WatchModeWarning = ({ isWatching }: Props) => (
}} > You are in the Watch mode. Only the latest location is being displayed here -
; +
+); -const mapStateToProps = (state: GlobalState) => ({ - isWatching: state.dashboard.isWatching, -}); +const mapStateToProps = (state: GlobalState) => ({ isWatching: state.dashboard.isWatching }); export default connect(mapStateToProps)(WatchModeWarning); diff --git a/src/client/components/WrappedViewport.js b/src/client/components/WrappedViewport.js index 1b075e9..3d5a918 100644 --- a/src/client/components/WrappedViewport.js +++ b/src/client/components/WrappedViewport.js @@ -1,8 +1,12 @@ import React from 'react'; -import Viewport from './Viewport'; -import { loadInitialData } from '~/reducer/dashboard'; + +import { loadInitialData } from 'reducer/dashboard'; + import store from '../store'; +import Viewport from './Viewport'; + + const WrappedViewport = ({ match }) => { store.dispatch(loadInitialData(match.params.token)); return ; diff --git a/src/client/constants.js b/src/client/constants.js index 29ddb33..3fa4d45 100644 --- a/src/client/constants.js +++ b/src/client/constants.js @@ -2,7 +2,6 @@ export const API_URI = '/api/site'; export const API_URL = window.location.origin + API_URI; -// Colors export const COLORS = { gold: '#fedd1e', white: '#fff', diff --git a/src/client/globalBus.js b/src/client/globalBus.js index 94c1714..49216fe 100644 --- a/src/client/globalBus.js +++ b/src/client/globalBus.js @@ -1,31 +1,36 @@ // @flow import emitter from 'event-emitter'; -import { type Tab } from '~/reducer/dashboard'; + +import { type Tab } from 'reducer/state'; export type FitBoundsPayload = {} & $Shape<{}>; -export const fitBoundsBus: Bus = makeBus(); // eslint-disable-line no-use-before-define + +// eslint-disable-next-line no-use-before-define +export const fitBoundsBus: Bus = makeBus(); export type ScrollToRowPayload = {| locationId: string |}; -export const scrollToRowBus: Bus = makeBus(); // eslint-disable-line no-use-before-define +// eslint-disable-next-line no-use-before-define +export const scrollToRowBus: Bus = makeBus(); export type ChangeTabPayload = {| tab: Tab |}; -export const changeTabBus: Bus = makeBus(); // eslint-disable-line no-use-before-define +// eslint-disable-next-line no-use-before-define +export const changeTabBus: Bus = makeBus(); type Bus = { subscribe: (handler: (payload: Payload) => any) => void, unsubscribe: (handler: (payload: Payload) => any) => void, emit: (payload: Payload) => void, }; -function makeBus (): Bus { +function makeBus(): Bus { const e = emitter(); return { - subscribe: function (handler: (payload: Payload) => any) { + subscribe(handler: (payload: Payload) => any) { e.on('event', handler); }, - unsubscribe: function (handler: (payload: Payload) => any) { + unsubscribe(handler: (payload: Payload) => any) { e.off('event', handler); }, - emit: function (payload: Payload) { + emit(payload: Payload) { e.emit('event', payload); }, }; diff --git a/src/client/main.js b/src/client/main.js index 175c67c..99695aa 100644 --- a/src/client/main.js +++ b/src/client/main.js @@ -1,55 +1,44 @@ import React from 'react'; -import ReactDOM from 'react-dom'; -import { AppContainer } from 'react-hot-loader'; -import { Provider } from 'react-redux'; - -import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; +import { render } from 'react-dom'; +import App from './client'; import store from './store'; -import WrappedViewport from './components/WrappedViewport'; // Detect users incorrectly hitting /locations/username instead of /username. // It seems people think because the plugin is POSTing -> /location/username that they must // view in browser at same url. This is incorrect. -const path = window.location.pathname; -const pathQuery = path.match(/^\/locations\/(.*)$/); +const { pathname } = window.location; +const pathQuery = pathname.match(/^\/locations\/(.*)$/); if (pathQuery) { // Redirect /locations/username -> /username - window.location.pathname = pathQuery[1]; + [, window.location.pathname] = pathQuery; } -const locationHash = (location.hash || '').substring(1); +const hash = (window.location.hash || '').substring(1); -if (locationHash) { - window.location = '/' + locationHash; +if (hash) { + window.location = `/${hash}`; } -const container = document.querySelector('#app-container'); - -const render = () => { - ReactDOM.render( - - - - - - - - - - , - container - ); -}; - -render(); - -if (module.hot) { - module.hot.accept('./components/Viewport', () => { - setImmediate(() => { - ReactDOM.unmountComponentAtNode(container); - render(); +document.addEventListener('DOMContentLoaded', () => { + const container = document.querySelector('#app-container'); + + if (module.hot) { + // eslint-disable-next-line import/no-extraneous-dependencies, global-require + const HotLoader = require('react-hot-loader').AppContainer; + render(, container); + module.hot.accept(['./components/Viewport', './client'], () => { + // eslint-disable-next-line import/no-extraneous-dependencies, global-require + const { default: NewApp } = require('./client'); + render( + + + , + container, + ); }); - }); -} + } else { + render(, container); + } +}); diff --git a/src/client/reducer/dashboard/index.js b/src/client/reducer/dashboard/index.js index 910dace..2c113f2 100644 --- a/src/client/reducer/dashboard/index.js +++ b/src/client/reducer/dashboard/index.js @@ -1,12 +1,19 @@ +/* eslint-disable no-console */ // @flow -import { API_URL } from '~/constants'; -import { type GlobalState } from '~/reducer/state'; -import cloneState from '~/utils/cloneState'; -import isEqual from 'lodash/isEqual'; import qs from 'querystring'; -import { fitBoundsBus, scrollToRowBus, changeTabBus } from '~/globalBus'; -import { setSettings, getSettings, getUrlSettings, setUrlSettings, type StoredSettings } from '~/storage'; -import GA from '~/utils/GA'; +import isEqual from 'lodash/isEqual'; + +import { + fitBoundsBus, scrollToRowBus, changeTabBus, +} from 'globalBus'; +import { + setSettings, getSettings, getUrlSettings, setUrlSettings, type StoredSettings, +} from 'storage'; +import GA from 'utils/GA'; +import { type GlobalState, type Tab } from 'reducer/state'; +import cloneState from 'utils/cloneState'; + +import { API_URL } from '../../constants'; export type Source = {| value: string, @@ -30,7 +37,6 @@ export type OrgToken = {| id: string, name: string, |}; -export type Tab = 'map' | 'list'; export type Location = {| accuracy: number, activity_confidence: number, @@ -236,53 +242,47 @@ type ThunkAction = (dispatch: Dispatch, getState: GetState) => Promise; export function setOrgTokens (orgTokens: OrgToken[]): SetOrgTokensAction { return { type: 'dashboard/SET_ORG_TOKENS', - orgTokens: orgTokens, + orgTokens, }; } export function setDevices (devices: Device[]): SetDevicesAction { return { type: 'dashboard/SET_DEVICES', - devices: devices, + devices, }; } export function setLocations (locations: Location[]): SetLocationsAction { return { type: 'dashboard/SET_LOCATIONS', - locations: locations, + locations, }; } export function setHasData (status: boolean): SetHasDataAction { return { type: 'dashboard/SET_HAS_DATA', - status: status, + status, }; } export function setIsLoading (status: boolean): SetIsLoadingAction { return { type: 'dashboard/SET_IS_LOADING', - status: status, + status, }; } export function autoselectOrInvalidateSelectedOrgToken (): AutoselectOrInvalidateSelectedOrgTokenAction { - return { - type: 'dashboard/AUTOSELECT_OR_INVALIDATE_SELECTED_ORG_TOKEN', - }; + return { type: 'dashboard/AUTOSELECT_OR_INVALIDATE_SELECTED_ORG_TOKEN' }; } export function autoselectOrInvalidateSelectedDevice (): AutoselectOrInvalidateSelectedDeviceAction { - return { - type: 'dashboard/AUTOSELECT_OR_INVALIDATE_SELECTED_DEVICE', - }; + return { type: 'dashboard/AUTOSELECT_OR_INVALIDATE_SELECTED_DEVICE' }; } export function invalidateSelectedLocation (): InvalidateSelectedLocationAction { - return { - type: 'dashboard/INVALIDATE_SELECTED_LOCATION', - }; + return { type: 'dashboard/INVALIDATE_SELECTED_LOCATION' }; } export function setShowMarkers (value: boolean): SetShowMarkersAction { @@ -329,7 +329,7 @@ export function setIsWatching (value: boolean): SetIsWatchingAction { export function setCurrentLocation (location: ?Location): SetCurrentLocationAction { return { type: 'dashboard/SET_CURRENT_LOCATION', - location: location, + location, }; } @@ -350,14 +350,14 @@ export function setEndDate (value: Date): SetEndDateAction { export function setDevice (deviceId: string): SetDeviceAction { return { type: 'dashboard/SET_DEVICE', - deviceId: deviceId, + deviceId, }; } export function setSelectedLocation (locationId: string): SetSelectedLocationAction { return { type: 'dashboard/SET_SELECTED_LOCATION', - locationId: locationId, + locationId, }; } @@ -371,14 +371,14 @@ export function unselectLocation (): SetSelectedLocationAction { export function applyExistingSettings (settings: StoredSettings): ApplyExistingSettinsAction { return { type: 'dashboard/APPLY_EXISTING_SETTINGS', - settings: settings, + settings, }; } export function setActiveTab (tab: Tab): SetActiveTabAction { return { type: 'dashboard/SET_ACTIVE_TAB', - tab: tab, + tab, }; } @@ -406,8 +406,108 @@ export function doAddTestMarker (value: Object): AddTestMarkerAction { // ------------------------------------ // Thunk Actions // ------------------------------------ -export function loadInitialData (id: string): ThunkAction { - return async function (dispatch: Dispatch, getState: GetState): Promise { + +export const loadOrgTokens = (): ThunkAction => async (dispatch: Dispatch, getState: GetState): Promise => { + const { dashboard: { orgTokenFromSearch } } = getState(); + const params = qs.stringify({ company_token: orgTokenFromSearch }); + try { + const response = await fetch(`${API_URL}/company_tokens?${params}`); + const records = await response.json(); + const orgTokens: OrgToken[] = records.map((x: { company_token: string }) => ({ + id: x.id, + name: x.company_token, + })); + return dispatch(setOrgTokens(orgTokens)); + } catch (e) { + console.error('loadOrgTokens', e); + return e; + } +}; + +export const loadDevices = (): ThunkAction => async (dispatch: Dispatch, getState: GetState): Promise => { + const { dashboard: { companyId, orgToken } } = getState(); + const params = qs.stringify({ + company_id: companyId, + company_token: orgToken, + }); + try { + const response = await fetch(`${API_URL}/devices?${params}`); + const records = await response.json(); + const devices: Device[] = records + .map((record: Object) => ({ + id: record.id, + name: `${record.device_id}(${record.framework})`, + })); + return dispatch(setDevices(devices)); + } catch (e) { + console.error('loadDevices', e); + return e; + } +}; + + +export const loadLocations = (): ThunkAction => async (dispatch: Dispatch, getState: GetState): Promise => { + const { + deviceId, orgToken, companyId, startDate, endDate, maxMarkers, + } = getState().dashboard; + GA.sendEvent('tracker', 'loadLocations', orgToken); + + const params = qs.stringify({ + company_id: companyId, + company_token: orgToken, + device_id: deviceId, + end_date: endDate.toISOString(), + limit: maxMarkers, + start_date: startDate.toISOString(), + }); + try { + const response = await fetch(`${API_URL}/locations?${params}`); + const records = await response.json(); + return dispatch(setLocations(records)); + } catch (e) { + console.error('loadLocations', e); + return e; + } +}; + +export const loadCurrentLocation = (): ThunkAction => async (dispatch: Dispatch, getState: GetState): Promise => { + const { + deviceId, companyId, company_token: orgToken, + } = getState().dashboard; + if (deviceId) { + const params = qs.stringify({ + device_id: deviceId, + company_id: companyId, + company_token: orgToken, + }); + try { + const response = await fetch(`${API_URL}/locations/latest?${params}`); + const currentLocation = await response.json(); + return await dispatch(setCurrentLocation(currentLocation)); + } catch (e) { + console.error('loadCurrentLocation', deviceId, e); + } + } + + return dispatch(setCurrentLocation(null)); +}; + +export const reload = + ({ loadUsers }: LoadParams = { loadUsers: true }): ThunkAction => async (dispatch: Dispatch): Promise => { + await dispatch(setIsLoading(true)); + loadUsers && await dispatch(loadOrgTokens()); + await dispatch(autoselectOrInvalidateSelectedOrgToken()); + await dispatch(loadDevices()); + await dispatch(autoselectOrInvalidateSelectedDevice()); + await dispatch(loadLocations()); + await dispatch(loadCurrentLocation()); + await dispatch(invalidateSelectedLocation()); + await dispatch(setIsLoading(false)); + fitBoundsBus.emit({}); + }; + +export const loadInitialData = + (id: string): ThunkAction => async (dispatch: Dispatch, getState: GetState): Promise => { const { dashboard: { hasData } } = getState(); if (hasData) { console.error('extra call after everything is set up!'); @@ -424,27 +524,11 @@ export function loadInitialData (id: string): ThunkAction { await dispatch(setHasData(true)); // set a timer as a side effect setTimeout(() => dispatch(reload()), 60 * 1000); - GA.sendEvent('tracker', 'load:' + id); + GA.sendEvent('tracker', `load:${id}`); }; -} - -export function reload ({ loadUsers }: LoadParams = { loadUsers: true }): ThunkAction { - return async function (dispatch: Dispatch, getState: GetState): Promise { - await dispatch(setIsLoading(true)); - loadUsers && await dispatch(loadOrgTokens()); - await dispatch(autoselectOrInvalidateSelectedOrgToken()); - await dispatch(loadDevices()); - await dispatch(autoselectOrInvalidateSelectedDevice()); - await dispatch(loadLocations()); - await dispatch(loadCurrentLocation()); - await dispatch(invalidateSelectedLocation()); - await dispatch(setIsLoading(false)); - fitBoundsBus.emit({}); - }; -} export function deleteActiveDevice (deleteOptions: ?DeleteOptions): ThunkAction { - return async function (dispatch: Dispatch, getState: GetState): Promise { + return async (dispatch: Dispatch, getState: GetState): Promise => { const { dashboard: { deviceId } } = getState(); if (!deviceId) { return; @@ -458,102 +542,15 @@ export function deleteActiveDevice (deleteOptions: ?DeleteOptions): ThunkAction try { await fetch(`${API_URL}/devices/${deviceId}${params}`, { method: 'delete' }); await dispatch(reload({ loadUsers: false })); - GA.sendEvent('tracker', 'delete device:' + deviceId); + GA.sendEvent('tracker', `delete device:${deviceId}`); } catch (e) { console.error('deleteActiveDevice', e); } }; } -export function loadOrgTokens (): ThunkAction { - return async function (dispatch: Dispatch, getState: GetState): Promise { - const { dashboard: { orgTokenFromSearch } } = getState(); - const params = qs.stringify({ - company_token: orgTokenFromSearch, - }); - try { - const response = await fetch(`${API_URL}/company_tokens?${params}`); - const records = await response.json(); - const orgTokens: OrgToken[] = records.map((x: { company_token: string }) => ({ - id: x.id, - name: x.company_token, - })); - return dispatch(setOrgTokens(orgTokens)); - } catch (e) { - console.error('loadOrgTokens', e); - } - }; -} - -export function loadDevices (): ThunkAction { - return async function (dispatch: Dispatch, getState: GetState): Promise { - const { dashboard: { companyId, orgToken } } = getState(); - const params = qs.stringify({ - company_id: companyId, - company_token: orgToken, - }); - try { - const response = await fetch(`${API_URL}/devices?${params}`); - const records = await response.json(); - const devices: Device[] = records - .map((record: Object) => ({ - id: record.id, - name: record.device_id + '(' + record.framework + ')', - })); - return dispatch(setDevices(devices)); - } catch (e) { - console.error('loadDevices', e); - } - }; -} - -export function loadLocations (): ThunkAction { - return async function (dispatch: Dispatch, getState: GetState): Promise { - const { deviceId, orgToken, companyId, startDate, endDate, maxMarkers } = getState().dashboard; - GA.sendEvent('tracker', 'loadLocations', orgToken); - - const params = qs.stringify({ - company_id: companyId, - company_token: orgToken, - device_id: deviceId, - end_date: endDate.toISOString(), - limit: maxMarkers, - start_date: startDate.toISOString(), - }); - try { - const response = await fetch(`${API_URL}/locations?${params}`); - const records = await response.json(); - return dispatch(setLocations(records)); - } catch (e) { - console.error('loadLocations', e); - } - }; -} - -export function loadCurrentLocation (): ThunkAction { - return async function (dispatch: Dispatch, getState: GetState): Promise { - const { deviceId, companyId, company_token: orgToken } = getState().dashboard; - if (deviceId) { - const params = qs.stringify({ - device_id: deviceId, - company_id: companyId, - company_token: orgToken, - }); - try { - const response = await fetch(`${API_URL}/locations/latest?${params}`); - const currentLocation = await response.json(); - return await dispatch(setCurrentLocation(currentLocation)); - } catch (e) { - console.error('loadCurrentLocation', deviceId, e); - } - } - - await dispatch(setCurrentLocation(null)); - }; -} - export function changeStartDate (value: Date) { - return async function (dispatch: Dispatch, getState: GetState): Promise { + return async (dispatch: Dispatch, getState: GetState): Promise => { await dispatch(setStartDate(value)); setSettings(getState().dashboard.orgTokenFromSearch, { startDate: value }); const { dashboard } = getState(); @@ -568,7 +565,7 @@ export function changeStartDate (value: Date) { }; } export function changeEndDate (value: Date) { - return async function (dispatch: Dispatch, getState: GetState): Promise { + return async (dispatch: Dispatch, getState: GetState): Promise => { await dispatch(setEndDate(value)); setSettings(getState().dashboard.orgTokenFromSearch, { endDate: value }); const { dashboard } = getState(); @@ -584,14 +581,14 @@ export function changeEndDate (value: Date) { } export function changeOrgToken (value: string) { - return async function (dispatch: Dispatch, getState: GetState): Promise { + return async (dispatch: Dispatch, getState: GetState): Promise => { await dispatch(setOrgToken(value)); setSettings(getState().dashboard.orgTokenFromSearch, { companyId: value }); await dispatch(reload({ loadUsers: false })); }; } export function changeDeviceId (value: string) { - return async function (dispatch: Dispatch, getState: GetState): Promise { + return async (dispatch: Dispatch, getState: GetState): Promise => { await dispatch(setDevice(value)); setSettings(getState().dashboard.orgTokenFromSearch, { deviceId: value }); const { dashboard } = getState(); @@ -607,56 +604,56 @@ export function changeDeviceId (value: string) { } export function changeIsWatching (value: boolean) { - return async function (dispatch: Dispatch, getState: GetState): Promise { + return async (dispatch: Dispatch, getState: GetState): Promise => { await dispatch(setIsWatching(value)); setSettings(getState().dashboard.orgTokenFromSearch, { isWatching: value }); }; } export function changeShowMarkers (value: boolean) { - return async function (dispatch: Dispatch, getState: GetState): Promise { + return async (dispatch: Dispatch, getState: GetState): Promise => { await dispatch(setShowMarkers(value)); setSettings(getState().dashboard.orgTokenFromSearch, { showMarkers: value }); }; } export function changeEnableClustering (value: boolean) { - return async function (dispatch: Dispatch, getState: GetState): Promise { + return async (dispatch: Dispatch, getState: GetState): Promise => { await dispatch(setEnableClustering(value)); setSettings(getState().dashboard.orgTokenFromSearch, { enableClustering: value }); }; } export function changeShowPolyline (value: boolean) { - return async function (dispatch: Dispatch, getState: GetState): Promise { + return async (dispatch: Dispatch, getState: GetState): Promise => { await dispatch(setShowPolyline(value)); setSettings(getState().dashboard.orgTokenFromSearch, { showPolyline: value }); }; } export function changeShowGeofenceHits (value: boolean) { - return async function (dispatch: Dispatch, getState: GetState): Promise { + return async (dispatch: Dispatch, getState: GetState): Promise => { await dispatch(setShowGeofenceHits(value)); setSettings(getState().dashboard.orgTokenFromSearch, { showGeofenceHits: value }); }; } export function changeMaxMarkers (value: number) { - return async function (dispatch: Dispatch, getState: GetState): Promise { + return async (dispatch: Dispatch, getState: GetState): Promise => { await dispatch(setShowMaxMarkers(value)); setSettings(getState().dashboard.orgTokenFromSearch, { maxMarkers: value }); }; } export function clickMarker (locationId: string) { - return async function (dispatch: Dispatch, getState: GetState): Promise { + return async (dispatch: Dispatch): Promise => { await dispatch(setSelectedLocation(locationId)); scrollToRowBus.emit({ locationId }); }; } export function changeActiveTab (tab: Tab) { - return async function (dispatch: Dispatch, getState: GetState): Promise { + return async (dispatch: Dispatch, getState: GetState): Promise => { await dispatch(setActiveTab(tab)); setSettings(getState().dashboard.orgTokenFromSearch, { activeTab: tab }); changeTabBus.emit({ tab }); @@ -664,7 +661,7 @@ export function changeActiveTab (tab: Tab) { } export function addTestMarker (value: Object): ThunkAction { - return async function (dispatch: Dispatch, getState: GetState): Promise { + return async (dispatch: Dispatch): Promise => { dispatch(doAddTestMarker(value)); }; } @@ -672,15 +669,16 @@ export function addTestMarker (value: Object): ThunkAction { // Action Handlers // ------------------------------------ -const setOrgTokensHandler = function (state: DashboardState, action: SetOrgTokensAction): DashboardState { - return cloneState(state, { orgTokens: action.orgTokens }); -}; +const setOrgTokensHandler = + (state: DashboardState, action: SetOrgTokensAction): DashboardState => cloneState( + state, + { orgTokens: action.orgTokens }, + ); -const setDevicesHandler = function (state: DashboardState, action: SetDevicesAction): DashboardState { - return cloneState(state, { devices: action.devices }); -}; +const setDevicesHandler = + (state: DashboardState, action: SetDevicesAction): DashboardState => cloneState(state, { devices: action.devices }); -const areLocationsEqual = function (existingLocations: Location[], newLocations: Location[]) { +const areLocationsEqual = (existingLocations: Location[], newLocations: Location[]) => { const firstExistingLocation = existingLocations[0]; const firstNewLocation = newLocations[0]; const lastExistingLocation = existingLocations[existingLocations.length - 1]; @@ -688,18 +686,16 @@ const areLocationsEqual = function (existingLocations: Location[], newLocations: return isEqual([firstExistingLocation, lastExistingLocation], [firstNewLocation, lastNewLocation]); }; -const setLocationsHandler = function (state: DashboardState, action: SetLocationsAction): DashboardState { +const setLocationsHandler = (state: DashboardState, action: SetLocationsAction): DashboardState => { if (areLocationsEqual(state.locations, action.locations)) { return state; - } else { - return cloneState(state, { locations: action.locations }); } + return cloneState(state, { locations: action.locations }); }; -const autoselectOrInvalidateSelectedOrgTokenHandler = function ( +const autoselectOrInvalidateSelectedOrgTokenHandler = ( state: DashboardState, - action: AutoselectOrInvalidateSelectedOrgTokenAction -): DashboardState { +): DashboardState => { const { orgTokens, companyId } = state; if (orgTokens.length === 0) { return cloneState(state, { companyId: 1 }); @@ -711,17 +707,15 @@ const autoselectOrInvalidateSelectedOrgTokenHandler = function ( const existingOrgToken = orgTokens && orgTokens.find((x: Device) => x.id === +companyId); if (!existingOrgToken) { return cloneState(state, { companyId: `${orgTokens[0].id}` }); - } else { - return state; } + return state; } return state; }; -const autoselectOrInvalidateSelectedDeviceHandler = function ( +const autoselectOrInvalidateSelectedDeviceHandler = ( state: DashboardState, - action: AutoselectOrInvalidateSelectedDeviceAction -): DashboardState { +): DashboardState => { const { devices, deviceId } = state; if (devices.length === 0) { return cloneState(state, { deviceId: null }); @@ -733,93 +727,121 @@ const autoselectOrInvalidateSelectedDeviceHandler = function ( const existingDevice = devices && devices.find((x: Device) => x.id === +deviceId); if (!existingDevice) { return cloneState(state, { deviceId: `${devices[0].id}` }); - } else { - return state; } + return state; } return state; }; -const invalidateSelectedLocationHandler = function ( +const invalidateSelectedLocationHandler = ( state: DashboardState, - action: InvalidateSelectedLocationAction -): DashboardState { - const { selectedLocationId, isWatching, currentLocation, locations } = state; +): DashboardState => { + const { + selectedLocationId, isWatching, currentLocation, locations, + } = state; if (!selectedLocationId) { return state; } if (isWatching) { return cloneState(state, { selectedLocationId: currentLocation ? currentLocation.uuid : null }); - } else { - const existingLocation = locations && locations.find((x: Location) => x.uuid === selectedLocationId); - if (!existingLocation) { - return cloneState(state, { selectedLocationId: null }); - } else { - return state; - } } + const existingLocation = locations && locations.find((x: Location) => x.uuid === selectedLocationId); + if (!existingLocation) { + return cloneState(state, { selectedLocationId: null }); + } + return state; }; -const setIsLoadingHandler = function (state: DashboardState, action: SetIsLoadingAction): DashboardState { - return cloneState(state, { isLoading: action.status }); -}; -const setHasDataHandler = function (state: DashboardState, action: SetHasDataAction): DashboardState { - return cloneState(state, { hasData: action.status }); -}; +const setIsLoadingHandler = + (state: DashboardState, action: SetIsLoadingAction): DashboardState => cloneState( + state, + { isLoading: action.status }, + ); +const setHasDataHandler = + (state: DashboardState, action: SetHasDataAction): DashboardState => cloneState(state, { hasData: action.status }); + +const setShowMarkersHandler = + (state: DashboardState, action: SetShowMarkersAction): DashboardState => cloneState( + state, + { showMarkers: action.value }, + ); + +const setEnableClusteringHandler = + (state: DashboardState, action: SetEnableClustringAction): DashboardState => cloneState( + state, + { enableClustering: action.value }, + ); + +const setShowPolylineHandler = + (state: DashboardState, action: SetShowPolylineAction): DashboardState => cloneState( + state, + { showPolyline: action.value }, + ); + +const setShowGeofenceHitsHandler = + (state: DashboardState, action: SetShowGeofenceHitsAction): DashboardState => cloneState( + state, + { showGeofenceHits: action.value }, + ); + +const setMaxMarkersHandler = + (state: DashboardState, action: SetMaxMarkersAction): DashboardState => cloneState( + state, + { maxMarkers: action.value }, + ); + +const setIsWatchingHandler = + (state: DashboardState, action: SetIsWatchingAction): DashboardState => cloneState( + state, + { isWatching: action.value }, + ); + +const setCurrentLocationHandler = + (state: DashboardState, action: SetCurrentLocationAction): DashboardState => cloneState( + state, + { currentLocation: action.location }, + ); +const setStartDateHandler = + (state: DashboardState, action: SetStartDateAction): DashboardState => cloneState( + state, + { startDate: action.value }, + ); +const setEndDateHandler = + (state: DashboardState, action: SetEndDateAction): DashboardState => cloneState( + state, + { endDate: action.value }, + ); + +const setDeviceHandler = (state: DashboardState, action: SetDeviceAction): DashboardState => cloneState( + state, + { deviceId: action.deviceId }, +); + +const setSelectedLocationHandler = + (state: DashboardState, action: SetSelectedLocationAction): DashboardState => cloneState( + state, + { selectedLocationId: action.locationId }, + ); + +const applyExistingSettingsHandler = ( + state: DashboardState, + action: ApplyExistingSettinsAction, +): DashboardState => cloneState(state, action.settings); -const setShowMarkersHandler = function (state: DashboardState, action: SetShowMarkersAction): DashboardState { - return cloneState(state, { showMarkers: action.value }); -}; +const setActiveTabHandler = + (state: DashboardState, action: SetActiveTabAction) => cloneState(state, { activeTab: action.tab }); -const setEnableClusteringHandler = function (state: DashboardState, action: SetEnableClustringAction): DashboardState { - return cloneState(state, { enableClustering: action.value }); -}; +const setOrgTokenHandler = + (state: DashboardState, action: SetOrgTokenAction) => cloneState(state, { companyId: action.value }); -const setShowPolylineHandler = function (state: DashboardState, action: SetShowPolylineAction): DashboardState { - return cloneState(state, { showPolyline: action.value }); -}; -const setShowGeofenceHitsHandler = function (state: DashboardState, action: SetShowGeofenceHitsAction): DashboardState { - return cloneState(state, { showGeofenceHits: action.value }); -}; -const setMaxMarkersHandler = function (state: DashboardState, action: SetMaxMarkersAction): DashboardState { - return cloneState(state, { maxMarkers: action.value }); -}; -const setIsWatchingHandler = function (state: DashboardState, action: SetIsWatchingAction): DashboardState { - return cloneState(state, { isWatching: action.value }); -}; -const setCurrentLocationHandler = function (state: DashboardState, action: SetCurrentLocationAction): DashboardState { - return cloneState(state, { currentLocation: action.location }); -}; -const setStartDateHandler = function (state: DashboardState, action: SetStartDateAction): DashboardState { - return cloneState(state, { startDate: action.value }); -}; -const setEndDateHandler = function (state: DashboardState, action: SetEndDateAction): DashboardState { - return cloneState(state, { endDate: action.value }); -}; -const setDeviceHandler = function (state: DashboardState, action: SetDeviceAction): DashboardState { - return cloneState(state, { deviceId: action.deviceId }); -}; -const setSelectedLocationHandler = function (state: DashboardState, action: SetSelectedLocationAction): DashboardState { - return cloneState(state, { selectedLocationId: action.locationId }); -}; -const applyExistingSettingsHandler = function ( - state: DashboardState, - action: ApplyExistingSettinsAction -): DashboardState { - return cloneState(state, action.settings); -}; -const setActiveTabHandler = function (state: DashboardState, action: SetActiveTabAction) { - return cloneState(state, { activeTab: action.tab }); -}; -const setOrgTokenHandler = function (state: DashboardState, action: SetOrgTokenAction) { - return cloneState(state, { companyId: action.value }); -}; -const setOrgTokenFromSearchHandler = function (state: DashboardState, action: SetOrgTokenFromSearchAction) { - return cloneState(state, { orgTokenFromSearch: action.value }); -}; +const setOrgTokenFromSearchHandler = + (state: DashboardState, action: SetOrgTokenFromSearchAction) => cloneState( + state, + { orgTokenFromSearch: action.value }, + ); -const addTestMarkerHandler = function (state: DashboardState, action: AddTestMarkerAction) { - let markers = [].concat(state.testMarkers); +const addTestMarkerHandler = (state: DashboardState, action: AddTestMarkerAction) => { + const markers = [].concat(state.testMarkers); markers.push(action.value.data); return cloneState(state, { testMarkers: markers }); }; @@ -827,15 +849,15 @@ const addTestMarkerHandler = function (state: DashboardState, action: AddTestMar // ------------------------------------ // Initial State // ------------------------------------ -const getStartDate = function () { - var startDate = new Date(); +const getStartDate = () => { + const startDate = new Date(); startDate.setHours(0); startDate.setMinutes(0); return startDate; }; -const getEndDate = function () { - var endDate = new Date(); +const getEndDate = () => { + const endDate = new Date(); endDate.setHours(23); endDate.setMinutes(59); return endDate; diff --git a/src/client/reducer/index.js b/src/client/reducer/index.js index 103fda0..3ecba30 100644 --- a/src/client/reducer/index.js +++ b/src/client/reducer/index.js @@ -1,6 +1,5 @@ import { combineReducers } from 'redux'; + import dashboard from './dashboard'; -export default combineReducers({ - dashboard, -}); +export default combineReducers({ dashboard }); diff --git a/src/client/reducer/state.js b/src/client/reducer/state.js index ae197a7..f333d70 100644 --- a/src/client/reducer/state.js +++ b/src/client/reducer/state.js @@ -4,3 +4,5 @@ import { type DashboardState } from './index'; export type GlobalState = { dashboard: DashboardState, }; + +export type Tab = 'map' | 'list'; diff --git a/src/client/storage.js b/src/client/storage.js index 1b2b6c3..ae326dc 100644 --- a/src/client/storage.js +++ b/src/client/storage.js @@ -1,9 +1,11 @@ // @flow -import cloneState from '~/utils/cloneState'; import queryString from 'query-string'; import isUndefined from 'lodash/isUndefined'; import omitBy from 'lodash/omitBy'; -import { type Tab } from './reducer/dashboard'; + +import { type Tab } from 'reducer/state'; +import cloneState from 'utils/cloneState'; + export type StoredSettings = {| activeTab: Tab, startDate: Date, @@ -18,7 +20,7 @@ export type StoredSettings = {| |}; const getLocalStorageKey = (key: string) => (key ? `settings#${key}` : 'settings'); -export function getSettings (key: string): $Shape { +export function getSettings(key: string): $Shape { const encodedSettings = localStorage.getItem(getLocalStorageKey(key)); if (encodedSettings) { const parsed = JSON.parse(encodedSettings); @@ -32,15 +34,14 @@ export function getSettings (key: string): $Shape { showPolyline: parsed.showPolyline, maxMarkers: parsed.maxMarkers, }), - isUndefined + isUndefined, ); return result; - } else { - return {}; } + return {}; } -function parseStartDate (date: ?string) { +function parseStartDate(date: ?string) { if (!date) { return undefined; } @@ -49,7 +50,7 @@ function parseStartDate (date: ?string) { } return new Date(date); } -function parseEndDate (date: ?string) { +function parseEndDate(date: ?string) { if (!date) { return undefined; } @@ -57,13 +58,12 @@ function parseEndDate (date: ?string) { return undefined; } if (date.split(' ').length === 1) { - return new Date(date + ' 23:59'); - } else { - return new Date(date); + return new Date(`${date} 23:59`); } + return new Date(date); } -function encodeStartDate (date: ?Date) { +function encodeStartDate(date: ?Date) { if (!date) { return undefined; } @@ -74,11 +74,10 @@ function encodeStartDate (date: ?Date) { const min = date.getMinutes(); if (h === 0 && min === 0) { return `${y}-${mon}-${d}`; - } else { - return `${y}-${mon}-${d} ${h}:${min}`; } + return `${y}-${mon}-${d} ${h}:${min}`; } -function encodeEndDate (date: ?Date) { +function encodeEndDate(date: ?Date) { if (!date) { return undefined; } @@ -89,29 +88,30 @@ function encodeEndDate (date: ?Date) { const min = date.getMinutes(); if (h === 23 && min === 59) { return `${y}-${mon}-${d}`; - } else { - return `${y}-${mon}-${d} ${h}:${min}`; } + return `${y}-${mon}-${d} ${h}:${min}`; } -export function getUrlSettings (): $Shape { - const params = queryString.parse(location.search); +export function getUrlSettings(): $Shape { + const params = queryString.parse(window.location.search); const result = omitBy( { deviceId: params.device, startDate: parseStartDate(params.start), endDate: parseEndDate(params.end), }, - isUndefined + isUndefined, ); return result; } -export function setUrlSettings (settings: {| +export function setUrlSettings(settings: {| deviceId: ?string, startDate: ?Date, endDate: ?Date, orgTokenFromSearch: string, |}) { - const { orgTokenFromSearch, startDate, endDate, deviceId } = settings; + const { + orgTokenFromSearch, startDate, endDate, deviceId, + } = settings; const mainPart = orgTokenFromSearch ? `/${orgTokenFromSearch}` : ''; const search = { device: deviceId, @@ -119,17 +119,21 @@ export function setUrlSettings (settings: {| start: encodeStartDate(startDate), }; const url = `${mainPart}?${queryString.stringify(search)}`; - history.replaceState({}, '', url); + window.history.replaceState({}, '', url); } -export function setSettings (key: string, settings: $Shape) { +export function setSettings(key: string, settings: $Shape) { const existingSettings = getSettings(key); const newSettings = cloneState(existingSettings, settings); // convert start/endDate to string if they are present const stringifiedNewSettings = omitBy( { - startDate: newSettings.startDate ? newSettings.startDate.toISOString() : undefined, - endDate: newSettings.endDate ? newSettings.endDate.toISOString() : undefined, + startDate: newSettings.startDate + ? newSettings.startDate.toISOString() + : undefined, + endDate: newSettings.endDate + ? newSettings.endDate.toISOString() + : undefined, showGeofenceHits: newSettings.showGeofenceHits, showMarkers: newSettings.showMarkers, showPolyline: newSettings.showPolyline, @@ -138,5 +142,8 @@ export function setSettings (key: string, settings: $Shape) { isUndefined, ); - localStorage.setItem(getLocalStorageKey(key), JSON.stringify(stringifiedNewSettings)); + localStorage.setItem( + getLocalStorageKey(key), + JSON.stringify(stringifiedNewSettings), + ); } diff --git a/src/client/store.js b/src/client/store.js index 4c9cc4e..0980b10 100644 --- a/src/client/store.js +++ b/src/client/store.js @@ -1,11 +1,15 @@ -import { createStore, applyMiddleware, compose } from 'redux'; +import { + createStore, applyMiddleware, compose, +} from 'redux'; import thunk from 'redux-thunk'; + import reducer from './reducer'; const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk))); if (module.hot) { + // eslint-disable-next-line global-require module.hot.accept('./reducer', () => store.replaceReducer(require('./reducer').default)); } diff --git a/src/client/utils/GA.js b/src/client/utils/GA.js index 68397d6..fe53f43 100644 --- a/src/client/utils/GA.js +++ b/src/client/utils/GA.js @@ -1,8 +1,10 @@ - const GA = { - sendEvent: function (category, action, label) { - if (!window.ga) { return; } + sendEvent(category, action, label) { + if (!window.ga) { + return; + } window.ga('send', 'event', category, action, label); + // eslint-disable-next-line no-console console.log('GA send event: ', category, action, label); }, }; diff --git a/src/client/utils/cloneState.js b/src/client/utils/cloneState.js index 9ec9369..c3a1bca 100644 --- a/src/client/utils/cloneState.js +++ b/src/client/utils/cloneState.js @@ -1,5 +1,8 @@ // @flow // key function for making a new state in reducers -export default function cloneState (originalObject: T, changes: $Shape): T { - return Object.assign({}, originalObject, changes); +export default function cloneState( + originalObject: T, + changes: $Shape, +): T { + return { ...originalObject, ...changes }; } diff --git a/src/client/utils/formatDate.js b/src/client/utils/formatDate.js index 470292c..2e12d51 100644 --- a/src/client/utils/formatDate.js +++ b/src/client/utils/formatDate.js @@ -1,5 +1,5 @@ import format from 'date-fns/format'; -export default function formatDate (date) { +export default function formatDate(date) { return format(date, 'MM-dd'); } diff --git a/src/server/database/CompanyModel.js b/src/server/database/CompanyModel.js index 536e46a..d3f9d29 100644 --- a/src/server/database/CompanyModel.js +++ b/src/server/database/CompanyModel.js @@ -1,4 +1,5 @@ import Sequelize from 'sequelize'; + import definedSequelizeDb from './define-sequelize-db'; const CompanyModel = definedSequelizeDb.define( @@ -13,12 +14,10 @@ const CompanyModel = definedSequelizeDb.define( created_at: { type: Sequelize.DATE }, updated_at: { type: Sequelize.DATE }, }, - { - timestamps: false, - } + { timestamps: false }, ); -CompanyModel.associate = (models) => { +CompanyModel.associate = models => { models.Company.hasMany(models.Device, { foreignKey: 'company_id' }); models.Company.hasMany(models.Location, { foreignKey: 'company_id' }); }; diff --git a/src/server/database/DeviceModel.js b/src/server/database/DeviceModel.js index a6420d2..c34d3b5 100644 --- a/src/server/database/DeviceModel.js +++ b/src/server/database/DeviceModel.js @@ -1,4 +1,5 @@ import Sequelize from 'sequelize'; + import definedSequelizeDb from './define-sequelize-db'; const DeviceModel = definedSequelizeDb.define( @@ -9,7 +10,10 @@ const DeviceModel = definedSequelizeDb.define( autoIncrement: true, primaryKey: true, }, - company_id: { type: Sequelize.INTEGER, references: { model: 'companies', key: 'id' } }, + company_id: { + type: Sequelize.INTEGER, + references: { model: 'companies', key: 'id' }, + }, // , references: { model: 'companies' } company_token: { type: Sequelize.TEXT }, device_id: { type: Sequelize.TEXT }, @@ -26,10 +30,10 @@ const DeviceModel = definedSequelizeDb.define( { fields: ['company_id'] }, { fields: ['company_token'] }, ], - } + }, ); -DeviceModel.associate = (models) => { +DeviceModel.associate = models => { models.Device.hasMany(models.Location, { foreignKey: 'device_id' }); }; diff --git a/src/server/database/LocationDefinition.js b/src/server/database/LocationDefinition.js index 49e4f30..61115f1 100644 --- a/src/server/database/LocationDefinition.js +++ b/src/server/database/LocationDefinition.js @@ -1,7 +1,9 @@ import Sequelize from 'sequelize'; export default { - id: { type: Sequelize.INTEGER, autoIncrement: true, primaryKey: true }, + id: { + type: Sequelize.INTEGER, autoIncrement: true, primaryKey: true, + }, uuid: { type: Sequelize.TEXT }, latitude: { type: Sequelize.DOUBLE }, longitude: { type: Sequelize.DOUBLE }, diff --git a/src/server/database/LocationModel.js b/src/server/database/LocationModel.js index 51fc045..350748e 100644 --- a/src/server/database/LocationModel.js +++ b/src/server/database/LocationModel.js @@ -1,32 +1,25 @@ import Sequelize from 'sequelize'; +import { isPostgres } from '../libs/utils'; + import definition from './LocationDefinition'; import definedSequelizeDb from './define-sequelize-db'; -import { - isPostgres, -} from '../libs/utils'; if (isPostgres) { - definition.data = { - type: Sequelize.JSONB, - }; + definition.data = { type: Sequelize.JSONB }; } -const LocationModel = definedSequelizeDb.define( - 'locations', - definition, - { - timestamps: false, - indexes: [ - { fields: ['recorded_at'] }, - { fields: ['device_id'] }, - { fields: ['company_id', 'device_id', 'recorded_at'] }, - ], - } -); +const LocationModel = definedSequelizeDb.define('locations', definition, { + timestamps: false, + indexes: [ + { fields: ['recorded_at'] }, + { fields: ['device_id'] }, + { fields: ['company_id', 'device_id', 'recorded_at'] }, + ], +}); -LocationModel.associate = (models) => { +LocationModel.associate = models => { models.Location.belongsTo(models.Device, { foreignKey: 'device_id' }); models.Location.belongsTo(models.Company, { foreignKey: 'company_id' }); }; diff --git a/src/server/database/define-sequelize-db.js b/src/server/database/define-sequelize-db.js index 9243d91..93314dc 100644 --- a/src/server/database/define-sequelize-db.js +++ b/src/server/database/define-sequelize-db.js @@ -1,5 +1,5 @@ -import Sequelize from 'sequelize'; -import path from 'path'; +import path from 'path'; import Sequelize from 'sequelize'; + import { isPostgres } from '../libs/utils'; @@ -15,5 +15,5 @@ export default new Sequelize( : { dialect: 'sqlite', storage: path.resolve(__dirname, 'db', 'background-geolocation.db'), - } + }, ); diff --git a/src/server/database/initializeDatabase.js b/src/server/database/initializeDatabase.js index adfa74e..b0b94bb 100644 --- a/src/server/database/initializeDatabase.js +++ b/src/server/database/initializeDatabase.js @@ -4,21 +4,26 @@ import Device from './DeviceModel'; import Company from './CompanyModel'; const isProduction = process.env.NODE_ENV === 'production'; -const syncOptions = { - logging: true, -}; +const syncOptions = { logging: true }; /** * Init / create location table */ -export default async function initializeDatabase () { - Device.associate({ Location, Device, Company }); - Company.associate({ Location, Device, Company }); - Location.associate({ Location, Device, Company }); +export default async function initializeDatabase() { + Device.associate({ + Location, Device, Company, + }); + Company.associate({ + Location, Device, Company, + }); + Location.associate({ + Location, Device, Company, + }); try { await definedSequelizeDb.authenticate(); } catch (err) { + // eslint-disable-next-line no-console console.error('Unable to connect to the database:', err); } @@ -30,6 +35,7 @@ export default async function initializeDatabase () { await Device.sync(syncOptions); await Location.sync(syncOptions); } catch (err) { + // eslint-disable-next-line no-console console.error('Unable to sync database:', err); } } diff --git a/src/server/database/migrate.js b/src/server/database/migrate.js index debc528..c9923ee 100644 --- a/src/server/database/migrate.js +++ b/src/server/database/migrate.js @@ -2,30 +2,35 @@ * Migrate records created from before Sequalize was introduced */ -var path = require('path'); -var sqlite3 = require('sqlite3').verbose(); -var fs = require('fs'); +const path = require('path'); +const fs = require('fs'); +// eslint-disable-next-line import/no-extraneous-dependencies +const sqlite3 = require('sqlite3').verbose(); -var DB_FILE = path.resolve(__dirname, 'background-geolocation.db'); -var LocationModel = require('./LocationModel.js'); -var dbh; +const DB_FILE = path.resolve(__dirname, 'background-geolocation.db'); +const LocationModel = require('./LocationModel.js'); + +let dbh; if (!fs.existsSync(DB_FILE)) { + // eslint-disable-next-line no-console console.log('- Failed to find background-geolocation.db: ', DB_FILE); } else { dbh = new sqlite3.Database(DB_FILE); - var query = 'SELECT * FROM locations'; + const query = 'SELECT * FROM locations'; - var onQuery = function (err, rows) { + const onQuery = (err, rows) => { if (err) { + // eslint-disable-next-line no-console console.log('ERROR: ', err); return; } - rows.forEach(function (row) { - var id = row.id; + rows.forEach(row => { + const { id } = row; + // eslint-disable-next-line no-param-reassign delete row.id; - LocationModel.update(row, { where: { id: id } }); + LocationModel.update(row, { where: { id } }); }); }; dbh.all(query, onQuery); diff --git a/src/server/index.js b/src/server/index.js index 54952d0..31410a6 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -1,12 +1,14 @@ -import initializeDatabase from './database/initializeDatabase'; +/* eslint-disable no-console */ +import { resolve, extname } from 'path'; import express from 'express'; import morgan from 'morgan'; import bodyParser from 'body-parser'; -import { resolve, extname } from 'path'; + import compress from 'compression'; import 'colors'; import opn from 'opn'; +import initializeDatabase from './database/initializeDatabase'; import siteApi from './routes/site-api'; import api from './routes/api-v2'; import tests from './routes/tests'; @@ -19,17 +21,15 @@ const app = express(); const buildPath = resolve(__dirname, '..', '..', 'build'); const parserLimits = { limit: parserLimit, extended: true }; -process - .on('uncaughtException', (err) => { - // eslint-disable-next-line no-console - console.error(' Exception %s: ', err.message, err.stack); - }); +process.on('uncaughtException', err => { + // eslint-disable-next-line no-console + console.error(' Exception %s: ', err.message, err.stack); +}); -process - .on('message', (msg) => { - // eslint-disable-next-line no-console - console.log('Server %s process.on( message = %s )', msg); - }); +process.on('message', msg => { + // eslint-disable-next-line no-console + console.log('Server %s process.on( message = %s )', msg); +}); app.disable('etag'); app.use(morgan(isProduction ? 'short' : 'dev')); @@ -37,7 +37,7 @@ app.use(compress()); app.use(bodyParser.json(parserLimits)); app.use(bodyParser.raw(parserLimits)); -(async function () { +((async () => { await initializeDatabase(); app.use(siteApi); @@ -56,10 +56,11 @@ app.use(bodyParser.raw(parserLimits)); } else { next(); } - app.use((err, req, res, next) => { - console.error(err.message, err.stack); - res.status(500).send({ message: err.message || 'Something broke!' }); - }); + }); + + app.use((err, req, res) => { + console.error(err.message, err.stack); + res.status(500).send({ message: err.message || 'Something broke!' }); }); app.listen(port, () => { @@ -69,10 +70,9 @@ app.use(bodyParser.raw(parserLimits)); // Spawning dedicated process on opened port.. only if not deployed on heroku if (!dyno) { - opn(`http://localhost:8080`) - .catch(error => console.error('Optional site open failed:', error)); + opn('http://localhost:8080').catch(error => console.error('Optional site open failed:', error)); } }); -})(); +})()); module.exports = app; diff --git a/src/server/libs/RNCrypto.js b/src/server/libs/RNCrypto.js index d0ceebb..ef56ee1 100644 --- a/src/server/libs/RNCrypto.js +++ b/src/server/libs/RNCrypto.js @@ -1,18 +1,19 @@ +/* eslint-disable no-console */ import crypto from 'crypto'; export const isEncryptedRequest = req => { const contentType = req.get('Content-Type'); - const result = (contentType && (contentType.indexOf('application/octet-stream') === 0)); + const result = contentType && contentType.indexOf('application/octet-stream') === 0; return result; }; /** -* Decrypt base64-encoded, RNCrypto encrypted data -* @param {String} data Base64-encoded text. -* @return {String} decrypted JSON -*/ -export const decrypt = (data) => { + * Decrypt base64-encoded, RNCrypto encrypted data + * @param {String} data Base64-encoded text. + * @return {String} decrypted JSON + */ +export const decrypt = data => { // Decryption password. Same as used by BackgroundGeolocation SDK to encrypt. const password = process.env.ENCRYPTION_PASSWORD; // Decode base64 data from HTTP body. @@ -36,7 +37,13 @@ export const decrypt = (data) => { const cipher = buffer.slice(34, buffer.length - 32); // Generate pbkdf2 encryption key using password and password salt - const encryptionKey = crypto.pbkdf2Sync(password, passwordSalt, 10000, 32, 'sha1'); + const encryptionKey = crypto.pbkdf2Sync( + password, + passwordSalt, + 10000, + 32, + 'sha1', + ); console.log('╔═════════════════════════════════════════════'.green); console.log('║ RNCrypto version %d'.green, version); @@ -53,8 +60,7 @@ export const decrypt = (data) => { try { // Decrypt the binary data in cipher - const json = decipher.update(cipher, 'binary', 'utf-8') + - decipher.final('utf-8'); + const json = decipher.update(cipher, 'binary', 'utf-8') + decipher.final('utf-8'); return JSON.parse(json); } catch (e) { diff --git a/src/server/libs/jwt.js b/src/server/libs/jwt.js index 5766994..62c30b7 100644 --- a/src/server/libs/jwt.js +++ b/src/server/libs/jwt.js @@ -25,8 +25,7 @@ export const getKeys = () => { subject: "iam@user.me", audience: "Client_Identity" // this should be provided by client */ -export const sign = (payload, { issuer, subject, audience } = signOptions) => { - const keys = getKeys(); +export const sign = (payload, { issuer, subject } = signOptions) => { // Token signing options const options = { issuer, @@ -38,18 +37,15 @@ export const sign = (payload, { issuer, subject, audience } = signOptions) => { return jwt.sign(payload, keys.private, options); }; -export const verify = (token, { issuer, subject, audience } = signOptions) => { - const keys = getKeys(); +export const verify = (token, { issuer, subject } = signOptions) => { const options = { issuer, subject, - audience: /.*/img, + audience: /.*/gim, // expiresIn: '782d', - algorithm: ['RS256'], + algorithm: ['RS256'], }; return jwt.verify(token, keys.public, options); }; -export const decode = (token) => { - return jwt.decode(token, { complete: true }); -}; +export const decode = token => jwt.decode(token, { complete: true }); diff --git a/src/server/libs/utils.js b/src/server/libs/utils.js index c3b7b8c..03b5513 100644 --- a/src/server/libs/utils.js +++ b/src/server/libs/utils.js @@ -1,5 +1,7 @@ +/* eslint-disable max-classes-per-file */ import { createReadStream } from 'fs'; import { resolve } from 'path'; + import { verify } from './jwt'; // If client registration fails due to not being connected to network, @@ -13,24 +15,24 @@ const DUMMY_TOKEN = 'DUMMY_TOKEN'; export const filterByCompany = !!process.env.SHARED_DASHBOARD; export const deniedCompanies = (process.env.DENIED_COMPANY_TOKENS || '').split(','); export const deniedDevices = (process.env.DENIED_DEVICE_TOKENS || '').split(','); -export const ddosBombCompanies = (process.env.DDOS_BOMB_COMPANY_TOKENS || '').split(','); +export const ddosBombCompanies = ( + process.env.DDOS_BOMB_COMPANY_TOKENS || '' +).split(','); export const isProduction = process.env.NODE_ENV === 'production'; export const isPostgres = !!process.env.DATABASE_URL; export const desc = isPostgres ? 'DESC NULLS LAST' : 'DESC'; -const check = (list, item) => list - .find(x => !!x && (item || '').toLowerCase().startsWith(x.toLowerCase())); +const check = (list, item) => list.find(x => !!x && (item || '').toLowerCase().startsWith(x.toLowerCase())); export const isDDosCompany = orgToken => check(ddosBombCompanies, orgToken); export const isDeniedCompany = orgToken => check(deniedCompanies, orgToken); export const isDeniedDevice = orgToken => check(deniedDevices, orgToken); -export const isAdmin = orgToken => !!filterByCompany & - !!process.env.ADMIN_TOKEN && +export const isAdmin = orgToken => !!filterByCompany & !!process.env.ADMIN_TOKEN && orgToken === process.env.ADMIN_TOKEN; -export const jsonb = data => isPostgres ? (data || null) : JSON.stringify(data); +export const jsonb = data => (isPostgres ? data || null : JSON.stringify(data)); -export class AccessDeniedError extends Error {}; -export class RegistrationRequiredError extends Error {}; +export class AccessDeniedError extends Error {} +export class RegistrationRequiredError extends Error {} export const raiseError = (res, message, error) => { const result = new AccessDeniedError(message); @@ -38,7 +40,7 @@ export const raiseError = (res, message, error) => { return error || result; }; -export function hydrate (row) { +export function hydrate(row) { const record = row.toJSON(); ['data'] .filter(x => typeof record[x] === 'string') @@ -47,6 +49,7 @@ export function hydrate (row) { try { record[x] = JSON.parse(record[x]); } catch (e) { + // eslint-disable-next-line no-console console.error(`could not parse ${x} ${record.id}`, e); delete record[x]; } @@ -64,18 +67,14 @@ export function hydrate (row) { ...record, uuid: data.uuid, }; - [ - 'data', - 'device', - 'activity', - 'battery', - 'coords', - ].forEach(x => delete result[x]); + ['data', 'device', 'activity', 'battery', 'coords'].forEach( + x => delete result[x], + ); return result; } -export function return1Gbfile (res) { +export function return1Gbfile(res) { const file1gb = resolve(__dirname, '..', '..', '..', 'text.null.gz'); res.setHeader('Content-Encoding', 'gzip, deflate'); createReadStream(file1gb).pipe(res); @@ -114,7 +113,7 @@ export const checkCompany = ({ org, model }) => { throw new AccessDeniedError( 'This is a question from the CEO of Transistor Software.\n' + 'Why are you spamming my demo server1/v2?\n' + - 'Please email me at chris@transistorsoft.com.' + 'Please email me at chris@transistorsoft.com.', ); } @@ -122,7 +121,7 @@ export const checkCompany = ({ org, model }) => { throw new AccessDeniedError( 'This is a question from the CEO of Transistor Software.\n' + 'Why are you spamming my demo server2/v2?\n' + - 'Please email me at chris@transistorsoft.com.' + 'Please email me at chris@transistorsoft.com.', ); } }; diff --git a/src/server/models/Device.js b/src/server/models/Device.js index 3fa6fe3..77f3d87 100644 --- a/src/server/models/Device.js +++ b/src/server/models/Device.js @@ -1,46 +1,60 @@ import { Op } from 'sequelize'; -import { findOrCreate as findOrCreateCompany } from './Org'; + import DeviceModel from '../database/DeviceModel'; import LocationModel from '../database/LocationModel'; import { - checkCompany, - filterByCompany, - desc, + checkCompany, filterByCompany, desc, } from '../libs/utils'; -export async function getDevice ({ id }) { +import { findOrCreate as findOrCreateCompany } from './Org'; + +export async function getDevice({ id }) { const whereConditions = { id }; const result = await DeviceModel.findOne({ where: whereConditions, - attributes: ['id', 'device_id', 'device_model', 'company_id', 'company_token'], + attributes: [ + 'id', + 'device_id', + 'device_model', + 'company_id', + 'company_token', + ], raw: true, }); return result; } -export async function getDevices (params) { +export async function getDevices(params) { const whereConditions = {}; if (filterByCompany) { params.company_id && (whereConditions.company_id = +params.company_id); } const result = await DeviceModel.findAll({ where: whereConditions, - attributes: ['id', 'device_id', 'device_model', 'company_id', 'company_token', 'framework'], - order: [['updated_at', desc], ['created_at', desc]], + attributes: [ + 'id', + 'device_id', + 'device_model', + 'company_id', + 'company_token', + 'framework', + ], + order: [ + ['updated_at', desc], + ['created_at', desc], + ], raw: true, }); return result; } -export async function deleteDevice ({ +export async function deleteDevice({ id: deviceId, start_date: startDate, end_date: endDate, }) { - const whereByDevice = { - device_id: deviceId, - }; + const whereByDevice = { device_id: deviceId }; const where = { ...whereByDevice }; if (startDate && endDate && new Date(startDate) && new Date(endDate)) { where.recorded_at = { [Op.between]: [new Date(startDate), new Date(endDate)] }; @@ -49,9 +63,7 @@ export async function deleteDevice ({ const locationsCount = await LocationModel.count({ where: whereByDevice }); if (!locationsCount) { await DeviceModel.destroy({ - where: { - id: deviceId, - }, + where: { id: deviceId }, cascade: true, }); } @@ -59,12 +71,10 @@ export async function deleteDevice ({ } export const findOrCreate = async ( - org = 'UNKNOWN', { - model, - uuid, - framework, - version, - } + org = 'UNKNOWN', + { + model, uuid, framework, version, + }, ) => { const device = { device_id: uuid || 'UNKNOWN', diff --git a/src/server/models/Location.js b/src/server/models/Location.js index ce221e8..e76b8cf 100644 --- a/src/server/models/Location.js +++ b/src/server/models/Location.js @@ -1,9 +1,10 @@ +/* eslint-disable no-console */ import { Op } from 'sequelize'; import Promise from 'bluebird'; + import CompanyModel from '../database/CompanyModel'; import DeviceModel from '../database/DeviceModel'; import LocationModel from '../database/LocationModel'; -import { findOrCreate } from './Device'; import { AccessDeniedError, filterByCompany, @@ -14,9 +15,12 @@ import { jsonb, } from '../libs/utils'; +import { findOrCreate } from './Device'; + + const include = [{ model: DeviceModel, as: 'device' }]; -export async function getStats () { +export async function getStats() { const minDate = await LocationModel.min('created_at'); const maxDate = await LocationModel.max('created_at'); const total = await LocationModel.count(); @@ -27,7 +31,7 @@ export async function getStats () { }; } -export async function getLocations (params) { +export async function getLocations(params) { const whereConditions = {}; if (params.start_date && params.end_date) { whereConditions.recorded_at = { [Op.between]: [new Date(params.start_date), new Date(params.end_date)] }; @@ -49,8 +53,8 @@ export async function getLocations (params) { return locations; } -export async function getLatestLocation (params) { - var whereConditions = {}; +export async function getLatestLocation(params) { + const whereConditions = {}; params.device_id && (whereConditions.device_id = +params.device_id); if (filterByCompany) { params.companyId && (whereConditions.company_id = +params.companyId); @@ -65,19 +69,23 @@ export async function getLatestLocation (params) { return result; } -export async function createLocation (params, device = {}) { +export async function createLocation(params, device = {}) { if (Array.isArray(params)) { - for (let location of params) { - try { - await createLocation(location, device); - } catch (e) { - throw e; - } - } - return; + return Promise.reduce( + params, + async (p, location) => { + try { + await createLocation(location, device); + } catch (e) { + console.error('createLocation', e); + throw e; + } + }, + 0, + ); } const { company_token: orgToken, id } = device; - const { location, company_token: token } = params; + const { location: list, company_token: token } = params; const deviceInfo = params.device || { model: 'UNKNOWN', uuid: 'UNKNOWN' }; const companyName = orgToken || token || 'UNKNOWN'; const now = new Date(); @@ -85,55 +93,66 @@ export async function createLocation (params, device = {}) { if (isDeniedCompany(companyName)) { throw new AccessDeniedError( 'This is a question from the CEO of Transistor Software.\n' + - 'Why are you spamming my demo server1?\n' + - 'Please email me at chris@transistorsoft.com.' + 'Why are you spamming my demo server1?\n' + + 'Please email me at chris@transistorsoft.com.', ); } - const locations = Array.isArray(location) ? location : (location ? [location] : []); - - for (let location of locations) { - if (isDeniedDevice(deviceInfo.model)) { - throw new AccessDeniedError( - 'This is a question from the CEO of Transistor Software.\n' + - 'Why are you spamming my demo server2?\n' + - 'Please email me at chris@transistorsoft.com.' - ); - } + const locations = Array.isArray(list) + ? list + : list + ? [list] + : []; + + return Promise.reduce( + locations, + async (p, location) => { + if (isDeniedDevice(deviceInfo.model)) { + throw new AccessDeniedError( + 'This is a question from the CEO of Transistor Software.\n' + + 'Why are you spamming my demo server2?\n' + + 'Please email me at chris@transistorsoft.com.', + ); + } - const currentDevice = id - ? device - : await findOrCreate(companyName, { ...deviceInfo }); + const currentDevice = id + ? device + : await findOrCreate(companyName, { ...deviceInfo }); - CompanyModel.update( - { updated_at: now }, - { where: { id: currentDevice.company_id } } - ); - DeviceModel.update( - { updated_at: now }, - { where: { id: currentDevice.id } } - ); + CompanyModel.update( + { updated_at: now }, + { where: { id: currentDevice.company_id } }, + ); + DeviceModel.update( + { updated_at: now }, + { where: { id: currentDevice.id } }, + ); - console.info( - 'location:create'.green, - 'org:name'.green, companyName, - 'org:id'.green, currentDevice.company_id, - 'device:id'.green, currentDevice.id, - ); + console.info( + 'location:create'.green, + 'org:name'.green, + companyName, + 'org:id'.green, + currentDevice.company_id, + 'device:id'.green, + currentDevice.id, + ); - await LocationModel.create({ - latitude: location.coords.latitude, - longitude: location.coords.longitude, - data: jsonb(location), - recorded_at: location.timestamp, - created_at: now, - company_id: currentDevice.company_id, - device_id: currentDevice.id, - }); - } + return LocationModel.create({ + latitude: location.coords.latitude, + longitude: location.coords.longitude, + data: jsonb(location), + recorded_at: location.timestamp, + created_at: now, + company_id: currentDevice.company_id, + device_id: currentDevice.id, + }); + }, + 0, + ); } -export async function deleteLocations (params) { +export async function deleteLocations(params) { const whereConditions = {}; const verify = {}; const companyId = params && (params.companyId || params.company_id); @@ -158,13 +177,9 @@ export async function deleteLocations (params) { await LocationModel.destroy({ where: whereConditions }); if (params.deviceId) { - const locationsCount = await LocationModel.count({ - where: verify, - }); + const locationsCount = await LocationModel.count({ where: verify }); if (!locationsCount && verify.device_id) { - await DeviceModel.destroy({ - where: { id: verify.device_id }, - }); + await DeviceModel.destroy({ where: { id: verify.device_id } }); } } else if (companyId) { const devices = await LocationModel.findAll({ @@ -174,12 +189,16 @@ export async function deleteLocations (params) { raw: true, }); const group = {}; - devices.forEach(x => (group[x.company_id] = (group[x.company_id] || []).concat([x.device_id]))); + devices.forEach( + x => (group[x.company_id] = (group[x.company_id] || []).concat([ + x.device_id, + ])), + ); const queries = Object.keys(group) - .map(companyId => DeviceModel.destroy({ + .map(id => DeviceModel.destroy({ where: { - company_id: +companyId, - id: { $notIn: group[companyId] }, + company_id: +id, + id: { $notIn: group[id] }, }, cascade: true, raw: true, diff --git a/src/server/models/Org.js b/src/server/models/Org.js index c48f87f..7c2d137 100644 --- a/src/server/models/Org.js +++ b/src/server/models/Org.js @@ -1,11 +1,10 @@ import { - isAdmin, - filterByCompany, + isAdmin, filterByCompany, desc, } from '../libs/utils'; import CompanyModel from '../database/CompanyModel'; -import { desc } from '../libs/utils'; -export async function getOrgs ({ company_token: org }) { + +export async function getOrgs({ company_token: org }) { if (!filterByCompany) { return [ { @@ -20,17 +19,18 @@ export async function getOrgs ({ company_token: org }) { attributes: ['id', 'company_token'], order: [['updated_at', desc]], raw: true, - }); return result; } -export async function findOrCreate ({ company_token: org }) { +export async function findOrCreate({ company_token: org }) { const now = new Date(); const [company] = await CompanyModel.findOrCreate({ where: { company_token: org }, - defaults: { created_at: now, company_token: org, updated_at: now }, + defaults: { + created_at: now, company_token: org, updated_at: now, + }, raw: true, }); return company; -}; +} diff --git a/src/server/routes/api-v2.js b/src/server/routes/api-v2.js index a424f94..a5b807f 100644 --- a/src/server/routes/api-v2.js +++ b/src/server/routes/api-v2.js @@ -1,17 +1,22 @@ -import { Router } from 'express'; import crypto from 'crypto'; +import { Router } from 'express'; -import { findOrCreate, getDevices, getDevice, deleteDevice } from '../models/Device'; -import { getOrgs } from '../models/Org'; -import { isEncryptedRequest, decrypt } from '../libs/RNCrypto'; +import { decrypt, isEncryptedRequest } from '../libs/RNCrypto'; +import { sign } from '../libs/jwt'; import { AccessDeniedError, - RegistrationRequiredError, checkAuth, - isProduction, isDDosCompany, + isProduction, + RegistrationRequiredError, return1Gbfile, } from '../libs/utils'; +import { + deleteDevice, + findOrCreate, + getDevice, + getDevices, +} from '../models/Device'; import { createLocation, deleteLocations, @@ -19,30 +24,32 @@ import { getLocations, getStats, } from '../models/Location'; -import { sign } from '../libs/jwt'; +import { getOrgs } from '../models/Org'; const router = new Router(); // curl -v -X POST http://localhost:9000/v2/register \ // -d '{"company_token":"test","device_id":"test"}' \ // -H 'Content-Type: application/json' -router.post('/register', async function (req, res) { +router.post('/register', async (req, res) => { const { + org, uuid, model, manufacturer, version, framework, + } = req.body; + + // eslint-disable-next-line no-console + console.info( + 'POST /register '.green, + 'org'.green, org, + 'uuid'.green, uuid, + 'model'.green, model, manufacturer, + 'version'.green, version, + 'framework'.green, framework, - } = req.body; - - console.info( - 'POST /register '.green, - 'org'.green, org, - 'uuid'.green, uuid, - 'model'.green, model, - 'version'.green, version, - 'framework'.green, framework, ); if (!org) { @@ -68,7 +75,10 @@ router.post('/register', async function (req, res) { }; const accessToken = sign(jwtInfo); - const refreshToken = crypto.createHash('md5').update(accessToken).digest('hex'); + const refreshToken = crypto + .createHash('md5') + .update(accessToken) + .digest('hex'); return res.send({ accessToken, @@ -79,26 +89,35 @@ router.post('/register', async function (req, res) { if (err instanceof AccessDeniedError) { return res.status(403).send({ error: err.message }); } + // eslint-disable-next-line no-console console.error('/register', err); return res.status(500).send(!isProduction ? err : err.message); } }); -router.all('/refresh_token', checkAuth, async function (req, res) { - const { org, deviceId, model } = req.jwt; +router.all('/refresh_token', checkAuth, async (req, res) => { + const { + org, deviceId, model, + } = req.jwt; const jwtInfo = { org, deviceId, model, }; + // eslint-disable-next-line no-console console.info( 'auth:refresh'.green, - 'org:name'.green, org, - 'device:id'.green, deviceId, + 'org:name'.green, + org, + 'device:id'.green, + deviceId, ); try { const accessToken = sign(jwtInfo); - const refreshToken = crypto.createHash('md5').update(accessToken).digest('hex'); + const refreshToken = crypto + .createHash('md5') + .update(accessToken) + .digest('hex'); return res.send({ accessToken, @@ -109,6 +128,7 @@ router.all('/refresh_token', checkAuth, async function (req, res) { if (err instanceof AccessDeniedError) { return res.status(403).send({ error: err.message }); } + // eslint-disable-next-line no-console console.error('/register', req.body, err); return res.status(500).send(!isProduction ? err : err.message); } @@ -117,44 +137,44 @@ router.all('/refresh_token', checkAuth, async function (req, res) { // curl -v http://localhost:9000/v2/company_tokens \ // -H 'Authorization: Bearer ey...Pg' // -router.get('/company_tokens', checkAuth, async function (req, res) { +router.get('/company_tokens', checkAuth, async (req, res) => { const { org } = req.jwt; try { const orgTokens = await getOrgs({ company_token: org }); res.send(orgTokens); } catch (err) { + // eslint-disable-next-line no-console console.error('/company_tokens', err); res.status(500).send({ error: err.message }); } }); -router.get('/devices', checkAuth, async function (req, res) { +router.get('/devices', checkAuth, async (req, res) => { try { const { deviceId } = req.jwt; const device = await getDevice({ id: deviceId }); - const devices = await getDevices({ - company_id: device.company_id, - }); + const devices = await getDevices({ company_id: device.company_id }); res.send(devices || []); } catch (err) { + // eslint-disable-next-line no-console console.error('/devices', err); res.status(500).send({ error: err.message }); } }); -router.delete('/devices/:id', checkAuth, async function (req, res) { +router.delete('/devices/:id', checkAuth, async (req, res) => { const { deviceId } = req.jwt; + // eslint-disable-next-line no-console console.info( 'devices:delete'.green, - 'device:id'.green, deviceId, - JSON.stringify(req.query) + 'device:id'.green, + deviceId, + JSON.stringify(req.query), ); const { - id, - end_date: endDate, - start_date: startDate, + id, end_date: endDate, start_date: startDate, } = req.params; try { await deleteDevice({ @@ -164,38 +184,43 @@ router.delete('/devices/:id', checkAuth, async function (req, res) { }); res.send({ success: true }); } catch (err) { + // eslint-disable-next-line no-console console.error(`/devices/${id}`, deviceId, req.query, err); res.status(500).send({ error: err.message }); } }); -router.get('/stats', checkAuth, async function (req, res) { +router.get('/stats', checkAuth, async (req, res) => { try { const stats = await getStats(); res.send(stats); } catch (err) { + // eslint-disable-next-line no-console console.error('/stats', err); res.status(500).send({ error: err.message }); } }); -router.get('/locations/latest', checkAuth, async function (req, res) { +router.get('/locations/latest', checkAuth, async (req, res) => { const { deviceId, org } = req.jwt; const device = await getDevice({ id: deviceId }); + // eslint-disable-next-line no-console console.info( 'locations:latest'.green, - 'org:name'.green, org, - 'device:id'.green, deviceId, - JSON.stringify(req.query) + 'org:name'.green, + org, + 'device:id'.green, + deviceId, + JSON.stringify(req.query), ); try { const latest = await getLatestLocation({ device_id: deviceId, company_id: device.company_id, - }); res.send(latest); } catch (err) { + // eslint-disable-next-line no-console console.error('/locations/latest', req.query, err); res.status(500).send({ error: err.message }); } @@ -204,19 +229,19 @@ router.get('/locations/latest', checkAuth, async function (req, res) { /** * GET /locations */ -router.get('/locations', checkAuth, async function (req, res) { +router.get('/locations', checkAuth, async (req, res) => { const { deviceId, org } = req.jwt; + // eslint-disable-next-line no-console console.info( 'locations:get'.green, - 'org:name'.green, org, - 'device:id'.green, deviceId, - JSON.stringify(req.query) + 'org:name'.green, + org, + 'device:id'.green, + deviceId, + JSON.stringify(req.query), ); const device = await getDevice({ id: deviceId }); - const { - end_date: endDate, - start_date: startDate, - } = req.params; + const { end_date: endDate, start_date: startDate } = req.params; try { const locations = await getLocations({ start_date: startDate, @@ -225,6 +250,7 @@ router.get('/locations', checkAuth, async function (req, res) { }); res.send(locations); } catch (err) { + // eslint-disable-next-line no-console console.error('/locations', req.query, err); res.status(500).send({ error: err.message }); } @@ -233,32 +259,40 @@ router.get('/locations', checkAuth, async function (req, res) { /** * POST /locations */ -router.post('/locations', checkAuth, async function (req, res) { +router.post('/locations', checkAuth, async (req, res) => { const { deviceId, org } = req.jwt; + // eslint-disable-next-line no-console console.info( 'locations:post'.green, - 'org:name'.green, org, - 'device:id'.green, deviceId + 'org:name'.green, + org, + 'device:id'.green, + deviceId, ); const { body } = req; const device = await getDevice({ id: deviceId }); - const data = isEncryptedRequest(req) - ? decrypt(body.toString()) - : body; + const data = isEncryptedRequest(req) ? decrypt(body.toString()) : body; // Can happen if Device is deleted from Dashboard but a JWT is still posting locations for it. if (device == null) { - console.error('Device ID %s not found. Was it deleted from dashboard?'.red, deviceId); - return res.status(410).send({ error: 'DEVICE_ID_NOT_FOUND', background_geolocation: ['stop'] }); + // eslint-disable-next-line no-console + console.error( + 'Device ID %s not found. Was it deleted from dashboard?'.red, + deviceId, + ); + return res.status(410).send({ + error: 'DEVICE_ID_NOT_FOUND', + background_geolocation: ['stop'], + }); } - const locations = (Array.isArray(data) ? data : (data ? [data] : [])) - .map(x => ({ - ...x, - company_id: device.company_id, - device_id: deviceId, - company_token: device.company_token, - })); + const array = Array.isArray(data) ? data : data ? [data] : []; + const locations = array.map(x => ({ + ...x, + company_id: device.company_id, + device_id: deviceId, + company_token: device.company_token, + })); if (isDDosCompany(device.company_token)) { return return1Gbfile(res); @@ -266,28 +300,33 @@ router.post('/locations', checkAuth, async function (req, res) { try { await createLocation(locations, device); - res.send({ success: true }); + return res.send({ success: true }); } catch (err) { if (err instanceof AccessDeniedError) { return res.status(403).send({ error: err.toString() }); - } else if (err instanceof RegistrationRequiredError) { + } + if (err instanceof RegistrationRequiredError) { return res.status(406).send({ error: err.toString() }); } + // eslint-disable-next-line no-console console.error('POST /locations', body, err); - res.status(500).send({ error: err.message }); + return res.status(500).send({ error: err.message }); } }); /** * POST /locations */ -router.post('/locations/:company_token', checkAuth, async function (req, res) { +router.post('/locations/:company_token', checkAuth, async (req, res) => { const { deviceId, org } = req.jwt; + // eslint-disable-next-line no-console console.info( 'locations:post'.green, - 'org:name'.green, org, - 'device:id'.green, deviceId + 'org:name'.green, + org, + 'device:id'.green, + deviceId, ); const device = await getDevice({ id: deviceId }); @@ -295,7 +334,7 @@ router.post('/locations/:company_token', checkAuth, async function (req, res) { return return1Gbfile(res); } - const data = (isEncryptedRequest(req)) + const data = isEncryptedRequest(req) ? decrypt(req.body.toString()) : req.body; data.company_token = device.company_token; @@ -307,34 +346,35 @@ router.post('/locations/:company_token', checkAuth, async function (req, res) { company_id: device.company_id, company_token: device.company_token, }, - device + device, ); - res.send({ success: true }); + return res.send({ success: true }); } catch (err) { if (err instanceof AccessDeniedError) { return res.status(403).send({ error: err.toString() }); } + // eslint-disable-next-line no-console console.error(`POST /locations${device.company_token}`, err); - res.status(500).send({ error: err.message }); + return res.status(500).send({ error: err.message }); } }); -router.delete('/locations', checkAuth, async function (req, res) { +router.delete('/locations', checkAuth, async (req, res) => { try { const { deviceId, org } = req.jwt; + // eslint-disable-next-line no-console console.info( 'locations:delete'.green, - 'org:name'.green, org, - 'device:id'.green, deviceId, - JSON.stringify(req.query) + 'org:name'.green, + org, + 'device:id'.green, + deviceId, + JSON.stringify(req.query), ); const device = await getDevice({ id: deviceId }); - const { - start_date: startDate, - end_date: endDate, - } = req.query; + const { start_date: startDate, end_date: endDate } = req.query; await deleteLocations({ companyId: device.company_id, @@ -344,6 +384,7 @@ router.delete('/locations', checkAuth, async function (req, res) { }); res.send({ success: true }); } catch (err) { + // eslint-disable-next-line no-console console.info('DELETE /locations', req.query, err); res.status(500).send({ error: err.message }); } diff --git a/src/server/routes/site-api.js b/src/server/routes/site-api.js index 133b08a..ebb9ae9 100644 --- a/src/server/routes/site-api.js +++ b/src/server/routes/site-api.js @@ -1,13 +1,16 @@ +/* eslint-disable no-console */ import fs from 'fs'; import { Router } from 'express'; -import { isEncryptedRequest, decrypt } from '../libs/RNCrypto'; + +import { sign } from '../libs/jwt'; +import { decrypt, isEncryptedRequest } from '../libs/RNCrypto'; import { AccessDeniedError, + isAdmin, isDDosCompany, return1Gbfile, } from '../libs/utils'; -import { getDevices, deleteDevice } from '../models/Device'; -import { getOrgs } from '../models/Org'; +import { deleteDevice, getDevices } from '../models/Device'; import { createLocation, deleteLocations, @@ -15,13 +18,14 @@ import { getLocations, getStats, } from '../models/Location'; +import { getOrgs } from '../models/Org'; const router = new Router(); /** * GET /company_tokens */ -router.get('/company_tokens', async function (req, res) { +router.get('/company_tokens', async (req, res) => { try { const orgs = await getOrgs(req.query); res.send(orgs); @@ -34,7 +38,7 @@ router.get('/company_tokens', async function (req, res) { /** * GET /devices */ -router.get('/devices', async function (req, res) { +router.get('/devices', async (req, res) => { try { const devices = await getDevices(req.query); res.send(devices); @@ -44,18 +48,25 @@ router.get('/devices', async function (req, res) { } }); -router.delete('/devices/:id', async function (req, res) { +router.delete('/devices/:id', async (req, res) => { try { - console.log(`DELETE /devices/${req.params.id}?${JSON.stringify(req.query)}\n`.green); + console.log( + `DELETE /devices/${req.params.id}?${JSON.stringify(req.query)}\n`.green, + ); await deleteDevice({ ...req.query, id: req.params.id }); res.send({ success: true }); } catch (err) { - console.error('/devices', JSON.stringify(req.params), JSON.stringify(req.query), err); + console.error( + '/devices', + JSON.stringify(req.params), + JSON.stringify(req.query), + err, + ); res.status(500).send({ error: 'Something failed!' }); } }); -router.get('/stats', async function (req, res) { +router.get('/stats', async (req, res) => { try { const stats = await getStats(); res.send(stats); @@ -65,7 +76,7 @@ router.get('/stats', async function (req, res) { } }); -router.get('/locations/latest', async function (req, res) { +router.get('/locations/latest', async (req, res) => { console.log('GET /locations %s'.green, JSON.stringify(req.query)); try { const latest = await getLatestLocation(req.query); @@ -79,7 +90,7 @@ router.get('/locations/latest', async function (req, res) { /** * GET /locations */ -router.get('/locations', async function (req, res) { +router.get('/locations', async (req, res) => { console.log('GET /locations %s'.green, JSON.stringify(req.query)); try { @@ -94,12 +105,10 @@ router.get('/locations', async function (req, res) { /** * POST /locations */ -router.post('/locations', async function (req, res) { +router.post('/locations', async (req, res) => { const { body } = req; - const data = isEncryptedRequest(req) - ? decrypt(body.toString()) - : body; - const locations = Array.isArray(data) ? data : (data ? [data] : []); + const data = isEncryptedRequest(req) ? decrypt(body.toString()) : body; + const locations = Array.isArray(data) ? data : data ? [data] : []; if (locations.find(({ company_token: org }) => isDDosCompany(org))) { return return1Gbfile(res); @@ -107,48 +116,47 @@ router.post('/locations', async function (req, res) { try { await createLocation(locations); - res.send({ success: true }); + return res.send({ success: true }); } catch (err) { if (err instanceof AccessDeniedError) { return res.status(403).send({ error: err.toString() }); } console.error('post /locations', err); - res.status(500).send({ error: 'Something failed!' }); + return res.status(500).send({ error: 'Something failed!' }); } }); /** * POST /locations */ -router.post('/locations/:company_token', async function (req, res) { +router.post('/locations/:company_token', async (req, res) => { const { company_token: org } = req.params; - console.info( - 'locations:post'.green, - 'org:name'.green, org, - ); + console.info('locations:post'.green, 'org:name'.green, org); if (isDDosCompany(org)) { return return1Gbfile(res); } - const data = (isEncryptedRequest(req)) ? decrypt(req.body.toString()) : req.body; + const data = isEncryptedRequest(req) + ? decrypt(req.body.toString()) + : req.body; data.company_token = org; try { await createLocation(data); - res.send({ success: true }); + return res.send({ success: true }); } catch (err) { if (err instanceof AccessDeniedError) { return res.status(403).send({ error: err.toString() }); } console.error('post /locations', org, err); - res.status(500).send({ error: 'Something failed!' }); + return res.status(500).send({ error: 'Something failed!' }); } }); -router.delete('/locations', async function (req, res) { +router.delete('/locations', async (req, res) => { console.info('locations:delete:query'.green, JSON.stringify(req.query)); try { @@ -162,15 +170,15 @@ router.delete('/locations', async function (req, res) { } }); -router.post('/locations_template', async function (req, res) { +router.post('/locations_template', async (req, res) => { console.log('POST /locations_template\n%s\n'.green, JSON.stringify(req.body)); res.set('Retry-After', 5); res.send({ success: true }); }); -router.post('/configure', async function (req, res) { - var response = { +router.post('/configure', async (req, res) => { + const response = { access_token: 'e7ebae5e-4bea-4d63-8f28-8a104acd2f4c', token_type: 'Bearer', expires_in: 3600, @@ -180,12 +188,25 @@ router.post('/configure', async function (req, res) { res.send(response); }); +router.post('/auth', async (req, res) => { + const { body: { org } } = req; + + if (isAdmin(org)) { + const jwtInfo = { org }; + + const accessToken = sign(jwtInfo); + res.send({ accessToken }); + } + + return res.status(401).send({ org, error: 'Await not public account' }); +}); + /** * Fetch iOS simulator city_drive route */ -router.get('/data/city_drive', async function (req, res) { +router.get('/data/city_drive', async (req, res) => { console.log('GET /data/city_drive.json'.green); - fs.readFile('./data/city_drive.json', 'utf8', function (_err, data) { + fs.readFile('./data/city_drive.json', 'utf8', (_err, data) => { res.send(data); }); }); diff --git a/src/server/routes/tests.js b/src/server/routes/tests.js index 8e71066..7957d09 100644 --- a/src/server/routes/tests.js +++ b/src/server/routes/tests.js @@ -1,32 +1,38 @@ import { Router } from 'express'; // import { isEncryptedRequest, decrypt } from '../libs/RNCrypto'; -import { - AccessDeniedError, - RegistrationRequiredError, -} from '../libs/utils'; +import { AccessDeniedError, RegistrationRequiredError } from '../libs/utils'; const router = new Router(); -router.post('/test/locations/500', async function (req, res) { +router.post('/test/locations/500', async (req, res) => { res.status(500).send({ message: 'Dummy error response' }); }); -router.post('/test/register', async function (req, res) { +router.post('/test/register', async (req, res) => { res.status(200).send({ accessToken: 'Dummy access token' }); }); -router.post('/test/status/:status', async function (req, res) { +router.post('/test/status/:status', async (req, res) => { const { status } = req.params; - res.status(+status).send({ message: `Dummy error response with ${status}`, status }); + res + .status(+status) + .send({ message: `Dummy error response with ${status}`, status }); }); -router.post('/test/error/403/AccessDeniedError', async function (req, res) { - res.status(403).send({ error: new AccessDeniedError('Dummy error').toString() }); +router.post('/test/error/403/AccessDeniedError', async (req, res) => { + res + .status(403) + .send({ error: new AccessDeniedError('Dummy error').toString() }); }); -router.post('/test/error/406/RegistrationRequiredError', async function (req, res) { - res.status(406).send({ error: new RegistrationRequiredError('Dummy error').toString() }); +router.post('/test/error/406/RegistrationRequiredError', async ( + req, + res, +) => { + res + .status(406) + .send({ error: new RegistrationRequiredError('Dummy error').toString() }); }); export default router; diff --git a/tests/api.test.js b/tests/api.test.js index e2c6778..9a2e3e5 100644 --- a/tests/api.test.js +++ b/tests/api.test.js @@ -1,9 +1,11 @@ /* eslint-disable no-unused-expressions */ -import chai from 'chai'; +import queryString from 'querystring'; import chai from 'chai'; import chaiHttp from 'chai-http'; -import queryString from 'querystring'; -import { location, regData, server } from './data'; + +import { + location, regData, server, +} from './data'; chai.use(chaiHttp); chai.should(); @@ -12,7 +14,8 @@ const { expect } = chai; let token; beforeAll(async () => { - const res = await chai.request(server) + const res = await chai + .request(server) .post('/api/register') .send(regData); ({ accessToken: token } = res.body); @@ -20,17 +23,23 @@ beforeAll(async () => { describe('jwt api', () => { test('/register', async () => { - const res = await chai.request(server) + const res = await chai + .request(server) .post('/api/register') .send(regData); expect(res).have.status(200); expect(res).to.be.json; - expect(res.body).to.have.property('accessToken').to.be.a('string'); - expect(res.body).to.have.property('refreshToken').to.be.a('string'); + expect(res.body) + .to.have.property('accessToken') + .to.be.a('string'); + expect(res.body) + .to.have.property('refreshToken') + .to.be.a('string'); }); test('/register without device info', async () => { - const res = await chai.request(server) + const res = await chai + .request(server) .post('/api/register') .send({ org: 'test' }); expect(res).have.status(500); @@ -39,15 +48,17 @@ describe('jwt api', () => { }); test('/company_tokens', async () => { - const res = await chai.request(server) + const res = await chai + .request(server) .get('/api/company_tokens') - .set('Authorization', 'Bearer ' + token); + .set('Authorization', `Bearer ${token}`); expect(res).have.status(200); expect(res).to.be.json; }); test('/company_tokens 403', async () => { - const res = await chai.request(server) + const res = await chai + .request(server) .get('/api/company_tokens') .set('Authorization', 'Bearer !!!'); expect(res).have.status(403); @@ -55,17 +66,23 @@ describe('jwt api', () => { }); test('/refresh_token', async () => { - const res = await chai.request(server) + const res = await chai + .request(server) .get('/api/refresh_token') - .set('Authorization', 'Bearer ' + token); + .set('Authorization', `Bearer ${token}`); expect(res).have.status(200); expect(res).to.be.json; - expect(res.body).to.have.property('accessToken').to.be.a('string'); - expect(res.body).to.have.property('refreshToken').to.be.a('string'); + expect(res.body) + .to.have.property('accessToken') + .to.be.a('string'); + expect(res.body) + .to.have.property('refreshToken') + .to.be.a('string'); }); test('/refresh_token 403', async () => { - const res = await chai.request(server) + const res = await chai + .request(server) .get('/api/refresh_token') .set('Authorization', 'Bearer !!'); expect(res).have.status(403); @@ -73,15 +90,17 @@ describe('jwt api', () => { }); test('/stats', async () => { - const res = await chai.request(server) + const res = await chai + .request(server) .get('/api/stats') - .set('Authorization', 'Bearer ' + token); + .set('Authorization', `Bearer ${token}`); expect(res).have.status(200); expect(res).to.be.json; }); test('/stats 403', async () => { - const res = await chai.request(server) + const res = await chai + .request(server) .get('/api/stats') .set('Authorization', 'Bearer '); expect(res).have.status(403); @@ -89,16 +108,18 @@ describe('jwt api', () => { }); test('/devices', async () => { - const res = await chai.request(server) + const res = await chai + .request(server) .get('/api/devices') - .set('Authorization', 'Bearer ' + token); + .set('Authorization', `Bearer ${token}`); expect(res).have.status(200); expect(res).to.be.json; // console.info('devices:data', res.body); }); test('/devices 403', async () => { - const res = await chai.request(server) + const res = await chai + .request(server) .get('/api/devices') .set('Authorization', 'Bearer '); expect(res).have.status(403); @@ -108,35 +129,39 @@ describe('jwt api', () => { describe('locations', () => { test('/locations/latest', async () => { - const res = await chai.request(server) + const res = await chai + .request(server) .get('/api/locations/latest') - .set('Authorization', 'Bearer ' + token); + .set('Authorization', `Bearer ${token}`); expect(res).have.status(200); expect(res).to.be.json; }); test('POST /locations', async () => { - const res = await chai.request(server) + const res = await chai + .request(server) .post('/api/locations') - .set('Authorization', 'Bearer ' + token) + .set('Authorization', `Bearer ${token}`) .send({ location }); expect(res).have.status(200); expect(res).to.be.json; }); test('POST /locations []', async () => { - const res = await chai.request(server) + const res = await chai + .request(server) .post('/api/locations') - .set('Authorization', 'Bearer ' + token) + .set('Authorization', `Bearer ${token}`) .send([{ location }]); expect(res).have.status(200); expect(res).to.be.json; }); test('/locations', async () => { - const res = await chai.request(server) + const res = await chai + .request(server) .get('/api/locations') - .set('Authorization', 'Bearer ' + token); + .set('Authorization', `Bearer ${token}`); expect(res).have.status(200); expect(res).to.be.json; expect(res.body).to.be.an('array'); @@ -144,24 +169,26 @@ describe('jwt api', () => { }); test('POST /locations/test', async () => { - const res = await chai.request(server) + const res = await chai + .request(server) .post('/api/locations/test') - .set('Authorization', 'Bearer ' + token) + .set('Authorization', `Bearer ${token}`) .send({ location }); expect(res).have.status(200); expect(res).to.be.json; }); test('DELETE /locations', async () => { - const res = await chai.request(server) + const res = await chai + .request(server) .delete( - '/api/locations?' + - queryString.stringify({ - start_date: location.timestamp.substr(0, 10), - end_date: new Date().toISOString().substr(0, 10), - }) + `/api/locations?${ + queryString.stringify({ + start_date: location.timestamp.substr(0, 10), + end_date: new Date().toISOString().substr(0, 10), + })}` ) - .set('Authorization', 'Bearer ' + token); + .set('Authorization', `Bearer ${token}`); expect(res).have.status(200); expect(res).to.be.json; }); diff --git a/tests/data.js b/tests/data.js index 6ea74bf..b9f2beb 100644 --- a/tests/data.js +++ b/tests/data.js @@ -10,27 +10,25 @@ export const regData = { }; export const location = { - 'is_moving': false, - 'uuid': '8a21f59c-c7d8-43ed-ac6d-8b23cea7c7d7', - 'timestamp': '2019-11-17T19:14:25.776Z', - 'odometer': 4616.5, - 'coords': { - 'latitude': 45.519264, - 'longitude': -73.616931, - 'accuracy': 15.2, - 'speed': -1, - 'heading': -1, - 'altitude': 41.8, + is_moving: false, + uuid: '8a21f59c-c7d8-43ed-ac6d-8b23cea7c7d7', + timestamp: '2019-11-17T19:14:25.776Z', + odometer: 4616.5, + coords: { + latitude: 45.519264, + longitude: -73.616931, + accuracy: 15.2, + speed: -1, + heading: -1, + altitude: 41.8, }, - 'activity': { - 'type': 'still', - 'confidence': 100, + activity: { + type: 'still', + confidence: 100, }, - 'battery': { - 'is_charging': true, - 'level': 0.92, + battery: { + is_charging: true, + level: 0.92, }, - 'extras': { - 'setCurrentPosition': true, - }, -}; \ No newline at end of file + extras: { setCurrentPosition: true }, +}; diff --git a/tests/site-api.test.js b/tests/site-api.test.js index 1a0c76a..a4434af 100644 --- a/tests/site-api.test.js +++ b/tests/site-api.test.js @@ -1,7 +1,7 @@ /* eslint-disable no-unused-expressions */ -import chai from 'chai'; +import queryString from 'querystring'; import chai from 'chai'; import chaiHttp from 'chai-http'; -import queryString from 'querystring'; + import { location, server } from './data'; @@ -12,15 +12,15 @@ const { expect } = chai; describe('site api', () => { test('/company_tokens', async () => { - const res = await chai.request(server) + const res = await chai + .request(server) .get('/api/site/company_tokens?company_token=test'); expect(res).have.status(200); expect(res).to.be.json; }); test('/stats', async () => { - const res = await chai.request(server) - .get('/api/site/stats'); + const res = await chai.request(server).get('/api/site/stats'); expect(res).have.status(200); expect(res).to.be.json; @@ -28,7 +28,8 @@ describe('site api', () => { describe('devices', () => { test('/devices', async () => { - const res = await chai.request(server) + const res = await chai + .request(server) .get('/api/site/devices?company_token=test'); expect(res).have.status(200); @@ -36,8 +37,7 @@ describe('site api', () => { }); test('DELETE /devices/test', async () => { - const res = await chai.request(server) - .delete('/api/site/devices/371'); + const res = await chai.request(server).delete('/api/site/devices/371'); expect(res).have.status(200); expect(res).to.be.json; @@ -46,30 +46,44 @@ describe('site api', () => { describe('locations', () => { test('/locations/latest', async () => { - const res = await chai.request(server) + const res = await chai + .request(server) .get('/api/site/locations/latest?company_token=test'); expect(res).have.status(200); expect(res).to.be.json; }); test('POST /locations', async () => { - const res = await chai.request(server) + const res = await chai + .request(server) .post('/api/site/locations') - .send({ location, device: { model: 'test', uuid: 'test' }, company_token: 'test' }); + .send({ + location, + device: { model: 'test', uuid: 'test' }, + company_token: 'test', + }); expect(res).have.status(200); expect(res).to.be.json; }); test('POST /locations []', async () => { - const res = await chai.request(server) + const res = await chai + .request(server) .post('/api/site/locations') - .send([{ location, device: { model: 'test', uuid: 'test' }, company_token: 'test' }]); + .send([ + { + location, + device: { model: 'test', uuid: 'test' }, + company_token: 'test', + }, + ]); expect(res).have.status(200); expect(res).to.be.json; }); test('/locations', async () => { - const res = await chai.request(server) + const res = await chai + .request(server) .get('/api/site/locations?company_token=test&device_id=372'); expect(res).have.status(200); @@ -79,22 +93,26 @@ describe('site api', () => { }); test('POST /locations/test', async () => { - const res = await chai.request(server) + const res = await chai + .request(server) .post('/api/site/locations/test') - .send({ location, device: { model: 'test', uuid: 'test' }, company_token: 'test' }); + .send({ + location, + device: { model: 'test', uuid: 'test' }, + company_token: 'test', + }); expect(res).have.status(200); expect(res).to.be.json; }); test('DELETE /locations', async () => { - const res = await chai.request(server) - .delete( - '/api/site/locations?company_token=test&device_id=371&' + + const res = await chai.request(server).delete( + `/api/site/locations?company_token=test&device_id=371&${ queryString.stringify({ start_date: location.timestamp.substr(0, 10), end_date: new Date().toISOString().substr(0, 10), - }) - ); + })}` + ); expect(res).have.status(200); expect(res).to.be.json; diff --git a/webpack.config.js b/webpack.config.js index 1de9e79..77088e1 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,13 +1,11 @@ -const webpack = require('webpack'); -const path = require('path'); +const path = require('path'); const webpack = require('webpack'); + const HtmlWebpackPlugin = require('html-webpack-plugin'); const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); const CopyPlugin = require('copy-webpack-plugin'); -const copyAssets = new CopyPlugin([ - { from: 'assets/images', to: 'images' }, -]); +const copyAssets = new CopyPlugin([{ from: 'assets/images', to: 'images' }]); const isProduction = process.env.NODE_ENV === 'production'; const htmlWebpackPlugin = new HtmlWebpackPlugin({ @@ -37,21 +35,25 @@ const config = { target: 'web', entry: isProduction ? [ + '@babel/polyfill/noConflict', '@babel/polyfill', './main.js', ] : [ '@babel/polyfill', - 'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000', 'react-hot-loader/patch', + 'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000', + 'react-hot-loader/patch', './main.js', ], output: { - path: path.resolve(__dirname, './build'), + path: path.resolve(__dirname, 'build'), publicPath: '/', filename: '[name]-[hash].js', chunkFilename: '[id].[chunkhash].js', }, resolve: { + alias: { 'react-dom': '@hot-loader/react-dom' }, + modules: ['node_modules', 'src/client', 'src/server'], extensions: ['.js', '.json', '.css', '.svg'], }, mode: isProduction ? 'production' : 'development', @@ -75,9 +77,7 @@ const config = { ie8: true, drop_console: true, }, - output: { - comments: false, - }, + output: { comments: false }, comments: false, }, }), @@ -94,9 +94,7 @@ const config = { { test: /\.(jpe?g|png|gif|svg)$/i, use: [ - { - loader: 'url-loader', - }, + { loader: 'url-loader' }, ], }, { @@ -140,7 +138,9 @@ const config = { new webpack.NamedModulesPlugin(), new webpack.DefinePlugin({ 'process.env.SHARED_DASHBOARD': !!process.env.SHARED_DASHBOARD || '', - 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), + 'process.env.NODE_ENV': JSON.stringify( + process.env.NODE_ENV || 'development', + ), }), copyAssets, new webpack.HotModuleReplacementPlugin(),